mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Negotiate ALPN and bound the TLS version in tls.connect and tls.createServer
tls.connect and tls.createServer accept ALPNProtocols as an array of strings, a Buffer or a Uint8Array in the wire format. The server answers with the first of its own protocols that the client offers, and both sockets report it as alpnProtocol. A client that offers none of the server's protocols fails with ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL. minVersion and maxVersion bound the handshake, and getProtocol() reports the version it picked. Ranges that do not overlap fail with Node's ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION or ERR_SSL_NO_PROTOCOLS_AVAILABLE, and invalid versions throw Node's TypeError. tls.connect's timeout option arms the socket's inactivity timeout before it connects. Refs #1438 Co-authored-by: saboursepideh <25549001+saboursepideh@users.noreply.github.com>
This commit is contained in:
co-authored by
saboursepideh
parent
0237d70df0
commit
9b70fb5e7b
@@ -68082,9 +68082,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68289,9 +68289,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -95364,7 +95364,9 @@
|
||||
"tests/corpus/2331-tls-server-options-typed.ts",
|
||||
"tests/corpus/2500-net-autosel-timeout.ts",
|
||||
"tests/corpus/2557-tls-ca-store.ts",
|
||||
"tests/corpus/2596-net-arg-ladders.cjs"
|
||||
"tests/corpus/2596-net-arg-ladders.cjs",
|
||||
"tests/corpus/6721-tls-alpn-protocols.ts",
|
||||
"tests/corpus/6722-tls-min-max-version.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -96365,8 +96367,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:tls.tlsSocket.getProtocol",
|
||||
"tests": [
|
||||
"tests/corpus/6722-tls-min-max-version.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96572,8 +96577,11 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:tls.alpnProtocol",
|
||||
"tests": [
|
||||
"tests/corpus/6721-tls-alpn-protocols.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
|
||||
@@ -365,6 +365,8 @@
|
||||
{ "chapter": "stream", "signatures": ["`writable` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/2893-child-stdin-duplex.ts", "tests/corpus/2895-child-stdin-write-after-end.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["Event: `'drain'`"], "status": "partial", "evidence": ["tests/corpus/2894-child-stdin-backpressure.ts"] },
|
||||
{ "chapter": "console", "signatures": ["Class: `Console`"], "status": "partial", "evidence": ["tests/corpus/4900-console-instances.ts", "tests/corpus/4901-console-instance-errors.ts"] },
|
||||
{ "chapter": "tls", "signatures": ["`alpnProtocol` Type: {string|boolean|null}"], "status": "partial", "evidence": ["tests/corpus/6721-tls-alpn-protocols.ts"] },
|
||||
{ "chapter": "tls", "signatures": ["`tlsSocket.getProtocol()`"], "status": "partial", "evidence": ["tests/corpus/6722-tls-min-max-version.ts"] },
|
||||
{ "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] },
|
||||
{ "chapter": "webstreams", "signatures": ["`streamConsumers.text(stream)`", "`streamConsumers.json(stream)`", "`streamConsumers.buffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/2629-stream-consumers.ts", "tests/corpus/2630-stream-consumers-errors.ts"] },
|
||||
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
|
||||
|
||||
@@ -4877,12 +4877,14 @@ const TLS_VALIDATED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
]);
|
||||
|
||||
/** True when a literal options bag must ride the runtime walker: JS uses
|
||||
* it for the typed TLS validation ladders, and HTTPS uses it for
|
||||
* it for the typed TLS validation ladders, HTTPS uses it for
|
||||
* keepAliveTimeoutBuffer so the HTTP option is consumed without losing
|
||||
* the literal's evaluation order. */
|
||||
* the literal's evaluation order, and `runtimeKeys` names options only the
|
||||
* runtime walk implements (tls.createServer's ALPNProtocols). */
|
||||
function tlsLiteralNeedsRuntimeWalk(
|
||||
node: ts.ObjectLiteralExpression,
|
||||
includeHttpTimeoutOptions = false,
|
||||
runtimeKeys: ReadonlySet<string> = new Set(),
|
||||
): boolean {
|
||||
const jsSource = isJsSourceFile(ts.getSourceFileOfNode(node));
|
||||
return node.properties.nodes.some(
|
||||
@@ -4890,19 +4892,29 @@ function tlsLiteralNeedsRuntimeWalk(
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
||||
((includeHttpTimeoutOptions && p.name.text === "keepAliveTimeoutBuffer") ||
|
||||
runtimeKeys.has(p.name.text) ||
|
||||
(jsSource && TLS_VALIDATED_OPTIONS.has(p.name.text))),
|
||||
);
|
||||
}
|
||||
|
||||
/** tls.createServer options the runtime walk implements and the literal
|
||||
* cert/key lowering does not. */
|
||||
const TLS_SERVER_RUNTIME_OPTIONS: ReadonlySet<string> = new Set([
|
||||
"ALPNProtocols",
|
||||
"minVersion",
|
||||
"maxVersion",
|
||||
]);
|
||||
|
||||
function lowerTlsServerOptionsOrDyn(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
what: string,
|
||||
includeHttpTimeoutOptions = false,
|
||||
runtimeKeys: ReadonlySet<string> = new Set(),
|
||||
): { cert: IrExpr; key: IrExpr; dyn?: undefined } | { dyn: IrExpr } {
|
||||
if (
|
||||
ts.isObjectLiteralExpression(node) &&
|
||||
!tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions)
|
||||
!tlsLiteralNeedsRuntimeWalk(node, includeHttpTimeoutOptions, runtimeKeys)
|
||||
) {
|
||||
return lowerTlsServerOptions(lowerer, node, what);
|
||||
}
|
||||
@@ -4934,14 +4946,14 @@ function lowerTlsServerOptionsOrDyn(
|
||||
* connect(options[, cb]) and connect(port[, host][, options][, cb]) with
|
||||
* a RUNTIME options record (the checked-dynamic JS lane's shape — a dyn
|
||||
* value, or a record that converts): the runtime walk implements
|
||||
* port/host/rejectUnauthorized/ca/servername and throws the catchable
|
||||
* port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable
|
||||
* fence for other documented members (the divergence-66 stance). The
|
||||
* callback (and 'secureConnect'/'connect' listeners) fires
|
||||
* post-handshake — Node's secureConnect timing. */
|
||||
function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
const args = expr.arguments.nodes;
|
||||
const FORMS_HINT =
|
||||
"the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername";
|
||||
"the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout";
|
||||
if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`tls.connect with ${args.length} arguments`, expr, FORMS_HINT);
|
||||
}
|
||||
@@ -5060,7 +5072,13 @@ function lowerTlsModuleCall(
|
||||
"the supported form is createServer({ cert, key }, socket => ...)",
|
||||
);
|
||||
}
|
||||
const opts = lowerTlsServerOptionsOrDyn(lowerer, args[0]!, "tls.createServer");
|
||||
const opts = lowerTlsServerOptionsOrDyn(
|
||||
lowerer,
|
||||
args[0]!,
|
||||
"tls.createServer",
|
||||
false,
|
||||
TLS_SERVER_RUNTIME_OPTIONS,
|
||||
);
|
||||
if (args.length === 1) {
|
||||
return opts.dyn !== undefined
|
||||
? { kind: "libCall", fn: "tls.createServerDyn", args: [opts.dyn], type: NETSERVER_T, loc }
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6721-tls-alpn-protocols.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6722-tls-min-max-version.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6723-tls-connect-timeout.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -422,6 +422,11 @@ const char *scr_error_message_code(const ScrStr *msg) {
|
||||
{ "self-signed certificate", false, "DEPTH_ZERO_SELF_SIGNED_CERT" },
|
||||
{ "unable to verify the first certificate", false, "UNABLE_TO_VERIFY_LEAF_SIGNATURE" },
|
||||
{ "Hostname/IP does not match certificate's altnames", false, "ERR_TLS_CERT_ALTNAME_INVALID" },
|
||||
{ "error:0A000460:SSL routines:ssl3_read_bytes:tlsv1 alert no application protocol", true,
|
||||
"ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL" },
|
||||
{ "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version", true,
|
||||
"ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION" },
|
||||
{ "error:0A0000BF:SSL routines::no protocols available", true, "ERR_SSL_NO_PROTOCOLS_AVAILABLE" },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) {
|
||||
size_t n = strlen(known[i].prefix);
|
||||
|
||||
+412
-31
@@ -255,6 +255,93 @@ void scr_secure_ctx_release_v(void *p) { scr_secure_ctx_release((ScrSecureCtx *)
|
||||
|
||||
/* ── the server config (one per created server, rc'd) ────────────────── */
|
||||
|
||||
/* An owned ALPN name list (NULL-terminated; NULL frees nothing). */
|
||||
static void scr_tls_alpn_free(char **names) {
|
||||
if (names == NULL) return;
|
||||
for (char **n = names; *n != NULL; n++) free(*n);
|
||||
free(names);
|
||||
}
|
||||
|
||||
static bool scr_tls_alpn_dyn(const ScrDyn *v, char ***out);
|
||||
static void scr_tls_opt_fence(const char *api, const char *key, const char *hint);
|
||||
static bool scr_tls_dyn_truthy(const ScrDyn *v);
|
||||
|
||||
/* Node's toV over a minVersion/maxVersion value: 'TLSv1' through 'TLSv1.3'
|
||||
* as 10 through 13, null or undefined as `def`. Anything else throws
|
||||
* ERR_TLS_INVALID_PROTOCOL_VERSION ("%j is not a valid <which> TLS protocol
|
||||
* version"). False with the exception pending. */
|
||||
static bool scr_tls_version_dyn(const ScrDyn *v, const char *which, int def, int *out) {
|
||||
if (v == NULL || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL) {
|
||||
*out = def;
|
||||
return true;
|
||||
}
|
||||
if (v->kind == SCR_DYN_STR) {
|
||||
static const struct { const char *name; int version; } names[] = {
|
||||
{ "TLSv1", 10 }, { "TLSv1.1", 11 }, { "TLSv1.2", 12 }, { "TLSv1.3", 13 },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof names / sizeof names[0]; i++) {
|
||||
if (v->v.str->len == strlen(names[i].name) &&
|
||||
memcmp(v->v.str->data, names[i].name, v->v.str->len) == 0) {
|
||||
*out = names[i].version;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ScrStr *shown = scr_util_format_j(v);
|
||||
if (shown == NULL) return false;
|
||||
char msg[300];
|
||||
int m = snprintf(msg, sizeof msg, "%.*s is not a valid %s TLS protocol version",
|
||||
(int)(shown->len < 200 ? shown->len : 200), shown->data, which);
|
||||
scr_str_release(shown);
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)m, "ERR_TLS_INVALID_PROTOCOL_VERSION");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The version range on an mbedTLS config. mbedTLS implements TLS 1.2 and
|
||||
* 1.3, so a minimum of TLSv1 or TLSv1.1 offers from 1.2, the lowest version
|
||||
* Node's default security level allows too. Answers false when no version
|
||||
* in [min, max] is available (a maximum below 1.2, or min above max), which
|
||||
* Node reports as ERR_SSL_NO_PROTOCOLS_AVAILABLE when the handshake starts. */
|
||||
static bool scr_tls_conf_versions(mbedtls_ssl_config *conf, int min, int max) {
|
||||
if (min < 12) min = 12;
|
||||
if (max < min) return false;
|
||||
mbedtls_ssl_conf_min_tls_version(conf, min == 13 ? MBEDTLS_SSL_VERSION_TLS1_3 : MBEDTLS_SSL_VERSION_TLS1_2);
|
||||
mbedtls_ssl_conf_max_tls_version(conf, max == 13 ? MBEDTLS_SSL_VERSION_TLS1_3 : MBEDTLS_SSL_VERSION_TLS1_2);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Node's SecureContext checks, in its order: secureProtocol conflicts with
|
||||
* an explicit minVersion or maxVersion, then each version. The range lands
|
||||
* in *min / *max (Node's defaults TLSv1.2 and TLSv1.3). False with the
|
||||
* exception pending. */
|
||||
static bool scr_tls_versions_dyn(const ScrDyn *opts, int *min, int *max) {
|
||||
const ScrDyn *vmin = opts != NULL && opts->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(opts, "minVersion", 10) : NULL;
|
||||
const ScrDyn *vmax = opts != NULL && opts->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(opts, "maxVersion", 10) : NULL;
|
||||
const ScrDyn *proto = opts != NULL && opts->kind == SCR_DYN_OBJ ? scr_dyn_obj_get(opts, "secureProtocol", 14) : NULL;
|
||||
if (proto != NULL && scr_tls_dyn_truthy(proto)) {
|
||||
const ScrDyn *version = vmin != NULL && vmin->kind != SCR_DYN_UNDEF && vmin->kind != SCR_DYN_NULL ? vmin
|
||||
: vmax != NULL && vmax->kind != SCR_DYN_UNDEF && vmax->kind != SCR_DYN_NULL ? vmax
|
||||
: NULL;
|
||||
if (version != NULL) {
|
||||
ScrStr *a = scr_util_format_j(version);
|
||||
if (a == NULL) return false;
|
||||
ScrStr *b = scr_util_format_j(proto);
|
||||
if (b == NULL) {
|
||||
scr_str_release(a);
|
||||
return false;
|
||||
}
|
||||
char msg[400];
|
||||
int m = snprintf(msg, sizeof msg, "TLS protocol version %.*s conflicts with secureProtocol %.*s",
|
||||
(int)(a->len < 150 ? a->len : 150), a->data, (int)(b->len < 150 ? b->len : 150), b->data);
|
||||
scr_str_release(a);
|
||||
scr_str_release(b);
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)m, "ERR_TLS_PROTOCOL_VERSION_CONFLICT");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return scr_tls_version_dyn(vmin, "minimum", 12, min) && scr_tls_version_dyn(vmax, "maximum", 13, max);
|
||||
}
|
||||
|
||||
typedef struct ScrTlsSrv {
|
||||
size_t rc;
|
||||
mbedtls_ssl_config conf;
|
||||
@@ -275,6 +362,12 @@ typedef struct ScrTlsSrv {
|
||||
* thunk that becomes each minted answer closure's fn */
|
||||
ScrClosure *sni_cb;
|
||||
void *sni_answer_fn;
|
||||
/* tls.createServer's ALPNProtocols (owned; NULL when none), in the
|
||||
* server's preference order. */
|
||||
char **alpn;
|
||||
/* maxVersion TLSv1.2: the ClientHello is pre-parsed so a hello that
|
||||
* offers only later versions gets OpenSSL's protocol_version alert. */
|
||||
bool max_tls12;
|
||||
} ScrTlsSrv;
|
||||
|
||||
/* The ALPN answers (static storage — mbedTLS keeps the pointers). Plain
|
||||
@@ -325,6 +418,7 @@ static void scr_tls_srv_release(ScrTlsSrv *s) {
|
||||
mbedtls_ssl_config_free(&s->conf);
|
||||
mbedtls_x509_crt_free(&s->cert);
|
||||
mbedtls_pk_free(&s->pk);
|
||||
scr_tls_alpn_free(s->alpn);
|
||||
free(s);
|
||||
}
|
||||
|
||||
@@ -342,6 +436,13 @@ typedef struct ScrTlsCli {
|
||||
* VERIFY_NONE socket (the https client's rejectUnauthorized:false)
|
||||
* never verified, so authorized answers false like Node's. */
|
||||
bool verify_recorded;
|
||||
/* The ALPNProtocols option's names, NULL-terminated, in the caller's
|
||||
* order (mbedtls_ssl_conf_alpn_protocols keeps the pointer); NULL when
|
||||
* none are offered. */
|
||||
char **alpn;
|
||||
/* No TLS version is in the configured range: the handshake fails as
|
||||
* soon as it starts (Node's ERR_SSL_NO_PROTOCOLS_AVAILABLE). */
|
||||
bool no_protocols;
|
||||
} ScrTlsCli;
|
||||
|
||||
/* The default trust anchors when no `ca` option is given: the host trust
|
||||
@@ -490,6 +591,7 @@ static ScrTlsCli *scr_tls_cli_new(ScrStr *host, bool reject_unauthorized, const
|
||||
|
||||
static void scr_tls_cli_free(ScrTlsCli *c) {
|
||||
mbedtls_ssl_config_free(&c->conf);
|
||||
scr_tls_alpn_free(c->alpn);
|
||||
mbedtls_x509_crt_free(&c->ca);
|
||||
free(c->hostname);
|
||||
free(c->verify_name);
|
||||
@@ -725,8 +827,13 @@ static int scr_tls_f_sni(void *p, mbedtls_ssl_context *ssl, const unsigned char
|
||||
* random, session id, cipher suites, compression, then the extension list
|
||||
* — server_name (type 0) holds one host_name (type 0) entry. Answers 1
|
||||
* with `out` set, 2 for a well-formed hello without SNI or anything
|
||||
* malformed (the real handshake will judge it). */
|
||||
static int scr_tls_parse_ch_body(const unsigned char *b, size_t n, char *out, size_t out_cap) {
|
||||
* malformed (the real handshake will judge it). *offers12 answers whether
|
||||
* the hello offers TLS 1.2: true without a supported_versions extension
|
||||
* (type 43), else whether its list holds 0x0303. */
|
||||
static int scr_tls_parse_ch_body(const unsigned char *b, size_t n, char *out, size_t out_cap,
|
||||
bool *offers12) {
|
||||
*offers12 = true;
|
||||
int found = 2;
|
||||
size_t i = 34; /* version(2) + random(32) */
|
||||
if (i + 1 > n) return 2;
|
||||
i += 1 + b[i]; /* session id: length byte + body */
|
||||
@@ -744,24 +851,35 @@ static int scr_tls_parse_ch_body(const unsigned char *b, size_t n, char *out, si
|
||||
size_t el = ((size_t)b[i + 2] << 8) | b[i + 3];
|
||||
i += 4;
|
||||
if (i + el > end) return 2;
|
||||
if (et == 0) {
|
||||
if (et == 0 && found == 2) {
|
||||
/* server_name: list length(2), then entry type(1) + length(2) + name */
|
||||
if (el < 5 || b[i + 2] != 0) return 2;
|
||||
size_t nl = ((size_t)b[i + 3] << 8) | b[i + 4];
|
||||
if (nl == 0 || nl + 5 > el || nl + 1 > out_cap) return 2;
|
||||
memcpy(out, b + i + 5, nl);
|
||||
out[nl] = 0;
|
||||
return 1;
|
||||
if (el >= 5 && b[i + 2] == 0) {
|
||||
size_t nl = ((size_t)b[i + 3] << 8) | b[i + 4];
|
||||
if (nl > 0 && nl + 5 <= el && nl + 1 <= out_cap) {
|
||||
memcpy(out, b + i + 5, nl);
|
||||
out[nl] = 0;
|
||||
found = 1;
|
||||
}
|
||||
}
|
||||
} else if (et == 43 && el >= 1) {
|
||||
/* supported_versions: list length(1), then 2-byte versions */
|
||||
size_t vl = b[i] < el - 1 ? b[i] : el - 1;
|
||||
*offers12 = false;
|
||||
for (size_t v = 0; v + 1 < vl; v += 2) {
|
||||
if (b[i + 1 + v] == 0x03 && b[i + 2 + v] == 0x03) *offers12 = true;
|
||||
}
|
||||
}
|
||||
i += el;
|
||||
}
|
||||
return 2;
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Reassemble the handshake stream from the raw record flight and look for
|
||||
* the server_name extension. 0 = need more bytes; 1 = found (out set);
|
||||
* 2 = no SNI / not parseable (serve the default cert). */
|
||||
static int scr_tls_parse_sni(const unsigned char *buf, size_t len, char *out, size_t out_cap) {
|
||||
static int scr_tls_parse_sni(const unsigned char *buf, size_t len, char *out, size_t out_cap,
|
||||
bool *offers12) {
|
||||
*offers12 = true;
|
||||
unsigned char *hs = NULL;
|
||||
size_t hs_len = 0, hs_cap = 0;
|
||||
size_t off = 0;
|
||||
@@ -785,7 +903,7 @@ static int scr_tls_parse_sni(const unsigned char *buf, size_t len, char *out, si
|
||||
size_t chlen = ((size_t)hs[1] << 16) | ((size_t)hs[2] << 8) | hs[3];
|
||||
if (chlen > 131072) { free(hs); return 2; }
|
||||
if (hs_len >= 4 + chlen) {
|
||||
int r = scr_tls_parse_ch_body(hs + 4, chlen, out, out_cap);
|
||||
int r = scr_tls_parse_ch_body(hs + 4, chlen, out, out_cap, offers12);
|
||||
free(hs);
|
||||
return r;
|
||||
}
|
||||
@@ -811,8 +929,19 @@ static int scr_tls_sni_collect(ScrTlsSock *t) {
|
||||
char servername[256];
|
||||
for (;;) {
|
||||
if (t->hello_len > 0) {
|
||||
int r = scr_tls_parse_sni(t->hello, t->hello_len, servername, sizeof servername);
|
||||
if (r == 2 || t->hello_len > 65536) {
|
||||
bool offers12;
|
||||
int r = scr_tls_parse_sni(t->hello, t->hello_len, servername, sizeof servername, &offers12);
|
||||
if (r != 0 && t->srv->max_tls12 && !offers12) {
|
||||
/* A TLS 1.2 server and a hello offering only later versions:
|
||||
* OpenSSL answers with a fatal protocol_version alert, which the
|
||||
* client reports as ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION. */
|
||||
static const unsigned char alert[] = { 0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x46 };
|
||||
int fd = scr_net_sock_fd(t->sock);
|
||||
if (fd >= 0) (void)!write(fd, alert, sizeof alert);
|
||||
scr_net_sock_transport_error(t->sock, NULL);
|
||||
return -1;
|
||||
}
|
||||
if (r == 2 || t->hello_len > 65536 || (r == 1 && t->srv->sni_cb == NULL)) {
|
||||
/* no SNI (or unparseable): skip the callback, serve the default
|
||||
* pair — Node's SNICallback never fires without a server_name */
|
||||
t->sni_state = SCR_SNI_DONE;
|
||||
@@ -899,6 +1028,12 @@ void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*move
|
||||
|
||||
static int scr_tls_handshake(void *tctx) {
|
||||
ScrTlsSock *t = (ScrTlsSock *)tctx;
|
||||
if (t->cli != NULL && t->cli->no_protocols) {
|
||||
/* OpenSSL's reason without its per-process prefix (SEMANTICS.md
|
||||
* divergence 55); scr_error_message_code gives Node's code. */
|
||||
scr_net_sock_transport_error(t->sock, "error:0A0000BF:SSL routines::no protocols available");
|
||||
return -1;
|
||||
}
|
||||
if (t->sni_state == SCR_SNI_COLLECT) {
|
||||
t->in_handshake = true;
|
||||
int c = scr_tls_sni_collect(t);
|
||||
@@ -927,6 +1062,20 @@ static int scr_tls_handshake(void *tctx) {
|
||||
* at this level and let the closed hook classify it */
|
||||
scr_net_sock_transport_error(t->sock, NULL);
|
||||
return -1;
|
||||
} else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE &&
|
||||
t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL &&
|
||||
t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_NO_APPLICATION_PROTOCOL) {
|
||||
/* The server shares none of the ALPNProtocols: OpenSSL's reason, which
|
||||
* Node reports as ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL
|
||||
* (scr_error_message_code), without its per-process prefix and source
|
||||
* location (SEMANTICS.md divergence 55). */
|
||||
snprintf(msg, sizeof msg,
|
||||
"error:0A000460:SSL routines:ssl3_read_bytes:tlsv1 alert no application protocol");
|
||||
} else if (r == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE &&
|
||||
t->ssl.MBEDTLS_PRIVATE(in_msg) != NULL &&
|
||||
t->ssl.MBEDTLS_PRIVATE(in_msg)[1] == MBEDTLS_SSL_ALERT_MSG_PROTOCOL_VERSION) {
|
||||
/* The server accepts none of the offered versions. */
|
||||
snprintf(msg, sizeof msg, "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version");
|
||||
} else {
|
||||
/* non-verify handshake failures: honest but not Node's exact openssl
|
||||
* string (SEMANTICS.md divergence 55) */
|
||||
@@ -1012,19 +1161,44 @@ static void scr_tls_srv_on_conn(void *ctx, ScrNetSocket *sock) {
|
||||
if (mbedtls_ssl_setup(&t->ssl, &srv->conf) != 0) scr_tls_oom();
|
||||
t->sock = sock;
|
||||
t->srv = scr_tls_srv_retain(srv);
|
||||
if (srv->sni_cb != NULL) t->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */
|
||||
if (srv->sni_cb != NULL || srv->max_tls12) t->sni_state = SCR_SNI_COLLECT; /* pre-parse the hello */
|
||||
mbedtls_ssl_set_bio(&t->ssl, t, scr_tls_bio_send, scr_tls_bio_recv, NULL);
|
||||
scr_net_sock_set_transport(sock, &SCR_TLS_OPS, t);
|
||||
}
|
||||
|
||||
static ScrNetServer *scr_tls_create_server_alpn(const char *cert, size_t cert_len,
|
||||
const char *key, size_t key_len,
|
||||
ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn, char **alpn /*moves, nullable*/,
|
||||
int min_version, int max_version) {
|
||||
ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false);
|
||||
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
|
||||
* that, so it stays a refusal. */
|
||||
scr_tls_srv_release(srv);
|
||||
scr_closure_release(handler);
|
||||
scr_tls_alpn_free(alpn);
|
||||
scr_tls_opt_fence("tls.createServer", "maxVersion",
|
||||
"a version range with neither TLSv1.2 nor TLSv1.3 has no lowering");
|
||||
return NULL;
|
||||
}
|
||||
if (alpn != NULL) {
|
||||
/* mbedTLS picks the first of these the client offers (Node's server
|
||||
* preference) and sends no_application_protocol when it offers none. */
|
||||
srv->alpn = alpn;
|
||||
mbedtls_ssl_conf_alpn_protocols(&srv->conf, (const char **)srv->alpn);
|
||||
}
|
||||
ScrNetServer *s = scr_net_create_server(handler, fn);
|
||||
scr_net_server_set_native_conn(s, &scr_tls_srv_on_conn, srv, &scr_tls_srv_release_v);
|
||||
scr_net_server_defer_connections(s); /* the handler is 'secureConnection' */
|
||||
return s;
|
||||
}
|
||||
|
||||
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) {
|
||||
ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false);
|
||||
ScrNetServer *s = scr_net_create_server(handler, fn);
|
||||
scr_net_server_set_native_conn(s, &scr_tls_srv_on_conn, srv, &scr_tls_srv_release_v);
|
||||
scr_net_server_defer_connections(s); /* the handler is 'secureConnection' */
|
||||
return s;
|
||||
return scr_tls_create_server_alpn(cert, cert_len, key, key_len, handler, fn, NULL, 12, 13);
|
||||
}
|
||||
|
||||
ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key,
|
||||
@@ -1730,8 +1904,10 @@ static bool scr_tls_opts_validate(const ScrDyn *opts) {
|
||||
* dyn options record; false = exception pending (partial results released).
|
||||
* Exported runtime-internally: scr_http2.c's createSecureServerDyn rides
|
||||
* the same walk (scr_http2.c always links alongside this unit). */
|
||||
bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
|
||||
ScrBytes **key_out) {
|
||||
/* `server_ok`: tls.createServer reads ALPNProtocols, minVersion and
|
||||
* maxVersion itself; the walk's other callers keep their fences. */
|
||||
static bool scr_tls_srv_opts_walk_ex(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
|
||||
ScrBytes **key_out, bool server_ok) {
|
||||
*cert_out = NULL;
|
||||
*key_out = NULL;
|
||||
if (opts == NULL || opts->kind != SCR_DYN_OBJ) {
|
||||
@@ -1770,6 +1946,9 @@ bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_
|
||||
if (strcmp(e->key, "rejectUnauthorized") == 0) {
|
||||
continue; /* meaningful only with requestCert, which fences */
|
||||
}
|
||||
if (server_ok && (strcmp(e->key, "ALPNProtocols") == 0 || strcmp(e->key, "minVersion") == 0 ||
|
||||
strcmp(e->key, "maxVersion") == 0))
|
||||
continue;
|
||||
if (strcmp(e->key, "SNICallback") == 0) {
|
||||
scr_tls_opt_fence(api, "SNICallback",
|
||||
"SNICallback lowers only on http2.createSecureServer({ allowHTTP1: true, ... }) with a literal options object");
|
||||
@@ -1803,6 +1982,11 @@ bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
|
||||
ScrBytes **key_out) {
|
||||
return scr_tls_srv_opts_walk_ex(opts, api, cert_out, key_out, false);
|
||||
}
|
||||
|
||||
/* createSecureContext over a RUNTIME options record: Node's typed
|
||||
* validations first (scr_tls_opts_validate), then the pem walk — a bag
|
||||
* that validates AND carries both cert and key builds the real context;
|
||||
@@ -1845,13 +2029,29 @@ void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence) {
|
||||
ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/,
|
||||
ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn) {
|
||||
ScrBytes *cert, *key;
|
||||
if (!scr_tls_srv_opts_walk(opts, "tls.createServer", &cert, &key)) {
|
||||
int min_version = 12, max_version = 13;
|
||||
if (!scr_tls_versions_dyn(opts, &min_version, &max_version)) {
|
||||
scr_closure_release(handler);
|
||||
return NULL;
|
||||
}
|
||||
ScrNetServer *s = scr_tls_create_server((const char *)cert->data, cert->len,
|
||||
(const char *)key->data, key->len, handler, fn);
|
||||
ScrBytes *cert, *key;
|
||||
if (!scr_tls_srv_opts_walk_ex(opts, "tls.createServer", &cert, &key, true)) {
|
||||
scr_closure_release(handler);
|
||||
return NULL;
|
||||
}
|
||||
/* The Server constructor converts ALPNProtocols after the secure
|
||||
* context validates (Node's order). */
|
||||
char **alpn = NULL;
|
||||
const ScrDyn *protocols = scr_dyn_obj_get(opts, "ALPNProtocols", 13);
|
||||
if (protocols != NULL && !scr_tls_alpn_dyn(protocols, &alpn)) {
|
||||
scr_bytes_release(cert);
|
||||
scr_bytes_release(key);
|
||||
scr_closure_release(handler);
|
||||
return NULL;
|
||||
}
|
||||
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);
|
||||
scr_bytes_release(cert);
|
||||
scr_bytes_release(key);
|
||||
return s;
|
||||
@@ -1896,20 +2096,157 @@ ScrNetServer *scr_https_create_server_dyn(const ScrDyn *opts /*borrowed*/,
|
||||
|
||||
/* ── tls.connect (the TLS client socket) ──────────────────────────────── */
|
||||
|
||||
/* Node's inspect of a Buffer inside ERR_INVALID_ARG_VALUE's message:
|
||||
* "<Buffer 00 01 ...>" with at most 50 bytes, the whole rendering cut at
|
||||
* 128 characters with "...". */
|
||||
static void scr_tls_alpn_value_fail(const char *reason, const uint8_t *data, size_t len) {
|
||||
char inspected[400];
|
||||
size_t n = (size_t)snprintf(inspected, sizeof inspected, "<Buffer");
|
||||
size_t shown = len < 50 ? len : 50;
|
||||
for (size_t i = 0; i < shown; i++) n += (size_t)snprintf(inspected + n, sizeof inspected - n, " %02x", data[i]);
|
||||
if (len > shown) n += (size_t)snprintf(inspected + n, sizeof inspected - n, " ... %zu more byte%s", len - shown, len - shown == 1 ? "" : "s");
|
||||
snprintf(inspected + n, sizeof inspected - n, ">");
|
||||
if (strlen(inspected) > 128) strcpy(inspected + 128, "...");
|
||||
char msg[600];
|
||||
int m = snprintf(msg, sizeof msg, "The argument 'ALPNProtocols' %s. Received %s", reason, inspected);
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)m, "ERR_INVALID_ARG_VALUE");
|
||||
}
|
||||
|
||||
/* The wire list (<len><name>...) as mbedTLS's NULL-terminated names, after
|
||||
* Node's validateALPNBuffer. An empty list offers nothing (*out stays
|
||||
* NULL). False with the exception pending. */
|
||||
static bool scr_tls_alpn_from_wire(const uint8_t *data, size_t len, char ***out) {
|
||||
size_t count = 0;
|
||||
for (size_t at = 0; at < len;) {
|
||||
size_t n = data[at];
|
||||
if (n == 0) {
|
||||
scr_tls_alpn_value_fail("must not contain zero-length protocol", data, len);
|
||||
return false;
|
||||
}
|
||||
if (at + 1 + n > len) {
|
||||
scr_tls_alpn_value_fail("contains truncated protocol", data, len);
|
||||
return false;
|
||||
}
|
||||
at += 1 + n;
|
||||
count++;
|
||||
}
|
||||
if (count == 0) return true;
|
||||
char **names = calloc(count + 1, sizeof *names);
|
||||
if (!names) scr_tls_oom();
|
||||
size_t k = 0;
|
||||
for (size_t at = 0; at < len; at += 1 + data[at]) {
|
||||
names[k] = malloc((size_t)data[at] + 1);
|
||||
if (!names[k]) scr_tls_oom();
|
||||
memcpy(names[k], data + at + 1, data[at]);
|
||||
names[k][data[at]] = '\0';
|
||||
k++;
|
||||
}
|
||||
*out = names;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* tls.connect's ALPNProtocols (Node's convertALPNProtocols, then
|
||||
* setALPNProtocols): an array of non-empty strings of at most 255 bytes, a
|
||||
* Uint8Array or other ArrayBufferView in the wire format, or a falsy value
|
||||
* (no ALPN). Any other truthy value is the binding's TypeError. False with
|
||||
* the exception pending. */
|
||||
static bool scr_tls_alpn_dyn(const ScrDyn *v, char ***out) {
|
||||
*out = NULL;
|
||||
ScrDyn *own = NULL;
|
||||
if (v->kind == SCR_DYN_TYPED_REF) {
|
||||
own = scr_dyn_typed_ref_materialize(v);
|
||||
if (own == NULL) return false;
|
||||
v = own;
|
||||
}
|
||||
bool ok = true;
|
||||
if (v->kind == SCR_DYN_ARR) {
|
||||
/* convertProtocols: Buffer.byteLength of every entry first (strings,
|
||||
* or the ArrayBuffer and view entries byteLength also measures), then
|
||||
* each length's checks, then buff.write, which takes strings only. */
|
||||
size_t count = v->v.arr.len;
|
||||
size_t total = 0;
|
||||
for (size_t i = 0; ok && i < count; i++) {
|
||||
const ScrDyn *e = v->v.arr.items[i];
|
||||
size_t n;
|
||||
if (e->kind == SCR_DYN_STR) {
|
||||
n = e->v.str->len;
|
||||
} else if (e->kind == SCR_DYN_BYTES || scr_buffer_storage_is(e)) {
|
||||
ScrBytes *view = scr_bytes_buffer_source(e);
|
||||
if (view == NULL) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
n = view->len;
|
||||
scr_bytes_release(view);
|
||||
} else {
|
||||
scr_dyn_arg_type_fail("string", "of type string or an instance of Buffer or ArrayBuffer", e);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (n == 0) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof name, "protocols[%zu]", i);
|
||||
scr_dyn_arg_value_fail(name, "must be a non-empty string", e);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (n > 255) {
|
||||
char msg[160];
|
||||
int m = snprintf(msg, sizeof msg,
|
||||
"The byte length of the protocol at index %zu exceeds the maximum length. It must be <= 255. Received %zu",
|
||||
i, n);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)m, "ERR_OUT_OF_RANGE");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
total += 1 + n;
|
||||
}
|
||||
uint8_t *wire = ok ? malloc(total + 1) : NULL;
|
||||
if (ok && !wire) scr_tls_oom();
|
||||
size_t at = 0;
|
||||
for (size_t i = 0; ok && i < count; i++) {
|
||||
const ScrDyn *e = v->v.arr.items[i];
|
||||
if (e->kind != SCR_DYN_STR) {
|
||||
static const char msg[] = "argument must be a string";
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
wire[at++] = (uint8_t)e->v.str->len;
|
||||
memcpy(wire + at, e->v.str->data, e->v.str->len);
|
||||
at += e->v.str->len;
|
||||
}
|
||||
if (ok) ok = scr_tls_alpn_from_wire(wire, at, out);
|
||||
free(wire);
|
||||
} else if (v->kind == SCR_DYN_BYTES) {
|
||||
ScrBytes *view = scr_bytes_buffer_source(v);
|
||||
if (view == NULL) ok = false;
|
||||
else {
|
||||
ok = scr_tls_alpn_from_wire(view->data, view->len, out);
|
||||
scr_bytes_release(view);
|
||||
}
|
||||
} else if (scr_tls_dyn_truthy(v)) {
|
||||
static const char msg[] = "Must give a Buffer as first argument";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
ok = false;
|
||||
}
|
||||
scr_dyn_release(own);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* tls.connect's documented keys outside the implemented set: present
|
||||
* throws (the server walk's stance). */
|
||||
static const char *const SCR_TLS_CONNECT_FENCED_OPTIONS[] = {
|
||||
"ALPNProtocols", "allowHalfOpen", "autoSelectFamily",
|
||||
"allowHalfOpen", "autoSelectFamily",
|
||||
"autoSelectFamilyAttemptTimeout", "cert", "certificateCompression", "checkServerIdentity",
|
||||
"ciphers", "clientCertEngine", "crl", "dhparam", "ecdhCurve",
|
||||
"enableTrace", "family", "fd", "hints", "highWaterMark",
|
||||
"honorCipherOrder", "keepAlive", "keepAliveInitialDelay", "key",
|
||||
"localAddress", "localPort", "lookup", "maxVersion", "minDHSize",
|
||||
"minVersion", "noDelay", "onread", "passphrase", "path", "pfx",
|
||||
"localAddress", "localPort", "lookup", "minDHSize", "noDelay", "onread", "passphrase", "path", "pfx",
|
||||
"privateKeyEngine", "privateKeyIdentifier", "pskCallback", "readable",
|
||||
"secureContext", "secureOptions", "secureProtocol", "session",
|
||||
"sessionIdContext", "sigalgs", "signal", "socket", "ticketKeys",
|
||||
"timeout", "writable", NULL,
|
||||
"writable", NULL,
|
||||
};
|
||||
|
||||
ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable*/,
|
||||
@@ -1920,12 +2257,19 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
ScrBytes *ca = NULL;
|
||||
const ScrStr *servername = NULL;
|
||||
const ScrStr *opt_host = NULL;
|
||||
char **alpn = NULL;
|
||||
const ScrDyn *alpn_value = NULL;
|
||||
const ScrDyn *timeout_value = NULL;
|
||||
int min_version = 12, max_version = 13;
|
||||
bool ok = true;
|
||||
if (opts != NULL && opts->kind != SCR_DYN_UNDEF && opts->kind != SCR_DYN_NULL) {
|
||||
if (opts->kind != SCR_DYN_OBJ) {
|
||||
scr_dyn_arg_type_fail("options", "of type object", opts);
|
||||
ok = false;
|
||||
}
|
||||
/* createSecureContext's version checks come before its other options
|
||||
* (Node's SecureContext constructor). */
|
||||
if (ok && !scr_tls_versions_dyn(opts, &min_version, &max_version)) ok = false;
|
||||
for (size_t i = 0; ok && i < opts->v.obj.len; i++) {
|
||||
const ScrDynEntry *e = &opts->v.obj.entries[i];
|
||||
const ScrDyn *v = e->value;
|
||||
@@ -1974,6 +2318,16 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
if (ca == NULL) ok = false;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(e->key, "ALPNProtocols") == 0) {
|
||||
alpn_value = v; /* converted after the context options, as TLSSocket does */
|
||||
continue;
|
||||
}
|
||||
if (strcmp(e->key, "minVersion") == 0 || strcmp(e->key, "maxVersion") == 0) continue;
|
||||
if (strcmp(e->key, "timeout") == 0) {
|
||||
/* `if (options.timeout) tlssock.setTimeout(options.timeout)` */
|
||||
timeout_value = scr_tls_dyn_truthy(v) ? v : NULL;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(e->key, "servername") == 0) {
|
||||
if (v->kind == SCR_DYN_STR) servername = v->v.str;
|
||||
else {
|
||||
@@ -1985,7 +2339,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
for (size_t k = 0; SCR_TLS_CONNECT_FENCED_OPTIONS[k] != NULL; k++) {
|
||||
if (strcmp(e->key, SCR_TLS_CONNECT_FENCED_OPTIONS[k]) == 0) {
|
||||
scr_tls_opt_fence("tls.connect", e->key,
|
||||
"port/host/rejectUnauthorized/ca/servername are the honestly-implemented members of a runtime options record");
|
||||
"port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout are the implemented members of a runtime options record");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
@@ -1993,6 +2347,26 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
/* anything else: an undocumented key, dropped like Node drops it */
|
||||
}
|
||||
}
|
||||
if (ok && alpn_value != NULL && !scr_tls_alpn_dyn(alpn_value, &alpn)) ok = false;
|
||||
/* socket.setTimeout(msecs) runs after the TLSSocket exists and before it
|
||||
* connects: Node's msecs checks, then the inactivity timer below. */
|
||||
double timeout_ms = 0;
|
||||
if (ok && timeout_value != NULL) {
|
||||
if (timeout_value->kind != SCR_DYN_NUM) {
|
||||
scr_dyn_arg_type_fail("msecs", "of type number", timeout_value);
|
||||
ok = false;
|
||||
} else if (!isfinite(timeout_value->v.num) || timeout_value->v.num < 0) {
|
||||
char recv[48], msg[160];
|
||||
scr_num_received(timeout_value->v.num, recv);
|
||||
int m = snprintf(msg, sizeof msg,
|
||||
"The value of \"msecs\" is out of range. It must be a non-negative finite number. Received %s",
|
||||
recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)m, "ERR_OUT_OF_RANGE");
|
||||
ok = false;
|
||||
} else {
|
||||
timeout_ms = timeout_value->v.num;
|
||||
}
|
||||
}
|
||||
if (ok && port < 0) {
|
||||
scr_dyn_arg_type_fail("options.port", "of type number", scr_dyn_undefined());
|
||||
ok = false;
|
||||
@@ -2000,6 +2374,7 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
ScrStr *dial_host = (ScrStr *)(host != NULL && host->len > 0 ? host : opt_host);
|
||||
if (ok && !scr_net_host_chk(dial_host)) ok = false;
|
||||
if (!ok) {
|
||||
scr_tls_alpn_free(alpn);
|
||||
scr_bytes_release(ca);
|
||||
scr_closure_release(cb);
|
||||
return NULL;
|
||||
@@ -2022,7 +2397,13 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
ca != NULL ? (const char *)ca->data : NULL,
|
||||
ca != NULL ? ca->len : 0, !reject);
|
||||
if (servername != NULL) scr_tls_cli_set_servername(cli, servername);
|
||||
if (alpn != NULL) {
|
||||
cli->alpn = alpn; /* moves */
|
||||
mbedtls_ssl_conf_alpn_protocols(&cli->conf, (const char **)cli->alpn);
|
||||
}
|
||||
cli->no_protocols = !scr_tls_conf_versions(&cli->conf, min_version, max_version);
|
||||
scr_tls_cli_wrap(s, cli);
|
||||
if (timeout_ms > 0) scr_net_sock_set_timeout(s, timeout_ms);
|
||||
scr_str_release(localhost_fallback);
|
||||
scr_bytes_release(ca);
|
||||
return s;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// tls.connect and tls.createServer with ALPNProtocols: the server answers
|
||||
// with the first of its own protocols the client offers, both sockets
|
||||
// report it as alpnProtocol, and a client that offers none of the
|
||||
// server's fails with ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL. An
|
||||
// array of strings, a Buffer or a Uint8Array in the wire format, an empty
|
||||
// list and a falsy value all work as in Node, and invalid values throw
|
||||
// Node's errors from tls.connect.
|
||||
import * as tls from "node:tls";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const dir = "tests/fixtures/server/certs/";
|
||||
const ca = readFileSync(dir + "ca.pem");
|
||||
const server = tls.createServer(
|
||||
{
|
||||
cert: readFileSync(dir + "localhost.pem"),
|
||||
key: readFileSync(dir + "localhost-key.pem"),
|
||||
ALPNProtocols: ["h2", "x/1"],
|
||||
},
|
||||
(socket) => {
|
||||
socket.end(`server:${String(socket.alpnProtocol)}`);
|
||||
},
|
||||
);
|
||||
|
||||
function attempt(port: number, label: string, protocols: unknown): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
const socket = tls.connect({ host: "localhost", port, ca, ALPNProtocols: protocols as any }, () => {
|
||||
console.log(label, "client:", JSON.stringify(socket.alpnProtocol));
|
||||
});
|
||||
let data = "";
|
||||
socket.on("data", (chunk: Buffer) => (data += chunk.toString()));
|
||||
socket.on("end", () => {
|
||||
console.log(label, data);
|
||||
resolve();
|
||||
});
|
||||
socket.on("error", (err) => {
|
||||
console.log(label, "error:", (err as NodeJS.ErrnoException).code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function invalid(label: string, protocols: unknown): void {
|
||||
try {
|
||||
const socket = tls.connect({ host: "127.0.0.1", port: 1, ALPNProtocols: protocols as any });
|
||||
socket.on("error", () => {});
|
||||
socket.destroy();
|
||||
console.log(label, "accepted");
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log(label, err.name, err.code, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const port = (server.address() as { port: number }).port;
|
||||
await attempt(port, "strings", ["x/1", "h2"]);
|
||||
await attempt(port, "only-second", ["zz", "x/1"]);
|
||||
await attempt(port, "buffer", Buffer.from([2, 0x68, 0x32]));
|
||||
await attempt(port, "uint8array", new Uint8Array([3, 0x78, 0x2f, 0x31]));
|
||||
await attempt(port, "absent", undefined);
|
||||
await attempt(port, "empty", []);
|
||||
await attempt(port, "falsy", 0);
|
||||
await attempt(port, "no-match", ["zz"]);
|
||||
server.close();
|
||||
|
||||
invalid("string", "h2");
|
||||
invalid("object", {});
|
||||
invalid("buffer-element", [Buffer.from("h2")]);
|
||||
invalid("number-element", [1]);
|
||||
invalid("empty-element", ["h2", ""]);
|
||||
invalid("too-long", ["a".repeat(256)]);
|
||||
invalid("zero-length", Buffer.from([0]));
|
||||
invalid("truncated", Buffer.from([5, 0x68]));
|
||||
invalid("long-zero", Buffer.alloc(60));
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
// tls.connect and tls.createServer with minVersion and maxVersion: the
|
||||
// handshake picks the highest version both ranges allow and getProtocol()
|
||||
// reports it. A range the server does not share fails with
|
||||
// ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION, a client range with neither TLSv1.2
|
||||
// nor TLSv1.3 fails with ERR_SSL_NO_PROTOCOLS_AVAILABLE once the handshake
|
||||
// starts, and an invalid version or a secureProtocol conflict throws Node's
|
||||
// TypeError from tls.connect.
|
||||
import * as tls from "node:tls";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const dir = "tests/fixtures/server/certs/";
|
||||
const ca = readFileSync(dir + "ca.pem");
|
||||
const cert = readFileSync(dir + "localhost.pem");
|
||||
const key = readFileSync(dir + "localhost-key.pem");
|
||||
|
||||
const upTo12 = tls.createServer({ cert, key, maxVersion: "TLSv1.2" }, (s) => s.end(`server:${s.getProtocol()}`));
|
||||
const from13 = tls.createServer({ cert, key, minVersion: "TLSv1.3" }, (s) => s.end(`server:${s.getProtocol()}`));
|
||||
const any = tls.createServer({ cert, key }, (s) => s.end(`server:${s.getProtocol()}`));
|
||||
|
||||
function listen(server: tls.Server): Promise<number> {
|
||||
return new Promise<number>((resolve) =>
|
||||
server.listen(0, "127.0.0.1", () => resolve((server.address() as { port: number }).port)),
|
||||
);
|
||||
}
|
||||
|
||||
interface Versions {
|
||||
minVersion?: unknown;
|
||||
maxVersion?: unknown;
|
||||
secureProtocol?: string;
|
||||
}
|
||||
|
||||
function attempt(label: string, port: number, versions: Versions): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
let socket: tls.TLSSocket;
|
||||
try {
|
||||
const options = {
|
||||
host: "localhost",
|
||||
port,
|
||||
ca,
|
||||
minVersion: versions.minVersion as any,
|
||||
maxVersion: versions.maxVersion as any,
|
||||
secureProtocol: versions.secureProtocol,
|
||||
};
|
||||
socket = tls.connect(options, () => console.log(label, "client:", socket.getProtocol()));
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log(label, err.name, err.code, err.message);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let data = "";
|
||||
socket.on("data", (chunk: Buffer) => (data += chunk.toString()));
|
||||
socket.on("end", () => {
|
||||
console.log(label, data);
|
||||
resolve();
|
||||
});
|
||||
socket.on("error", (err) => {
|
||||
console.log(label, "error:", (err as NodeJS.ErrnoException).code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const p12 = await listen(upTo12);
|
||||
const p13 = await listen(from13);
|
||||
const pAny = await listen(any);
|
||||
await attempt("defaults", pAny, {});
|
||||
await attempt("max-1.2", pAny, { maxVersion: "TLSv1.2" });
|
||||
await attempt("min-1.3", pAny, { minVersion: "TLSv1.3" });
|
||||
await attempt("min-1-against-1.2", p12, { minVersion: "TLSv1" });
|
||||
await attempt("min-1.3-against-1.2", p12, { minVersion: "TLSv1.3" });
|
||||
await attempt("max-1.2-against-1.3", p13, { maxVersion: "TLSv1.2" });
|
||||
await attempt("max-1.1", pAny, { maxVersion: "TLSv1.1" });
|
||||
await attempt("min-above-max", pAny, { minVersion: "TLSv1.3", maxVersion: "TLSv1.2" });
|
||||
await attempt("null-min", pAny, { minVersion: null });
|
||||
await attempt("invalid-min", pAny, { minVersion: "TLSv9" });
|
||||
await attempt("invalid-max", pAny, { maxVersion: 3 });
|
||||
await attempt("conflict", pAny, { minVersion: "TLSv1.2", secureProtocol: "TLSv1_2_method" });
|
||||
upTo12.close();
|
||||
from13.close();
|
||||
any.close();
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,51 @@
|
||||
// tls.connect's timeout option arms the socket's inactivity timeout
|
||||
// before it connects, so a handshake the peer never answers emits
|
||||
// 'timeout'. Node applies it only when truthy, so 0 and NaN arm nothing,
|
||||
// and setTimeout's msecs checks throw from tls.connect for a value that is
|
||||
// not a number, negative, or not finite.
|
||||
import * as net from "node:net";
|
||||
import * as tls from "node:tls";
|
||||
|
||||
// The server reads (and ignores) what arrives, so each connection closes
|
||||
// when its client is destroyed.
|
||||
const silent = net.createServer((socket) => {
|
||||
socket.resume();
|
||||
});
|
||||
|
||||
function attempt(port: number, label: string, timeout: unknown): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
let socket: tls.TLSSocket;
|
||||
try {
|
||||
socket = tls.connect({ host: "127.0.0.1", port, timeout: timeout as any });
|
||||
} catch (e) {
|
||||
const err = e as NodeJS.ErrnoException;
|
||||
console.log(label, err.name, err.code, err.message);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const fallback = setTimeout(() => {
|
||||
console.log(label, "no timeout event");
|
||||
socket.destroy();
|
||||
resolve();
|
||||
}, 300);
|
||||
socket.on("timeout", () => {
|
||||
console.log(label, "timeout");
|
||||
clearTimeout(fallback);
|
||||
socket.destroy();
|
||||
resolve();
|
||||
});
|
||||
socket.on("error", (err) => console.log(label, "error", (err as NodeJS.ErrnoException).code));
|
||||
});
|
||||
}
|
||||
|
||||
silent.listen(0, "127.0.0.1", async () => {
|
||||
const port = (silent.address() as net.AddressInfo).port;
|
||||
await attempt(port, "50ms", 50);
|
||||
await attempt(port, "fractional", 30.7);
|
||||
await attempt(port, "zero", 0);
|
||||
await attempt(port, "nan", NaN);
|
||||
await attempt(port, "negative", -1);
|
||||
await attempt(port, "infinite", Infinity);
|
||||
await attempt(port, "string", "x");
|
||||
silent.close();
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4300, "san": 60600 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3000, "san": 4700 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6200, "san": 60300 }
|
||||
Reference in New Issue
Block a user