Read http.request options at run time, speak HTTP over Unix sockets, and fill the boot path's net and fetch gaps (#1515)

This commit is contained in:
Chris Tate
2026-10-20 10:28:21 -05:00
committed by GitHub
37 changed files with 2125 additions and 36 deletions
+4
View File
@@ -230,6 +230,10 @@ title: "Limitations"
<include>../limitations/udp-socket-options.mdx</include>
### Unix domain sockets and request options
<include>../limitations/unix-domain-sockets.mdx</include>
### Process arguments and errors
<include>../limitations/process-arguments-and-errors.mdx</include>
@@ -0,0 +1,2 @@
`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS. On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`. `server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration). Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server. `listen(path, backlog, callback)` is refused at compile time; pass `listen(path, callback)`.
When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors. The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.
+25 -4
View File
@@ -4694,6 +4694,11 @@ declare module "net" {
on(event: "close", listener: (hadError: boolean) => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
on(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
/* addListener IS on (Node aliases them) — the suite spells both. */
addListener(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
@@ -4705,6 +4710,11 @@ declare module "net" {
addListener(event: "close", listener: (hadError: boolean) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
addListener(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
@@ -4715,6 +4725,11 @@ declare module "net" {
once(event: "close", listener: (hadError: boolean) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
once(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
}
export interface Server {
/* Whether the server keeps the event loop alive (ref'd by default). */
@@ -4733,6 +4748,8 @@ declare module "net" {
/* The positional bind address (the options form's host in argument
* spelling). */
listen(port: number, host: string, callback?: () => void): Server;
/* A Unix-socket server; the socket file is removed when it closes. */
listen(path: string, callback?: () => void): Server;
/* The explicit-interface bind: host is an IP literal (absent = the
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort
* requests kernel connection distribution where Node supports it. */
@@ -5056,10 +5073,11 @@ declare module "http" {
* callback receives the response as an IncomingMessage (statusCode,
* headers, body via on("data"/"end")). */
export interface RequestOptions {
hostname?: string;
host?: string;
port?: number;
path?: string;
hostname?: string | null;
host?: string | null;
/* A string port (a URL's .port) is accepted, as by Node. */
port?: number | string | null;
path?: string | null;
method?: string;
timeout?: number;
headers?: OutgoingHttpHeaders;
@@ -5073,6 +5091,8 @@ declare module "http" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
/* HTTP over the Unix socket at this path instead of host:port. */
socketPath?: string;
/* The options-record stance: Node ignores an options record's
* unknown keys, so every key typechecks here — the option WALK in
* the lowering decides per key (lower, fence by name, or drop
@@ -5348,6 +5368,7 @@ declare module "https" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
socketPath?: string;
/* The options-record stance (see http.RequestOptions). */
[option: string]: unknown;
}
@@ -177,6 +177,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// programs additionally stamp the httpReq/httpRes ops — the C main's
// install lines, gated on the same predicates native/executable.ts links by.
const usesNet = runtimeFeatures.net;
const usesNetLookup = runtimeFeatures.netLookup;
const usesDgram = runtimeFeatures.dgram;
const usesHttp2 = runtimeFeatures.http2;
const usesHttp = runtimeFeatures.http;
@@ -226,6 +227,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`);
if (usesStream) emitter.declare(`declare void @scr_stream_install()`);
if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`);
if (usesNet) {
emitter.declare(`declare void @scr_net_install()`);
emitter.declare(`declare void @scr_net_dyn_install()`);
@@ -837,6 +839,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
@@ -60,7 +60,54 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
...handler,
]);
}
if (
e.fn === "http.requestDyn" ||
e.fn === "http.requestDynCb" ||
e.fn === "https.requestDyn" ||
e.fn === "https.requestDynCb"
) {
// [options, autoEnd(, cb)]: the runtime walks the options record and
// attaches its signal through events.addAbortListener (the event-target
// unit, linked for these calls by runtime-features).
const args = e.args.map((arg) => host.emitExpr(arg));
host.usesTimers = true;
const cb = args[2];
const handler =
cb === undefined
? [ptr(), ptr()]
: callbackAdapter(
host,
cb,
callbackType(cb).params.length === 0
? "scr_http_resp_thunk0"
: "scr_http_resp_thunk_res",
["ptr", "ptr"],
);
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
return finishRuntimeCall(
host,
e,
e.fn.startsWith("https.") ? "scr_https_request_dyn" : "scr_http_request_dyn",
[
abiValue(host, args[0]!),
abiValue(host, args[1]!),
...handler,
ptr("@scr_events_add_abort_listener"),
],
);
}
const B = host.B;
if (e.fn === "net.listenPath" || e.fn === "net.listenPathCb") {
const args = e.args.map((a) => host.emitExpr(a));
let cb = "null";
if (e.fn === "net.listenPathCb") {
host.toHeap(args[2]!);
cb = args[2]!.name;
}
host.declare(`declare void @scr_net_listen_path(ptr, ptr, ptr)`);
B.line(`call void @scr_net_listen_path(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${cb})`);
return { name: "", type: e.type };
}
if (e.fn === "net.connectOptsChk") {
return emitAlwaysThrowLibCall(host, e, "scr_net_connect_opts_chk");
}
@@ -1157,6 +1157,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"net.sockReadyState": "scr_net_sock_ready_state",
"net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret",
"net.sockOnCloseDyn": "scr_net_sock_on_close_dyn",
"net.sockOnLookup": "scr_net_sock_on_lookup",
"net.serverCloseDyn": "scr_net_server_close_dyn",
"net.serverUnixPath": "scr_net_server_unix_path",
"net.serverAddressCheck": "scr_net_server_address_check",
"net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn",
"net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn",
"net.isIP": "scr_net_is_ip",
@@ -1632,6 +1636,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"sc.arrayBuffer",
"net.listen",
"net.listenCb",
"net.listenPath",
"net.listenPathCb",
"net.listenOpts",
"net.listenOptsCb",
"net.listenOptsReusePort",
@@ -1650,6 +1656,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"http.requestCb",
"http.requestUrl",
"http.requestUrlCb",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"https.request",
"https.requestCb",
"https.requestUrl",
@@ -53,7 +53,7 @@ import {
sideEffectFreeOptionValue,
TLS_SERVER_DOCUMENTED_OPTIONS,
} from "./surfaces.js";
import { conditionalSpreadOf } from "./expressions/object-literals.js";
import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { resultIsDiscarded } from "./call-position.js";
import { lowerCallbackArg as lowerCallbackArgShared } from "./callback-arg.js";
import { pairsSnapshotHelper } from "./pairs-snapshot.js";
@@ -1627,6 +1627,29 @@ function lowerNetServerMethodCall(
: [receiver, port, host, v6only, reusePort, cbV],
);
}
// listen(path[, cb]): a string first argument names a Unix socket
// (the runtime takes a numeric string as a port, Node's isPipeName).
if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") {
if (args.length > 2) {
lowerer.noLowering(
`listen(path) with ${args.length} arguments`,
call,
"the supported form is listen(path[, callback])",
);
}
const path = lowerer.lowerExprExpecting(args[0]!, STRING);
if (args.length === 1) return listenResult("net.listenPath", [receiver, path]);
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
"listen callbacks",
0,
() => false,
"use ()",
[],
);
return listenResult("net.listenPathCb", [receiver, path, cb]);
}
const port = lowerer.lowerExprExpecting(args[0]!, F64);
// The optional middle host — listen(port, '127.0.0.1'[, cb]): a
// string-typed second argument is the bind address (the net.connect
@@ -1682,6 +1705,21 @@ function lowerNetServerMethodCall(
if (args.length === 0) {
return { kind: "libCall", fn: "net.serverClose", args: [receiver], type: VOID, loc };
}
if (
lowerer.checker
.getCallSignatures(lowerer.typeOf(args[0]!))
.some((sig) => sig.getParameters().length > 0)
) {
// close((err) => ...): undefined once the server closes, or Node's
// ERR_SERVER_NOT_RUNNING when it was not listening.
return {
kind: "libCall",
fn: "net.serverCloseDyn",
args: [receiver, lowerSocketDynListener(lowerer, args[0]!, "close callbacks")],
type: VOID,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[0]!,
@@ -2055,6 +2093,32 @@ function lowerNetServerMethodCall(
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
if (result && isAddressInfoRecord(lowerer, result)) {
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
if (unixListenReachable(lowerer)) {
// A Unix-socket server's address() is its path, which this record
// cannot hold: the check throws for one before the record reads.
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "net.serverAddressCheck",
args: [server()],
type: VOID,
loc,
},
loc,
},
],
result: { kind: "libCall", fn: "net.serverAddress", args: [server()], type: result, loc },
type: result,
loc,
};
}
return { kind: "libCall", fn: "net.serverAddress", args: [receiver], type: result, loc };
}
// @types/node's `string | AddressInfo | null`: the record while the
@@ -2074,20 +2138,50 @@ function lowerNetServerMethodCall(
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = varRef(local.id, NETSERVER_T, loc);
const recordArm: IrExpr = {
kind: "unionWrap",
unionId,
tag: lowerer.armTag(unionId, record),
value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
type: result,
loc,
};
const stringTag = lowerer.armTag(unionId, STRING);
let listeningArm: IrExpr = recordArm;
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
if (stringTag >= 0 && unixListenReachable(lowerer)) {
// Node answers a Unix-socket server's address() with its path.
const pathLocal = lowerer.declareHiddenLocal("%addressPath", STRING);
const path = (): IrExpr => varRef(pathLocal.id, STRING, loc);
stmts.push({
kind: "varDecl",
localId: pathLocal.id,
init: { kind: "libCall", fn: "net.serverUnixPath", args: [server], type: STRING, loc },
loc,
});
listeningArm = {
kind: "ternary",
cond: {
kind: "strEq",
negated: true,
left: path(),
right: strLit("", loc),
type: BOOL,
loc,
},
then: { kind: "unionWrap", unionId, tag: stringTag, value: path(), type: result, loc },
else_: recordArm,
type: result,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
stmts,
result: {
kind: "ternary",
cond: { kind: "libCall", fn: "net.serverListening", args: [server], type: BOOL, loc },
then: {
kind: "unionWrap",
unionId,
tag: lowerer.armTag(unionId, record),
value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
type: result,
loc,
},
then: listeningArm,
else_: {
kind: "unionWrap",
unionId,
@@ -2143,6 +2237,34 @@ function socketEncodingReachable(lowerer: Lowerer): boolean {
return reachable;
}
/** Whether some server in the program may listen on a Unix socket: a
* `.listen(x, ...)` call on a server (or an untyped value) whose first
* argument is string-typed. Only then does address() check for the socket
* path, so other programs keep the record-only lowering. */
function unixListenReachable(lowerer: Lowerer): boolean {
if (lowerer.unixListenReachable !== null) return lowerer.unixListenReachable;
let reachable = false;
for (const file of lowerer.program.getSourceFiles()) {
if (file.isDeclarationFile || !file.text.includes("listen")) continue;
ts.walkPreorder(file, (node) => {
if (reachable) return "skip";
if (!ts.isCallExpression(node) || node.arguments.nodes.length === 0) return undefined;
const callee = node.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "listen") return undefined;
const kind = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(callee.expression))?.kind;
if (kind !== undefined && kind !== "netServer" && kind !== "dyn") return undefined;
const arg = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.arguments.nodes[0]!));
// Only a string-typed argument lowers to listen(path) (an untyped
// one checks into a port).
if (arg?.kind === "string") reachable = true;
return undefined;
});
if (reachable) break;
}
lowerer.unixListenReachable = reachable;
return reachable;
}
/** An inline 'data' listener whose chunk parameter is typed Buffer, for a
* socket that may have an encoding set: the chunk binds as a checked
* dynamic value, so the body sees what Node passes (a string after
@@ -2736,6 +2858,17 @@ function lowerNetSocketMethodCall(
: "net.sockOnConnect";
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
if (event === "lookup") {
// (err, address, family, host) after the host name resolves, before
// the connect: once per resolved address, or once with the error.
return {
kind: "libCall",
fn: "net.sockOnLookup",
args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "lookup listeners"), once],
type: VOID,
loc,
};
}
if (event === "finish") {
// Fires once when the FIN goes out — once either way (the event
// happens at most once per socket).
@@ -2795,7 +2928,7 @@ function lowerNetSocketMethodCall(
lowerer.noLowering(
`socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
args[0]!,
'"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
'"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
);
}
lowerer.noLowering(
@@ -3971,6 +4104,30 @@ export function lowerServerProperty(
if (lowerer.mapTypeOf(lowerer.typeOf(inner.expression))?.kind !== "netServer") return null;
if (!lowerer.isStdlibMember(inner)) return null;
const receiver = coerceToHandle(lowerer, inner.expression, NETSERVER_T);
if (unixListenReachable(lowerer)) {
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "net.serverAddressCheck",
args: [server()],
type: VOID,
loc,
},
loc,
},
],
result: { kind: "libCall", fn: "net.serverPort", args: [server()], type: F64, loc },
type: F64,
loc,
};
}
return { kind: "libCall", fn: "net.serverPort", args: [receiver], type: F64, loc };
}
@@ -5827,6 +5984,44 @@ function lowerHttp2ModuleCall(
);
}
/** The checked-dynamic options value of a request whose options the
* runtime reads: a non-literal options value (a RequestOptions variable —
* checked-dynamic storage — or a record that converts to one), or a
* literal the static walk cannot lower because it spreads another options
* value or sets socketPath. Null keeps the static literal walk (and its
* fences), which every other literal takes unchanged. */
function runtimeRequestOptions(
lowerer: Lowerer,
optsNode: ts.Expression,
secureish: boolean,
): IrExpr | null {
if (ts.isObjectLiteralExpression(optsNode)) {
const needsRuntime = optsNode.properties.nodes.some((prop) => {
if (ts.isSpreadAssignment(prop)) {
// the static walk's one spread: `...(c ? { rejectUnauthorized } : {})` on https
const cs = conditionalSpreadOf(prop.expression);
return !(
secureish &&
cs !== null &&
cs !== "unsupported" &&
cs.props.length === 1 &&
cs.props[0]!.name.text === "rejectUnauthorized"
);
}
return (
(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
prop.name.text === "socketPath"
);
});
if (!needsRuntime) return null;
return lowerDynObjectLiteral(lowerer, optsNode);
}
const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
if (!t || !(t.kind === "dyn" || (t.kind === "record" && lowerer.dynConvertible(t)))) return null;
return lowerer.coerceToExpected(lowerer.lowerExpr(optsNode), DYN);
}
/** http.request(options[, cb]) / http.get(options[, cb]) — the client.
* The options argument must be an OBJECT LITERAL; the lowered keys are
* hostname/host, port, path, method, timeout, and headers (a nested
@@ -5869,6 +6064,40 @@ function lowerHttpClientCall(
);
}
const optsNode = args[0]!;
const runtimeOptions =
binding === null ? runtimeRequestOptions(lowerer, optsNode, secureish) : null;
if (runtimeOptions !== null) {
// The options record is read at run time (the ClientRequest
// constructor's walk in the runtime): a RequestOptions variable, or a
// literal that spreads one or sets socketPath.
const autoEnd = boolLit(member === "get", loc);
const secureDyn = secure === true;
if (args.length === 1) {
return {
kind: "libCall",
fn: secureDyn ? "https.requestDyn" : "http.requestDyn",
args: [runtimeOptions, autoEnd],
type: HTTPCLIENTREQ_T,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
"response callbacks",
1,
(p) => p.kind === "httpReq",
"use (res) or ()",
[HTTPREQ_T],
);
return {
kind: "libCall",
fn: secureDyn ? "https.requestDynCb" : "http.requestDynCb",
args: [runtimeOptions, autoEnd, cb],
type: HTTPCLIENTREQ_T,
loc,
};
}
if (!ts.isObjectLiteralExpression(optsNode)) {
const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
if ((t?.kind === "string" || t?.kind === "url") && binding === null) {
@@ -7149,6 +7378,27 @@ function lowerHttpReqMethodCall(
const event = evT.isStringLiteralType() ? evT.value : null;
const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T);
if (event === "data") {
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!));
const firstParam = sigs.length === 1 ? sigs[0]!.getParameters()[0] : undefined;
const param =
firstParam !== undefined
? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(firstParam))
: null;
if (param !== null && param.kind === "string") {
// A chunk typed string (the setEncoding('utf8') shape), as on a
// socket: the listener takes the chunk through a checked
// boundary, so a string arrives after setEncoding and a Buffer
// without it fails the check instead of being decoded.
const boxed = lowerSocketDynListener(lowerer, args[1]!, "data listeners");
const cb: IrExpr = { kind: "dynCheck", value: boxed, type: funcOf([DYN], VOID), loc };
return {
kind: "libCall",
fn: "http.reqOnData",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
@@ -7711,6 +7961,30 @@ function lowerHttpResMethodCall(
if (cbArg !== null) return endWithCb(fn, [receiver, data], cbArg);
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
}
// end(payload ? text : undefined): an absent chunk ends without data,
// as end() does. The checked-dynamic end takes each arm at run time.
if (name === "end" && data.type.kind === "union" && lowerer.dynConvertible(data.type)) {
const arms = lowerer.unions.get(data.type.unionId)?.arms ?? [];
if (
arms.every(
(arm) =>
arm.kind === "string" ||
(arm.kind === "bytes" && arm.elem === "u8") ||
arm.kind === "undefinedT" ||
arm.kind === "nullT",
)
) {
const dynData: IrExpr = { kind: "dynFrom", value: data, type: DYN, loc: locOf(dataNode) };
if (cbArg !== null) return endWithCb("http.resEndDyn", [receiver, dynData], cbArg);
return {
kind: "libCall",
fn: "http.resEndDyn",
args: [receiver, dynData],
type: VOID,
loc,
};
}
}
lowerer.noLowering(
`${name} of '${lowerer.fmt(data.type)}' data`,
dataNode ?? call,
@@ -1950,6 +1950,10 @@ export class Lowerer {
* setEncoding call whose receiver is a socket or an untyped value.
* Computed once on first use (null until then). */
socketEncodingReachable: boolean | null = null;
/** Whether some server in this program may listen on a Unix socket: a
* listen call whose first argument is a string. Computed once on first
* use (null until then); address() answers the path only when true. */
unixListenReachable: boolean | null = null;
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
* types are inferred from the body — the call site needs them settled),
* collected here for run()'s function list (the liftedFns discipline). */
@@ -1375,6 +1375,116 @@ function isLookupOptionsUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibF
return all;
}
/** The binary and stream members of fetch's BodyInit (undici-types):
* NodeJS.ArrayBufferView expands to the typed arrays and DataView. */
const FETCH_BODY_MEMBERS: ReadonlySet<string> = new Set([
"ArrayBuffer",
"SharedArrayBuffer",
"AsyncIterable",
"Iterable",
"Blob",
"FormData",
"URLSearchParams",
"ReadableStream",
"DataView",
"Buffer",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float16Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
]);
/** fetch's request body type, BodyInit, however TypeScript spells it
* (`RequestInit["body"]`, `NonNullable<...>`, an optional member): a
* union of strings, the stdlib binary and stream types and nullish
* values that includes both FormData and URLSearchParams. Native fetch
* reads such a body at run time, so it rides checked-dynamic storage. */
function isFetchBodyUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
if (!type.isUnionType()) return false;
let formData = false;
let searchParams = false;
for (const member of ts.constituentTypes(type)) {
if (member.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
continue;
const symbol = member.getSymbol();
if (
symbol === undefined ||
!FETCH_BODY_MEMBERS.has(symbol.name) ||
!symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
)
return false;
if (symbol.name === "FormData") formData = true;
if (symbol.name === "URLSearchParams") searchParams = true;
}
return formData && searchParams;
}
/** A mapped object type (Omit, Pick, Partial) whose members all come from
* one stdlib interface that maps to the checked dynamic. */
function isDynInterfaceSubset(type: ts.Type, ctx: TypeMapperCtx): boolean {
if (!(type.flags & ts.TypeFlags.Object) || type.isClassOrInterface()) return false;
const alias = type.getAliasSymbol();
if (alias === undefined) return false;
const props = ctx.checker.getPropertiesOfType(type);
if (props.length === 0) return false;
let owner: ts.InterfaceDeclaration | undefined;
for (const prop of props) {
const decl = prop.declarations[0];
const parent = decl?.parent;
if (
parent === undefined ||
!ts.isInterfaceDeclaration(parent) ||
!ctx.isStdlibFile(parent.getSourceFile()) ||
(owner !== undefined && owner !== parent)
)
return false;
owner = parent;
}
return (
owner !== undefined && mapType(ctx.checker.getTypeAtLocation(owner.name), ctx)?.kind === "dyn"
);
}
/** fetch's HeadersInit (undici-types): a union of the stdlib Headers
* class with header-pair arrays and/or a header record, plus nullish
* values. Native fetch reads it at run time like the init around it. */
function isFetchHeadersUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
if (!type.isUnionType()) return false;
let headers = false;
let other = false;
for (const member of ts.constituentTypes(type)) {
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
const symbol = member.getSymbol();
if (
symbol?.name === "Headers" &&
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
) {
headers = true;
continue;
}
const alias = member.getAliasSymbol()?.name;
if (
symbol?.name === "Array" ||
symbol?.name === "ReadonlyArray" ||
alias === "Record" ||
alias === "HeaderRecord"
) {
other = true;
continue;
}
return false;
}
return headers && other;
}
function isReceiveMessageResult(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
@@ -1542,6 +1652,25 @@ function classExprNeedsContext(decl: ts.ClassLikeDeclaration): boolean {
return false;
}
/** True for the type of a TypeScript object literal with no contextual
* type (`const options = { ...location, path }`) that spreads a value
* mapping to the checked dynamic (a stdlib options type such as
* http.RequestOptions). A literal with a contextual type is built in that
* type's representation instead, and JavaScript literals keep their own
* rules. */
function spreadsCheckedDynamic(symbol: ts.Symbol | undefined, ctx: TypeMapperCtx): boolean {
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false;
const decl = symbol.declarations[0];
if (decl === undefined || !ts.isObjectLiteralExpression(decl)) return false;
if (isJsSourceFile(decl.getSourceFile())) return false;
if (ctx.checker.getContextualType(decl)) return false;
return decl.properties.nodes.some(
(prop) =>
ts.isSpreadAssignment(prop) &&
mapType(ctx.checker.getTypeAtLocation(prop.expression), ctx)?.kind === "dyn",
);
}
function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { checker, unions, classNamer, resolveTypeParam } = ctx;
// TypeScript's intrinsic globalThis symbol has no declaration. A stored
@@ -1562,9 +1691,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
if (moduleId !== null) return { kind: "moduleNs", moduleId };
}
// A TypeScript object literal that spreads a checked-dynamic value
// (`{ ...requestOptions, path, timeout }`): its keys are the source's
// runtime keys, which no record shape holds, so the literal rides
// checked-dynamic storage like its source.
if (spreadsCheckedDynamic(intrinsic, ctx)) return DYN;
// worker_threads.receiveMessageOnPort's `{ message: any } | undefined`:
// the runtime's { message } object, a checked-dynamic value.
if (isReceiveMessageResult(type, ctx)) return DYN;
if (isFetchBodyUnion(type, ctx) || isFetchHeadersUnion(type, ctx)) return DYN;
// Omit/Pick/Partial of a stdlib interface that rides checked-dynamic
// storage (`Omit<RequestInit, "body" | "headers">`): every member comes
// from that interface, so the value is a subset of the same runtime-read
// options object and rides the same storage.
if (isDynInterfaceSubset(type, ctx)) return DYN;
// The lookup-function option union (`LookupOneOptions | LookupAllOptions
// | number | undefined`): with a LookupAllOptions arm it is an option bag
// dns.lookup reads at run time, so it rides checked-dynamic storage.
@@ -263,6 +263,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// ("Invalid URL") or a non-http scheme (ERR_INVALID_PROTOCOL).
"http.requestUrl",
"http.requestUrlCb",
// The runtime options walk throws Node's validation errors and the
// fence for documented options it does not implement.
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
// address() of a Unix-socket server read as the AddressInfo record.
"net.serverAddressCheck",
// The h2 client's throwing entries: connect on a bad/non-http
// authority (ERR_INVALID_URL / ERR_INVALID_PROTOCOL), request on a
// closed/destroyed session (ERR_HTTP2_INVALID_SESSION), setEncoding on
@@ -1280,6 +1280,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"net.createServer": { argTypes: [], result: NETSERVER_T },
"net.createServerCb": { argTypes: [null], result: NETSERVER_T },
"net.listen": { argTypes: [NETSERVER_T, F64], result: VOID },
/** listen(path[, cb]): a Unix-socket server; the socket file is removed
* when the server closes. Failures are the async 'error'. */
"net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID },
"net.listenPathCb": {
argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }],
result: VOID,
},
"net.listenCb": {
argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1322,6 +1329,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* answers null (the serverPort stance). */
"net.serverAddress": { argTypes: [NETSERVER_T], result: VOID },
"net.serverClose": { argTypes: [NETSERVER_T], result: VOID },
/** server.close((err) => ...): the callback (a dyn function) gets
* undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING
* when it was not listening. */
"net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID },
/** server.address() in programs that may listen on a Unix socket: the
* path a listening server answers ("" for a TCP server), and the check
* a record-typed address() makes first (throws for a Unix server). */
"net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING },
"net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID },
"net.serverCloseCb": {
argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1911,6 +1927,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* non-http scheme. */
"http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
/** request/get with a runtime options record: a RequestOptions
* variable, or a literal that spreads one or sets socketPath. Args
* (options, autoEnd[, cb]). The runtime reads the record in the
* ClientRequest constructor's order (host/port or socketPath, path,
* method, headers, timeout, agent, signal, maxHeaderSize) with Node's
* errors, and documented options it does not implement throw the
* runtime fence. The "https." pair adds the TLS options. MAY THROW. */
"http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
"http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
"https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
/** The paused-mode demux surface (portless's first-byte TLS peek:
* once('readable') + read(1) + unshift + emit('connection')).
* sockOnReadable registers a zero-param listener (a consumer: arrived
@@ -1956,6 +1983,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** Listeners with a parameter, as dyn functions: close(hadError),
* write's cb(err) and end's cb. */
"net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
/** socket.on/once('lookup', listener): the listener (a dyn function)
* gets (err, address, family, host) once per resolved address, or once
* with the lookup error, before the connect. */
"net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
"net.sockOnWriteFlush": {
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -131,6 +131,10 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */
const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"http.clientSignal",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"rl.createSignal",
"rl.questionAsyncSignal",
"sp.finishedOptsSignal",
@@ -541,6 +545,9 @@ const RUNTIME_FEATURE_RULES = {
fetch: rule({ prefixes: ["fetch."] }),
fileHandle: rule({ types: ["fileHandle"] }),
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
/** A socket 'lookup' listener: the C main installs the net unit's
* lookup records (scr_net_lookup_install). */
netLookup: rule({ fns: ["net.sockOnLookup"] }),
http: rule({
prefixes: ["http.", "tls.", "https.", "http2."],
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
@@ -667,6 +674,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
fetch: false,
fileHandle: false,
fsWatch: false,
netLookup: false,
http: false,
http2: false,
inspect: false,
+10 -6
View File
@@ -4427,7 +4427,9 @@ function validateFunction(
e.fn === "http.clientOnResponse" ||
e.fn === "http.requestAgentCb" ||
e.fn === "https.requestAgentCb" ||
e.fn === "https.requestUrlCb"
e.fn === "https.requestUrlCb" ||
e.fn === "http.requestDynCb" ||
e.fn === "https.requestDynCb"
) {
// The response listener: void, no params or exactly (res: httpReq).
const cbT =
@@ -4438,11 +4440,13 @@ function validateFunction(
? 9
: e.fn === "http.requestUrlCb" || e.fn === "https.requestUrlCb"
? 3
: e.fn === "http.requestAgentCb"
? 8
: e.fn === "https.requestAgentCb"
? 10
: 1
: e.fn === "http.requestDynCb" || e.fn === "https.requestDynCb"
? 2
: e.fn === "http.requestAgentCb"
? 8
: e.fn === "https.requestAgentCb"
? 10
: 1
]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6440-http-socket-path.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6441-http-request-options-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6442-https-request-options-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6443-net-lookup-http-close-text.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/fetch-init-members.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/http-socket-path.ts"
],
"diags": []
}
+19 -2
View File
@@ -5179,7 +5179,21 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
* arrays, and plain objects (with the ordinary JS object coercion
* protocol), while null and undefined continue to mean no body. */
ScrDyn *coerced_body = NULL;
if (body_present && body->kind != SCR_DYN_STR &&
/* A URLSearchParams body is its serialization, sent as
* application/x-www-form-urlencoded;charset=UTF-8 (extract a body). */
bool form_body = false;
if (body_present && body->kind == SCR_DYN_HANDLE &&
body->v.handle.tag == SCR_DYNH_SEARCH_PARAMS) {
coerced_body = scr_dyn_handle_ops_of(body)->invoke(body->v.handle.ptr, (ScrDyn *)body,
"toString", NULL, 0, "URLSearchParams");
if (!coerced_body || coerced_body->kind != SCR_DYN_STR) {
scr_dyn_release(coerced_body);
return sf_reject_now(promise, "fetch failed");
}
body = coerced_body;
form_body = true;
}
if (body_present && !form_body && body->kind != SCR_DYN_STR &&
body->kind != SCR_DYN_BYTES && !body_stream) {
ScrStr *text = scr_dyn_string_coerce_js(body);
if (!text) {
@@ -5322,7 +5336,10 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
/* Fetch Metadata is controlled by the fetch implementation. Undici
* replaces a caller-provided sec-fetch-mode with "cors". */
sf_strip_header(&headers, "sec-fetch-mode");
if (body && body->kind == SCR_DYN_STR &&
if (form_body && !sf_pairs_have(headers, "content-type")) {
sf_push_header_text(headers, "content-type",
"application/x-www-form-urlencoded;charset=UTF-8", 47);
} else if (body && body->kind == SCR_DYN_STR &&
!sf_pairs_have(headers, "content-type")) {
sf_push_header_text(headers, "content-type",
"text/plain;charset=UTF-8", 24);
+502 -3
View File
@@ -806,6 +806,8 @@ static bool scr_http_token_char(unsigned char ch) {
(ch >= 'a' && ch <= 'z') || (ch != 0 && strchr("!#$%&'*+-.^_`|~", ch) != NULL);
}
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/);
/* node:http's public validators use the same token and field-vchar rules
* as OutgoingMessage. Strings are UTF-8 here, so the Latin-1 obs-text
* range accepted by Node's JS regex is C2/C3 plus one continuation byte. */
@@ -814,7 +816,12 @@ void scr_http_validate_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*bo
for (size_t i = 0; i < name->len && valid; i++)
valid = scr_http_token_char((unsigned char)name->data[i]);
if (valid) return;
scr_http_throw_invalid_header_name(name, label);
}
/* Node's ERR_INVALID_HTTP_TOKEN for a header name; `name` is the name's
* string form. */
static void scr_http_throw_invalid_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/) {
ScrJsonBuf b;
scr_jb_init(&b);
ScrStr *shown = label->len ? label : NULL;
@@ -5582,6 +5589,18 @@ static void scr_http_agent_destroy(ScrHttpAgent *a) {
}
}
/* The socketPath option of the next request built through
* scr_http_request_agent_ex (borrowed; the caller keeps it alive across
* the call), like https's servername: that request dials the Unix socket
* instead of host:port, and the agent keys its sockets by the path, as
* Node's getName does. */
static SCR_TL ScrStr *scr_http_next_socket_path = NULL;
/* The Unix-socket dialer, set by the runtime options path that sets the
* path; a pointer (not static) so ordinary requests do not link the
* Unix-socket half of the net unit. */
extern SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred);
SCR_TL ScrNetSocket *(*scr_http_socket_dialer)(ScrStr *path, bool deferred) = NULL;
ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
@@ -5590,6 +5609,8 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
ScrStr *pool_key /*borrowed, nullable*/) {
ScrStr *socket_path = scr_http_next_socket_path;
scr_http_next_socket_path = NULL;
/* the null/undefined arms: the scheme's global agent, like Node */
if (agent == NULL || agent->kind == SCR_DYN_UNDEF || agent->kind == SCR_DYN_NULL)
agent = scr_http_global_agent(default_port == 443);
@@ -5619,8 +5640,10 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po
scr_arr_push_ref(copy, scr_str_new("close", 5));
pairs = copy;
}
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, false) : NULL;
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, pairs,
auto_end, cb, fn, default_port, wrap, wrap_ctx, NULL);
auto_end, cb, fn, default_port, wrap, wrap_ctx,
presock);
if (copy) scr_arr_release(copy);
return c;
}
@@ -5636,11 +5659,14 @@ ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double po
char portbuf[16];
int portn = snprintf(portbuf, sizeof portbuf, "%d", (int)p);
ScrStr *name = scr_http_agent_name(host->data, host->len, portbuf, (size_t)portn,
NULL, 0, 0, NULL, 0);
NULL, 0, 0, socket_path ? socket_path->data : NULL,
socket_path ? socket_path->len : 0);
/* a free pooled socket for the name serves the request right away */
ScrHttpAgentFree *reuse = ag->keep_alive ? scr_http_agent_free_take(ag, name, pool_key) : NULL;
bool queue = reuse == NULL && scr_http_agent_active(ag, name) >= ag->max_sockets;
ScrNetSocket *presock = queue || reuse ? scr_net_connect_deferred(p, host) : NULL;
ScrNetSocket *presock = socket_path != NULL ? scr_http_socket_dialer(socket_path, queue || reuse)
: queue || reuse ? scr_net_connect_deferred(p, host)
: NULL;
ScrHttpClientReq *c = scr_http_request_impl(host, p, path, method, timeout_ms, header_pairs,
auto_end, cb, fn, default_port, wrap, wrap_ctx,
presock);
@@ -6968,3 +6994,476 @@ ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method
scr_str_release(path);
return c;
}
/* ── http.request(options) with a runtime options record ─────────────────
*
* The options value of http.request/get when the compiler cannot see an
* object literal: a RequestOptions variable, or a literal that spreads one
* or sets socketPath. Node's ClientRequest constructor reads it at run
* time; so does this walk, in Node's validation order and with its error
* shapes. Documented options outside the implemented set throw the
* runtime fence when they carry a value, rather than being ignored;
* undocumented keys are ignored, as Node ignores them. */
/* The documented options this walk does not implement. A truthy value
* fences; setHost and setDefaultHeaders fence only when false. */
static const char *const SCR_HTTP_REQ_FENCED_OPTIONS[] = {
"auth", "createConnection", "family", "hints", "httpValidation", "insecureHTTPParser",
"joinDuplicateHeaders", "localAddress", "localPort", "lookup", "uniqueHeaders", NULL,
};
static const char SCR_HTTP_REQ_SUPPORTED[] =
"hostname/host, port, path, method, headers, timeout, agent, signal, socketPath, "
"defaultPort, protocol, and maxHeaderSize are the supported options";
/* validateHeaderName and validateHeaderValue over one pair of the flat
* headers array: a name that is not a string throws
* ERR_INVALID_HTTP_TOKEN with its string form, and an array value
* validates each element. */
static void scr_http_req_validate_header_pair(const ScrDyn *name, const ScrDyn *value) {
ScrStr *label = scr_str_new("", 0);
if (name->kind == SCR_DYN_STR) {
scr_http_validate_header_name(name->v.str, label);
} else {
ScrStr *text = scr_dyn_string_coerce_js(name);
if (text != NULL) {
scr_http_throw_invalid_header_name(text, label);
scr_str_release(text);
}
}
scr_str_release(label);
if (scr_exc_pending()) return;
if (value->kind == SCR_DYN_ARR) {
for (size_t k = 0; k < value->v.arr.len && !scr_exc_pending(); k++)
scr_http_validate_header_value(name->v.str, value->v.arr.items[k]);
} else {
scr_http_validate_header_value(name->v.str, value);
}
}
/* util.inspect's one-line rendering of a short array ("[ 'x-a', 5 ]"): up
* to six primitive entries whose rendering stays within inspect's
* 80-column break length (a start of n + 11, plus the separators and each
* entry). NULL when inspect would group or break the entries, or an entry
* is not a primitive or renders outside ASCII (inspect counts UTF-16
* units). */
static ScrStr *scr_http_inspect_short_array(const ScrDyn *list) {
size_t n = list->v.arr.len;
if (n == 0 || n > 6) return NULL;
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "[ ");
size_t total = 2 * n + 11;
bool fits = true;
for (size_t i = 0; i < n && fits; i++) {
const ScrDyn *v = list->v.arr.items[i];
ScrStr *part = NULL;
switch (v->kind) {
case SCR_DYN_STR: part = scr_str_inspect_quoted(v->v.str, false); break;
case SCR_DYN_NUM:
part = v->v.num == 0 && signbit(v->v.num) ? scr_str_new("-0", 2) : scr_f64_to_scrstr(v->v.num);
break;
case SCR_DYN_BOOL: part = v->v.b ? scr_str_new("true", 4) : scr_str_new("false", 5); break;
case SCR_DYN_NULL: part = scr_str_new("null", 4); break;
case SCR_DYN_UNDEF: part = scr_str_new("undefined", 9); break;
default: break;
}
if (part == NULL) {
fits = false;
break;
}
for (size_t k = 0; k < part->len && fits; k++) fits = (unsigned char)part->data[k] < 0x80;
total += part->len;
if (total > 80) fits = false;
if (i > 0) scr_jb_puts(&b, ", ");
scr_jb_write(&b, part->data, part->len);
scr_str_release(part);
}
scr_jb_puts(&b, " ]");
ScrStr *out = scr_jb_finish(&b);
if (fits) return out;
scr_str_release(out);
return NULL;
}
/* Node's ERR_INVALID_ARG_VALUE for a flat headers array of odd length.
* Arrays whose inspected form scr_http_inspect_short_array cannot
* reproduce fence instead. */
static void scr_http_req_odd_headers_fail(const char *api, const ScrDyn *list) {
ScrStr *shown = scr_http_inspect_short_array(list);
if (shown == NULL) {
scr_http_req_opts_fence(api, "headers",
"the odd-length headers array error prints up to six short primitive entries here");
return;
}
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "The argument 'headers' is invalid. Received ");
scr_jb_write(&b, shown->data, shown->len);
scr_str_release(shown);
ScrStr *msg = scr_jb_finish(&b);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_ARG_VALUE");
scr_str_release(msg);
}
void scr_http_req_opts_fence(const char *api, const char *key, const char *hint) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, api);
scr_jb_puts(&b, " option '");
scr_jb_puts(&b, key);
scr_jb_puts(&b, "' has no static lowering — ");
scr_jb_puts(&b, hint);
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
}
void scr_http_req_opts_free(ScrHttpReqOpts *o) {
scr_str_release(o->host);
scr_str_release(o->path);
scr_str_release(o->method);
scr_str_release(o->socket_path);
if (o->headers) scr_arr_release(o->headers);
scr_dyn_release(o->agent);
scr_dyn_release(o->signal);
scr_dyn_release(o->view);
memset(o, 0, sizeof *o);
}
/* A member of the options record, NULL when absent or undefined. */
static const ScrDyn *scr_http_req_opt(const ScrDyn *opts, const char *key) {
const ScrDyn *v = scr_dyn_obj_get(opts, key, strlen(key));
return v == NULL || v->kind == SCR_DYN_UNDEF ? NULL : v;
}
static bool scr_http_req_truthy(const ScrDyn *v) { return v != NULL && scr_dyn_truthy(v); }
/* validatePort's accepted values: an integer in [0, 65535] or a string
* holding one. NaN when the value is not a valid port. */
static double scr_http_req_port_value(const ScrDyn *v) {
if (v->kind == SCR_DYN_NUM) {
double n = v->v.num;
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
}
if (v->kind == SCR_DYN_STR) {
bool blank = true;
for (size_t i = 0; i < v->v.str->len && blank; i++)
blank = isspace((unsigned char)v->v.str->data[i]) != 0;
if (blank) return NAN;
ScrStr *s = scr_str_retain(v->v.str);
double n = scr_string_to_number(s);
scr_str_release(s);
return n == n && trunc(n) == n && n >= 0 && n <= 65535 ? n : NAN;
}
return NAN;
}
/* Reads `opts` (borrowed) into `o` in the ClientRequest constructor's
* order. False with the exception pending (`o` holds nothing). */
bool scr_http_req_opts_read(const ScrDyn *opts, bool secure, const char *api, ScrHttpReqOpts *o) {
memset(o, 0, sizeof *o);
o->port = -1;
o->default_port = secure ? 443 : 80;
if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) {
o->view = scr_dyn_typed_ref_materialize(opts);
opts = o->view;
}
if (opts == NULL || opts->kind == SCR_DYN_UNDEF || opts->kind == SCR_DYN_NULL) {
opts = NULL; /* Node spreads it: every option takes its default */
} else if (opts->kind != SCR_DYN_OBJ) {
scr_http_req_opts_fence(api, "options", "pass an options object or a URL string");
scr_http_req_opts_free(o);
return false;
}
#define OPT(key) (opts != NULL ? scr_http_req_opt(opts, key) : NULL)
/* agent: false, null/undefined (the global agent), or an Agent */
const ScrDyn *agent = OPT("agent");
if (agent != NULL && agent->kind != SCR_DYN_NULL &&
!(agent->kind == SCR_DYN_BOOL && !agent->v.b) &&
!(agent->kind == SCR_DYN_HANDLE && agent->v.handle.tag == SCR_DYNH_HTTP_AGENT)) {
scr_dyn_prop_type_fail("options.agent", "one of Agent-like Object, undefined, or false", agent);
scr_http_req_opts_free(o);
return false;
}
if (agent != NULL) o->agent = scr_dyn_retain((ScrDyn *)agent);
bool agent_secure = secure;
if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
agent_secure = ((ScrHttpAgent *)agent->v.handle.ptr)->secure;
/* path: String(options.path) when truthy, without unescaped characters */
const ScrDyn *path = OPT("path");
if (scr_http_req_truthy(path)) {
o->path = scr_dyn_string_coerce_js(path);
if (o->path == NULL) {
scr_http_req_opts_free(o);
return false;
}
bool bad = false;
for (size_t i = 0; i < o->path->len && !bad; i++) {
unsigned char ch = (unsigned char)o->path->data[i];
if (ch < 0x21) bad = true;
else if (ch >= 0x80) {
/* U+0080..U+00FF (lead byte C2/C3) pass; anything above is unescaped */
if (ch != 0xc2 && ch != 0xc3) bad = true;
else i++;
}
}
if (bad) {
static const char msg[] = "Request path contains unescaped characters";
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_UNESCAPED_CHARACTERS");
scr_http_req_opts_free(o);
return false;
}
} else {
o->path = scr_str_new("/", 1);
}
/* protocol: options.protocol || the agent's, which must match */
const ScrDyn *protocol = OPT("protocol");
const char *expected = agent_secure ? "https:" : "http:";
if (scr_http_req_truthy(protocol)) {
ScrStr *p = scr_dyn_string_coerce_js(protocol);
if (p == NULL) {
scr_http_req_opts_free(o);
return false;
}
if (p->len != strlen(expected) || memcmp(p->data, expected, p->len) != 0) {
char msg[192];
int n = snprintf(msg, sizeof msg, "Protocol \"%.*s\" not supported. Expected \"%s\"",
(int)(p->len < 64 ? p->len : 64), p->data, expected);
scr_str_release(p);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_INVALID_PROTOCOL");
scr_http_req_opts_free(o);
return false;
}
scr_str_release(p);
}
/* defaultPort, then port (validated when the dial is TCP) */
const ScrDyn *default_port = OPT("defaultPort");
if (scr_http_req_truthy(default_port) && default_port->kind == SCR_DYN_NUM)
o->default_port = (int)default_port->v.num;
else if (agent != NULL && agent->kind == SCR_DYN_HANDLE)
o->default_port = (int)((ScrHttpAgent *)agent->v.handle.ptr)->default_port;
const ScrDyn *port = OPT("port");
/* host: validateHost(hostname) || validateHost(host) || 'localhost' */
static const char *const host_keys[] = { "hostname", "host" };
for (size_t i = 0; i < 2; i++) {
const ScrDyn *h = OPT(host_keys[i]);
if (h == NULL || h->kind == SCR_DYN_NULL) continue;
if (h->kind != SCR_DYN_STR) {
char name[24];
snprintf(name, sizeof name, "options.%s", host_keys[i]);
scr_dyn_prop_type_fail(name, "of type string or one of undefined or null", h);
scr_http_req_opts_free(o);
return false;
}
if (o->host == NULL && h->v.str->len > 0) o->host = scr_str_retain(h->v.str);
}
if (o->host == NULL) o->host = scr_str_new("localhost", 9);
const ScrDyn *set_host = OPT("setHost");
if (set_host != NULL && set_host->kind == SCR_DYN_BOOL && !set_host->v.b) {
scr_http_req_opts_fence(api, "setHost", "the Host header is always sent — leave setHost at its true default");
scr_http_req_opts_free(o);
return false;
}
const ScrDyn *set_default = OPT("setDefaultHeaders");
if (set_default != NULL && set_default->kind == SCR_DYN_BOOL && !set_default->v.b) {
scr_http_req_opts_fence(api, "setDefaultHeaders", "the default headers are always sent — leave setDefaultHeaders at its true default");
scr_http_req_opts_free(o);
return false;
}
const ScrDyn *socket_path = OPT("socketPath");
/* timeout: getTimerDuration(options.timeout, 'timeout') */
const ScrDyn *timeout = OPT("timeout");
if (timeout != NULL) {
if (timeout->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("timeout", "of type number", timeout);
scr_http_req_opts_free(o);
return false;
}
double t = timeout->v.num;
if (!(t >= 0 && isfinite(t))) {
char recv[48], msg[192];
scr_num_received(t, recv);
int n = snprintf(msg, sizeof msg,
"The value of \"timeout\" 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)n, "ERR_OUT_OF_RANGE");
scr_http_req_opts_free(o);
return false;
}
o->timeout = t;
}
/* method: a string token, upper-cased; GET when absent or empty */
const ScrDyn *method = OPT("method");
if (method != NULL && method->kind != SCR_DYN_NULL && method->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.method", "of type string", method);
scr_http_req_opts_free(o);
return false;
}
if (method != NULL && method->kind == SCR_DYN_STR && method->v.str->len > 0) {
o->method = scr_str_new(method->v.str->data, method->v.str->len);
for (size_t i = 0; i < o->method->len; i++) {
if (!scr_http_token_char((unsigned char)o->method->data[i])) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "Method must be a valid HTTP token [\"");
scr_jb_write(&b, o->method->data, o->method->len);
scr_jb_puts(&b, "\"]");
ScrStr *msg = scr_jb_finish(&b);
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, "ERR_INVALID_HTTP_TOKEN");
scr_str_release(msg);
scr_http_req_opts_free(o);
return false;
}
}
for (size_t i = 0; i < o->method->len; i++) {
unsigned char ch = (unsigned char)o->method->data[i];
if (ch >= 'a' && ch <= 'z') o->method->data[i] = (char)(ch - 'a' + 'A');
}
} else {
o->method = scr_str_new("GET", 3);
}
const ScrDyn *max_header = OPT("maxHeaderSize");
if (max_header != NULL) {
if (max_header->kind != SCR_DYN_NUM) {
scr_dyn_arg_type_fail("maxHeaderSize", "of type number", max_header);
scr_http_req_opts_free(o);
return false;
}
o->max_header_size = max_header->v.num;
o->has_max_header_size = true;
}
for (size_t i = 0; SCR_HTTP_REQ_FENCED_OPTIONS[i] != NULL; i++) {
if (scr_http_req_truthy(OPT(SCR_HTTP_REQ_FENCED_OPTIONS[i]))) {
scr_http_req_opts_fence(api, SCR_HTTP_REQ_FENCED_OPTIONS[i], SCR_HTTP_REQ_SUPPORTED);
scr_http_req_opts_free(o);
return false;
}
}
/* headers: an object (String(number) values, arrays as repeated lines)
* or the flat [name, value, ...] array */
const ScrDyn *headers = OPT("headers");
ScrDyn *headers_view = NULL;
if (headers != NULL && headers->kind == SCR_DYN_TYPED_REF) {
headers_view = scr_dyn_typed_ref_materialize(headers);
headers = headers_view;
}
if (headers != NULL && headers->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < headers->v.obj.len; i++) {
const ScrDynEntry *e = &headers->v.obj.entries[i];
ScrStr *name = scr_str_new(e->key, e->key_len);
ScrStr *label = scr_str_new("", 0);
scr_http_validate_header_name(name, label);
scr_str_release(label);
const ScrDyn *v = e->value;
if (!scr_exc_pending()) {
if (v->kind == SCR_DYN_ARR) {
for (size_t k = 0; k < v->v.arr.len && !scr_exc_pending(); k++)
scr_http_validate_header_value(name, v->v.arr.items[k]);
} else {
scr_http_validate_header_value(name, v);
}
}
scr_str_release(name);
if (scr_exc_pending()) break;
}
if (!scr_exc_pending()) o->headers = scr_http_dynh_pairs(headers);
} else if (headers != NULL && headers->kind == SCR_DYN_ARR) {
/* Node's _storeHeader checks that the flat [name, value, ...] form has
* an even length, then validates each name and value. The
* [[name, value], ...] entry form is not modeled. */
size_t n = headers->v.arr.len;
if (n > 0 && headers->v.arr.items[0]->kind == SCR_DYN_ARR) {
scr_http_req_opts_fence(
api, "headers",
"headers is an object of string or number values or a flat [name, value, ...] array here");
} else if (n % 2 != 0) {
scr_http_req_odd_headers_fail(api, headers);
} else {
for (size_t i = 0; i < n && !scr_exc_pending(); i += 2)
scr_http_req_validate_header_pair(headers->v.arr.items[i], headers->v.arr.items[i + 1]);
}
if (!scr_exc_pending()) o->headers = scr_http_dynh_flat_pairs(headers);
} else if (headers != NULL && headers->kind != SCR_DYN_NULL) {
scr_http_req_opts_fence(api, "headers", "headers is an object of string or number values here");
}
scr_dyn_release(headers_view);
if (scr_exc_pending()) {
scr_http_req_opts_free(o);
return false;
}
if (o->headers == NULL) o->headers = scr_arr_new(SCR_ELEM_STR, 0);
const ScrDyn *signal = OPT("signal");
if (signal != NULL) o->signal = scr_dyn_retain((ScrDyn *)signal);
/* the connection: a Unix socket (net validates the path) or host:port */
if (scr_http_req_truthy(socket_path)) {
if (socket_path->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.path", "of type string", socket_path);
scr_http_req_opts_free(o);
return false;
}
o->socket_path = scr_str_retain(socket_path->v.str);
if (scr_http_req_truthy(port)) {
double n = scr_http_req_port_value(port);
if (n == n) o->port = n;
}
} else if (scr_http_req_truthy(port)) {
double n = scr_http_req_port_value(port);
if (n != n) {
char detail[96], msg[192];
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
int len = snprintf(msg, sizeof msg, "Port should be >= 0 and < 65536. Received %s.", d);
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
scr_http_req_opts_free(o);
return false;
}
o->port = n;
}
#undef OPT
return true;
}
/* Builds the request `o` describes (its references stay with `o`). The
* signal is checked before construction and attached after it, like the
* literal lowering's sequence. +1, or NULL with the exception pending. */
ScrHttpClientReq *scr_http_req_opts_send(ScrHttpReqOpts *o, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener),
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
ScrStr *pool_key /*borrowed, nullable*/) {
if (o->signal != NULL) {
scr_http_client_signal_check(o->signal);
if (scr_exc_pending()) {
if (cb) scr_closure_release(cb);
return NULL;
}
}
/* without a port option the agent's default port applies (< 0), and
* without an agent the scheme's */
double port = o->port >= 0 ? o->port : o->agent != NULL && o->agent->kind == SCR_DYN_HANDLE ? -1
: o->default_port;
scr_http_next_socket_path = o->socket_path;
scr_http_socket_dialer = &scr_net_connect_path;
ScrHttpClientReq *c = scr_http_request_agent_ex(
o->host, port, o->path, o->method, o->timeout, o->headers, auto_end, o->agent, cb, fn,
o->default_port, wrap, wrap_ctx, pool_key);
if (c == NULL) return NULL;
if (o->has_max_header_size) {
scr_http_client_max_header_size(c, o->max_header_size);
if (scr_exc_pending()) return c;
}
if (o->signal != NULL) scr_http_client_add_abort_signal(c, o->signal, add_abort);
return c;
}
/* http.request/http.get(options[, cb]) with a runtime options record. */
ScrHttpClientReq *scr_http_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)) {
ScrHttpReqOpts o;
if (!scr_http_req_opts_read(opts, false, "http.request", &o)) {
if (cb) scr_closure_release(cb);
return NULL;
}
ScrHttpClientReq *c = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort, NULL, NULL, NULL);
scr_http_req_opts_free(&o);
return c;
}
+34
View File
@@ -371,6 +371,40 @@ ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* http.request/get with a runtime options record (a RequestOptions
* variable, or a literal spreading one or setting socketPath). The walk
* reads the record in the ClientRequest constructor's order; https adds
* its TLS options (scr_tls.c) and sends through the same path. */
typedef struct ScrHttpReqOpts {
ScrStr *host; /* +1: hostname || host || "localhost" */
double port; /* < 0: no port option */
int default_port; /* defaultPort, the agent's, or the scheme's */
ScrStr *path; /* +1 */
ScrStr *method; /* +1, upper-cased */
double timeout; /* 0 = none */
ScrArr *headers; /* +1 name/value pairs */
ScrDyn *agent; /* +1, NULL = the global agent */
ScrDyn *signal; /* +1, NULL = none */
ScrStr *socket_path; /* +1, NULL = dial host:port */
double max_header_size;
bool has_max_header_size;
ScrDyn *view; /* +1 materialized typed record, if any */
} ScrHttpReqOpts;
bool scr_http_req_opts_read(const ScrDyn *opts /*borrowed*/, bool secure, const char *api,
ScrHttpReqOpts *o); /* false = exception pending */
void scr_http_req_opts_free(ScrHttpReqOpts *o);
void scr_http_req_opts_fence(const char *api, const char *key, const char *hint); /* throws */
ScrHttpClientReq *scr_http_req_opts_send(ScrHttpReqOpts *o, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener),
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx,
ScrStr *pool_key /*borrowed, nullable*/); /* +1 */
ScrHttpClientReq *scr_http_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)); /* +1 */
ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)); /* +1 */
#ifdef SCR_RC_AUDIT
long scr_http_live_count(void);
#endif
+365 -1
View File
@@ -21,6 +21,7 @@
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/un.h> /* AF_UNIX: listen(path) and http's socketPath */
#if defined(__FreeBSD__) || defined(__DragonFly__) || defined(_AIX) || defined(__sun)
#include <sys/param.h>
#endif
@@ -442,6 +443,7 @@ struct ScrNetServer {
* '::' when bound_v6, else '0.0.0.0' — Node's own
* address().address answers) */
ScrStr *pending_err; /* deferred listen failure */
ScrStr *unix_path; /* listen(path): the bound socket file, removed at close */
size_t nconns;
ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls;
/* server.maxConnections (unset = undefined) and its 'drop' listeners */
@@ -532,6 +534,14 @@ struct ScrNetSocket {
/* the in-flight dial chain's LAST failure — surfaced (moved into
* pending_err) only when the list exhausts; a later success drops it */
ScrStr *dial_err;
/* a dial that is not the TCP sockaddr path (the Unix-socket connect):
* scr_net_sock_dial_peer hands the dial to it; unix_path is its target */
void (*dial_hook)(ScrNetSocket *s);
ScrStr *unix_path;
/* 'lookup' listeners and the resolutions waiting to reach them (the
* lookup unit's records; see scr_net_lookup_ops) */
ScrNetLs lookup_ls;
struct ScrNetLookupEv *lookup_evs;
ScrNetLs data_ls, end_ls, close_ls, err_ls, conn_ls, timeout_ls, readable_ls;
/* Flow control (pause()/resume()): user_paused holds reads OFF even
* with consumers (kernel/TCP backpressure is the buffer); flowing
@@ -608,6 +618,21 @@ typedef struct ScrNetTunnel {
size_t in_len;
} ScrNetTunnel;
/* The 'lookup' event's half of the runtime, installed by the first
* 'lookup' registration: programs without a 'lookup' listener never link
* it, and the hook sites below are a NULL check. */
typedef struct ScrNetLookupOps {
/* a host name resolved (ip/family) or failed (err_msg, ip NULL) */
void (*record)(ScrNetSocket *s, const char *host, size_t host_len, const char *ip, int family,
const char *err_msg);
void (*flush)(ScrNetSocket *s); /* deliver the recorded resolutions */
void (*free)(ScrNetSocket *s);
} ScrNetLookupOps;
/* Not static: the compiler must not resolve the calls through it to the
* one implementation, which would link that half into every program. */
extern SCR_TL const ScrNetLookupOps *scr_net_lookup_ops;
SCR_TL const ScrNetLookupOps *scr_net_lookup_ops = NULL;
static void scr_net_tunnel_free(ScrNetTunnel *t) {
if (t == NULL) return;
free(t->out);
@@ -648,6 +673,7 @@ void scr_net_server_release(ScrNetServer *s) {
scr_dyn_release(s->drop_max_connection);
scr_str_release(s->bound_host);
scr_str_release(s->pending_err);
scr_str_release(s->unix_path);
if (s->fd >= 0) scr_net_close_fd_raw(s->fd);
#ifdef SCR_RC_AUDIT
scr_net_live--;
@@ -685,6 +711,9 @@ void scr_net_sock_release(ScrNetSocket *s) {
free(s->dial_ips);
scr_str_release(s->dial_err);
scr_str_release(s->dial_host);
scr_str_release(s->unix_path);
scr_net_ls_drop(&s->lookup_ls);
if (s->lookup_evs != NULL) scr_net_lookup_ops->free(s);
scr_net_tunnel_free(s->tunnel);
free(s->wbuf);
free(s->rbuf);
@@ -1385,6 +1414,10 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) {
* one): the transport handshake starts, or 'connect' fires and buffered
* writes flush. */
static void scr_net_sock_connected(ScrNetSocket *s) {
if (s->lookup_evs != NULL) {
scr_net_lookup_ops->flush(s);
if (scr_exc_pending()) return;
}
scr_net_sock_update_write(s); /* keeps the filter iff bytes are buffered */
scr_net_sock_update_read(s);
scr_net_sock_touch(s);
@@ -1892,6 +1925,11 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab
scr_net_close_fd_raw(s->fd); /* stops accepting NOW (registrations dropped first) */
s->fd = -1;
s->listening = false;
#ifndef _WIN32
/* a Unix-socket server removes its socket file as it closes, like
* libuv's pipe close */
if (s->unix_path != NULL) unlink(s->unix_path->data);
#endif
}
/* A close BEFORE the deferred 'listening' delivered cancels it — Node's
* emitListeningNT checks the handle close() nulled, so the listen
@@ -1905,6 +1943,9 @@ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullab
s->close_seq = ++scr_net_close_seq;
}
s->closing = true; /* the sweep fires 'close' once nconns drains */
/* a server that never listened is not registered yet: Node still emits
* its 'close' (and runs close callbacks) */
scr_net_server_register(s);
/* Node's http server.close() first closes its idle keep-alive
* connections; busy ones end after their response (scr_http.c). Only
* HTTP connections carry an idle probe, so plain net servers keep
@@ -2207,7 +2248,10 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) {
* autoselect), and any other name is resolved when the dial starts. */
static void scr_net_sock_set_peer(ScrNetSocket *s, ScrStr *host /*borrowed, nullable*/) {
const char *h = host && host->len > 0 ? host->data : "localhost";
if (strcmp(h, "localhost") == 0) h = "127.0.0.1";
if (strcmp(h, "localhost") == 0) {
h = "127.0.0.1";
if (scr_net_lookup_ops != NULL) scr_net_lookup_ops->record(s, "localhost", 9, h, 4, NULL);
}
snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h);
struct in_addr a4;
struct in6_addr a6;
@@ -2253,6 +2297,9 @@ static void scr_net_sock_dial_host(ScrNetSocket *s) {
out = inet_ntop(AF_INET6, &((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr, ip, sizeof ip);
}
if (out == NULL) continue;
if (scr_net_lookup_ops != NULL)
scr_net_lookup_ops->record(s, host->data, host->len, ip, ai->ai_family == AF_INET6 ? 6 : 4,
NULL);
if (first == AF_UNSPEC) first = ai->ai_family;
size_t f = ai->ai_family == first ? 0 : 1;
if (fam[f] == NULL) {
@@ -2283,6 +2330,7 @@ static void scr_net_sock_dial_host(ScrNetSocket *s) {
char msg[320];
snprintf(msg, sizeof msg, "getaddrinfo %s %.*s", code,
(int)(host->len < 255 ? host->len : 255), host->data);
if (scr_net_lookup_ops != NULL) scr_net_lookup_ops->record(s, host->data, host->len, NULL, 0, msg);
free(ips);
scr_str_release(host);
s->pending_err = scr_str_new(msg, strlen(msg));
@@ -2304,6 +2352,10 @@ static void scr_net_sock_dial_host(ScrNetSocket *s) {
* queue — the socket exists, buffers writes, and dials when a slot
* frees). */
static void scr_net_sock_dial_peer(ScrNetSocket *s) {
if (s->dial_hook != NULL) {
s->dial_hook(s);
return;
}
if (s->dial_host != NULL) {
scr_net_sock_dial_host(s);
return;
@@ -2552,6 +2604,179 @@ void scr_net_sock_dial_start(ScrNetSocket *s) {
scr_net_sock_dial_peer(s);
}
/* ── Unix domain sockets ───────────────────────────────────────────────
*
* listen(path) and the http client's socketPath option. Node (libuv)
* reports these failures with the path where a TCP error names
* host:port: "connect ENOENT /run/x.sock", "listen EADDRINUSE: address
* already in use /run/x.sock". Windows has no Unix-socket support here
* (Node maps paths to named pipes there), so both forms fail with ENOTSUP. */
static const char *scr_net_unix_errname(int err) {
switch (err) {
case ENOENT: return "ENOENT";
case ENOTDIR: return "ENOTDIR";
case ENAMETOOLONG: return "ENAMETOOLONG";
case EAGAIN: return "EAGAIN";
case EPROTOTYPE: return "EPROTOTYPE";
default: return scr_net_errname(err);
}
}
static const char *scr_net_unix_reason(int err) {
switch (err) {
case EADDRINUSE: return "address already in use";
case EACCES: return "permission denied";
case ENOENT: return "no such file or directory";
case ENOTDIR: return "not a directory";
case ENAMETOOLONG: return "name too long";
case EINVAL: return "invalid argument";
default: return "operation not supported";
}
}
/* Fills a sockaddr_un for `path`; false (with errno set) when it does not
* fit, which libuv reports as ENAMETOOLONG too. */
#ifndef _WIN32
static bool scr_net_unix_addr(const ScrStr *path, struct sockaddr_un *sa) {
memset(sa, 0, sizeof *sa);
sa->sun_family = AF_UNIX;
if (path->len >= sizeof sa->sun_path || memchr(path->data, '\0', path->len) != NULL) {
errno = ENAMETOOLONG;
return false;
}
memcpy(sa->sun_path, path->data, path->len);
return true;
}
#endif
/* The dial_hook of a Unix-socket client: a nonblocking connect that
* completes through the same write-readiness path as TCP. A local
* connect usually settles at once; its failure is the deferred 'error'. */
static void scr_net_sock_dial_unix(ScrNetSocket *s) {
int err = ENOTSUP;
#ifndef _WIN32
struct sockaddr_un sa;
if (scr_net_unix_addr(s->unix_path, &sa)) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
fputs("scriptc: socket() failed\n", stderr);
abort();
}
scr_net_nonblock(fd);
s->fd = fd;
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == 0 || errno == EINPROGRESS) {
s->connecting = true;
scr_net_sock_update_write(s);
return;
}
}
err = errno;
#endif
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "connect ");
scr_jb_puts(&b, scr_net_unix_errname(err));
scr_jb_putc(&b, ' ');
scr_jb_write(&b, s->unix_path->data, s->unix_path->len);
s->pending_err = scr_jb_finish(&b);
s->had_error = true;
s->connecting = false;
scr_net_sock_close_fd(s);
}
/* A client socket for the Unix socket at `path` (borrowed). `deferred`
* creates it the way scr_net_connect_deferred does: registered and
* buffering, dialed by scr_net_sock_dial_start. +1. */
ScrNetSocket *scr_net_connect_path(ScrStr *path, bool deferred) {
ScrNetSocket *s = scr_net_sock_new();
s->unix_path = scr_str_retain(path);
s->dial_hook = &scr_net_sock_dial_unix;
if (!scr_net_poller_init()) {
fputs("scriptc: event poller init failed\n", stderr);
abort();
}
if (deferred) {
s->connecting = true;
s->dial_deferred = true;
} else {
scr_net_sock_dial_unix(s);
}
scr_net_sock_register(s);
return s;
}
/* server.address() in programs that listen on a path (the compiler links
* these only there): the Unix-socket path a listening server answers in
* place of the AddressInfo record (+1, "" for a TCP server), and the check
* a record-typed address() read makes first. */
ScrStr *scr_net_server_unix_path(ScrNetServer *s) {
return s->listening && s->unix_path != NULL ? scr_str_retain(s->unix_path) : scr_str_new("", 0);
}
void scr_net_server_address_check(ScrNetServer *s) {
if (!s->listening || s->unix_path == NULL) return;
static const char msg[] =
"server.address() of a server listening on a Unix socket is the socket path string; "
"type the result as string | AddressInfo | null (Node's declaration) to read it";
scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1);
}
/* server.listen(path[, cb]): binds a Unix socket now; a failure is the
* deferred 'error', success the deferred 'listening', as for a port. */
void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/,
ScrClosure *cb /*moves, nullable*/) {
/* Node's isPipeName: a string that reads as a non-negative number is a port */
double port = scr_string_to_number(path);
if (port >= 0) {
scr_net_listen_any(s, port, false, cb);
return;
}
if (cb) scr_net_ls_add(&s->listening_cbs, cb, NULL, true);
if (s->listening || s->close_emitted) return;
if (!scr_net_poller_init()) {
fputs("scriptc: event poller init failed\n", stderr);
abort();
}
int err = ENOTSUP;
#ifndef _WIN32
struct sockaddr_un sa;
if (scr_net_unix_addr(path, &sa)) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
fputs("scriptc: socket() failed\n", stderr);
abort();
}
scr_net_nonblock(fd);
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == 0 && listen(fd, 511) == 0) {
s->fd = fd;
s->listening = true;
s->emit_listening = true;
scr_str_release(s->unix_path);
s->unix_path = scr_str_retain(path);
scr_net_watch_read(fd, s, true);
scr_net_server_register(s);
return;
}
err = errno;
close(fd);
} else {
err = errno;
}
#endif
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "listen ");
scr_jb_puts(&b, scr_net_unix_errname(err));
scr_jb_puts(&b, ": ");
scr_jb_puts(&b, scr_net_unix_reason(err));
scr_jb_putc(&b, ' ');
scr_jb_write(&b, path->data, path->len);
if (!s->pending_err) s->pending_err = scr_jb_finish(&b);
else scr_str_release(scr_jb_finish(&b));
scr_net_server_register(s); /* the sweep delivers the failure */
}
/* ── the caller-lookup dial (net.connect with a lookup option) ─────────
*
* portless's createLoopbackConnection: connect({ host, port,
@@ -4094,6 +4319,136 @@ void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowe
(void *)&scr_net_dyn_fire_arg, once);
}
/* ── 'lookup' ─────────────────────────────────────────────────────────── */
typedef struct ScrNetLookupEv {
struct ScrNetLookupEv *next;
ScrStr *host;
ScrStr *ip; /* NULL with err_msg */
int family;
ScrStr *err_msg; /* "getaddrinfo ENOTFOUND host" */
} ScrNetLookupEv;
static void scr_net_lookup_record(ScrNetSocket *s, const char *host, size_t host_len,
const char *ip, int family, const char *err_msg) {
ScrNetLookupEv *ev = calloc(1, sizeof *ev);
if (!ev) scr_net_oom();
ev->host = scr_str_new(host, host_len);
ev->ip = ip != NULL ? scr_str_new(ip, strlen(ip)) : NULL;
ev->family = family;
ev->err_msg = err_msg != NULL ? scr_str_new(err_msg, strlen(err_msg)) : NULL;
ScrNetLookupEv **link = &s->lookup_evs;
while (*link) link = &(*link)->next;
*link = ev;
}
static void scr_net_lookup_ev_free(ScrNetLookupEv *ev) {
scr_str_release(ev->host);
scr_str_release(ev->ip);
scr_str_release(ev->err_msg);
free(ev);
}
static void scr_net_lookup_free(ScrNetSocket *s) {
while (s->lookup_evs) {
ScrNetLookupEv *ev = s->lookup_evs;
s->lookup_evs = ev->next;
scr_net_lookup_ev_free(ev);
}
}
/* Node emits ('lookup', err, address, family, host) from the resolver's
* answer, before it dials: here the resolutions recorded at the dial reach
* the listeners just before 'connect' (or the lookup's 'error'). */
static void scr_net_lookup_flush(ScrNetSocket *s) {
while (s->lookup_evs && !scr_exc_pending()) {
ScrNetLookupEv *ev = s->lookup_evs;
s->lookup_evs = ev->next;
if (s->lookup_ls.n > 0) {
ScrDyn *args[4];
if (ev->err_msg != NULL) {
ScrError *e = scr_error_new(SCR_ERR_ERROR, ev->err_msg);
const char *code = strchr(ev->err_msg->data, ' ');
if (code != NULL) {
char buf[32];
size_t n = strcspn(code + 1, " ");
if (n < sizeof buf) {
memcpy(buf, code + 1, n);
buf[n] = '\0';
scr_error_set_code(e, buf);
}
}
args[0] = scr_dyn_from_error(e);
scr_error_release(e);
args[1] = scr_dyn_retain(scr_dyn_undefined());
args[2] = scr_dyn_retain(scr_dyn_undefined());
} else {
args[0] = scr_dyn_new_null();
args[1] = scr_dyn_new_str(ev->ip);
args[2] = scr_dyn_new_num((double)ev->family);
}
args[3] = scr_dyn_new_str(ev->host);
ScrNetL *snap;
size_t n = scr_net_ls_snapshot(&s->lookup_ls, &snap);
scr_dyn_this_push(s, SCR_DYNH_NET_SOCKET);
for (size_t i = 0; i < n; i++) {
if (!scr_exc_pending()) {
ScrDyn *fn = scr_dyn_listener_fn(snap[i].cb);
scr_dyn_release(scr_dyn_call(fn, args, 4, "listener"));
scr_dyn_release(fn);
}
scr_closure_release(snap[i].cb);
}
scr_dyn_this_pop();
free(snap);
for (size_t i = 0; i < 4; i++) scr_dyn_release(args[i]);
}
scr_net_lookup_ev_free(ev);
}
}
static const ScrNetLookupOps scr_net_lookup_ops_impl = {
&scr_net_lookup_record,
&scr_net_lookup_flush,
&scr_net_lookup_free,
};
/* The C main of a program with a 'lookup' listener installs the lookup
* records before any socket dials. */
void scr_net_lookup_install(void) { scr_net_lookup_ops = &scr_net_lookup_ops_impl; }
/* socket.on/once('lookup', listener) with a dyn function listener. The
* socket resolved when net.connect ran, before the listener was added; it
* delivers its recorded answers at the connect, like Node's asynchronous
* lookup. */
void scr_net_sock_on_lookup(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once) {
if (s->close_emitted) return;
scr_net_ls_add(&s->lookup_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg),
(void *)&scr_net_dyn_fire_arg, once);
}
/* server.close((err) => ...): the callback runs at 'close' with undefined,
* or with Node's ERR_SERVER_NOT_RUNNING when the server was not listening. */
static void scr_net_close_not_running_fire(ScrClosure *cb) {
ScrDyn *fn = scr_dyn_listener_fn(cb);
static const char msg[] = "Server is not running.";
ScrStr *text = scr_str_new(msg, sizeof msg - 1);
ScrError *e = scr_error_new(SCR_ERR_ERROR, text);
scr_str_release(text);
scr_error_set_code(e, "ERR_SERVER_NOT_RUNNING");
ScrDyn *err = scr_dyn_from_error(e);
scr_error_release(e);
scr_dyn_release(scr_dyn_call(fn, &err, 1, "listener"));
scr_dyn_release(err);
scr_dyn_release(fn);
}
void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed*/) {
ScrClosure *cb = scr_dyn_listener_closure_fn(
listener, s->listening ? (void *)&scr_dyn_listener_fire0 : (void *)&scr_net_close_not_running_fire);
scr_net_server_close(s, cb);
}
void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) {
if (s->close_emitted) return;
scr_net_ls_add(&s->wcb_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg),
@@ -4752,6 +5107,13 @@ static void scr_net_sweep(void) {
return;
}
}
if (sock->lookup_evs != NULL && sock->pending_err) {
scr_net_lookup_ops->flush(sock);
if (scr_exc_pending()) {
scr_net_sock_release(sock);
return;
}
}
if (sock->pending_err) {
ScrStr *msg = sock->pending_err;
sock->pending_err = NULL;
@@ -4810,6 +5172,7 @@ static void scr_net_sweep(void) {
scr_net_ls_drop(&sock->finish_ls);
scr_net_ls_drop(&sock->wcb_ls);
scr_net_ls_drop(&sock->ready_ls);
scr_net_ls_drop(&sock->lookup_ls);
if (sock->pipe_dst) {
scr_net_sock_release(sock->pipe_dst);
sock->pipe_dst = NULL;
@@ -5011,6 +5374,7 @@ static void scr_net_cleanup_atexit(void) {
scr_net_ls_drop(&s->conn_ls);
scr_net_ls_drop(&s->timeout_ls);
scr_net_ls_drop(&s->readable_ls);
scr_net_ls_drop(&s->lookup_ls);
/* Tear down the protocol layer's parser NOW: an in-flight http
* exchange (a handler that threw before res.end()) holds req/res,
* and the res holds this socket back — a cycle the lean untraced
+10
View File
@@ -164,6 +164,11 @@ void scr_net_server_timeout_option_set(ScrNetServer *s, double field, const stru
ScrStr *scr_net_server_addr_ip(ScrNetServer *s); /* +1 — address().address */
ScrStr *scr_net_server_addr_family(ScrNetServer *s); /* +1 — address().family */
void scr_net_server_close(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/);
void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed*/);
void scr_net_sock_on_lookup(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once);
void scr_net_lookup_install(void);
ScrStr *scr_net_server_unix_path(ScrNetServer *s); /* +1, "" unless listening on a path */
void scr_net_server_address_check(ScrNetServer *s); /* throws for a Unix-socket server */
void scr_net_server_close_all_connections(ScrNetServer *s);
void scr_net_server_close_idle_connections(ScrNetServer *s);
ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s); /* +1 */
@@ -237,6 +242,11 @@ bool scr_net_sock_readable(ScrNetSocket *s);
/* The deferred dial (the http agent's maxSockets queue): the socket
* registers "connecting" and buffers writes; dial_start runs the dial. */
ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/); /* +1 */
/* A client socket for the Unix socket at `path`, dialed now or (deferred)
* by scr_net_sock_dial_start. A failure is the deferred 'error'. */
ScrNetSocket *scr_net_connect_path(ScrStr *path /*borrowed*/, bool deferred); /* +1 */
/* server.listen(path[, cb]): a Unix-socket server, its file removed at close. */
void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, ScrClosure *cb /*moves, nullable*/);
void scr_net_sock_dial_start(ScrNetSocket *s);
void scr_net_sock_set_timeout(ScrNetSocket *s, double ms);
void scr_net_sock_on_timeout(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
+68
View File
@@ -1512,6 +1512,74 @@ static ScrBytes *scr_tls_pem_dyn(const ScrDyn *v, const char *what, bool concat)
return NULL;
}
/* https.request/get with a runtime options record: the http walk, then
* the TLS options the literal https path lowers (rejectUnauthorized, ca,
* servername). Other tls.connect options fence when they carry a value. */
static const char *const SCR_HTTPS_REQ_FENCED_OPTIONS[] = {
"ALPNProtocols", "cert", "checkServerIdentity", "ciphers", "crl", "dhparam", "ecdhCurve",
"honorCipherOrder", "key", "maxVersion", "minVersion", "passphrase", "pfx", "secureContext",
"secureOptions", "secureProtocol", "session", "sessionIdContext", "sigalgs", NULL,
};
ScrHttpClientReq *scr_https_request_dyn(const ScrDyn *opts /*borrowed*/, bool auto_end,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn,
ScrDyn *(*add_abort)(ScrDyn *signal, ScrDyn *listener)) {
ScrHttpReqOpts o;
if (!scr_http_req_opts_read(opts, true, "https.request", &o)) {
if (cb) scr_closure_release(cb);
return NULL;
}
const ScrDyn *rec = o.view != NULL ? o.view : opts;
bool reject = true;
ScrBytes *ca = NULL;
ScrStr *servername = NULL;
bool ok = true;
if (rec != NULL && rec->kind == SCR_DYN_OBJ) {
for (size_t i = 0; ok && SCR_HTTPS_REQ_FENCED_OPTIONS[i] != NULL; i++) {
const char *key = SCR_HTTPS_REQ_FENCED_OPTIONS[i];
const ScrDyn *v = scr_dyn_obj_get(rec, key, strlen(key));
if (v != NULL && scr_tls_dyn_truthy(v)) {
scr_http_req_opts_fence("https.request", key,
"rejectUnauthorized, ca, and servername are the supported TLS options");
ok = false;
}
}
const ScrDyn *r = ok ? scr_dyn_obj_get(rec, "rejectUnauthorized", 18) : NULL;
if (r != NULL && r->kind == SCR_DYN_BOOL && !r->v.b) reject = false;
const ScrDyn *c = ok ? scr_dyn_obj_get(rec, "ca", 2) : NULL;
if (c != NULL && c->kind != SCR_DYN_UNDEF && c->kind != SCR_DYN_NULL) {
ca = scr_tls_pem_dyn(c, "an https.request 'ca' option", true);
if (ca == NULL) ok = false;
}
const ScrDyn *sn = ok ? scr_dyn_obj_get(rec, "servername", 10) : NULL;
if (sn != NULL && sn->kind != SCR_DYN_UNDEF && sn->kind != SCR_DYN_NULL) {
if (sn->kind != SCR_DYN_STR) {
scr_dyn_prop_type_fail("options.servername", "of type string", sn);
ok = false;
} else {
servername = scr_str_retain(sn->v.str);
}
}
}
if (!ok) {
if (ca) scr_bytes_release(ca);
scr_http_req_opts_free(&o);
if (cb) scr_closure_release(cb);
return NULL;
}
const char *ca_data = ca ? (const char *)ca->data : NULL;
size_t ca_len = ca ? ca->len : 0;
ScrTlsCli *cli = scr_tls_cli_for(o.host, reject, ca_data, ca_len, servername);
ScrStr *key = scr_tls_pool_key(o.host, reject, ca_data, ca_len, servername);
scr_str_release(servername);
ScrHttpClientReq *req = scr_http_req_opts_send(&o, auto_end, cb, fn, add_abort,
&scr_tls_cli_wrap, cli, key);
scr_str_release(key);
if (ca) scr_bytes_release(ca);
scr_http_req_opts_free(&o);
return req;
}
/* The literal path's runtime-valued cert/key extraction (the compiler's
* tls.pemDyn libCall): `what` arrives precomposed. undefined throws — the
* literal walk required the member's presence. */
+109
View File
@@ -0,0 +1,109 @@
// HTTP over a Unix domain socket: http.createServer().listen(path) and
// http.request / http.get with a socketPath option, from an object literal,
// from a RequestOptions variable, and from a literal that spreads one. The
// Host header defaults to localhost. A missing socket fails the request
// with ENOENT, a second listen on a bound path fails with EADDRINUSE, and
// closing the server removes the socket file.
import * as http from "node:http";
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const sock = join(tmpdir(), `scriptc-http-${process.pid}.sock`);
const shown = (text: string): string => text.split(sock).join("<sock>");
const server = http.createServer((req, res) => {
let body = "";
req.on("data", (chunk: Buffer) => {
body += chunk.toString();
});
req.on("end", () => {
res.setHeader("content-type", "text/plain");
res.end(`${req.method} ${req.url} host=${req.headers.host} x=${req.headers["x-n"]} body=${body}`);
});
});
function collect(res: http.IncomingMessage, label: string, done: () => void): void {
let text = "";
res.on("data", (chunk: Buffer) => {
text += chunk.toString();
});
res.on("end", () => {
console.log(label, res.statusCode, res.headers["content-type"], text);
done();
});
}
function literal(): Promise<void> {
return new Promise<void>((resolve) => {
const req = http.request(
{ socketPath: sock, path: "/literal", method: "POST", headers: { "x-n": "1" } },
(res) => collect(res, "literal", resolve),
);
req.end("one");
});
}
const base: http.RequestOptions = { socketPath: sock };
function fromVariable(): Promise<void> {
return new Promise<void>((resolve) => {
const headers: Record<string, string | number> = { "x-n": 2, "content-type": "text/plain" };
const options: http.RequestOptions = { ...base, path: "/variable", method: "PUT", headers, timeout: 5000 };
const req = http.request(options, (res) => collect(res, "variable", resolve));
req.write("two");
req.end();
});
}
function spread(): Promise<void> {
return new Promise<void>((resolve) => {
const req = http.request({ ...base, path: "/spread", method: "POST", headers: { "x-n": "3" } }, (res) =>
collect(res, "spread", resolve),
);
req.end("three");
});
}
function get(): Promise<void> {
return new Promise<void>((resolve) => {
const options: http.RequestOptions = { socketPath: sock, path: "/get?q=4" };
http.get(options, (res) => collect(res, "get", resolve));
});
}
function missing(): Promise<void> {
return new Promise<void>((resolve) => {
const options: http.RequestOptions = { socketPath: `${sock}.missing`, path: "/" };
const req = http.request(options, () => console.log("unexpected response"));
req.on("error", (err: NodeJS.ErrnoException) => {
console.log("missing", err.code, shown(err.message));
resolve();
});
req.end();
});
}
function busy(): Promise<void> {
return new Promise<void>((resolve) => {
const other = http.createServer();
other.on("error", (err: NodeJS.ErrnoException) => {
console.log("busy", err.code, shown(err.message));
resolve();
});
other.listen(sock);
});
}
server.listen(sock, async () => {
console.log("listening", existsSync(sock));
await literal();
await fromVariable();
await spread();
await get();
await missing();
await busy();
server.close(() => {
console.log("closed", existsSync(sock));
});
});
@@ -0,0 +1,79 @@
// http.request options read at run time: a RequestOptions value chosen by
// a runtime condition (a TCP endpoint with a string port, or a socket
// path), spread into an options object with a timeout and an optional
// signal; agent: false; an aborted signal; a flat [name, value, ...]
// headers array; and Node's validation errors for the method, port, host,
// timeout, path, protocol and agent options and for flat header names and
// values.
import * as http from "node:http";
const server = http.createServer((req, res) => {
const host = (req.headers.host ?? "").replace(/:\d+$/, ":<port>");
res.end(`${req.method} ${req.url} host=${host} connection=${req.headers.connection} n=${req.headers["x-n"]}`);
});
function send(label: string, options: http.RequestOptions): Promise<void> {
return new Promise<void>((resolve) => {
const req = http.request(options, (res) => {
let text = "";
res.on("data", (chunk: Buffer) => {
text += chunk.toString();
});
res.on("end", () => {
console.log(label, res.statusCode, text);
resolve();
});
});
req.on("error", (err: NodeJS.ErrnoException) => {
console.log(label, "error", err.name, err.code, err.message);
resolve();
});
req.end();
});
}
function invalid(label: string, options: http.RequestOptions): void {
try {
const req = http.request(options);
req.on("error", () => {});
req.destroy();
console.log(label, "accepted");
} catch (err) {
const e = err as NodeJS.ErrnoException;
console.log(label, e.name, e.code, e.message);
}
}
server.listen(0, "127.0.0.1", async () => {
const port = (server.address() as { port: number }).port;
const endpoint = process.env.NO_SUCH_ENDPOINT_VAR ?? `http://127.0.0.1:${port}`;
const url = new URL(endpoint);
const location: http.RequestOptions = process.env.NO_SUCH_SOCKET_VAR
? { socketPath: process.env.NO_SUCH_SOCKET_VAR }
: { port: url.port, host: url.hostname };
const headers: Record<string, string | number> = { "x-n": 1 };
const controller: AbortController | undefined = undefined;
const options = { ...location, path: "/rpc", method: "post", headers, timeout: 2000, signal: controller };
// The flat [name, value, ...] headers form; @types/node types headers as an object.
const flat = (path: string, list: unknown[]): http.RequestOptions =>
({ ...location, path, headers: list }) as unknown as http.RequestOptions;
await send("spread", options);
await send("agent false", { ...location, path: "/close", agent: false });
const aborted = new AbortController();
aborted.abort();
await send("aborted", { ...location, path: "/never", signal: aborted.signal });
await send("flat headers", flat("/flat", ["host", "flat.test", "x-n", 2]));
invalid("method", { ...location, method: "g t" });
invalid("port", { host: "127.0.0.1", port: "http" });
invalid("host", { hostname: 5 } as unknown as http.RequestOptions);
invalid("timeout", { ...location, timeout: -1 });
invalid("path", { ...location, path: "/a b" });
invalid("protocol", { ...location, protocol: "https:" });
invalid("agent", { ...location, agent: 5 } as unknown as http.RequestOptions);
invalid("flat name", flat("/", ["x n", "1"]));
invalid("flat number name", flat("/", [5, "1"]));
invalid("flat crlf value", flat("/", ["x-a", "ok\r\nx-b: injected"]));
invalid("flat crlf array value", flat("/", ["x-a", ["ok", "b\r\nx-c: injected"]]));
invalid("flat odd length", flat("/", ["x-a"]));
server.close();
});
@@ -0,0 +1,61 @@
// https.request with options read at run time: a RequestOptions value
// spread with the CA, over TCP and over a Unix socket, an unverifiable
// certificate with and without rejectUnauthorized, and the protocol check.
import * as https from "node:https";
import { readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ca = readFileSync("tests/fixtures/server/certs/ca.pem");
const sock = join(tmpdir(), `scriptc-https-${process.pid}.sock`);
const server = https.createServer(
{
cert: readFileSync("tests/fixtures/server/certs/localhost.pem"),
key: readFileSync("tests/fixtures/server/certs/localhost-key.pem"),
},
(req, res) => res.end(`${req.method} ${req.url} host=${(req.headers.host ?? "").replace(/:\d+$/, ":<port>")}`),
);
const unix = https.createServer(
{
cert: readFileSync("tests/fixtures/server/certs/localhost.pem"),
key: readFileSync("tests/fixtures/server/certs/localhost-key.pem"),
},
(req, res) => res.end(`unix ${req.method} ${req.url} host=${req.headers.host}`),
);
function send(label: string, options: https.RequestOptions): Promise<void> {
return new Promise<void>((resolve) => {
const req = https.request(options, (res) => {
let body = "";
res.on("data", (c: Buffer) => (body += c.toString()));
res.on("end", () => {
console.log(label, res.statusCode, body);
resolve();
});
});
req.on("error", (err: NodeJS.ErrnoException) => {
console.log(label, "error", err.code);
resolve();
});
req.end();
});
}
server.listen(0, () => {
const port = (server.address() as { port: number }).port;
unix.listen(sock, async () => {
const tcp: https.RequestOptions = { host: "localhost", port };
await send("tcp", { ...tcp, path: "/a", ca, agent: false });
await send("unix", { socketPath: sock, path: "/b", ca, agent: false });
await send("unverified", { ...tcp, path: "/c", agent: false });
await send("unchecked", { ...tcp, path: "/d", rejectUnauthorized: false, agent: false });
try {
https.request({ ...tcp, protocol: "http:" });
} catch (err) {
console.log("protocol", (err as NodeJS.ErrnoException).code, (err as Error).message);
}
server.close();
unix.close();
});
});
@@ -0,0 +1,75 @@
// A socket's 'lookup' event for a host name (none for an IP address),
// string 'data' listeners after setEncoding on both sides of an HTTP
// exchange, res.end() of a string or undefined, and the close callback of
// a server that never listened.
import * as http from "node:http";
import * as net from "node:net";
function lookups(): Promise<void> {
return new Promise<void>((resolve) => {
const tcp = net.createServer((s) => s.end("hi"));
tcp.listen(0, "127.0.0.1", () => {
const port = (tcp.address() as net.AddressInfo).port;
const named = net.connect({ host: "localhost", port });
let reported = false;
named.on("lookup", (err: Error | null, address: string, family: number, host: string) => {
// macOS also answers ::1; report the IPv4 answer the dial uses.
if (family === 4 && !reported) {
reported = true;
console.log("lookup", err, address, family, host);
}
});
named.once("connect", () => console.log("connect after lookup", reported));
named.on("data", () => {});
named.on("close", () => {
const direct = net.connect({ host: "127.0.0.1", port });
// A 'lookup' listener that never runs and holds its socket: closing
// the socket drops it.
direct.once("lookup", () => console.log("unexpected lookup", direct.remotePort));
direct.once("connect", () => console.log("connect without lookup"));
direct.on("data", () => {});
direct.on("close", () => tcp.close(() => resolve()));
});
});
});
}
const server = http.createServer((req, res) => {
req.setEncoding("utf8");
const chunks: string[] = [];
req.on("data", (chunk: string) => chunks.push(chunk));
req.on("end", () => {
const body = chunks.join("");
res.writeHead(200, { "content-type": "text/plain" });
const payload = body ? body.toUpperCase() : undefined;
res.end(payload);
});
});
function post(port: number, body: string): Promise<void> {
return new Promise<void>((resolve) => {
const req = http.request({ host: "127.0.0.1", port, method: "POST", path: "/" }, (res) => {
res.setEncoding("utf8");
let text = "";
res.on("data", (part: string) => {
text += part;
});
res.on("end", () => {
console.log("response", JSON.stringify(body), "->", JSON.stringify(text));
resolve();
});
});
req.end(body);
});
}
lookups().then(() => {
server.listen(0, "127.0.0.1", async () => {
const port = (server.address() as net.AddressInfo).port;
await post(port, "hello");
await post(port, "");
server.close(() => console.log("closed"));
const idle = http.createServer();
idle.close(() => console.log("never listened closed"));
});
});
+68
View File
@@ -0,0 +1,68 @@
// Options derived from RequestInit under @types/node: an interface that
// extends Omit<RequestInit, ...> holds fetch's BodyInit and HeadersInit
// members, which are destructured, narrowed and passed back into fetch,
// with the remaining members carried as an Omit<...> of RequestInit.
import * as http from "node:http";
import type { AddressInfo } from "node:net";
type PassthroughRequestInit = Omit<RequestInit, "signal" | "redirect" | "dispatcher">;
type FetchBody = NonNullable<RequestInit["body"]>;
interface PostOptions extends PassthroughRequestInit {
timeoutMs?: number;
}
function describe(body: FetchBody | null | undefined): string {
return typeof body === "string" ? `text(${body.length})` : body == null ? "none" : "binary or form";
}
async function send(params: {
port: number;
method: string;
body: FetchBody | null | undefined;
headers: RequestInit["headers"];
init: Omit<PassthroughRequestInit, "body" | "headers" | "method">;
timeoutMs: number;
}): Promise<Response> {
const { port, method, body, headers, init, timeoutMs } = params;
const fetchInit: RequestInit = {
...init,
method,
headers,
body: body ?? undefined,
signal: AbortSignal.timeout(timeoutMs),
};
return fetch(`http://127.0.0.1:${port}/`, fetchInit);
}
async function post(port: number, options: PostOptions = {}): Promise<string> {
const { timeoutMs, body, method, headers, ...rest } = options;
const res = await send({
port,
method: method ?? "POST",
body,
headers,
init: rest,
timeoutMs: timeoutMs ?? 5000,
});
return `${describe(body)} -> ${res.status} ${await res.text()}`;
}
const server = http.createServer((req, res) => {
let data = "";
req.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
req.on("end", () => {
res.end(`${req.method} type=${req.headers["content-type"]} x=${req.headers["x-n"]} body=${data}`);
});
});
server.listen(0, "127.0.0.1", async () => {
const port = (server.address() as AddressInfo).port;
console.log(await post(port, { body: "plain", headers: { "x-n": "1" } }));
console.log(await post(port, { body: new URLSearchParams("a=1&b=2"), headers: [["x-n", "2"]] }));
console.log(await post(port, { body: Buffer.from("bytes"), method: "PUT", timeoutMs: 2000 }));
console.log(await post(port, { method: "DELETE", headers: new Headers({ "x-n": "4" }) }));
server.close();
});
+80
View File
@@ -0,0 +1,80 @@
// An HTTP client that reaches its API over a Unix socket unless an
// endpoint URL is configured, typed by @types/node: the location is an
// http.RequestOptions chosen at run time, spread into each request's
// options with a path, method, headers, timeout and an optional signal.
// server.address() answers the socket path (a TCP server's the record), and
// server.close callbacks take Node's ERR_SERVER_NOT_RUNNING error.
import * as http from "node:http";
import { existsSync } from "node:fs";
import type { AddressInfo } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
const socketPath: string = join(tmpdir(), `scriptc-api-${process.pid}.sock`);
const endpoint = process.env.NO_SUCH_API_ENDPOINT_VAR ? new URL(process.env.NO_SUCH_API_ENDPOINT_VAR) : undefined;
const location: http.RequestOptions = endpoint
? { port: endpoint.port, host: endpoint.hostname }
: { socketPath };
function call(path: string, body: string, timeout: number, ac?: AbortController): Promise<string> {
return new Promise<string>((resolve, reject) => {
const headers: Record<string, string | number> = {
"content-type": "application/json",
"content-length": Buffer.byteLength(body),
};
const options = {
...location,
path,
method: "POST",
headers,
timeout,
signal: ac?.signal,
};
const request = http.request(options, (res) => {
let data = "";
res.on("data", (part: Buffer) => {
data += part.toString();
});
res.on("error", reject);
res.on("end", () => resolve(`${res.statusCode} ${data}`));
});
request.on("error", reject);
request.on("timeout", () => request.destroy(new Error("timed out")));
request.end(body);
});
}
const server = http.createServer((req, res) => {
let body = "";
req.on("data", (part: Buffer) => {
body += part.toString();
});
req.on("end", () => {
res.end(`${req.method} ${req.url} host=${req.headers.host} body=${body}`);
});
});
function describeAddress(address: string | AddressInfo | null): string {
if (address === null) return "null";
return typeof address === "string" ? address.split(socketPath).join("<sock>") : `${address.family} port>0=${address.port > 0}`;
}
server.listen(socketPath, async () => {
console.log("address", describeAddress(server.address()));
const tcp = http.createServer();
await new Promise<void>((resolve) => tcp.listen(0, "127.0.0.1", () => resolve()));
console.log("tcp address", describeAddress(tcp.address()));
tcp.close();
console.log(await call("/v1/start", JSON.stringify({ id: 1 }), 5000));
console.log(await call("/v1/status", JSON.stringify({ id: 2 }), 5000, new AbortController()));
try {
const ac = new AbortController();
ac.abort();
await call("/v1/never", "{}", 5000, ac);
} catch (err) {
console.log("aborted", (err as Error).name);
}
server.close((err) => console.log("closed", err === undefined, existsSync(socketPath)));
server.close((err) => console.log("closed again", err?.name, (err as NodeJS.ErrnoException)?.code, err?.message));
http.createServer().close((err) => console.log("never listened", (err as NodeJS.ErrnoException)?.code));
});
@@ -25,15 +25,6 @@ options-record.ts:20:22 - error SC2020: 'get with agent: false and a non-literal
hint: agent: false lowers by injecting Connection: close into a LITERAL headers object — add connection: "close" to the record instead, or write the headers inline
options-record.ts:23:22 - error SC2020: 'get option 'socketPath'' is part of the standard library types but has no scriptc lowering yet
22 | // Documented client options with no lowering fence by name.
23 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
| ^~~~~~~~~~~~~~~~~~~~~~~~~
24 | http.get({ port: 80, localAddress: "127.0.0.1" }, () => {});
hint: hostname/host, port, path, method, timeout, headers, agent, signal, and maxHeaderSize are the supported options
options-record.ts:24:22 - error SC2020: 'get option 'localAddress'' is part of the standard library types but has no scriptc lowering yet
23 | http.get({ port: 80, socketPath: "/tmp/sock.s" }, () => {});
@@ -0,0 +1 @@
{ "plain": 3157, "san": 11133 }
@@ -0,0 +1 @@
{ "plain": 3186, "san": 59437 }
@@ -0,0 +1 @@
{ "plain": 3069, "san": 6484 }
@@ -0,0 +1 @@
{ "plain": 2847, "san": 10348 }
+38
View File
@@ -462,6 +462,44 @@ test("node-types: lookup-option unions and narrowed header-record entries under
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: http.request over a Unix socket with RequestOptions read at run time", async () => {
const outDir = outDirFor("http-socket-path");
const entry = join(nodeTypesDir, "http-socket-path.ts");
const result = await compile(entry, {
outPath: join(outDir, "http-socket-path"),
outDir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const native = await execFileAsync(result.binaryPath);
const node = await execFileAsync(process.execPath, [
"--disable-warning=MODULE_TYPELESS_PACKAGE_JSON",
entry,
]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: RequestInit-derived options carry BodyInit and HeadersInit into fetch", async () => {
const outDir = outDirFor("fetch-init-members");
const entry = join(nodeTypesDir, "fetch-init-members.ts");
const result = await compile(entry, {
outPath: join(outDir, "fetch-init-members"),
outDir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const native = await execFileAsync(result.binaryPath);
const node = await execFileAsync(process.execPath, [
"--disable-warning=MODULE_TYPELESS_PACKAGE_JSON",
entry,
]);
expect(native.stdout).toBe(node.stdout);
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: stream subclasses extend the @types/node stream classes", async () => {
const outDir = outDirFor("stream-subclass-hooks");
const entry = join(nodeTypesDir, "stream-subclass-hooks.ts");