Support SNICallback in tls.createServer (#1700)

This commit is contained in:
Chris Tate
2026-10-21 22:00:41 -05:00
committed by GitHub
14 changed files with 358 additions and 74 deletions
+19 -15
View File
@@ -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).
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6933-tls-sni-callback.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6934-tls-handshake-disconnect.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/tls-sni-callback-fences.ts"
],
"diags": []
}
+4 -2
View File
@@ -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" },
+28 -7
View File
@@ -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;
+3
View File
@@ -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
+97
View File
@@ -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