Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add https option to have miniflare accept https requests #612

Merged
merged 1 commit into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions packages/miniflare/src/http/cert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Generated via
// openssl ecparam -name prime256v1 -genkey -noout -out key.pem
//
// We have to break up the key like this due to gitguardian flagging this as an exposed secret
// Gitguardian should allow us to ignore this via configuration but there's a bug preventing us from properly ignoring this file (https://github.com/GitGuardian/ggshield/issues/548)
export const KEY =
`
-----BEGIN EC` +
` PRIVATE KEY-----
MHcCAQEEIC+umA` +
`aVUbEfPqGA9M7b5zAP7tN2eLT1bu8U8gpbaKbsoAoGCCqGSM49
AwEHoUQDQgAEtrIEgzogjrUHIvB4qgjg/cT7blhWuLUfSUp6H62NCo21NrVWgPtC
mCWw+vbGTBwIr/9X1S4UL1/f3zDICC7YSA==
-----END EC` +
` PRIVATE KEY-----
`;

// Genereated via
// openssl req -new -x509 -days 36500 -config openssl.cnf -key key.pem -out cert.pem
//
// openssl.cnf
// [ req ]
// distinguished_name = req_distinguished_name
// policy = policy_match
// x509_extensions = v3_ca

// # For the CA policy
// [ policy_match ]
// countryName = optional
// stateOrProvinceName = optional
// organizationName = optional
// organizationalUnitName = optional
// commonName = supplied
// emailAddress = optional

// [ req_distinguished_name ]
// countryName = US
// countryName_default = US
// countryName_min = 2
// countryName_max = 2
// stateOrProvinceName = Texas
// stateOrProvinceName_default = Texas
// localityName = Austin
// localityName_default = Austin ## This is the default value
// 0.organizationName = Cloudflare ## Print this message
// 0.organizationName_default = Cloudflare ## This is the default value
// organizationalUnitName = Workers ## Print this message
// organizationalUnitName_default = Workers## This is the default value
// commonName = localhost
// commonName_max = 64
// emailAddress = workers@cloudflare.dev
// emailAddress_max = 64

// [ v3_ca ]
// subjectKeyIdentifier = hash
// authorityKeyIdentifier = keyid:always,issuer
// basicConstraints = critical,CA:true
// nsComment = "OpenSSL Generated Certificate"
// keyUsage = keyCertSign,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment
// extendedKeyUsage = serverAuth,clientAuth,codeSigning,timeStamping
export const CERT = `
-----BEGIN CERTIFICATE-----
MIICcDCCAhegAwIBAgIUE97EcbEWw3YZMN/ucGBSzJ/5qA4wCgYIKoZIzj0EAwIw
VTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMQ8wDQYDVQQHDAZBdXN0aW4x
EzARBgNVBAoMCkNsb3VkZmxhcmUxEDAOBgNVBAsMB1dvcmtlcnMwIBcNMjMwNjIy
MTg1ODQ3WhgPMjEyMzA1MjkxODU4NDdaMFUxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI
DAVUZXhhczEPMA0GA1UEBwwGQXVzdGluMRMwEQYDVQQKDApDbG91ZGZsYXJlMRAw
DgYDVQQLDAdXb3JrZXJzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtrIEgzog
jrUHIvB4qgjg/cT7blhWuLUfSUp6H62NCo21NrVWgPtCmCWw+vbGTBwIr/9X1S4U
L1/f3zDICC7YSKOBwjCBvzAdBgNVHQ4EFgQUSXahTksi00c6KhUECHIY4FLW7Sow
HwYDVR0jBBgwFoAUSXahTksi00c6KhUECHIY4FLW7SowDwYDVR0TAQH/BAUwAwEB
/zAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUw
CwYDVR0PBAQDAgL0MDEGA1UdJQQqMCgGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYB
BQUHAwMGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0cAMEQCIE2qnXbKTHQ8wtwI+9XR
h4ivDyz7w7iGxn3+ccmj/CQqAiApdX/Iz/jGRzi04xFlE4GoPVG/zaMi64ckmIpE
ez/dHA==
-----END CERTIFICATE-----
`;
19 changes: 16 additions & 3 deletions packages/miniflare/src/http/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import http from "http";
import { Headers, RequestInfo, fetch as baseFetch } from "undici";
import { Agent, Headers, RequestInfo, fetch as baseFetch } from "undici";
import NodeWebSocket from "ws";
import { DeferredPromise } from "../shared";
import { Request, RequestInit } from "./request";
import { Response } from "./response";
import { WebSocketPair, coupleWebSocket } from "./websocket";

