mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Support SNICallback in tls.createServer (#1700)
This commit is contained in:
@@ -3,6 +3,7 @@ import type { IrType } from "../../ir/ir.js";
|
||||
import { typeKey } from "../../ir/ir.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import {
|
||||
type AbiArgument,
|
||||
abiValue,
|
||||
callbackAdapter,
|
||||
callbackType,
|
||||
@@ -84,6 +85,23 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** The (closure, answer thunk) pair of an SNICallback argument: a function,
|
||||
* or a function | undefined union whose undefined arm passes no callback. */
|
||||
export function sniCallbackArgs(host: LlvmEmitterContext, cb: LlValue): AbiArgument[] {
|
||||
let type = cb.type,
|
||||
value = cb.name;
|
||||
if (type.kind === "union") {
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const tag = arms.findIndex((arm) => arm.kind === "func");
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
} else host.toHeap(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined)
|
||||
throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
return [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))];
|
||||
}
|
||||
|
||||
export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)),
|
||||
a = args.map((arg) => abiValue(host, arg));
|
||||
@@ -160,21 +178,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
let handler = [ptr(), ptr()],
|
||||
sni = [ptr(), ptr()];
|
||||
if (e.fn === "http2.createSecureServerReq") handler = requestHandler(host, args[2]!);
|
||||
if (e.fn === "http2.createSecureServerSni") {
|
||||
const cb = args[2]!;
|
||||
let type = cb.type,
|
||||
value = cb.name;
|
||||
if (type.kind === "union") {
|
||||
const arms = host.unionsById.get(type.unionId)?.arms ?? [];
|
||||
const tag = arms.findIndex((arm) => arm.kind === "func");
|
||||
if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm");
|
||||
type = arms[tag]!;
|
||||
value = host.unwrapNullableClosure(value, tag);
|
||||
} else host.toHeap(cb);
|
||||
if (type.kind !== "func" || type.params[1] === undefined)
|
||||
throw new InternalCompilerError("SNI callback lacks its answer function");
|
||||
sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))];
|
||||
}
|
||||
if (e.fn === "http2.createSecureServerSni") sni = sniCallbackArgs(host, args[2]!);
|
||||
return finishRuntimeCall(host, e, "scr_http2_create_secure_server_allow_http1", [
|
||||
...rawBytes(host, args[0]!),
|
||||
...rawBytes(host, args[1]!),
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
rawBytes,
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
import { sniCallbackArgs } from "./lib-http2.js";
|
||||
import { emitAlwaysThrowLibCall } from "./lib-shared.js";
|
||||
|
||||
export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
@@ -43,6 +44,27 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
[...rawBytes(host, args[0]!), ...rawBytes(host, args[1]!), ...handler],
|
||||
);
|
||||
}
|
||||
case "tls.createServerSni":
|
||||
case "tls.createServerSniCb": {
|
||||
const cb = args[3];
|
||||
const handler =
|
||||
cb === undefined
|
||||
? [ptr(), ptr()]
|
||||
: callbackAdapter(
|
||||
host,
|
||||
cb,
|
||||
callbackType(cb).params.length === 0
|
||||
? "scr_net_conn_thunk0"
|
||||
: "scr_net_conn_thunk_sock",
|
||||
["ptr", "ptr"],
|
||||
);
|
||||
return finishRuntimeCall(host, e, "scr_tls_create_server_sni", [
|
||||
...rawBytes(host, args[0]!),
|
||||
...rawBytes(host, args[1]!),
|
||||
...handler,
|
||||
...sniCallbackArgs(host, args[2]!),
|
||||
]);
|
||||
}
|
||||
case "tls.createServerDyn":
|
||||
case "tls.createServerDynCb":
|
||||
case "https.createServerDyn":
|
||||
|
||||
@@ -4749,7 +4749,8 @@ function lowerTlsServerOptions(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
what: string,
|
||||
): { cert: IrExpr; key: IrExpr } {
|
||||
allowSni = false,
|
||||
): { cert: IrExpr; key: IrExpr; sni?: IrExpr } {
|
||||
if (!ts.isObjectLiteralExpression(node)) {
|
||||
lowerer.noLowering(
|
||||
`${what} with a non-literal options argument`,
|
||||
@@ -4760,6 +4761,8 @@ function lowerTlsServerOptions(
|
||||
const loc = locOf(node);
|
||||
let cert: IrExpr | null = null;
|
||||
let key: IrExpr | null = null;
|
||||
let sni: IrExpr | undefined;
|
||||
let pendingSni: IrStmt | null = null;
|
||||
for (const prop of node.properties.nodes) {
|
||||
let initializer: ts.Expression | null;
|
||||
if (
|
||||
@@ -4777,6 +4780,25 @@ function lowerTlsServerOptions(
|
||||
);
|
||||
}
|
||||
const k = (prop.name as ts.Identifier | ts.StringLiteral).text;
|
||||
if (k === "SNICallback" && allowSni) {
|
||||
const value =
|
||||
initializer !== null
|
||||
? lowerer.lowerExpr(initializer)
|
||||
: lowerer.lowerShorthandValue(prop as ts.ShorthandPropertyAssignment);
|
||||
const checked = checkSniCallbackValue(lowerer, value, prop, what);
|
||||
if (cert !== null && key !== null) {
|
||||
sni = checked;
|
||||
} else {
|
||||
// The runtime call reads cert and key first. A cert or key entry
|
||||
// after the callback can change the binding the callback reads,
|
||||
// so the callback is saved ahead of the next of them, in source
|
||||
// order.
|
||||
const saved = lowerer.declareHiddenLocal("%sniCallback", checked.type);
|
||||
pendingSni = { kind: "varDecl", localId: saved.id, init: checked, loc: locOf(prop) };
|
||||
sni = varRef(saved.id, checked.type, locOf(prop));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (k !== "cert" && k !== "key") {
|
||||
const init = ts.isPropertyAssignment(prop) ? prop.initializer : null;
|
||||
if (
|
||||
@@ -4801,7 +4823,7 @@ function lowerTlsServerOptions(
|
||||
"cert and key (PEM strings or Buffers) are the supported options",
|
||||
{
|
||||
SNICallback:
|
||||
"SNICallback lowers only on http2.createSecureServer({ allowHTTP1: true, ... }) — serve one cert/key pair here",
|
||||
"SNICallback lowers on tls.createServer and http2.createSecureServer({ allowHTTP1: true, ... }) — serve one cert/key pair here",
|
||||
requestCert:
|
||||
"client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert: false is the lowered value",
|
||||
},
|
||||
@@ -4838,6 +4860,10 @@ function lowerTlsServerOptions(
|
||||
"cert/key are PEM strings or Buffers here",
|
||||
);
|
||||
}
|
||||
if (pendingSni !== null) {
|
||||
v = { kind: "seqExpr", stmts: [pendingSni], result: v, type: v.type, loc };
|
||||
pendingSni = null;
|
||||
}
|
||||
if (k === "cert") cert = v;
|
||||
else key = v;
|
||||
}
|
||||
@@ -4848,7 +4874,7 @@ function lowerTlsServerOptions(
|
||||
"the supported options object is { cert, key } (PEM strings or Buffers)",
|
||||
);
|
||||
}
|
||||
return { cert, key };
|
||||
return sni === undefined ? { cert, key } : { cert, key, sni };
|
||||
}
|
||||
|
||||
/** The options argument of tls/https createServer, split three ways: an
|
||||
@@ -4911,12 +4937,24 @@ function lowerTlsServerOptionsOrDyn(
|
||||
what: string,
|
||||
includeHttpTimeoutOptions = false,
|
||||
runtimeKeys: ReadonlySet<string> = new Set(),
|
||||
): { cert: IrExpr; key: IrExpr; dyn?: undefined } | { dyn: IrExpr } {
|
||||
if (
|
||||
ts.isObjectLiteralExpression(node) &&
|
||||
!tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions, runtimeKeys)
|
||||
) {
|
||||
return lowerTlsServerOptions(lowerer, node, what);
|
||||
allowSni = false,
|
||||
): { cert: IrExpr; key: IrExpr; sni?: IrExpr; dyn?: undefined } | { dyn: IrExpr } {
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
if (!tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions, runtimeKeys))
|
||||
return lowerTlsServerOptions(lowerer, node, what, allowSni);
|
||||
const sniOption = node.properties.nodes.find(
|
||||
(p) =>
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
||||
p.name.text === "SNICallback",
|
||||
);
|
||||
if (allowSni && sniOption !== undefined) {
|
||||
lowerer.noLowering(
|
||||
`${what} with SNICallback and options the runtime walk reads`,
|
||||
sniOption,
|
||||
"SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet",
|
||||
);
|
||||
}
|
||||
}
|
||||
const v = lowerer.lowerExpr(node);
|
||||
if (v.type.kind === "dyn") return { dyn: v };
|
||||
@@ -5078,10 +5116,25 @@ function lowerTlsModuleCall(
|
||||
"tls.createServer",
|
||||
false,
|
||||
TLS_SERVER_RUNTIME_OPTIONS,
|
||||
true,
|
||||
);
|
||||
if (args.length === 1) {
|
||||
return opts.dyn !== undefined
|
||||
? { kind: "libCall", fn: "tls.createServerDyn", args: [opts.dyn], type: NETSERVER_T, loc }
|
||||
if (opts.dyn !== undefined)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "tls.createServerDyn",
|
||||
args: [opts.dyn],
|
||||
type: NETSERVER_T,
|
||||
loc,
|
||||
};
|
||||
return opts.sni !== undefined
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "tls.createServerSni",
|
||||
args: [opts.cert, opts.key, opts.sni],
|
||||
type: NETSERVER_T,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "tls.createServer",
|
||||
@@ -5099,11 +5152,19 @@ function lowerTlsModuleCall(
|
||||
"use (socket) or ()",
|
||||
[NETSOCKET_T],
|
||||
);
|
||||
return opts.dyn !== undefined
|
||||
if (opts.dyn !== undefined)
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "tls.createServerDynCb",
|
||||
args: [opts.dyn, cb],
|
||||
type: NETSERVER_T,
|
||||
loc,
|
||||
};
|
||||
return opts.sni !== undefined
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "tls.createServerDynCb",
|
||||
args: [opts.dyn, cb],
|
||||
fn: "tls.createServerSniCb",
|
||||
args: [opts.cert, opts.key, opts.sni, cb],
|
||||
type: NETSERVER_T,
|
||||
loc,
|
||||
}
|
||||
@@ -5481,6 +5542,44 @@ const SNI_HINT =
|
||||
"...(x ? { SNICallback: x } : {}) — the callback is (servername, cb) => void " +
|
||||
"with cb: (err: Error | null, ctx?: tls.SecureContext) => void";
|
||||
|
||||
/** An SNICallback option value: (servername: string, cb) => void where cb
|
||||
* is (err: Error | null, ctx?: SecureContext) => void, or that function
|
||||
* unioned with undefined (no callback). cb may declare fewer params (the
|
||||
* emitted answer thunk matches its ABI), but what it does declare must
|
||||
* carry the arms the thunk decodes. */
|
||||
function lowerSniCallbackValue(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
blame: ts.Node,
|
||||
what: string,
|
||||
): IrExpr {
|
||||
return checkSniCallbackValue(lowerer, lowerer.lowerExpr(node), blame, what);
|
||||
}
|
||||
|
||||
function checkSniCallbackValue(lowerer: Lowerer, v: IrExpr, blame: ts.Node, what: string): IrExpr {
|
||||
const shapeOk = (t: IrType): boolean => {
|
||||
if (t.kind !== "func" || t.ret.kind !== "void" || t.params.length !== 2) return false;
|
||||
if (t.params[0]!.kind !== "string") return false;
|
||||
const cbT = t.params[1]!;
|
||||
if (cbT.kind !== "func" || cbT.ret.kind !== "void" || cbT.params.length > 2) return false;
|
||||
const armKinds = (u: IrType): string[] =>
|
||||
u.kind === "union" ? (lowerer.unions.get(u.unionId)?.arms ?? []).map((a) => a.kind) : [];
|
||||
const p0 = cbT.params[0];
|
||||
if (p0 !== undefined && !armKinds(p0).includes("nullT")) return false;
|
||||
const p1 = cbT.params[1];
|
||||
if (p1 !== undefined && !armKinds(p1).includes("secureCtx")) return false;
|
||||
return true;
|
||||
};
|
||||
const arms = v.type.kind === "union" ? (lowerer.unions.get(v.type.unionId)?.arms ?? []) : [];
|
||||
const funcArm = v.type.kind === "func" ? v.type : arms.find((a) => a.kind === "func");
|
||||
const unionOk =
|
||||
v.type.kind !== "union" || (arms.length === 2 && arms.some((a) => a.kind === "undefinedT"));
|
||||
if (funcArm === undefined || !unionOk || !shapeOk(funcArm)) {
|
||||
lowerer.noLowering(`a ${what} SNICallback of '${lowerer.fmt(v.type)}' values`, blame, SNI_HINT);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/** Strips parentheses and type assertions: the portless spelling wraps
|
||||
* an options spread as `...({ ... } as Record<string, unknown>)`. */
|
||||
function stripParensAndCasts(node: ts.Expression): ts.Expression {
|
||||
@@ -5733,43 +5832,8 @@ function lowerHttp2ModuleCall(
|
||||
let sni: IrExpr | null = null;
|
||||
let allowHttp1 = false;
|
||||
let enableConnectProtocol = false;
|
||||
// The SNICallback value shape: (servername: string, cb) => void where
|
||||
// cb is (err: Error | null, ctx?: SecureContext) => void — cb may
|
||||
// declare fewer params (the emitted answer thunk matches its ABI),
|
||||
// but what it does declare must carry the arms the thunk decodes.
|
||||
const sniShapeOk = (t: IrType): boolean => {
|
||||
if (t.kind !== "func" || t.ret.kind !== "void" || t.params.length !== 2) return false;
|
||||
if (t.params[0]!.kind !== "string") return false;
|
||||
const cbT = t.params[1]!;
|
||||
if (cbT.kind !== "func" || cbT.ret.kind !== "void" || cbT.params.length > 2) return false;
|
||||
const armKinds = (u: IrType): string[] =>
|
||||
u.kind === "union" ? (lowerer.unions.get(u.unionId)?.arms ?? []).map((a) => a.kind) : [];
|
||||
const p0 = cbT.params[0];
|
||||
if (p0 !== undefined && !armKinds(p0).includes("nullT")) return false;
|
||||
const p1 = cbT.params[1];
|
||||
if (p1 !== undefined && !armKinds(p1).includes("secureCtx")) return false;
|
||||
return true;
|
||||
};
|
||||
const lowerSniValue = (node: ts.Expression, blame: ts.Node): void => {
|
||||
const v = lowerer.lowerExpr(node);
|
||||
const funcArm =
|
||||
v.type.kind === "func"
|
||||
? v.type
|
||||
: v.type.kind === "union"
|
||||
? (lowerer.unions.get(v.type.unionId)?.arms ?? []).find((a) => a.kind === "func")
|
||||
: undefined;
|
||||
const unionOk =
|
||||
v.type.kind !== "union" ||
|
||||
((lowerer.unions.get(v.type.unionId)?.arms ?? []).length === 2 &&
|
||||
(lowerer.unions.get(v.type.unionId)?.arms ?? []).some((a) => a.kind === "undefinedT"));
|
||||
if (funcArm === undefined || !unionOk || !sniShapeOk(funcArm)) {
|
||||
lowerer.noLowering(
|
||||
`a createSecureServer SNICallback of '${lowerer.fmt(v.type)}' values`,
|
||||
blame,
|
||||
SNI_HINT,
|
||||
);
|
||||
}
|
||||
sni = v;
|
||||
sni = lowerSniCallbackValue(lowerer, node, blame, "createSecureServer");
|
||||
};
|
||||
const lowerOption = (prop: ts.ObjectLiteralElementLike): void => {
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
|
||||
@@ -2110,6 +2110,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* surface — the handles are the same kinds. */
|
||||
"tls.createServer": { argTypes: [null, null], result: NETSERVER_T },
|
||||
"tls.createServerCb": { argTypes: [null, null, null], result: NETSERVER_T },
|
||||
/** The SNICallback forms: (cert, key, SNICallback[, handler]). The
|
||||
* callback is http2.createSecureServerSni's shape, a function or a
|
||||
* function | undefined union. */
|
||||
"tls.createServerSni": { argTypes: [null, null, null], result: NETSERVER_T },
|
||||
"tls.createServerSniCb": { argTypes: [null, null, null, null], result: NETSERVER_T },
|
||||
// The runtime options records (divergence 66's stance): a dyn options
|
||||
// value whose members read at runtime; pemDyn extracts a runtime-valued
|
||||
// cert/key member (arg 1 is the precomposed fence label).
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6933-tls-sni-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6934-tls-handshake-disconnect.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/tls-sni-callback-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -409,13 +409,15 @@ SCR_TL ScrError *scr_event_error_payload = NULL;
|
||||
|
||||
/* The code Node attaches to the runtime's event-error messages whose text
|
||||
* carries no errno name: connResetException's "socket hang up" and
|
||||
* "aborted", the HTTP parser's header overflow, and the TLS verification
|
||||
* failures (the openssl reason Node reports as the code). NULL for any
|
||||
* "aborted", the TLS client's disconnect before the handshake completes,
|
||||
* the HTTP parser's header overflow, and the TLS verification failures (the openssl reason Node reports as the code). NULL for any
|
||||
* other message. */
|
||||
const char *scr_error_message_code(const ScrStr *msg) {
|
||||
static const struct { const char *prefix; bool whole; const char *code; } known[] = {
|
||||
{ "socket hang up", true, "ECONNRESET" },
|
||||
{ "aborted", true, "ECONNRESET" },
|
||||
{ "Client network socket disconnected before secure TLS connection was established", true,
|
||||
"ECONNRESET" },
|
||||
{ "Parse Error: Header overflow", true, "HPE_HEADER_OVERFLOW" },
|
||||
{ "certificate has expired", true, "CERT_HAS_EXPIRED" },
|
||||
{ "self-signed certificate in certificate chain", true, "SELF_SIGNED_CERT_IN_CHAIN" },
|
||||
|
||||
@@ -1057,10 +1057,10 @@ static int scr_tls_handshake(void *tctx) {
|
||||
if (r == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
|
||||
scr_tls_verify_msg(t, msg, sizeof msg);
|
||||
} else if (r == MBEDTLS_ERR_SSL_CONN_EOF || r == MBEDTLS_ERR_NET_CONN_RESET) {
|
||||
/* the peer died mid-handshake: the http layer's own premature-close
|
||||
* shape ('socket hang up') is Node's message — tear down without an error
|
||||
* at this level and let the closed hook classify it */
|
||||
scr_net_sock_transport_error(t->sock, NULL);
|
||||
/* The peer closed or reset the connection mid-handshake: Node's
|
||||
* ECONNRESET for tls.connect and https.request alike. */
|
||||
scr_net_sock_transport_error(
|
||||
t->sock, "Client network socket disconnected before secure TLS connection was established");
|
||||
return -1;
|
||||
} else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE &&
|
||||
t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL &&
|
||||
@@ -1170,8 +1170,15 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le
|
||||
const char *key, size_t key_len,
|
||||
ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn, char **alpn /*moves, nullable*/,
|
||||
int min_version, int max_version) {
|
||||
int min_version, int max_version,
|
||||
ScrClosure *sni_cb /*moves, nullable*/,
|
||||
void *sni_answer_fn) {
|
||||
ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false);
|
||||
if (sni_cb != NULL) {
|
||||
srv->sni_cb = sni_cb; /* moves; released with srv */
|
||||
srv->sni_answer_fn = sni_answer_fn;
|
||||
mbedtls_ssl_conf_sni(&srv->conf, &scr_tls_f_sni, NULL);
|
||||
}
|
||||
srv->max_tls12 = max_version == 12;
|
||||
if (!scr_tls_conf_versions(&srv->conf, min_version, max_version)) {
|
||||
/* Node's server then fails every handshake; nothing here models
|
||||
@@ -1198,7 +1205,21 @@ static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_le
|
||||
ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const char *key,
|
||||
size_t key_len, ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn) {
|
||||
return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13);
|
||||
return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
/* tls.createServer({ cert, key, SNICallback }): the pre-parse machinery
|
||||
* (design note atop the SNI section) hands each ClientHello's servername
|
||||
* to the callback and serves the context it answers, or the default pair
|
||||
* for none. A NULL sni_cb (the callback union's undefined arm) is
|
||||
* scr_tls_create_server. */
|
||||
ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const char *key,
|
||||
size_t key_len, ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn, ScrClosure *sni_cb /*moves, nullable*/,
|
||||
void *sni_answer_fn) {
|
||||
return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13, sni_cb,
|
||||
sni_answer_fn);
|
||||
}
|
||||
|
||||
ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key,
|
||||
@@ -2024,7 +2045,7 @@ ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/,
|
||||
}
|
||||
ScrNetServer *s = scr_tls_create_server_alpn((const char *)cert->data, cert->len,
|
||||
(const char *)key->data, key->len, handler, fn,
|
||||
alpn, min_version, max_version);
|
||||
alpn, min_version, max_version, NULL, NULL);
|
||||
scr_bytes_release(cert);
|
||||
scr_bytes_release(key);
|
||||
return s;
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
* (port 443 default, `ca` / rejectUnauthorized for local-CA flows —
|
||||
* cert/key/ca all arrive as borrowed PEM bytes). */
|
||||
ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 */
|
||||
/* The SNICallback form: sni_cb (moves, nullable) receives (servername, answer), and
|
||||
* sni_answer_fn is the emitted thunk that hands the answer to scr_tls_sni_answer. */
|
||||
ScrNetServer *scr_tls_create_server_sni(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn, ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn); /* +1 */
|
||||
|
||||
/* ── RUNTIME tls/https options records (the divergence-66 stance: a
|
||||
* non-literal options value reads its members at runtime; members whose
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// tls.createServer with an SNICallback: the callback receives each
|
||||
// ClientHello's servername and answers a SecureContext synchronously or
|
||||
// later, or none, so the default cert/key pair serves; an error answer
|
||||
// fails that handshake. Clients that trust the CA see the certificate the
|
||||
// answer chose: alt.localhost verifies only with the alt context, and a
|
||||
// name served the default localhost pair fails Node's hostname check.
|
||||
import { readFileSync } from "node:fs";
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
const certs = "tests/fixtures/server/certs";
|
||||
const ca = readFileSync(`${certs}/ca.pem`);
|
||||
const cert = readFileSync(`${certs}/localhost.pem`);
|
||||
const key = readFileSync(`${certs}/localhost-key.pem`);
|
||||
const altContext = tls.createSecureContext({
|
||||
cert: readFileSync(`${certs}/alt.pem`),
|
||||
key: readFileSync(`${certs}/alt-key.pem`),
|
||||
});
|
||||
|
||||
const server = tls.createServer(
|
||||
{
|
||||
cert,
|
||||
key,
|
||||
SNICallback: (servername: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => {
|
||||
console.log("sni", servername);
|
||||
if (servername === "alt.localhost") setTimeout(() => cb(null, altContext), 5);
|
||||
else if (servername === "fail.localhost") cb(new Error(`no certificate for ${servername}`));
|
||||
else cb(null, undefined);
|
||||
},
|
||||
},
|
||||
(socket) => {
|
||||
socket.end("hello");
|
||||
},
|
||||
);
|
||||
|
||||
// The callback is read where it appears, effects included: a later cert
|
||||
// initializer that reassigns its binding does not change the callback the
|
||||
// server uses.
|
||||
type Sni = (servername: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => void;
|
||||
let chosen: Sni = (servername, cb) => {
|
||||
console.log("first callback", servername);
|
||||
cb(null, altContext);
|
||||
};
|
||||
const ordered = tls.createServer(
|
||||
{
|
||||
SNICallback: (console.log("read callback"), chosen),
|
||||
cert: (console.log("read cert"),
|
||||
(chosen = (servername, cb) => {
|
||||
console.log("second callback", servername);
|
||||
cb(null, undefined);
|
||||
}),
|
||||
cert),
|
||||
key,
|
||||
},
|
||||
(socket) => {
|
||||
socket.end("ordered");
|
||||
},
|
||||
);
|
||||
|
||||
function attempt(port: number, servername: string, next: () => void): void {
|
||||
let done = false;
|
||||
const finish = (line: string): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
console.log(servername, line);
|
||||
next();
|
||||
};
|
||||
const socket = tls.connect({ port, host: "127.0.0.1", servername, ca }, () => {
|
||||
let text = "";
|
||||
socket.on("data", (data: Buffer) => {
|
||||
text += data.toString();
|
||||
});
|
||||
socket.on("end", () => finish(`authorized ${socket.authorized} ${text}`));
|
||||
});
|
||||
socket.on("error", (error: Error) => {
|
||||
finish(`error ${(error as NodeJS.ErrnoException).code}`);
|
||||
});
|
||||
}
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
attempt(port, "localhost", () =>
|
||||
attempt(port, "alt.localhost", () =>
|
||||
attempt(port, "other.localhost", () =>
|
||||
attempt(port, "fail.localhost", () =>
|
||||
server.close(() => {
|
||||
console.log("closed");
|
||||
ordered.listen(0, "127.0.0.1", () => {
|
||||
const orderedPort = (ordered.address() as net.AddressInfo).port;
|
||||
attempt(orderedPort, "alt.localhost", () => ordered.close(() => console.log("ordered closed")));
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// A server that closes the connection before the TLS handshake completes:
|
||||
// tls.connect and https.get fail with ECONNRESET and Node's "Client network
|
||||
// socket disconnected before secure TLS connection was established".
|
||||
import * as https from "node:https";
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
socket.end();
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
const client = tls.connect({ port, host: "127.0.0.1", rejectUnauthorized: false }, () => {
|
||||
console.log("tls connected");
|
||||
});
|
||||
client.on("error", (error: Error) => {
|
||||
console.log("tls", (error as NodeJS.ErrnoException).code, error.message);
|
||||
const request = https.get({ host: "127.0.0.1", port, rejectUnauthorized: false }, () => {
|
||||
console.log("https response");
|
||||
});
|
||||
request.on("error", (err: Error) => {
|
||||
console.log("https", (err as NodeJS.ErrnoException).code, err.message);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// tls.createServer SNICallback forms that have no lowering.
|
||||
import { readFileSync } from "node:fs";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
const cert = readFileSync("cert.pem");
|
||||
const key = readFileSync("key.pem");
|
||||
type Answer = (err: Error | null, ctx?: tls.SecureContext) => void;
|
||||
function makeSni(): (servername: string, cb: Answer) => void {
|
||||
return (_servername, cb) => cb(null, undefined);
|
||||
}
|
||||
|
||||
// SNICallback with options the runtime options walk reads.
|
||||
tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni });
|
||||
@@ -0,0 +1,8 @@
|
||||
tls-sni-callback-fences.ts:13:54 - error SC2020: 'tls.createServer with SNICallback and options the runtime walk reads' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
12 | // SNICallback with options the runtime options walk reads.
|
||||
13 | tls.createServer({ cert, key, ALPNProtocols: ["h2"], SNICallback: makeSni });
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
14 |
|
||||
|
||||
hint: SNICallback lowers with cert and key; ALPNProtocols, minVersion, maxVersion and the validated options are not combined with it yet
|
||||
Reference in New Issue
Block a user