Compare commits

...

12 Commits

Author SHA1 Message Date
pfg
9350959792 exception check fix 1 2025-06-23 20:44:59 -07:00
pfg
ef7f3ed95c Merge branch 'main' into pfg/tls-secure-session-2 2025-06-23 20:37:48 -07:00
pfg
a60c56e587 test-tls-client-resume 2025-06-04 20:45:26 -07:00
pfg
6989f72348 . 2025-06-04 19:45:59 -07:00
pfg
f95f46d593 remove repro 2025-06-04 19:45:11 -07:00
pfg
098398f2d0 test-tls-secure-session 2025-06-04 19:38:49 -07:00
pfg
6577e17fff part 1/2: impl session events 2025-06-04 19:35:59 -07:00
pfg
a9aedbfaf7 . 2025-06-04 16:26:11 -07:00
pfg
7cb776968e ? 2025-06-04 16:17:27 -07:00
pfg
941e033e71 wip.2 2025-06-04 14:45:50 -07:00
pfg
8d988d9738 wip.1 2025-06-04 14:11:33 -07:00
pfg
712b29afe9 test 2025-06-03 15:27:33 -07:00
6 changed files with 214 additions and 11 deletions

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -3193,23 +3193,14 @@ void GlobalObject::finishCreation(VM& vm)
[](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSMap>::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<JSC::JSMap*>(registryValue);
}

View File

@@ -641,6 +641,14 @@ const SocketHandlers2: SocketHandler<NonNullable<import("node:net").Socket["_han
req!.oncomplete(error.errno, self._handle, req, true, true);
socket.data.req = undefined;
},
session(socket, session) {
const { self } = socket.data;
const bunTLS = self[bunTlsSymbol];
const isTLS = typeof bunTLS === "function";
if (isTLS) {
self.emit("session", session);
}
},
};
function kConnectTcp(self, addressType, req, address, port) {

View File

@@ -0,0 +1,115 @@
// 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';
// Check that the ticket from the first connection causes session resumption
// when used to make a second connection.
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('agent2-key.pem'),
cert: fixtures.readKey('agent2-cert.pem')
};
// create server
const server = tls.Server(options, common.mustCall((socket) => {
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();
}));

View File

@@ -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();
}));
}));