diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index b15b32aff..3515e0011 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "a38d2d004b348e926af4", + "artifactVersion": "1dd6d714f44893573704", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index c007fdbe5..ba2cd261c 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -494,9 +494,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52122,9 +52122,9 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52140,9 +52140,9 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -52158,9 +52158,9 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 58e08f0e6..af3ca3822 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 37, - "verify-gap": 2395, - "audit-partial": 953, + "verify-gap": 2392, + "audit-partial": 956, "classify": 2, "implement": 85 } @@ -51298,12 +51298,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51318,12 +51321,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, @@ -51338,12 +51344,15 @@ "priority": "normal", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:net", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index f3ea536e8..3b474aafe 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -521,9 +521,9 @@ "apiEntries": 119, "static": { "supported": 0, - "partial": 34, + "partial": 37, "refused": 0, - "not-implemented": 85, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -73516,8 +73516,12 @@ "anchor": "netisipinput", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIP", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73534,8 +73538,12 @@ "anchor": "netisipv4input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv4", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, @@ -73552,8 +73560,12 @@ "anchor": "netisipv6input", "nodeStability": null, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:net" + "status": "partial", + "evidence": "surface-manifest:node-builtin.net.isIPv6", + "tests": [ + "tests/corpus/2500-net-autosel-timeout.ts", + "tests/corpus/2596-net-arg-ladders.cjs" + ] }, "anchorSource": "exact" }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 15e4d6569..c70759364 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3415,6 +3415,17 @@ declare module "net" { ref(): void; unref(): void; readonly remoteAddress: string | undefined; + /* The peer's fields are cached at first read, like remoteAddress; the + * local ones are undefined while connecting and after close. */ + readonly remotePort: number | undefined; + readonly remoteFamily: string | undefined; + readonly localAddress: string | undefined; + readonly localPort: number | undefined; + readonly localFamily: string | undefined; + readonly bytesRead: number; + readonly connecting: boolean; + readonly pending: boolean; + readonly readyState: "opening" | "open" | "readOnly" | "writeOnly" | "closed"; /* true once the fd is gone (destroy() or full close) — Node's flag. */ readonly destroyed: boolean; /* the write half is open: no end() yet, no FIN sent, fd alive. */ @@ -3424,8 +3435,11 @@ declare module "net" { /* every byte the write paths accepted (buffered included — Node * counts those too; plaintext on TLS sockets). */ readonly bytesWritten: number; - write(data: string | Uint8Array): void; - end(data?: string | Uint8Array): void; + /* write's callback runs once the data is flushed, with null; end's + * once the socket finishes. */ + write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void; + end(callback?: () => void): void; + end(data: string | Uint8Array, callback?: () => void): void; destroy(): void; /* Flow control: pause holds reads off (kernel/TCP backpressure is * the buffer); resume flows — and discards without listeners, so @@ -3434,6 +3448,8 @@ declare module "net" { resume(): Socket; /* TCP_NODELAY on the live fd; missing means true. Chains. */ setNoDelay(noDelay?: boolean): Socket; + /* SO_KEEPALIVE with the initial delay in ms. Chains. */ + setKeepAlive(enable?: boolean, initialDelay?: number): Socket; /* end() now, destroy once the FIN actually flushed. */ destroySoon(): void; /* setEncoding('utf8'): 'data' delivers strings — the IncomingMessage @@ -3444,7 +3460,8 @@ declare module "net" { pipe( destination: Socket | import("http").ServerResponse | import("http2").Http2ServerResponse, ): void; - setTimeout(ms: number): void; + /* The callback is a once('timeout') listener (removed again by 0). */ + setTimeout(ms: number, callback?: () => void): void; /* The paused-mode demux surface: once('readable') + read(1) + * unshift — read answers exactly n buffered bytes or null. */ read(size?: number): Buffer | null; @@ -3455,25 +3472,34 @@ declare module "net" { * registration never fires — Node's exact split. 'session' fires * once with the serialized session (the received-ticket event). */ on(event: "data", listener: (chunk: Buffer) => void): void; + /* After setEncoding the chunk is a string. */ + on(event: "data", listener: (chunk: string) => void): void; on( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + on(event: "close", listener: (hadError: boolean) => void): void; on(event: "error", listener: (err: Error) => void): void; on(event: "session", listener: (session: Buffer) => 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. */ + addListener(event: "data", listener: (chunk: string) => void): void; addListener( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + addListener(event: "close", listener: (hadError: boolean) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener(event: "session", listener: (session: Buffer) => 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; once( - event: "end" | "close" | "connect" | "timeout" | "readable" | "finish" | "secureConnect", + event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect", listener: () => void, ): void; + once(event: "close", listener: (hadError: boolean) => void): void; once(event: "error", listener: (err: Error) => void): void; once(event: "session", listener: (session: Buffer) => void): void; } @@ -3482,6 +3508,11 @@ declare module "net" { ref(): void; unref(): void; readonly listening: boolean; + /* Connections past this many are closed with a 'drop' event; + * undefined (the default) or null lifts the cap. */ + maxConnections: number | undefined; + /* cb(null, count) on the next tick. */ + getConnections(callback: (error: Error | null, count: number) => void): Server; /* Node answers the server itself (`return this` chaining). */ listen(port: number, callback?: () => void): Server; /* The positional bind address (the options form's host in argument @@ -3503,6 +3534,7 @@ declare module "net" { emit(event: "connection", socket: Socket): boolean; on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; on(event: "close" | "listening", listener: () => void): void; + on(event: "drop", listener: (data?: DropArgument) => void): void; on(event: "timeout", listener: (socket: Socket) => void): void; on(event: "error", listener: (err: Error) => void): void; /* The WebSocket handover: fires INSTEAD of 'request' for @@ -3520,6 +3552,7 @@ declare module "net" { /* addListener IS on (Node aliases them) — the suite spells both. */ addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; addListener(event: "close" | "listening", listener: () => void): void; + addListener(event: "drop", listener: (data?: DropArgument) => void): void; addListener(event: "timeout", listener: (socket: Socket) => void): void; addListener(event: "error", listener: (err: Error) => void): void; addListener( @@ -3532,6 +3565,7 @@ declare module "net" { ): void; once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void; once(event: "close" | "listening", listener: () => void): void; + once(event: "drop", listener: (data?: DropArgument) => void): void; once(event: "timeout", listener: (socket: Socket) => void): void; once(event: "error", listener: (err: Error) => void): void; once( @@ -3543,6 +3577,20 @@ declare module "net" { listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void, ): void; } + /* The 'drop' payload: the turned-away connection's addresses. */ + export interface DropArgument { + localAddress?: string; + localPort?: number; + localFamily?: string; + remoteAddress?: string; + remotePort?: number; + remoteFamily?: string; + } + export type AddressInfo = import("node:dgram").AddressInfo; + /* Node's IPv4Reg and IPv6Reg tests: 4, 6, or 0 for isIP. */ + export function isIP(input: string): number; + export function isIPv4(input: string): boolean; + export function isIPv6(input: string): boolean; /* The Socket VALUE — the `x instanceof net.Socket` narrowing target * (the h2 compat 'connect' listener's test). Constructing bare sockets * has no lowering; the construct signature exists for instanceof. */ diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 19e9d1c13..5da0c4d3d 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -250,6 +250,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) e.fn === "net.sockOnConnect" || e.fn === "net.sockOnTimeout" || e.fn === "net.sockOnReadable" || + e.fn === "net.sockOnReady" || e.fn === "http.reqOnEnd" || e.fn === "http.reqOnClose" || e.fn === "http.reqOnAborted" || @@ -268,6 +269,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "net.sockOnConnect": "scr_net_sock_on_connect", "net.sockOnTimeout": "scr_net_sock_on_timeout", "net.sockOnReadable": "scr_net_sock_on_readable", + "net.sockOnReady": "scr_net_sock_on_ready", "http.reqOnEnd": "scr_http_req_on_end", "http.reqOnClose": "scr_http_req_on_close", "http.reqOnAborted": "scr_http_req_on_aborted", @@ -640,5 +642,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } + if (e.fn === "net.sockOnWriteFlush") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[1]!); + host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`); + B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); + return { name: "", type: e.type }; + } + if (e.fn === "net.sockSetTimeoutCb") { + const args = e.args.map((a) => host.emitExpr(a)); + host.moveTemp(args[2]!); + host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)"); + B.line( + `call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, + ); + return { name: "", type: e.type }; + } return host.emitGenericLibCall(e); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index ce44e2ce4..84f039f2c 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -973,13 +973,29 @@ export const LIB_FN_SYMS: Record = { "net.sockDestroyed": "scr_net_sock_destroyed", "net.sockWritable": "scr_net_sock_writable", "net.sockSetEncoding": "scr_net_sock_set_encoding", - "net.sockSetTimeout": "scr_net_sock_set_timeout", + "net.sockSetTimeout": "scr_net_sock_set_timeout_checked", "net.sockUnshift": "scr_net_sock_unshift_bytes", "net.sockPause": "scr_net_sock_pause", "net.sockResume": "scr_net_sock_resume", "net.sockSetNoDelay": "scr_net_sock_set_nodelay", "net.sockDestroySoon": "scr_net_sock_destroy_soon", "net.sockBytesWritten": "scr_net_sock_bytes_written", + "net.sockAddrField": "scr_net_sock_addr_field", + "net.sockBytesRead": "scr_net_sock_bytes_read", + "net.sockConnecting": "scr_net_sock_connecting", + "net.sockPending": "scr_net_sock_pending", + "net.sockReadyState": "scr_net_sock_ready_state", + "net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret", + "net.sockOnCloseDyn": "scr_net_sock_on_close_dyn", + "net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn", + "net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn", + "net.isIP": "scr_net_is_ip", + "net.isIPv4": "scr_net_is_ipv4", + "net.isIPv6": "scr_net_is_ipv6", + "net.serverMaxConnectionsGet": "scr_net_server_max_connections_get", + "net.serverMaxConnectionsSet": "scr_net_server_max_connections_set", + "net.serverOnDrop": "scr_net_server_on_drop", + "net.serverGetConnections": "scr_net_server_get_connections", "net.sockReadable": "scr_net_sock_readable", "net.sockPipeRes": "scr_http_sock_pipe_res", "net.serverEmitConnection": "scr_net_server_emit_connection", diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 050fe9be3..757020407 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -224,7 +224,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE body: [ { kind: "return", - value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, loc }, + value: { kind: "closure", fnName: impl, captures: ["f.0"], type: toT, adapts: true, loc }, loc, }, ], @@ -234,6 +234,30 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE return { kind: "call", callee: name, args: [cb], type: toT, loc }; } +/** A socket or server listener with parameters that the runtime calls + * through the checked-dynamic boundary (close(hadError), write's + * cb(err), 'drop', getConnections): a dyn function value as is, or a + * typed function boxed with per-argument checks. */ +function lowerSocketDynListener(lowerer: Lowerer, node: ts.Expression, what: string): IrExpr { + const cb = lowerer.lowerExpr(node); + if (cb.type.kind === "dyn") return cb; + if ( + cb.type.kind === "func" && + canBoxFuncIntoDyn( + cb.type, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ) { + return { kind: "dynFrom", value: cb, type: DYN, loc: locOf(node) }; + } + lowerer.noLowering( + `${what} that cannot cross the checked-dynamic boundary`, + node, + "the callback's parameters and return value must have checked-dynamic conversions", + ); +} + /** A registration/side-effect libCall re-shaped to answer its RECEIVER — * Node's `return this` chaining (server.listen(...), res.writeHead(...)): * an interned helper runs the void call and returns its first argument. @@ -1283,6 +1307,33 @@ export function lowerHttpServerTimeoutAssignment( }; } +/** `server.maxConnections = n`: the value crosses as a dyn (undefined + * and null unset the cap). */ +export function lowerNetServerMaxConnectionsAssignment( + lowerer: Lowerer, + left: ts.Expression, + right: ts.Expression, + loc: SrcLoc, +): IrStmt | null { + if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return null; + if (left.name.text !== "maxConnections") return null; + if (lowerer.mapTypeOf(lowerer.typeOf(left.expression))?.kind !== "netServer") return null; + if (!lowerer.isStdlibMember(left)) return null; + const receiver = coerceToHandle(lowerer, left.expression, NETSERVER_T); + const value = lowerer.coerceToExpected(lowerer.lowerExpr(right), DYN); + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "net.serverMaxConnectionsSet", + args: [receiver, value], + type: VOID, + loc, + }, + loc, + }; +} + /** Method calls on net.Server receivers: listen(port[, cb]), close([cb]), * on/once("connection" | "error" | "close", cb). address() alone fences * toward the composed address().port read. Null for other receivers. */ @@ -1296,6 +1347,34 @@ function lowerNetServerMethodCall( const name = access.name.text; const loc = locOf(call); const args = call.arguments; + if (name === "getConnections") { + // cb(null, count) on the next tick; answers the server. + if (args.length !== 1) { + lowerer.noLowering( + `getConnections with ${args.length} arguments`, + call, + "the form is getConnections(callback)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T); + const cb = lowerSocketDynListener(lowerer, args[0]!, "getConnections callbacks"); + if (resultIsDiscarded(call)) { + return { + kind: "libCall", + fn: "net.serverGetConnections", + args: [receiver, cb], + type: VOID, + loc, + }; + } + return receiverReturningCall( + lowerer, + "net.serverGetConnections", + [receiver, cb], + NETSERVER_T, + loc, + ); + } if (name === "setTimeout") { if (args.length < 1 || args.length > 2) lowerer.noLowering( @@ -1744,6 +1823,17 @@ function lowerNetServerMethodCall( loc, }; } + if (event === "drop") { + // A connection past maxConnections, with its local and remote + // addresses. + return { + kind: "libCall", + fn: "net.serverOnDrop", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "drop listeners"), once], + type: VOID, + loc, + }; + } if (event === "session") { // The h2c server's per-connection session event: (session). The // result is voidized; the handle passes by reference. @@ -1885,7 +1975,7 @@ function lowerNetServerMethodCall( lowerer.noLowering( `server.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"connection", "request", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', + '"connection", "request", "drop", "stream", "session", "upgrade", "connect", "error", "close", "listening", and "sessionError" are the supported server events (as literals)', ); } if (name === "emit") { @@ -2019,6 +2109,85 @@ function lowerNetSocketMethodCall( } } } + // A trailing callback: write's runs once the data is flushed (with + // null for the error), end's once the socket finishes ('finish'). + const last = args.length > 0 ? args[args.length - 1]! : null; + if ( + last !== null && + args.length <= 2 && + (name === "end" || args.length === 2) && + lowerer.checker.getCallSignatures(lowerer.typeOf(last)).length > 0 + ) { + const sockLocal = lowerer.declareHiddenLocal("%sockCb", NETSOCKET_T); + const stmts: IrStmt[] = [ + { + kind: "varDecl", + localId: sockLocal.id, + init: coerceToHandle(lowerer, access.expression, NETSOCKET_T), + loc, + }, + ]; + const receiver = (): IrExpr => ({ + kind: "varRef", + localId: sockLocal.id, + type: NETSOCKET_T, + loc, + }); + const dataNode = args.length === 2 ? args[0]! : null; + let main: IrExpr = { + kind: "libCall", + fn: "net.sockEnd", + args: [receiver()], + type: VOID, + loc, + }; + if (dataNode !== null) { + const data = lowerer.lowerExpr(dataNode); + const fn: IrLibFn | null = + data.type.kind === "string" + ? name === "write" + ? "net.sockWrite" + : "net.sockEndStr" + : data.type.kind === "bytes" && data.type.elem === "u8" + ? name === "write" + ? "net.sockWriteBytes" + : "net.sockEndBytes" + : data.type.kind === "dyn" + ? name === "write" + ? "net.sockWriteDyn" + : "net.sockEndDyn" + : null; + if (fn === null) { + lowerer.noLowering( + `${name} of '${lowerer.fmt(data.type)}' data`, + dataNode, + NARROW_DATA_HINT, + ); + } + main = { kind: "libCall", fn, args: [receiver(), data], type: VOID, loc }; + } + const cb = lowerer.lowerExpr(last); + let register: IrExpr; + if (cb.type.kind === "func" && cb.type.params.length === 0 && cb.type.ret.kind === "void") { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlush" : "net.sockOnFinish", + args: [receiver(), cb], + type: VOID, + loc, + }; + } else { + register = { + kind: "libCall", + fn: name === "write" ? "net.sockOnWriteFlushDyn" : "net.sockOnFinishDyn", + args: [receiver(), lowerSocketDynListener(lowerer, last, `${name} callbacks`)], + type: VOID, + loc, + }; + } + stmts.push({ kind: "exprStmt", expr: main, loc }); + return { kind: "seqExpr", stmts, result: register, type: VOID, loc }; + } const maxArgs = name === "write" ? 1 : 1; const minArgs = name === "write" ? 1 : 0; if (args.length < minArgs || args.length > maxArgs) { @@ -2083,17 +2252,62 @@ function lowerNetSocketMethodCall( } if (name === "setTimeout") { requireStatementPosition(lowerer, call, "socket.setTimeout(...)"); - if (args.length !== 1) { + if (args.length !== 1 && args.length !== 2) { lowerer.noLowering( `setTimeout with ${args.length} arguments`, call, - "the supported form is setTimeout(ms) — register the callback separately: socket.once('timeout', ...)", + "the supported forms are setTimeout(ms) and setTimeout(ms, callback)", ); } const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); const ms = lowerer.lowerExprExpecting(args[0]!, F64); + if (args.length === 2) { + // Node adds the callback as a once('timeout') listener (removes it + // for 0); msecs is checked first either way. + const { cb } = lowerCallbackArg( + lowerer, + args[1]!, + "setTimeout callbacks", + 0, + () => false, + "use ()", + [], + ); + return { + kind: "libCall", + fn: "net.sockSetTimeoutCb", + args: [receiver, ms, cb], + type: VOID, + loc, + }; + } return { kind: "libCall", fn: "net.sockSetTimeout", args: [receiver, ms], type: VOID, loc }; } + if (name === "setKeepAlive") { + // SO_KEEPALIVE plus the initial delay (whole seconds, at least 1); + // Node's defaults are false and 0. Answers the socket. + if (args.length > 2) { + lowerer.noLowering( + `setKeepAlive with ${args.length} arguments`, + call, + "the form is setKeepAlive(enable?, initialDelay?)", + ); + } + const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); + const enable: IrExpr = + args.length >= 1 ? lowerer.lowerExprExpecting(args[0]!, BOOL) : boolLit(false, loc); + const delay: IrExpr = + args.length === 2 + ? lowerer.lowerExprExpecting(args[1]!, F64) + : { kind: "numLit", value: 0, type: F64, loc }; + return { + kind: "libCall", + fn: "net.sockSetKeepAlive", + args: [receiver, enable, delay], + type: NETSOCKET_T, + loc, + }; + } if (name === "pause" || name === "resume") { // Flow control (the struct's flag comments in scr_net.c): pause holds // reads off — kernel/TCP backpressure is the buffer; resume flows @@ -2212,6 +2426,31 @@ function lowerNetSocketMethodCall( const event = evT.isStringLiteralType() ? evT.value : null; const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_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 !== "dyn" && + !(param.kind === "bytes" && param.elem === "u8") + ) { + // A chunk typed string (or string | Buffer): 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: "net.sockOnData", + args: [receiver, cb, once], + type: VOID, + loc, + }; + } const { cb } = lowerCallbackArg( lowerer, args[1]!, @@ -2241,10 +2480,26 @@ function lowerNetSocketMethodCall( loc, }; } + if ( + event === "close" && + lowerer.checker + .getCallSignatures(lowerer.typeOf(args[1]!)) + .some((sig) => sig.getParameters().length > 0) + ) { + // close(hadError): true when the socket closed because of an error. + return { + kind: "libCall", + fn: "net.sockOnCloseDyn", + args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "close listeners"), once], + type: VOID, + loc, + }; + } if ( event === "end" || event === "close" || event === "connect" || + event === "ready" || event === "timeout" || event === "readable" ) { @@ -2266,7 +2521,9 @@ function lowerNetSocketMethodCall( ? "net.sockOnTimeout" : event === "readable" ? "net.sockOnReadable" - : "net.sockOnConnect"; + : event === "ready" + ? "net.sockOnReady" + : "net.sockOnConnect"; return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc }; } if (event === "finish") { @@ -2328,13 +2585,13 @@ function lowerNetSocketMethodCall( lowerer.noLowering( `socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, args[0]!, - '"data", "end", "close", "error", "connect", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', + '"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)', ); } lowerer.noLowering( `Socket.${name}`, call, - "write, end, destroy, pipe, setTimeout, remoteAddress, read, unshift, and on/once of data/end/close/error/connect/timeout/readable are the supported Socket members", + "write, end, destroy, pipe, setTimeout, setKeepAlive, setNoDelay, the address fields, read, unshift, and on/once of data/end/close/error/connect/ready/timeout/readable are the supported Socket members", lowerer.checker.getSymbolAtLocation(access.name), ); } @@ -2978,6 +3235,25 @@ export function lowerServerProperty( const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); return { kind: "libCall", fn: "net.serverListening", args: [receiver], type: BOOL, loc }; } + if (expr.name.text === "maxConnections") { + const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.serverMaxConnectionsGet", + args: [receiver], + type: DYN, + loc, + }; + // The property's declared type: an earlier `= undefined` narrows + // the read's own type to undefined. + const sym = lowerer.checker.getSymbolAtLocation(expr.name); + const declared = lowerer.mapTypeOf( + sym ? lowerer.checker.getTypeOfSymbol(sym) : lowerer.typeOf(expr), + ); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } const field = httpServerTimeoutField(expr.name.text); if (field !== null) { const receiver = coerceToHandle(lowerer, expr.expression, NETSERVER_T); @@ -3205,6 +3481,46 @@ export function lowerServerProperty( loc, }; } + if (recvKind === "netSocket" && lowerer.isStdlibMember(expr)) { + const name = expr.name.text; + // The other address fields: the peer's cached like remoteAddress, the + // local ones read live (undefined while connecting and after close). + const field = [ + "remotePort", + "remoteFamily", + "localAddress", + "localPort", + "localFamily", + ].indexOf(name); + if (field >= 0) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + const value: IrExpr = { + kind: "libCall", + fn: "net.sockAddrField", + args: [receiver, { kind: "numLit", value: field, type: F64, loc }], + type: DYN, + loc, + }; + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return declared === null || declared.kind === "dyn" + ? value + : lowerer.coerceInto(expr, value, declared); + } + const scalar: { fn: IrLibFn; type: IrType } | null = + name === "bytesRead" + ? { fn: "net.sockBytesRead", type: F64 } + : name === "connecting" + ? { fn: "net.sockConnecting", type: BOOL } + : name === "pending" + ? { fn: "net.sockPending", type: BOOL } + : name === "readyState" + ? { fn: "net.sockReadyState", type: STRING } + : null; + if (scalar !== null) { + const receiver = coerceToHandle(lowerer, expr.expression, NETSOCKET_T); + return { kind: "libCall", fn: scalar.fn, args: [receiver], type: scalar.type, loc }; + } + } if ( recvKind === "httpClientReq" && lowerer.isStdlibMember(expr) && diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 5d6305aa8..5ae5bef1a 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -126,6 +126,7 @@ import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-strea import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, + lowerNetServerMaxConnectionsAssignment, lowerServerCloseOverrideAssignment, } from "./lower-server.js"; import { lowerUrlAssignment } from "./builtins/url.js"; @@ -6166,6 +6167,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); if (timeoutProp) return timeoutProp; } + // server.maxConnections — net.Server's connection cap. + { + const capProp = lowerNetServerMaxConnectionsAssignment( + lowerer, + expr.left, + expr.right, + locOf(expr), + ); + if (capProp) return capProp; + } // `N.x = v` — a namespace-qualified write: exported `let` // members are module globals, so the qualified spelling assigns // exactly like the bare one inside the namespace body. diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 939b12776..a4cd79bfe 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -1763,6 +1763,10 @@ export const BUILTIN_MODULE_FNS: Record< params: [F64], result: VOID, }, + // Node's IPv4Reg / IPv6Reg tests over String(input). + isIP: { fn: "net.isIP", params: [DYN], result: F64 }, + isIPv4: { fn: "net.isIPv4", params: [DYN], result: BOOL }, + isIPv6: { fn: "net.isIPv6", params: [DYN], result: BOOL }, }, http: {}, // node:tls and node:https ride the same spoke (tls.createServer's diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 555ba9efc..c18e5a990 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3031,6 +3031,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ), }; } + // net's 'drop' payload: the record the runtime's address object checks + // into, each optional field with its declared type. + if (isStdlibInterface("DropArgument")) { + const known = [ + "localAddress", + "localPort", + "localFamily", + "remoteAddress", + "remotePort", + "remoteFamily", + ]; + const fields: { name: string; type: IrType }[] = []; + for (const property of checker.getPropertiesOfType(widened)) { + if (!known.includes(property.name)) return null; + const type = mapType(checker.getTypeOfSymbol(property), ctx); + if (!type) return null; + fields.push({ name: property.name, type }); + } + const order = fields.map((field) => field.name); + fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + return { kind: "record", shapeId: ctx.shapes.intern(fields, false, undefined, order) }; + } // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index f73633f01..20b896112 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -182,6 +182,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "readable.nextChunkDyn", // The checked-dynamic chunk forms throw Node's ERR_INVALID_ARG_TYPE // chunk TypeError on a non-string/Buffer dyn kind. + // socket.setTimeout's msecs check (ERR_OUT_OF_RANGE). + "net.sockSetTimeout", + "net.sockSetTimeoutCb", "net.sockWriteDyn", "net.sockEndDyn", "http.resWriteDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index dc2aeb7b6..efd568238 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1686,6 +1686,38 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "net.sockSetNoDelay": { argTypes: [NETSOCKET_T, BOOL], result: NETSOCKET_T }, "net.sockDestroySoon": { argTypes: [NETSOCKET_T], result: VOID }, "net.sockBytesWritten": { argTypes: [NETSOCKET_T], result: F64 }, + /** remotePort (0), remoteFamily (1), localAddress (2), localPort (3), + * localFamily (4): dyn values, undefined when unknown. */ + "net.sockAddrField": { argTypes: [NETSOCKET_T, F64], result: DYN }, + "net.sockBytesRead": { argTypes: [NETSOCKET_T], result: F64 }, + "net.sockConnecting": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockPending": { argTypes: [NETSOCKET_T], result: BOOL }, + "net.sockReadyState": { argTypes: [NETSOCKET_T], result: STRING }, + "net.sockSetKeepAlive": { argTypes: [NETSOCKET_T, BOOL, F64], result: NETSOCKET_T }, + "net.sockOnReady": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL], + result: VOID, + }, + /** 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 }, + "net.sockOnWriteFlush": { + argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.sockOnWriteFlushDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockOnFinishDyn": { argTypes: [NETSOCKET_T, DYN], result: VOID }, + "net.sockSetTimeoutCb": { + argTypes: [NETSOCKET_T, F64, { kind: "func", params: [], ret: VOID }], + result: VOID, + }, + "net.isIP": { argTypes: [DYN], result: F64 }, + "net.isIPv4": { argTypes: [DYN], result: BOOL }, + "net.isIPv6": { argTypes: [DYN], result: BOOL }, + "net.serverMaxConnectionsGet": { argTypes: [NETSERVER_T], result: DYN }, + "net.serverMaxConnectionsSet": { argTypes: [NETSERVER_T, DYN], result: VOID }, + "net.serverOnDrop": { argTypes: [NETSERVER_T, DYN, BOOL], result: VOID }, + "net.serverGetConnections": { argTypes: [NETSERVER_T, DYN], result: VOID }, "net.sockReadable": { argTypes: [NETSOCKET_T], result: BOOL }, /** socket.on('finish', cb) / end(cb): fires once when the FIN goes out * (sweep-deferred, never the registering stack). */ diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index b4fcc0a07..a27748984 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1394,6 +1394,24 @@ "name": "net.getDefaultAutoSelectFamilyAttemptTimeout", "status": "static" }, + { + "id": "node-builtin.net.isIP", + "kind": "node-builtin", + "name": "net.isIP", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv4", + "kind": "node-builtin", + "name": "net.isIPv4", + "status": "static" + }, + { + "id": "node-builtin.net.isIPv6", + "kind": "node-builtin", + "name": "net.isIPv6", + "status": "static" + }, { "id": "node-builtin.net.setDefaultAutoSelectFamilyAttemptTimeout", "kind": "node-builtin", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json new file mode 100644 index 000000000..0f0c242e9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5644-net-socket-addresses.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5644-net-socket-addresses.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json new file mode 100644 index 000000000..b4277d87a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5645-net-socket-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5645-net-socket-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json new file mode 100644 index 000000000..f7618cd63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5646-net-server-connections.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5646-net-server-connections.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json new file mode 100644 index 000000000..2664352c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5647-net-is-ip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5647-net-is-ip.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 461acbedc..8293759ee 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -330,6 +331,62 @@ void scr_net_fire0_this(ScrNetLs *l, void *self, ScrDynHandleTag tag) { void scr_net_fire0(ScrNetLs *l) { scr_net_fire0_this(l, NULL, 0); } +/* A list mixing zero-argument closures (fn NULL) with dyn listeners + * (fn = a ScrNetArgFn) — the close(hadError) and write(chunk, cb(err)) + * shapes: the dyn ones receive `arg`. */ +typedef void (*ScrNetArgFn)(ScrClosure *cb, const ScrDyn *arg); + +static void scr_net_fire_arg_this(ScrNetLs *l, const ScrDyn *arg, void *self, ScrDynHandleTag tag) { + ScrNetL *snap; + size_t n = scr_net_ls_snapshot(l, &snap); + scr_dyn_this_push(self, tag); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) { + if (snap[i].fn != NULL) ((ScrNetArgFn)snap[i].fn)(snap[i].cb, arg); + else ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb); + } + scr_closure_release(snap[i].cb); + } + scr_dyn_this_pop(); + free(snap); +} + +/* The dyn listener's call with one argument (none for NULL). */ +static void scr_net_dyn_fire_arg(ScrClosure *cb, const ScrDyn *arg) { + ScrDyn *fn = scr_dyn_listener_fn(cb); + ScrDyn *args[1] = { (ScrDyn *)arg }; + scr_dyn_release(scr_dyn_call(fn, args, arg != NULL ? 1 : 0, "listener")); + scr_dyn_release(fn); +} + +/* The JS function a listener entry stands for: the boxed function of a + * dyn listener closure, otherwise the closure's own identity. */ +static ScrClosure *scr_net_listener_root(const ScrNetL *e) { + ScrClosure *c = e->cb; + if (e->fn == (void *)&scr_net_dyn_fire_arg || c->fn == (void *)&scr_dyn_listener_fire0) { + ScrDyn *boxed = scr_dyn_listener_fn(c); + ScrClosure *inner = boxed->kind == SCR_DYN_FUNC ? boxed->v.fn.clo : NULL; + scr_dyn_release(boxed); + if (inner != NULL) return inner->identity ? inner->identity : inner; + } + return c->identity ? c->identity : c; +} + +/* The listener standing for the same function as `root` (through any + * adapter) leaves the list: removeListener's first match. */ +static void scr_net_ls_remove_fn(ScrNetLs *l, ScrClosure *root) { + root = root->identity ? root->identity : root; + for (size_t i = 0; i < l->n; i++) { + ScrClosure *c = l->ls[i].cb; + if (scr_net_listener_root(&l->ls[i]) != root) continue; + scr_closure_release(c); + scr_net_once_release(l->ls[i].shared_once); + memmove(&l->ls[i], &l->ls[i + 1], (l->n - i - 1) * sizeof *l->ls); + l->n--; + return; + } +} + /* Fire an error list through the ScrChildErrFn adapters (msg borrowed), * the emitting handle bound as the ambient receiver. An 'error' with NO * listener is fatal, like Node's unhandled 'error' EventEmitter throw: @@ -387,6 +444,10 @@ struct ScrNetServer { ScrStr *pending_err; /* deferred listen failure */ size_t nconns; ScrNetLs conn_ls, err_ls, close_ls, listening_cbs, timeout_ls; + /* server.maxConnections (unset = undefined) and its 'drop' listeners */ + bool max_conns_set; + double max_conns; + ScrNetLs drop_ls; /* protocol layer (scr_http.c): a C-level connection consumer + its * context, freed with the handle */ ScrNetNativeConnFn native_conn; @@ -448,6 +509,8 @@ struct ScrNetSocket { * value read while connected survives destroy; never-read sockets * answer undefined after close */ ScrStr *remote_cache; + int remote_port_cache; + bool remote_v6_cache; ScrStr *pending_err; /* deferred connect failure */ /* the caller-lookup dial (net.connect with a lookup option): true while * the lookup decides; dial_ips holds its answered addresses (+1 each), @@ -483,6 +546,8 @@ struct ScrNetSocket { * buffer to drain (this surface's flush moment). */ ScrNetLs finish_ls, wcb_ls; bool finish_pending; /* wr_done seen: fire finish_ls at the next sweep */ + ScrNetLs ready_ls; /* 'ready': right after the client's 'connect' listeners */ + size_t bytes_read; /* socket.bytesRead: every byte delivered from the peer */ /* a server whose 'connection' waits for this socket's handshake — the * TLS defer target (+1; the socket's own `server` backref stays the * ACCEPTING server, which may be the demux wrapper) */ @@ -549,6 +614,7 @@ void scr_net_server_release(ScrNetServer *s) { scr_net_ls_drop(&s->close_ls); scr_net_ls_drop(&s->listening_cbs); scr_net_ls_drop(&s->timeout_ls); + scr_net_ls_drop(&s->drop_ls); scr_closure_release(s->close_override); for (size_t i = 0; i < 5; i++) scr_dyn_release(s->http_timeout_dyn[i]); scr_str_release(s->bound_host); @@ -583,6 +649,7 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_net_ls_drop(&s->readable_ls); scr_net_ls_drop(&s->finish_ls); scr_net_ls_drop(&s->wcb_ls); + scr_net_ls_drop(&s->ready_ls); scr_str_release(s->pending_err); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); @@ -1164,6 +1231,7 @@ static void scr_net_sock_read(ScrNetSocket *s) { return; } scr_net_sock_touch(s); + s->bytes_read += (size_t)n; if (flowing) { if (!scr_net_sock_deliver(s, buf, (size_t)n)) return; } else if (s->readable_ls.n == 0) { @@ -1260,6 +1328,8 @@ static void scr_net_sock_connect_done(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); } @@ -1312,6 +1382,8 @@ static void scr_net_sock_transport_pump(ScrNetSocket *s) { } scr_net_fire0_this(&s->conn_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; + scr_net_fire0_this(&s->ready_ls, s, SCR_DYNH_NET_SOCKET); + if (scr_exc_pending()) return; scr_net_sock_flush(s); scr_net_sock_update_read(s); scr_net_sock_update_write(s); @@ -1679,6 +1751,51 @@ void scr_net_server_close_idle_connections(ScrNetServer *s) { scr_net_server_close_http_connections(s, true); } +/* server.maxConnections: undefined until set; a set number turns away + * connections once that many are open ('drop'). Setting undefined or + * null unsets it. +1 for the getter. */ +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s) { + return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : scr_dyn_retain(scr_dyn_undefined()); +} + +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/) { + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) { + s->max_conns_set = false; + return; + } + double n = NAN; + scr_dyn_number_coerce_js(value, &n); + s->max_conns_set = true; + s->max_conns = n; +} + +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->drop_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +/* server.getConnections(cb): cb(null, count) on the next tick, with the + * count at the call, as Node's process.nextTick(end, null, count). */ +static void scr_net_get_connections_fire(ScrClosure *self) { + ScrDyn *fn = (ScrDyn *)scr_box_get_ref(self->caps[0]); + ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_num(scr_box_get_f64(self->caps[1])) }; + scr_dyn_release(scr_dyn_call(fn, args, 2, "callback")); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + scr_dyn_release(fn); +} + +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/) { + ScrClosure *clo = scr_dyn_listener_closure_fn(callback, (void *)&scr_net_get_connections_fire); + ScrClosure *job = scr_closure_new((void *)&scr_net_get_connections_fire, 2); + job->caps[0] = scr_box_retain(clo->caps[0]); + job->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(job->caps[1], (double)s->nconns); + scr_closure_release(clo); + scr_next_tick(job); +} + /* wrapper.close = fn: the override MOVES in (a compiler-emitted zero-arg * wrapper — see the struct comment); reassignment releases the old one, * matching JS's last-write-wins. */ @@ -1748,6 +1865,48 @@ void scr_net_server_on_timeout(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNet scr_net_ls_add(&s->timeout_ls, cb, (void *)fn, once); } +/* One end of a dropped connection as Node's 'drop' data reports it. */ +static void scr_net_drop_side(ScrDyn *data, const char *prefix, const struct sockaddr_storage *ss) { + char addr[INET6_ADDRSTRLEN] = ""; + int port = 0; + bool v6 = ss->ss_family == AF_INET6; + if (v6) { + inet_ntop(AF_INET6, &((const struct sockaddr_in6 *)ss)->sin6_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in6 *)ss)->sin6_port); + } else { + inet_ntop(AF_INET, &((const struct sockaddr_in *)ss)->sin_addr, addr, sizeof addr); + port = ntohs(((const struct sockaddr_in *)ss)->sin_port); + } + char key[24]; + int n = snprintf(key, sizeof key, "%sAddress", prefix); + ScrStr *a = scr_str_new(addr, strlen(addr)); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(a)); + scr_str_release(a); + n = snprintf(key, sizeof key, "%sPort", prefix); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_num(port)); + n = snprintf(key, sizeof key, "%sFamily", prefix); + ScrStr *f = scr_str_new(v6 ? "IPv6" : "IPv4", 4); + scr_dyn_obj_set(data, key, (size_t)n, scr_dyn_new_str(f)); + scr_str_release(f); +} + +/* A connection past server.maxConnections: 'drop' with the local and + * remote addresses (a null-prototype object in Node), then the close. */ +static void scr_net_server_drop_conn(ScrNetServer *srv, int fd) { + if (srv->drop_ls.n > 0) { + ScrDyn *data = scr_dyn_new_obj(); + data->null_proto = true; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "local", &ss); + len = sizeof ss; + if (getpeername(fd, (struct sockaddr *)&ss, &len) == 0) scr_net_drop_side(data, "remote", &ss); + scr_net_fire_arg_this(&srv->drop_ls, data, srv, SCR_DYNH_NET_SERVER); + scr_dyn_release(data); + } + scr_net_close_fd_raw(fd); +} + /* Accept until EAGAIN; each connection fires the 'connection' listeners * directly (the accepted socket passes +1 per listener via the adapter). */ static void scr_net_server_accept(ScrNetServer *srv) { @@ -1761,6 +1920,13 @@ static void scr_net_server_accept(ScrNetServer *srv) { scr_net_nonblock(fd); int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + if (srv->max_conns_set && (double)srv->nconns >= srv->max_conns) { + /* server.maxConnections: Node closes the connection and emits + * 'drop' with the addresses instead of 'connection' */ + scr_net_server_drop_conn(srv, fd); + if (scr_exc_pending()) return; + continue; + } ScrNetSocket *sock = scr_net_sock_new(); sock->fd = fd; sock->server = scr_net_server_retain(srv); @@ -2307,6 +2473,19 @@ void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms) { #endif } +/* socket.setKeepAlive(enable, initialDelay) as the program calls it: + * answers the socket (+1), Node's chaining. */ +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms) { + scr_net_sock_set_keepalive(s, enable, delay_ms); + return scr_net_sock_retain(s); +} + +/* end(callback) with a dyn callback: once 'finish', called without + * arguments. */ +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + scr_net_sock_on_finish(s, scr_dyn_listener_closure0(listener)); +} + /* socket.destroySoon(): end the write half now, destroy once the FIN is * actually out (buffered bytes flush first — Node's 'finish'-then-destroy). */ void scr_net_sock_destroy_soon(ScrNetSocket *s) { @@ -2377,7 +2556,8 @@ bool scr_net_sock_encrypted(ScrNetSocket *s) { return s->tops != NULL; } ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { if (s->remote_cache) return scr_str_retain(s->remote_cache); - if (s->fd < 0) return NULL; + /* Node's _getpeername answers {} while connecting */ + if (s->fd < 0 || scr_net_sock_connecting(s)) return NULL; struct sockaddr_storage ss; socklen_t len = sizeof ss; if (getpeername(s->fd, (struct sockaddr *)&ss, &len) != 0) return NULL; @@ -2390,9 +2570,251 @@ ScrStr *scr_net_sock_remote_address(ScrNetSocket *s) { } if (!out) return NULL; s->remote_cache = scr_str_new(out, strlen(out)); + s->remote_v6_cache = ss.ss_family == AF_INET6; + s->remote_port_cache = ntohs(ss.ss_family == AF_INET6 ? ((struct sockaddr_in6 *)&ss)->sin6_port + : ((struct sockaddr_in *)&ss)->sin_port); return scr_str_retain(s->remote_cache); } +/* socket.remotePort / remoteFamily: the same cached peer read as + * remoteAddress; NaN / NULL answer undefined. */ +double scr_net_sock_remote_port(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + return s->remote_cache ? (double)s->remote_port_cache : NAN; +} + +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s) { + scr_str_release(scr_net_sock_remote_address(s)); + if (!s->remote_cache) return NULL; + return s->remote_v6_cache ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* socket.localAddress / localPort / localFamily: read from the open + * socket (undefined while connecting and after close, as in Node). */ +static bool scr_net_sock_local(ScrNetSocket *s, char *addr, size_t cap, int *port, bool *v6) { + if (s->fd < 0 || s->connecting || s->dial_deferred || s->lookup_wait) return false; + struct sockaddr_storage ss; + socklen_t len = sizeof ss; + if (getsockname(s->fd, (struct sockaddr *)&ss, &len) != 0) return false; + if (ss.ss_family == AF_INET) { + if (!inet_ntop(AF_INET, &((struct sockaddr_in *)&ss)->sin_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in *)&ss)->sin_port); + *v6 = false; + return true; + } + if (ss.ss_family == AF_INET6) { + if (!inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&ss)->sin6_addr, addr, (socklen_t)cap)) return false; + *port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port); + *v6 = true; + return true; + } + return false; +} + +ScrStr *scr_net_sock_local_address(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? scr_str_new(addr, strlen(addr)) : NULL; +} + +double scr_net_sock_local_port(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + return scr_net_sock_local(s, addr, sizeof addr, &port, &v6) ? (double)port : NAN; +} + +ScrStr *scr_net_sock_local_family(ScrNetSocket *s) { + char addr[INET6_ADDRSTRLEN]; + int port; + bool v6; + if (!scr_net_sock_local(s, addr, sizeof addr, &port, &v6)) return NULL; + return v6 ? scr_str_new("IPv6", 4) : scr_str_new("IPv4", 4); +} + +/* The address fields as dyn values (+1): remotePort (0), remoteFamily + * (1), localAddress (2), localPort (3) and localFamily (4), each + * undefined when unknown. */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field) { + int f = (int)field; + if (f == 0 || f == 3) { + double port = f == 0 ? scr_net_sock_remote_port(s) : scr_net_sock_local_port(s); + return isnan(port) ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(port); + } + ScrStr *v = f == 1 ? scr_net_sock_remote_family(s) + : f == 2 ? scr_net_sock_local_address(s) + : scr_net_sock_local_family(s); + if (v == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *d = scr_dyn_new_str(v); + scr_str_release(v); + return d; +} + +double scr_net_sock_bytes_read(ScrNetSocket *s) { return (double)s->bytes_read; } + +/* socket.connecting: a dial in progress (deferred, looking up, or + * awaiting the TCP handshake). */ +bool scr_net_sock_connecting(ScrNetSocket *s) { + return s->fd >= 0 ? s->connecting : (s->dial_deferred || s->lookup_wait); +} + +/* socket.pending: no open handle yet (or anymore), or still connecting. */ +bool scr_net_sock_pending(ScrNetSocket *s) { return s->fd < 0 || scr_net_sock_connecting(s); } + +/* socket.readyState, Node's getter: opening while connecting, then by + * which halves are still readable and writable. +1. */ +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s) { + const char *state; + if (scr_net_sock_connecting(s)) state = "opening"; + else { + bool r = scr_net_sock_readable(s), w = scr_net_sock_writable(s); + state = r && w ? "open" : r ? "readOnly" : w ? "writeOnly" : "closed"; + } + return scr_str_new(state, strlen(state)); +} + +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once) { + if (s->close_emitted) { + scr_closure_release(cb); + return; + } + scr_net_ls_add(&s->ready_ls, cb, NULL, once); +} + +/* close(hadError) and write(chunk, cb(err)) listeners as dyn functions, + * called with the hadError boolean and null. */ +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once) { + if (s->close_emitted) return; + scr_net_ls_add(&s->close_ls, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), + (void *)&scr_net_dyn_fire_arg, once); +} + +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), + (void *)&scr_net_dyn_fire_arg, true); +} + +/* socket.setTimeout(ms[, callback]) as the program calls it: Node's + * msecs check (a non-negative finite number), then the timer; a callback + * becomes a once('timeout') listener, and with 0 it is removed. */ +static bool scr_net_timeout_msecs_valid(double ms) { + if (isfinite(ms) && ms >= 0) return true; + char recv[48], msg[160]; + scr_num_received(ms, recv); + int len = snprintf(msg, sizeof msg, + "The value of \"msecs\" is out of range. It must be a non-negative finite number. " + "Received %s", + recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); + return false; +} + +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms) { + if (!scr_net_timeout_msecs_valid(ms)) return; + scr_net_sock_set_timeout(s, ms); +} + +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/) { + if (!scr_net_timeout_msecs_valid(ms)) { + scr_closure_release(cb); + return; + } + scr_net_sock_set_timeout(s, ms); + if (ms == 0) { + scr_net_ls_remove_fn(&s->timeout_ls, cb); + scr_closure_release(cb); + return; + } + scr_net_sock_on_timeout(s, cb, true); +} + +/* net.isIP / isIPv4 / isIPv6, as Node's IPv4Reg and IPv6Reg match: dotted + * decimal without leading zeros, and the RFC 4291 text forms (one "::" + * standing for at least one group, an IPv4 tail as two groups) with an + * optional %zone. */ +static bool scr_net_ipv4_text(const char *p, size_t n) { + size_t i = 0; + for (int part = 0; part < 4; part++) { + if (part > 0) { + if (i >= n || p[i] != '.') return false; + i++; + } + size_t start = i; + int value = 0; + while (i < n && p[i] >= '0' && p[i] <= '9' && i - start < 3) value = value * 10 + (p[i++] - '0'); + size_t digits = i - start; + if (digits == 0 || (digits > 1 && p[start] == '0') || value > 255) return false; + } + return i == n; +} + +static bool scr_net_ipv6_text(const char *p, size_t n) { + /* an optional zone: '%' and at least one of [0-9a-zA-Z-.:] */ + for (size_t i = 0; i < n; i++) { + if (p[i] != '%') continue; + if (i + 1 == n) return false; + for (size_t j = i + 1; j < n; j++) { + char c = p[j]; + if (!(isalnum((unsigned char)c) || c == '-' || c == '.' || c == ':')) return false; + } + n = i; + break; + } + int groups = 0; + bool compressed = false; + size_t i = 0; + if (n >= 2 && p[0] == ':' && p[1] == ':') { + compressed = true; + i = 2; + if (i == n) return true; + } else if (n >= 1 && p[0] == ':') { + return false; + } + while (i < n) { + size_t start = i; + while (i < n && isxdigit((unsigned char)p[i]) && i - start < 5) i++; + size_t digits = i - start; + if (i < n && p[i] == '.') { + /* the IPv4 tail ends the address */ + if (!scr_net_ipv4_text(p + start, n - start)) return false; + groups += 2; + i = n; + break; + } + if (digits == 0 || digits > 4) return false; + groups++; + if (i == n) break; + if (p[i] != ':') return false; + i++; + if (i < n && p[i] == ':') { + if (compressed) return false; + compressed = true; + i++; + if (i == n) break; + } else if (i == n) { + return false; /* a single trailing ':' */ + } + } + return compressed ? groups <= 7 : groups == 8; +} + +static ScrStr *scr_net_ip_input(const ScrDyn *input) { + return input->kind == SCR_DYN_STR ? scr_str_retain(input->v.str) : scr_dyn_string_coerce_js(input); +} + +double scr_net_is_ip(const ScrDyn *input) { + ScrStr *t = scr_net_ip_input(input); + if (!t) return 0; + double r = scr_net_ipv4_text(t->data, t->len) ? 4 : scr_net_ipv6_text(t->data, t->len) ? 6 : 0; + scr_str_release(t); + return r; +} + +bool scr_net_is_ipv4(const ScrDyn *input) { return scr_net_is_ip(input) == 4; } +bool scr_net_is_ipv6(const ScrDyn *input) { return scr_net_is_ip(input) == 6; } + void scr_net_sock_destroy(ScrNetSocket *s) { if (s->fd < 0 && !s->in_registry) return; s->rd_eof = true; @@ -2898,7 +3320,9 @@ static void scr_net_sweep(void) { * stack. A dead socket fires them too (the buffered bytes are * gone either way; Node errors them — documented divergence, * the no-backpressure stance). */ - scr_net_fire0_this(&sock->wcb_ls, sock, SCR_DYNH_NET_SOCKET); + ScrDyn *no_error = scr_dyn_new_null(); + scr_net_fire_arg_this(&sock->wcb_ls, no_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(no_error); if (scr_exc_pending()) { scr_net_sock_release(sock); return; @@ -2954,7 +3378,11 @@ static void scr_net_sweep(void) { return; } } - scr_net_fire0_this(&sock->close_ls, sock, SCR_DYNH_NET_SOCKET); + { + ScrDyn *had_error = scr_dyn_new_bool(sock->had_error); + scr_net_fire_arg_this(&sock->close_ls, had_error, sock, SCR_DYNH_NET_SOCKET); + scr_dyn_release(had_error); + } /* settle: listeners drop (cycle story) */ scr_net_ls_drop(&sock->data_ls); scr_net_ls_drop(&sock->end_ls); @@ -2965,6 +3393,7 @@ static void scr_net_sweep(void) { scr_net_ls_drop(&sock->readable_ls); scr_net_ls_drop(&sock->finish_ls); scr_net_ls_drop(&sock->wcb_ls); + scr_net_ls_drop(&sock->ready_ls); if (sock->pipe_dst) { scr_net_sock_release(sock->pipe_dst); sock->pipe_dst = NULL; @@ -3359,13 +3788,23 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho scr_dyn_arg_type_fail("msecs", "of type number", ms); return NULL; } - scr_net_sock_set_timeout(s, ms->v.num); - if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { - scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); - } else if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) { + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF && args[1]->kind != SCR_DYN_FUNC) { scr_dyn_check_listener(args[1], "callback"); return NULL; } + if (!scr_net_timeout_msecs_valid(ms->v.num)) return NULL; + scr_net_sock_set_timeout(s, ms->v.num); + if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { + if (ms->v.num == 0) scr_net_ls_remove_fn(&s->timeout_ls, args[1]->v.fn.clo); + else scr_net_sock_on_timeout(s, scr_dyn_listener_closure0(args[1]), true); + } + return scr_dyn_retain(self); + } + if (strcmp(method, "setKeepAlive") == 0) { + bool enable = argc > 0 && scr_dyn_truthy(args[0]); + double delay = 0; + if (argc > 1 && args[1]->kind != SCR_DYN_UNDEF) scr_dyn_number_coerce_js(args[1], &delay); + scr_net_sock_set_keepalive(s, enable, delay); return scr_dyn_retain(self); } if (strcmp(method, "setEncoding") == 0) { @@ -3410,7 +3849,7 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho if (scr_net_dynh_tls->invoke(s, method, args, argc, &out)) return out; } { - static const char *const known[] = { "connect", "setKeepAlive", + static const char *const known[] = { "connect", "address", "cork", "uncork", "read", "unpipe", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "resetAndDestroy", NULL }; @@ -3447,6 +3886,28 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { return scr_net_sock_encrypted(s) ? scr_dyn_new_bool(true) : NULL; } if (strcmp(key, "bytesWritten") == 0) return scr_dyn_new_num(scr_net_sock_bytes_written(s)); + if (strcmp(key, "bytesRead") == 0) return scr_dyn_new_num(scr_net_sock_bytes_read(s)); + if (strcmp(key, "connecting") == 0) return scr_dyn_new_bool(scr_net_sock_connecting(s)); + if (strcmp(key, "pending") == 0) return scr_dyn_new_bool(scr_net_sock_pending(s)); + if (strcmp(key, "readyState") == 0) { + ScrStr *state = scr_net_sock_ready_state(s); + ScrDyn *d = scr_dyn_new_str(state); + scr_str_release(state); + return d; + } + { + static const char *const fields[] = { "remotePort", "remoteFamily", "localAddress", "localPort", + "localFamily", NULL }; + for (size_t i = 0; fields[i]; i++) { + if (strcmp(key, fields[i]) != 0) continue; + ScrDyn *d = scr_net_sock_addr_field(s, (double)i); + if (d->kind == SCR_DYN_UNDEF) { + scr_dyn_release(d); + return NULL; + } + return d; + } + } if (strcmp(key, "readable") == 0) return scr_dyn_new_bool(scr_net_sock_readable(s)); if (strcmp(key, "writableHighWaterMark") == 0 || strcmp(key, "readableHighWaterMark") == 0) { /* Node's default stream highWaterMark — a constant here (backpressure @@ -3458,9 +3919,7 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { if (scr_net_dynh_tls->get(s, key, &out)) return out; } { - static const char *const known[] = { "remotePort", "remoteFamily", "localAddress", - "localPort", "localFamily", "bytesRead", "connecting", "pending", - "readyState", "bufferSize", "timeout", "closed", "errored", NULL }; + static const char *const known[] = { "bufferSize", "timeout", "closed", "errored", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_unsupported(key, NULL); @@ -3643,6 +4102,15 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method scr_net_server_set_ref((ScrNetServer *)h, method[0] == 'r'); return scr_dyn_retain(self); } + if (strcmp(method, "getConnections") == 0) { + const ScrDyn *cb = argc > 0 ? args[0] : scr_dyn_undefined(); + if (cb->kind != SCR_DYN_FUNC) { + scr_dyn_check_listener(cb, "callback"); + return NULL; + } + scr_net_server_get_connections((ScrNetServer *)h, cb); + return scr_dyn_retain(self); + } if (strcmp(method, "toString") == 0) { ScrStr *str = scr_str_new("[object Object]", 15); ScrDyn *d = scr_dyn_new_str(str); @@ -3650,7 +4118,7 @@ static ScrDyn *scr_net_dynh_srv_invoke(void *h, ScrDyn *self, const char *method return d; } { - static const char *const known[] = { "setTimeout", "getConnections", + static const char *const known[] = { "setTimeout", "off", "removeListener", "removeAllListeners", "emit", "prependListener", "prependOnceListener", "listenerCount", "listeners", "closeAllConnections", "closeIdleConnections", NULL }; @@ -3690,7 +4158,8 @@ static ScrDyn *scr_net_dynh_srv_get(void *h, const char *key, size_t key_len) { return scr_dyn_new_num(scr_net_server_timeout_get(s, (double)timeout_field)); } { - static const char *const known[] = { "maxConnections", "connections", "maxHeadersCount", NULL }; + if (strcmp(key, "maxConnections") == 0) return s->max_conns_set ? scr_dyn_new_num(s->max_conns) : NULL; + static const char *const known[] = { "connections", "maxHeadersCount", NULL }; for (size_t i = 0; known[i]; i++) { if (strcmp(key, known[i]) == 0) { scr_net_dynh_srv_unsupported(key, NULL); @@ -3714,6 +4183,10 @@ static bool scr_net_dynh_srv_set(void *h, const char *key, size_t key_len, const } return true; } + if (strcmp(key, "maxConnections") == 0) { + scr_net_server_max_connections_set(s, value); + return true; + } return false; } diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h index 117a242c7..ad81ac200 100644 --- a/packages/runtime/src/scr_net.h +++ b/packages/runtime/src/scr_net.h @@ -162,6 +162,10 @@ 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_all_connections(ScrNetServer *s); void scr_net_server_close_idle_connections(ScrNetServer *s); +ScrDyn *scr_net_server_max_connections_get(ScrNetServer *s); /* +1 */ +void scr_net_server_max_connections_set(ScrNetServer *s, const ScrDyn *value /*borrowed*/); +void scr_net_server_on_drop(ScrNetServer *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_server_get_connections(ScrNetServer *s, const ScrDyn *callback /*borrowed*/); /* The REAL close behind `wrapper.close.bind(wrapper)` — never consults * the override (the proxy-through idiom cannot recurse). */ void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/); @@ -233,6 +237,26 @@ 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); ScrStr *scr_net_sock_remote_address(ScrNetSocket *s); /* +1 or NULL (undefined arm) */ +double scr_net_sock_remote_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_remote_family(ScrNetSocket *s); /* +1 or NULL */ +ScrStr *scr_net_sock_local_address(ScrNetSocket *s); /* +1 or NULL */ +double scr_net_sock_local_port(ScrNetSocket *s); /* NaN = undefined */ +ScrStr *scr_net_sock_local_family(ScrNetSocket *s); /* +1 or NULL */ +ScrDyn *scr_net_sock_addr_field(ScrNetSocket *s, double field); /* +1 */ +double scr_net_sock_bytes_read(ScrNetSocket *s); +bool scr_net_sock_connecting(ScrNetSocket *s); +bool scr_net_sock_pending(ScrNetSocket *s); +ScrStr *scr_net_sock_ready_state(ScrNetSocket *s); /* +1 */ +void scr_net_sock_on_ready(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once); +void scr_net_sock_on_close_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/, bool once); +void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +void scr_net_sock_on_finish_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/); +ScrNetSocket *scr_net_sock_set_keepalive_ret(ScrNetSocket *s, bool enable, double delay_ms); /* +1 */ +void scr_net_sock_set_timeout_checked(ScrNetSocket *s, double ms); +void scr_net_sock_set_timeout_cb(ScrNetSocket *s, double ms, ScrClosure *cb /*moves*/); +double scr_net_is_ip(const ScrDyn *input); +bool scr_net_is_ipv4(const ScrDyn *input); +bool scr_net_is_ipv6(const ScrDyn *input); bool scr_net_sock_encrypted(ScrNetSocket *s); /* true = TLS transport (false maps to the undefined arm) */ void scr_net_sock_pipe(ScrNetSocket *src, ScrNetSocket *dst); void scr_net_sock_on_data(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once); diff --git a/tests/corpus/5644-net-socket-addresses.ts b/tests/corpus/5644-net-socket-addresses.ts new file mode 100644 index 000000000..fff2f7aee --- /dev/null +++ b/tests/corpus/5644-net-socket-addresses.ts @@ -0,0 +1,70 @@ +// A socket's address fields, byte counts and state: the remote fields +// are cached at first read and stay readable after close, the local ones +// are undefined while connecting and after close, and readyState follows +// the open halves. +import * as net from "node:net"; + +// The server's 'connection' and the client's 'connect' race (either can +// run first), so each side records its line and both print once both +// have run. +let serverLine: string | null = null; +let clientLine: string | null = null; +let startClient: (() => void) | null = null; +function bothConnected(): void { + if (serverLine === null || clientLine === null || startClient === null) return; + console.log(serverLine); + console.log(clientLine); + startClient(); +} + +const server = net.createServer((sock) => { + const port = (server.address() as net.AddressInfo).port; + serverLine = + `server side ${sock.remoteAddress} ${sock.remoteFamily} ${typeof sock.remotePort} ` + + `${sock.localAddress} ${sock.localPort === port} ${sock.localFamily}`; + bothConnected(); + sock.on("data", (d: Buffer) => { + console.log("server got", d.toString(), sock.bytesRead); + sock.end("bye"); + }); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + console.log( + "before connect", + c.readyState, + c.connecting, + c.pending, + c.remoteAddress, + c.remotePort, + c.localAddress, + c.localPort, + c.bytesRead, + ); + c.on("connect", () => { + clientLine = + `connected ${c.readyState} ${c.connecting} ${c.pending} ${c.remotePort === port} ` + + `${c.remoteFamily} ${c.localAddress} ${typeof c.localPort} ${c.localFamily}`; + startClient = () => c.write("hello"); + bothConnected(); + }); + c.on("data", (d: Buffer) => console.log("client got", d.toString(), c.bytesRead)); + c.on("end", () => console.log("end", c.readyState, c.bytesRead, c.bytesWritten)); + c.on("close", () => { + console.log( + "closed", + c.readyState, + c.pending, + c.remoteAddress, + c.remotePort === port, + c.localAddress, + c.localPort, + ); + const d = net.connect(port, "127.0.0.1"); + d.destroy(); + console.log("destroyed before connect", d.readyState, d.connecting, d.pending, d.remoteAddress); + d.on("close", () => server.close()); + }); +}); diff --git a/tests/corpus/5645-net-socket-callbacks.ts b/tests/corpus/5645-net-socket-callbacks.ts new file mode 100644 index 000000000..c80704828 --- /dev/null +++ b/tests/corpus/5645-net-socket-callbacks.ts @@ -0,0 +1,72 @@ +// write and end callbacks, close listeners with hadError, the 'ready' +// event, string 'data' chunks after setEncoding, setTimeout with a callback (and its msecs checks) and +// setKeepAlive's chaining. +import * as net from "node:net"; + +// Each side keeps its own list: the client's callbacks and the server's +// events can interleave either way, so the lists print one after the +// other at the end. +const events: string[] = []; +const serverEvents: string[] = []; +let serverSockets = 0; +const server = net.createServer((sock) => { + sock.setEncoding("utf8"); + sock.on("data", (d: string) => { + serverEvents.push(`server data ${typeof d} ${d}`); + sock.end("pong", () => serverEvents.push("server end callback")); + }); + // the first connection's server side (the timeout connection's close + // order against the client's depends on the event loop phase) + if (serverSockets++ === 0) { + sock.on("close", (hadError: boolean) => serverEvents.push(`server close ${hadError}`)); + } +}); + +function attempt(label: string, f: () => void): void { + try { + f(); + events.push(`${label} ok`); + } catch (err) { + const e = err as NodeJS.ErrnoException; + events.push(`${label} ${e.name} ${e.code} ${e.message}`); + } +} + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const c = net.connect(port, "127.0.0.1"); + c.on("connect", () => { + events.push("connect"); + c.write("ping", (err?: Error | null) => events.push(`write callback ${err === null}`)); + events.push(`setKeepAlive chains ${c.setKeepAlive(true, 1000) === c}`); + }); + c.on("ready", () => events.push("ready")); + c.on("data", (d: Buffer) => events.push(`client data ${d.toString()}`)); + c.on("end", () => { + events.push("client end"); + c.end(() => events.push("client end callback")); + }); + c.on("close", (hadError: boolean) => { + events.push(`client close ${hadError}`); + const t = net.connect(port, "127.0.0.1", () => { + attempt("negative", () => t.setTimeout(-1)); + attempt("NaN", () => t.setTimeout(NaN)); + attempt("Infinity", () => t.setTimeout(Infinity)); + const never = () => events.push("removed callback ran"); + t.setTimeout(20, never); + t.setTimeout(0, never); + t.setTimeout(30, () => { + events.push("timeout callback"); + t.destroy(); + }); + }); + t.on("timeout", () => events.push("timeout event")); + t.on("close", (hadError: boolean) => { + events.push(`t close ${hadError}`); + server.close(() => { + console.log(serverEvents.join("\n")); + console.log(events.join("\n")); + }); + }); + }); +}); diff --git a/tests/corpus/5646-net-server-connections.ts b/tests/corpus/5646-net-server-connections.ts new file mode 100644 index 000000000..9f2e01aec --- /dev/null +++ b/tests/corpus/5646-net-server-connections.ts @@ -0,0 +1,44 @@ +// server.maxConnections turns away connections past the cap with a +// 'drop' event carrying their addresses, and getConnections reports the +// open count on the next tick. +import * as net from "node:net"; + +const server = net.createServer((sock) => { + sock.on("data", () => {}); +}); +console.log("default", server.maxConnections); +server.maxConnections = 1; +console.log("set", server.maxConnections); +server.on("drop", (data?: net.DropArgument) => { + console.log( + "drop", + data?.localAddress, + typeof data?.localPort, + data?.localFamily, + data?.remoteAddress, + typeof data?.remotePort, + data?.remoteFamily, + ); +}); + +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const first = net.connect(port, "127.0.0.1", () => { + const second = net.connect(port, "127.0.0.1"); + second.resume(); + second.on("close", () => { + console.log("second closed"); + process.nextTick(() => console.log("tick before")); + const same = server.getConnections((err: Error | null, count: number) => { + console.log("connections", err, count); + }); + console.log("getConnections chains", same === server); + process.nextTick(() => console.log("tick after")); + Promise.resolve().then(() => console.log("microtask")); + server.maxConnections = undefined; + console.log("unset", server.maxConnections); + first.destroy(); + server.close(() => console.log("server closed")); + }); + }); +}); diff --git a/tests/corpus/5647-net-is-ip.ts b/tests/corpus/5647-net-is-ip.ts new file mode 100644 index 000000000..4db2cc0cc --- /dev/null +++ b/tests/corpus/5647-net-is-ip.ts @@ -0,0 +1,34 @@ +// net.isIP, isIPv4 and isIPv6 follow Node's address patterns: dotted +// decimal without leading zeros, and IPv6 text forms with one "::", an +// IPv4 tail and an optional %zone. +import * as net from "node:net"; + +const inputs = [ + "1.2.3.4", + "255.255.255.255", + "01.2.3.4", + "256.1.1.1", + "1.2.3", + "1.2.3.4.5", + "::", + "::1", + "fe80::1%eth0", + "fe80::1%", + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7::", + "::1:2:3:4:5:6:7", + "1:2:3:4:5:6:7:8:9", + "1::2::3", + ":1::", + "1:", + "::ffff:1.2.3.4", + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5:6:7:1.2.3.4", + "12345::1", + "abcd:EF01::", + "", + "localhost", +]; +for (const input of inputs) { + console.log(JSON.stringify(input), net.isIP(input), net.isIPv4(input), net.isIPv6(input)); +}