From a54dd09e04561af8760d5ef77ae8b933b67c35fa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 30 Oct 2026 06:14:57 -0500 Subject: [PATCH] Validate untyped listen ports and give net errors Node's system properties --- .../compiler/ambient/node-fallback/net.d.ts | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/lower-server.ts | 92 ++++++++- packages/compiler/src/ir/builtin-effects.ts | 3 + .../compiler/src/ir/builtin-signatures.ts | 1 + .../corpus/http-server-listen-forms.ts.json | 6 + .../http-server-listen-untyped.cjs.json | 6 + .../net-error-system-properties.cjs.json | 6 + .../net-listen-untyped-arguments.cjs.json | 6 + packages/runtime/src/scr_net.c | 191 ++++++++++++++++-- packages/runtime/src/scr_net.h | 5 + tests/corpus/http-server-listen-forms.ts | 24 +++ tests/corpus/http-server-listen-untyped.cjs | 24 +++ tests/corpus/net-error-system-properties.cjs | 52 +++++ tests/corpus/net-listen-untyped-arguments.cjs | 52 +++++ .../net-error-system-properties.cjs.json | 1 + .../net-listen-untyped-arguments.cjs.json | 1 + 17 files changed, 450 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-forms.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-untyped.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-error-system-properties.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-listen-untyped-arguments.cjs.json create mode 100644 tests/corpus/http-server-listen-forms.ts create mode 100644 tests/corpus/http-server-listen-untyped.cjs create mode 100644 tests/corpus/net-error-system-properties.cjs create mode 100644 tests/corpus/net-listen-untyped-arguments.cjs create mode 100644 tests/harness/corpus-costs/net-error-system-properties.cjs.json create mode 100644 tests/harness/corpus-costs/net-listen-untyped-arguments.cjs.json diff --git a/packages/compiler/ambient/node-fallback/net.d.ts b/packages/compiler/ambient/node-fallback/net.d.ts index 3520518e7..3374894c1 100644 --- a/packages/compiler/ambient/node-fallback/net.d.ts +++ b/packages/compiler/ambient/node-fallback/net.d.ts @@ -178,6 +178,8 @@ declare module "net" { getConnections(callback: (error: Error | null, count: number) => void): Server; /* Node answers the server itself (`return this` chaining). */ listen(port: number, callback?: () => void): Server; + /* No port: an arbitrary unused one. */ + listen(callback?: () => void): Server; /* The positional bind address (the options form's host in argument * spelling). */ listen(port: number, host: string, callback?: () => void): Server; diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 7231e9820..5be7eb0b3 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1488,6 +1488,7 @@ export const LIB_FN_SYMS: Record = { // the client request forms have non-generic shapes — special-cased. "net.serverPort": "scr_net_server_port", "net.serverListening": "scr_net_server_listening", + "net.listenPortDyn": "scr_net_listen_port_dyn", "net.sockSetRef": "scr_net_sock_set_ref", "net.serverSetRef": "scr_net_server_set_ref", "net.serverOff": "scr_net_server_off", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 7ba3f7191..f1b68659f 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1531,11 +1531,11 @@ function lowerNetServerMethodCall( return { kind: "libCall", fn, args: [receiver], type: VOID, loc }; } if (name === "listen") { - if (args.length < 1 || args.length > 3) { + if (args.length > 3) { lowerer.noLowering( `listen with ${args.length} arguments`, call, - "the supported form is listen(port[, host][, callback]) — port 0 binds an ephemeral port", + "the supported form is listen([port][, host][, callback]) — port 0 binds an ephemeral port", ); } // Node answers the server (`return this` chaining — `const s = @@ -1549,6 +1549,90 @@ function lowerNetServerMethodCall( return receiverReturningCall(lowerer, fn, callArgs, NETSERVER_T, loc); }; const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + // listen() and listen(callback): Node binds an arbitrary unused port + // (normalizeArgs takes a function first argument as the callback). + if (args.length === 0) return listenResult("net.listen", [receiver, numLit(0, loc)]); + const first = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); + // An untyped first argument: a checked-dynamic value, a JavaScript value + // read into an untyped slot, or a checker `any`. + const untyped = + args.length > 0 && + (first?.kind === "dyn" || + first?.kind === "jsval" || + (lowerer.typeOf(args[0]!).flags & ts.TypeFlags.Any) !== 0); + if (args.length === 1 && first?.kind === "func") { + const { cb } = lowerCallbackArg( + lowerer, + args[0]!, + "listen callbacks", + 0, + () => false, + "use ()", + [], + ); + return listenResult("net.listenCb", [receiver, numLit(0, loc), cb]); + } + // An untyped first argument takes its port from net.listenPortDyn, + // Node's normalizeArgs and validatePort at run time: 0 for a function, + // undefined or null, the number of a port number or numeric string, + // ERR_SOCKET_BAD_PORT for a bad port, and the fence for an options + // object or a pipe path, which have no lowering from untyped values. + const portOfUntyped = (value: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "net.listenPortDyn", + args: [ + value, + ladderFenceExpr( + lowerer, + "listen with a runtime options object or pipe path", + args[0]!, + "pass the port as a number, the path as a string, or the options as an object literal", + ), + ], + type: F64, + loc, + }); + if (args.length === 1 && untyped) { + // An untyped single argument: a function is the callback on an + // arbitrary port; any other value is the port. + const server = lowerer.declareHiddenLocal("%listenServer", NETSERVER_T); + const arg = lowerer.declareHiddenLocal("%listenArg", DYN); + const serverRef = (): IrExpr => varRef(server.id, NETSERVER_T, loc); + const argRef = (): IrExpr => varRef(arg.id, DYN, loc); + const listenStmt = (fn: IrLibFn, callArgs: IrExpr[]): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "libCall", fn, args: callArgs, type: VOID, loc }, + loc, + }); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: server.id, init: receiver, loc }, + { + kind: "varDecl", + localId: arg.id, + init: lowerer.coerceToExpected(lowerer.lowerExpr(args[0]!), DYN), + loc, + }, + { + kind: "if", + cond: { kind: "dynTest", test: "function", value: argRef(), type: BOOL, loc }, + then: [ + listenStmt("net.listenCb", [ + serverRef(), + numLit(0, loc), + { kind: "dynCheck", value: argRef(), type: funcOf([], VOID), loc }, + ]), + ], + else_: [listenStmt("net.listen", [serverRef(), portOfUntyped(argRef())])], + loc, + }, + ], + result: serverRef(), + type: NETSERVER_T, + loc, + }; + } // The options-object form — listen({ port, host?, ipv6Only?, reusePort? }[, cb]), // the portless listenOnProxyInterface shape: host binds that ONE // address (an IP literal — the runtime has no resolver here; absent @@ -1762,7 +1846,9 @@ function lowerNetServerMethodCall( ); return listenResult("net.listenPathCb", [receiver, path, cb]); } - const port = lowerer.lowerExprExpecting(args[0]!, F64); + const port = untyped + ? portOfUntyped(lowerer.coerceToExpected(lowerer.lowerExpr(args[0]!), DYN)) + : 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 // disambiguation), routed through the options-form entry. diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 3ee94312a..7c298e993 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -992,6 +992,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "net.connectCb", "net.connectPath", "net.connectPathCb", + // listen's port from a checked-dynamic first argument: ERR_SOCKET_BAD_PORT, + // or the fence for forms without a lowering. + "net.listenPortDyn", // resetAndDestroy on a TLS socket throws ERR_INVALID_HANDLE_TYPE. "net.sockResetAndDestroy", // SocketAddress's option checks and parse()'s input check. diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 802d190a5..9a7d304ee 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1711,6 +1711,7 @@ function buildLibFnSignatures() { * compiler-rendered fence — ALWAYS THROWS (the error.nodeThrow * polymorphic-result carve-out). May-throw seed. */ "net.connectOptsChk": { argTypes: [DYN, STRING], result: VOID }, + "net.listenPortDyn": { argTypes: [DYN, STRING], result: F64 }, "net.connectCb": { argTypes: [F64, STRING, { kind: "func", params: [], ret: VOID }], result: NETSOCKET_T, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-forms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-forms.ts.json new file mode 100644 index 000000000..5e1906f01 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-forms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/http-server-listen-forms.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-untyped.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-untyped.cjs.json new file mode 100644 index 000000000..ddb986179 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/http-server-listen-untyped.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/http-server-listen-untyped.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-error-system-properties.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-error-system-properties.cjs.json new file mode 100644 index 000000000..3d96f5745 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-error-system-properties.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-error-system-properties.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-listen-untyped-arguments.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-listen-untyped-arguments.cjs.json new file mode 100644 index 000000000..9d6dece2f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-listen-untyped-arguments.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-listen-untyped-arguments.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index a31cd4688..658e4c0d3 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -1,6 +1,7 @@ #include "scr_platform.h" #include "scr_runtime.h" +#include "scr_system_errors.h" #include #include @@ -413,6 +414,95 @@ void scr_net_fire_err_this(ScrNetLs *l, ScrStr *msg, void *self, ScrDynHandleTag void scr_net_fire_err(ScrNetLs *l, ScrStr *msg) { scr_net_fire_err_this(l, msg, NULL, 0); } +/* The libuv errno of an error code name (Node's err.errno), 0 for a name + * outside libuv's map. */ +static double scr_net_uv_errno(const char *code, size_t len) { +#define SCR_NET_UV_ERRNO(name, text) \ + if (len == sizeof #name - 1 && memcmp(code, #name, len) == 0) return SCR_NODE_UV__##name; + SCR_NODE_UV_ERRNO_MAP(SCR_NET_UV_ERRNO) +#undef SCR_NET_UV_ERRNO + return 0; +} + +/* The libuv message of an error code name, or NULL. */ +static const char *scr_net_uv_text(const char *code, size_t len) { +#define SCR_NET_UV_TEXT(name, text) \ + if (len == sizeof #name - 1 && memcmp(code, #name, len) == 0) return text; + SCR_NODE_UV_ERRNO_MAP(SCR_NET_UV_TEXT) +#undef SCR_NET_UV_TEXT + return NULL; +} + +/* Node's system error for a net failure message this unit writes: + * " [ ]" (errnoException, exceptionWithHostPort) or + * " : [ ]" (uvExceptionWithHostPort). + * The error carries the code, the libuv errno and the syscall; for + * connect, listen and bind, the detail is the address, as "
:" + * with a port, and a listen on a pipe path has port -1. NULL for any other + * message; +1 otherwise. */ +static ScrError *scr_net_system_error(ScrStr *msg) { + const char *data = msg->data; + const char *end = data + msg->len; + const char *space = memchr(data, ' ', msg->len); + if (space == NULL || space == data) return NULL; + size_t syscall_len = (size_t)(space - data); + const char *code = space + 1; + const char *code_end = code; + while (code_end < end && *code_end != ' ' && *code_end != ':') code_end++; + size_t code_len = (size_t)(code_end - code); + if (code_len < 2 || code[0] != 'E') return NULL; + for (size_t i = 0; i < code_len; i++) { + char c = code[i]; + if (!((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) return NULL; + } + double uv = scr_net_uv_errno(code, code_len); + if (uv == 0) return NULL; + const char *detail = code_end; + if (detail < end && *detail == ':') { + /* the libuv form: skip ": " */ + const char *text = scr_net_uv_text(code, code_len); + size_t text_len = text != NULL ? strlen(text) : 0; + if (text == NULL || (size_t)(end - detail) < 2 + text_len || + memcmp(detail + 2, text, text_len) != 0) + return NULL; + detail += 2 + text_len; + } + if (detail < end && *detail == ' ') detail++; + ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); + char code_buf[32]; + size_t n = code_len < sizeof code_buf - 1 ? code_len : sizeof code_buf - 1; + memcpy(code_buf, code, n); + code_buf[n] = '\0'; + scr_error_set_code(e, code_buf); + e->system_errno = uv; + e->system_call = scr_str_new(data, syscall_len); + bool with_address = (syscall_len == 7 && memcmp(data, "connect", 7) == 0) || + (syscall_len == 6 && memcmp(data, "listen", 6) == 0) || + (syscall_len == 4 && memcmp(data, "bind", 4) == 0); + if (with_address && detail < end) { + const char *colon = NULL; + for (const char *q = detail; q < end; q++) + if (*q == ':') colon = q; + bool digits = colon != NULL && colon + 1 < end; + for (const char *q = colon != NULL ? colon + 1 : end; q < end; q++) + if (*q < '0' || *q > '9') digits = false; + const char *address_end = digits ? colon : end; + ScrStr *address = scr_str_new(detail, (size_t)(address_end - detail)); + scr_error_set_prop(e, "address", 7, scr_dyn_new_str(address)); + /* a pipe listen: Node's listenInCluster passes port -1 with the path */ + bool pipe_listen = false; + if (!digits && syscall_len == 6) { + unsigned char ip[16]; + pipe_listen = inet_pton(AF_INET, address->data, ip) != 1 && + inet_pton(AF_INET6, address->data, ip) != 1; + } + scr_str_release(address); + if (digits) scr_error_set_prop(e, "port", 4, scr_dyn_new_num(strtod(colon + 1, NULL))); + else if (pipe_listen) scr_error_set_prop(e, "port", 4, scr_dyn_new_num(-1)); + } + return e; +} + /* ── the handles ─────────────────────────────────────────────────────── */ enum { SCR_NET_K_SERVER = 1, SCR_NET_K_SOCKET = 2 }; @@ -3075,7 +3165,7 @@ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) { char detail[64], msg[160]; const char *d = scr_dyn_specific_type(port, detail, sizeof detail); int len = snprintf(msg, sizeof msg, - "options.port should be >= 0 and < 65536. Received %s", d); + "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"); return; } @@ -3098,6 +3188,60 @@ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) { scr_throw_lowering_fence(fence); } +/* The port of listen(arg[, ...]) for a checked-dynamic first argument, as + * Node's Server.prototype.listen normalizes it: a function (listen(cb)), + * undefined and null bind an ephemeral port (0); a number, or a string + * whose Number() is >= 0, passes validatePort and answers its integer. + * Other values take forms that have no lowering here (an options object, a + * pipe path, and the ERR_INVALID_ARG_VALUE kinds) and throw the + * compiler-rendered fence. */ +/* Node's validatePort over listen's port value: 1 with *out set for a + * function, undefined or null (0) or a valid port; 0 with ERR_SOCKET_BAD_PORT + * pending for a bad port; -1 for a value that is no port form here (an + * options object, a pipe path, and the ERR_INVALID_ARG_VALUE kinds). */ +static int scr_net_listen_port_check(const ScrDyn *v, double *out) { + *out = 0; + if (v == NULL || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL || + v->kind == SCR_DYN_FUNC) + return 1; + double n; + bool ok; + if (v->kind == SCR_DYN_NUM) { + n = v->v.num; + ok = n >= 0 && n <= 65535 && trunc(n) == n; + } else if (v->kind == SCR_DYN_STR) { + ScrStr *ps = scr_str_retain(v->v.str); + n = scr_string_to_number(ps); + if (!(n >= 0)) { /* Node's isPipeName: a path */ + scr_str_release(ps); + return -1; + } + ScrStr *trimmed = scr_str_trim(ps); + ok = trimmed->len > 0 && n <= 65535 && trunc(n) == n; + scr_str_release(trimmed); + scr_str_release(ps); + } else { + return -1; + } + if (!ok) { + char detail[64], msg[160]; + const char *d = scr_dyn_specific_type(v, detail, sizeof detail); + int len = snprintf(msg, sizeof msg, + "options.port should be >= 0 and < 65536. Received %s.", d); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, len < 0 ? 0 : (size_t)len, + "ERR_SOCKET_BAD_PORT"); + return 0; + } + *out = n; + return 1; +} + +double scr_net_listen_port_dyn(const ScrDyn *v, const ScrStr *fence) { + double port; + if (scr_net_listen_port_check(v, &port) < 0) scr_throw_lowering_fence(fence); + return port; +} + /* A dial the caller starts LATER (the http agent's maxSockets queue): * the socket exists, registers, and buffers writes as "connecting" — * scr_net_sock_dial_start runs the actual dial when a slot frees. */ @@ -3290,6 +3434,8 @@ void scr_net_listen_path(ScrNetServer *s, ScrStr *path /*borrowed*/, err = errno; } #endif + /* libuv's uv_pipe_bind reports ENOENT as EACCES, as Windows does */ + if (err == ENOENT) err = EACCES; ScrJsonBuf b; scr_jb_init(&b); scr_jb_puts(&b, "listen "); @@ -6060,8 +6206,10 @@ static bool scr_net_sweep_sock(ScrNetSocket *sock) { if (sock->pending_code != NULL) { coded = scr_error_new(SCR_ERR_ERROR, msg); coded->code = scr_str_new(sock->pending_code, strlen(sock->pending_code)); - scr_event_error_payload = coded; + } else { + coded = scr_net_system_error(msg); } + if (coded != NULL) scr_event_error_payload = coded; scr_net_fire_err_this(&sock->err_ls, msg, sock, SCR_DYNH_NET_SOCKET); scr_event_error_payload = saved; if (coded != NULL) scr_error_release(coded); @@ -6126,7 +6274,12 @@ static void scr_net_sweep(void) { ScrStr *msg = srv->pending_err; srv->pending_err = NULL; ScrAlsCtx *prev = scr_als_dispatch_enter(srv->listen_ctx); + ScrError *system = scr_net_system_error(msg); + ScrError *saved = scr_event_error_payload; + if (system != NULL) scr_event_error_payload = system; scr_net_fire_err_this(&srv->err_ls, msg, srv, SCR_DYNH_NET_SERVER); + scr_event_error_payload = saved; + if (system != NULL) scr_error_release(system); scr_als_dispatch_leave(prev); scr_str_release(msg); /* A failed listen SETTLES the handle: listeners drop (an error @@ -6875,29 +7028,27 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method return scr_dyn_retain(self); } if (strcmp(method, "listen") == 0) { - /* listen([port][, host][, cb]) — the static lowering's shapes over - * dyn arguments; the options-object form throws the 'not supported yet' + /* listen([port][, host][, backlog][, cb]) and listen(path[, cb]) over + * dyn arguments, as Node's normalizeArgs reads them: the last argument + * is the callback when it is a function, a first argument that is a + * function, undefined or null binds an unused port, a string whose + * Number() is not >= 0 is a pipe path, and a string second argument is + * the host. The options-object form throws the 'not supported yet' * Error. */ + const ScrDyn *first = argc > 0 ? args[0] : NULL; + const ScrDyn *cb = argc > 0 && args[argc - 1]->kind == SCR_DYN_FUNC ? args[argc - 1] : NULL; double port = 0; - ScrStr *host = NULL; - const ScrDyn *cb = NULL; - size_t i = 0; - if (i < argc && args[i]->kind == SCR_DYN_NUM) { port = args[i]->v.num; i++; } - else if (i < argc && args[i]->kind == SCR_DYN_STR && args[i]->v.str->len > 0 && - args[i]->v.str->data[0] >= '0' && args[i]->v.str->data[0] <= '9') { - /* Node coerces a numeric port string */ - port = strtod(args[i]->v.str->data, NULL); - i++; - } else if (i < argc && args[i]->kind == SCR_DYN_OBJ) { + int form = scr_net_listen_port_check(first, &port); + if (form == 0) return NULL; + if (form < 0 && first->kind == SCR_DYN_STR) { + scr_net_listen_path(s, first->v.str, cb ? scr_dyn_listener_closure0(cb) : NULL); + return scr_dyn_retain(self); + } + if (form < 0) { scr_net_dynh_srv_unsupported("listen", "the options-object form on a dynamic value is not modeled (use listen(port[, host][, cb]))"); return NULL; } - if (i < argc && args[i]->kind == SCR_DYN_STR) { host = args[i]->v.str; i++; } - if (i < argc && args[i]->kind == SCR_DYN_FUNC) { cb = args[i]; i++; } - if (i < argc && args[i]->kind != SCR_DYN_UNDEF && args[i]->kind != SCR_DYN_NULL) { - scr_net_dynh_srv_unsupported("listen", "unrecognized arguments (the modeled shape is listen(port[, host][, cb]))"); - return NULL; - } + ScrStr *host = argc > 1 && args[1]->kind == SCR_DYN_STR ? args[1]->v.str : NULL; ScrClosure *clo = cb ? scr_dyn_listener_closure0(cb) : NULL; if (host) scr_net_listen_opts(s, port, host, false, clo); else scr_net_listen(s, port, clo); diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index f22943717..f088f6c35 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -229,6 +229,11 @@ ScrNetSocket *scr_net_connect_attempt(double port, ScrStr *host /*borrowed*/, co * autoSelectFamily, attempt budget), then the compiler-rendered fence — * always leaves an exception pending. Borrowed. */ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence); +/* The port of listen(arg[, ...]) for a checked-dynamic first argument + * (Node's normalizeArgs and validatePort): 0 for a function, undefined or + * null; ERR_SOCKET_BAD_PORT pending for a bad port; the fence pending for + * an options object or a pipe path. Borrowed. */ +double scr_net_listen_port_dyn(const ScrDyn *v, const ScrStr *fence); /* connect with a caller lookup (net.connect({ ..., lookup })): invokes * lookup(hostname, options, answer-closure) synchronously; answer_fn is * the emitted per-shape thunk that decodes the answer down to diff --git a/tests/corpus/http-server-listen-forms.ts b/tests/corpus/http-server-listen-forms.ts new file mode 100644 index 000000000..ae93f5ea8 --- /dev/null +++ b/tests/corpus/http-server-listen-forms.ts @@ -0,0 +1,24 @@ +// server.listen() and server.listen(callback) bind an arbitrary unused port, +// as Node's normalizeArgs takes a function first argument as the callback. +import * as http from "node:http"; +import type { AddressInfo } from "node:net"; + +const first = http.createServer((req, res) => res.end("first")); +first.listen(() => { + const port = (first.address() as AddressInfo).port; + console.log("callback form port > 0", port > 0); + http.get({ host: "127.0.0.1", port }, (res) => { + res.setEncoding("utf8"); + res.on("data", (chunk: string) => console.log("body", chunk)); + res.on("end", () => { + first.close(); + const second = http.createServer((req2, res2) => res2.end("second")); + second.on("listening", () => { + const port2 = (second.address() as AddressInfo).port; + console.log("no-argument form port > 0", port2 > 0, port2 !== port); + second.close(); + }); + second.listen(); + }); + }); +}); diff --git a/tests/corpus/http-server-listen-untyped.cjs b/tests/corpus/http-server-listen-untyped.cjs new file mode 100644 index 000000000..d927c6961 --- /dev/null +++ b/tests/corpus/http-server-listen-untyped.cjs @@ -0,0 +1,24 @@ +// server.listen(value) with an untyped value: a function is the 'listening' +// callback on an arbitrary unused port, as in Node; a number is the port. +'use strict'; +const http = require('http'); + +function wrap(fn) { + return function (...args) { + return fn.apply(this, args); + }; +} + +const server = http.createServer((req, res) => res.end('ok')); +server.listen(wrap(() => { + const port = server.address().port; + console.log('port > 0', port > 0); + server.close(() => { + const other = http.createServer(); + const chosen = [0][0]; + other.listen(chosen, wrap(() => { + console.log('numeric port > 0', other.address().port > 0); + other.close(); + })); + }); +})); diff --git a/tests/corpus/net-error-system-properties.cjs b/tests/corpus/net-error-system-properties.cjs new file mode 100644 index 000000000..12bd69731 --- /dev/null +++ b/tests/corpus/net-error-system-properties.cjs @@ -0,0 +1,52 @@ +// net 'error' events carry Node's system error properties: errno, code +// and syscall, and for listen and connect the address and port (a Unix +// socket's path, with port -1 for listen). A reset connection's read error +// carries errno, code and syscall only. +"use strict"; +const net = require("net"); + +function describe(label, err, port) { + const hide = (text) => (port === undefined ? text : text.replace(String(port), "")); + console.log( + label, + JSON.stringify({ + message: hide(err.message), + code: err.code, + errno: err.errno, + syscall: err.syscall, + address: err.address, + port: port !== undefined && err.port === port ? "" : err.port, + }), + ); +} + +const taken = net.createServer(); +taken.listen(0, "127.0.0.1", () => { + const port = taken.address().port; + const second = net.createServer(); + second.on("error", (err) => { + describe("listen in use", err, port); + const unix = net.createServer(); + unix.on("error", (err2) => { + describe("listen path", err2); + const client = net.connect(1, "127.0.0.1"); + client.on("error", (err3) => { + describe("connect refused", err3); + const resetting = net.createServer((socket) => { + socket.on("error", (err4) => { + describe("read reset", err4); + resetting.close(); + taken.close(); + }); + }); + resetting.listen(0, "127.0.0.1", () => { + const victim = net.connect(resetting.address().port, "127.0.0.1", () => { + setTimeout(() => victim.resetAndDestroy(), 20); + }); + }); + }); + }); + unix.listen("/nonexistent-dir/sock"); + }); + second.listen(port, "127.0.0.1"); +}); diff --git a/tests/corpus/net-listen-untyped-arguments.cjs b/tests/corpus/net-listen-untyped-arguments.cjs new file mode 100644 index 000000000..07e3bffa3 --- /dev/null +++ b/tests/corpus/net-listen-untyped-arguments.cjs @@ -0,0 +1,52 @@ +// server.listen with untyped arguments follows Node's normalizeArgs and +// validatePort: a function is the 'listening' callback on an unused port, +// undefined and null take an unused port, a numeric string is a port, and +// a port out of range or not an integer throws ERR_SOCKET_BAD_PORT, as a +// bad port in a connect options object checked at run time does. +"use strict"; +const net = require("net"); + +// Values read from JSON are untyped, as a test helper's results are. +const raw = JSON.parse( + '{"none":null,"zero":0,"text":"0","bad":[-1,65536,1.5,""," ","70000"]}', +); +const wrap = (fn) => (...args) => fn(...args); +const forms = [ + ["callback only", (server, done) => server.listen(wrap(done))], + ["undefined port", (server, done) => server.listen(raw.missing, done)], + ["null port", (server, done) => server.listen(raw.none, done)], + ["numeric string", (server, done) => server.listen(raw.text, done)], + ["host and callback", (server, done) => server.listen(raw.zero, "127.0.0.1", done)], +]; + +function next(i) { + if (i === forms.length) { + for (const bad of raw.bad) { + const server = net.createServer(); + try { + server.listen(bad); + console.log(JSON.stringify(bad), "listened"); + server.close(); + } catch (err) { + console.log(JSON.stringify(bad), err.name, err.code, err.message); + } + } + // A connect options object with a computed key is checked at run time. + const key = ["po", "rt"].join(""); + try { + net.connect({ [key]: -1 }).destroy(); + console.log("connect", "connected"); + } catch (err) { + console.log("connect", err.name, err.code, err.message); + } + return; + } + const [label, listen] = forms[i]; + const server = net.createServer(); + listen(server, () => { + const port = server.address().port; + console.log(label, typeof port, port > 0); + server.close(() => next(i + 1)); + }); +} +next(0); diff --git a/tests/harness/corpus-costs/net-error-system-properties.cjs.json b/tests/harness/corpus-costs/net-error-system-properties.cjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/net-error-system-properties.cjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 } diff --git a/tests/harness/corpus-costs/net-listen-untyped-arguments.cjs.json b/tests/harness/corpus-costs/net-listen-untyped-arguments.cjs.json new file mode 100644 index 000000000..9622c5beb --- /dev/null +++ b/tests/harness/corpus-costs/net-listen-untyped-arguments.cjs.json @@ -0,0 +1 @@ +{ "plain": 2000, "san": 3000 }