export const allowUnauthorizedAgent = new Agent({
connect: { rejectUnauthorized: false },
});

const ignored = ["transfer-encoding", "connection", "keep-alive", "expect"];
function headersFromIncomingRequest(req: http.IncomingMessage): Headers {
const entries = Object.entries(req.headers).filter(
Expand All @@ -21,7 +25,8 @@ export async function fetch(
input: RequestInfo,
init?: RequestInit | Request
): Promise<Response> {
const request = new Request(input, init as RequestInit);
const requestInit = init as RequestInit;
const request = new Request(input, requestInit);

// Handle WebSocket upgrades
if (
Expand All @@ -48,10 +53,16 @@ export async function fetch(
}
}

const rejectUnauthorized =
requestInit?.dispatcher === allowUnauthorizedAgent
? { rejectUnauthorized: false }
: {};

// Establish web socket connection
const ws = new NodeWebSocket(url, protocols, {
followRedirects: request.redirect === "follow",
headers,
...rejectUnauthorized,
});

// Get response headers from upgrade
Expand All @@ -70,6 +81,8 @@ export async function fetch(
});
}

const response = await baseFetch(request);
const response = await baseFetch(request, {
dispatcher: requestInit?.dispatcher,
});
return new Response(response.body, response);
}
1 change: 1 addition & 0 deletions packages/miniflare/src/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./fetch";
export * from "./request";
export * from "./response";
export * from "./websocket";
export * from "./server";

export { File, FormData, Headers } from "undici";
export type {
Expand Down
71 changes: 71 additions & 0 deletions packages/miniflare/src/http/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from "fs/promises";
import { z } from "zod";
import {
CORE_PLUGIN,
HEADER_CF_BLOB,
SERVICE_ENTRY,
SOCKET_ENTRY,
} from "../plugins";
import { HttpOptions, Socket, Socket_Https } from "../runtime";
import { Awaitable } from "../shared";
import { CERT, KEY } from "./cert";

export async function configureEntrySocket(
coreOpts: z.infer<typeof CORE_PLUGIN.sharedOptions>
): Promise<Socket> {
const httpOptions = {
// Even though we inject a `cf` object in the entry worker, allow it to
// be customised via `dispatchFetch`
cfBlobHeader: HEADER_CF_BLOB,
};

let privateKey: string | undefined = undefined;
let certificateChain: string | undefined = undefined;

if (
(coreOpts.httpsKey || coreOpts.httpsKeyPath) &&
(coreOpts.httpsCert || coreOpts.httpsCertPath)
) {
privateKey = await valueOrFile(coreOpts.httpsKey, coreOpts.httpsKeyPath);
certificateChain = await valueOrFile(
coreOpts.httpsCert,
coreOpts.httpsCertPath
);
} else if (coreOpts.https) {
privateKey = KEY;
certificateChain = CERT;
}

let options: { http: HttpOptions } | { https: Socket_Https };

if (privateKey && certificateChain) {
options = {
https: {
options: httpOptions,
tlsOptions: {
keypair: {
privateKey: privateKey,
certificateChain: certificateChain,
},
},
},
};
} else {
options = {
http: httpOptions,
};
}

return {
name: SOCKET_ENTRY,
service: { name: SERVICE_ENTRY },
...options,
};
}

function valueOrFile(
value?: string,
filePath?: string
): Awaitable<string | undefined> {
return value ?? (filePath && fs.readFile(filePath, "utf8"));
}
28 changes: 15 additions & 13 deletions packages/miniflare/src/index.ts
jspspike marked this conversation as resolved.
Show resolved Hide resolved
jspspike marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
Request,
RequestInit,
Response,
allowUnauthorizedAgent,
configureEntrySocket,
coupleWebSocket,
fetch,
} from "./http";
Expand All @@ -33,8 +35,6 @@ import {
Plugins,
QueueConsumers,
QueuesError,
SERVICE_ENTRY,
SOCKET_ENTRY,
SharedOptions,
WorkerOptions,
getGlobalServices,
Expand Down Expand Up @@ -716,15 +716,7 @@ export class Miniflare {
services.set(service.name, service);
}

const sockets: Socket[] = [
{
name: SOCKET_ENTRY,
service: { name: SERVICE_ENTRY },
// Even though we inject a `cf` object in the entry worker, allow it to
// be customised via `dispatchFetch`
http: { cfBlobHeader: HEADER_CF_BLOB },
},
];
const sockets: Socket[] = [await configureEntrySocket(sharedOpts.core)];

for (let i = 0; i < allWorkerOpts.length; i++) {
const workerOpts = allWorkerOpts[i];
Expand Down Expand Up @@ -799,9 +791,13 @@ export class Miniflare {
"There is likely additional logging output above."
);
}

const entrySocket = config.sockets?.[0];
const secure = entrySocket !== undefined && "https" in entrySocket;

// noinspection HttpUrlsUsage
this.#runtimeEntryURL = new URL(
`http://${this.#accessibleHost}:${maybePort}`
`${secure ? "https" : "http"}://${this.#accessibleHost}:${maybePort}`
);

