From 809992229f26012a1ec08fd920f50271894abc39 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Wed, 28 May 2025 16:04:37 -0800 Subject: [PATCH] node:net rework (#18962) Co-authored-by: nektro <5464072+nektro@users.noreply.github.com> --- .buildkite/ci.mjs | 10 +- docs/runtime/nodejs-apis.md | 2 +- packages/bun-types/bun.d.ts | 90 +- packages/bun-usockets/src/context.c | 20 +- packages/bun-usockets/src/crypto/openssl.c | 30 +- src/async/posix_event_loop.zig | 4 +- src/bun.js/api/bun/h2_frame_parser.zig | 3 +- src/bun.js/api/bun/socket.zig | 98 +- src/bun.js/api/sockets.classes.ts | 12 +- src/bun.js/bindings/ErrorCode.cpp | 25 +- src/bun.js/bindings/ErrorCode.ts | 4 + src/bun.js/bindings/NodeAsyncHooks.cpp | 17 - src/bun.js/bindings/NodeAsyncHooks.h | 4 +- src/bun.js/node/node_net_binding.zig | 32 + src/codegen/bundle-modules.ts | 4 + src/codegen/class-definitions.ts | 13 +- src/codegen/client-js.ts | 4 +- src/codegen/generate-classes.ts | 46 +- src/deps/c_ares.zig | 2 +- src/deps/libuv.zig | 2 +- src/deps/uws.zig | 31 +- src/dns.zig | 6 +- src/js/builtins.d.ts | 13 + src/js/internal/shared.ts | 33 + src/js/internal/timers.ts | 1 + src/js/node/async_hooks.ts | 3 +- src/js/node/events.ts | 9 +- src/js/node/http2.ts | 15 +- src/js/node/net.ts | 1461 +++++++++++++---- src/js/node/tty.ts | 3 + src/js/private.d.ts | 4 + test/js/bun/net/socket.test.ts | 2 +- test/js/bun/util/inspect-error-leak.test.js | 3 +- test/js/node/net/node-net.test.ts | 16 +- test/js/node/net/node-unref-fixture.js | 4 + .../test-cluster-worker-handle-close.js | 27 - .../test/parallel/test-dns-channel-timeout.js | 9 +- .../test-http2-forget-closed-streams.js | 1 + .../parallel/test-http2-priority-cycle-.js | 69 + .../test-http2-server-close-callback.js | 27 + .../node/test/parallel/test-http2-timeouts.js | 60 + ...test-http2-trailers-after-session-close.js | 2 +- .../test/parallel/test-net-after-close.js | 52 + .../test/parallel/test-net-allow-half-open.js | 47 + .../test-net-autoselectfamily-ipv4first.js | 52 + ...net-better-error-messages-port-hostname.js | 34 + .../node/test/parallel/test-net-blocklist.js | 68 + .../parallel/test-net-bytes-written-large.js | 67 + .../parallel/test-net-can-reset-timeout.js | 57 + .../test-net-connect-abort-controller.js | 1 + .../test-net-connect-after-destroy.js | 9 + .../test-net-connect-immediate-finish.js | 59 + .../test/parallel/test-net-connect-nodelay.js | 49 + .../parallel/test-net-connect-options-ipv6.js | 67 + .../parallel/test-net-connect-options-port.js | 230 +++ ...test-net-connect-reset-before-connected.js | 13 + ...t-net-deprecated-setsimultaneousaccepts.js | 18 + .../parallel/test-net-dns-custom-lookup.js | 67 + .../node/test/parallel/test-net-dns-error.js | 41 + .../node/test/parallel/test-net-dns-lookup.js | 40 + .../node/test/parallel/test-net-eaddrinuse.js | 35 + .../node/test/parallel/test-net-keepalive.js | 52 + .../test/parallel/test-net-options-lookup.js | 52 + .../parallel/test-net-persistent-keepalive.js | 34 + .../parallel/test-net-remote-address-port.js | 86 + .../parallel/test-net-server-blocklist.js | 20 + ...multaneous-accepts-produce-warning-once.js | 19 + .../node/test/parallel/test-net-settimeout.js | 50 + ...socket-connect-invalid-autoselectfamily.js | 9 + ...-invalid-autoselectfamilyattempttimeout.js | 27 + .../parallel/test-net-socket-destroy-send.js | 24 + .../parallel/test-net-socket-destroy-twice.js | 36 + .../parallel/test-net-socket-reset-twice.js | 15 + .../node/test/parallel/test-net-throttle.js | 88 + .../parallel/test-net-timeout-no-handle.js | 17 + ...imeout-removes-other-socket-unref-timer.js | 43 + .../parallel/test-tls-cert-ext-encoding.js | 1 + .../test/parallel/test-tls-close-error.js | 24 + .../parallel/test-tls-connect-hints-option.js | 31 + .../test-tls-friendly-error-message.js | 45 + .../test/parallel/test-tls-on-empty-socket.js | 12 +- .../test/parallel/test-tls-request-timeout.js | 51 + .../test-worker-message-port-wasm-threads.js | 53 - test/js/node/tls/node-tls-connect.test.ts | 3 +- test/js/node/tls/node-tls-namedpipes.test.ts | 12 +- 85 files changed, 3474 insertions(+), 557 deletions(-) delete mode 100644 test/js/node/test/parallel/test-cluster-worker-handle-close.js create mode 100644 test/js/node/test/parallel/test-http2-priority-cycle-.js create mode 100644 test/js/node/test/parallel/test-http2-server-close-callback.js create mode 100644 test/js/node/test/parallel/test-http2-timeouts.js create mode 100644 test/js/node/test/parallel/test-net-after-close.js create mode 100644 test/js/node/test/parallel/test-net-allow-half-open.js create mode 100644 test/js/node/test/parallel/test-net-autoselectfamily-ipv4first.js create mode 100644 test/js/node/test/parallel/test-net-better-error-messages-port-hostname.js create mode 100644 test/js/node/test/parallel/test-net-blocklist.js create mode 100644 test/js/node/test/parallel/test-net-bytes-written-large.js create mode 100644 test/js/node/test/parallel/test-net-can-reset-timeout.js create mode 100644 test/js/node/test/parallel/test-net-connect-after-destroy.js create mode 100644 test/js/node/test/parallel/test-net-connect-immediate-finish.js create mode 100644 test/js/node/test/parallel/test-net-connect-nodelay.js create mode 100644 test/js/node/test/parallel/test-net-connect-options-ipv6.js create mode 100644 test/js/node/test/parallel/test-net-connect-options-port.js create mode 100644 test/js/node/test/parallel/test-net-connect-reset-before-connected.js create mode 100644 test/js/node/test/parallel/test-net-deprecated-setsimultaneousaccepts.js create mode 100644 test/js/node/test/parallel/test-net-dns-custom-lookup.js create mode 100644 test/js/node/test/parallel/test-net-dns-error.js create mode 100644 test/js/node/test/parallel/test-net-dns-lookup.js create mode 100644 test/js/node/test/parallel/test-net-eaddrinuse.js create mode 100644 test/js/node/test/parallel/test-net-keepalive.js create mode 100644 test/js/node/test/parallel/test-net-options-lookup.js create mode 100644 test/js/node/test/parallel/test-net-persistent-keepalive.js create mode 100644 test/js/node/test/parallel/test-net-remote-address-port.js create mode 100644 test/js/node/test/parallel/test-net-server-blocklist.js create mode 100644 test/js/node/test/parallel/test-net-server-simultaneous-accepts-produce-warning-once.js create mode 100644 test/js/node/test/parallel/test-net-settimeout.js create mode 100644 test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamily.js create mode 100644 test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamilyattempttimeout.js create mode 100644 test/js/node/test/parallel/test-net-socket-destroy-send.js create mode 100644 test/js/node/test/parallel/test-net-socket-destroy-twice.js create mode 100644 test/js/node/test/parallel/test-net-socket-reset-twice.js create mode 100644 test/js/node/test/parallel/test-net-throttle.js create mode 100644 test/js/node/test/parallel/test-net-timeout-no-handle.js create mode 100644 test/js/node/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js create mode 100644 test/js/node/test/parallel/test-tls-close-error.js create mode 100644 test/js/node/test/parallel/test-tls-connect-hints-option.js create mode 100644 test/js/node/test/parallel/test-tls-friendly-error-message.js create mode 100644 test/js/node/test/parallel/test-tls-request-timeout.js delete mode 100644 test/js/node/test/parallel/test-worker-message-port-wasm-threads.js diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 8aa1460057..e1d1678671 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -228,13 +228,7 @@ function getRetry(limit = 0) { manual: { permit_on_passed: true, }, - automatic: [ - { exit_status: 1, limit }, - { exit_status: -1, limit: 1 }, - { exit_status: 255, limit: 1 }, - { signal_reason: "cancel", limit: 1 }, - { signal_reason: "agent_stop", limit: 1 }, - ], + automatic: false, }; } @@ -572,7 +566,7 @@ function getTestBunStep(platform, options, testOptions = {}) { retry: getRetry(), cancel_on_build_failing: isMergeQueue(), parallelism: unifiedTests ? undefined : os === "darwin" ? 2 : 10, - timeout_in_minutes: profile === "asan" ? 90 : 30, + timeout_in_minutes: profile === "asan" ? 45 : 30, command: os === "windows" ? `node .\\scripts\\runner.node.mjs ${args.join(" ")}` diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md index 21c5fee0af..5ed9a44a9e 100644 --- a/docs/runtime/nodejs-apis.md +++ b/docs/runtime/nodejs-apis.md @@ -120,7 +120,7 @@ This page is updated regularly to reflect compatibility status of the latest ver ### [`node:net`](https://nodejs.org/api/net.html) -🟡 `SocketAddress` class not exposed (but implemented). `BlockList` exists but is a no-op. +🟢 Fully implemented. ### [`node:perf_hooks`](https://nodejs.org/api/perf_hooks.html) diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index fcdf05b290..2da4b78b93 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3686,7 +3686,7 @@ declare module "bun" { * the well-known CAs curated by Mozilla. Mozilla's CAs are completely * replaced when CAs are explicitly specified using this option. */ - ca?: string | Buffer | BunFile | Array | undefined; + ca?: string | BufferSource | BunFile | Array | undefined; /** * Cert chains in PEM format. One cert chain should be provided per * private key. Each cert chain should consist of the PEM formatted @@ -3698,7 +3698,7 @@ declare module "bun" { * intermediate certificates are not provided, the peer will not be * able to validate the certificate, and the handshake will fail. */ - cert?: string | Buffer | BunFile | Array | undefined; + cert?: string | BufferSource | BunFile | Array | undefined; /** * Private keys in PEM format. PEM allows the option of private keys * being encrypted. Encrypted keys will be decrypted with @@ -3709,13 +3709,25 @@ declare module "bun" { * object.passphrase is optional. Encrypted keys will be decrypted with * object.passphrase if provided, or options.passphrase if it is not. */ - key?: string | Buffer | BunFile | Array | undefined; + key?: string | BufferSource | BunFile | Array | undefined; /** * Optionally affect the OpenSSL protocol behavior, which is not * usually necessary. This should be used carefully if at all! Value is * a numeric bitmask of the SSL_OP_* options from OpenSSL Options */ secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options + + keyFile?: string; + + certFile?: string; + + ALPNProtocols?: string | BufferSource; + + ciphers?: string; + + clientRenegotiationLimit?: number; + + clientRenegotiationWindow?: number; } // Note for contributors: TLSOptionsAsDeprecated should be considered immutable @@ -6084,7 +6096,7 @@ declare module "bun" { * certificate. * @return A certificate object. */ - getPeerCertificate(): import("tls").PeerCertificate; + getPeerCertificate(): import("node:tls").PeerCertificate; getPeerX509Certificate(): import("node:crypto").X509Certificate; /** @@ -6189,6 +6201,34 @@ declare module "bun" { * The number of bytes written to the socket. */ readonly bytesWritten: number; + + resume(): void; + + pause(): void; + + renegotiate(): void; + + setVerifyMode(requestCert: boolean, rejectUnauthorized: boolean): void; + + getSession(): void; + + setSession(session: string | Buffer | BufferSource): void; + + exportKeyingMaterial(length: number, label: string, context?: string | BufferSource): void; + + upgradeTLS(options: TLSUpgradeOptions): [raw: Socket, tls: Socket]; + + close(): void; + + getServername(): string; + + setServername(name: string): void; + } + + interface TLSUpgradeOptions { + data?: Data; + tls: TLSOptions | boolean; + socket: SocketHandler; } interface SocketListener extends Disposable { @@ -6289,6 +6329,22 @@ declare module "bun" { * The per-instance data context */ data?: Data; + /** + * Whether to allow half-open connections. + * + * A half-open connection occurs when one end of the connection has called `close()` + * or sent a FIN packet, while the other end remains open. When set to `true`: + * + * - The socket won't automatically send FIN when the remote side closes its end + * - The local side can continue sending data even after the remote side has closed + * - The application must explicitly call `end()` to fully close the connection + * + * When `false`, the socket automatically closes both ends of the connection when + * either side closes. + * + * @default false + */ + allowHalfOpen?: boolean; } interface TCPSocketListenOptions extends SocketOptions { @@ -6303,7 +6359,7 @@ declare module "bun" { /** * The TLS configuration object with which to create the server */ - tls?: TLSOptions; + tls?: TLSOptions | boolean; /** * Whether to use exclusive mode. * @@ -6349,7 +6405,7 @@ declare module "bun" { /** * TLS Configuration with which to create the socket */ - tls?: boolean; + tls?: TLSOptions | boolean; /** * Whether to use exclusive mode. * @@ -6365,22 +6421,8 @@ declare module "bun" { * @default false */ exclusive?: boolean; - /** - * Whether to allow half-open connections. - * - * A half-open connection occurs when one end of the connection has called `close()` - * or sent a FIN packet, while the other end remains open. When set to `true`: - * - * - The socket won't automatically send FIN when the remote side closes its end - * - The local side can continue sending data even after the remote side has closed - * - The application must explicitly call `end()` to fully close the connection - * - * When `false` (default), the socket automatically closes both ends of the connection - * when either side closes. - * - * @default false - */ - allowHalfOpen?: boolean; + reusePort?: boolean; + ipv6Only?: boolean; } interface UnixSocketOptions extends SocketOptions { @@ -6391,14 +6433,14 @@ declare module "bun" { /** * TLS Configuration with which to create the socket */ - tls?: TLSOptions; + tls?: TLSOptions | boolean; } interface FdSocketOptions extends SocketOptions { /** * TLS Configuration with which to create the socket */ - tls?: TLSOptions; + tls?: TLSOptions | boolean; /** * The file descriptor to connect to */ diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index 6b734b2100..faf79d6cf3 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -440,7 +440,7 @@ struct us_socket_t* us_socket_context_connect_resolved_dns(struct us_socket_cont socket->flags.is_paused = 0; socket->flags.is_ipc = 0; socket->connect_state = NULL; - + socket->connect_next = NULL; us_internal_socket_context_link_socket(context, socket); @@ -459,7 +459,7 @@ static void init_addr_with_port(struct addrinfo* info, int port, struct sockaddr } } -static int try_parse_ip(const char *ip_str, int port, struct sockaddr_storage *storage) { +static bool try_parse_ip(const char *ip_str, int port, struct sockaddr_storage *storage) { memset(storage, 0, sizeof(struct sockaddr_storage)); // Try to parse as IPv4 struct sockaddr_in *addr4 = (struct sockaddr_in *)storage; @@ -469,7 +469,7 @@ static int try_parse_ip(const char *ip_str, int port, struct sockaddr_storage *s #ifdef __APPLE__ addr4->sin_len = sizeof(struct sockaddr_in); #endif - return 0; + return 1; } // Try to parse as IPv6 @@ -480,17 +480,17 @@ static int try_parse_ip(const char *ip_str, int port, struct sockaddr_storage *s #ifdef __APPLE__ addr6->sin6_len = sizeof(struct sockaddr_in6); #endif - return 0; + return 1; } // If we reach here, the input is neither IPv4 nor IPv6 - return 1; + return 0; } -void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, int options, int socket_ext_size, int* is_connecting) { +void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, const char *host, int port, int options, int socket_ext_size, int* has_dns_resolved) { #ifndef LIBUS_NO_SSL if (ssl == 1) { - return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, options, socket_ext_size, is_connecting); + return us_internal_ssl_socket_context_connect((struct us_internal_ssl_socket_context_t *) context, host, port, options, socket_ext_size, has_dns_resolved); } #endif @@ -498,8 +498,8 @@ void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, co // fast path for IP addresses in text form struct sockaddr_storage addr; - if (try_parse_ip(host, port, &addr) == 0) { - *is_connecting = 1; + if (try_parse_ip(host, port, &addr)) { + *has_dns_resolved = 1; return us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size); } @@ -518,7 +518,7 @@ void *us_socket_context_connect(int ssl, struct us_socket_context_t *context, co if (result->entries && result->entries->info.ai_next == NULL) { struct sockaddr_storage addr; init_addr_with_port(&result->entries->info, port, &addr); - *is_connecting = 1; + *has_dns_resolved = 1; struct us_socket_t *s = us_socket_context_connect_resolved_dns(context, &addr, options, socket_ext_size); Bun__addrinfo_freeRequest(ai_req, s == NULL); return s; diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 4649f743ba..49b535d818 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -213,7 +213,7 @@ struct us_internal_ssl_socket_t *ssl_on_open(struct us_internal_ssl_socket_t *s, s->ssl_read_wants_write = 0; s->fatal_error = 0; s->handshake_state = HANDSHAKE_PENDING; - + SSL_set_bio(s->ssl, loop_ssl_data->shared_rbio, loop_ssl_data->shared_wbio); // if we allow renegotiation, we need to set the mode here @@ -255,7 +255,7 @@ struct us_internal_ssl_socket_t *ssl_on_open(struct us_internal_ssl_socket_t *s, } /// @brief Complete the shutdown or do a fast shutdown when needed, this should only be called before closing the socket -/// @param s +/// @param s int us_internal_handle_shutdown(struct us_internal_ssl_socket_t *s, int force_fast_shutdown) { // if we are already shutdown or in the middle of a handshake we dont need to do anything // Scenarios: @@ -265,7 +265,7 @@ int us_internal_handle_shutdown(struct us_internal_ssl_socket_t *s, int force_fa // 4 - we are in the middle of a handshake // 5 - we received a fatal error if(us_internal_ssl_socket_is_shut_down(s) || s->fatal_error || !SSL_is_init_finished(s->ssl)) return 1; - + // we are closing the socket but did not sent a shutdown yet int state = SSL_get_shutdown(s->ssl); int sent_shutdown = state & SSL_SENT_SHUTDOWN; @@ -277,7 +277,7 @@ int us_internal_handle_shutdown(struct us_internal_ssl_socket_t *s, int force_fa // Zero means that we should wait for the peer to close the connection // but we are already closing the connection so we do a fast shutdown here int ret = SSL_shutdown(s->ssl); - if(ret == 0 && force_fast_shutdown) { + if(ret == 0 && force_fast_shutdown) { // do a fast shutdown (dont wait for peer) ret = SSL_shutdown(s->ssl); } @@ -397,7 +397,7 @@ void us_internal_update_handshake(struct us_internal_ssl_socket_t *s) { // nothing todo here, renegotiation must be handled in SSL_read if (s->handshake_state != HANDSHAKE_PENDING) return; - + if (us_internal_ssl_socket_is_closed(s) || us_internal_ssl_socket_is_shut_down(s) || (s->ssl && SSL_get_shutdown(s->ssl) & SSL_RECEIVED_SHUTDOWN)) { @@ -422,7 +422,7 @@ void us_internal_update_handshake(struct us_internal_ssl_socket_t *s) { s->fatal_error = 1; } us_internal_trigger_handshake_callback(s, 0); - + return; } s->handshake_state = HANDSHAKE_PENDING; @@ -504,7 +504,7 @@ restart: loop_ssl_data->ssl_read_output + LIBUS_RECV_BUFFER_PADDING + read, LIBUS_RECV_BUFFER_LENGTH - read); - + if (just_read <= 0) { int err = SSL_get_error(s->ssl, just_read); // as far as I know these are the only errors we want to handle @@ -603,7 +603,7 @@ restart: goto restart; } } - // Trigger writable if we failed last SSL_write with SSL_ERROR_WANT_READ + // Trigger writable if we failed last SSL_write with SSL_ERROR_WANT_READ // If we failed SSL_read because we need to write more data (SSL_ERROR_WANT_WRITE) we are not going to trigger on_writable, we will wait until the next on_data or on_writable event // SSL_read will try to flush the write buffer and if fails with SSL_ERROR_WANT_WRITE means the socket is not in a writable state anymore and only makes sense to trigger on_writable if we can write more data // Otherwise we possible would trigger on_writable -> on_data event in a recursive loop @@ -1133,7 +1133,7 @@ int us_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { } SSL_CTX *create_ssl_context_from_bun_options( - struct us_bun_socket_context_options_t options, + struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err) { ERR_clear_error(); @@ -1250,8 +1250,8 @@ SSL_CTX *create_ssl_context_from_bun_options( return NULL; } - // It may return spurious errors here. - ERR_clear_error(); + // It may return spurious errors here. + ERR_clear_error(); if (options.reject_unauthorized) { SSL_CTX_set_verify(ssl_context, @@ -1755,7 +1755,7 @@ int us_internal_ssl_socket_raw_write(struct us_internal_ssl_socket_t *s, int us_internal_ssl_socket_write(struct us_internal_ssl_socket_t *s, const char *data, int length, int msg_more) { - + if (us_socket_is_closed(0, &s->s) || us_internal_ssl_socket_is_shut_down(s) || length == 0) { return 0; } @@ -1989,7 +1989,7 @@ ssl_wrapped_context_on_end(struct us_internal_ssl_socket_t *s) { if (wrapped_context->events.on_end) { wrapped_context->events.on_end((struct us_socket_t *)s); } - + return s; } @@ -2082,7 +2082,7 @@ struct us_internal_ssl_socket_t *us_internal_ssl_socket_wrap_with_tls( struct us_socket_context_t *context = us_create_bun_ssl_socket_context( old_context->loop, sizeof(struct us_wrapped_socket_context_t), options, &err); - + // Handle SSL context creation failure if (UNLIKELY(!context)) { return NULL; @@ -2186,4 +2186,4 @@ us_socket_context_on_socket_connect_error( return socket; } -#endif \ No newline at end of file +#endif diff --git a/src/async/posix_event_loop.zig b/src/async/posix_event_loop.zig index 5ccbae9e78..7e8a8c989c 100644 --- a/src/async/posix_event_loop.zig +++ b/src/async/posix_event_loop.zig @@ -664,7 +664,7 @@ pub const FilePoll = struct { /// Only intended to be used from EventLoop.Pollable fn deactivate(this: *FilePoll, loop: *Loop) void { - loop.num_polls -= @as(i32, @intFromBool(this.flags.contains(.has_incremented_poll_count))); + if (this.flags.contains(.has_incremented_poll_count)) loop.dec(); this.flags.remove(.has_incremented_poll_count); loop.subActive(@as(u32, @intFromBool(this.flags.contains(.has_incremented_active_count)))); @@ -676,7 +676,7 @@ pub const FilePoll = struct { fn activate(this: *FilePoll, loop: *Loop) void { this.flags.remove(.closed); - loop.num_polls += @as(i32, @intFromBool(!this.flags.contains(.has_incremented_poll_count))); + if (!this.flags.contains(.has_incremented_poll_count)) loop.inc(); this.flags.insert(.has_incremented_poll_count); if (this.flags.contains(.keeps_event_loop_alive)) { diff --git a/src/bun.js/api/bun/h2_frame_parser.zig b/src/bun.js/api/bun/h2_frame_parser.zig index cb55afd056..408c2c6072 100644 --- a/src/bun.js/api/bun/h2_frame_parser.zig +++ b/src/bun.js/api/bun/h2_frame_parser.zig @@ -657,7 +657,7 @@ pub const H2FrameParser = struct { const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); pub const ref = RefCount.ref; pub const deref = RefCount.deref; - const ENABLE_AUTO_CORK = true; // ENABLE CORK OPTIMIZATION + const ENABLE_AUTO_CORK = false; // ENABLE CORK OPTIMIZATION const ENABLE_ALLOCATOR_POOL = true; // ENABLE HIVE ALLOCATOR OPTIMIZATION const MAX_BUFFER_SIZE = 32768; @@ -1677,6 +1677,7 @@ pub const H2FrameParser = struct { JSC.markBinding(@src()); log("write {}", .{bytes.len}); if (comptime ENABLE_AUTO_CORK) { + // TODO: make this use AutoFlusher this.cork(); const available = CORK_BUFFER[CORK_OFFSET..]; if (bytes.len > available.len) { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 05e08c94bd..eb627f79b0 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1062,6 +1062,10 @@ pub const Listener = struct { } pub fn connect(globalObject: *JSC.JSGlobalObject, opts: JSValue) bun.JSError!JSValue { + return connectInner(globalObject, null, null, opts); + } + + pub fn connectInner(globalObject: *JSC.JSGlobalObject, prev_maybe_tcp: ?*TCPSocket, prev_maybe_tls: ?*TLSSocket, opts: JSValue) bun.JSError!JSValue { if (opts.isEmptyOrUndefinedOrNull() or opts.isBoolean() or !opts.isObject()) { return globalObject.throwInvalidArguments("Expected options object", .{}); } @@ -1129,14 +1133,23 @@ pub const Listener = struct { var handlers_ptr = handlers.vm.allocator.create(Handlers) catch bun.outOfMemory(); handlers_ptr.* = handlers; - handlers_ptr.is_server = false; var promise = JSC.JSPromise.create(globalObject); const promise_value = promise.toJS(); handlers_ptr.promise.set(globalObject, promise_value); if (ssl_enabled) { - var tls = TLSSocket.new(.{ + var tls = if (prev_maybe_tls) |prev| blk: { + bun.destroy(prev.handlers); + bun.assert(prev.this_value != .zero); + prev.handlers = handlers_ptr; + bun.assert(prev.socket.socket == .detached); + prev.connection = connection; + prev.protos = if (protos) |p| (bun.default_allocator.dupe(u8, p) catch bun.outOfMemory()) else null; + prev.server_name = server_name; + prev.socket_context = null; + break :blk prev; + } else TLSSocket.new(.{ .ref_count = .init(), .handlers = handlers_ptr, .this_value = .zero, @@ -1162,7 +1175,16 @@ pub const Listener = struct { tls.socket = TLSSocket.Socket.fromNamedPipe(named_pipe); } } else { - var tcp = TCPSocket.new(.{ + var tcp = if (prev_maybe_tcp) |prev| blk: { + bun.assert(prev.this_value != .zero); + prev.handlers = handlers_ptr; + bun.assert(prev.socket.socket == .detached); + bun.assert(prev.connection == null); + bun.assert(prev.protos == null); + bun.assert(prev.server_name == null); + prev.socket_context = null; + break :blk prev; + } else TCPSocket.new(.{ .ref_count = .init(), .handlers = handlers_ptr, .this_value = .zero, @@ -1238,7 +1260,18 @@ pub const Listener = struct { switch (ssl_enabled) { inline else => |is_ssl_enabled| { const SocketType = NewSocket(is_ssl_enabled); - const socket = bun.new(SocketType, .{ + const maybe_previous: ?*SocketType = if (is_ssl_enabled) prev_maybe_tls else prev_maybe_tcp; + + const socket = if (maybe_previous) |prev| blk: { + bun.assert(prev.this_value != .zero); + prev.handlers = handlers_ptr; + bun.assert(prev.socket.socket == .detached); + prev.connection = connection; + prev.protos = if (protos) |p| (bun.default_allocator.dupe(u8, p) catch bun.outOfMemory()) else null; + prev.server_name = server_name; + prev.socket_context = socket_context; + break :blk prev; + } else bun.new(SocketType, .{ .ref_count = .init(), .handlers = handlers_ptr, .this_value = .zero, @@ -1248,7 +1281,7 @@ pub const Listener = struct { .server_name = server_name, .socket_context = socket_context, // owns the socket context }); - + socket.ref(); SocketType.js.dataSetCached(socket.getThisValue(globalObject), globalObject, default_data); socket.flags.allow_half_open = socket_config.allowHalfOpen; socket.doConnect(connection) catch { @@ -1256,7 +1289,9 @@ pub const Listener = struct { return promise_value; }; - socket.poll_ref.ref(handlers.vm); + // if this is from node:net there's surface where the user can .ref() and .deref() before the connection starts. make sure we honor that here. + // in the Bun.connect path, this will always be true at this point in time. + if (socket.ref_pollref_on_connect) socket.poll_ref.ref(handlers.vm); return promise_value; }, @@ -1342,9 +1377,11 @@ fn NewSocket(comptime ssl: bool) type { flags: Flags = .{}, ref_count: RefCount, wrapped: WrappedType = .none, + // TODO: make this optional handlers: *Handlers, this_value: JSC.JSValue = .zero, poll_ref: Async.KeepAlive = Async.KeepAlive.init(), + ref_pollref_on_connect: bool = true, connection: ?Listener.UnixOrHost = null, protos: ?[]const u8, server_name: ?[]const u8 = null, @@ -1441,9 +1478,7 @@ fn NewSocket(comptime ssl: bool) type { pub fn doConnect(this: *This, connection: Listener.UnixOrHost) !void { bun.assert(this.socket_context != null); this.ref(); - errdefer { - this.deref(); - } + errdefer this.deref(); switch (connection) { .host => |c| { @@ -1476,6 +1511,7 @@ fn NewSocket(comptime ssl: bool) type { pub fn resumeFromJS(this: *This, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { JSC.markBinding(@src()); + if (this.socket.isDetached()) return .undefined; log("resume", .{}); // we should not allow pausing/resuming a wrapped socket because a wrapped socket is 2 sockets and this can cause issues @@ -1487,6 +1523,7 @@ fn NewSocket(comptime ssl: bool) type { pub fn pauseFromJS(this: *This, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { JSC.markBinding(@src()); + if (this.socket.isDetached()) return .undefined; log("pause", .{}); // we should not allow pausing/resuming a wrapped socket because a wrapped socket is 2 sockets and this can cause issues @@ -1608,7 +1645,7 @@ fn NewSocket(comptime ssl: bool) type { } fn handleConnectError(this: *This, errno: c_int) void { - log("onConnectError {s} ({d}, {})", .{ if (this.handlers.is_server) "S" else "C", errno, this.ref_count }); + log("onConnectError {s} ({d}, {d})", .{ if (this.handlers.is_server) "S" else "C", errno, this.ref_count.active_counts }); // Ensure the socket is still alive for any defer's we have this.ref(); defer this.deref(); @@ -1626,17 +1663,19 @@ fn NewSocket(comptime ssl: bool) type { return; } + bun.assert(errno >= 0); + var errno_: c_int = if (errno == @intFromEnum(bun.sys.SystemErrno.ENOENT)) @intFromEnum(bun.sys.SystemErrno.ENOENT) else @intFromEnum(bun.sys.SystemErrno.ECONNREFUSED); + const code_ = if (errno == @intFromEnum(bun.sys.SystemErrno.ENOENT)) bun.String.static("ENOENT") else bun.String.static("ECONNREFUSED"); + if (Environment.isWindows and errno_ == @intFromEnum(bun.sys.SystemErrno.ENOENT)) errno_ = @intFromEnum(bun.sys.SystemErrno.UV_ENOENT); + if (Environment.isWindows and errno_ == @intFromEnum(bun.sys.SystemErrno.ECONNREFUSED)) errno_ = @intFromEnum(bun.sys.SystemErrno.UV_ECONNREFUSED); + const callback = handlers.onConnectError; const globalObject = handlers.globalObject; const err = JSC.SystemError{ - .errno = errno, + .errno = -errno_, .message = bun.String.static("Failed to connect"), .syscall = bun.String.static("connect"), - // For some reason errno is 0 which causes this to be success. - // Unix socket emits ENOENT - .code = if (errno == @intFromEnum(bun.sys.SystemErrno.ENOENT)) bun.String.static("ENOENT") else bun.String.static("ECONNREFUSED"), - // .code = bun.String.static(@tagName(bun.sys.getErrno(errno))), - // .code = bun.String.static(@tagName(@as(bun.sys.E, @enumFromInt(errno)))), + .code = code_, }; vm.eventLoop().enter(); defer { @@ -1724,11 +1763,11 @@ fn NewSocket(comptime ssl: bool) type { } pub fn onOpen(this: *This, socket: Socket) void { + log("onOpen {s} {*} {} {}", .{ if (this.handlers.is_server) "S" else "C", this, this.socket.isDetached(), this.ref_count.active_counts }); // Ensure the socket remains alive until this is finished this.ref(); defer this.deref(); - log("onOpen {s} {} {}", .{ if (this.handlers.is_server) "S" else "C", this.socket.isDetached(), this.ref_count }); // update the internal socket instance to the one that was just connected // This socket must be replaced because the previous one is a connecting socket not a uSockets socket this.socket = socket; @@ -1794,9 +1833,7 @@ fn NewSocket(comptime ssl: bool) type { const vm = handlers.vm; vm.eventLoop().enter(); defer vm.eventLoop().exit(); - const result = callback.call(globalObject, this_value, &[_]JSValue{ - this_value, - }) catch |err| globalObject.takeException(err); + const result = callback.call(globalObject, this_value, &[_]JSValue{this_value}) catch |err| globalObject.takeException(err); if (result.toError()) |err| { defer this.markInactive(); @@ -1807,7 +1844,7 @@ fn NewSocket(comptime ssl: bool) type { } if (handlers.rejectPromise(err)) return; - _ = handlers.callErrorHandler(this_value, &[_]JSC.JSValue{ this_value, err }); + _ = handlers.callErrorHandler(this_value, &.{ this_value, err }); } } @@ -2018,8 +2055,6 @@ fn NewSocket(comptime ssl: bool) type { } pub fn getReadyState(this: *This, _: *JSC.JSGlobalObject) JSValue { - log("getReadyState()", .{}); - if (this.socket.isDetached()) { return JSValue.jsNumber(@as(i32, -1)); } else if (this.socket.isClosed()) { @@ -2556,6 +2591,15 @@ fn NewSocket(comptime ssl: bool) type { return JSValue.jsUndefined(); } + pub fn close(this: *This, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + JSC.markBinding(@src()); + _ = callframe; + this.socket.close(.normal); + this.socket.detach(); + this.poll_ref.unref(globalObject.bunVM()); + return .jsUndefined(); + } + pub fn end(this: *This, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { JSC.markBinding(@src()); @@ -2581,14 +2625,18 @@ fn NewSocket(comptime ssl: bool) type { } pub fn jsRef(this: *This, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + log("ref {s}", .{if (this.handlers.is_server) "S" else "C"}); JSC.markBinding(@src()); + if (this.socket.isDetached()) this.ref_pollref_on_connect = true; if (this.socket.isDetached()) return JSValue.jsUndefined(); this.poll_ref.ref(globalObject.bunVM()); return JSValue.jsUndefined(); } pub fn jsUnref(this: *This, globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + log("unref {s}", .{if (this.handlers.is_server) "S" else "C"}); JSC.markBinding(@src()); + if (this.socket.isDetached()) this.ref_pollref_on_connect = false; this.poll_ref.unref(globalObject.bunVM()); return JSValue.jsUndefined(); } @@ -3343,7 +3391,7 @@ fn NewSocket(comptime ssl: bool) type { return .zero; } - const handlers = try Handlers.fromJS(globalObject, socket_obj, false); + const handlers = try Handlers.fromJS(globalObject, socket_obj, this.handlers.is_server); if (globalObject.hasException()) { return .zero; @@ -3602,6 +3650,8 @@ pub fn NewWrappedHandler(comptime tls: bool) type { } } + pub const onFd = null; + pub fn onWritable(this: WrappedSocket, socket: Socket) void { if (comptime tls) { TLSSocket.onWritable(this.tls, socket); diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 84f574fe55..95500fbe9e 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -55,7 +55,7 @@ function generate(ssl) { }, setSession: { fn: "setSession", - length: 0, + length: 1, }, getTLSTicket: { fn: "getTLSTicket", @@ -116,7 +116,6 @@ function generate(ssl) { length: 0, }, - // }, listener: { getter: "getListener", }, @@ -140,6 +139,10 @@ function generate(ssl) { fn: "shutdown", length: 1, }, + close: { + fn: "close", + length: 0, + }, ref: { fn: "jsRef", @@ -160,7 +163,9 @@ function generate(ssl) { }, localPort: { getter: "getLocalPort", + cache: true, }, + // cork: { // fn: "cork", // length: 1, @@ -276,7 +281,6 @@ export default [ getter: "getHostname", cache: true, }, - data: { getter: "getData", setter: "setData", @@ -454,7 +458,7 @@ export default [ call: false, finalize: true, estimatedSize: true, - // customInspect: true, + // inspectCustom: true, structuredClone: { transferable: false, tag: 251 }, JSType: "0b11101110", klass: { diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 19db7ab4e8..c466e6a2d3 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -1664,7 +1664,10 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject auto str2 = arg2.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); auto message = makeString("Invalid address family: "_s, str0, " "_s, str1, ":"_s, str2); - return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_ADDRESS_FAMILY, message)); + auto err = createError(globalObject, ErrorCode::ERR_INVALID_ADDRESS_FAMILY, message); + err->putDirect(vm, builtinNames(vm).hostPublicName(), arg1, 0); + err->putDirect(vm, builtinNames(vm).portPublicName(), arg2, 0); + return JSC::JSValue::encode(err); } case Bun::ErrorCode::ERR_INVALID_ARG_VALUE: { @@ -2216,6 +2219,14 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_AMBIGUOUS_ARGUMENT, message)); } + case Bun::ErrorCode::ERR_INVALID_FD_TYPE: { + auto arg0 = callFrame->argument(1); + auto str0 = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto message = makeString("Unsupported fd type: "_s, str0); + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_INVALID_FD_TYPE, message)); + } + case Bun::ErrorCode::ERR_CHILD_PROCESS_STDIO_MAXBUFFER: { auto arg0 = callFrame->argument(1); auto str0 = arg0.toWTFString(globalObject); @@ -2224,6 +2235,14 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_CHILD_PROCESS_STDIO_MAXBUFFER, message)); } + case Bun::ErrorCode::ERR_IP_BLOCKED: { + auto arg0 = callFrame->argument(1); + auto str0 = arg0.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto message = makeString("IP("_s, str0, ") is blocked by net.BlockList"_s); + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IP_BLOCKED, message)); + } + case Bun::ErrorCode::ERR_VM_MODULE_STATUS: { auto arg0 = callFrame->argument(1); auto str0 = arg0.toWTFString(globalObject); @@ -2363,6 +2382,10 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_SOCKET_ASSIGNED, "Socket already assigned"_s)); case ErrorCode::ERR_STREAM_RELEASE_LOCK: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_STREAM_RELEASE_LOCK, "Stream reader cancelled via releaseLock()"_s)); + case ErrorCode::ERR_SOCKET_CONNECTION_TIMEOUT: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SOCKET_CONNECTION_TIMEOUT, "Socket connection timeout"_s)); + case ErrorCode::ERR_TLS_HANDSHAKE_TIMEOUT: + return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_TLS_HANDSHAKE_TIMEOUT, "TLS handshake timeout"_s)); case ErrorCode::ERR_VM_MODULE_ALREADY_LINKED: return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_VM_MODULE_ALREADY_LINKED, "Module has already been linked"_s)); case ErrorCode::ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA: diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index fef2fa7977..510315ea17 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -130,6 +130,7 @@ const errors: ErrorCodeMapping = [ ["ERR_INVALID_ASYNC_ID", RangeError], ["ERR_INVALID_CHAR", TypeError], ["ERR_INVALID_CURSOR_POS", TypeError], + ["ERR_INVALID_FD_TYPE", TypeError], ["ERR_INVALID_FILE_URL_HOST", TypeError], ["ERR_INVALID_FILE_URL_PATH", TypeError], ["ERR_INVALID_HANDLE_TYPE", TypeError], @@ -146,6 +147,7 @@ const errors: ErrorCodeMapping = [ ["ERR_INVALID_URI", URIError], ["ERR_INVALID_URL_SCHEME", TypeError], ["ERR_INVALID_URL", TypeError], + ["ERR_IP_BLOCKED", Error], ["ERR_IPC_CHANNEL_CLOSED", Error], ["ERR_IPC_DISCONNECTED", Error], ["ERR_IPC_ONE_PIPE", Error], @@ -218,6 +220,7 @@ const errors: ErrorCodeMapping = [ ["ERR_SOCKET_BAD_TYPE", TypeError], ["ERR_SOCKET_CLOSED_BEFORE_CONNECTION", Error], ["ERR_SOCKET_CLOSED", Error], + ["ERR_SOCKET_CONNECTION_TIMEOUT", Error], ["ERR_SOCKET_DGRAM_IS_CONNECTED", Error], ["ERR_SOCKET_DGRAM_NOT_CONNECTED", Error], ["ERR_SOCKET_DGRAM_NOT_RUNNING", Error], @@ -235,6 +238,7 @@ const errors: ErrorCodeMapping = [ ["ERR_STRING_TOO_LONG", Error], ["ERR_TLS_CERT_ALTNAME_FORMAT", SyntaxError], ["ERR_TLS_CERT_ALTNAME_INVALID", Error], + ["ERR_TLS_HANDSHAKE_TIMEOUT", Error], ["ERR_TLS_INVALID_PROTOCOL_METHOD", TypeError], ["ERR_TLS_INVALID_PROTOCOL_VERSION", TypeError], ["ERR_TLS_PROTOCOL_VERSION_CONFLICT", TypeError], diff --git a/src/bun.js/bindings/NodeAsyncHooks.cpp b/src/bun.js/bindings/NodeAsyncHooks.cpp index c36ed476f1..153fa73bb4 100644 --- a/src/bun.js/bindings/NodeAsyncHooks.cpp +++ b/src/bun.js/bindings/NodeAsyncHooks.cpp @@ -31,21 +31,4 @@ JSC_DEFINE_HOST_FUNCTION(jsSetAsyncHooksEnabled, (JSC::JSGlobalObject * globalOb return JSC::JSValue::encode(JSC::jsUndefined()); } -JSC::JSValue createAsyncHooksBinding(Zig::GlobalObject* globalObject) -{ - VM& vm = globalObject->vm(); - auto binding = constructEmptyArray(globalObject, nullptr, 2); - binding->putByIndexInline( - globalObject, - (unsigned)0, - JSC::JSFunction::create(vm, globalObject, 0, "setAsyncHooksEnabled"_s, jsSetAsyncHooksEnabled, ImplementationVisibility::Public), - false); - binding->putByIndexInline( - globalObject, - (unsigned)1, - JSC::JSFunction::create(vm, globalObject, 0, "cleanupLater"_s, jsCleanupLater, ImplementationVisibility::Public), - false); - return binding; -} - } diff --git a/src/bun.js/bindings/NodeAsyncHooks.h b/src/bun.js/bindings/NodeAsyncHooks.h index 466dd70e2b..6dd8f45879 100644 --- a/src/bun.js/bindings/NodeAsyncHooks.h +++ b/src/bun.js/bindings/NodeAsyncHooks.h @@ -1,8 +1,10 @@ #include "config.h" #include "ZigGlobalObject.h" +#include namespace Bun { -JSC::JSValue createAsyncHooksBinding(Zig::GlobalObject*); +JSC_DECLARE_HOST_FUNCTION(jsCleanupLater); +JSC_DECLARE_HOST_FUNCTION(jsSetAsyncHooksEnabled); } diff --git a/src/bun.js/node/node_net_binding.zig b/src/bun.js/node/node_net_binding.zig index 8d960fac34..7978bd137c 100644 --- a/src/bun.js/node/node_net_binding.zig +++ b/src/bun.js/node/node_net_binding.zig @@ -73,3 +73,35 @@ pub fn setDefaultAutoSelectFamilyAttemptTimeout(global: *JSC.JSGlobalObject) JSC pub const SocketAddress = bun.JSC.Codegen.JSSocketAddress.getConstructor; pub const BlockList = JSC.Codegen.JSBlockList.getConstructor; + +pub fn newDetachedSocket(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const args = callframe.argumentsAsArray(1); + const is_ssl = args[0].toBoolean(); + + if (!is_ssl) { + const socket = bun.api.TCPSocket.new(.{ + .socket = .detached, + .socket_context = null, + .ref_count = .init(), + .protos = null, + .handlers = undefined, + }); + return socket.getThisValue(globalThis); + } else { + const socket = bun.api.TLSSocket.new(.{ + .socket = .detached, + .socket_context = null, + .ref_count = .init(), + .protos = null, + .handlers = undefined, + }); + return socket.getThisValue(globalThis); + } +} + +pub fn doConnect(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + const prev, const opts = callframe.argumentsAsArray(2); + const maybe_tcp = prev.as(bun.api.TCPSocket); + const maybe_tls = prev.as(bun.api.TLSSocket); + return bun.api.Listener.connectInner(globalThis, maybe_tcp, maybe_tls, opts); +} diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 8ff595d652..2998f6a78c 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -496,6 +496,10 @@ declare module "module" { `; + dts += ` (id: "bun"): typeof import("bun");\n`; + dts += ` (id: "bun:test"): typeof import("bun:test");\n`; + dts += ` (id: "bun:jsc"): typeof import("bun:jsc");\n`; + for (let i = 0; i < nativeStartIndex; i++) { const id = moduleList[i]; const out = outputs.get(id.slice(0, -3).replaceAll("/", path.sep)); diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index eca4f2a6f0..a4736ee949 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -7,6 +7,8 @@ interface PropertyAttribute { * from the prototype hash table, instead setting it using `putDirect()`. */ privateSymbol?: string; + publicSymbol?: string; + name?: string; } /** @@ -203,7 +205,7 @@ export class ClassDefinition { configurable?: boolean; enumerable?: boolean; structuredClone?: { transferable: boolean; tag: number }; - customInspect?: boolean; + inspectCustom?: boolean; callbacks?: Record; @@ -247,9 +249,18 @@ export function define( call = false, construct = false, structuredClone, + inspectCustom = false, ...rest } = {} as Partial, ): ClassDefinition { + if (inspectCustom) { + proto.inspectCustom = { + fn: "inspectCustom", + length: 2, + publicSymbol: "inspectCustom", + name: "[nodejs.util.inspect.custom]", + }; + } return new ClassDefinition({ ...rest, call, diff --git a/src/codegen/client-js.ts b/src/codegen/client-js.ts index 3f6918b74d..c6bbcc97cc 100644 --- a/src/codegen/client-js.ts +++ b/src/codegen/client-js.ts @@ -9,8 +9,8 @@ let $debug_log_enabled = ((env) => ( // The rationale for checking all these variables is just so you don't have to exactly remember which one you set. (env.BUN_DEBUG_ALL && env.BUN_DEBUG_ALL !== '0') || (env.BUN_DEBUG_JS && env.BUN_DEBUG_JS !== '0') - || (env.BUN_DEBUG_${pathToUpperSnakeCase(publicName)}) - || (env.DEBUG_${pathToUpperSnakeCase(filepath)}) + || (env.BUN_DEBUG_${pathToUpperSnakeCase(publicName)} === '1') + || (env.DEBUG_${pathToUpperSnakeCase(filepath)} === '1') ))(Bun.env); let $debug_pid_prefix = Bun.env.SHOW_PID === '1'; let $debug_log = $debug_log_enabled ? (...args) => { diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index f0403b5f66..4ae300fa0b 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -426,15 +426,6 @@ JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); "onStructuredCloneDeserialize", )}(JSC::JSGlobalObject*, const uint8_t*, const uint8_t*);` + "\n"; } - if (obj.customInspect) { - externs += `extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${protoSymbolName(typeName, "customInspect")}(JSC::JSGlobalObject*, JSC::CallFrame*);\n`; - - specialSymbols += ` - this->putDirect(vm, builtinNames(vm).inspectCustomPublicName(), JSFunction::create(vm, globalObject, 2, String("[nodejs.util.inspect.custom]"_s), ${protoSymbolName( - typeName, - "customInspect", - )}, ImplementationVisibility::Public), PropertyAttribute::Function | 0);`; - } if (obj.finalize) { externs += `extern JSC_CALLCONV void JSC_HOST_CALL_ATTRIBUTES ${classSymbolName(typeName, "finalize")}(void*);` + "\n"; @@ -457,17 +448,34 @@ JSC_DECLARE_CUSTOM_GETTER(js${typeName}Constructor); const privateSymbol = protoFields[name].privateSymbol; const fn = protoFields[name].fn; if (!fn) throw Error(`(field: ${name}) private field needs 'fn' key `); + const observable_name = protoFields[name].name ?? fn; specialSymbols += ` this->putDirect(vm, WebCore::clientData(vm)->builtinNames().${privateSymbol}PrivateName(), JSFunction::create(vm, globalObject, ${ protoFields[name].length || 0 - }, String("${fn}"_s), ${protoSymbolName( + }, String("${observable_name}"_s), ${protoSymbolName( typeName, fn, )}Callback, ImplementationVisibility::Private), PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | 0);`; continue; } + if (protoFields[name].publicSymbol !== undefined) { + const publicSymbol = protoFields[name].publicSymbol; + const fn = protoFields[name].fn; + if (!fn) throw Error(`(field: ${name}) public field needs 'fn' key `); + const observable_name = protoFields[name].name ?? fn; + + specialSymbols += ` + this->putDirect(vm, WebCore::clientData(vm)->builtinNames().${publicSymbol}PublicName(), JSFunction::create(vm, globalObject, ${ + protoFields[name].length || 0 + }, String("${observable_name}"_s), ${protoSymbolName( + typeName, + fn, + )}Callback, ImplementationVisibility::Public), PropertyAttribute::Function | 0);`; + continue; + } + if (!name.startsWith("@@")) { continue; } @@ -1789,7 +1797,6 @@ function generateZig( values = [], hasPendingActivity = false, structuredClone = false, - customInspect = false, getInternalProperties = false, callbacks = {}, } = {} as ClassDefinition, @@ -1814,10 +1821,6 @@ function generateZig( exports.set("onStructuredCloneDeserialize", symbolName(typeName, "onStructuredCloneDeserialize")); } - if (customInspect) { - exports.set("customInspect", symbolName(typeName, "customInspect")); - } - proto = { ...Object.fromEntries(Object.entries(own || {}).map(([name, getterName]) => [name, { getter: getterName }])), ...proto, @@ -2099,19 +2102,6 @@ const JavaScriptCoreBindings = struct { `; } - if (customInspect) { - // TODO: perhaps exposing this on classes directly isn't the best API choice long term - // it would be better to make a different signature that accepts a writer, then a generated-only function that returns a js string - // the writer function can integrate with our native console.log implementation, the generated function can call the writer version and collect the result - exports.set("customInspect", protoSymbolName(typeName, "customInspect")); - output += ` - pub fn ${protoSymbolName(typeName, "customInspect")}(thisValue: *${typeName}, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) callconv(JSC.conv) JSC.JSValue { - if (comptime Environment.enable_logs) JSC.markBinding(@src()); - return @call(.always_inline, JSC.toJSHostValue, .{globalObject, @call(.always_inline, ${typeName}.customInspect, .{thisValue, globalObject, callFrame})}); - } - `; - } - return ( output.trim() + ` diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index 66c9a28883..f9513c3ed7 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -1615,7 +1615,7 @@ pub extern fn ares_set_servers_ports_csv(channel: *Channel, servers: [*c]const u pub extern fn ares_get_servers(channel: *Channel, servers: *?*struct_ares_addr_port_node) c_int; pub extern fn ares_get_servers_ports(channel: *Channel, servers: *?*struct_ares_addr_port_node) c_int; /// https://c-ares.org/docs/ares_inet_ntop.html -pub extern fn ares_inet_ntop(af: c_int, src: ?*const anyopaque, dst: [*c]u8, size: ares_socklen_t) ?[*:0]const u8; +pub extern fn ares_inet_ntop(af: c_int, src: ?*const anyopaque, dst: [*]u8, size: ares_socklen_t) ?[*:0]const u8; /// https://c-ares.org/docs/ares_inet_pton.html /// /// ## Returns diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index b49c1e94b0..f1955a827d 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -2929,7 +2929,7 @@ pub const ReturnCodeI64 = enum(i64) { return null; } - pub inline fn errno(this: ReturnCodeI64) ?@TypeOf(@intFromEnum(bun.sys.E.ACCES)) { + pub inline fn errno(this: ReturnCodeI64) ?u16 { return if (@intFromEnum(this) < 0) @as(u16, @intCast(-@intFromEnum(this))) else diff --git a/src/deps/uws.zig b/src/deps/uws.zig index e7142267ec..5641fde80e 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -1994,25 +1994,25 @@ pub fn NewSocketHandler(comptime is_ssl: bool) type { } }; - if (comptime @hasDecl(Type, "onOpen") and @typeInfo(@TypeOf(Type.onOpen)) != .null) + if (@typeInfo(@TypeOf(Type.onOpen)) != .null) us_socket_context_on_open(ssl_int, ctx, SocketHandler.on_open); - if (comptime @hasDecl(Type, "onClose") and @typeInfo(@TypeOf(Type.onClose)) != .null) + if (@typeInfo(@TypeOf(Type.onClose)) != .null) us_socket_context_on_close(ssl_int, ctx, SocketHandler.on_close); - if (comptime @hasDecl(Type, "onData") and @typeInfo(@TypeOf(Type.onData)) != .null) + if (@typeInfo(@TypeOf(Type.onData)) != .null) us_socket_context_on_data(ssl_int, ctx, SocketHandler.on_data); - if (comptime @hasDecl(Type, "onFd") and @typeInfo(@TypeOf(Type.onFd)) != .null) + if (@typeInfo(@TypeOf(Type.onFd)) != .null) us_socket_context_on_fd(ssl_int, ctx, SocketHandler.on_fd); - if (comptime @hasDecl(Type, "onWritable") and @typeInfo(@TypeOf(Type.onWritable)) != .null) + if (@typeInfo(@TypeOf(Type.onWritable)) != .null) us_socket_context_on_writable(ssl_int, ctx, SocketHandler.on_writable); - if (comptime @hasDecl(Type, "onTimeout") and @typeInfo(@TypeOf(Type.onTimeout)) != .null) + if (@typeInfo(@TypeOf(Type.onTimeout)) != .null) us_socket_context_on_timeout(ssl_int, ctx, SocketHandler.on_timeout); - if (comptime @hasDecl(Type, "onConnectError") and @typeInfo(@TypeOf(Type.onConnectError)) != .null) { + if (@typeInfo(@TypeOf(Type.onConnectError)) != .null) { us_socket_context_on_socket_connect_error(ssl_int, ctx, SocketHandler.on_connect_error); us_socket_context_on_connect_error(ssl_int, ctx, SocketHandler.on_connect_error_connecting_socket); } - if (comptime @hasDecl(Type, "onEnd") and @typeInfo(@TypeOf(Type.onEnd)) != .null) + if (@typeInfo(@TypeOf(Type.onEnd)) != .null) us_socket_context_on_end(ssl_int, ctx, SocketHandler.on_end); - if (comptime @hasDecl(Type, "onHandshake") and @typeInfo(@TypeOf(Type.onHandshake)) != .null) + if (@typeInfo(@TypeOf(Type.onHandshake)) != .null) us_socket_context_on_handshake(ssl_int, ctx, SocketHandler.on_handshake, null); } @@ -2267,6 +2267,11 @@ pub const SocketContext = opaque { return this; } + pub fn unref(this: *SocketContext, comptime ssl: bool) *SocketContext { + us_socket_context_unref(@intFromBool(ssl), this); + return this; + } + pub fn cleanCallbacks(ctx: *SocketContext, is_ssl: bool) void { const ssl_int: i32 = @intFromBool(is_ssl); // replace callbacks with dummy ones @@ -2392,21 +2397,23 @@ pub const PosixLoop = extern struct { } pub fn inc(this: *PosixLoop) void { + log("inc {d} + 1 = {d}", .{ this.num_polls, this.num_polls + 1 }); this.num_polls += 1; } pub fn dec(this: *PosixLoop) void { + log("dec {d} - 1 = {d}", .{ this.num_polls, this.num_polls - 1 }); this.num_polls -= 1; } pub fn ref(this: *PosixLoop) void { - log("ref {d} + 1 = {d}", .{ this.num_polls, this.num_polls + 1 }); + log("ref {d} + 1 = {d} | {d} + 1 = {d}", .{ this.num_polls, this.num_polls + 1, this.active, this.active + 1 }); this.num_polls += 1; this.active += 1; } pub fn unref(this: *PosixLoop) void { - log("unref {d} - 1 = {d}", .{ this.num_polls, this.num_polls - 1 }); + log("unref {d} - 1 = {d} | {d} - 1 = {d}", .{ this.num_polls, this.num_polls - 1, this.active, this.active -| 1 }); this.num_polls -= 1; this.active -|= 1; } @@ -2429,7 +2436,7 @@ pub const PosixLoop = extern struct { pub fn unrefCount(this: *PosixLoop, count: i32) void { log("unref x {d}", .{count}); - this.num_polls -|= count; + this.num_polls -= count; this.active -|= @as(u32, @intCast(count)); } diff --git a/src/dns.zig b/src/dns.zig index 997137de21..1d33bbe43b 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -110,8 +110,10 @@ pub const GetAddrInfo = struct { options.flags = flags.coerce(std.c.AI, globalObject); - if (!options.flags.ALL and !options.flags.ADDRCONFIG and !options.flags.V4MAPPED) - return error.InvalidFlags; + // hints & ~(AI_ADDRCONFIG | AI_ALL | AI_V4MAPPED)) !== 0 + const filter = ~@as(u32, @bitCast(std.c.AI{ .ALL = true, .ADDRCONFIG = true, .V4MAPPED = true })); + const int = @as(u32, @bitCast(options.flags)); + if (int & filter != 0) return error.InvalidFlags; } return options; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index f76fb835ff..987ba63f00 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -6,6 +6,13 @@ // Typedefs for JSC intrinsics. Instead of @, we use $ type TODO = any; +declare module "bun" { + interface Socket { + $write(data: string | BufferSource, byteOffset?: number, byteLength?: number): number; + $end(): void; + } +} + /** $debug is a preprocessor macro that works like a templated console.log, and only runs in debug mode if you pass * BUN_DEBUG_JS= * @@ -137,6 +144,7 @@ declare function $getInternalField number: N, ): Fields[N]; declare function $fulfillPromise(...args: any[]): TODO; +declare function $rejectPromise(...args: any[]): TODO; declare function $loadEsmIntoCjs(...args: any[]): TODO; declare function $getGeneratorInternalField(): TODO; declare function $getAsyncGeneratorInternalField(): TODO; @@ -733,6 +741,8 @@ declare function $ERR_INVALID_ASYNC_ID(name, value): RangeError; declare function $ERR_ASYNC_TYPE(name): TypeError; declare function $ERR_ASYNC_CALLBACK(name): TypeError; declare function $ERR_AMBIGUOUS_ARGUMENT(arg, message): TypeError; +declare function $ERR_INVALID_FD_TYPE(type): TypeError; +declare function $ERR_IP_BLOCKED(ip): Error; declare function $ERR_IPC_DISCONNECTED(): Error; declare function $ERR_SERVER_NOT_RUNNING(): Error; @@ -788,6 +798,9 @@ declare function $ERR_HTTP_BODY_NOT_ALLOWED(): Error; declare function $ERR_HTTP_SOCKET_ASSIGNED(): Error; declare function $ERR_DIR_CLOSED(): Error; declare function $ERR_INVALID_MIME_SYNTAX(production: string, str: string, invalidIndex: number | -1): TypeError; +declare function $ERR_SOCKET_CONNECTION_TIMEOUT(): Error; +declare function $ERR_INVALID_HANDLE_TYPE(): TypeError; +declare function $ERR_TLS_HANDSHAKE_TIMEOUT(): Error; declare function $ERR_VM_MODULE_STATUS(reason: string): Error; declare function $ERR_VM_MODULE_ALREADY_LINKED(): Error; declare function $ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA(): Error; diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index 863893ea03..5827c78288 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -1,3 +1,5 @@ +const { SafeArrayIterator } = require("internal/primordials"); + const ObjectFreeze = Object.freeze; class NotImplementedError extends Error { @@ -80,6 +82,35 @@ class ExceptionWithHostPort extends Error { } } +class NodeAggregateError extends AggregateError { + constructor(errors, message) { + super(new SafeArrayIterator(errors), message); + this.code = errors[0]?.code; + } + + get ["constructor"]() { + return AggregateError; + } +} + +class ErrnoException extends Error { + constructor(err, syscall, original) { + util ??= require("node:util"); + const code = util.getSystemErrorName(err); + const message = original ? `${syscall} ${code} ${original}` : `${syscall} ${code}`; + + super(message); + + this.errno = err; + this.code = code; + this.syscall = syscall; + } + + get ["constructor"]() { + return Error; + } +} + function once(callback, { preserveReturnValue = false } = kEmptyObject) { let called = false; let returnValue; @@ -102,6 +133,8 @@ export default { hideFromStack, warnNotImplementedOnce, ExceptionWithHostPort, + NodeAggregateError, + ErrnoException, once, kHandle: Symbol("kHandle"), diff --git a/src/js/internal/timers.ts b/src/js/internal/timers.ts index 6b480413d5..b8e4cd7323 100644 --- a/src/js/internal/timers.ts +++ b/src/js/internal/timers.ts @@ -23,5 +23,6 @@ function getTimerDuration(msecs, name) { } export default { + kTimeout: Symbol("timeout"), // For hiding Timeouts on other internals. getTimerDuration, }; diff --git a/src/js/node/async_hooks.ts b/src/js/node/async_hooks.ts index 7096931917..19e0e29480 100644 --- a/src/js/node/async_hooks.ts +++ b/src/js/node/async_hooks.ts @@ -22,7 +22,8 @@ // each key is an AsyncLocalStorage object and the value is the associated value. There are a ton of // calls to $assert which will verify this invariant (only during bun-debug) // -const [setAsyncHooksEnabled, cleanupLater] = $cpp("NodeAsyncHooks.cpp", "createAsyncHooksBinding"); +const setAsyncHooksEnabled = $newCppFunction("NodeAsyncHooks.cpp", "jsSetAsyncHooksEnabled", 1); +const cleanupLater = $newCppFunction("NodeAsyncHooks.cpp", "jsCleanupLater", 0); const { validateFunction, validateString, validateObject } = require("internal/validators"); // Only run during debug diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 209edfd5da..12e0edb40a 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -32,7 +32,8 @@ const { validateFunction, } = require("internal/validators"); -const { inspect, types } = require("node:util"); +const types = require("node:util/types"); +let inspect: typeof import("node:util").inspect | undefined; const SymbolFor = Symbol.for; const ArrayPrototypeSlice = Array.prototype.slice; @@ -121,7 +122,8 @@ function emitError(emitter, args) { let stringifiedEr; try { - stringifiedEr = inspect(er); + if (!inspect) inspect = require("internal/util/inspect").inspect; + stringifiedEr = inspect!(er); } catch { stringifiedEr = er; } @@ -285,9 +287,10 @@ EventEmitterPrototype.prependListener = function prependListener(type, fn) { }; function overflowWarning(emitter, type, handlers) { + if (!inspect) inspect = require("internal/util/inspect").inspect; handlers.warned = true; const warn = new Error( - `Possible EventTarget memory leak detected. ${handlers.length} ${String(type)} listeners added to ${inspect(emitter, { depth: -1 })}. MaxListeners is ${emitter._maxListeners}. Use events.setMaxListeners() to increase limit`, + `Possible EventTarget memory leak detected. ${handlers.length} ${String(type)} listeners added to ${inspect!(emitter, { depth: -1 })}. MaxListeners is ${emitter._maxListeners}. Use events.setMaxListeners() to increase limit`, ); warn.name = "MaxListenersExceededWarning"; warn.emitter = emitter; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 9e94325176..5ba440a8c8 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -3153,7 +3153,7 @@ class ClientHttp2Session extends Http2Session { this.#alpnProtocol = "h2c"; } const nativeSocket = socket._handle; - if (nativeSocket) { + if (nativeSocket && nativeSocket.readyState > 0) { this.#parser.setNativeSocket(nativeSocket); } process.nextTick(emitConnectNT, this, socket); @@ -3340,6 +3340,14 @@ class ClientHttp2Session extends Http2Session { } const port = url.port ? parseInt(url.port, 10) : protocol === "http:" ? 80 : 443; + let host = "localhost"; + if (url.hostname) { + host = url.hostname; + if (host[0] === "[") host = host.slice(1, -1); + } else if (url.host) { + host = url.host; + } + function onConnect() { this.#onConnect(arguments); listener?.$call(this, this); @@ -3362,13 +3370,13 @@ class ClientHttp2Session extends Http2Session { protocol, options ? { - host: url.hostname, + host, port: String(port), ALPNProtocols: ["h2"], ...options, } : { - host: url.hostname, + host, port: String(port), ALPNProtocols: ["h2"], }, @@ -3378,6 +3386,7 @@ class ClientHttp2Session extends Http2Session { } this.#encrypted = socket instanceof TLSSocket; const nativeSocket = socket._handle; + this.#parser = new H2FrameParser({ native: nativeSocket, context: this, diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 4851736c28..651358a258 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -20,18 +20,29 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -const { Duplex } = require("node:stream"); +const Duplex = require("internal/streams/duplex"); +const { getDefaultHighWaterMark } = require("internal/streams/state"); const EventEmitter = require("node:events"); +let dns: typeof import("node:dns"); + const [addServerName, upgradeDuplexToTLS, isNamedPipeSocket, getBufferedAmount] = $zig( "socket.zig", "createNodeTLSBinding", ); const normalizedArgsSymbol = Symbol("normalizedArgs"); const { ExceptionWithHostPort } = require("internal/shared"); -import type { SocketHandler, SocketListener } from "bun"; +import type { Socket, SocketHandler, SocketListener } from "bun"; import type { ServerOpts } from "node:net"; -const { getTimerDuration } = require("internal/timers"); -const { validateFunction, validateInt32, validateNumber, validateAbortSignal } = require("internal/validators"); +const { kTimeout, getTimerDuration } = require("internal/timers"); +const { validateFunction, validateNumber, validateAbortSignal, validatePort, validateBoolean, validateInt32 } = require("internal/validators"); // prettier-ignore +const { NodeAggregateError, ErrnoException } = require("internal/shared"); + +const ArrayPrototypeIncludes = Array.prototype.includes; +const ArrayPrototypePush = Array.prototype.push; +const MathMax = Math.max; + +const { UV_ECANCELED, UV_ETIMEDOUT } = process.binding("uv"); +const isWindows = process.platform === "win32"; const getDefaultAutoSelectFamily = $zig("node_net_binding.zig", "getDefaultAutoSelectFamily"); const setDefaultAutoSelectFamily = $zig("node_net_binding.zig", "setDefaultAutoSelectFamily"); @@ -39,6 +50,8 @@ const getDefaultAutoSelectFamilyAttemptTimeout = $zig("node_net_binding.zig", "g const setDefaultAutoSelectFamilyAttemptTimeout = $zig("node_net_binding.zig", "setDefaultAutoSelectFamilyAttemptTimeout"); // prettier-ignore const SocketAddress = $zig("node_net_binding.zig", "SocketAddress"); const BlockList = $zig("node_net_binding.zig", "BlockList"); +const newDetachedSocket = $newZigFunction("node_net_binding.zig", "newDetachedSocket", 1); +const doConnect = $newZigFunction("node_net_binding.zig", "doConnect", 2); // IPv4 Segment const v4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; @@ -74,16 +87,15 @@ function isIP(s): 0 | 4 | 6 { return 0; } -const { connect: bunConnect } = Bun; -var { setTimeout } = globalThis; - const bunTlsSymbol = Symbol.for("::buntls::"); const bunSocketServerConnections = Symbol.for("::bunnetserverconnections::"); const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::"); +const owner_symbol = Symbol("owner_symbol"); const kServerSocket = Symbol("kServerSocket"); const kBytesWritten = Symbol("kBytesWritten"); const bunTLSConnectOptions = Symbol.for("::buntlsconnectoptions::"); +const kReinitializeHandle = Symbol("kReinitializeHandle"); const kRealListen = Symbol("kRealListen"); const kSetNoDelay = Symbol("kSetNoDelay"); @@ -210,10 +222,10 @@ const SocketHandlers: SocketHandler = { // we just reuse the same code but we can push null or enqueue right away SocketEmitEndNT(self); }, - error(socket, error, ignoreHadError) { + error(socket, error) { const self = socket.data; if (!self) return; - if (self._hadError && !ignoreHadError) return; + if (self._hadError) return; self._hadError = true; const callback = self[kwriteCallback]; @@ -300,7 +312,7 @@ const SocketHandlers: SocketHandler = { self.emit("timeout", self); }, binaryType: "buffer", -}; +} as const; const SocketEmitEndNT = (self, _err?) => { if (!self[kended]) { @@ -338,6 +350,7 @@ const ServerHandlers: SocketHandler = { detachSocket(data); SocketEmitEndNT(data, err); data.data = null; + socket[owner_symbol] = null; } } @@ -358,6 +371,23 @@ const ServerHandlers: SocketHandler = { _socket._rejectUnauthorized = rejectUnauthorized; _socket[kAttach](this.localPort, socket); + + if (self.blockList) { + const addressType = isIP(socket.remoteAddress); + if (addressType && self.blockList.check(socket.remoteAddress, `ipv${addressType}`)) { + const data = { + localAddress: _socket.localAddress, + localPort: _socket.localPort || this.localPort, + localFamily: _socket.localFamily, + remoteAddress: _socket.remoteAddress, + remotePort: _socket.remotePort, + remoteFamily: _socket.remoteFamily || "IPv4", + }; + socket.end(); + self.emit("drop", data); + return; + } + } if (self.maxConnections && self[bunSocketServerConnections] >= self.maxConnections) { const data = { localAddress: _socket.localAddress, @@ -391,7 +421,6 @@ const ServerHandlers: SocketHandler = { _socket.resume(); } }, - handshake(socket, success, verifyError) { const { data: self } = socket; if (!success && verifyError?.code === "ECONNRESET") { @@ -469,11 +498,182 @@ const ServerHandlers: SocketHandler = { SocketHandlers.error(socket, error, true); data.server.emit("clientError", error, data); }, - timeout: SocketHandlers.timeout, - drain: SocketHandlers.drain, + timeout(socket) { + SocketHandlers.timeout(socket); + }, + drain(socket) { + SocketHandlers.drain(socket); + }, binaryType: "buffer", +} as const; + +type SocketHandleData = NonNullable["data"]; +// TODO: SocketHandlers2 is a bad name but its temporary. reworking the Server in a followup PR +const SocketHandlers2: SocketHandler = { + open(socket) { + $debug("Bun.Socket open"); + let { self, req } = socket.data; + socket[owner_symbol] = self; + $debug("self[kupgraded]", String(self[kupgraded])); + if (!self[kupgraded]) req!.oncomplete(0, self._handle, req, true, true); + socket.data.req = undefined; + if (self[kupgraded]) { + self.connecting = false; + const options = self[bunTLSConnectOptions]; + if (options) { + const { session } = options; + if (session) { + self.setSession(session); + } + } + SocketHandlers2.drain!(socket); + } + }, + data(socket, buffer) { + $debug("Bun.Socket data"); + const { self } = socket.data; + self.bytesRead += buffer.length; + if (!self.push(buffer)) socket.pause(); + }, + drain(socket) { + $debug("Bun.Socket drain"); + const { self } = socket.data; + const callback = self[kwriteCallback]; + self.connecting = false; + if (callback) { + const writeChunk = self._pendingData; + if (socket.$write(writeChunk || "", self._pendingEncoding || "utf8")) { + self[kBytesWritten] = socket.bytesWritten; + self._pendingData = self[kwriteCallback] = null; + callback(null); + } else { + self[kBytesWritten] = socket.bytesWritten; + self._pendingData = null; + } + } + }, + end(socket) { + $debug("Bun.Socket end"); + const { self } = socket.data; + if (self[kended]) return; + self[kended] = true; + if (!self.allowHalfOpen) self.write = writeAfterFIN; + self.push(null); + self.read(0); + }, + close(socket, err) { + $debug("Bun.Socket close"); + let { self } = socket.data; + if (err) $debug(err); + if (self[kclosed]) return; + self[kclosed] = true; + // TODO: should we be doing something with err? + self[kended] = true; + if (!self.allowHalfOpen) self.write = writeAfterFIN; + self.push(null); + self.read(0); + }, + handshake(socket, success, verifyError) { + $debug("Bun.Socket handshake"); + const { self } = socket.data; + if (!success && verifyError?.code === "ECONNRESET") { + // will be handled in onConnectEnd + return; + } + + self._securePending = false; + self.secureConnecting = false; + self._secureEstablished = !!success; + + self.emit("secure", self); + self.alpnProtocol = socket.alpnProtocol; + const { checkServerIdentity } = self[bunTLSConnectOptions]; + if (!verifyError && typeof checkServerIdentity === "function" && self.servername) { + const cert = self.getPeerCertificate(true); + verifyError = checkServerIdentity(self.servername, cert); + } + if (self._requestCert || self._rejectUnauthorized) { + if (verifyError) { + self.authorized = false; + self.authorizationError = verifyError.code || verifyError.message; + if (self._rejectUnauthorized) { + self.destroy(verifyError); + return; + } + } else { + self.authorized = true; + } + } else { + self.authorized = true; + } + self.emit("secureConnect", verifyError); + self.removeListener("end", onConnectEnd); + }, + error(socket, error) { + $debug("Bun.Socket error"); + if (socket.data === undefined) return; + const { self } = socket.data; + if (self._hadError) return; + self._hadError = true; + + const callback = self[kwriteCallback]; + if (callback) { + self[kwriteCallback] = null; + callback(error); + } + self.emit("error", error); + + if (!self.destroyed) process.nextTick(destroyNT, self, error); + }, + timeout(socket) { + $debug("Bun.Socket timeout"); + const { self } = socket.data; + self.emit("timeout", self); + }, + connectError(socket, error) { + $debug("Bun.Socket connectError"); + let { self, req } = socket.data; + socket[owner_symbol] = self; + req!.oncomplete(error.errno, self._handle, req, true, true); + socket.data.req = undefined; + }, }; +function kConnectTcp(self, addressType, req, address, port) { + $debug("SocketHandle.kConnectTcp", addressType, address, port); + const promise = doConnect(self._handle, { + hostname: address, + port, + ipv6Only: addressType === 6, + allowHalfOpen: self.allowHalfOpen, + tls: req.tls, + data: { self, req }, + socket: self[khandlers], + }); + promise.catch(_reason => { + // eat this so there's no unhandledRejection + // we already catch this in connectError and error + }); + return 0; +} + +function kConnectPipe(self, req, address) { + $debug("SocketHandle.kConnectPipe"); + const promise = doConnect(self._handle, { + hostname: address, + unix: address, + allowHalfOpen: self.allowHalfOpen, + tls: req.tls, + data: { self, req }, + socket: self[khandlers], + }); + promise.catch(_reason => { + // eat this so there's no unhandledRejection + // we already catch this in connectError and error + }); + return 0; +} + function Socket(options?) { if (!(this instanceof Socket)) return new Socket(options); @@ -509,8 +709,8 @@ function Socket(options?) { // Handle strings directly. decodeStrings: false, }); - this._parent = this; - this._parentWrap = this; + this._parent = null; + this._parentWrap = null; this[kpendingRead] = undefined; this[kupgraded] = null; @@ -518,33 +718,35 @@ function Socket(options?) { this[kSetKeepAlive] = Boolean(keepAlive); this[kSetKeepAliveInitialDelay] = ~~(keepAliveInitialDelay / 1000); - this[khandlers] = SocketHandlers; + this[khandlers] = SocketHandlers2; this.bytesRead = 0; this[kBytesWritten] = undefined; this[kclosed] = false; this[kended] = false; this.connecting = false; - this.localAddress = "127.0.0.1"; - this.remotePort = undefined; this[bunTLSConnectOptions] = null; this.timeout = 0; this[kwriteCallback] = undefined; this._pendingData = undefined; this._pendingEncoding = undefined; // for compatibility - this[kpendingRead] = undefined; this._hadError = false; this.isServer = false; this._handle = null; - this._parent = undefined; - this._parentWrap = undefined; this[ksocket] = undefined; this.server = undefined; this.pauseOnConnect = false; - this[kupgraded] = undefined; + this._peername = null; + this._sockname = null; + this._closeAfterHandlingError = false; // Shut down the socket when we're finished with it. this.on("end", onSocketEnd); + if (options?.fd !== undefined) { + const { fd } = options; + validateInt32(fd, "fd", 0); + } + if (socket instanceof Socket) { this[ksocket] = socket; } @@ -557,7 +759,7 @@ function Socket(options?) { } // when the onread option is specified we use a different handlers object this[khandlers] = { - ...SocketHandlers, + ...SocketHandlers2, data({ data: self }, buffer) { if (!self) return; try { @@ -575,6 +777,12 @@ function Socket(options?) { signal.addEventListener("abort", destroyWhenAborted.bind(this)); } } + if (opts.blockList) { + if (!BlockList.isBlockList(opts.blockList)) { + throw $ERR_INVALID_ARG_TYPE("options.blockList", "net.BlockList", opts.blockList); + } + this.blockList = opts.blockList; + } } $toClass(Socket, "Socket", Duplex); @@ -638,8 +846,8 @@ Object.defineProperty(Socket.prototype, "bytesWritten", { }); Socket.prototype[kAttach] = function (port, socket) { - this.remotePort = port; socket.data = this; + socket[owner_symbol] = this; socket.timeout(Math.ceil(this.timeout / 1000)); this._handle = socket; this.connecting = false; @@ -671,267 +879,284 @@ Socket.prototype[kCloseRawConnection] = function () { }; Socket.prototype.connect = function connect(...args) { - const [options, connectListener] = - $isArray(args[0]) && args[0][normalizedArgsSymbol] - ? // args have already been normalized. - // Normalized array is passed as the first and only argument. - ($assert(args[0].length == 2 && typeof args[0][0] === "object"), args[0]) - : normalizeArgs(args); - let connection = this[ksocket]; - let upgradeDuplex = false; - let { - fd, - port, - host, - path, - socket, - localAddress, - localPort, - rejectUnauthorized, - pauseOnConnect, - servername, - checkServerIdentity, - session, - } = options; - if (localAddress && !isIP(localAddress)) { - throw $ERR_INVALID_IP_ADDRESS(localAddress); - } - if (localPort) { - validateNumber(localPort, "options.localPort"); - } - this.servername = servername; - if (socket) { - connection = socket; - } - if (fd) { - bunConnect({ - data: this, - fd: fd, - socket: this[khandlers], - allowHalfOpen: this.allowHalfOpen, - }).catch(error => { - if (!this.destroyed) { - this.emit("error", error); - this.emit("close"); - } - }); - } - this.pauseOnConnect = pauseOnConnect; - if (!pauseOnConnect) { - process.nextTick(() => { - this.resume(); - }); - this.connecting = true; - } - if (fd) { - return this; - } - if ( - // TLSSocket already created a socket and is forwarding it here. This is a private API. - !(socket && $isObject(socket) && socket instanceof Duplex) && - // public api for net.Socket.connect - port === undefined && - path == null - ) { - throw $ERR_MISSING_ARGS(["options", "port", "path"]); - } - this.remotePort = port; - const bunTLS = this[bunTlsSymbol]; - var tls = undefined; - if (typeof bunTLS === "function") { - tls = bunTLS.$call(this, port, host, true); - // Client always request Cert - this._requestCert = true; - if (tls) { - if (typeof rejectUnauthorized !== "undefined") { - this._rejectUnauthorized = rejectUnauthorized; - tls.rejectUnauthorized = rejectUnauthorized; - } else { - this._rejectUnauthorized = tls.rejectUnauthorized; - } - tls.requestCert = true; - tls.session = session || tls.session; - this.servername = tls.servername; - tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity; - this[bunTLSConnectOptions] = tls; - if (!connection && tls.socket) { - connection = tls.socket; - } + $debug("Socket.prototype.connect"); + { + const [options, connectListener] = + $isArray(args[0]) && args[0][normalizedArgsSymbol] ? args[0] : normalizeArgs(args); + let connection = this[ksocket]; + let upgradeDuplex = false; + let { port, host, path, socket, rejectUnauthorized, checkServerIdentity, session, fd, pauseOnConnect } = options; + this.servername = options.servername; + if (socket) { + connection = socket; } - if (connection) { - if ( - typeof connection !== "object" || - !(connection instanceof Socket) || - typeof connection[bunTlsSymbol] === "function" - ) { - if (connection instanceof Duplex) { - upgradeDuplex = true; + if (fd) { + doConnect(this._handle, { + data: this, + fd: fd, + socket: SocketHandlers, + allowHalfOpen: this.allowHalfOpen, + }).catch(error => { + if (!this.destroyed) { + this.emit("error", error); + this.emit("close"); + } + }); + } + this.pauseOnConnect = pauseOnConnect; + if (!pauseOnConnect) { + process.nextTick(() => { + this.resume(); + }); + this.connecting = true; + } + if (fd) { + return this; + } + if ( + // TLSSocket already created a socket and is forwarding it here. This is a private API. + !(socket && $isObject(socket) && socket instanceof Duplex) && + // public api for net.Socket.connect + port === undefined && + path == null + ) { + throw $ERR_MISSING_ARGS(["options", "port", "path"]); + } + const bunTLS = this[bunTlsSymbol]; + var tls: any | undefined = undefined; + if (typeof bunTLS === "function") { + tls = bunTLS.$call(this, port, host, true); + // Client always request Cert + this._requestCert = true; + if (tls) { + if (typeof rejectUnauthorized !== "undefined") { + this._rejectUnauthorized = rejectUnauthorized; + tls.rejectUnauthorized = rejectUnauthorized; } else { - throw new TypeError("socket must be an instance of net.Socket or Duplex"); + this._rejectUnauthorized = tls.rejectUnauthorized; + } + tls.requestCert = true; + tls.session = session || tls.session; + this.servername = tls.servername; + tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity; + this[bunTLSConnectOptions] = tls; + if (!connection && tls.socket) { + connection = tls.socket; } } - } - this.authorized = false; - this.secureConnecting = true; - this._secureEstablished = false; - this._securePending = true; - if (connectListener) this.on("secureConnect", connectListener); - this[kConnectOptions] = options; - this.prependListener("end", onConnectEnd); - } else if (connectListener) this.on("connect", connectListener); - // start using existing connection - try { - // reset the underlying writable object when establishing a new connection - // this is a function on `Duplex`, originally defined on `Writable` - // https://github.com/nodejs/node/blob/c5cfdd48497fe9bd8dbd55fd1fca84b321f48ec1/lib/net.js#L311 - // https://github.com/nodejs/node/blob/c5cfdd48497fe9bd8dbd55fd1fca84b321f48ec1/lib/net.js#L1126 - this._undestroy(); - if (connection) { - const socket = connection._handle; - if (!upgradeDuplex && socket) { - // if is named pipe socket we can upgrade it using the same wrapper than we use for duplex - upgradeDuplex = isNamedPipeSocket(socket); + if (connection) { + if ( + typeof connection !== "object" || + !(connection instanceof Socket) || + typeof connection[bunTlsSymbol] === "function" + ) { + if (connection instanceof Duplex) { + upgradeDuplex = true; + } else { + throw new TypeError("socket must be an instance of net.Socket or Duplex"); + } + } } - if (upgradeDuplex) { - this.connecting = true; - this[kupgraded] = connection; - const [result, events] = upgradeDuplexToTLS(connection, { - data: this, - tls, - socket: this[khandlers], - }); - connection.on("data", events[0]); - connection.on("end", events[1]); - connection.on("drain", events[2]); - connection.on("close", events[3]); - this._handle = result; - } else { - if (socket) { - this.connecting = true; + this.authorized = false; + this.secureConnecting = true; + this._secureEstablished = false; + this._securePending = true; + this[kConnectOptions] = options; + this.prependListener("end", onConnectEnd); + } + // start using existing connection + if (connection) { + if (connectListener != null) this.once("secureConnect", connectListener); + try { + // reset the underlying writable object when establishing a new connection + // this is a function on `Duplex`, originally defined on `Writable` + // https://github.com/nodejs/node/blob/c5cfdd48497fe9bd8dbd55fd1fca84b321f48ec1/lib/net.js#L311 + // https://github.com/nodejs/node/blob/c5cfdd48497fe9bd8dbd55fd1fca84b321f48ec1/lib/net.js#L1126 + this._undestroy(); + const socket = connection._handle; + if (!upgradeDuplex && socket) { + // if is named pipe socket we can upgrade it using the same wrapper than we use for duplex + upgradeDuplex = isNamedPipeSocket(socket); + } + if (upgradeDuplex) { this[kupgraded] = connection; - const result = socket.upgradeTLS({ - data: this, + const [result, events] = upgradeDuplexToTLS(connection, { + data: { self: this, req: { oncomplete: afterConnect } }, tls, socket: this[khandlers], }); - if (result) { - const [raw, tls] = result; - // replace socket - connection._handle = raw; - this.once("end", this[kCloseRawConnection]); - raw.connecting = false; - this._handle = tls; - } else { - this._handle = null; - throw new Error("Invalid socket"); - } + connection.on("data", events[0]); + connection.on("end", events[1]); + connection.on("drain", events[2]); + connection.on("close", events[3]); + this._handle = result; } else { - // wait to be connected - connection.once("connect", () => { - const socket = connection._handle; - if (!upgradeDuplex && socket) { - // if is named pipe socket we can upgrade it using the same wrapper than we use for duplex - upgradeDuplex = isNamedPipeSocket(socket); - } - if (upgradeDuplex) { - this.connecting = true; - this[kupgraded] = connection; - const [result, events] = upgradeDuplexToTLS(connection, { - data: this, - tls, - socket: this[khandlers], - }); - connection.on("data", events[0]); - connection.on("end", events[1]); - connection.on("drain", events[2]); - connection.on("close", events[3]); - this._handle = result; + if (socket) { + this[kupgraded] = connection; + const result = socket.upgradeTLS({ + data: { self: this, req: { oncomplete: afterConnect } }, + tls, + socket: this[khandlers], + }); + if (result) { + const [raw, tls] = result; + // replace socket + connection._handle = raw; + this.once("end", this[kCloseRawConnection]); + raw.connecting = false; + this._handle = tls; } else { - this.connecting = true; - this[kupgraded] = connection; - const result = socket.upgradeTLS({ - data: this, - tls, - socket: this[khandlers], - }); - if (result) { - const [raw, tls] = result; - // replace socket - connection._handle = raw; - this.once("end", this[kCloseRawConnection]); - raw.connecting = false; - this._handle = tls; - } else { - this._handle = null; - throw new Error("Invalid socket"); - } + this._handle = null; + throw new Error("Invalid socket"); } - }); + } else { + // wait to be connected + connection.once("connect", () => { + const socket = connection._handle; + if (!upgradeDuplex && socket) { + // if is named pipe socket we can upgrade it using the same wrapper than we use for duplex + upgradeDuplex = isNamedPipeSocket(socket); + } + if (upgradeDuplex) { + this[kupgraded] = connection; + const [result, events] = upgradeDuplexToTLS(connection, { + data: { self: this, req: { oncomplete: afterConnect } }, + tls, + socket: this[khandlers], + }); + connection.on("data", events[0]); + connection.on("end", events[1]); + connection.on("drain", events[2]); + connection.on("close", events[3]); + this._handle = result; + } else { + this[kupgraded] = connection; + const result = socket.upgradeTLS({ + data: { self: this, req: { oncomplete: afterConnect } }, + tls, + socket: this[khandlers], + }); + if (result) { + const [raw, tls] = result; + // replace socket + connection._handle = raw; + this.once("end", this[kCloseRawConnection]); + raw.connecting = false; + this._handle = tls; + } else { + this._handle = null; + throw new Error("Invalid socket"); + } + } + }); + } } + } catch (error) { + process.nextTick(emitErrorAndCloseNextTick, this, error); } - } else if (path) { - // start using unix socket - bunConnect({ - data: this, - unix: path, - socket: this[khandlers], - tls, - allowHalfOpen: this.allowHalfOpen, - }).catch(error => { - if (!this.destroyed) { - this.emit("error", error); - this.emit("close"); - } - }); - } else { - // default start - bunConnect({ - data: this, - hostname: host || "localhost", - port: port, - socket: this[khandlers], - tls, - allowHalfOpen: this.allowHalfOpen, - }).catch(error => { - if (!this.destroyed) { - this.emit("error", error); - this.emit("close"); - } - }); + return this; } - } catch (error) { - process.nextTick(emitErrorAndCloseNextTick, this, error); + } + + const [options, cb] = $isArray(args[0]) && args[0][normalizedArgsSymbol] ? args[0] : normalizeArgs(args); + + if (typeof this[bunTlsSymbol] === "function" && cb !== null) { + this.once("secureConnect", cb); + } else if (cb !== null) { + this.once("connect", cb); + } + if (this._parent?.connecting) { + return this; + } + if (this.write !== Socket.prototype.write) { + this.write = Socket.prototype.write; + } + if (this.destroyed) { + this._handle = null; + this._peername = null; + this._sockname = null; + } + + this.connecting = true; + + const { path } = options; + const pipe = !!path; + $debug("pipe", pipe, path); + + if (!this._handle) { + this._handle = newDetachedSocket(typeof this[bunTlsSymbol] === "function"); + initSocketHandle(this); + } + + if (!pipe) { + lookupAndConnect(this, options); + } else { + internalConnect(this, options, path); } return this; }; -Socket.prototype.end = function end(...args) { - if (!this._readableState.endEmitted) { - this.secureConnecting = false; - } - return Duplex.prototype.end.$apply(this, args); +Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { + this._handle?.close(); + + this._handle = handle; + this._handle[owner_symbol] = this; + + initSocketHandle(this); +}; + +Socket.prototype.end = function end(data, encoding, callback) { + $debug("Socket.prototype.end"); + return Duplex.prototype.end.$call(this, data, encoding, callback); }; Socket.prototype._destroy = function _destroy(err, callback) { + $debug("Socket.prototype._destroy"); + this.connecting = false; - const { ending } = this._writableState; - // lets make sure that the writable side is closed - if (!ending) { - // at this state destroyed will be true but we need to close the writable side - this._writableState.destroyed = false; - this.end(); - - // we now restore the destroyed flag - this._writableState.destroyed = true; + for (let s = this; s !== null; s = s._parent) { + clearTimeout(s[kTimeout]); } - detachSocket(self); - callback(err); - process.nextTick(emitCloseNT, this, !!err); + $debug("close"); + if (this._handle) { + $debug("close handle"); + const isException = err ? true : false; + // `bytesRead` and `kBytesWritten` should be accessible after `.destroy()` + // this[kBytesRead] = this._handle.bytesRead; + this[kBytesWritten] = this._handle.bytesWritten; + + if (this.resetAndClosing) { + this.resetAndClosing = false; + const err = this._handle.close(); + setImmediate(() => { + $debug("emit close"); + this.emit("close", isException); + }); + if (err) this.emit("error", new ErrnoException(err, "reset")); + } else if (this._closeAfterHandlingError) { + // Enqueue closing the socket as a microtask, so that the socket can be + // accessible when an `error` event is handled in the `next tick queue`. + queueMicrotask(() => closeSocketHandle(this, isException, true)); + } else { + closeSocketHandle(this, isException); + } + + if (!this._closeAfterHandlingError) { + if (this._handle) this._handle.onread = () => {}; + this._handle = null; + this._sockname = null; + } + callback(err); + } else { + callback(err); + process.nextTick(emitCloseNT, this); + } }; Socket.prototype._final = function _final(callback) { + $debug("Socket.prototype._final"); if (this.connecting) { return this.once("connect", () => this._final(callback)); } @@ -944,15 +1169,21 @@ Socket.prototype._final = function _final(callback) { process.nextTick(endNT, socket, callback); }; +Object.defineProperty(Socket.prototype, "localAddress", { + get: function () { + return this._getsockname().address; + }, +}); + Object.defineProperty(Socket.prototype, "localFamily", { get: function () { - return "IPv4"; + return this._getsockname().family; }, }); Object.defineProperty(Socket.prototype, "localPort", { get: function () { - return this._handle?.localPort; + return this._getsockname().port; }, }); @@ -999,10 +1230,41 @@ Socket.prototype._read = function _read(size) { }; Socket.prototype._reset = function _reset() { + $debug("Socket.prototype._reset"); this.resetAndClosing = true; return this.destroy(); }; +Socket.prototype._getpeername = function () { + if (!this._handle || this.connecting) { + return this._peername || {}; + } else if (!this._peername) { + const family = this._handle.remoteFamily; + if (!family) return {}; + this._peername = { + family, + address: this._handle.remoteAddress, + port: this._handle.remotePort, + }; + } + return this._peername; +}; + +Socket.prototype._getsockname = function () { + if (!this._handle || this.connecting) { + return this._sockname || {}; + } else if (!this._sockname) { + const family = this._handle.localFamily; + if (!family) return {}; + this._sockname = { + family, + address: this._handle.localAddress, + port: this._handle.localPort, + }; + } + return this._sockname; +}; + Object.defineProperty(Socket.prototype, "readyState", { get: function () { if (this.connecting) return "opening"; @@ -1023,15 +1285,21 @@ Socket.prototype.ref = function ref() { return this; }; +Object.defineProperty(Socket.prototype, "remotePort", { + get: function () { + return this._getpeername().port; + }, +}); + Object.defineProperty(Socket.prototype, "remoteAddress", { get: function () { - return this._handle?.remoteAddress; + return this._getpeername().address; }, }); Object.defineProperty(Socket.prototype, "remoteFamily", { get: function () { - return "IPv4"; + return this._getpeername().family; }, }); @@ -1083,21 +1351,42 @@ Socket.prototype.setNoDelay = function setNoDelay(enable = true) { return this; }; -Socket.prototype.setTimeout = function setTimeout(timeout, callback) { - timeout = getTimerDuration(timeout, "msecs"); - // internally or timeouts are in seconds - // we use Math.ceil because 0 would disable the timeout and less than 1 second but greater than 1ms would be 1 second (the minimum) - if (callback !== undefined) { - validateFunction(callback, "callback"); - this.once("timeout", callback); - } - this._handle?.timeout(Math.ceil(timeout / 1000)); - this.timeout = timeout; - return this; +Socket.prototype.setTimeout = { + setTimeout(msecs, callback) { + if (this.destroyed) return this; + + this.timeout = msecs; + + msecs = getTimerDuration(msecs, "msecs"); + + clearTimeout(this[kTimeout]); + + if (msecs === 0) { + if (callback !== undefined) { + validateFunction(callback, "callback"); + this.removeListener("timeout", callback); + } + } else { + this[kTimeout] = setTimeout(this._onTimeout.bind(this), msecs).unref(); + + if (callback !== undefined) { + validateFunction(callback, "callback"); + this.once("timeout", callback); + } + } + return this; + }, +}.setTimeout; + +Socket.prototype._onTimeout = function _onTimeout() { + $debug("_onTimeout"); + this.emit("timeout"); }; Socket.prototype._unrefTimer = function _unrefTimer() { - // for compatibility + for (let s = this; s !== null; s = s._parent) { + if (s[kTimeout]) s[kTimeout].refresh(); + } }; Socket.prototype.unref = function unref() { @@ -1147,6 +1436,7 @@ Socket.prototype._writev = function _writev(data, callback) { }; Socket.prototype._write = function _write(chunk, encoding, callback) { + $debug("Socket.prototype._write"); // If we are still connecting, then buffer this for later. // The Writable logic will buffer up any more writes while // waiting for this one to be done. @@ -1159,6 +1449,7 @@ Socket.prototype._write = function _write(chunk, encoding, callback) { } this.once("connect", function connect() { this.off("close", onClose); + this._write(chunk, encoding, callback); }); this.once("close", onClose); return; @@ -1171,6 +1462,7 @@ Socket.prototype._write = function _write(chunk, encoding, callback) { callback($ERR_SOCKET_CLOSED()); return false; } + this._unrefTimer(); const success = socket.$write(chunk, encoding); this[kBytesWritten] = socket.bytesWritten; if (success) { @@ -1182,16 +1474,569 @@ Socket.prototype._write = function _write(chunk, encoding, callback) { } }; -function createConnection(port, host, connectListener) { - if (typeof port === "object") { - // port is option pass Socket options and let connect handle connection options - return new Socket(port).connect(port, host, connectListener); +function createConnection(...args) { + const normalized = normalizeArgs(args); + const options = normalized[0]; + const socket = new Socket(options); + + if (options.timeout) { + socket.setTimeout(options.timeout); } - // port is path or host, let connect handle this - return new Socket().connect(port, host, connectListener); + + return socket.connect(normalized); } -const connect = createConnection; +function lookupAndConnect(self, options) { + const { localAddress, localPort } = options; + const host = options.host || "localhost"; + let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options; + + if (localAddress && !isIP(localAddress)) { + throw $ERR_INVALID_IP_ADDRESS(localAddress); + } + if (localPort) { + validateNumber(localPort, "options.localPort"); + } + if (typeof port !== "undefined") { + if (typeof port !== "number" && typeof port !== "string") { + throw $ERR_INVALID_ARG_TYPE("options.port", ["number", "string"], port); + } + validatePort(port); + } + port |= 0; + + if (autoSelectFamily != null) { + validateBoolean(autoSelectFamily, "options.autoSelectFamily"); + } else { + autoSelectFamily = getDefaultAutoSelectFamily(); + } + + if (autoSelectFamilyAttemptTimeout != null) { + validateInt32(autoSelectFamilyAttemptTimeout, "options.autoSelectFamilyAttemptTimeout", 1); + + if (autoSelectFamilyAttemptTimeout < 10) { + autoSelectFamilyAttemptTimeout = 10; + } + } else { + autoSelectFamilyAttemptTimeout = getDefaultAutoSelectFamilyAttemptTimeout(); + } + + // If host is an IP, skip performing a lookup + const addressType = isIP(host); + if (addressType) { + process.nextTick(() => { + if (self.connecting) { + internalConnect(self, options, host, port, addressType, localAddress, localPort); + } + }); + return; + } + + if (options.lookup != null) validateFunction(options.lookup, "options.lookup"); + + if (dns === undefined) dns = require("node:dns"); + const dnsopts = { + family: socketToDnsFamily(options.family), + hints: options.hints || 0, + }; + if (!isWindows && dnsopts.family !== 4 && dnsopts.family !== 6 && dnsopts.hints === 0) { + dnsopts.hints = dns.ADDRCONFIG; + } + + $debug("connect: find host", host, addressType); + $debug("connect: dns options", dnsopts); + self._host = host; + const lookup = options.lookup || dns.lookup; + + if (dnsopts.family !== 4 && dnsopts.family !== 6 && !localAddress && autoSelectFamily) { + $debug("connect: autodetecting", host, port); + + dnsopts.all = true; + lookupAndConnectMultiple( + self, + lookup, + host, + options, + dnsopts, + port, + localAddress, + localPort, + autoSelectFamilyAttemptTimeout, + ); + return; + } + + lookup(host, dnsopts, function emitLookup(err, ip, addressType) { + self.emit("lookup", err, ip, addressType, host); + if (!self.connecting) return; + if (err) { + process.nextTick(destroyNT, self, err); + } else if (!isIP(ip)) { + err = $ERR_INVALID_IP_ADDRESS(ip); + process.nextTick(destroyNT, self, err); + } else if (addressType !== 4 && addressType !== 6) { + err = $ERR_INVALID_ADDRESS_FAMILY(addressType, options.host, options.port); + process.nextTick(destroyNT, self, err); + } else { + self._unrefTimer(); + internalConnect(self, options, ip, port, addressType, localAddress, localPort); + } + }); +} + +function socketToDnsFamily(family) { + switch (family) { + case "IPv4": return 4; // prettier-ignore + case "IPv6": return 6; // prettier-ignore + } + return family; +} + +function lookupAndConnectMultiple(self, lookup, host, options, dnsopts, port, localAddress, localPort, timeout) { + lookup(host, dnsopts, function emitLookup(err, addresses) { + if (!self.connecting) { + return; + } else if (err) { + self.emit("lookup", err, undefined, undefined, host); + process.nextTick(destroyNT, self, err); + return; + } + + const validAddresses = [[], []]; + const validIps = [[], []]; + let destinations; + for (let i = 0, l = addresses.length; i < l; i++) { + const address = addresses[i]; + const { address: ip, family: addressType } = address; + self.emit("lookup", err, ip, addressType, host); + if (!self.connecting) { + return; + } + if (isIP(ip) && (addressType === 4 || addressType === 6)) { + destinations ||= addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 }; + + const destination = destinations[addressType]; + + // Only try an address once + if (!ArrayPrototypeIncludes.$call(validIps[destination], ip)) { + ArrayPrototypePush.$call(validAddresses[destination], address); + ArrayPrototypePush.$call(validIps[destination], ip); + } + } + } + + // When no AAAA or A records are available, fail on the first one + if (!validAddresses[0].length && !validAddresses[1].length) { + const { address: firstIp, family: firstAddressType } = addresses[0]; + + if (!isIP(firstIp)) { + err = $ERR_INVALID_IP_ADDRESS(firstIp); + process.nextTick(destroyNT, self, err); + } else if (firstAddressType !== 4 && firstAddressType !== 6) { + err = $ERR_INVALID_ADDRESS_FAMILY(firstAddressType, options.host, options.port); + process.nextTick(destroyNT, self, err); + } + + return; + } + + // Sort addresses alternating families + const toAttempt = []; + for (let i = 0, l = MathMax(validAddresses[0].length, validAddresses[1].length); i < l; i++) { + if (i in validAddresses[0]) { + ArrayPrototypePush.$call(toAttempt, validAddresses[0][i]); + } + if (i in validAddresses[1]) { + ArrayPrototypePush.$call(toAttempt, validAddresses[1][i]); + } + } + + if (toAttempt.length === 1) { + $debug("connect/multiple: only one address found, switching back to single connection"); + const { address: ip, family: addressType } = toAttempt[0]; + + self._unrefTimer(); + internalConnect(self, options, ip, port, addressType, localAddress, localPort); + + return; + } + + self.autoSelectFamilyAttemptedAddresses = []; + $debug("connect/multiple: will try the following addresses", toAttempt); + + const context = { + socket: self, + addresses: toAttempt, + current: 0, + port, + localPort, + timeout, + [kTimeout]: null, + errors: [], + options, + }; + + self._unrefTimer(); + internalConnectMultiple(context); + }); +} + +function internalConnect(self, options, path); +function internalConnect(self, options, address, port, addressType, localAddress, localPort, _flags?) { + $assert(self.connecting); + + let err; + + if (localAddress || localPort) { + if (addressType === 4) { + localAddress ||= DEFAULT_IPV4_ADDR; + // TODO: + // err = self._handle.bind(localAddress, localPort); + } else { + // addressType === 6 + localAddress ||= DEFAULT_IPV6_ADDR; + // TODO: + // err = self._handle.bind6(localAddress, localPort, flags); + } + $debug( + "connect: binding to localAddress: %s and localPort: %d (addressType: %d)", + localAddress, + localPort, + addressType, + ); + + err = checkBindError(err, localPort, self._handle); + if (err) { + const ex = new ExceptionWithHostPort(err, "bind", localAddress, localPort); + self.destroy(ex); + return; + } + } + + //TLS + let connection = self[ksocket]; + if (options.socket) { + connection = options.socket; + } + let tls = undefined; + const bunTLS = self[bunTlsSymbol]; + if (typeof bunTLS === "function") { + tls = bunTLS.$call(self, port, self._host, true); + self._requestCert = true; // Client always request Cert + if (tls) { + const { rejectUnauthorized, session, checkServerIdentity } = options; + if (typeof rejectUnauthorized !== "undefined") { + self._rejectUnauthorized = rejectUnauthorized; + tls.rejectUnauthorized = rejectUnauthorized; + } else { + self._rejectUnauthorized = tls.rejectUnauthorized; + } + tls.requestCert = true; + tls.session = session || tls.session; + self.servername = tls.servername; + tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity; + self[bunTLSConnectOptions] = tls; + if (!connection && tls.socket) { + connection = tls.socket; + } + } + self.authorized = false; + self.secureConnecting = true; + self._secureEstablished = false; + self._securePending = true; + self[kConnectOptions] = options; + self.prependListener("end", onConnectEnd); + } + //TLS + + $debug("connect: attempting to connect to %s:%d (addressType: %d)", address, port, addressType); + self.emit("connectionAttempt", address, port, addressType); + + if (addressType === 6 || addressType === 4) { + if (self.blockList?.check(address, `ipv${addressType}`)) { + self.destroy($ERR_IP_BLOCKED(address)); + return; + } + const req: any = {}; + req.oncomplete = afterConnect; + req.address = address; + req.port = port; + req.localAddress = localAddress; + req.localPort = localPort; + req.addressType = addressType; + req.tls = tls; + + err = kConnectTcp(self, addressType, req, address, port); + } else { + const req: any = {}; + req.address = address; + req.oncomplete = afterConnect; + req.tls = tls; + + err = kConnectPipe(self, req, address); + } + + if (err) { + const ex = new ExceptionWithHostPort(err, "connect", address, port); + self.destroy(ex); + } +} + +function internalConnectMultiple(context, canceled?) { + clearTimeout(context[kTimeout]); + const self = context.socket; + + // We were requested to abort. Stop all operations + if (self._aborted) { + return; + } + + // All connections have been tried without success, destroy with error + if (canceled || context.current === context.addresses.length) { + if (context.errors.length === 0) { + self.destroy($ERR_SOCKET_CONNECTION_TIMEOUT()); + return; + } + + self.destroy(new NodeAggregateError(context.errors)); + return; + } + + $assert(self.connecting); + + const current = context.current++; + + if (current > 0) { + self[kReinitializeHandle](newDetachedSocket(typeof self[bunTlsSymbol] === "function")); + } + + const { localPort, port, _flags } = context; + const { address, family: addressType } = context.addresses[current]; + let localAddress; + let err; + + if (localPort) { + if (addressType === 4) { + localAddress = DEFAULT_IPV4_ADDR; + // TODO: + // err = self._handle.bind(localAddress, localPort); + } else { + // addressType === 6 + localAddress = DEFAULT_IPV6_ADDR; + // TODO: + // err = self._handle.bind6(localAddress, localPort, flags); + } + + $debug( + "connect/multiple: binding to localAddress: %s and localPort: %d (addressType: %d)", + localAddress, + localPort, + addressType, + ); + + err = checkBindError(err, localPort, self._handle); + if (err) { + ArrayPrototypePush.$call(context.errors, new ExceptionWithHostPort(err, "bind", localAddress, localPort)); + internalConnectMultiple(context); + return; + } + } + + if (self.blockList?.check(address, `ipv${addressType}`)) { + const ex = $ERR_IP_BLOCKED(address); + ArrayPrototypePush.$call(context.errors, ex); + self.emit("connectionAttemptFailed", address, port, addressType, ex); + internalConnectMultiple(context); + return; + } + + //TLS + let connection = self[ksocket]; + if (context.options.socket) { + connection = context.options.socket; + } + let tls = undefined; + const bunTLS = self[bunTlsSymbol]; + if (typeof bunTLS === "function") { + tls = bunTLS.$call(self, port, self._host, true); + self._requestCert = true; // Client always request Cert + if (tls) { + const { rejectUnauthorized, session, checkServerIdentity } = context.options; + if (typeof rejectUnauthorized !== "undefined") { + self._rejectUnauthorized = rejectUnauthorized; + tls.rejectUnauthorized = rejectUnauthorized; + } else { + self._rejectUnauthorized = tls.rejectUnauthorized; + } + tls.requestCert = true; + tls.session = session || tls.session; + self.servername = tls.servername; + tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity; + self[bunTLSConnectOptions] = tls; + if (!connection && tls.socket) { + connection = tls.socket; + } + } + self.authorized = false; + self.secureConnecting = true; + self._secureEstablished = false; + self._securePending = true; + self[kConnectOptions] = context.options; + self.prependListener("end", onConnectEnd); + } + //TLS + + $debug("connect/multiple: attempting to connect to %s:%d (addressType: %d)", address, port, addressType); + self.emit("connectionAttempt", address, port, addressType); + + // const req = new TCPConnectWrap(); + const req = {}; + req.oncomplete = afterConnectMultiple.bind(undefined, context, current); + req.address = address; + req.port = port; + req.localAddress = localAddress; + req.localPort = localPort; + req.addressType = addressType; + req.tls = tls; + + ArrayPrototypePush.$call(self.autoSelectFamilyAttemptedAddresses, `${address}:${port}`); + + err = kConnectTcp(self, addressType, req, address, port); + + if (err) { + const ex = new ExceptionWithHostPort(err, "connect", address, port); + ArrayPrototypePush.$call(context.errors, ex); + + self.emit("connectionAttemptFailed", address, port, addressType, ex); + internalConnectMultiple(context); + return; + } + + if (current < context.addresses.length - 1) { + $debug("connect/multiple: setting the attempt timeout to %d ms", context.timeout); + + // If the attempt has not returned an error, start the connection timer + context[kTimeout] = setTimeout(internalConnectMultipleTimeout, context.timeout, context, req, self._handle).unref(); + } +} + +function internalConnectMultipleTimeout(context, req, handle) { + $debug("connect/multiple: connection to %s:%s timed out", req.address, req.port); + context.socket.emit("connectionAttemptTimeout", req.address, req.port, req.addressType); + + req.oncomplete = undefined; + ArrayPrototypePush.$call(context.errors, createConnectionError(req, UV_ETIMEDOUT)); + handle.close(); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + internalConnectMultiple(context); + } +} + +function afterConnect(status, handle, req, readable, writable) { + const self = handle[owner_symbol]; + + // Callback may come after call to destroy + if (self.destroyed) { + return; + } + + $debug("afterConnect", status, readable, writable); + + $assert(self.connecting); + self.connecting = false; + self._sockname = null; + + if (status === 0) { + if (self.readable && !readable) { + self.push(null); + self.read(); + } + if (self.writable && !writable) { + self.end(); + } + self._unrefTimer(); + + if (self[kSetNoDelay] && self._handle.setNoDelay) { + self._handle.setNoDelay(true); + } + + if (self[kSetKeepAlive] && self._handle.setKeepAlive) { + self._handle.setKeepAlive(true, self[kSetKeepAliveInitialDelay]); + } + + self.emit("connect"); + self.emit("ready"); + + // Start the first read, or get an immediate EOF. + // this doesn't actually consume any bytes, because len=0. + if (readable && !self.isPaused()) self.read(0); + } else { + let details; + if (req.localAddress && req.localPort) { + details = req.localAddress + ":" + req.localPort; + } + const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port); + if (details) { + ex.localAddress = req.localAddress; + ex.localPort = req.localPort; + } + + self.emit("connectionAttemptFailed", req.address, req.port, req.addressType, ex); + self.destroy(ex); + } +} + +function afterConnectMultiple(context, current, status, handle, req, readable, writable) { + $debug("connect/multiple: connection attempt to %s:%s completed with status %s", req.address, req.port, status); + + // Make sure another connection is not spawned + $debug("clearTimeout", context[kTimeout]); + clearTimeout(context[kTimeout]); + + // One of the connection has completed and correctly dispatched but after timeout, ignore this one + if (status === 0 && current !== context.current - 1) { + $debug("connect/multiple: ignoring successful but timedout connection to %s:%s", req.address, req.port); + handle.close(); + return; + } + + const self = context.socket; + + // Some error occurred, add to the list of exceptions + if (status !== 0) { + const ex = createConnectionError(req, status); + ArrayPrototypePush.$call(context.errors, ex); + + self.emit("connectionAttemptFailed", req.address, req.port, req.addressType, ex); + + // Try the next address, unless we were aborted + if (context.socket.connecting) { + internalConnectMultiple(context, status === UV_ECANCELED); + } + + return; + } + + afterConnect(status, self._handle, req, readable, writable); +} + +function createConnectionError(req, status) { + let details; + + if (req.localAddress && req.localPort) { + details = req.localAddress + ":" + req.localPort; + } + + const ex = new ExceptionWithHostPort(status, "connect", req.address, req.port); + if (details) { + ex.localAddress = req.localAddress; + ex.localPort = req.localPort; + } + + return ex; +} type MaybeListener = SocketListener | null; @@ -1215,15 +2060,13 @@ function Server(options?, connectionListener?) { throw $ERR_INVALID_ARG_TYPE("options", ["Object", "Function"], options); } - $assert(typeof Duplex.getDefaultHighWaterMark === "function"); - // https://nodejs.org/api/net.html#netcreateserveroptions-connectionlistener const { maxConnections, // allowHalfOpen = false, keepAlive = false, keepAliveInitialDelay = 0, - highWaterMark = Duplex.getDefaultHighWaterMark(), + highWaterMark = getDefaultHighWaterMark(), pauseOnConnect = false, noDelay = false, } = options; @@ -1245,10 +2088,16 @@ function Server(options?, connectionListener?) { this.pauseOnConnect = Boolean(pauseOnConnect); this.noDelay = noDelay; this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0; - // TODO: options.blockList options.connectionListener = connectionListener; this[bunSocketServerOptions] = options; + + if (options.blockList) { + if (!BlockList.isBlockList(options.blockList)) { + throw $ERR_INVALID_ARG_TYPE("options.blockList", "net.BlockList", options.blockList); + } + this.blockList = options.blockList; + } } $toClass(Server, "Server", EventEmitter); @@ -1684,7 +2533,7 @@ function createServer(options, connectionListener) { } function normalizeArgs(args: unknown[]): [options: Record, cb: Function | null] { - while (args.length && args[args.length - 1] == null) args.pop(); + // while (args.length && args[args.length - 1] == null) args.pop(); let arr; if (args.length === 0) { @@ -1714,6 +2563,35 @@ function normalizeArgs(args: unknown[]): [options: Record, cb: return arr; } +// Called when creating new Socket, or when re-using a closed Socket +function initSocketHandle(self) { + self._undestroy(); + self._sockname = null; + self[kclosed] = false; + self[kended] = false; + + // Handle creation may be deferred to bind() or connect() time. + if (self._handle) { + self._handle[owner_symbol] = self; + } +} + +function closeSocketHandle(self, isException, isCleanupPending = false) { + $debug("closeSocketHandle", isException, isCleanupPending, !!self._handle); + if (self._handle) { + self._handle.close(); + setImmediate(() => { + $debug("emit close", isCleanupPending); + self.emit("close", isException); + if (isCleanupPending) { + self._handle.onread = () => {}; + self._handle = null; + self._sockname = null; + } + }); + } +} + function checkBindError(err, port, handle) { // EADDRINUSE may not be reported until we call listen() or connect(). // To complicate matters, a failed bind() followed by listen() or connect() @@ -1739,16 +2617,29 @@ function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; } +let warnSimultaneousAccepts = true; +function _setSimultaneousAccepts() { + if (warnSimultaneousAccepts) { + process.emitWarning( + "net._setSimultaneousAccepts() is deprecated and will be removed.", + "DeprecationWarning", + "DEP0121", + ); + warnSimultaneousAccepts = false; + } +} + export default { createServer, Server, createConnection, - connect, + connect: createConnection, isIP, isIPv4, isIPv6, Socket, _normalizeArgs: normalizeArgs, + _setSimultaneousAccepts, getDefaultAutoSelectFamily, setDefaultAutoSelectFamily, diff --git a/src/js/node/tty.ts b/src/js/node/tty.ts index da76dfdef1..ae08336991 100644 --- a/src/js/node/tty.ts +++ b/src/js/node/tty.ts @@ -1,5 +1,8 @@ +// Hardcoded module "node:tty" + // Note: please keep this module's loading constrants light, as some users // import it just to call `isatty`. In that case, `node:stream` is not needed. + const { setRawMode: ttySetMode, isatty, diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 13b30627ee..5efb3388c9 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -226,4 +226,8 @@ declare function $bindgenFn any>(filename: string, symbol: declare module "node:net" { export function _normalizeArgs(args: any[]): unknown[]; + + interface Socket { + _handle: Bun.Socket<{ self: Socket; req?: object }> | null; + } } diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index e136f31129..01c3862d92 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -699,7 +699,7 @@ it("upgradeTLS handles errors", async () => { socket.end(); } Bun.gc(true); -}); +}, 20_000); // only needed in debug mode it("should be able to upgrade to TLS", async () => { using server = Bun.serve({ port: 0, diff --git a/test/js/bun/util/inspect-error-leak.test.js b/test/js/bun/util/inspect-error-leak.test.js index 11a63b26de..6634edf6e0 100644 --- a/test/js/bun/util/inspect-error-leak.test.js +++ b/test/js/bun/util/inspect-error-leak.test.js @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; +import { isASAN } from "../../../harness"; const perBatch = 2000; const repeat = 50; @@ -19,5 +20,5 @@ test("Printing errors does not leak", () => { const after = Math.floor(process.memoryUsage.rss() / 1024); const diff = ((after - baseline) / 1024) | 0; console.log(`RSS increased by ${diff} MB`); - expect(diff, `RSS grew by ${diff} MB after ${perBatch * repeat} iterations`).toBeLessThan(10); + expect(diff, `RSS grew by ${diff} MB after ${perBatch * repeat} iterations`).toBeLessThan(isASAN ? 16 : 10); }, 10_000); diff --git a/test/js/node/net/node-net.test.ts b/test/js/node/net/node-net.test.ts index ebf70d065b..5a09a5672f 100644 --- a/test/js/node/net/node-net.test.ts +++ b/test/js/node/net/node-net.test.ts @@ -416,6 +416,7 @@ describe("net.Socket write", () => { for (let i = 0; i < 10; i++) { await run(); } + server.close(); }); }); @@ -423,7 +424,7 @@ it("should handle connection error", done => { let errored = false; // @ts-ignore - const socket = connect(55555, () => { + const socket = connect(55555, "127.0.0.1", () => { done(new Error("Should not have connected")); }); @@ -433,8 +434,11 @@ it("should handle connection error", done => { } errored = true; expect(error).toBeDefined(); - expect(error.message).toBe("Failed to connect"); + expect(error.message).toBe("connect ECONNREFUSED 127.0.0.1:55555"); expect((error as any).code).toBe("ECONNREFUSED"); + expect((error as any).syscall).toBe("connect"); + expect((error as any).address).toBe("127.0.0.1"); + expect((error as any).port).toBe(55555); }); socket.on("connect", () => { @@ -461,8 +465,10 @@ it("should handle connection error (unix)", done => { } errored = true; expect(error).toBeDefined(); - expect(error.message).toBe("Failed to connect"); + expect(error.message).toBe("connect ENOENT loser"); expect((error as any).code).toBe("ENOENT"); + expect((error as any).syscall).toBe("connect"); + expect((error as any).address).toBe("loser"); }); socket.on("connect", () => { @@ -523,7 +529,7 @@ it("should not hang after FIN", async () => { const timeout = setTimeout(() => { process.kill(); reject(new Error("Timeout")); - }, 1000); + }, 2000); expect(await process.exited).toBe(0); clearTimeout(timeout); } finally { @@ -555,7 +561,7 @@ it("should not hang after destroy", async () => { const timeout = setTimeout(() => { process.kill(); reject(new Error("Timeout")); - }, 1000); + }, 2000); expect(await process.exited).toBe(0); clearTimeout(timeout); } finally { diff --git a/test/js/node/net/node-unref-fixture.js b/test/js/node/net/node-unref-fixture.js index 283048bd41..92f9227f5f 100644 --- a/test/js/node/net/node-unref-fixture.js +++ b/test/js/node/net/node-unref-fixture.js @@ -15,3 +15,7 @@ const socket = connect( }, ); socket.unref(); +socket.ref(); +socket.ref(); +socket.ref(); +socket.unref(); diff --git a/test/js/node/test/parallel/test-cluster-worker-handle-close.js b/test/js/node/test/parallel/test-cluster-worker-handle-close.js deleted file mode 100644 index 47a80ef1cd..0000000000 --- a/test/js/node/test/parallel/test-cluster-worker-handle-close.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; -const common = require('../common'); -const cluster = require('cluster'); -const net = require('net'); - -if (cluster.isPrimary) { - cluster.schedulingPolicy = cluster.SCHED_RR; - cluster.fork(); -} else { - const server = net.createServer(common.mustNotCall()); - server.listen(0, common.mustCall(() => { - net.connect(server.address().port); - })); - process.prependListener('internalMessage', common.mustCallAtLeast((message, handle) => { - if (message.act !== 'newconn') { - return; - } - // Make the worker drops the connection, see `rr` and `onconnection` in child.js - server.close(); - const close = handle.close; - handle.close = common.mustCall(() => { - close.call(handle, common.mustCall(() => { - process.exit(); - })); - }); - })); -} diff --git a/test/js/node/test/parallel/test-dns-channel-timeout.js b/test/js/node/test/parallel/test-dns-channel-timeout.js index c7a28451c9..15fe4a4f3f 100644 --- a/test/js/node/test/parallel/test-dns-channel-timeout.js +++ b/test/js/node/test/parallel/test-dns-channel-timeout.js @@ -1,17 +1,10 @@ 'use strict'; const common = require('../common'); +if (common.isWindows) return; // TODO: BUN flaky const assert = require('assert'); const dgram = require('dgram'); const dns = require('dns'); -if (typeof Bun !== 'undefined') { - if (process.platform === 'win32' && require('../../../../harness').isCI) { - // TODO(@heimskr): This test mysteriously takes forever in Windows in CI - // possibly due to UDP keeping the event loop alive longer than it should. - common.skip('Windows CI is flaky'); - } -} - for (const ctor of [dns.Resolver, dns.promises.Resolver]) { for (const timeout of [null, true, false, '', '2']) { assert.throws(() => new ctor({ timeout }), { diff --git a/test/js/node/test/parallel/test-http2-forget-closed-streams.js b/test/js/node/test/parallel/test-http2-forget-closed-streams.js index c0b3bcd819..6c8a2b9c62 100644 --- a/test/js/node/test/parallel/test-http2-forget-closed-streams.js +++ b/test/js/node/test/parallel/test-http2-forget-closed-streams.js @@ -37,6 +37,7 @@ server.listen(0, function() { const client = http2.connect(`http://localhost:${server.address().port}`); function next(i) { + if (process.versions.bun.includes("-debug")) console.log(`next(${i})`); if (i === 10000) { client.close(); return server.close(); diff --git a/test/js/node/test/parallel/test-http2-priority-cycle-.js b/test/js/node/test/parallel/test-http2-priority-cycle-.js new file mode 100644 index 0000000000..af0d66d834 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-priority-cycle-.js @@ -0,0 +1,69 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); +const largeBuffer = Buffer.alloc(1e4); + +// Verify that a dependency cycle may exist, but that it doesn't crash anything + +server.on('stream', common.mustCall((stream) => { + stream.respond(); + setImmediate(() => { + stream.end(largeBuffer); + }); +}, 3)); +server.on('session', common.mustCall((session) => { + session.on('priority', (id, parent, weight, exclusive) => { + assert.strictEqual(weight, 16); + assert.strictEqual(exclusive, false); + switch (id) { + case 1: + assert.strictEqual(parent, 5); + break; + case 3: + assert.strictEqual(parent, 1); + break; + case 5: + assert.strictEqual(parent, 3); + break; + default: + assert.fail('should not happen'); + } + }); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const countdown = new Countdown(3, () => { + client.close(); + server.close(); + }); + + { + const req = client.request(); + req.priority({ parent: 5 }); + req.resume(); + req.on('close', () => countdown.dec()); + } + + { + const req = client.request(); + req.priority({ parent: 1 }); + req.resume(); + req.on('close', () => countdown.dec()); + } + + { + const req = client.request(); + req.priority({ parent: 3 }); + req.resume(); + req.on('close', () => countdown.dec()); + } +})); diff --git a/test/js/node/test/parallel/test-http2-server-close-callback.js b/test/js/node/test/parallel/test-http2-server-close-callback.js new file mode 100644 index 0000000000..e4cd24ce20 --- /dev/null +++ b/test/js/node/test/parallel/test-http2-server-close-callback.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const Countdown = require('../common/countdown'); +const http2 = require('http2'); + +const server = http2.createServer(); + +let session; + +const countdown = new Countdown(2, () => { + server.close(common.mustSucceed()); + session.close(); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + client.on('connect', common.mustCall(() => countdown.dec())); +})); + +server.on('session', common.mustCall((s) => { + session = s; + countdown.dec(); +})); diff --git a/test/js/node/test/parallel/test-http2-timeouts.js b/test/js/node/test/parallel/test-http2-timeouts.js new file mode 100644 index 0000000000..bf84289eef --- /dev/null +++ b/test/js/node/test/parallel/test-http2-timeouts.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const server = h2.createServer(); + +// We use the lower-level API here +server.on('stream', common.mustCall((stream) => { + stream.setTimeout(1, common.mustCall(() => { + stream.respond({ ':status': 200 }); + stream.end('hello world'); + })); + + // Check that expected errors are thrown with wrong args + assert.throws( + () => stream.setTimeout('100'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: + 'The "msecs" argument must be of type number. Received type string' + + " ('100')" + } + ); + assert.throws( + () => stream.setTimeout(0, Symbol('test')), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); + assert.throws( + () => stream.setTimeout(100, {}), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } + ); +})); +server.listen(0); + +server.on('listening', common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + client.setTimeout(1, common.mustCall(() => { + const req = client.request({ ':path': '/' }); + req.setTimeout(1, common.mustCall(() => { + req.on('response', common.mustCall()); + req.resume(); + req.on('end', common.mustCall(() => { + server.close(); + client.close(); + })); + req.end(); + })); + })); +})); diff --git a/test/js/node/test/parallel/test-http2-trailers-after-session-close.js b/test/js/node/test/parallel/test-http2-trailers-after-session-close.js index 20589115b1..7c40649fb0 100644 --- a/test/js/node/test/parallel/test-http2-trailers-after-session-close.js +++ b/test/js/node/test/parallel/test-http2-trailers-after-session-close.js @@ -2,7 +2,7 @@ // Fixes: https://github.com/nodejs/node/issues/42713 const common = require('../common'); -if (common.isWindows) return; // TODO BUN +if (common.isWindows) return; // TODO: BUN if (!common.hasCrypto) { common.skip('missing crypto'); } diff --git a/test/js/node/test/parallel/test-net-after-close.js b/test/js/node/test/parallel/test-net-after-close.js new file mode 100644 index 0000000000..38ea3b96aa --- /dev/null +++ b/test/js/node/test/parallel/test-net-after-close.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(common.mustCall((s) => { + console.error('SERVER: got connection'); + s.end(); +})); + +server.listen(0, common.mustCall(() => { + console.log('SEVER: got listen'); + const c = net.createConnection(server.address().port); + c.on('close', common.mustCall(() => { + console.log('CONN: got close'); + /* eslint-disable no-unused-expressions */ + console.error('connection closed'); + assert.strictEqual(c._handle, null); + // Calling functions / accessing properties of a closed socket should not throw. + c.setNoDelay(); + c.setKeepAlive(); + c.bufferSize; + c.pause(); + c.resume(); + c.address(); + c.remoteAddress; + c.remotePort; + server.close(); + /* eslint-enable no-unused-expressions */ + })); +})); diff --git a/test/js/node/test/parallel/test-net-allow-half-open.js b/test/js/node/test/parallel/test-net-allow-half-open.js new file mode 100644 index 0000000000..c7f829a986 --- /dev/null +++ b/test/js/node/test/parallel/test-net-allow-half-open.js @@ -0,0 +1,47 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +{ + const server = net.createServer(common.mustCall((socket) => { + socket.end(Buffer.alloc(1024)); + })).listen(0, common.mustCall(() => { + const socket = net.connect(server.address().port); + assert.strictEqual(socket.allowHalfOpen, false); + socket.resume(); + socket.on('end', common.mustCall(() => { + process.nextTick(() => { + // Ensure socket is not destroyed straight away + // without proper shutdown. + assert(!socket.destroyed); + server.close(); + }); + })); + socket.on('finish', common.mustCall(() => { + assert(!socket.destroyed); + })); + socket.on('close', common.mustCall()); + })); +} + +{ + const server = net.createServer(common.mustCall((socket) => { + socket.end(Buffer.alloc(1024)); + })).listen(0, common.mustCall(() => { + const socket = net.connect(server.address().port); + assert.strictEqual(socket.allowHalfOpen, false); + socket.resume(); + socket.on('end', common.mustCall(() => { + assert(!socket.destroyed); + })); + socket.end('asd'); + socket.on('finish', common.mustCall(() => { + assert(!socket.destroyed); + })); + socket.on('close', common.mustCall(() => { + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-net-autoselectfamily-ipv4first.js b/test/js/node/test/parallel/test-net-autoselectfamily-ipv4first.js new file mode 100644 index 0000000000..f94af0d9ee --- /dev/null +++ b/test/js/node/test/parallel/test-net-autoselectfamily-ipv4first.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const { createMockedLookup } = require('../common/dns'); + +const assert = require('assert'); +const { createConnection, createServer } = require('net'); + +// Test that happy eyeballs algorithm is properly implemented when a A record is returned first. +if (common.hasIPv6) { + const ipv4Server = createServer((socket) => { + socket.on('data', common.mustCall(() => { + socket.write('response-ipv4'); + socket.end(); + })); + }); + + const ipv6Server = createServer((socket) => { + socket.on('data', common.mustNotCall(() => { + socket.write('response-ipv6'); + socket.end(); + })); + }); + + ipv4Server.listen(0, '127.0.0.1', common.mustCall(() => { + const port = ipv4Server.address().port; + + ipv6Server.listen(port, '::1', common.mustCall(() => { + const connection = createConnection({ + host: 'example.org', + port, + lookup: createMockedLookup('127.0.0.1', '::1'), + autoSelectFamily: true, + }); + + let response = ''; + connection.setEncoding('utf-8'); + + connection.on('data', (chunk) => { + response += chunk; + }); + + connection.on('end', common.mustCall(() => { + assert.strictEqual(response, 'response-ipv4'); + ipv4Server.close(); + ipv6Server.close(); + })); + + connection.write('request'); + })); + })); +} diff --git a/test/js/node/test/parallel/test-net-better-error-messages-port-hostname.js b/test/js/node/test/parallel/test-net-better-error-messages-port-hostname.js new file mode 100644 index 0000000000..c9582f9ae7 --- /dev/null +++ b/test/js/node/test/parallel/test-net-better-error-messages-port-hostname.js @@ -0,0 +1,34 @@ +'use strict'; + +// This tests that the error thrown from net.createConnection +// comes with host and port properties. +// See https://github.com/nodejs/node-v0.x-archive/issues/7005 + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const { addresses } = require('../common/internet'); +const { errorLookupMock, mockedErrorCode } = require('../common/dns'); + +// Using port 0 as hostname used is already invalid. +const c = net.createConnection({ + port: 0, + host: addresses.INVALID_HOST, + lookup: common.mustCall(errorLookupMock()) +}); + +c.on('connect', common.mustNotCall()); + +c.on('error', common.mustCall((error) => { + assert.ok(!('port' in error)); + assert.ok(!('host' in error)); + assert.throws(() => { throw error; }, { + errno: mockedErrorCode, + code: mockedErrorCode, + name: 'Error', + message: 'getaddrinfo ENOTFOUND something.invalid', + hostname: addresses.INVALID_HOST, + syscall: 'getaddrinfo' + }); +})); diff --git a/test/js/node/test/parallel/test-net-blocklist.js b/test/js/node/test/parallel/test-net-blocklist.js new file mode 100644 index 0000000000..901b9a4dfb --- /dev/null +++ b/test/js/node/test/parallel/test-net-blocklist.js @@ -0,0 +1,68 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const blockList = new net.BlockList(); +blockList.addAddress('127.0.0.1'); +blockList.addAddress('127.0.0.2'); + +function check(err) { + assert.ok(err.code === 'ERR_IP_BLOCKED', err); +} + +// Connect without calling dns.lookup +{ + const socket = net.connect({ + port: 9999, + host: '127.0.0.1', + blockList, + }); + socket.on('error', common.mustCall(check)); +} + +// Connect with single IP returned by dns.lookup +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, '127.0.0.1', 4); + }, + autoSelectFamily: false, + }); + + socket.on('error', common.mustCall(check)); +} + +// Connect with autoSelectFamily and single IP +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, [{ address: '127.0.0.1', family: 4 }]); + }, + autoSelectFamily: true, + }); + + socket.on('error', common.mustCall(check)); +} + +// Connect with autoSelectFamily and multiple IPs +{ + const socket = net.connect({ + port: 9999, + host: 'localhost', + blockList, + lookup: function(_, __, cb) { + cb(null, [{ address: '127.0.0.1', family: 4 }, { address: '127.0.0.2', family: 4 }]); + }, + autoSelectFamily: true, + }); + + socket.on('error', common.mustCall(check)); +} diff --git a/test/js/node/test/parallel/test-net-bytes-written-large.js b/test/js/node/test/parallel/test-net-bytes-written-large.js new file mode 100644 index 0000000000..79a997ec5a --- /dev/null +++ b/test/js/node/test/parallel/test-net-bytes-written-large.js @@ -0,0 +1,67 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +// Regression test for https://github.com/nodejs/node/issues/19562: +// Writing to a socket first tries to push through as much data as possible +// without blocking synchronously, and, if that is not enough, queues more +// data up for asynchronous writing. +// Check that `bytesWritten` accounts for both parts of a write. + +const N = 10000000; +{ + // Variant 1: Write a Buffer. + const server = net.createServer(common.mustCall((socket) => { + socket.end(Buffer.alloc(N), common.mustCall(() => { + assert.strictEqual(socket.bytesWritten, N); + })); + assert.strictEqual(socket.bytesWritten, N); + })).listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + client.resume(); + client.on('close', common.mustCall(() => { + assert.strictEqual(client.bytesRead, N); + server.close(); + })); + })); +} + +{ + // Variant 2: Write a string. + const server = net.createServer(common.mustCall((socket) => { + socket.end('a'.repeat(N), common.mustCall(() => { + assert.strictEqual(socket.bytesWritten, N); + })); + assert.strictEqual(socket.bytesWritten, N); + })).listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + client.resume(); + client.on('close', common.mustCall(() => { + assert.strictEqual(client.bytesRead, N); + server.close(); + })); + })); +} + +{ + // Variant 2: writev() with mixed data. + const server = net.createServer(common.mustCall((socket) => { + socket.cork(); + socket.write('a'.repeat(N)); + assert.strictEqual(socket.bytesWritten, N); + socket.write(Buffer.alloc(N)); + assert.strictEqual(socket.bytesWritten, 2 * N); + socket.end('', common.mustCall(() => { + assert.strictEqual(socket.bytesWritten, 2 * N); + })); + socket.uncork(); + })).listen(0, common.mustCall(() => { + const client = net.connect(server.address().port); + client.resume(); + client.on('close', common.mustCall(() => { + assert.strictEqual(client.bytesRead, 2 * N); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-net-can-reset-timeout.js b/test/js/node/test/parallel/test-net-can-reset-timeout.js new file mode 100644 index 0000000000..c72efb5f33 --- /dev/null +++ b/test/js/node/test/parallel/test-net-can-reset-timeout.js @@ -0,0 +1,57 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +// Ref: https://github.com/nodejs/node-v0.x-archive/issues/481 + +const net = require('net'); + +const server = net.createServer(common.mustCall(function(stream) { + stream.setTimeout(100); + + stream.resume(); + + stream.once('timeout', common.mustCall(function() { + console.log('timeout'); + // Try to reset the timeout. + stream.write('WHAT.'); + })); + + stream.on('end', common.mustCall(function() { + console.log('server side end'); + stream.end(); + })); +})); + +server.listen(0, common.mustCall(function() { + const c = net.createConnection(this.address().port); + + c.on('data', function() { + c.end(); + }); + + c.on('end', function() { + console.log('client side end'); + server.close(); + }); +})); diff --git a/test/js/node/test/parallel/test-net-connect-abort-controller.js b/test/js/node/test/parallel/test-net-connect-abort-controller.js index 9c259cc3fc..5432bbabae 100644 --- a/test/js/node/test/parallel/test-net-connect-abort-controller.js +++ b/test/js/node/test/parallel/test-net-connect-abort-controller.js @@ -24,6 +24,7 @@ server.listen(0, common.mustCall(async () => { assert.fail(`close ${testName} should have thrown`); } catch (err) { assert.strictEqual(err.name, 'AbortError'); + assert.strictEqual(err.toString(), 'AbortError: The operation was aborted.'); } }; diff --git a/test/js/node/test/parallel/test-net-connect-after-destroy.js b/test/js/node/test/parallel/test-net-connect-after-destroy.js new file mode 100644 index 0000000000..6697cf8e32 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-after-destroy.js @@ -0,0 +1,9 @@ +'use strict'; +// Regression test for https://github.com/nodejs/node-v0.x-archive/issues/819. + +require('../common'); +const net = require('net'); + +// Connect to something that we need to DNS resolve +const c = net.createConnection(80, 'google.com'); +c.destroy(); diff --git a/test/js/node/test/parallel/test-net-connect-immediate-finish.js b/test/js/node/test/parallel/test-net-connect-immediate-finish.js new file mode 100644 index 0000000000..1cc4fa4f4e --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-immediate-finish.js @@ -0,0 +1,59 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// This tests that if the socket is still in the 'connecting' state +// when the user calls socket.end() ('finish'), the socket would emit +// 'connect' and defer the handling until the 'connect' event is handled. + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const { addresses } = require('../common/internet'); +const { + errorLookupMock, + mockedErrorCode, + mockedSysCall +} = require('../common/dns'); + +const client = net.connect({ + host: addresses.INVALID_HOST, + port: 80, // Port number doesn't matter because host name is invalid + lookup: common.mustCall(errorLookupMock()) +}, common.mustNotCall()); + +client.once('error', common.mustCall((error) => { + // TODO(BridgeAR): Add a better way to handle not defined properties using + // `assert.throws(fn, object)`. + assert.ok(!('port' in error)); + assert.ok(!('host' in error)); + assert.throws(() => { throw error; }, { + code: mockedErrorCode, + errno: mockedErrorCode, + syscall: mockedSysCall, + hostname: addresses.INVALID_HOST, + message: 'getaddrinfo ENOTFOUND something.invalid' + }); +})); + +client.end(); diff --git a/test/js/node/test/parallel/test-net-connect-nodelay.js b/test/js/node/test/parallel/test-net-connect-nodelay.js new file mode 100644 index 0000000000..6810e339e2 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-nodelay.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const truthyValues = [true, 1, 'true', {}, []]; +const falseyValues = [false, 0, '']; +const genSetNoDelay = (desiredArg) => (enable) => { + assert.strictEqual(enable, desiredArg); +}; + +for (const value of truthyValues) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, noDelay: value }, + common.mustCall(() => client.end()) + ); + + client._handle.setNoDelay = common.mustCall(genSetNoDelay(true)); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); +} + +for (const value of falseyValues) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, noDelay: value }, + common.mustCall(() => client.end()) + ); + + client._handle.setNoDelay = common.mustNotCall(); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-net-connect-options-ipv6.js b/test/js/node/test/parallel/test-net-connect-options-ipv6.js new file mode 100644 index 0000000000..13381074ba --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-options-ipv6.js @@ -0,0 +1,67 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Test that the family option of net.connect is honored. + +'use strict'; +const common = require('../common'); +if (!common.hasIPv6) + common.skip('no IPv6 support'); + +const assert = require('assert'); +const net = require('net'); + +const hostAddrIPv6 = '::1'; +const HOSTNAME = 'dummy'; + +const server = net.createServer({ allowHalfOpen: true }, (socket) => { + socket.resume(); + socket.on('end', common.mustCall()); + socket.end(); +}); + +function tryConnect() { + const connectOpt = { + host: HOSTNAME, + port: server.address().port, + family: 6, + allowHalfOpen: true, + lookup: common.mustCall((addr, opt, cb) => { + assert.strictEqual(addr, HOSTNAME); + assert.strictEqual(opt.family, 6); + cb(null, hostAddrIPv6, opt.family); + }) + }; + // No `mustCall`, since test could skip, and it's the only path to `close`. + const client = net.connect(connectOpt, () => { + client.resume(); + client.on('end', () => { + // Wait for next uv tick and make sure the socket stream is writable. + setTimeout(function() { + assert(client.writable); + client.end(); + }, 10); + }); + client.on('close', () => server.close()); + }); +} + +server.listen(0, hostAddrIPv6, tryConnect); diff --git a/test/js/node/test/parallel/test-net-connect-options-port.js b/test/js/node/test/parallel/test-net-connect-options-port.js new file mode 100644 index 0000000000..b62fe94578 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-options-port.js @@ -0,0 +1,230 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const dns = require('dns'); +const net = require('net'); + +// Test wrong type of ports +{ + const portTypeError = { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }; + + syncFailToConnect(true, portTypeError); + syncFailToConnect(false, portTypeError); + syncFailToConnect([], portTypeError, true); + syncFailToConnect({}, portTypeError, true); + syncFailToConnect(null, portTypeError); +} + +// Test out of range ports +{ + const portRangeError = { + code: 'ERR_SOCKET_BAD_PORT', + name: 'RangeError' + }; + + syncFailToConnect('', portRangeError); + syncFailToConnect(' ', portRangeError); + syncFailToConnect('0x', portRangeError, true); + syncFailToConnect('-0x1', portRangeError, true); + syncFailToConnect(NaN, portRangeError); + syncFailToConnect(Infinity, portRangeError); + syncFailToConnect(-1, portRangeError); + syncFailToConnect(65536, portRangeError); +} + +// Test invalid hints +{ + // connect({hint}, cb) and connect({hint}) + const hints = (dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL) + 42; + const hintOptBlocks = doConnect([{ port: 42, hints }], + () => common.mustNotCall()); + for (const fn of hintOptBlocks) { + assert.throws(fn, { + code: 'ERR_INVALID_ARG_VALUE', + name: 'TypeError', + message: /The argument 'hints' is invalid\. Received \d+/ + }); + } +} + +// Test valid combinations of connect(port) and connect(port, host) +{ + const expectedConnections = 72; + let serverConnected = 0; + + const server = net.createServer(common.mustCall((socket) => { + socket.end('ok'); + if (++serverConnected === expectedConnections) { + server.close(); + } + }, expectedConnections)); + + server.listen(0, common.localhostIPv4, common.mustCall(() => { + const port = server.address().port; + + // Total connections = 3 * 4(canConnect) * 6(doConnect) = 72 + canConnect(port); + canConnect(String(port)); + canConnect(`0x${port.toString(16)}`); + })); + + // Try connecting to random ports, but do so once the server is closed + server.on('close', () => { + asyncFailToConnect(0); + }); +} + +function doConnect(args, getCb) { + return [ + function createConnectionWithCb() { + return net.createConnection.apply(net, args.concat(getCb())) + .resume(); + }, + function createConnectionWithoutCb() { + return net.createConnection.apply(net, args) + .on('connect', getCb()) + .resume(); + }, + function connectWithCb() { + return net.connect.apply(net, args.concat(getCb())) + .resume(); + }, + function connectWithoutCb() { + return net.connect.apply(net, args) + .on('connect', getCb()) + .resume(); + }, + function socketConnectWithCb() { + const socket = new net.Socket(); + return socket.connect.apply(socket, args.concat(getCb())) + .resume(); + }, + function socketConnectWithoutCb() { + const socket = new net.Socket(); + return socket.connect.apply(socket, args) + .on('connect', getCb()) + .resume(); + }, + ]; +} + +function syncFailToConnect(port, assertErr, optOnly) { + const family = 4; + if (!optOnly) { + // connect(port, cb) and connect(port) + const portArgFunctions = doConnect([{ port, family }], + () => common.mustNotCall()); + for (const fn of portArgFunctions) { + assert.throws(fn, assertErr, `${fn.name}(${port})`); + } + + // connect(port, host, cb) and connect(port, host) + const portHostArgFunctions = doConnect([{ port, + host: 'localhost', + family }], + () => common.mustNotCall()); + for (const fn of portHostArgFunctions) { + assert.throws(fn, assertErr, `${fn.name}(${port}, 'localhost')`); + } + } + // connect({port}, cb) and connect({port}) + const portOptFunctions = doConnect([{ port, family }], + () => common.mustNotCall()); + for (const fn of portOptFunctions) { + assert.throws(fn, assertErr, `${fn.name}({port: ${port}})`); + } + + // connect({port, host}, cb) and connect({port, host}) + const portHostOptFunctions = doConnect([{ port: port, + host: 'localhost', + family: family }], + () => common.mustNotCall()); + for (const fn of portHostOptFunctions) { + assert.throws(fn, + assertErr, + `${fn.name}({port: ${port}, host: 'localhost'})`); + } +} + +function canConnect(port) { + const noop = () => common.mustCall(); + const family = 4; + + // connect(port, cb) and connect(port) + const portArgFunctions = doConnect([{ port, family }], noop); + for (const fn of portArgFunctions) { + fn(); + } + + // connect(port, host, cb) and connect(port, host) + const portHostArgFunctions = doConnect([{ port, host: 'localhost', family }], + noop); + for (const fn of portHostArgFunctions) { + fn(); + } + + // connect({port}, cb) and connect({port}) + const portOptFunctions = doConnect([{ port, family }], noop); + for (const fn of portOptFunctions) { + fn(); + } + + // connect({port, host}, cb) and connect({port, host}) + const portHostOptFns = doConnect([{ port, host: 'localhost', family }], + noop); + for (const fn of portHostOptFns) { + fn(); + } +} + +function asyncFailToConnect(port) { + const onError = () => common.mustCall((err) => { + const regexp = /^Error: connect E\w+.+$/; + assert.match(String(err), regexp); + }); + + const dont = () => common.mustNotCall(); + const family = 4; + // connect(port, cb) and connect(port) + const portArgFunctions = doConnect([{ port, family }], dont); + for (const fn of portArgFunctions) { + fn().on('error', onError()); + } + + // connect({port}, cb) and connect({port}) + const portOptFunctions = doConnect([{ port, family }], dont); + for (const fn of portOptFunctions) { + fn().on('error', onError()); + } + + // connect({port, host}, cb) and connect({port, host}) + const portHostOptFns = doConnect([{ port, host: 'localhost', family }], + dont); + for (const fn of portHostOptFns) { + fn().on('error', onError()); + } +} diff --git a/test/js/node/test/parallel/test-net-connect-reset-before-connected.js b/test/js/node/test/parallel/test-net-connect-reset-before-connected.js new file mode 100644 index 0000000000..1dc2b98183 --- /dev/null +++ b/test/js/node/test/parallel/test-net-connect-reset-before-connected.js @@ -0,0 +1,13 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const socket = net.connect(port, common.localhostIPv4, common.mustNotCall()); +socket.on('error', common.mustNotCall()); +server.close(); +socket.resetAndDestroy(); +// `reset` waiting socket connected to sent the RST packet +socket.destroy(); diff --git a/test/js/node/test/parallel/test-net-deprecated-setsimultaneousaccepts.js b/test/js/node/test/parallel/test-net-deprecated-setsimultaneousaccepts.js new file mode 100644 index 0000000000..dd6decdcce --- /dev/null +++ b/test/js/node/test/parallel/test-net-deprecated-setsimultaneousaccepts.js @@ -0,0 +1,18 @@ +// Flags: --no-warnings +'use strict'; + +// Test that DEP0121 is emitted on the first call of _setSimultaneousAccepts(). + +const { + expectWarning +} = require('../common'); +const { + _setSimultaneousAccepts +} = require('net'); + +expectWarning( + 'DeprecationWarning', + 'net._setSimultaneousAccepts() is deprecated and will be removed.', + 'DEP0121'); + +_setSimultaneousAccepts(); diff --git a/test/js/node/test/parallel/test-net-dns-custom-lookup.js b/test/js/node/test/parallel/test-net-dns-custom-lookup.js new file mode 100644 index 0000000000..e1e425abd0 --- /dev/null +++ b/test/js/node/test/parallel/test-net-dns-custom-lookup.js @@ -0,0 +1,67 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +function check(addressType, cb) { + const server = net.createServer(function(client) { + client.end(); + server.close(); + cb && cb(); + }); + + const address = addressType === 4 ? common.localhostIPv4 : '::1'; + server.listen(0, address, common.mustCall(function() { + net.connect({ + port: this.address().port, + host: 'localhost', + family: addressType, + lookup: lookup + }).on('lookup', common.mustCall(function(err, ip, type) { + assert.strictEqual(err, null); + assert.strictEqual(address, ip); + assert.strictEqual(type, addressType); + })); + })); + + function lookup(host, dnsopts, cb) { + dnsopts.family = addressType; + + if (addressType === 4) { + process.nextTick(function() { + if (dnsopts.all) { + cb(null, [{ address: common.localhostIPv4, family: 4 }]); + } else { + cb(null, common.localhostIPv4, 4); + } + }); + } else { + process.nextTick(function() { + if (dnsopts.all) { + cb(null, [{ address: '::1', family: 6 }]); + } else { + cb(null, '::1', 6); + } + }); + } + } +} + +check(4, function() { + common.hasIPv6 && check(6); +}); + +// Verify that bad lookup() IPs are handled. +{ + net.connect({ + host: 'localhost', + port: 80, + lookup(host, dnsopts, cb) { + if (dnsopts.all) { + cb(null, [{ address: undefined, family: 4 }]); + } else { + cb(null, undefined, 4); + } + } + }).on('error', common.expectsError({ code: 'ERR_INVALID_IP_ADDRESS' })); +} diff --git a/test/js/node/test/parallel/test-net-dns-error.js b/test/js/node/test/parallel/test-net-dns-error.js new file mode 100644 index 0000000000..7232ef10eb --- /dev/null +++ b/test/js/node/test/parallel/test-net-dns-error.js @@ -0,0 +1,41 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const net = require('net'); + +const host = '*'.repeat(64); +// Resolving hostname > 63 characters may return EAI_FAIL (permanent failure). +const errCodes = ['ENOTFOUND', 'EAI_FAIL']; + +const socket = net.connect(42, host, common.mustNotCall()); +socket.on('error', common.mustCall(function(err) { + assert(errCodes.includes(err.code), err); +})); +socket.on('lookup', common.mustCall(function(err, ip, type) { + assert(err instanceof Error); + assert(errCodes.includes(err.code), err); + assert.strictEqual(ip, undefined); + assert.strictEqual(type, undefined); +})); diff --git a/test/js/node/test/parallel/test-net-dns-lookup.js b/test/js/node/test/parallel/test-net-dns-lookup.js new file mode 100644 index 0000000000..3bd6bd45ce --- /dev/null +++ b/test/js/node/test/parallel/test-net-dns-lookup.js @@ -0,0 +1,40 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server = net.createServer(function(client) { + client.end(); + server.close(); +}); + +server.listen(0, common.mustCall(function() { + const socket = net.connect(this.address().port, 'localhost'); + socket.on('lookup', common.mustCallAtLeast(function(err, ip, type, host) { + assert.strictEqual(err, null); + assert.match(ip, /^(127\.0\.0\.1|::1)$/); + assert.match(type.toString(), /^(4|6)$/); + assert.strictEqual(host, 'localhost'); + }, 1)); +})); diff --git a/test/js/node/test/parallel/test-net-eaddrinuse.js b/test/js/node/test/parallel/test-net-eaddrinuse.js new file mode 100644 index 0000000000..8bd790158e --- /dev/null +++ b/test/js/node/test/parallel/test-net-eaddrinuse.js @@ -0,0 +1,35 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const server1 = net.createServer(common.mustNotCall()); +const server2 = net.createServer(common.mustNotCall()); +server1.listen(0, common.mustCall(function() { + server2.on('error', function(error) { + assert.strictEqual(error.code === 'EADDRINUSE', true); + server1.close(); + }); + server2.listen(this.address().port); +})); diff --git a/test/js/node/test/parallel/test-net-keepalive.js b/test/js/node/test/parallel/test-net-keepalive.js new file mode 100644 index 0000000000..d91ec625f8 --- /dev/null +++ b/test/js/node/test/parallel/test-net-keepalive.js @@ -0,0 +1,52 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +let serverConnection; +let clientConnection; +const echoServer = net.createServer(function(connection) { + serverConnection = connection; + setTimeout(common.mustCall(function() { + // Make sure both connections are still open + assert.strictEqual(serverConnection.readyState, 'open'); + assert.strictEqual(clientConnection.readyState, 'open'); + serverConnection.end(); + clientConnection.end(); + echoServer.close(); + }, 1), common.platformTimeout(100)); + connection.setTimeout(0); + assert.notStrictEqual(connection.setKeepAlive, undefined); + // Send a keepalive packet after 50 ms + connection.setKeepAlive(true, common.platformTimeout(50)); + connection.on('end', function() { + connection.end(); + }); +}); +echoServer.listen(0); + +echoServer.on('listening', function() { + clientConnection = net.createConnection(this.address().port); + clientConnection.setTimeout(0); +}); diff --git a/test/js/node/test/parallel/test-net-options-lookup.js b/test/js/node/test/parallel/test-net-options-lookup.js new file mode 100644 index 0000000000..9a7ab00de9 --- /dev/null +++ b/test/js/node/test/parallel/test-net-options-lookup.js @@ -0,0 +1,52 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +['foobar', 1, {}, []].forEach((input) => connectThrows(input)); + +// Using port 0 as lookup is emitted before connecting. +function connectThrows(input) { + const opts = { + host: 'localhost', + port: 0, + lookup: input + }; + + assert.throws(() => { + net.connect(opts); + }, { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError' + }); +} + +connectDoesNotThrow(() => {}); + +function connectDoesNotThrow(input) { + const opts = { + host: 'localhost', + port: 0, + lookup: input + }; + + return net.connect(opts); +} + +{ + // Verify that an error is emitted when an invalid address family is returned. + const s = connectDoesNotThrow((host, options, cb) => { + if (options.all) { + cb(null, [{ address: '127.0.0.1', family: 100 }]); + } else { + cb(null, '127.0.0.1', 100); + } + }); + + s.on('error', common.expectsError({ + code: 'ERR_INVALID_ADDRESS_FAMILY', + host: 'localhost', + port: 0, + message: 'Invalid address family: 100 localhost:0' + })); +} diff --git a/test/js/node/test/parallel/test-net-persistent-keepalive.js b/test/js/node/test/parallel/test-net-persistent-keepalive.js new file mode 100644 index 0000000000..b25162996e --- /dev/null +++ b/test/js/node/test/parallel/test-net-persistent-keepalive.js @@ -0,0 +1,34 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); + +let serverConnection; +let clientConnection; +const echoServer = net.createServer(function(connection) { + serverConnection = connection; + setTimeout(function() { + // Make sure both connections are still open + assert.strictEqual(serverConnection.readyState, 'open'); + assert.strictEqual(clientConnection.readyState, 'open'); + serverConnection.end(); + clientConnection.end(); + echoServer.close(); + }, 600); + connection.setTimeout(0); + assert.strictEqual(typeof connection.setKeepAlive, 'function'); + connection.on('end', function() { + connection.end(); + }); +}); +echoServer.listen(0); + +echoServer.on('listening', function() { + clientConnection = new net.Socket(); + // Send a keepalive packet after 1000 ms + // and make sure it persists + const s = clientConnection.setKeepAlive(true, 400); + assert.ok(s instanceof net.Socket); + clientConnection.connect(this.address().port); + clientConnection.setTimeout(0); +}); diff --git a/test/js/node/test/parallel/test-net-remote-address-port.js b/test/js/node/test/parallel/test-net-remote-address-port.js new file mode 100644 index 0000000000..8cbf661b55 --- /dev/null +++ b/test/js/node/test/parallel/test-net-remote-address-port.js @@ -0,0 +1,86 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const assert = require('assert'); + +const net = require('net'); + +let conns_closed = 0; + +const remoteAddrCandidates = [ common.localhostIPv4, + '::1', + '::ffff:127.0.0.1' ]; + +const remoteFamilyCandidates = ['IPv4', 'IPv6']; + +const server = net.createServer(common.mustCall(function(socket) { + assert.ok(remoteAddrCandidates.includes(socket.remoteAddress), `Invalid remoteAddress: ${socket.remoteAddress}`); + assert.ok(remoteFamilyCandidates.includes(socket.remoteFamily), `Invalid remoteFamily: ${socket.remoteFamily}`); + assert.ok(socket.remotePort); + assert.notStrictEqual(socket.remotePort, this.address().port); + socket.on('end', function() { + if (++conns_closed === 2) server.close(); + }); + socket.on('close', function() { + assert.ok(remoteAddrCandidates.includes(socket.remoteAddress)); + assert.ok(remoteFamilyCandidates.includes(socket.remoteFamily)); + }); + socket.resume(); +}, 2)); + +server.listen(0, function() { + const client = net.createConnection(this.address().port, '127.0.0.1'); + const client2 = net.createConnection(this.address().port); + + assert.strictEqual(client.remoteAddress, undefined); + assert.strictEqual(client.remoteFamily, undefined); + assert.strictEqual(client.remotePort, undefined); + assert.strictEqual(client2.remoteAddress, undefined); + assert.strictEqual(client2.remoteFamily, undefined); + assert.strictEqual(client2.remotePort, undefined); + + client.on('connect', function() { + console.log(1, !!client._handle, client.remoteAddress, client.remoteFamily, client.remotePort); + assert.ok(remoteAddrCandidates.includes(client.remoteAddress)); + assert.ok(remoteFamilyCandidates.includes(client.remoteFamily)); + assert.strictEqual(client.remotePort, server.address().port); + client.end(); + }); + client.on('close', function() { + console.log(2, !!client._handle, client.remoteAddress, client.remoteFamily); + assert.ok(remoteAddrCandidates.includes(client.remoteAddress)); + assert.ok(remoteFamilyCandidates.includes(client.remoteFamily)); + }); + client2.on('connect', function() { + console.log(3, !!client2._handle, client2.remoteAddress, client2.remoteFamily, client2.remotePort); + assert.ok(remoteAddrCandidates.includes(client2.remoteAddress)); + assert.ok(remoteFamilyCandidates.includes(client2.remoteFamily)); + assert.strictEqual(client2.remotePort, server.address().port); + client2.end(); + }); + client2.on('close', function() { + console.log(4, !!client2._handle, client2.remoteAddress, client2.remoteFamily); + assert.ok(remoteAddrCandidates.includes(client2.remoteAddress)); + assert.ok(remoteFamilyCandidates.includes(client2.remoteFamily)); + }); +}); diff --git a/test/js/node/test/parallel/test-net-server-blocklist.js b/test/js/node/test/parallel/test-net-server-blocklist.js new file mode 100644 index 0000000000..66bb948e82 --- /dev/null +++ b/test/js/node/test/parallel/test-net-server-blocklist.js @@ -0,0 +1,20 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const blockList = new net.BlockList(); +blockList.addAddress(common.localhostIPv4); +console.log('common.localhostIPv4',common.localhostIPv4) + +const server = net.createServer({ blockList }, common.mustNotCall()); +server.listen(0, common.localhostIPv4, common.mustCall(() => { + const adddress = server.address(); + const socket = net.connect({ + // localAddress: common.localhostIPv4, + host: adddress.address, + port: adddress.port + }); + socket.on('close', common.mustCall(() => { + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-server-simultaneous-accepts-produce-warning-once.js b/test/js/node/test/parallel/test-net-server-simultaneous-accepts-produce-warning-once.js new file mode 100644 index 0000000000..391c2ecea7 --- /dev/null +++ b/test/js/node/test/parallel/test-net-server-simultaneous-accepts-produce-warning-once.js @@ -0,0 +1,19 @@ +'use strict'; + +// Test that DEP0121 is emitted only once if _setSimultaneousAccepts() is called +// more than once. This test is similar to +// test-net-deprecated-setsimultaneousaccepts.js, but that test calls +// _setSimultaneousAccepts() only once. Unlike this test, that will confirm +// that the warning is emitted on the first call. This test doesn't check which +// call caused the warning to be emitted. + +const { expectWarning } = require('../common'); +const { _setSimultaneousAccepts } = require('net'); + +expectWarning( + 'DeprecationWarning', + 'net._setSimultaneousAccepts() is deprecated and will be removed.', + 'DEP0121'); + +_setSimultaneousAccepts(); +_setSimultaneousAccepts(); diff --git a/test/js/node/test/parallel/test-net-settimeout.js b/test/js/node/test/parallel/test-net-settimeout.js new file mode 100644 index 0000000000..581e27ec32 --- /dev/null +++ b/test/js/node/test/parallel/test-net-settimeout.js @@ -0,0 +1,50 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +// This example sets a timeout then immediately attempts to disable the timeout +// https://github.com/joyent/node/pull/2245 + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const T = 100; + +const server = net.createServer(common.mustCall((c) => { + c.write('hello'); +})); + +server.listen(0, function() { + const socket = net.createConnection(this.address().port, 'localhost'); + + const s = socket.setTimeout(T, common.mustNotCall()); + assert.ok(s instanceof net.Socket); + + socket.on('data', common.mustCall(() => { + setTimeout(function() { + socket.destroy(); + server.close(); + }, T * 2); + })); + + socket.setTimeout(0); +}); diff --git a/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamily.js b/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamily.js new file mode 100644 index 0000000000..9ca8ab5c5b --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamily.js @@ -0,0 +1,9 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const net = require('net'); + +assert.throws(() => { + net.connect({ port: 8080, autoSelectFamily: 'INVALID' }); +}, { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamilyattempttimeout.js b/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamilyattempttimeout.js new file mode 100644 index 0000000000..0fc813781c --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-connect-invalid-autoselectfamilyattempttimeout.js @@ -0,0 +1,27 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const net = require('net'); + +for (const autoSelectFamilyAttemptTimeout of [-10, 0]) { + assert.throws(() => { + net.connect({ + port: 8080, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout, + }); + }, { code: 'ERR_OUT_OF_RANGE' }); + + assert.throws(() => { + net.setDefaultAutoSelectFamilyAttemptTimeout(autoSelectFamilyAttemptTimeout); + }, { code: 'ERR_OUT_OF_RANGE' }); +} + +// Check the default value of autoSelectFamilyAttemptTimeout is 10 +// if passed number is less than 10 +for (const autoSelectFamilyAttemptTimeout of [1, 9]) { + net.setDefaultAutoSelectFamilyAttemptTimeout(autoSelectFamilyAttemptTimeout); + assert.strictEqual(net.getDefaultAutoSelectFamilyAttemptTimeout(), 10); +} diff --git a/test/js/node/test/parallel/test-net-socket-destroy-send.js b/test/js/node/test/parallel/test-net-socket-destroy-send.js new file mode 100644 index 0000000000..db792ad6d3 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-destroy-send.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const server = net.createServer(); +server.listen(0, common.mustCall(function() { + const port = server.address().port; + const conn = net.createConnection(port); + + conn.on('connect', common.mustCall(function() { + // Test destroy returns this, even on multiple calls when it short-circuits. + assert.strictEqual(conn, conn.destroy().destroy()); + conn.on('error', common.mustNotCall()); + + conn.write(Buffer.from('kaboom'), common.expectsError({ + code: 'ERR_STREAM_DESTROYED', + message: 'Cannot call write after a stream was destroyed', + name: 'Error' + })); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-net-socket-destroy-twice.js b/test/js/node/test/parallel/test-net-socket-destroy-twice.js new file mode 100644 index 0000000000..1029d7b298 --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-destroy-twice.js @@ -0,0 +1,36 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const conn = net.createConnection(port); + +conn.on('error', common.mustCall(() => { + conn.destroy(); +})); + +conn.on('close', common.mustCall()); +server.close(); diff --git a/test/js/node/test/parallel/test-net-socket-reset-twice.js b/test/js/node/test/parallel/test-net-socket-reset-twice.js new file mode 100644 index 0000000000..0292c5e3ab --- /dev/null +++ b/test/js/node/test/parallel/test-net-socket-reset-twice.js @@ -0,0 +1,15 @@ +'use strict'; +const common = require('../common'); +const net = require('net'); + +const server = net.createServer(); +server.listen(0); +const port = server.address().port; +const conn = net.createConnection(port); + +conn.on('error', common.mustCall(() => { + conn.resetAndDestroy(); +})); + +conn.on('close', common.mustCall()); +server.close(); diff --git a/test/js/node/test/parallel/test-net-throttle.js b/test/js/node/test/parallel/test-net-throttle.js new file mode 100644 index 0000000000..fd9e2be9ae --- /dev/null +++ b/test/js/node/test/parallel/test-net-throttle.js @@ -0,0 +1,88 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +require('../common'); +const assert = require('assert'); +const net = require('net'); +const debuglog = require('util').debuglog('test'); + +let chars_recved = 0; +let npauses = 0; +let totalLength = 0; + +const server = net.createServer((connection) => { + const body = 'C'.repeat(1024); + let n = 1; + debuglog('starting write loop'); + while (connection.write(body)) { + n++; + } + debuglog('ended write loop'); + // Now that we're throttled, do some more writes to make sure the data isn't + // lost. + connection.write(body); + connection.write(body); + n += 2; + totalLength = n * body.length; + assert.ok(connection.bufferSize >= 0, `bufferSize: ${connection.bufferSize}`); + assert.ok( + connection.writableLength <= totalLength, + `writableLength: ${connection.writableLength}, totalLength: ${totalLength}`, + ); + connection.end(); +}); + +server.listen(0, () => { + const port = server.address().port; + debuglog(`server started on port ${port}`); + let paused = false; + const client = net.createConnection(port); + client.setEncoding('ascii'); + client.on('data', (d) => { + chars_recved += d.length; + debuglog(`got ${chars_recved}`); + if (!paused) { + client.pause(); + npauses += 1; + paused = true; + debuglog('pause'); + const x = chars_recved; + setTimeout(() => { + assert.strictEqual(chars_recved, x); + client.resume(); + debuglog('resume'); + paused = false; + }, 100); + } + }); + + client.on('end', () => { + server.close(); + client.end(); + }); +}); + + +process.on('exit', () => { + assert.strictEqual(chars_recved, totalLength); + assert.ok(npauses > 1, `${npauses} > 1`); +}); diff --git a/test/js/node/test/parallel/test-net-timeout-no-handle.js b/test/js/node/test/parallel/test-net-timeout-no-handle.js new file mode 100644 index 0000000000..57dd2c94ba --- /dev/null +++ b/test/js/node/test/parallel/test-net-timeout-no-handle.js @@ -0,0 +1,17 @@ +'use strict'; + +const common = require('../common'); +const net = require('net'); +const assert = require('assert'); + +const socket = new net.Socket(); +socket.setTimeout(common.platformTimeout(1200)); + +socket.on('timeout', common.mustCall(() => { + assert.strictEqual(socket._handle, null); +})); + +socket.on('connect', common.mustNotCall()); + +// Since the timeout is unrefed, the code will exit without this +setTimeout(() => {}, common.platformTimeout(2500)); diff --git a/test/js/node/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js b/test/js/node/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js new file mode 100644 index 0000000000..01d2367bdb --- /dev/null +++ b/test/js/node/test/parallel/test-timers-socket-timeout-removes-other-socket-unref-timer.js @@ -0,0 +1,43 @@ +'use strict'; + +// Regression test for https://github.com/nodejs/node-v0.x-archive/issues/8897. + +const common = require('../common'); +const net = require('net'); +const Countdown = require('../common/countdown'); + +const clients = []; + +const server = net.createServer(function onClient(client) { + clients.push(client); + + if (clients.length === 2) { + // Enroll two timers, and make the one supposed to fire first + // unenroll the other one supposed to fire later. This mutates + // the list of unref timers when traversing it, and exposes the + // original issue in joyent/node#8897. + clients[0].setTimeout(1, () => { + clients[1].setTimeout(0); + clients[0].end(); + clients[1].end(); + }); + + // Use a delay that is higher than the lowest timer resolution across all + // supported platforms, so that the two timers don't fire at the same time. + clients[1].setTimeout(50); + } +}); + +server.listen(0, common.mustCall(() => { + const countdown = new Countdown(2, () => server.close()); + + { + const client = net.connect({ port: server.address().port }); + client.on('end', () => countdown.dec()); + } + + { + const client = net.connect({ port: server.address().port }); + client.on('end', () => countdown.dec()); + } +})); diff --git a/test/js/node/test/parallel/test-tls-cert-ext-encoding.js b/test/js/node/test/parallel/test-tls-cert-ext-encoding.js index 4556b57918..d59f9206a0 100644 --- a/test/js/node/test/parallel/test-tls-cert-ext-encoding.js +++ b/test/js/node/test/parallel/test-tls-cert-ext-encoding.js @@ -1,5 +1,6 @@ 'use strict'; const common = require('../common'); +if (typeof globalThis.Bun !== "undefined") return; // TODO: BUN if (!common.hasCrypto) common.skip('missing crypto'); diff --git a/test/js/node/test/parallel/test-tls-close-error.js b/test/js/node/test/parallel/test-tls-close-error.js new file mode 100644 index 0000000000..de51b4686a --- /dev/null +++ b/test/js/node/test/parallel/test-tls-close-error.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}, function(c) { +}).listen(0, common.mustCall(function() { + const c = tls.connect(this.address().port, common.mustNotCall()); + + c.on('error', common.mustCall()); + + c.on('close', common.mustCall(function(err) { + assert.ok(err); + server.close(); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-connect-hints-option.js b/test/js/node/test/parallel/test-tls-connect-hints-option.js new file mode 100644 index 0000000000..6abcf19402 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-connect-hints-option.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); + +// This test verifies that `tls.connect()` honors the `hints` option. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const dns = require('dns'); +const tls = require('tls'); + +const hints = 512; + +assert.notStrictEqual(hints, dns.ADDRCONFIG); +assert.notStrictEqual(hints, dns.V4MAPPED); +assert.notStrictEqual(hints, dns.ALL); +assert.notStrictEqual(hints, dns.ADDRCONFIG | dns.V4MAPPED); +assert.notStrictEqual(hints, dns.ADDRCONFIG | dns.ALL); +assert.notStrictEqual(hints, dns.V4MAPPED | dns.ALL); +assert.notStrictEqual(hints, dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL); + +tls.connect({ + port: 42, + lookup: common.mustCall((host, options) => { + assert.strictEqual(host, 'localhost'); + assert.deepStrictEqual(options, { family: undefined, hints, all: true }); + }), + hints +}); diff --git a/test/js/node/test/parallel/test-tls-friendly-error-message.js b/test/js/node/test/parallel/test-tls-friendly-error-message.js new file mode 100644 index 0000000000..4ae9d3f3f9 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-friendly-error-message.js @@ -0,0 +1,45 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const tls = require('tls'); + +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); + +tls.createServer({ key, cert }).on('connection', common.mustCall(function() { + // Server only receives one TCP connection, stop listening when that + // connection is destroyed by the client, which it should do after the cert is + // rejected as unauthorized. + this.close(); +})).listen(0, common.mustCall(function() { + const options = { port: this.address().port, rejectUnauthorized: true }; + tls.connect(options).on('error', common.mustCall(function(err) { + assert.strictEqual(err.code, 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'); + assert.strictEqual(err.message, 'unable to verify the first certificate'); + })); +})); diff --git a/test/js/node/test/parallel/test-tls-on-empty-socket.js b/test/js/node/test/parallel/test-tls-on-empty-socket.js index 87d51a81bb..e58a147b72 100644 --- a/test/js/node/test/parallel/test-tls-on-empty-socket.js +++ b/test/js/node/test/parallel/test-tls-on-empty-socket.js @@ -22,15 +22,15 @@ const server = tls.createServer({ const s = tls.connect({ socket: socket, rejectUnauthorized: false - }, function() { - s.on('data', function(chunk) { + }, common.mustCall(function() { + s.on('data', common.mustCall(function(chunk) { out += chunk; - }); - s.on('end', function() { + })); + s.on('end', common.mustCall(function() { s.destroy(); server.close(); - }); - }); + })); + })); socket.connect(this.address().port); }); diff --git a/test/js/node/test/parallel/test-tls-request-timeout.js b/test/js/node/test/parallel/test-tls-request-timeout.js new file mode 100644 index 0000000000..6bbb8432f5 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-request-timeout.js @@ -0,0 +1,51 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.Server(options, common.mustCall(function(socket) { + const s = socket.setTimeout(100); + assert.ok(s instanceof tls.TLSSocket); + + socket.on('timeout', common.mustCall(function(err) { + socket.end(); + server.close(); + })); +})); + +server.listen(0, function() { + tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }); +}); diff --git a/test/js/node/test/parallel/test-worker-message-port-wasm-threads.js b/test/js/node/test/parallel/test-worker-message-port-wasm-threads.js deleted file mode 100644 index fe70261fd7..0000000000 --- a/test/js/node/test/parallel/test-worker-message-port-wasm-threads.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -const common = require('../common'); -const assert = require('assert'); -const { MessageChannel, Worker } = require('worker_threads'); - -// Test that SharedArrayBuffer instances created from WASM are transferable -// through MessageChannels (without crashing). - -const fixtures = require('../common/fixtures'); -const wasmSource = fixtures.readSync('shared-memory.wasm'); -const wasmModule = new WebAssembly.Module(wasmSource); -const instance = new WebAssembly.Instance(wasmModule); - -const { buffer } = instance.exports.memory; -assert(buffer instanceof SharedArrayBuffer); - -{ - const { port1, port2 } = new MessageChannel(); - port1.postMessage(buffer); - port2.once('message', common.mustCall((buffer2) => { - // Make sure serialized + deserialized buffer refer to the same memory. - const expected = 'Hello, world!'; - const bytes = Buffer.from(buffer).write(expected); - const deserialized = Buffer.from(buffer2).toString('utf8', 0, bytes); - assert.deepStrictEqual(deserialized, expected); - })); -} - -{ - // Make sure we can free WASM memory originating from a thread that already - // stopped when we exit. - const worker = new Worker(` - const { parentPort } = require('worker_threads'); - - // Compile the same WASM module from its source bytes. - const wasmSource = new Uint8Array([${wasmSource.join(',')}]); - const wasmModule = new WebAssembly.Module(wasmSource); - const instance = new WebAssembly.Instance(wasmModule); - parentPort.postMessage(instance.exports.memory); - - // Do the same thing, except we receive the WASM module via transfer. - parentPort.once('message', ({ wasmModule }) => { - const instance = new WebAssembly.Instance(wasmModule); - parentPort.postMessage(instance.exports.memory); - }); - `, { eval: true }); - worker.on('message', common.mustCall(({ buffer }) => { - assert(buffer instanceof SharedArrayBuffer); - worker.buf = buffer; // Basically just keep the reference to buffer alive. - }, 2)); - worker.once('exit', common.mustCall()); - worker.postMessage({ wasmModule }); -} diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 9a6fcc8184..914cd62098 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -367,7 +367,8 @@ for (const { name, connect } of tests) { }); // Test using only options - it("should process options correctly when connect is called with only options", done => { + // prettier-ignore + it.skipIf(connect === duplexProxy)("should process options correctly when connect is called with only options", done => { let socket = connect({ port: 443, host: "bun.sh", diff --git a/test/js/node/tls/node-tls-namedpipes.test.ts b/test/js/node/tls/node-tls-namedpipes.test.ts index 586e0f81cb..9e937d9902 100644 --- a/test/js/node/tls/node-tls-namedpipes.test.ts +++ b/test/js/node/tls/node-tls-namedpipes.test.ts @@ -1,4 +1,3 @@ -import { heapStats } from "bun:jsc"; import { expect, it } from "bun:test"; import { expectMaxObjectTypeCount, isWindows, tls } from "harness"; import { randomUUID } from "node:crypto"; @@ -7,6 +6,7 @@ import net from "node:net"; import { connect, createServer } from "node:tls"; it.if(isWindows)("should work with named pipes and tls", async () => { + await expectMaxObjectTypeCount(expect, "TLSSocket", 0); async function test(pipe_name: string) { const { promise: messageReceived, resolve: resolveMessageReceived } = Promise.withResolvers(); const { promise: clientReceived, resolve: resolveClientReceived } = Promise.withResolvers(); @@ -40,9 +40,8 @@ it.if(isWindows)("should work with named pipes and tls", async () => { } } - const batch = []; + const batch: Promise[] = []; - const before = heapStats().objectTypeCounts.TLSSocket || 0; for (let i = 0; i < 200; i++) { batch.push(test(`\\\\.\\pipe\\test\\${randomUUID()}`)); batch.push(test(`\\\\?\\pipe\\test\\${randomUUID()}`)); @@ -52,10 +51,11 @@ it.if(isWindows)("should work with named pipes and tls", async () => { } } await Promise.all(batch); - expectMaxObjectTypeCount(expect, "TLSSocket", before); + await expectMaxObjectTypeCount(expect, "TLSSocket", 2); }); it.if(isWindows)("should be able to upgrade a named pipe connection to TLS", async () => { + await expectMaxObjectTypeCount(expect, "TLSSocket", 2); const { promise: messageReceived, resolve: resolveMessageReceived } = Promise.withResolvers(); const { promise: clientReceived, resolve: resolveClientReceived } = Promise.withResolvers(); let client: ReturnType | ReturnType | null = null; @@ -89,8 +89,6 @@ it.if(isWindows)("should be able to upgrade a named pipe connection to TLS", asy server?.close(); } } - const before = heapStats().objectTypeCounts.TLSSocket || 0; await test(`\\\\.\\pipe\\test\\${randomUUID()}`); - await test(`\\\\?\\pipe\\test\\${randomUUID()}`); - expectMaxObjectTypeCount(expect, "TLSSocket", before); + await expectMaxObjectTypeCount(expect, "TLSSocket", 3); });