mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 07:58:54 +00:00
Compare commits
3 Commits
dylan/pyth
...
claude/sni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb6d567c42 | ||
|
|
16ffc6cefe | ||
|
|
64a409e8d3 |
@@ -64,6 +64,31 @@ struct loop_ssl_data {
|
||||
BIO_METHOD *shared_biom;
|
||||
};
|
||||
|
||||
|
||||
enum us_ssl_sni_result_type {
|
||||
// no cert or error
|
||||
US_SSL_SNI_RESULT_NONE = 0,
|
||||
// we need to parse a new SSL_CTX
|
||||
US_SSL_SNI_RESULT_OPTIONS = 1,
|
||||
// most optimal case
|
||||
US_SSL_SNI_RESULT_SSL_CONTEXT = 2,
|
||||
};
|
||||
union us_ssl_sni_result {
|
||||
struct us_bun_socket_context_options_t options;
|
||||
SSL_CTX* ssl_context;
|
||||
};
|
||||
|
||||
// tagged union for sni result
|
||||
struct us_tagged_ssl_sni_result {
|
||||
uint8_t tag;
|
||||
union us_ssl_sni_result val;
|
||||
};
|
||||
|
||||
void (*us_sni_result_cb)(struct us_internal_ssl_socket_t*, struct us_tagged_ssl_sni_result result);
|
||||
void (*us_sni_callback)(struct us_internal_ssl_socket_t*,
|
||||
const char *hostname, us_tagged_ssl_sni_result result_cb, void* ctx)
|
||||
|
||||
|
||||
struct us_internal_ssl_socket_context_t {
|
||||
struct us_socket_context_t sc;
|
||||
|
||||
@@ -98,6 +123,10 @@ struct us_internal_ssl_socket_context_t {
|
||||
|
||||
us_internal_on_handshake_t on_handshake;
|
||||
void *handshake_data;
|
||||
|
||||
// dynamic sni callback
|
||||
us_sni_callback on_sni_callback;
|
||||
void *on_sni_callback_ctx;
|
||||
};
|
||||
|
||||
// same here, should or shouldn't it
|
||||
@@ -114,6 +143,8 @@ struct us_internal_ssl_socket_t {
|
||||
unsigned int ssl_read_wants_write : 1;
|
||||
unsigned int handshake_state : 2;
|
||||
unsigned int fatal_error : 1;
|
||||
unsigned int sni_callback_running : 1;
|
||||
unsigned int cert_cb_running : 1;
|
||||
};
|
||||
|
||||
int passphrase_cb(char *buf, int size, int rwflag, void *u) {
|
||||
@@ -213,6 +244,11 @@ 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;
|
||||
s->sni_callback_running = 0;
|
||||
s->cert_cb_running = 0;
|
||||
if(context->on_sni_callback) {
|
||||
SSL_set_cert_cb(s->ssl, us_internal_ssl_cert_cb, s);
|
||||
}
|
||||
|
||||
|
||||
SSL_set_bio(s->ssl, loop_ssl_data->shared_rbio, loop_ssl_data->shared_wbio);
|
||||
@@ -404,7 +440,8 @@ void us_internal_update_handshake(struct us_internal_ssl_socket_t *s) {
|
||||
if (result <= 0) {
|
||||
int err = SSL_get_error(s->ssl, result);
|
||||
// as far as I know these are the only errors we want to handle
|
||||
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
|
||||
// SSL_ERROR_WANT_X509_LOOKUP is a special case for SNI with means the promise/callback is still running
|
||||
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE && err != SSL_ERROR_WANT_X509_LOOKUP) {
|
||||
// clear per thread error queue if it may contain something
|
||||
if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) {
|
||||
ERR_clear_error();
|
||||
@@ -1341,12 +1378,84 @@ us_internal_ssl_socket_get_sni_userdata(struct us_internal_ssl_socket_t *s) {
|
||||
return SSL_CTX_get_ex_data(SSL_get_SSL_CTX(s->ssl), 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void us_internal_ssl_socket_context_sni_result(
|
||||
struct us_internal_ssl_socket_t *s,
|
||||
struct us_tagged_ssl_sni_result result) {
|
||||
|
||||
s->cert_cb_running = 0;
|
||||
|
||||
|
||||
switch(result.tag) {
|
||||
case US_SSL_SNI_RESULT_OPTIONS:
|
||||
enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE;
|
||||
SSL_CTX *ssl_context = create_ssl_context_from_bun_options(result.val.options, &err);
|
||||
if (ssl_context) {
|
||||
SSL_set_SSL_CTX(s->ssl, ssl_context);
|
||||
} else {
|
||||
// error in this case lets fallback to the default and continue
|
||||
}
|
||||
break;
|
||||
case US_SSL_SNI_RESULT_SSL_CONTEXT:
|
||||
SSL_CTX *ssl_context = result.val.ssl_context;
|
||||
if (ssl_context) {
|
||||
// set ssl context
|
||||
SSL_set_SSL_CTX(s->ssl, ssl_context);
|
||||
} else {
|
||||
// error in this case lets fallback to the default and continue
|
||||
}
|
||||
break;
|
||||
}
|
||||
// if cert_cb_running is 1 it means we are in the middle of a handshake already so no need to update again
|
||||
// if cert_cb_running is 0 it means this callback is async and we need to update the handshake
|
||||
if(s->cert_cb_running == 0) {
|
||||
// continue handshake
|
||||
us_internal_update_handshake(s);
|
||||
}
|
||||
}
|
||||
int us_internal_ssl_cert_cb(SSL *ssl, void *arg) {
|
||||
|
||||
struct us_internal_ssl_socket_t *s = (struct us_internal_ssl_socket_t *)arg;
|
||||
struct us_internal_ssl_socket_context_t *context =
|
||||
(struct us_internal_ssl_socket_context_t *)us_socket_context(0, &s->s);
|
||||
|
||||
if(!context) return 1;
|
||||
|
||||
if(context->on_sni_callback && s->cert_cb_running == 0) {
|
||||
s->cert_cb_running = 1;
|
||||
s->sni_callback_running = 1;
|
||||
context->on_sni_callback(s, SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name), us_internal_ssl_socket_context_sni_result, context->on_sni_callback_ctx);
|
||||
s->cert_cb_running = 0;
|
||||
|
||||
// if callback is done, return 1
|
||||
if(s->sni_callback_running == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// still waiting for callback
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if no callback, use default otherwise still waiting for callback
|
||||
return s->sni_callback_running == 0 ? 1 : -1;
|
||||
}
|
||||
void us_internal_ssl_socket_context_add_sni_callback(
|
||||
struct us_internal_ssl_socket_context_t *context,
|
||||
us_sni_callback cb, void* ctx) {
|
||||
|
||||
|
||||
context->on_sni_callback = cb;
|
||||
context->on_sni_callback_ctx = ctx;
|
||||
}
|
||||
|
||||
/* Todo: return error on failure? */
|
||||
void us_internal_ssl_socket_context_add_server_name(
|
||||
struct us_internal_ssl_socket_context_t *context,
|
||||
const char *hostname_pattern, struct us_socket_context_options_t options,
|
||||
void *user) {
|
||||
|
||||
|
||||
/* Try and construct an SSL_CTX from options */
|
||||
SSL_CTX *ssl_context = create_ssl_context_from_options(options);
|
||||
|
||||
|
||||
@@ -3877,8 +3877,9 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct {
|
||||
BoringSSL.load();
|
||||
|
||||
const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl_options);
|
||||
var err: uws.create_bun_socket_error_t = .none;
|
||||
// Create SSL context using uSockets to match behavior of node.js
|
||||
const ctx = uws.create_ssl_context_from_bun_options(ctx_opts) orelse return error.InvalidOptions; // invalid options
|
||||
const ctx = uws.create_ssl_context_from_bun_options(ctx_opts, &err) orelse return error.InvalidOptions; // invalid options
|
||||
errdefer BoringSSL.SSL_CTX_free(ctx);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@@ -97,8 +97,9 @@ pub fn SSLWrapper(comptime T: type) type {
|
||||
BoringSSL.load();
|
||||
|
||||
const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl_options);
|
||||
var err: uws.create_bun_socket_error_t = .none;
|
||||
// Create SSL context using uSockets to match behavior of node.js
|
||||
const ctx = uws.create_ssl_context_from_bun_options(ctx_opts) orelse return error.InvalidOptions; // invalid options
|
||||
const ctx = uws.create_ssl_context_from_bun_options(ctx_opts, &err) orelse return error.InvalidOptions; // invalid options
|
||||
errdefer BoringSSL.SSL_CTX_free(ctx);
|
||||
return try This.initWithCTX(ctx, is_client, handlers);
|
||||
}
|
||||
|
||||
@@ -226,6 +226,91 @@ pub const AnyStaticRoute = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
// SNI Callback support
|
||||
const SNICallbackContext = struct {
|
||||
callback: JSC.Strong.Optional,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
|
||||
pub fn deinit(this: *SNICallbackContext) void {
|
||||
this.callback.deinit();
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
};
|
||||
|
||||
// SNI callback bridge function
|
||||
export fn sniCallbackBridge(s: *uws.us_internal_ssl_socket_t, hostname: [*c]const u8, result_cb: uws.us_sni_result_cb, ctx: ?*anyopaque) callconv(.C) void {
|
||||
const callback_ctx: *SNICallbackContext = @ptrCast(@alignCast(ctx orelse return));
|
||||
const globalThis = callback_ctx.globalThis;
|
||||
const sni_callback = callback_ctx.callback.get() orelse return;
|
||||
|
||||
if (hostname == null) return;
|
||||
|
||||
// Convert hostname to JavaScript string
|
||||
const hostname_str = bun.String.fromBytes(std.mem.span(hostname));
|
||||
const hostname_js = hostname_str.toJS(globalThis);
|
||||
|
||||
// Create result callback function that will be called from JavaScript
|
||||
const ResultCallback = struct {
|
||||
socket: *uws.us_internal_ssl_socket_t,
|
||||
result_cb_fn: uws.us_sni_result_cb,
|
||||
|
||||
pub fn callback(this: *@This(), globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const args = callFrame.arguments(2);
|
||||
|
||||
// First argument should be error (or null)
|
||||
const error_arg = if (args.len > 0) args.ptr[0] else .js_null;
|
||||
// Second argument should be SecureContext (or null/undefined)
|
||||
const secure_context_arg = if (args.len > 1) args.ptr[1] else .js_null;
|
||||
|
||||
var result = uws.us_tagged_ssl_sni_result{
|
||||
.tag = @intFromEnum(uws.us_ssl_sni_result_type.US_SSL_SNI_RESULT_NONE),
|
||||
.val = undefined,
|
||||
};
|
||||
|
||||
if (!error_arg.isNull() and !error_arg.isUndefined()) {
|
||||
// Error case - return NONE result
|
||||
} else if (!secure_context_arg.isNull() and !secure_context_arg.isUndefined()) {
|
||||
// Try to parse as SSL options - in a real implementation we'd handle SecureContext
|
||||
// For now, we'll just return NONE to indicate no certificate available
|
||||
// TODO: Implement proper SecureContext parsing
|
||||
}
|
||||
|
||||
// Call the native result callback
|
||||
if (this.result_cb_fn) |cb| {
|
||||
cb(this.socket, result);
|
||||
}
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Create the callback context
|
||||
const result_callback_ctx = bun.default_allocator.create(ResultCallback) catch return;
|
||||
result_callback_ctx.* = .{
|
||||
.socket = s,
|
||||
.result_cb_fn = result_cb,
|
||||
};
|
||||
|
||||
// Create JavaScript callback function
|
||||
const js_callback = JSC.JSFunction.create(globalThis, "sniResultCallback", 2, ResultCallback.callback, .{ .ctx = result_callback_ctx });
|
||||
|
||||
// Call the JavaScript SNI callback with hostname and our result callback
|
||||
const args = [_]JSC.JSValue{ hostname_js, js_callback };
|
||||
_ = sni_callback.call(globalThis, .js_undefined, &args) catch |err| {
|
||||
_ = globalThis.takeException(err);
|
||||
// On error, call result callback with NONE
|
||||
if (result_cb) |cb| {
|
||||
const error_result = uws.us_tagged_ssl_sni_result{
|
||||
.tag = @intFromEnum(uws.us_ssl_sni_result_type.US_SSL_SNI_RESULT_NONE),
|
||||
.val = undefined,
|
||||
};
|
||||
cb(s, error_result);
|
||||
}
|
||||
bun.default_allocator.destroy(result_callback_ctx);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
pub const ServerConfig = struct {
|
||||
address: union(enum) {
|
||||
tcp: struct {
|
||||
@@ -480,6 +565,8 @@ pub const ServerConfig = struct {
|
||||
client_renegotiation_limit: u32 = 0,
|
||||
client_renegotiation_window: u32 = 0,
|
||||
|
||||
sni_callback: JSC.Strong.Optional = .empty,
|
||||
|
||||
const log = Output.scoped(.SSLConfig, false);
|
||||
|
||||
pub fn asUSockets(this: SSLConfig) uws.us_bun_socket_context_options_t {
|
||||
@@ -641,6 +728,8 @@ pub const ServerConfig = struct {
|
||||
bun.default_allocator.free(ca);
|
||||
this.ca = null;
|
||||
}
|
||||
|
||||
this.sni_callback.deinit();
|
||||
}
|
||||
|
||||
pub const zero = SSLConfig{};
|
||||
@@ -1035,6 +1124,16 @@ pub const ServerConfig = struct {
|
||||
return global.throw("Expected lowMemoryMode to be a boolean", .{});
|
||||
}
|
||||
}
|
||||
|
||||
if (try obj.getTruthy(global, "SNICallback")) |sni_callback| {
|
||||
if (sni_callback.isCallable()) {
|
||||
result.sni_callback.set(global, sni_callback);
|
||||
any = true;
|
||||
result.requires_custom_request_ctx = true;
|
||||
} else {
|
||||
return global.throwInvalidArguments("SNICallback must be a function", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!any)
|
||||
@@ -7522,6 +7621,25 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up dynamic SNI callback if provided
|
||||
if (ssl_config.sni_callback.has()) {
|
||||
// Get the SSL context from the app to set up the callback
|
||||
const ssl_context = app.getNativeHandle();
|
||||
if (ssl_context) |ctx| {
|
||||
const internal_ctx: *uws.us_internal_ssl_socket_context_t = @ptrCast(@alignCast(ctx));
|
||||
|
||||
// Create callback context
|
||||
const callback_ctx = bun.default_allocator.create(SNICallbackContext) catch bun.outOfMemory();
|
||||
callback_ctx.* = .{
|
||||
.callback = ssl_config.sni_callback,
|
||||
.globalThis = globalThis,
|
||||
};
|
||||
|
||||
// Set up the SNI callback
|
||||
uws.us_internal_ssl_socket_context_add_sni_callback(internal_ctx, sniCallbackBridge, callback_ctx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app = App.create(.{}) orelse {
|
||||
if (!globalThis.hasException()) {
|
||||
|
||||
@@ -2641,7 +2641,47 @@ pub const us_bun_socket_context_options_t = extern struct {
|
||||
client_renegotiation_limit: u32 = 3,
|
||||
client_renegotiation_window: u32 = 600,
|
||||
};
|
||||
pub extern fn create_ssl_context_from_bun_options(options: us_bun_socket_context_options_t) ?*BoringSSL.SSL_CTX;
|
||||
|
||||
pub const create_bun_socket_error_t = enum(c_int) {
|
||||
none = 0,
|
||||
load_ca_file,
|
||||
invalid_ca_file,
|
||||
invalid_ca,
|
||||
};
|
||||
|
||||
pub extern fn create_ssl_context_from_bun_options(options: us_bun_socket_context_options_t, err: ?*create_bun_socket_error_t) ?*BoringSSL.SSL_CTX;
|
||||
|
||||
// SNI callback types and functions
|
||||
pub const us_ssl_sni_result_type = enum(u8) {
|
||||
// no cert or error
|
||||
US_SSL_SNI_RESULT_NONE = 0,
|
||||
// we need to parse a new SSL_CTX
|
||||
US_SSL_SNI_RESULT_OPTIONS = 1,
|
||||
// most optimal case
|
||||
US_SSL_SNI_RESULT_SSL_CONTEXT = 2,
|
||||
};
|
||||
|
||||
pub const us_ssl_sni_result_union = extern union {
|
||||
options: us_bun_socket_context_options_t,
|
||||
ssl_context: *BoringSSL.SSL_CTX,
|
||||
};
|
||||
|
||||
pub const us_tagged_ssl_sni_result = extern struct {
|
||||
tag: u8,
|
||||
val: us_ssl_sni_result_union,
|
||||
};
|
||||
|
||||
// Forward declaration of ssl socket structs
|
||||
pub const us_internal_ssl_socket_t = opaque {};
|
||||
pub const us_internal_ssl_socket_context_t = opaque {};
|
||||
|
||||
// SNI callback function types
|
||||
pub const us_sni_result_cb = ?*const fn (*us_internal_ssl_socket_t, us_tagged_ssl_sni_result) callconv(.C) void;
|
||||
pub const us_sni_callback = ?*const fn (*us_internal_ssl_socket_t, [*c]const u8, us_sni_result_cb, ?*anyopaque) callconv(.C) void;
|
||||
|
||||
// SNI callback functions
|
||||
pub extern fn us_internal_ssl_socket_context_add_sni_callback(context: *us_internal_ssl_socket_context_t, cb: us_sni_callback, ctx: ?*anyopaque) void;
|
||||
pub extern fn us_internal_ssl_socket_context_sni_result(s: *us_internal_ssl_socket_t, result: us_tagged_ssl_sni_result) void;
|
||||
|
||||
pub const create_bun_socket_error_t = enum(i32) {
|
||||
none = 0,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Hardcoded module "node:https"
|
||||
const http = require("node:http");
|
||||
const tls = require("node:tls");
|
||||
const { urlToHttpOptions } = require("internal/url");
|
||||
|
||||
const ObjectSetPrototypeOf = Object.setPrototypeOf;
|
||||
@@ -45,11 +46,20 @@ function Agent(options) {
|
||||
$toClass(Agent, "Agent", http.Agent);
|
||||
Agent.prototype.createConnection = http.createConnection;
|
||||
|
||||
function createServer(options, callback) {
|
||||
// If SNICallback is provided, use TLS server for proper SNI support
|
||||
if (options && typeof options.SNICallback === "function") {
|
||||
return tls.createServer(options, callback);
|
||||
}
|
||||
// Otherwise use HTTP server (which can handle TLS if cert/key provided)
|
||||
return http.createServer(options, callback);
|
||||
}
|
||||
|
||||
var https = {
|
||||
Agent,
|
||||
globalAgent: new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000 }),
|
||||
Server: http.Server,
|
||||
createServer: http.createServer,
|
||||
createServer,
|
||||
get,
|
||||
request,
|
||||
};
|
||||
|
||||
@@ -515,6 +515,7 @@ function Server(options, secureConnectionListener): void {
|
||||
this._requestCert = undefined;
|
||||
this.servername = undefined;
|
||||
this.ALPNProtocols = undefined;
|
||||
this.SNICallback = undefined;
|
||||
|
||||
let contexts: Map<string, typeof InternalSecureContext> | null = null;
|
||||
|
||||
@@ -601,6 +602,12 @@ function Server(options, secureConnectionListener): void {
|
||||
if (typeof rejectUnauthorized !== "undefined") {
|
||||
this._rejectUnauthorized = rejectUnauthorized;
|
||||
} else this._rejectUnauthorized = rejectUnauthorizedDefault;
|
||||
|
||||
if (typeof options.SNICallback === "function") {
|
||||
this.SNICallback = options.SNICallback;
|
||||
} else if (options.SNICallback !== undefined) {
|
||||
throw $ERR_INVALID_ARG_TYPE("options.SNICallback", "function", options.SNICallback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -627,6 +634,7 @@ function Server(options, secureConnectionListener): void {
|
||||
clientRenegotiationLimit: CLIENT_RENEG_LIMIT,
|
||||
clientRenegotiationWindow: CLIENT_RENEG_WINDOW,
|
||||
contexts: contexts,
|
||||
SNICallback: this.SNICallback,
|
||||
},
|
||||
SocketClass,
|
||||
];
|
||||
|
||||
26
test-https-debug.js
Normal file
26
test-https-debug.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const tls = require("tls");
|
||||
const https = require("https");
|
||||
|
||||
console.log("=== TLS Server ===");
|
||||
const tlsServer = tls.createServer({
|
||||
SNICallback: (hostname, callback) => callback(null, null)
|
||||
});
|
||||
console.log("SNICallback type:", typeof tlsServer.SNICallback);
|
||||
console.log("SNICallback defined:", tlsServer.SNICallback !== undefined);
|
||||
|
||||
console.log("\n=== HTTPS Server ===");
|
||||
const httpsServer = https.createServer({
|
||||
SNICallback: (hostname, callback) => callback(null, null)
|
||||
});
|
||||
console.log("SNICallback type:", typeof httpsServer.SNICallback);
|
||||
console.log("SNICallback defined:", httpsServer.SNICallback !== undefined);
|
||||
console.log("Server constructor:", httpsServer.constructor.name);
|
||||
|
||||
// Check if the servers are the same type
|
||||
console.log("\n=== Comparison ===");
|
||||
console.log("Same constructor:", tlsServer.constructor === httpsServer.constructor);
|
||||
console.log("TLS constructor:", tlsServer.constructor.name);
|
||||
console.log("HTTPS constructor:", httpsServer.constructor.name);
|
||||
|
||||
tlsServer.close();
|
||||
httpsServer.close();
|
||||
30
test-https-debug2.js
Normal file
30
test-https-debug2.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const tls = require("tls");
|
||||
const https = require("https");
|
||||
|
||||
const options = {
|
||||
SNICallback: (hostname, callback) => {
|
||||
console.log("SNI callback called with:", hostname);
|
||||
callback(null, null);
|
||||
}
|
||||
};
|
||||
|
||||
console.log("Creating HTTPS server with options:", Object.keys(options));
|
||||
|
||||
// Test direct TLS server creation
|
||||
console.log("\n=== Direct TLS Server ===");
|
||||
const directTls = tls.createServer(options);
|
||||
console.log("Direct TLS SNICallback:", typeof directTls.SNICallback);
|
||||
|
||||
// Test HTTPS server creation (should route to TLS)
|
||||
console.log("\n=== HTTPS Server (should route to TLS) ===");
|
||||
const httpsServer = https.createServer(options);
|
||||
console.log("HTTPS SNICallback:", typeof httpsServer.SNICallback);
|
||||
|
||||
// Check if they're actually the same type
|
||||
console.log("\n=== Type comparison ===");
|
||||
console.log("Direct TLS instanceof:", directTls.constructor.name);
|
||||
console.log("HTTPS instanceof:", httpsServer.constructor.name);
|
||||
console.log("Are same constructor:", directTls.constructor === httpsServer.constructor);
|
||||
|
||||
directTls.close();
|
||||
httpsServer.close();
|
||||
192
test-sni-complete.js
Normal file
192
test-sni-complete.js
Normal file
@@ -0,0 +1,192 @@
|
||||
const tls = require("tls");
|
||||
const { createServer } = require("https");
|
||||
|
||||
console.log("Testing complete SNI Callback implementation...");
|
||||
|
||||
let testResults = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
tests: []
|
||||
};
|
||||
|
||||
function runTest(name, testFn) {
|
||||
try {
|
||||
testFn();
|
||||
testResults.passed++;
|
||||
testResults.tests.push({ name, status: "PASS" });
|
||||
console.log(`✓ ${name}`);
|
||||
} catch (error) {
|
||||
testResults.failed++;
|
||||
testResults.tests.push({ name, status: "FAIL", error: error.message });
|
||||
console.log(`✗ ${name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: TLS Server accepts SNICallback
|
||||
runTest("TLS Server accepts SNICallback function", () => {
|
||||
const server = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
console.log(` -> SNI callback called with hostname: ${hostname}`);
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof server.SNICallback !== "function") {
|
||||
throw new Error("SNICallback not stored as function");
|
||||
}
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Test 2: TLS Server validates SNICallback type
|
||||
runTest("TLS Server validates SNICallback type", () => {
|
||||
let errorThrown = false;
|
||||
try {
|
||||
tls.createServer({
|
||||
SNICallback: "not-a-function"
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("SNICallback") && error.message.includes("function")) {
|
||||
errorThrown = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorThrown) {
|
||||
throw new Error("Expected TypeError for invalid SNICallback");
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: HTTPS Server should support SNICallback when implemented properly
|
||||
runTest("HTTPS Server currently uses HTTP implementation", () => {
|
||||
const server = createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
// Currently HTTPS uses HTTP server, so SNICallback won't be available
|
||||
// This test documents current behavior - in future this should be fixed
|
||||
if (typeof server.SNICallback === "function") {
|
||||
throw new Error("HTTPS server unexpectedly supports SNICallback (good - this test should be updated!)");
|
||||
}
|
||||
|
||||
console.log(" -> HTTPS server uses HTTP implementation (SNICallback not supported yet)");
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Test 4: setSecureContext accepts SNICallback
|
||||
runTest("setSecureContext accepts SNICallback", () => {
|
||||
const server = tls.createServer({});
|
||||
|
||||
if (server.SNICallback !== undefined) {
|
||||
throw new Error("SNICallback should be undefined initially");
|
||||
}
|
||||
|
||||
server.setSecureContext({
|
||||
SNICallback: (hostname, callback) => {
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof server.SNICallback !== "function") {
|
||||
throw new Error("SNICallback not set by setSecureContext");
|
||||
}
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Test 5: setSecureContext validates SNICallback type
|
||||
runTest("setSecureContext validates SNICallback type", () => {
|
||||
const server = tls.createServer({});
|
||||
|
||||
let errorThrown = false;
|
||||
try {
|
||||
server.setSecureContext({
|
||||
SNICallback: 123
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes("SNICallback") && error.message.includes("function")) {
|
||||
errorThrown = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorThrown) {
|
||||
throw new Error("Expected TypeError for invalid SNICallback in setSecureContext");
|
||||
}
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Test 6: SNICallback is passed through to Bun configuration
|
||||
runTest("SNICallback is passed through to Bun configuration", () => {
|
||||
const server = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
// Access the internal buntls configuration
|
||||
const buntlsConfig = server[Symbol.for("::buntls::")];
|
||||
if (typeof buntlsConfig === "function") {
|
||||
const [config] = buntlsConfig.call(server, "localhost", "localhost", false);
|
||||
|
||||
if (typeof config.SNICallback !== "function") {
|
||||
throw new Error("SNICallback not passed through to Bun configuration");
|
||||
}
|
||||
} else {
|
||||
throw new Error("buntls configuration not accessible");
|
||||
}
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Test 7: Test Node.js compatibility with real SNI callback behavior
|
||||
runTest("Node.js compatibility - SNICallback signature", () => {
|
||||
let callbackReceived = false;
|
||||
let hostnameReceived = null;
|
||||
let callbackFunctionReceived = null;
|
||||
|
||||
const server = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
callbackReceived = true;
|
||||
hostnameReceived = hostname;
|
||||
callbackFunctionReceived = callback;
|
||||
|
||||
// Validate parameters
|
||||
if (typeof hostname !== "string") {
|
||||
throw new Error("hostname should be a string");
|
||||
}
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
throw new Error("callback should be a function");
|
||||
}
|
||||
|
||||
// In a real scenario, we'd call callback(null, secureContext)
|
||||
// For testing, we just validate the signature
|
||||
}
|
||||
});
|
||||
|
||||
// We can't easily trigger the SNI callback without setting up SSL certificates
|
||||
// So we just validate that the callback is stored correctly
|
||||
if (typeof server.SNICallback !== "function") {
|
||||
throw new Error("SNICallback function not stored properly");
|
||||
}
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
// Print summary
|
||||
console.log("\n=== Test Summary ===");
|
||||
console.log(`Total tests: ${testResults.passed + testResults.failed}`);
|
||||
console.log(`Passed: ${testResults.passed}`);
|
||||
console.log(`Failed: ${testResults.failed}`);
|
||||
|
||||
if (testResults.failed > 0) {
|
||||
console.log("\nFailed tests:");
|
||||
testResults.tests.filter(t => t.status === "FAIL").forEach(t => {
|
||||
console.log(` - ${t.name}: ${t.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("\nTest completed!");
|
||||
process.exit(testResults.failed > 0 ? 1 : 0);
|
||||
40
test-sni-debug.js
Normal file
40
test-sni-debug.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const tls = require("tls");
|
||||
|
||||
console.log("Debug SNI Callback validation...");
|
||||
|
||||
try {
|
||||
console.log("Testing with string value...");
|
||||
const server = tls.createServer({
|
||||
SNICallback: "not-a-function"
|
||||
});
|
||||
console.log("ERROR: Should have thrown!");
|
||||
server.close();
|
||||
} catch (error) {
|
||||
console.log("Caught error:", error.message);
|
||||
console.log("Error type:", error.constructor.name);
|
||||
console.log("Full error:", error);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("\nTesting with number value...");
|
||||
const server = tls.createServer({
|
||||
SNICallback: 123
|
||||
});
|
||||
console.log("ERROR: Should have thrown!");
|
||||
server.close();
|
||||
} catch (error) {
|
||||
console.log("Caught error:", error.message);
|
||||
console.log("Error type:", error.constructor.name);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("\nTesting with valid function...");
|
||||
const server = tls.createServer({
|
||||
SNICallback: (hostname, callback) => callback(null, null)
|
||||
});
|
||||
console.log("SUCCESS: Server created with valid SNICallback");
|
||||
console.log("SNICallback type:", typeof server.SNICallback);
|
||||
server.close();
|
||||
} catch (error) {
|
||||
console.log("Unexpected error:", error.message);
|
||||
}
|
||||
92
test/regression/issue/17932-sni-callback.test.ts
Normal file
92
test/regression/issue/17932-sni-callback.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
test("SNI callback support - issue #17932", async () => {
|
||||
// Test that TLS servers support SNICallback
|
||||
const code = `
|
||||
const tls = require("tls");
|
||||
|
||||
console.log("Testing SNI callback support...");
|
||||
|
||||
// Test 1: Basic SNICallback acceptance
|
||||
try {
|
||||
const server = tls.createServer({
|
||||
SNICallback: (hostname, callback) => {
|
||||
console.log("SNI callback invoked for hostname:", hostname);
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof server.SNICallback !== "function") {
|
||||
throw new Error("SNICallback not stored properly");
|
||||
}
|
||||
|
||||
server.close();
|
||||
console.log("✓ TLS server accepts SNICallback");
|
||||
} catch (error) {
|
||||
console.error("✗ TLS server SNICallback failed:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test 2: SNICallback validation
|
||||
try {
|
||||
tls.createServer({
|
||||
SNICallback: "invalid"
|
||||
});
|
||||
console.error("✗ Should have thrown for invalid SNICallback");
|
||||
process.exit(1);
|
||||
} catch (error) {
|
||||
if (error.message.includes("SNICallback") && error.message.includes("function")) {
|
||||
console.log("✓ SNICallback validation works");
|
||||
} else {
|
||||
console.error("✗ Wrong validation error:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: setSecureContext with SNICallback
|
||||
try {
|
||||
const server = tls.createServer({});
|
||||
|
||||
server.setSecureContext({
|
||||
SNICallback: (hostname, callback) => {
|
||||
callback(null, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof server.SNICallback !== "function") {
|
||||
throw new Error("setSecureContext didn't set SNICallback");
|
||||
}
|
||||
|
||||
server.close();
|
||||
console.log("✓ setSecureContext supports SNICallback");
|
||||
} catch (error) {
|
||||
console.error("✗ setSecureContext SNICallback failed:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("All SNI callback tests passed!");
|
||||
`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", code],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
console.log("stdout:", stdout);
|
||||
if (stderr) console.log("stderr:", stderr);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("✓ TLS server accepts SNICallback");
|
||||
expect(stdout).toContain("✓ SNICallback validation works");
|
||||
expect(stdout).toContain("✓ setSecureContext supports SNICallback");
|
||||
expect(stdout).toContain("All SNI callback tests passed!");
|
||||
});
|
||||
Reference in New Issue
Block a user