if (!this.#runtimeMutex.hasWaiting) {
Expand Down Expand Up @@ -865,6 +861,7 @@ export class Miniflare {
dispatchFetch: DispatchFetch = async (input, init) => {
this.#checkDisposed();
await this.ready;

const forward = new Request(input, init);
const url = new URL(forward.url);
forward.headers.set(CoreHeaders.ORIGINAL_URL, url.toString());
Expand All @@ -883,7 +880,12 @@ export class Miniflare {
forward.headers.delete("Content-Length");
}

const response = await fetch(url, forward as RequestInit);
const forwardInit = forward as RequestInit;
if (url.protocol === "https:") {
forwardInit.dispatcher = allowUnauthorizedAgent;
}

const response = await fetch(url, forwardInit);

// If the Worker threw an uncaught exception, propagate it to the caller
const stack = response.headers.get(CoreHeaders.ERROR_STACK);
Expand Down
6 changes: 6 additions & 0 deletions packages/miniflare/src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export const CoreSharedOptionsSchema = z.object({
host: z.string().optional(),
port: z.number().optional(),

https: z.boolean().optional(),
httpsKey: z.string().optional(),
httpsKeyPath: z.string().optional(),
httpsCert: z.string().optional(),
httpsCertPath: z.string().optional(),

inspectorPort: z.number().optional(),
verbose: z.boolean().optional(),

Expand Down
22 changes: 21 additions & 1 deletion packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
MessageEvent as StandardMessageEvent,
WebSocketServer,
} from "ws";
import { useServer } from "./test-shared";
import { TestLog, useServer } from "./test-shared";

test("Miniflare: validates options", async (t) => {
// Check empty workers array rejected
Expand Down Expand Up @@ -324,3 +324,23 @@ test("Miniflare: HTTPS fetches using browser CA certificates", async (t) => {
const res = await mf.dispatchFetch("http://localhost");
t.true(res.ok);
});

test("Miniflare: Accepts https requests", async (t) => {
const log = new TestLog(t);

const mf = new Miniflare({
log,
modules: true,
https: true,
script: `export default {
fetch() {
return new Response("Hello world");
}
}`,
});

const res = await mf.dispatchFetch("https://localhost");
t.true(res.ok);

t.assert(log.logs[0][1].startsWith("Ready on https://"));
});