mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
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:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6440-http-socket-path.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6441-http-request-options-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6442-https-request-options-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6443-net-lookup-http-close-text.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/fetch-init-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/http-socket-path.ts.json
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/http-socket-path.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user