diff --git a/src/js/internal/test/binding.ts b/src/js/internal/test/binding.ts index 3b7919cc436c..4369da0f4073 100644 --- a/src/js/internal/test/binding.ts +++ b/src/js/internal/test/binding.ts @@ -92,6 +92,12 @@ function internalBinding(name: string) { return { TCP: TestTCPWrap, constants: { SOCKET: 0, SERVER: 1 } }; case "util": return { isInsideNodeModules }; + // The engine tests construct or replace binding.SecureContext before requiring node:tls. + case "crypto": + return { SecureContext: require("node:tls").SecureContext }; + // BoringSSL has no SSL_trace(), the same as a node built without it. + case "tls_wrap": + return { HAVE_SSL_TRACE: false }; case "cares_wrap": // Only the pure IP-normalizer the vendored tls/dns tests reach for; the // resolver surface lives in node:dns. diff --git a/src/js/internal/timers.ts b/src/js/internal/timers.ts index ef48040fc6b8..c3147857c7db 100644 --- a/src/js/internal/timers.ts +++ b/src/js/internal/timers.ts @@ -23,6 +23,7 @@ function getTimerDuration(msecs, name) { } export default { + TIMEOUT_MAX, // For hiding Timeouts on other internals. A registered symbol so the node // test harness's --expose-internals shim ("internal/timers" virtual module // in test/js/node/test/common/index.js) can hand the same symbol to ported diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 1e9d83b4dda9..b46703b20138 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -140,6 +140,11 @@ const kSetTOS = Symbol("kSetTOS"); const kSetKeepAlive = Symbol("kSetKeepAlive"); const kSyncWriteFd = Symbol("kSyncWriteFd"); const kSetKeepAliveInitialDelay = Symbol("kSetKeepAliveInitialDelay"); + +// The delay is stored in whole seconds like node; the native handle takes int32 milliseconds. +function keepAliveDelayMs(seconds) { + return MathMin(seconds * 1000, 2147483647); +} const kConnectOptions = Symbol("connect-options"); const kAttach = Symbol("kAttach"); const kCloseRawConnection = Symbol("kCloseRawConnection"); @@ -503,7 +508,7 @@ const SocketHandlers: SocketHandler = { } if (self[kSetKeepAlive]) { - socket.setKeepAlive(true, self[kSetKeepAliveInitialDelay]); + socket.setKeepAlive(true, keepAliveDelayMs(self[kSetKeepAliveInitialDelay])); } // A TOS value set before the connection existed (setTypeOfService before @@ -1216,7 +1221,7 @@ function onconnection(err, clientHandle) { if (self.keepAlive && clientHandle.setKeepAlive) { _socket[kSetKeepAlive] = true; _socket[kSetKeepAliveInitialDelay] = self.keepAliveInitialDelay; - clientHandle.setKeepAlive(true, self.keepAliveInitialDelay); + clientHandle.setKeepAlive(true, keepAliveDelayMs(self.keepAliveInitialDelay)); } self._connections++; @@ -1585,9 +1590,8 @@ function Socket(options?) { this[kSetNoDelay] = Boolean(noDelay); this[kSetKeepAlive] = Boolean(keepAlive); - // Bun's native _handle.setKeepAlive takes milliseconds (it is the public - // Bun.Socket), so store ms here. Node stores seconds because libuv does. - this[kSetKeepAliveInitialDelay] = MathMax(0, ~~keepAliveInitialDelay); + // Whole seconds, as in node. + this[kSetKeepAliveInitialDelay] = MathMax(0, ~~(keepAliveInitialDelay / 1000)); this[khandlers] = SocketHandlers2; this.bytesRead = 0; @@ -1873,7 +1877,7 @@ Socket.prototype[kAttach] = function (port, socket) { } if (this[kSetKeepAlive]) { - socket.setKeepAlive(true, this[kSetKeepAliveInitialDelay]); + socket.setKeepAlive(true, keepAliveDelayMs(this[kSetKeepAliveInitialDelay])); } if (!this[kupgraded]) { @@ -2149,7 +2153,8 @@ Socket.prototype.connect = function connect(...args) { Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { this._handle?.close(); - this._handle = handle; + // node's TLSSocket override creates the handle itself; Bun's TLS sockets share this method. + this._handle = handle ?? newDetachedSocket(typeof this[bunTlsSymbol] === "function"); this._handle[owner_symbol] = this; initSocketHandle(this); @@ -2592,10 +2597,8 @@ Socket.prototype.resetAndDestroy = function resetAndDestroy() { Socket.prototype.setKeepAlive = function setKeepAlive(enable = false, initialDelayMsecs = 0) { enable = Boolean(enable); - // Bun's native _handle.setKeepAlive takes milliseconds; the ms→seconds - // conversion for TCP_KEEPIDLE lives in the native binding. Clamp to 0 so - // negatives and ~~ overflow match Node's no-validate behavior. - const initialDelay = MathMax(0, ~~initialDelayMsecs); + // Whole seconds, as in node. + const initialDelay = MathMax(0, ~~(initialDelayMsecs / 1000)); if (!this._handle) { this[kSetKeepAlive] = enable; @@ -2608,7 +2611,7 @@ Socket.prototype.setKeepAlive = function setKeepAlive(enable = false, initialDel if (enable !== this[kSetKeepAlive] || (enable && this[kSetKeepAliveInitialDelay] !== initialDelay)) { this[kSetKeepAlive] = enable; this[kSetKeepAliveInitialDelay] = initialDelay; - this._handle.setKeepAlive(enable, initialDelay); + this._handle.setKeepAlive(enable, keepAliveDelayMs(initialDelay)); } return this; }; @@ -3379,7 +3382,7 @@ function afterConnect(status, handle, req, readable, writable) { } if (self[kSetKeepAlive] && self._handle.setKeepAlive) { - self._handle.setKeepAlive(true, self[kSetKeepAliveInitialDelay]); + self._handle.setKeepAlive(true, keepAliveDelayMs(self[kSetKeepAliveInitialDelay])); } self.emit("connect"); @@ -3522,7 +3525,7 @@ function Server(options?, connectionListener?) { // https://github.com/nodejs/node/blob/843dc5f0d5ad/lib/net.js#L1880 this.allowHalfOpen = allowHalfOpen; this.keepAlive = Boolean(keepAlive); - this.keepAliveInitialDelay = MathMax(0, ~~keepAliveInitialDelay); + this.keepAliveInitialDelay = MathMax(0, ~~(keepAliveInitialDelay / 1000)); this.highWaterMark = highWaterMark; this.pauseOnConnect = Boolean(pauseOnConnect); this.noDelay = Boolean(noDelay); diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 5b99788ee43c..79ce31fc60b9 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -200,10 +200,13 @@ const SUPPORTED_ECDH_GROUPS = new Set([ function validateSecureContextOptions(options) { const { ciphers, + key, passphrase, ecdhCurve, minVersion, maxVersion, + privateKeyIdentifier, + privateKeyEngine, sessionTimeout, sigalgs, ticketKeys, @@ -218,6 +221,28 @@ function validateSecureContextOptions(options) { validateString(sigalgs, "options.sigalgs"); if (sigalgs === "") throw $ERR_INVALID_ARG_VALUE("options.sigalgs", sigalgs); } + // BoringSSL has no ENGINE support; same checks and order as node's no-engine build (lib/internal/tls/secure-context.js). + if (privateKeyIdentifier !== undefined && privateKeyIdentifier !== null) { + if (privateKeyEngine === undefined || privateKeyEngine === null) { + // Engine is required when privateKeyIdentifier is present + throw $ERR_INVALID_ARG_VALUE("options.privateKeyEngine", privateKeyEngine); + } + if (key) { + // Both data key and engine key can't be set at the same time + throw $ERR_INVALID_ARG_VALUE("options.privateKeyIdentifier", privateKeyIdentifier); + } + if (typeof privateKeyIdentifier === "string" && typeof privateKeyEngine === "string") { + throw $ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED("Custom engines not supported by this OpenSSL"); + } else if (typeof privateKeyIdentifier !== "string") { + throw $ERR_INVALID_ARG_TYPE( + "options.privateKeyIdentifier", + ["string", "null", "undefined"], + privateKeyIdentifier, + ); + } else { + throw $ERR_INVALID_ARG_TYPE("options.privateKeyEngine", ["string", "null", "undefined"], privateKeyEngine); + } + } if (ecdhCurve !== undefined) { validateString(ecdhCurve, "options.ecdhCurve"); if (ecdhCurve !== "auto") { @@ -638,20 +663,6 @@ var InternalSecureContext = class SecureContext { throw new TypeError("servername argument must be an string"); if (options.secureOptions != null && typeof options.secureOptions !== "number") throw new TypeError("secureOptions argument must be an number"); - const privateKeyIdentifier = options.privateKeyIdentifier; - if (!$isUndefinedOrNull(privateKeyIdentifier)) { - const privateKeyEngine = options.privateKeyEngine; - if ($isUndefinedOrNull(privateKeyEngine)) - throw $ERR_INVALID_ARG_VALUE("options.privateKeyEngine", privateKeyEngine); - if (typeof privateKeyEngine !== "string") - throw $ERR_INVALID_ARG_TYPE("options.privateKeyEngine", ["string", "null", "undefined"], privateKeyEngine); - if (typeof privateKeyIdentifier !== "string") - throw $ERR_INVALID_ARG_TYPE( - "options.privateKeyIdentifier", - ["string", "null", "undefined"], - privateKeyIdentifier, - ); - } } const requestedCiphers = options?.ciphers; if (requestedCiphers && StringPrototypeIncludes.$call(requestedCiphers, "TLS_")) { diff --git a/src/runtime/socket/tls_socket_functions.rs b/src/runtime/socket/tls_socket_functions.rs index 2070dbff8515..2259662cbb7b 100644 --- a/src/runtime/socket/tls_socket_functions.rs +++ b/src/runtime/socket/tls_socket_functions.rs @@ -423,13 +423,14 @@ pub(super) fn set_max_send_fragment( return Err(global.throw(format_args!("Expected size to be a number"))); } let size = arg.coerce_to_int64(global)?; - if !(512..=16384).contains(&size) { + if size < 0 { return Ok(JSValue::FALSE); } let Some(ssl_ptr) = this.socket.get().ssl() else { return Ok(JSValue::FALSE); }; + // Like node, return SSL_set_max_send_fragment's own verdict (BoringSSL clamps the size). Ok(JSValue::from( ffi::SSL_set_max_send_fragment( boringssl::SSL::opaque_ref(ssl_ptr), diff --git a/src/runtime/timer/ImmediateObject.rs b/src/runtime/timer/ImmediateObject.rs index c0a744cc1636..114e8e367969 100644 --- a/src/runtime/timer/ImmediateObject.rs +++ b/src/runtime/timer/ImmediateObject.rs @@ -11,6 +11,10 @@ use super::{Kind, TimerObjectInternals}; super::impl_timer_object!(ImmediateObject, ImmediateObject, "Immediate"); impl ImmediateObject { + /// `impl_timer_object!`'s `dispose` hook; an Immediate has no `_idleTimeout`. + #[inline] + pub(crate) fn before_explicit_cancel(&self, _global: &JSGlobalObject, _this_value: JSValue) {} + pub(crate) fn init( global: &JSGlobalObject, id: i32, diff --git a/src/runtime/timer/TimeoutObject.rs b/src/runtime/timer/TimeoutObject.rs index 5fd26f857fea..863d30ab507d 100644 --- a/src/runtime/timer/TimeoutObject.rs +++ b/src/runtime/timer/TimeoutObject.rs @@ -32,10 +32,21 @@ impl TimeoutObject { #[bun_jsc::host_fn(method)] pub fn close(this: &Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult { + this.before_explicit_cancel(global, frame.this()); this.internals.cancel(global.bun_vm_ptr()); Ok(frame.this()) } + /// Node's `unenroll`: an explicitly cancelled timer reads back `_idleTimeout === -1`. + pub(crate) fn mark_unenrolled(this_value: JSValue, global: &JSGlobalObject) { + js::idle_timeout_set_cached(this_value, global, JSValue::js_number(-1.0)); + } + + /// `impl_timer_object!`'s `dispose` hook. + pub(crate) fn before_explicit_cancel(&self, global: &JSGlobalObject, this_value: JSValue) { + Self::mark_unenrolled(this_value, global); + } + // Cached-property getters/setters — codegen passes `this_value` (the JS // wrapper) so the cached `WriteBarrier` slot on the C++ side can be read/written. // Signature does not match the standard `host_fn(getter/setter)` shape; the diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index fdc040a9d1f5..e4fc5fda14aa 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -398,6 +398,12 @@ impl All { }; let Some(timer) = timer else { return Ok(()) }; + if kind != Kind::SetImmediate + // SAFETY: timer points to a live TimerObjectInternals + && let Some(js_timer) = unsafe { (*timer).this_value.get().try_get() } + { + TimeoutObject::mark_unenrolled(js_timer, global_this); + } // SAFETY: timer points to a live TimerObjectInternals unsafe { (*timer).cancel(vm) }; Ok(()) diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index 0a4643f37976..bf1ffe70906a 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -249,8 +249,9 @@ macro_rules! impl_timer_object { pub fn dispose( this: &Self, global: &::bun_jsc::JSGlobalObject, - _frame: &::bun_jsc::CallFrame, + frame: &::bun_jsc::CallFrame, ) -> ::bun_jsc::JsResult<::bun_jsc::JSValue> { + this.before_explicit_cancel(global, frame.this()); this.internals.cancel(global.bun_vm_ptr()); Ok(::bun_jsc::JSValue::UNDEFINED) } diff --git a/test/js/node/net/node-net-server.test.ts b/test/js/node/net/node-net-server.test.ts index 2fb444c2e07e..3f537e6f18ff 100644 --- a/test/js/node/net/node-net-server.test.ts +++ b/test/js/node/net/node-net-server.test.ts @@ -1,6 +1,6 @@ import { realpathSync } from "fs"; import { bunEnv, bunExe, tempDir } from "harness"; -import { AddressInfo, createServer, Server, Socket } from "net"; +import { AddressInfo, connect, createServer, Server, Socket } from "net"; import { createTest } from "node-harness"; import { once } from "node:events"; import { tmpdir } from "os"; @@ -662,3 +662,30 @@ it("server.unref() on a pipe/unix-socket listener lets the process exit", async expect(exitCode === 0 ? "" : stderr).toBe(""); expect(exitCode).toBe(0); }); + +describe("keepAlive initial delay", () => { + // node stores the delay in whole seconds and passes it on to libuv without a + // ceiling; the native handle takes int32 milliseconds, so the conversion + // back must clamp instead of throwing inside the connection callbacks. + it("a delay above int32 milliseconds is stored in seconds and does not throw", async () => { + const delay = 3_000_000_000; + await using server = createServer({ keepAlive: true, keepAliveInitialDelay: delay }); + expect(server.keepAliveInitialDelay).toBe(delay / 1000); + server.on("connection", socket => socket.end("ok")); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + + const client = connect({ port: (server.address() as AddressInfo).port, host: "127.0.0.1" }); + await once(client, "connect"); + expect(client.setKeepAlive(true, delay)).toBe(client); + const [data] = await once(client, "data"); + expect(String(data)).toBe("ok"); + await once(client, "close"); + }); + + it("sub-second delays truncate to whole seconds like node", async () => { + await using server = createServer({ keepAlive: true, keepAliveInitialDelay: 1999 }); + expect(server.keepAliveInitialDelay).toBe(1); + expect(createServer({ keepAliveInitialDelay: 999 }).keepAliveInitialDelay).toBe(0); + }); +}); diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index a4ab5ba15f73..e1927e51b405 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -1494,7 +1494,23 @@ function installBunExposeInternalsShim() { const normalizedArgsSymbol = Object.getOwnPropertySymbols(probe).find( s => s.description === "normalizedArgs", ); - return { loader: "object", exports: { normalizedArgsSymbol } }; + // The per-socket option symbols are module-private in Bun's node:net, + // like normalizedArgsSymbol; recover the real ones from a probe + // socket (the constructor always sets them) and the prototype. + const socketProbe = new net.Socket(); + const bySymbolName = name => + Object.getOwnPropertySymbols(socketProbe).find(s => s.description === name) ?? + Object.getOwnPropertySymbols(net.Socket.prototype).find(s => s.description === name); + return { + loader: "object", + exports: { + normalizedArgsSymbol, + kSetNoDelay: bySymbolName("kSetNoDelay"), + kSetKeepAlive: bySymbolName("kSetKeepAlive"), + kSetKeepAliveInitialDelay: bySymbolName("kSetKeepAliveInitialDelay"), + kReinitializeHandle: bySymbolName("kReinitializeHandle"), + }, + }; }); // node's internal/options: map the few CLI options vendored http tests ask // about onto the equivalent runtime values. Unknown options return undefined. diff --git a/test/js/node/test/parallel/test-tls-clientcertengine-unsupported.js b/test/js/node/test/parallel/test-tls-clientcertengine-unsupported.js new file mode 100644 index 000000000000..0d24005dc0a1 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-clientcertengine-unsupported.js @@ -0,0 +1,30 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +// Monkey-patch SecureContext +const { internalBinding } = require('internal/test/binding'); +const binding = internalBinding('crypto'); +const NativeSecureContext = binding.SecureContext; + +binding.SecureContext = function() { + const rv = new NativeSecureContext(); + rv.setClientCertEngine = undefined; + return rv; +}; + +const tls = require('tls'); + +{ + assert.throws( + () => { tls.createSecureContext({ clientCertEngine: 'Cannonmouth' }); }, + { + code: 'ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', + message: 'Custom engines not supported by this OpenSSL' + } + ); +} diff --git a/test/js/node/test/parallel/test-tls-connect-keepalive-nodelay.js b/test/js/node/test/parallel/test-tls-connect-keepalive-nodelay.js new file mode 100644 index 000000000000..81eaafabc64a --- /dev/null +++ b/test/js/node/test/parallel/test-tls-connect-keepalive-nodelay.js @@ -0,0 +1,65 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +// This test verifies that tls.connect() forwards keepAlive, +// keepAliveInitialDelay, and noDelay options to the underlying socket. + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); +const { + kSetNoDelay, + kSetKeepAlive, + kSetKeepAliveInitialDelay, +} = require('internal/net'); + +const key = fixtures.readKey('agent1-key.pem'); +const cert = fixtures.readKey('agent1-cert.pem'); + +// Test: keepAlive, keepAliveInitialDelay, and noDelay +{ + const server = tls.createServer({ key, cert }, (socket) => { + socket.end(); + }); + + server.listen(0, common.mustCall(() => { + const socket = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + keepAlive: true, + keepAliveInitialDelay: 1000, + noDelay: true, + }, common.mustCall(() => { + assert.strictEqual(socket[kSetKeepAlive], true); + assert.strictEqual(socket[kSetKeepAliveInitialDelay], 1); + assert.strictEqual(socket[kSetNoDelay], true); + socket.destroy(); + server.close(); + })); + })); +} + +// Test: defaults (options not set) +{ + const server = tls.createServer({ key, cert }, (socket) => { + socket.end(); + }); + + server.listen(0, common.mustCall(() => { + const socket = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(socket[kSetKeepAlive], false); + assert.strictEqual(socket[kSetKeepAliveInitialDelay], 0); + assert.strictEqual(socket[kSetNoDelay], false); + socket.destroy(); + server.close(); + })); + })); +} diff --git a/test/js/node/test/parallel/test-tls-enable-trace-cli.js b/test/js/node/test/parallel/test-tls-enable-trace-cli.js new file mode 100644 index 000000000000..634ce950dade --- /dev/null +++ b/test/js/node/test/parallel/test-tls-enable-trace-cli.js @@ -0,0 +1,68 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); + +// Test --trace-tls CLI flag. + +const assert = require('assert'); +const { fork } = require('child_process'); + +if (process.argv[2] === 'test') + return test(); + +const binding = require('internal/test/binding').internalBinding; + +if (!binding('tls_wrap').HAVE_SSL_TRACE) + return common.skip('no SSL_trace() compiled into openssl'); + +const child = fork(__filename, ['test'], { + silent: true, + execArgv: ['--trace-tls'] +}); + +let stdout = ''; +let stderr = ''; +child.stdout.setEncoding('utf8'); +child.stderr.setEncoding('utf8'); +child.stdout.on('data', (data) => stdout += data); +child.stderr.on('data', (data) => stderr += data); +child.on('close', common.mustCall((code, signal) => { + // For debugging and observation of actual trace output. + console.log(stderr); + + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + assert.strictEqual(stdout.trim(), ''); + assert.match(stderr, /Warning: Enabling --trace-tls can expose sensitive/); + assert.match(stderr, /Sent (?:TLS )?Record/); +})); + +function test() { + const { + connect, keys + } = require(fixtures.path('tls-connect')); + + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key + }, + }, common.mustCall((err, pair, cleanup) => { + if (pair.server.err) { + console.trace('server', pair.server.err); + } + if (pair.client.err) { + console.trace('client', pair.client.err); + } + assert.ifError(pair.server.err); + assert.ifError(pair.client.err); + + return cleanup(); + })); +} diff --git a/test/js/node/test/parallel/test-tls-enable-trace.js b/test/js/node/test/parallel/test-tls-enable-trace.js new file mode 100644 index 000000000000..28c78e133710 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-enable-trace.js @@ -0,0 +1,58 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) common.skip('missing crypto'); +const fixtures = require('../common/fixtures'); + +// Test enableTrace: option for TLS. + +const assert = require('assert'); +const { fork } = require('child_process'); + +if (process.argv[2] === 'test') + return test(); + +const binding = require('internal/test/binding').internalBinding; + +if (!binding('tls_wrap').HAVE_SSL_TRACE) + return common.skip('no SSL_trace() compiled into openssl'); + +const child = fork(__filename, ['test'], { silent: true }); + +let stderr = ''; +child.stderr.setEncoding('utf8'); +child.stderr.on('data', (data) => stderr += data); +child.on('close', common.mustCall(() => { + assert.match(stderr, /Received (?:TLS )?Record/); + assert.match(stderr, /ClientHello/); +})); + +// For debugging and observation of actual trace output. +child.stderr.pipe(process.stderr); +child.stdout.pipe(process.stdout); + +child.on('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); +})); + +function test() { + const { + connect, keys + } = require(fixtures.path('tls-connect')); + + connect({ + client: { + checkServerIdentity: (servername, cert) => { }, + ca: `${keys.agent1.cert}\n${keys.agent6.ca}`, + }, + server: { + cert: keys.agent6.cert, + key: keys.agent6.key, + enableTrace: true, + }, + }, common.mustCall((err, pair, cleanup) => { + pair.client.conn.enableTrace(); + + return cleanup(); + })); +} diff --git a/test/js/node/test/parallel/test-tls-keyengine-unsupported.js b/test/js/node/test/parallel/test-tls-keyengine-unsupported.js new file mode 100644 index 000000000000..5fcfe495d516 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-keyengine-unsupported.js @@ -0,0 +1,35 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +// Monkey-patch SecureContext +const { internalBinding } = require('internal/test/binding'); +const binding = internalBinding('crypto'); +const NativeSecureContext = binding.SecureContext; + +binding.SecureContext = function() { + const rv = new NativeSecureContext(); + rv.setEngineKey = undefined; + return rv; +}; + +const tls = require('tls'); + +{ + assert.throws( + () => { + tls.createSecureContext({ + privateKeyEngine: 'engine', + privateKeyIdentifier: 'key' + }); + }, + { + code: 'ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', + message: 'Custom engines not supported by this OpenSSL' + } + ); +} diff --git a/test/js/node/test/parallel/test-tls-reinitialize-listeners.js b/test/js/node/test/parallel/test-tls-reinitialize-listeners.js new file mode 100644 index 000000000000..ffd7c825b0f6 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-reinitialize-listeners.js @@ -0,0 +1,42 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) { + common.skip('missing crypto'); +} + +const events = require('events'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); +const { kReinitializeHandle } = require('internal/net'); + +// Test that repeated calls to kReinitializeHandle() do not result in repeatedly +// adding new listeners on the socket (i.e. no MaxListenersExceededWarnings) + +process.on('warning', common.mustNotCall()); + +const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}); + +server.listen(0, common.mustCall(function() { + const socket = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }); + + socket.on('secureConnect', common.mustCall(function() { + for (let i = 0; i < events.defaultMaxListeners + 1; i++) { + socket[kReinitializeHandle](); + } + + socket.destroy(); + })); + + socket.on('close', function() { + server.close(); + }); +})); diff --git a/test/js/node/test/parallel/test-tls-wrap-timeout.js b/test/js/node/test/parallel/test-tls-wrap-timeout.js new file mode 100644 index 000000000000..d1e713e36dd4 --- /dev/null +++ b/test/js/node/test/parallel/test-tls-wrap-timeout.js @@ -0,0 +1,53 @@ +// Flags: --expose-internals + +'use strict'; +const common = require('../common'); +const { kTimeout, TIMEOUT_MAX } = require('internal/timers'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const tls = require('tls'); +const net = require('net'); +const fixtures = require('../common/fixtures'); + +const options = { + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') +}; + +const server = tls.createServer(options, common.mustCall((c) => { + setImmediate(() => { + c.write('hello', () => { + setImmediate(() => { + c.destroy(); + server.close(); + }); + }); + }); +})); + +let socket; +let lastIdleStart; + +server.listen(0, common.mustCall(() => { + socket = net.connect(server.address().port, common.mustCall(() => { + const s = socket.setTimeout(TIMEOUT_MAX, common.mustNotCall('timeout')); + assert.ok(s instanceof net.Socket); + + assert.notStrictEqual(socket[kTimeout]._idleTimeout, -1); + lastIdleStart = socket[kTimeout]._idleStart; + + const tsocket = tls.connect({ + socket: socket, + rejectUnauthorized: false + }); + tsocket.resume(); + })); +})); + +process.on('exit', () => { + assert.strictEqual(socket[kTimeout]._idleTimeout, -1); + assert(lastIdleStart < socket[kTimeout]._idleStart); +}); diff --git a/test/js/node/timers/node-timers.test.ts b/test/js/node/timers/node-timers.test.ts index 35b595c4d5c3..e4b5a39c2ffb 100644 --- a/test/js/node/timers/node-timers.test.ts +++ b/test/js/node/timers/node-timers.test.ts @@ -220,6 +220,40 @@ describe("clear", () => { }); }); +describe("_idleTimeout", () => { + const idleTimeoutOf = (timer: object) => (timer as { _idleTimeout: number })._idleTimeout; + + // node's unenroll() sets _idleTimeout to -1 on every explicit cancel path. + it.each([ + ["clearTimeout", (t: NodeJS.Timeout) => void clearTimeout(t)], + ["clearInterval", (t: NodeJS.Timeout) => void clearInterval(t)], + ["close()", (t: NodeJS.Timeout) => void t.close()], + ["Symbol.dispose", (t: NodeJS.Timeout) => t[Symbol.dispose]()], + ])("%s sets it to -1", (_, cancel) => { + const timeout = setTimeout(() => { + throw new Error("timeout not cleared"); + }, 1000); + expect(idleTimeoutOf(timeout)).toBe(1000); + cancel(timeout); + expect(idleTimeoutOf(timeout)).toBe(-1); + }); + + it("a timer that fired keeps its duration", async () => { + const { promise, resolve } = Promise.withResolvers(); + const timeout = setTimeout(resolve, 1); + await promise; + expect(idleTimeoutOf(timeout)).toBe(1); + }); + + it("an immediate has none, and disposing it does not add one", () => { + const immediate = setImmediate(() => { + throw new Error("immediate not cleared"); + }); + immediate[Symbol.dispose](); + expect("_idleTimeout" in immediate).toBe(false); + }); +}); + describe("_idleStart", () => { // https://github.com/oven-sh/bun/issues/26508 // Next.js 16 Cache Components writes `t2._idleStart = t1._idleStart` so two diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index 2250aad54dbe..deecc6e3120d 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -170,10 +170,13 @@ it("initializes authorizationError to null in the TLSSocket constructor", () => socket.destroy(); }); -it("setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwing", async () => { - // Node returns whatever SSL_set_max_send_fragment returns: OpenSSL rejects a - // size outside [512, 16384] with 0 (-> false). BoringSSL clamps and always - // returns 1, so bun enforces the same contract in the native binding. +it("setMaxSendFragment returns BoringSSL's verdict (clamps out-of-range sizes, reports success)", async () => { + // Node returns whatever SSL_set_max_send_fragment returns. Bun links + // BoringSSL, which clamps out-of-range sizes into [512, 16384] and reports + // success; upstream test-tls-max-send-fragment.js pins exactly this for + // process.features.openssl_is_boringssl builds. Negative sizes stay false + // (Node's C++ binding would CHECK-crash on a non-Uint32; Bun refuses + // gracefully). const server = tls.createServer(COMMON_CERT_, s => s.on("data", () => {})); await once(server.listen(0, "127.0.0.1"), "listening"); const connected = Promise.withResolvers(); @@ -186,12 +189,12 @@ it("setMaxSendFragment mirrors OpenSSL's [512, 16384] acceptance without throwin await connected.promise; const results = [0, -1, 511, 512, 16384, 16385].map(size => [size, client.setMaxSendFragment(size)]); expect(results).toEqual([ - [0, false], + [0, true], [-1, false], - [511, false], + [511, true], [512, true], [16384, true], - [16385, false], + [16385, true], ]); } finally { client.destroy();