Serve plain HTTP only and drop the local certificate tooling
The app never terminates TLS: a reverse proxy or tunnel does, and isSecureRequest still reads X-Forwarded-Proto so cookies stay Secure. Removes the cert generator, TLS resolution and HTTP redirect, the local CA download, and the TLS envs, and trims the installer, Dockerfile health check and docs to match. Also softens the composer's loading row.
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import buildCaProfile from "./build-ca-profile";
|
||||
|
||||
const pem = `-----BEGIN CERTIFICATE-----
|
||||
MIIBhTCCASugAwIBAgIUTEST
|
||||
-----END CERTIFICATE-----
|
||||
`;
|
||||
|
||||
const profile = (display_name = "Cobble local CA") =>
|
||||
buildCaProfile({
|
||||
certificate_pem: pem,
|
||||
display_name,
|
||||
identifier: "org.example.cobble.ca",
|
||||
});
|
||||
|
||||
describe("buildCaProfile", () => {
|
||||
test("is a root certificate profile", () => {
|
||||
expect(profile()).toContain("<string>com.apple.security.root</string>");
|
||||
expect(profile()).toContain("<string>Configuration</string>");
|
||||
});
|
||||
|
||||
test("embeds the PEM body as DER base64", () => {
|
||||
const body = profile();
|
||||
|
||||
expect(body).toContain("MIIBhTCCASugAwIBAgIUTEST");
|
||||
expect(body).not.toContain("BEGIN CERTIFICATE");
|
||||
expect(body).not.toContain("END CERTIFICATE");
|
||||
});
|
||||
|
||||
test("names the profile with its identifier", () => {
|
||||
const body = profile();
|
||||
|
||||
expect(body).toContain("<string>Cobble local CA</string>");
|
||||
expect(body).toContain("<string>org.example.cobble.ca</string>");
|
||||
expect(body).toContain(
|
||||
"<string>org.example.cobble.ca.certificate</string>",
|
||||
);
|
||||
});
|
||||
|
||||
test("escapes XML in the name", () => {
|
||||
expect(profile("Ben & Co <CA>")).toContain("Ben & Co <CA>");
|
||||
});
|
||||
|
||||
test("refuses an empty certificate", () => {
|
||||
expect(() =>
|
||||
buildCaProfile({
|
||||
certificate_pem: " ",
|
||||
display_name: "Cobble",
|
||||
identifier: "org.example.cobble.ca",
|
||||
}),
|
||||
).toThrow("The CA certificate is empty.");
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
type Params = {
|
||||
/**
|
||||
* The CA certificate in PEM, as `ca.crt` holds it.
|
||||
*/
|
||||
certificate_pem: string;
|
||||
/**
|
||||
* The name iOS and macOS show for the profile.
|
||||
*/
|
||||
display_name: string;
|
||||
/**
|
||||
* Reverse-DNS identifier of the profile, e.g. `org.example.cobble.ca`.
|
||||
*/
|
||||
identifier: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fixed UUIDs, so installing the profile again replaces it on the device
|
||||
* instead of leaving an older Cobble CA behind.
|
||||
*/
|
||||
const ProfileUUID = "B7F3C2A1-6D4E-4F8A-9C1B-2E5A7D9F0B31";
|
||||
const CertificateUUID = "D2A8E4C6-1B3F-4A7D-8E5C-9F0B6A2C4D18";
|
||||
|
||||
const XmlEscapes: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
|
||||
function escapeXml({ value }: { value: string }): string {
|
||||
return value.replace(/[&<>"']/g, (character) => XmlEscapes[character]!);
|
||||
}
|
||||
|
||||
/**
|
||||
* The certificate as base64 DER, which is what a plist `<data>` holds:
|
||||
* the PEM body itself, with its markers and line breaks removed.
|
||||
*/
|
||||
function certificateData({ certificate_pem }: { certificate_pem: string }) {
|
||||
const data = certificate_pem
|
||||
.replace(/-----BEGIN CERTIFICATE-----/g, "")
|
||||
.replace(/-----END CERTIFICATE-----/g, "")
|
||||
.replace(/\s+/g, "");
|
||||
|
||||
if (!data) {
|
||||
throw new Error("The CA certificate is empty.");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* An iOS/macOS configuration profile carrying the local CA
|
||||
* (`com.apple.security.root`). Opening it on an iPhone offers a one-tap
|
||||
* install instead of a file to find and install by hand; iOS still asks
|
||||
* for the separate trust switch afterwards, which no profile can skip on
|
||||
* an unmanaged device.
|
||||
*/
|
||||
export default function buildCaProfile({
|
||||
certificate_pem,
|
||||
display_name,
|
||||
identifier,
|
||||
}: Params): string {
|
||||
const name = escapeXml({ value: display_name });
|
||||
const id = escapeXml({ value: identifier });
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadCertificateFileName</key>
|
||||
<string>${name}.crt</string>
|
||||
<key>PayloadContent</key>
|
||||
<data>
|
||||
${certificateData({ certificate_pem })}
|
||||
</data>
|
||||
<key>PayloadDescription</key>
|
||||
<string>Lets this device trust the HTTPS certificate ${name} serves.</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>${name}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>${id}.certificate</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.security.root</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>${CertificateUUID}</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDescription</key>
|
||||
<string>Installs the local certificate authority that signs ${name}, then turn on trust under Certificate Trust Settings.</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>${name}</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>${id}</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
<false/>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>${ProfileUUID}</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import buildHttpsRedirectUrl from "./build-https-redirect-url";
|
||||
|
||||
const redirect = (url: string, https_port: string | number = 7227) =>
|
||||
buildHttpsRedirectUrl({ request_url: new URL(url), https_port });
|
||||
|
||||
describe("buildHttpsRedirectUrl", () => {
|
||||
test("moves the request to the HTTPS port", () => {
|
||||
expect(redirect("http://localhost:7228/")).toBe(
|
||||
"https://localhost:7227/",
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps the path and the query", () => {
|
||||
expect(redirect("http://localhost:7228/admin/bots?a=1&b=2")).toBe(
|
||||
"https://localhost:7227/admin/bots?a=1&b=2",
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps an IPv6 literal valid", () => {
|
||||
expect(redirect("http://[::1]:7228/login")).toBe(
|
||||
"https://[::1]:7227/login",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the host the browser asked for", () => {
|
||||
expect(redirect("http://cobble.tailnet.ts.net:7228/")).toBe(
|
||||
"https://cobble.tailnet.ts.net:7227/",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
type Params = {
|
||||
/**
|
||||
* The plain-HTTP request that should move to HTTPS.
|
||||
*/
|
||||
request_url: URL;
|
||||
/**
|
||||
* The port the app serves HTTPS on.
|
||||
*/
|
||||
https_port: string | number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The HTTPS address for a plain-HTTP request: same host, path and query,
|
||||
* on the HTTPS port. `request_url.hostname` keeps the brackets of an IPv6
|
||||
* literal, so the result stays a valid URL.
|
||||
*/
|
||||
export default function buildHttpsRedirectUrl({
|
||||
request_url,
|
||||
https_port,
|
||||
}: Params): string {
|
||||
return `https://${request_url.hostname}:${https_port}${request_url.pathname}${request_url.search}`;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import buildSanList from "./build-san-list";
|
||||
|
||||
describe("buildSanList", () => {
|
||||
test("always covers localhost", () => {
|
||||
expect(buildSanList({})).toEqual([
|
||||
"DNS:localhost",
|
||||
"IP:127.0.0.1",
|
||||
"IP:::1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("adds the machine's name, its short form and its mDNS name", () => {
|
||||
const list = buildSanList({ hostname: "studio.tailnet.ts.net" });
|
||||
|
||||
expect(list).toContain("DNS:studio.tailnet.ts.net");
|
||||
expect(list).toContain("DNS:studio");
|
||||
expect(list).toContain("DNS:studio.local");
|
||||
});
|
||||
|
||||
test("does not invent localhost.local", () => {
|
||||
expect(buildSanList({ hostname: "localhost" })).toEqual([
|
||||
"DNS:localhost",
|
||||
"IP:127.0.0.1",
|
||||
"IP:::1",
|
||||
]);
|
||||
});
|
||||
|
||||
test("classifies addresses and names", () => {
|
||||
const list = buildSanList({
|
||||
addresses: ["192.168.1.20", "fe80::1"],
|
||||
hosts: ["cobble.tailnet.ts.net", "10.8.0.1"],
|
||||
});
|
||||
|
||||
expect(list).toContain("IP:192.168.1.20");
|
||||
expect(list).toContain("IP:fe80::1");
|
||||
expect(list).toContain("DNS:cobble.tailnet.ts.net");
|
||||
expect(list).toContain("IP:10.8.0.1");
|
||||
expect(list).not.toContain("DNS:10.8.0.1");
|
||||
});
|
||||
|
||||
test("deduplicates names regardless of case", () => {
|
||||
const list = buildSanList({
|
||||
hostname: "Studio",
|
||||
hosts: ["studio", "STUDIO.local"],
|
||||
});
|
||||
|
||||
expect(list.filter((entry) => entry == "DNS:studio")).toHaveLength(1);
|
||||
expect(
|
||||
list.filter((entry) => entry == "DNS:studio.local"),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("ignores empty entries", () => {
|
||||
expect(buildSanList({ hosts: ["", " "] })).toEqual([
|
||||
"DNS:localhost",
|
||||
"IP:127.0.0.1",
|
||||
"IP:::1",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import net from "net";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The machine's own name, as `os.hostname()` gives it. Its short form
|
||||
* and `<short>.local` are added too, since that is how a LAN reaches
|
||||
* it.
|
||||
*/
|
||||
hostname?: string;
|
||||
/**
|
||||
* Addresses the app answers on.
|
||||
*/
|
||||
addresses?: string[];
|
||||
/**
|
||||
* Names or addresses given by the user, e.g. a VPN's DNS name.
|
||||
*/
|
||||
hosts?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The `subjectAltName` entries a certificate is issued for: `DNS:name` for
|
||||
* a name, `IP:address` for an address, deduplicated. `localhost` and both
|
||||
* loopback addresses are always there, so `https://localhost` (and a
|
||||
* forwarded port) keep working.
|
||||
*
|
||||
* A name is classified by `net.isIP`, so a user can pass an address
|
||||
* wherever a host is expected without it becoming a useless DNS entry.
|
||||
*/
|
||||
export default function buildSanList({
|
||||
hostname,
|
||||
addresses,
|
||||
hosts,
|
||||
}: Params): string[] {
|
||||
const entries: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const add = ({ type, value }: { type: "DNS" | "IP"; value: string }) => {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) return;
|
||||
|
||||
const entry = `${type}:${
|
||||
type == "DNS" ? trimmed.toLowerCase() : trimmed
|
||||
}`;
|
||||
const key = entry.toLowerCase();
|
||||
|
||||
if (seen.has(key)) return;
|
||||
|
||||
seen.add(key);
|
||||
entries.push(entry);
|
||||
};
|
||||
|
||||
const addHost = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) return;
|
||||
|
||||
add({ type: net.isIP(trimmed) ? "IP" : "DNS", value: trimmed });
|
||||
};
|
||||
|
||||
add({ type: "DNS", value: "localhost" });
|
||||
add({ type: "IP", value: "127.0.0.1" });
|
||||
add({ type: "IP", value: "::1" });
|
||||
|
||||
const own = (hostname ?? "").trim();
|
||||
|
||||
if (own) {
|
||||
addHost(own);
|
||||
|
||||
const short = own.split(".")[0];
|
||||
|
||||
if (short && short != "localhost") {
|
||||
add({ type: "DNS", value: short });
|
||||
|
||||
if (short != own) add({ type: "DNS", value: `${short}.local` });
|
||||
}
|
||||
}
|
||||
|
||||
for (const address of addresses ?? []) addHost(address);
|
||||
for (const host of hosts ?? []) addHost(host);
|
||||
|
||||
return entries;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import pickTlsFiles from "./pick-tls-files";
|
||||
|
||||
const fallback = {
|
||||
fallback_cert_file: "certs/server.crt",
|
||||
fallback_key_file: "certs/server.key",
|
||||
fallback_ca_file: "certs/ca.crt",
|
||||
};
|
||||
|
||||
const exists = (paths: string[]) => (path: string) => paths.includes(path);
|
||||
|
||||
const pick = (
|
||||
options: Partial<Parameters<typeof pickTlsFiles>[0]> = {},
|
||||
paths: string[] = ["certs/server.crt", "certs/server.key", "certs/ca.crt"],
|
||||
) =>
|
||||
pickTlsFiles({
|
||||
...fallback,
|
||||
file_exists: exists(paths),
|
||||
...options,
|
||||
});
|
||||
|
||||
describe("pickTlsFiles", () => {
|
||||
test("prefers the TLS variables", () => {
|
||||
const picked = pick({
|
||||
cert_file: "/etc/cert.pem",
|
||||
key_file: "/etc/key.pem",
|
||||
ca_file: "/etc/ca.pem",
|
||||
});
|
||||
|
||||
expect(picked).toEqual({
|
||||
cert_file: "/etc/cert.pem",
|
||||
key_file: "/etc/key.pem",
|
||||
ca_file: "/etc/ca.pem",
|
||||
source: "explicit",
|
||||
});
|
||||
});
|
||||
|
||||
test("uses certs/ when the variables are unset, in any environment", () => {
|
||||
const picked = pick({ cert_file: "", key_file: "" });
|
||||
|
||||
expect(picked).toEqual({
|
||||
cert_file: "certs/server.crt",
|
||||
key_file: "certs/server.key",
|
||||
ca_file: "certs/ca.crt",
|
||||
source: "fallback",
|
||||
});
|
||||
});
|
||||
|
||||
test("serves HTTP when the certificate is explicitly disabled", () => {
|
||||
expect(
|
||||
pick({
|
||||
cert_file: "/etc/cert.pem",
|
||||
key_file: "/etc/key.pem",
|
||||
disabled: "1",
|
||||
}),
|
||||
).toEqual({ source: "disabled" });
|
||||
expect(pick({ disabled: "true" })).toEqual({ source: "disabled" });
|
||||
expect(pick({ disabled: "no" }).source).toBe("fallback");
|
||||
});
|
||||
|
||||
test("serves HTTP when there is nothing to serve with", () => {
|
||||
expect(pick({}, []).source).toBe("none");
|
||||
expect(pick({}, ["certs/server.crt"]).source).toBe("none");
|
||||
});
|
||||
|
||||
test("keeps half a configuration for the resolver to reject", () => {
|
||||
expect(pick({ cert_file: "/etc/cert.pem" }, []).source).toBe("explicit");
|
||||
});
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
type Params = {
|
||||
/**
|
||||
* `TLS_CERT_FILE`, when the environment names one.
|
||||
*/
|
||||
cert_file?: string;
|
||||
/**
|
||||
* `TLS_KEY_FILE`, when the environment names one.
|
||||
*/
|
||||
key_file?: string;
|
||||
/**
|
||||
* `TLS_CA_FILE`, served for devices to trust.
|
||||
*/
|
||||
ca_file?: string;
|
||||
/**
|
||||
* `TLS_DISABLED`, the explicit opt-out.
|
||||
*/
|
||||
disabled?: string;
|
||||
/**
|
||||
* Where a certificate is looked for when the environment names none:
|
||||
* `certs/` at the project root, which is also where the container's
|
||||
* `/app/certs` mount lands.
|
||||
*/
|
||||
fallback_cert_file: string;
|
||||
fallback_key_file: string;
|
||||
fallback_ca_file: string;
|
||||
file_exists: (path: string) => boolean;
|
||||
};
|
||||
|
||||
export type TlsFiles = {
|
||||
cert_file?: string;
|
||||
key_file?: string;
|
||||
ca_file?: string;
|
||||
/**
|
||||
* Where the choice came from, for the startup log: the environment,
|
||||
* the `certs/` fallback, an explicit disable, or nothing at all.
|
||||
*/
|
||||
source: "explicit" | "fallback" | "disabled" | "none";
|
||||
};
|
||||
|
||||
const DisabledValues = new Set(["1", "true", "yes", "on"]);
|
||||
|
||||
/**
|
||||
* Which certificate files to serve with, in every environment: the
|
||||
* `TLS_*` variables when they are set, else `certs/` when it exists, and
|
||||
* plain HTTP only when `TLS_DISABLED` says so or there is nothing to serve
|
||||
* with.
|
||||
*
|
||||
* Nothing here is tied to `NODE_ENV`: a production process that has a
|
||||
* certificate serves HTTPS, and the only way to turn it off is to say so.
|
||||
*/
|
||||
export default function pickTlsFiles({
|
||||
cert_file,
|
||||
key_file,
|
||||
ca_file,
|
||||
disabled,
|
||||
fallback_cert_file,
|
||||
fallback_key_file,
|
||||
fallback_ca_file,
|
||||
file_exists,
|
||||
}: Params): TlsFiles {
|
||||
if (disabled && DisabledValues.has(disabled.trim().toLowerCase())) {
|
||||
return { source: "disabled" };
|
||||
}
|
||||
|
||||
const cert = cert_file?.trim();
|
||||
const key = key_file?.trim();
|
||||
|
||||
if (cert || key) {
|
||||
return { cert_file: cert, key_file: key, ca_file: ca_file?.trim(), source: "explicit" };
|
||||
}
|
||||
|
||||
if (file_exists(fallback_cert_file) && file_exists(fallback_key_file)) {
|
||||
return {
|
||||
cert_file: fallback_cert_file,
|
||||
key_file: fallback_key_file,
|
||||
ca_file: ca_file?.trim() ||
|
||||
(file_exists(fallback_ca_file) ? fallback_ca_file : undefined),
|
||||
source: "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
return { source: "none" };
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { afterAll, describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import resolveTlsOptions from "./resolve-tls-options";
|
||||
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), "cobble-tls-"));
|
||||
const cert = path.join(dir, "server.crt");
|
||||
const key = path.join(dir, "server.key");
|
||||
|
||||
writeFileSync(cert, "certificate");
|
||||
writeFileSync(key, "key");
|
||||
|
||||
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
describe("resolveTlsOptions", () => {
|
||||
test("plain HTTP when neither is set", () => {
|
||||
expect(resolveTlsOptions({})).toBeUndefined();
|
||||
expect(
|
||||
resolveTlsOptions({ cert_file: "", key_file: " " }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("reads both files when they are set", async () => {
|
||||
const tls = resolveTlsOptions({ cert_file: cert, key_file: key });
|
||||
|
||||
expect(tls).toBeDefined();
|
||||
expect(await tls!.cert.text()).toBe("certificate");
|
||||
expect(await tls!.key.text()).toBe("key");
|
||||
});
|
||||
|
||||
test("refuses half a configuration", () => {
|
||||
expect(() => resolveTlsOptions({ cert_file: cert })).toThrow(
|
||||
"TLS_CERT_FILE and TLS_KEY_FILE must both be set, or neither.",
|
||||
);
|
||||
expect(() => resolveTlsOptions({ key_file: key })).toThrow(
|
||||
"TLS_CERT_FILE and TLS_KEY_FILE must both be set, or neither.",
|
||||
);
|
||||
});
|
||||
|
||||
test("refuses a missing file", () => {
|
||||
expect(() =>
|
||||
resolveTlsOptions({
|
||||
cert_file: path.join(dir, "missing.crt"),
|
||||
key_file: key,
|
||||
}),
|
||||
).toThrow("Can't read the TLS file");
|
||||
});
|
||||
|
||||
test("refuses a directory", () => {
|
||||
expect(() =>
|
||||
resolveTlsOptions({ cert_file: dir, key_file: key }),
|
||||
).toThrow("Can't read the TLS file");
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { accessSync, constants, statSync } from "fs";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* `TLS_CERT_FILE`, the certificate (or chain) the app serves.
|
||||
*/
|
||||
cert_file?: string;
|
||||
/**
|
||||
* `TLS_KEY_FILE`, its private key.
|
||||
*/
|
||||
key_file?: string;
|
||||
};
|
||||
|
||||
export type TlsOptions = {
|
||||
cert: ReturnType<typeof Bun.file>;
|
||||
key: ReturnType<typeof Bun.file>;
|
||||
};
|
||||
|
||||
/**
|
||||
* `Bun.serve`'s TLS options from the certificate paths the installer sets,
|
||||
* or `undefined` when the app runs over plain HTTP.
|
||||
*
|
||||
* A half-configured or unreadable certificate is an error, never a silent
|
||||
* fall back to HTTP: downgrading the connection quietly would drop the
|
||||
* `Secure` cookies and the secure context the installed app and Web Push
|
||||
* need.
|
||||
*/
|
||||
export default function resolveTlsOptions({
|
||||
cert_file,
|
||||
key_file,
|
||||
}: Params): TlsOptions | undefined {
|
||||
const cert = cert_file?.trim();
|
||||
const key = key_file?.trim();
|
||||
|
||||
if (!cert && !key) return undefined;
|
||||
|
||||
if (!cert || !key) {
|
||||
throw new Error(
|
||||
"TLS_CERT_FILE and TLS_KEY_FILE must both be set, or neither.",
|
||||
);
|
||||
}
|
||||
|
||||
for (const file of [cert, key]) {
|
||||
try {
|
||||
if (!statSync(file).isFile()) throw new Error("not a file");
|
||||
|
||||
accessSync(file, constants.R_OK);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Can't read the TLS file ${file}. Check that it is mounted and readable.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { cert: Bun.file(cert), key: Bun.file(key) };
|
||||
}
|
||||
Reference in New Issue
Block a user