diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 1d6551f4867b..b013827c746a 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -48,6 +48,37 @@ fn selectALPNCallback(_: ?*BoringSSL.SSL, out: [*c][*c]const u8, outlen: [*c]u8, } } +const kMaxSessionSize = 10 * 1024; +fn newSessionCallback(ssl: ?*BoringSSL.SSL, session: ?*BoringSSL.SSL_SESSION) callconv(.C) c_int { + const this = bun.cast(*TLSSocket, BoringSSL.SSL_get_app_data(ssl)); + const globalThis = this.handlers.globalObject; + + // TODO: only do this if there is a session listener in JS; otherwise it's a waste of time + const size: c_int = BoringSSL.i2d_SSL_SESSION(session, null); + if (size > 0 and size < kMaxSessionSize) { + const thisValue = this.getThisValue(globalThis); + const vm = globalThis.bunVM(); + vm.eventLoop().enter(); + defer vm.eventLoop().exit(); + + const arraybuffer, const slice = JSC.ArrayBuffer.alloc(globalThis, .ArrayBuffer, @intCast(size)) catch |err| { + _ = this.handlers.callErrorHandler(thisValue, &.{ thisValue, globalThis.takeError(err) }); + return 0; + }; + var ticket_cptr: [*c]u8 = slice.ptr; + const write_len = BoringSSL.i2d_SSL_SESSION(session, &ticket_cptr); + if (write_len > 0) { + bun.assert(write_len == slice.len); + _ = this.handlers.onSession.call(globalThis, thisValue, &.{ thisValue, arraybuffer }) catch |err| { + _ = this.handlers.callErrorHandler(thisValue, &.{ thisValue, globalThis.takeError(err) }); + return 0; + }; + } + } + + return 0; +} + pub const Handlers = @import("socket/Handlers.zig"); pub const SocketConfig = Handlers.SocketConfig; @@ -439,11 +470,17 @@ pub fn NewSocket(comptime ssl: bool) type { } if (this.protos) |protos| { if (this.handlers.is_server) { - BoringSSL.SSL_CTX_set_alpn_select_cb(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), selectALPNCallback, bun.cast(*anyopaque, this)); + BoringSSL.SSL_CTX_set_alpn_select_cb(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), selectALPNCallback, this); } else { _ = BoringSSL.SSL_set_alpn_protos(ssl_ptr, protos.ptr, @as(c_uint, @intCast(protos.len))); } } + + if (!this.handlers.is_server and this.handlers.onSession != .zero) { + _ = BoringSSL.SSL_CTX_set_session_cache_mode(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), BoringSSL.SSL_SESS_CACHE_CLIENT | BoringSSL.SSL_SESS_CACHE_NO_INTERNAL); + _ = BoringSSL.SSL_set_app_data(ssl_ptr, @as(*anyopaque, this)); + BoringSSL.SSL_CTX_sess_set_new_cb(BoringSSL.SSL_get_SSL_CTX(ssl_ptr), &newSessionCallback); + } } } } @@ -1485,6 +1522,7 @@ pub fn NewSocket(comptime ssl: bool) type { pub const onConnectError = NewSocket(false).onConnectError; pub const onEnd = NewSocket(false).onEnd; pub const onHandshake = NewSocket(false).onHandshake; + pub const onSession = NewSocket(false).onSession; }, ); @@ -1530,6 +1568,7 @@ pub fn NewSocket(comptime ssl: bool) type { .onEnd = this.handlers.onEnd, .onError = this.handlers.onError, .onHandshake = this.handlers.onHandshake, + .onSession = this.handlers.onSession, .binary_type = this.handlers.binary_type, .is_server = this.handlers.is_server, }; diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index ce37da515f8a..040e0d686843 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -9,6 +9,7 @@ onConnectError: JSC.JSValue = .zero, onEnd: JSC.JSValue = .zero, onError: JSC.JSValue = .zero, onHandshake: JSC.JSValue = .zero, +onSession: JSC.JSValue = .zero, binary_type: BinaryType = .Buffer, @@ -127,6 +128,7 @@ pub fn fromJS(globalObject: *JSC.JSGlobalObject, opts: JSC.JSValue, is_server: b .{ "onEnd", "end" }, .{ "onError", "error" }, .{ "onHandshake", "handshake" }, + .{ "onSession", "session" }, }; inline for (pairs) |pair| { if (try opts.getTruthyComptime(globalObject, pair.@"1")) |callback_value| { @@ -173,6 +175,7 @@ pub fn unprotect(this: *Handlers) void { this.onEnd.unprotect(); this.onError.unprotect(); this.onHandshake.unprotect(); + this.onSession.unprotect(); } pub fn protect(this: *Handlers) void { @@ -188,6 +191,7 @@ pub fn protect(this: *Handlers) void { this.onEnd.protect(); this.onError.protect(); this.onHandshake.protect(); + this.onSession.protect(); } const BinaryType = JSC.ArrayBuffer.BinaryType; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 615d9486dea9..423aee447295 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3193,23 +3193,14 @@ void GlobalObject::finishCreation(VM& vm) [](const JSC::LazyProperty::Initializer& init) { auto* global = init.owner; auto& vm = init.vm; - auto scope = DECLARE_THROW_SCOPE(vm); - - // if we get the termination exception, we'd still like to set a non-null Map so that - // we don't segfault - auto setEmpty = [&]() { - ASSERT(scope.exception()); - init.set(JSC::JSMap::create(init.vm, init.owner->mapStructure())); - }; + auto scope = DECLARE_CATCH_SCOPE(vm); JSMap* registry = nullptr; auto loaderValue = global->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "Loader"_s)); scope.assertNoExceptionExceptTermination(); - RETURN_IF_EXCEPTION(scope, setEmpty()); if (loaderValue) { auto registryValue = loaderValue.getObject()->getIfPropertyExists(global, JSC::Identifier::fromString(vm, "registry"_s)); scope.assertNoExceptionExceptTermination(); - RETURN_IF_EXCEPTION(scope, setEmpty()); if (registryValue) { registry = jsCast(registryValue); } diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 456ec1309990..ed013a08d146 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -641,6 +641,14 @@ const SocketHandlers2: SocketHandler { + socket.end('Goodbye'); +}, 2)); + +// start listening +server.listen(0, common.mustCall(function() { + let sessionx = null; // From right after connect, invalid for TLS1.3 + let session1 = null; // Delivered by the session event, always valid. + let sessions = 0; + let tls13; + const client1 = tls.connect({ + port: this.address().port, + rejectUnauthorized: false + }, common.mustCall(() => { + tls13 = client1.getProtocol() === 'TLSv1.3'; + assert.strictEqual(client1.isSessionReused(), false); + sessionx = client1.getSession(); + assert(sessionx); + + if (session1) + reconnect(); + })); + + client1.on('data', common.mustCall()); + + client1.once('session', common.mustCall((session) => { + console.log('session1'); + session1 = session; + assert(session1); + if (sessionx) + reconnect(); + })); + + client1.on('session', () => { + console.log('client1 session#', ++sessions); + }); + + client1.on('close', () => { + console.log('client1 close'); + assert.strictEqual(sessions, tls13 ? 2 : 1); + }); + + function reconnect() { + assert(sessionx); + assert(session1); + if (tls13) + // For TLS1.3, the session immediately after handshake is a dummy, + // unresumable session. The one delivered later in session event is + // resumable. + assert.notStrictEqual(sessionx.compare(session1), 0); + else + // For TLS1.2, they are identical. + assert.strictEqual(sessionx.compare(session1), 0); + + const opts = { + port: server.address().port, + rejectUnauthorized: false, + session: session1, + }; + + const client2 = tls.connect(opts, common.mustCall(() => { + console.log('connect2'); + assert.strictEqual(client2.isSessionReused(), true); + })); + + client2.on('close', common.mustCall(() => { + console.log('close2'); + server.close(); + })); + + client2.resume(); + } + + client1.resume(); +})); diff --git a/test/js/node/test/parallel/test-tls-secure-session.js b/test/js/node/test/parallel/test-tls-secure-session.js new file mode 100644 index 000000000000..b4b9638a2ccc --- /dev/null +++ b/test/js/node/test/parallel/test-tls-secure-session.js @@ -0,0 +1,46 @@ +'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 options = { + key: fixtures.readKey('agent1-key.pem'), + + // NOTE: Certificate Common Name is 'agent1' + cert: fixtures.readKey('agent1-cert.pem'), + + // NOTE: TLS 1.3 creates new session ticket **after** handshake so + // `getSession()` output will be different even if the session was reused + // during the handshake. + secureProtocol: 'TLSv1_2_method' +}; + +const server = tls.createServer(options, common.mustCall((socket) => { + socket.end(); +})).listen(0, common.mustCall(() => { + let connected = false; + let session = null; + + const client = tls.connect({ + rejectUnauthorized: false, + port: server.address().port, + }, common.mustCall(() => { + assert(!connected); + assert(!session); + + connected = true; + })); + + client.on('session', common.mustCall((newSession) => { + assert(connected); + assert(!session); + + session = newSession; + + client.end(); + server.close(); + })); +}));