1import * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
3import * as NodeFs from "node:fs";
4import * as NodeTls from "node:tls";
5import type * as Config from "../workerd/Config.ts";
6
7export class Internet extends Context.Service<Internet, Config.Service>()(
8 "cloudflare-runtime/Internet",
9) {}
10
11/**
12 * Extra trusted CAs from `NODE_EXTRA_CA_CERTS`, read FRESH on each call.
13 *
14 * This deliberately does NOT match Node's read-once-at-startup semantics:
15 * this layer is built once per long-lived sidecar process, but the file the
16 * env var points at may not exist yet when the sidecar starts — `alchemy
17 * dev` points it at the local emulator's self-signed CA
18 * (`~/.floci/ca.pem`), which is minted while the FIRST apply is already
19 * deploying workers in parallel. Reading at every workerd config build
20 * means the first worker (re)start after the CA lands trusts it, instead
21 * of the whole sidecar being stuck with an empty store for its lifetime.
22 */
23const extraTrustedCertificates = (): string[] => {
24 const bundlePath = process.env.NODE_EXTRA_CA_CERTS;
25 if (bundlePath === undefined) return [];
26 let pem: string;
27 try {
28 pem = NodeFs.readFileSync(bundlePath, "utf8");
29 } catch {
30 return [];
31 }
32 // Split the bundle into individual certificates and add each individually:
33 // https://github.com/cloudflare/miniflare/pull/587/files#r1271579671
34 return (
35 pem.match(
36 /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g,
37 ) ?? []
38 );
39};
40
41export const InternetLive = Layer.sync(Internet, () => {
42 // `workerd`'s `trustBrowserCas` should probably be named `trustSystemCas`.
43 // Rather than using a bundled CA store like Node, it uses
44 // `SSL_CTX_set_default_verify_paths()` to use the system CA store:
45 // https://github.com/capnproto/capnproto/blob/6e26d260d1d91e0465ca12bbb5230a1dfa28f00d/c%2B%2B/src/kj/compat/tls.c%2B%2B#L745
46 // Unfortunately, this doesn't work on Windows. Luckily, Node exposes its own
47 // bundled CA store's certificates, so we just use those.
48 const baseCertificates =
49 process.platform === "win32" ? Array.from(NodeTls.rootCertificates) : [];
50 return {
51 name: "internet",
52 get network() {
53 return {
54 // Allow access to private/public addresses:
55 // https://github.com/cloudflare/miniflare/issues/412
56 allow: ["public", "private", "240.0.0.0/4"],
57 deny: [],
58 tlsOptions: {
59 trustBrowserCas: true,
60 trustedCertificates: [
61 ...baseCertificates,
62 ...extraTrustedCertificates(),
63 ],
64 },
65 };
66 },
67 };
68});