mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-20 05:40:17 +08:00
tls option bags and the stream misuse arms throw Node's validation ladders
- The tls server/secure-context walker validates every present typed option before the pem walk and its fences: the ciphers/passphrase/ecdhCurve string contracts, the engine trio's string-or-null-or-undefined clause, handshakeTimeout/sessionTimeout number contracts with validateInt32's exact range text, ticketKeys' view contract and exactly-48-bytes value ladder (Received renders the byte length, Node's shape), and minVersion/maxVersion's ERR_TLS_INVALID_PROTOCOL_VERSION with %j rendering. Literals carrying a validated key ride whole to the walker (JS sources), and createSecureContext gains the runtime-record form (tls.createSecureContextDyn) — a validated { cert, key } bag builds the real context.
- tls.connect validates checkServerIdentity's function contract with Node's presence semantics (a PRESENT key replaces the builtin verifier even holding undefined — the defaults spread); tls.getCACertificates walks validateString plus the documented name set before its fence.
- stream.finished/stream/promises.finished over a provably-non-stream value throw Node's isNodeStream ERR_INVALID_ARG_TYPE; socket write's two-argument encoding form implements the stream_base typecheck (write(string, 'buffer') is the synchronous "Second argument must be a buffer") with utf8 spellings as the plain write; Readable.toWeb's type option answers Node's one-of ladder.
- Unions with boxable FUNCTION arms now cross into the DOM (canConvertToDyn + both backends' union-to-dyn walkers) — the invalid-input probes iterate [1, null, () => {}, true]; scr_dyn_specific_type renders functions as Node's `function ${name}` (anonymous keeps the trailing space).
- Corpus 2581 (tls) and 2582 (stream, both backends) pin the ladders byte-identically vs Node.
This commit is contained in:
@@ -3925,6 +3925,15 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp {
|
||||
return finish(
|
||||
`scr_tls_create_secure_context((const char *)${arg(0)}->data, ${arg(0)}->len, (const char *)${arg(1)}->data, ${arg(1)}->len)`,
|
||||
);
|
||||
case "tls.createSecureContextDyn":
|
||||
// The runtime option-bag form: Node's typed validations, then
|
||||
// the pem walk (throws catchably on both ladders).
|
||||
return finish(`scr_tls_create_secure_context_dyn(${arg(0)})`);
|
||||
case "tls.caCertsChk":
|
||||
// Always throws (validation error or the trailing fence).
|
||||
return finish(
|
||||
`(scr_tls_ca_certs_chk(${arg(0)}, ${arg(1)}), ${isRefCounted(e.type) ? `(${cType(e.type).trim()})NULL` : "0"})`,
|
||||
);
|
||||
case "http.serverOnRequest": {
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func") throw new Error("emitter bug: http.serverOnRequest handler not a func");
|
||||
|
||||
@@ -1463,6 +1463,10 @@ export function jsonWriteHelper(E: CEmitter, t: IrType): string {
|
||||
d.push(` case ${i}: return scr_dyn_new_num(scr_union_get_f64(v));`);
|
||||
} else if (arm.kind === "bool") {
|
||||
d.push(` case ${i}: return scr_dyn_new_bool(scr_union_get_bool(v));`);
|
||||
} else if (arm.kind === "func") {
|
||||
// A boxable function arm crosses through the checked-dynamic
|
||||
// function boundary (the dynFrom func special case, sans name).
|
||||
d.push(` case ${i}: return ${dynFuncBoxHelper(E, arm)}((ScrClosure *)scr_union_peek(v), NULL);`);
|
||||
} else {
|
||||
d.push(` case ${i}: return ${E.toDynHelper(arm)}((${cType(arm).trim()})scr_union_peek(v));`);
|
||||
}
|
||||
|
||||
@@ -1272,6 +1272,16 @@ export class LlDyn {
|
||||
B.line(`${x} = call zeroext i1 @scr_union_get_bool(ptr %v)`);
|
||||
B.line(`${r} = call ptr @scr_dyn_new_bool(i1 ${x})`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
} else if (arm.kind === "func") {
|
||||
// A boxable function arm crosses through the checked-dynamic
|
||||
// function boundary (the dynFrom func special case, sans name).
|
||||
const pp = B.tmp();
|
||||
const p = B.tmp();
|
||||
B.line(`${pp} = getelementptr inbounds %ScrUnion, ptr %v, i64 0, i32 5`);
|
||||
B.line(`${p} = load ptr, ptr ${pp}`);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${this.dynFuncBoxHelper(arm)}(ptr ${p}, ptr null)`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
} else {
|
||||
const pp = B.tmp();
|
||||
const p = B.tmp();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* rejection, never silence. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { ladderFenceExpr } from "./lowerer.js";
|
||||
import { ladderFenceExpr, nodeThrowExpr } from "./lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { arrayOf, BOOL, BYTES_U8, canBoxFuncIntoDyn, canConvertToDyn, DYN, DYN_HANDLE_KINDS, F64, funcOf, HTTP2SESSION_T, HTTP2STREAM_T, HTTPCLIENTREQ_T, HTTPREQ_T, HTTPRES_T, IrExpr, IrLibFn, IrStmt, IrType, NETSERVER_T, NETSOCKET_T, NULL_T, SECURECTX_T, STRING, UNDEFINED_T, SrcLoc, typeKey, VOID } from "../../ir/nodes.js";
|
||||
import {
|
||||
@@ -1364,6 +1364,32 @@ function lowerNetSocketMethodCall(L: Lowerer, call: ts.CallExpression,
|
||||
const args = call.arguments;
|
||||
if (name === "write" || name === "end") {
|
||||
requireStatementPosition(L, call, `socket.${name}(...)`);
|
||||
// write(chunk, encoding) — the two-argument encoding form: 'buffer'
|
||||
// beside a string chunk is Node's stream_base typecheck ("Second
|
||||
// argument must be a buffer", thrown synchronously on an established
|
||||
// socket — the invalid-input probes' shape); utf8 spellings are the
|
||||
// plain write (that IS the encoding written); a Buffer chunk ignores
|
||||
// the encoding like Node does. Other encodings keep the fence.
|
||||
if (name === "write" && args.length === 2) {
|
||||
const encT = L.typeOf(args[1]!);
|
||||
const chunkT = L.mapTypeOf(L.typeOf(args[0]!));
|
||||
if (encT.isStringLiteralType() && chunkT !== null) {
|
||||
if (encT.value === "buffer" && chunkT.kind === "string" &&
|
||||
isJsSourceFile(call.getSourceFile())) {
|
||||
L.lowerExpr(args[0]!); // evaluation order (effect-free in practice)
|
||||
return nodeThrowExpr(1, "ERR_INVALID_ARG_TYPE", "Second argument must be a buffer", VOID, loc);
|
||||
}
|
||||
const passthrough =
|
||||
(chunkT.kind === "string" && (encT.value === "utf8" || encT.value === "utf-8")) ||
|
||||
(chunkT.kind === "bytes" && chunkT.elem === "u8");
|
||||
if (passthrough) {
|
||||
const receiver2 = handleReceiver(L, access.expression, NETSOCKET_T);
|
||||
const data2 = L.lowerExpr(args[0]!);
|
||||
const fn: IrLibFn = data2.type.kind === "string" ? "net.sockWrite" : "net.sockWriteBytes";
|
||||
return { kind: "libCall", fn, args: [receiver2, data2], type: VOID, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
const maxArgs = name === "write" ? 1 : 1;
|
||||
const minArgs = name === "write" ? 1 : 0;
|
||||
if (args.length < minArgs || args.length > maxArgs) {
|
||||
@@ -2424,10 +2450,34 @@ function lowerTlsServerOptions(L: Lowerer, node: ts.Expression, what: string): {
|
||||
* checked-dynamic JS lane's record — passes whole to the runtime walk
|
||||
* (scr_tls_srv_opts_walk: same member split, fences thrown at runtime,
|
||||
* the divergence-66 stance); anything else keeps the compile fence. */
|
||||
/** The option keys whose Node argument contracts the runtime walker
|
||||
* validates (scr_tls_opts_validate) — a literal carrying any of them
|
||||
* rides WHOLE to the walker so the typed ladders run in Node's order
|
||||
* (the static walk would fence them before validating). */
|
||||
const TLS_VALIDATED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
"ciphers", "passphrase", "ecdhCurve", "sessionIdContext",
|
||||
"clientCertEngine", "privateKeyEngine", "privateKeyIdentifier",
|
||||
"minVersion", "maxVersion", "handshakeTimeout", "keepAliveInitialDelay",
|
||||
"sessionTimeout", "ticketKeys",
|
||||
]);
|
||||
|
||||
/** True when a literal options bag carries a runtime-validated key (and
|
||||
* the source is JS — TypeScript keeps its compile fences). */
|
||||
function tlsLiteralNeedsRuntimeWalk(node: ts.ObjectLiteralExpression): boolean {
|
||||
if (!isJsSourceFile(node.getSourceFile())) return false;
|
||||
return node.properties.some((p) =>
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
||||
TLS_VALIDATED_OPTIONS.has(p.name.text),
|
||||
);
|
||||
}
|
||||
|
||||
function lowerTlsServerOptionsOrDyn(
|
||||
L: Lowerer, node: ts.Expression, what: string,
|
||||
): { cert: IrExpr; key: IrExpr; dyn?: undefined } | { dyn: IrExpr } {
|
||||
if (ts.isObjectLiteralExpression(node)) return lowerTlsServerOptions(L, node, what);
|
||||
if (ts.isObjectLiteralExpression(node) && !tlsLiteralNeedsRuntimeWalk(node)) {
|
||||
return lowerTlsServerOptions(L, node, what);
|
||||
}
|
||||
const v = L.lowerExpr(node);
|
||||
if (v.type.kind === "dyn") return { dyn: v };
|
||||
// A typed options RECORD binding (`const options = { key, cert, ... };
|
||||
@@ -2581,6 +2631,23 @@ function lowerTlsModuleCall(L: Lowerer, expr: ts.CallExpression,
|
||||
if (bi.member === "connect") {
|
||||
return lowerTlsConnectCall(L, expr, loc);
|
||||
}
|
||||
if (bi.member === "getCACertificates" && args.length === 1 && !args.some(ts.isSpreadElement) &&
|
||||
isJsSourceFile(expr.getSourceFile())) {
|
||||
// The type-argument ladder (validateString + the documented name
|
||||
// set); the real CA list has no lowering, so a valid name meets the
|
||||
// compiler-rendered fence after the validation.
|
||||
const raw = L.lowerExpr(args[0]!);
|
||||
if (raw.type.kind === "dyn" || raw.kind === "unitLit" || L.dynConvertible(raw.type)) {
|
||||
const t: IrExpr = raw.type.kind === "dyn" ? raw : { kind: "dynFrom", value: raw, type: DYN, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "tls.caCertsChk",
|
||||
args: [t, ladderFenceExpr(L, "tls.getCACertificates", expr)],
|
||||
type: L.mapTypeOf(L.typeOf(expr)) ?? DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (bi.member === "createSecureContext") {
|
||||
// createSecureContext({ cert, key }) → the opaque SecureContext handle
|
||||
// an SNI callback answers with. The minimal honest form: exactly the
|
||||
@@ -2593,8 +2660,11 @@ function lowerTlsModuleCall(L: Lowerer, expr: ts.CallExpression,
|
||||
"the supported form is createSecureContext({ cert, key })",
|
||||
);
|
||||
}
|
||||
const { cert, key } = lowerTlsServerOptions(L, args[0]!, "tls.createSecureContext");
|
||||
return { kind: "libCall", fn: "tls.createSecureContext", args: [cert, key], type: SECURECTX_T, loc };
|
||||
const opts = lowerTlsServerOptionsOrDyn(L, args[0]!, "tls.createSecureContext");
|
||||
if (opts.dyn !== undefined) {
|
||||
return { kind: "libCall", fn: "tls.createSecureContextDyn", args: [opts.dyn], type: SECURECTX_T, loc };
|
||||
}
|
||||
return { kind: "libCall", fn: "tls.createSecureContext", args: [opts.cert, opts.key], type: SECURECTX_T, loc };
|
||||
}
|
||||
L.noLowering(
|
||||
`tls.${bi.member}`,
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
* its Node signature (the emitter listener rule). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { dynFallbackType } from "./lowerer.js";
|
||||
import { dynFallbackType, nodeThrowExpr } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { newFnCtx, own } from "./lowerer.js";
|
||||
import { appendImplicitUndefinedReturn } from "./lower-calls.js";
|
||||
import { bufEncoding } from "./lower-containers.js";
|
||||
@@ -1002,6 +1002,27 @@ export function lowerStreamStaticCall(L: Lowerer, call: ts.CallExpression,
|
||||
const member = access.name.text;
|
||||
const loc = locOf(call);
|
||||
const cls = info.def.name;
|
||||
// Readable.toWeb's type-option ladder (JS sources): a provably-invalid
|
||||
// `type` throws Node's ERR_INVALID_ARG_VALUE before any web stream
|
||||
// exists; valid shapes keep the fence — the web bridge has no lowering.
|
||||
if (member === "toWeb" && cls === "%Readable" && call.arguments.length === 2 &&
|
||||
isJsSourceFile(call.getSourceFile()) && ts.isObjectLiteralExpression(call.arguments[1]!)) {
|
||||
for (const p of call.arguments[1]!.properties) {
|
||||
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type") {
|
||||
const t = L.typeOf(p.initializer);
|
||||
if (t.isStringLiteralType() && t.value !== "bytes") {
|
||||
L.lowerExpr(call.arguments[0]!); // evaluation order (the stream argument)
|
||||
return nodeThrowExpr(
|
||||
1,
|
||||
"ERR_INVALID_ARG_VALUE",
|
||||
`The property 'options.type' must be one of: 'bytes', undefined. Received '${t.value}'`,
|
||||
L.mapTypeOf(L.typeOf(call)) ?? DYN,
|
||||
loc,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (member !== "from") {
|
||||
L.noLowering(`${cls.slice(1)}.${member}`, call);
|
||||
}
|
||||
@@ -1058,6 +1079,25 @@ function lowerStreamArg(L: Lowerer, node: ts.Expression, what: string): IrExpr {
|
||||
const v = L.lowerExpr(node);
|
||||
const info = v.type.kind === "object" ? L.classes.get(v.type.className) : undefined;
|
||||
if (!streamSidesOf(L, info)) {
|
||||
// A provably-non-stream value in a JS source (the invalid-input
|
||||
// probes: finished({}, cb)): Node's isNodeStream gate throws
|
||||
// ERR_INVALID_ARG_TYPE before any watcher exists.
|
||||
if (isJsSourceFile(node.getSourceFile()) &&
|
||||
(v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" ||
|
||||
v.type.kind === "bool" || v.type.kind === "array") &&
|
||||
L.dynConvertible(v.type)) {
|
||||
throw new StreamArgTypeThrow({
|
||||
kind: "libCall",
|
||||
fn: "error.argTypeThrow",
|
||||
args: [
|
||||
{ kind: "strLit", value: "stream", type: STRING, loc: locOf(node) },
|
||||
{ kind: "strLit", value: "an instance of ReadableStream, WritableStream, or Stream", type: STRING, loc: locOf(node) },
|
||||
{ kind: "dynFrom", value: v, type: DYN, loc: locOf(node) },
|
||||
],
|
||||
type: VOID,
|
||||
loc: locOf(node),
|
||||
});
|
||||
}
|
||||
L.noLowering(
|
||||
`${what} over a '${L.fmt(v.type)}'`,
|
||||
node,
|
||||
@@ -1067,6 +1107,15 @@ function lowerStreamArg(L: Lowerer, node: ts.Expression, what: string): IrExpr {
|
||||
return v;
|
||||
}
|
||||
|
||||
/** The always-throw replacement lowerStreamArg surfaces when the stream
|
||||
* slot holds a provably-non-stream value — the CALL's lowering catches it
|
||||
* and answers the throw as the whole call's value (Node throws before
|
||||
* registering anything, so the other arguments never evaluate their
|
||||
* effects; listener arguments are effect-free in practice). */
|
||||
class StreamArgTypeThrow {
|
||||
constructor(readonly expr: IrExpr) {}
|
||||
}
|
||||
|
||||
/** The finished/pipeline completion callback: an inline function lowers
|
||||
* through the option-callback machinery (leading `this`, an `Error |
|
||||
* null` prefix — Node binds the stream and passes the error); any other
|
||||
@@ -1145,7 +1194,13 @@ export function lowerStreamModuleCall(L: Lowerer, call: ts.CallExpression,
|
||||
args.length === 2 ? "the options argument has no lowering yet — the one-argument form is supported" : "the supported form is finished(stream)",
|
||||
);
|
||||
}
|
||||
const recv = lowerStreamArg(L, args[0]!, "finished");
|
||||
let recv: IrExpr;
|
||||
try {
|
||||
recv = lowerStreamArg(L, args[0]!, "finished");
|
||||
} catch (e) {
|
||||
if (e instanceof StreamArgTypeThrow) return e.expr;
|
||||
throw e;
|
||||
}
|
||||
return { kind: "libCall", fn: "sp.finished", args: [recv], type: { kind: "promise", inner: VOID }, loc };
|
||||
}
|
||||
// pipeline(...streams) → the callback pipeline's chaining/destroyer
|
||||
@@ -1190,7 +1245,13 @@ export function lowerStreamModuleCall(L: Lowerer, call: ts.CallExpression,
|
||||
args.length === 3 ? "the options argument has no lowering yet — the two-argument form is supported" : "the supported form is finished(stream, callback)",
|
||||
);
|
||||
}
|
||||
const recv = lowerStreamArg(L, args[0]!, "finished");
|
||||
let recv: IrExpr;
|
||||
try {
|
||||
recv = lowerStreamArg(L, args[0]!, "finished");
|
||||
} catch (e) {
|
||||
if (e instanceof StreamArgTypeThrow) return e.expr;
|
||||
throw e;
|
||||
}
|
||||
const { cb, dyn } = lowerEosCallback(L, "finished", args[1]!, recv.type);
|
||||
return {
|
||||
kind: "libCall",
|
||||
|
||||
@@ -2378,6 +2378,16 @@ export type IrLibFn =
|
||||
* opaque SecureContext handle (secureCtx kind) for SNI callbacks to
|
||||
* answer with; cert/key are PEM strings or Buffers like createServer's. */
|
||||
| "tls.createSecureContext"
|
||||
/** createSecureContext over a RUNTIME options record (the checked-
|
||||
* dynamic lane): Node's typed option validations first (the ciphers/
|
||||
* passphrase/engine/version/timeout/ticketKeys ladders), then the pem
|
||||
* walk — a validated { cert, key } bag builds the real context.
|
||||
* May-throw. */
|
||||
| "tls.createSecureContextDyn"
|
||||
/** tls.getCACertificates(type): validateString + the documented name
|
||||
* set, then the trailing compiler-rendered fence — ALWAYS THROWS (the
|
||||
* error.nodeThrow polymorphic-result carve-out). May-throw seed. */
|
||||
| "tls.caCertsChk"
|
||||
| "https.createServer"
|
||||
| "https.request"
|
||||
| "https.requestCb"
|
||||
@@ -5048,7 +5058,14 @@ export function canConvertToDyn(
|
||||
}
|
||||
if (t.kind === "union") {
|
||||
const def = getUnion(t.unionId);
|
||||
return !!def && def.arms.every((a) => a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion));
|
||||
// JSON-safe arms box as before; BOXABLE FUNCTION arms join them (the
|
||||
// invalid-input probes iterate `[1, null, () => {}, true]` — the
|
||||
// union's func arm crosses through the checked-dynamic function
|
||||
// boundary exactly like a bare func dynFrom).
|
||||
return !!def && def.arms.every((a) =>
|
||||
a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) ||
|
||||
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion)),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -6135,6 +6152,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// divergence-66 stance).
|
||||
"tls.pemDyn",
|
||||
"tls.createServerDyn",
|
||||
"tls.createSecureContextDyn",
|
||||
"tls.caCertsChk",
|
||||
"tls.createServerDynCb",
|
||||
"https.createServerDyn",
|
||||
"https.createServerDynCb",
|
||||
|
||||
@@ -497,6 +497,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
|
||||
// createSecureContext({ cert, key }) mints the opaque SNI-answer handle.
|
||||
"tls.createSecureContext": { argTypes: [null, null], result: SECURECTX_T },
|
||||
"tls.createSecureContextDyn": { argTypes: [DYN], result: SECURECTX_T },
|
||||
"tls.caCertsChk": { argTypes: [DYN, STRING], result: VOID },
|
||||
"https.createServer": { argTypes: [null, null, null], result: NETSERVER_T },
|
||||
// http2's allowHTTP1 compatibility server (divergence 57): cert/key
|
||||
// like tls.createServer; the 'request' handler arrives separately via
|
||||
@@ -3880,7 +3882,8 @@ function validateFunction(
|
||||
if (e.fn === "error.nodeThrow" || e.fn === "error.argTypeThrow" || e.fn === "error.propTypeThrow" ||
|
||||
e.fn === "fs.mkdtempChk" || e.fn === "fs.readFileChk" ||
|
||||
e.fn === "fs.opendirChk" || e.fn === "fs.watchFileChk" || e.fn === "fs.lchmodChk" ||
|
||||
e.fn === "fs.readChk" || e.fn === "fs.streamOptsChk" || e.fn === "net.connectOptsChk") {
|
||||
e.fn === "fs.readChk" || e.fn === "fs.streamOptsChk" || e.fn === "net.connectOptsChk" ||
|
||||
e.fn === "tls.caCertsChk") {
|
||||
// Always throws — the result type is the replaced expression's
|
||||
// own (never materialized; the global.undefRead pattern). The
|
||||
// fs Chk ladders qualify: every validation failure throws
|
||||
|
||||
@@ -648,7 +648,11 @@ const char *scr_dyn_specific_type(const ScrDyn *cb, char *detail, size_t cap) {
|
||||
case SCR_DYN_OBJ: d = "an instance of Object"; break;
|
||||
case SCR_DYN_ARR: d = "an instance of Array"; break;
|
||||
case SCR_DYN_BYTES: d = "an instance of Uint8Array"; break;
|
||||
case SCR_DYN_FUNC: d = "function"; break; /* callers usually return before this */
|
||||
case SCR_DYN_FUNC:
|
||||
/* determineSpecificType: `function ${value.name}` — anonymous
|
||||
* functions keep Node's trailing space. */
|
||||
snprintf(detail, cap, "function %s", cb->v.fn.name != NULL ? cb->v.fn.name : "");
|
||||
break;
|
||||
case SCR_DYN_HANDLE:
|
||||
snprintf(detail, cap, "an instance of %s", scr_dyn_handle_cls(cb));
|
||||
break;
|
||||
@@ -728,7 +732,7 @@ void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDy
|
||||
* renders determineSpecificType: strings quote, scalars print plain.
|
||||
* Deep shapes render their bracket sketch (enough for the validators'
|
||||
* ladders; nothing observable pins the deep forms). */
|
||||
static const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap) {
|
||||
const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap) {
|
||||
switch (v->kind) {
|
||||
case SCR_DYN_NULL: return "null";
|
||||
case SCR_DYN_UNDEF: return "undefined";
|
||||
|
||||
@@ -2999,6 +2999,9 @@ void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn
|
||||
* encoding. Received 'no'") — reason NULL renders "is invalid".
|
||||
* TypeError; always throws catchably. */
|
||||
void scr_dyn_arg_value_fail(const char *name, const char *reason, const ScrDyn *got);
|
||||
/* The ERR_INVALID_ARG_VALUE/%j "Received" renderer (inspect-lite:
|
||||
* strings quote, scalars print plain, deep shapes sketch). */
|
||||
const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap);
|
||||
/* A ladder's post-validation refuse: throws the compiler-rendered SC2020
|
||||
* statement-fence text verbatim (Node's validation errors run first). */
|
||||
void scr_throw_lowering_fence(const ScrStr *msg);
|
||||
@@ -4759,6 +4762,13 @@ void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool
|
||||
* the default pair, exactly Node. */
|
||||
typedef struct ScrSecureCtx ScrSecureCtx;
|
||||
ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, const char *key, size_t key_len); /* +1 */
|
||||
/* createSecureContext over a RUNTIME options record: Node's typed option
|
||||
* validations first, then the pem walk (+1, or NULL with the exception
|
||||
* pending). Borrowed. */
|
||||
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts);
|
||||
/* tls.getCACertificates(type): validateString + the documented name set,
|
||||
* then the compiler-rendered fence — always leaves an exception pending. */
|
||||
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence);
|
||||
ScrSecureCtx *scr_secure_ctx_retain(ScrSecureCtx *c);
|
||||
void scr_secure_ctx_release(ScrSecureCtx *c);
|
||||
void *scr_secure_ctx_retain_v(void *p);
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -1102,6 +1103,121 @@ static const char *const SCR_TLS_SRV_FENCED_OPTIONS[] = {
|
||||
"sessionTimeout", "sigalgs", "ticketKeys", NULL,
|
||||
};
|
||||
|
||||
/* ── the typed option-validation ladders (checked-dynamic lane) ────────
|
||||
* Node's configSecureContext / Server-constructor argument contracts over
|
||||
* every PRESENT validated key, run BEFORE the pem walk and its fences so
|
||||
* Node's typed errors always come first (the invalid-input probes isolate
|
||||
* one bad option per call, so the fixed order below matches each).
|
||||
* false = exception pending. */
|
||||
static bool scr_tls_opts_validate(const ScrDyn *opts) {
|
||||
if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true;
|
||||
static const char *const STR_OPTS[] = { "ciphers", "passphrase", "ecdhCurve", "sessionIdContext", NULL };
|
||||
for (size_t i = 0; STR_OPTS[i] != NULL; i++) {
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, STR_OPTS[i], strlen(STR_OPTS[i]));
|
||||
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_STR) {
|
||||
char name[48];
|
||||
snprintf(name, sizeof name, "options.%s", STR_OPTS[i]);
|
||||
scr_dyn_prop_type_fail(name, "of type string", v);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const char *const ENGINE_OPTS[] = { "clientCertEngine", "privateKeyEngine", "privateKeyIdentifier", NULL };
|
||||
for (size_t i = 0; ENGINE_OPTS[i] != NULL; i++) {
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, ENGINE_OPTS[i], strlen(ENGINE_OPTS[i]));
|
||||
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_STR) {
|
||||
char name[48];
|
||||
snprintf(name, sizeof name, "options.%s", ENGINE_OPTS[i]);
|
||||
scr_dyn_prop_type_fail(name, "of type string or one of null or undefined", v);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const char *const VERSION_OPTS[] = { "minVersion", "maxVersion", NULL };
|
||||
for (size_t i = 0; VERSION_OPTS[i] != NULL; i++) {
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, VERSION_OPTS[i], strlen(VERSION_OPTS[i]));
|
||||
if (v == NULL || v->kind == SCR_DYN_UNDEF) continue;
|
||||
bool valid = false;
|
||||
if (v->kind == SCR_DYN_STR) {
|
||||
static const char *const KNOWN[] = { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3", NULL };
|
||||
for (size_t k = 0; KNOWN[k] != NULL; k++) {
|
||||
if (v->v.str->len == strlen(KNOWN[k]) && memcmp(v->v.str->data, KNOWN[k], v->v.str->len) == 0) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
/* %j: strings render JSON-quoted, everything else inspect-lite. */
|
||||
char rendered[96];
|
||||
if (v->kind == SCR_DYN_STR) {
|
||||
snprintf(rendered, sizeof rendered, "\"%.*s\"",
|
||||
(int)(v->v.str->len < 80 ? v->v.str->len : 80), v->v.str->data);
|
||||
} else {
|
||||
/* %j over non-strings: JSON.stringify's scalar renderings. */
|
||||
char lite[64];
|
||||
snprintf(rendered, sizeof rendered, "%s", scr_dyn_inspect_lite(v, lite, sizeof lite));
|
||||
}
|
||||
char msg[192];
|
||||
int len = snprintf(msg, sizeof msg, "%s is not a valid %s TLS protocol version",
|
||||
rendered, VERSION_OPTS[i][2] == 'n' ? "minimum" : "maximum");
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_TLS_INVALID_PROTOCOL_VERSION");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static const char *const NUM_OPTS[] = { "handshakeTimeout", "keepAliveInitialDelay", NULL };
|
||||
for (size_t i = 0; NUM_OPTS[i] != NULL; i++) {
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, NUM_OPTS[i], strlen(NUM_OPTS[i]));
|
||||
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL && v->kind != SCR_DYN_NUM) {
|
||||
char name[48];
|
||||
snprintf(name, sizeof name, "options.%s", NUM_OPTS[i]);
|
||||
scr_dyn_prop_type_fail(name, "of type number", v);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
{
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, "sessionTimeout", 14);
|
||||
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
||||
if (v->kind != SCR_DYN_NUM) {
|
||||
scr_dyn_prop_type_fail("options.sessionTimeout", "of type number", v);
|
||||
return false;
|
||||
}
|
||||
double n = v->v.num;
|
||||
char recv[48], msg[192];
|
||||
if (!(isfinite(n) && trunc(n) == n)) {
|
||||
scr_num_received(n, recv);
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"options.sessionTimeout\" is out of range. It must be an integer. Received %s", recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
}
|
||||
if (n < 0 || n > 2147483647.0) {
|
||||
scr_num_received(n, recv);
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The value of \"options.sessionTimeout\" is out of range. It must be >= 0 && <= 2147483647. Received %s", recv);
|
||||
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const ScrDyn *v = scr_dyn_obj_get(opts, "ticketKeys", 10);
|
||||
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
||||
if (v->kind != SCR_DYN_BYTES) {
|
||||
scr_dyn_prop_type_fail("options.ticketKeys", "an instance of Buffer, TypedArray, or DataView", v);
|
||||
return false;
|
||||
}
|
||||
double bytelen = scr_bytes_byte_len(v->v.bytes);
|
||||
if (bytelen != 48) {
|
||||
char msg[160];
|
||||
int len = snprintf(msg, sizeof msg,
|
||||
"The property 'options.ticketKeys' must be exactly 48 bytes. Received %.0f", bytelen);
|
||||
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)len, "ERR_INVALID_ARG_VALUE");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The server-options walk. Fills the cert/key out-params (+1 each) from a
|
||||
* DOM options record; false = exception pending (partial results released). */
|
||||
static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes **cert_out,
|
||||
@@ -1112,6 +1228,7 @@ static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes
|
||||
scr_dyn_arg_type_fail("options", "of type object", opts ? opts : scr_dyn_undefined());
|
||||
return false;
|
||||
}
|
||||
if (!scr_tls_opts_validate(opts)) return false; /* Node's typed errors first */
|
||||
bool ok = true;
|
||||
for (size_t i = 0; ok && i < opts->v.obj.len; i++) {
|
||||
const ScrDynEntry *e = &opts->v.obj.entries[i];
|
||||
@@ -1176,6 +1293,45 @@ static bool scr_tls_srv_opts_walk(const ScrDyn *opts, const char *api, ScrBytes
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
* everything else met its ladder error or the walk's per-key fence.
|
||||
* +1, or NULL with the exception pending. */
|
||||
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts /*borrowed*/) {
|
||||
ScrBytes *cert, *key;
|
||||
if (!scr_tls_srv_opts_walk(opts, "tls.createSecureContext", &cert, &key)) return NULL;
|
||||
ScrSecureCtx *c = scr_tls_create_secure_context((const char *)cert->data, cert->len,
|
||||
(const char *)key->data, key->len);
|
||||
scr_bytes_release(cert);
|
||||
scr_bytes_release(key);
|
||||
return c;
|
||||
}
|
||||
|
||||
/* tls.getCACertificates(type): validateString, then the documented name
|
||||
* set — an unknown name answers ERR_INVALID_ARG_VALUE; the real CA list
|
||||
* has no lowering, so a valid name meets the fence. Always throws. */
|
||||
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence) {
|
||||
if (type->kind != SCR_DYN_STR) {
|
||||
scr_dyn_arg_type_fail("type", "of type string", type);
|
||||
return;
|
||||
}
|
||||
static const char *const KNOWN[] = { "default", "system", "bundled", "extra", NULL };
|
||||
bool valid = false;
|
||||
for (size_t i = 0; KNOWN[i] != NULL; i++) {
|
||||
if (type->v.str->len == strlen(KNOWN[i]) &&
|
||||
memcmp(type->v.str->data, KNOWN[i], type->v.str->len) == 0) {
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!valid) {
|
||||
scr_dyn_arg_value_fail("type", NULL, type);
|
||||
return;
|
||||
}
|
||||
scr_throw_lowering_fence(fence);
|
||||
}
|
||||
|
||||
ScrNetServer *scr_tls_create_server_dyn(const ScrDyn *opts /*borrowed*/,
|
||||
ScrClosure *handler /*moves, nullable*/,
|
||||
ScrNetConnFn fn) {
|
||||
@@ -1241,6 +1397,22 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
|
||||
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;
|
||||
if (strcmp(e->key, "checkServerIdentity") == 0) {
|
||||
/* Node spreads user options over the defaults, so a PRESENT key
|
||||
* replaces the builtin verifier even when its value is undefined
|
||||
* — validateFunction then throws for anything non-callable. A
|
||||
* real function keeps the fence (custom verification has no
|
||||
* lowering). */
|
||||
if (v->kind != SCR_DYN_FUNC) {
|
||||
scr_dyn_prop_type_fail("options.checkServerIdentity", "of type function", v);
|
||||
ok = false;
|
||||
} else {
|
||||
scr_tls_opt_fence("tls.connect", "checkServerIdentity",
|
||||
"custom identity verification has no lowering — the runtime verifies against the servername/host");
|
||||
ok = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (v->kind == SCR_DYN_UNDEF) continue;
|
||||
if (strcmp(e->key, "port") == 0) {
|
||||
if (port >= 0) continue; /* the argument form wins, like Node */
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// The tls option-bag validation ladders: misuse of the implemented tls
|
||||
// surface answers Node's exact typed errors instead of fencing — the
|
||||
// createSecureContext/createServer string contracts (ciphers, passphrase,
|
||||
// ecdhCurve), the engine trio's string-or-null-or-undefined clause, the
|
||||
// number contracts (handshakeTimeout, sessionTimeout with validateInt32's
|
||||
// range), ticketKeys' view contract and exact-48-bytes value ladder,
|
||||
// minVersion/maxVersion's ERR_TLS_INVALID_PROTOCOL_VERSION with %j
|
||||
// rendering, tls.connect's checkServerIdentity function contract (a
|
||||
// PRESENT key replaces the builtin verifier even holding undefined —
|
||||
// Node's defaults spread), and getCACertificates' type/value ladder.
|
||||
'use strict';
|
||||
const tls = require('tls');
|
||||
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
|
||||
show(() => { tls.createSecureContext({ ciphers: 1 }); });
|
||||
show(() => { tls.createServer({ ciphers: 1 }); });
|
||||
show(() => { tls.createSecureContext({ key: 'dummykey', passphrase: 1 }); });
|
||||
show(() => { tls.createServer({ key: 'dummykey', passphrase: 1 }); });
|
||||
show(() => { tls.createServer({ ecdhCurve: 1 }); });
|
||||
show(() => { tls.createServer({ handshakeTimeout: 'abcd' }); });
|
||||
show(() => { tls.createServer({ sessionTimeout: 'abcd' }); });
|
||||
show(() => { tls.createServer({ ticketKeys: 'abcd' }); });
|
||||
show(() => { tls.createServer({ ticketKeys: Buffer.alloc(0) }); });
|
||||
show(() => { tls.createServer({ ticketKeys: Buffer.alloc(51) }); });
|
||||
show(() => { tls.createSecureContext({ clientCertEngine: 0 }); });
|
||||
show(() => { tls.createSecureContext({ privateKeyEngine: 0, privateKeyIdentifier: 'key' }); });
|
||||
show(() => { tls.createSecureContext({ privateKeyEngine: 'engine', privateKeyIdentifier: 0 }); });
|
||||
show(() => { tls.createSecureContext({ minVersion: 'fhqwhgads' }); });
|
||||
show(() => { tls.createSecureContext({ maxVersion: 'fhqwhgads' }); });
|
||||
show(() => { tls.createSecureContext({ minVersion: 42 }); });
|
||||
show(() => { tls.createSecureContext({ sessionTimeout: -1 }); });
|
||||
show(() => { tls.createSecureContext({ sessionTimeout: 2 ** 31 }); });
|
||||
show(() => { tls.createSecureContext({ sessionTimeout: 1.5 }); });
|
||||
for (const checkServerIdentity of [undefined, null, 1, true]) {
|
||||
show(() => { tls.connect({ checkServerIdentity }); });
|
||||
}
|
||||
for (const invalid of [1, null, () => {}, true]) {
|
||||
show(() => tls.getCACertificates(invalid));
|
||||
}
|
||||
show(() => tls.getCACertificates('test'));
|
||||
@@ -0,0 +1,32 @@
|
||||
// The stream-surface validation ladders: finished() over a provably-non-
|
||||
// stream value answers Node's isNodeStream ERR_INVALID_ARG_TYPE (the
|
||||
// watcher never registers), socket write's two-argument encoding form
|
||||
// implements Node's stream_base typecheck — write(string, 'buffer') is
|
||||
// the synchronous "Second argument must be a buffer" TypeError on an
|
||||
// established socket, utf8 spellings are the plain write — and
|
||||
// Readable.toWeb's `type` option answers Node's one-of ladder before any
|
||||
// web-stream machinery.
|
||||
'use strict';
|
||||
const net = require('net');
|
||||
const { Duplex, Readable, finished } = require('stream');
|
||||
const show = (fn) => { try { fn(); console.log('ok'); } catch (e) { console.log(`${e.name}|${e.code}|${e.message}`); } };
|
||||
|
||||
show(() => { finished({}, () => {}); });
|
||||
show(() => { finished('nope', () => {}); });
|
||||
|
||||
const nodeStream = new Readable({ read() {} });
|
||||
show(() => { Readable.toWeb(nodeStream, { type: 'wrong type' }); });
|
||||
nodeStream.destroy();
|
||||
|
||||
const streamObj = new Duplex();
|
||||
streamObj.end();
|
||||
finished(streamObj, () => console.log('finished fired'));
|
||||
|
||||
const server = net.createServer().listen(0, () => {
|
||||
const client = net.connect(server.address().port, () => {
|
||||
show(() => { client.write('broken', 'buffer'); });
|
||||
client.write('fine', 'utf8');
|
||||
client.destroy();
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user