mirror of
https://github.com/oven-sh/bun
synced 2026-02-28 12:31:00 +01:00
## Summary - **Enable keepalive for custom TLS configs (mTLS):** Previously, all connections using custom TLS configurations (client certificates, custom CA, etc.) had `disable_keepalive=true` forced, causing a new TCP+TLS handshake on every request. This removes that restriction and properly tracks SSL contexts per connection. - **Intern SSLConfig with reference counting:** Identical TLS configurations are now deduplicated via a global registry (`SSLConfig.GlobalRegistry`), enabling O(1) pointer-equality lookups instead of O(n) content comparisons. Uses `ThreadSafeRefCount` for safe lifetime management across threads. - **Bounded SSL context cache with LRU eviction:** The custom SSL context map in `HTTPThread` is now bounded (max 60 entries, 30-minute TTL) with proper cleanup of both SSL contexts and their associated SSLConfig references when evicted. - **Correct keepalive pool isolation:** Pooled sockets now track their `ssl_config` (with refcount) and `owner` context, ensuring connections are only reused when the TLS configuration matches exactly, and sockets return to the correct pool on release. Fixes #27358 ## Changed files - `src/bun.js/api/server/SSLConfig.zig` — ref counting, content hashing, GlobalRegistry interning - `src/bun.js/webcore/fetch.zig` — intern SSLConfig on creation, deref on cleanup - `src/http.zig` — `custom_ssl_ctx` field, `getSslCtx()` helper, updated all callback sites - `src/http/HTTPContext.zig` — `ssl_config`/`owner` on PooledSocket, pointer-equality matching - `src/http/HTTPThread.zig` — `SslContextCacheEntry` with timestamps, TTL + LRU eviction ## Test plan - [x] `test/regression/issue/27358.test.ts` — verifies keepalive connection reuse with custom TLS and isolation between different configs - [x] `test/js/bun/http/tls-keepalive.test.ts` — comprehensive tests: keepalive reuse, config isolation, stress test (50 sequential requests), keepalive-disabled control - [x] `test/js/bun/http/tls-keepalive-leak-fixture.js` — memory leak detection fixture (50k requests with same config, 200 requests with distinct configs) ## Changelog <!-- CHANGELOG:START --> Fixed a bug where HTTP connections using custom TLS configurations (mTLS, custom CA certificates) could not reuse keepalive connections, causing a new TCP+TLS handshake for every request and leaking SSL contexts. Custom TLS connections now properly participate in keepalive pooling with correct isolation between different configurations. <!-- CHANGELOG:END --> 🤖 Generated with [Claude Code](https://claude.com/claude-code) (0% 16-shotted by claude-opus-4-6, 3 memories recalled) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
539 lines
19 KiB
Zig
539 lines
19 KiB
Zig
const SSLConfig = @This();
|
|
|
|
server_name: ?[*:0]const u8 = null,
|
|
|
|
key_file_name: ?[*:0]const u8 = null,
|
|
cert_file_name: ?[*:0]const u8 = null,
|
|
|
|
ca_file_name: ?[*:0]const u8 = null,
|
|
dh_params_file_name: ?[*:0]const u8 = null,
|
|
|
|
passphrase: ?[*:0]const u8 = null,
|
|
|
|
key: ?[][*:0]const u8 = null,
|
|
cert: ?[][*:0]const u8 = null,
|
|
ca: ?[][*:0]const u8 = null,
|
|
|
|
secure_options: u32 = 0,
|
|
request_cert: i32 = 0,
|
|
reject_unauthorized: i32 = 0,
|
|
ssl_ciphers: ?[*:0]const u8 = null,
|
|
protos: ?[*:0]const u8 = null,
|
|
client_renegotiation_limit: u32 = 0,
|
|
client_renegotiation_window: u32 = 0,
|
|
requires_custom_request_ctx: bool = false,
|
|
is_using_default_ciphers: bool = true,
|
|
low_memory_mode: bool = false,
|
|
ref_count: RC = .init(),
|
|
cached_hash: u64 = 0,
|
|
|
|
const RC = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", destroy, .{});
|
|
pub const ref = RC.ref;
|
|
pub const deref = RC.deref;
|
|
|
|
const ReadFromBlobError = bun.JSError || error{
|
|
NullStore,
|
|
NotAFile,
|
|
EmptyFile,
|
|
};
|
|
|
|
fn readFromBlob(
|
|
global: *jsc.JSGlobalObject,
|
|
blob: *bun.webcore.Blob,
|
|
) ReadFromBlobError![:0]const u8 {
|
|
const store = blob.store orelse return error.NullStore;
|
|
const file = switch (store.data) {
|
|
.file => |f| f,
|
|
else => return error.NotAFile,
|
|
};
|
|
var fs: jsc.Node.fs.NodeFS = .{};
|
|
const maybe = fs.readFileWithOptions(
|
|
.{ .path = file.pathlike },
|
|
.sync,
|
|
.null_terminated,
|
|
);
|
|
const result = switch (maybe) {
|
|
.result => |result| result,
|
|
.err => |err| return global.throwValue(try err.toJS(global)),
|
|
};
|
|
if (result.null_terminated.len == 0) return error.EmptyFile;
|
|
return bun.default_allocator.dupeZ(u8, result.null_terminated);
|
|
}
|
|
|
|
pub fn asUSockets(this: *const SSLConfig) uws.SocketContext.BunSocketContextOptions {
|
|
var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{};
|
|
|
|
if (this.key_file_name != null)
|
|
ctx_opts.key_file_name = this.key_file_name;
|
|
if (this.cert_file_name != null)
|
|
ctx_opts.cert_file_name = this.cert_file_name;
|
|
if (this.ca_file_name != null)
|
|
ctx_opts.ca_file_name = this.ca_file_name;
|
|
if (this.dh_params_file_name != null)
|
|
ctx_opts.dh_params_file_name = this.dh_params_file_name;
|
|
if (this.passphrase != null)
|
|
ctx_opts.passphrase = this.passphrase;
|
|
ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(this.low_memory_mode);
|
|
|
|
if (this.key) |key| {
|
|
ctx_opts.key = key.ptr;
|
|
ctx_opts.key_count = @intCast(key.len);
|
|
}
|
|
if (this.cert) |cert| {
|
|
ctx_opts.cert = cert.ptr;
|
|
ctx_opts.cert_count = @intCast(cert.len);
|
|
}
|
|
if (this.ca) |ca| {
|
|
ctx_opts.ca = ca.ptr;
|
|
ctx_opts.ca_count = @intCast(ca.len);
|
|
}
|
|
|
|
if (this.ssl_ciphers != null) {
|
|
ctx_opts.ssl_ciphers = this.ssl_ciphers;
|
|
}
|
|
ctx_opts.request_cert = this.request_cert;
|
|
ctx_opts.reject_unauthorized = this.reject_unauthorized;
|
|
|
|
return ctx_opts;
|
|
}
|
|
|
|
/// Returns socket options for client-side TLS with manual verification.
|
|
/// Sets request_cert=1 (to receive server cert) and reject_unauthorized=0
|
|
/// (to handle verification manually in handshake callback).
|
|
pub fn asUSocketsForClientVerification(this: *const SSLConfig) uws.SocketContext.BunSocketContextOptions {
|
|
var opts = this.asUSockets();
|
|
opts.request_cert = 1;
|
|
opts.reject_unauthorized = 0;
|
|
return opts;
|
|
}
|
|
|
|
/// Returns a copy of this config for client-side TLS with manual verification.
|
|
/// Sets request_cert=1 (to receive server cert) and reject_unauthorized=0
|
|
/// (to handle verification manually in handshake callback).
|
|
pub fn forClientVerification(this: SSLConfig) SSLConfig {
|
|
var copy = this;
|
|
copy.request_cert = 1;
|
|
copy.reject_unauthorized = 0;
|
|
return copy;
|
|
}
|
|
|
|
pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool {
|
|
inline for (comptime std.meta.fields(SSLConfig)) |field| {
|
|
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
|
|
const first = @field(this, field.name);
|
|
const second = @field(other, field.name);
|
|
switch (field.type) {
|
|
?[*:0]const u8 => {
|
|
// Compare optional single strings
|
|
if (first) |a| {
|
|
const b = second orelse return false;
|
|
if (!stringsEqual(a, b)) return false;
|
|
} else {
|
|
if (second != null) return false;
|
|
}
|
|
},
|
|
?[][*:0]const u8 => {
|
|
// Compare optional arrays of strings (e.g., key, cert, ca)
|
|
if (first) |slice1| {
|
|
const slice2 = second orelse return false;
|
|
if (slice1.len != slice2.len) return false;
|
|
for (slice1, slice2) |a, b| {
|
|
if (!stringsEqual(a, b)) return false;
|
|
}
|
|
} else {
|
|
if (second != null) return false;
|
|
}
|
|
},
|
|
else => if (first != second) return false,
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn stringsEqual(a: [*:0]const u8, b: [*:0]const u8) bool {
|
|
const lhs = bun.asByteSlice(a);
|
|
const rhs = bun.asByteSlice(b);
|
|
return strings.eqlLong(lhs, rhs, true);
|
|
}
|
|
|
|
fn freeStrings(slice: *?[][*:0]const u8) void {
|
|
const inner = slice.* orelse return;
|
|
for (inner) |string| {
|
|
bun.freeSensitive(bun.default_allocator, std.mem.span(string));
|
|
}
|
|
bun.default_allocator.free(inner);
|
|
slice.* = null;
|
|
}
|
|
|
|
fn freeString(string: *?[*:0]const u8) void {
|
|
const inner = string.* orelse return;
|
|
bun.freeSensitive(bun.default_allocator, std.mem.span(inner));
|
|
string.* = null;
|
|
}
|
|
|
|
pub fn deinit(this: *SSLConfig) void {
|
|
bun.meta.useAllFields(SSLConfig, .{
|
|
.server_name = freeString(&this.server_name),
|
|
.key_file_name = freeString(&this.key_file_name),
|
|
.cert_file_name = freeString(&this.cert_file_name),
|
|
.ca_file_name = freeString(&this.ca_file_name),
|
|
.dh_params_file_name = freeString(&this.dh_params_file_name),
|
|
.passphrase = freeString(&this.passphrase),
|
|
.key = freeStrings(&this.key),
|
|
.cert = freeStrings(&this.cert),
|
|
.ca = freeStrings(&this.ca),
|
|
.secure_options = {},
|
|
.request_cert = {},
|
|
.reject_unauthorized = {},
|
|
.ssl_ciphers = freeString(&this.ssl_ciphers),
|
|
.protos = freeString(&this.protos),
|
|
.client_renegotiation_limit = {},
|
|
.client_renegotiation_window = {},
|
|
.requires_custom_request_ctx = {},
|
|
.is_using_default_ciphers = {},
|
|
.low_memory_mode = {},
|
|
.ref_count = {},
|
|
.cached_hash = {},
|
|
});
|
|
}
|
|
|
|
fn cloneStrings(slice: ?[][*:0]const u8) ?[][*:0]const u8 {
|
|
const inner = slice orelse return null;
|
|
const result = bun.handleOom(bun.default_allocator.alloc([*:0]const u8, inner.len));
|
|
for (inner, result) |string, *out| {
|
|
out.* = bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string)));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fn cloneString(string: ?[*:0]const u8) ?[*:0]const u8 {
|
|
return bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string orelse return null)));
|
|
}
|
|
|
|
pub fn clone(this: *const SSLConfig) SSLConfig {
|
|
return .{
|
|
.server_name = cloneString(this.server_name),
|
|
.key_file_name = cloneString(this.key_file_name),
|
|
.cert_file_name = cloneString(this.cert_file_name),
|
|
.ca_file_name = cloneString(this.ca_file_name),
|
|
.dh_params_file_name = cloneString(this.dh_params_file_name),
|
|
.passphrase = cloneString(this.passphrase),
|
|
.key = cloneStrings(this.key),
|
|
.cert = cloneStrings(this.cert),
|
|
.ca = cloneStrings(this.ca),
|
|
.secure_options = this.secure_options,
|
|
.request_cert = this.request_cert,
|
|
.reject_unauthorized = this.reject_unauthorized,
|
|
.ssl_ciphers = cloneString(this.ssl_ciphers),
|
|
.protos = cloneString(this.protos),
|
|
.client_renegotiation_limit = this.client_renegotiation_limit,
|
|
.client_renegotiation_window = this.client_renegotiation_window,
|
|
.requires_custom_request_ctx = this.requires_custom_request_ctx,
|
|
.is_using_default_ciphers = this.is_using_default_ciphers,
|
|
.low_memory_mode = this.low_memory_mode,
|
|
.ref_count = .init(),
|
|
.cached_hash = 0,
|
|
};
|
|
}
|
|
|
|
pub fn contentHash(this: *SSLConfig) u64 {
|
|
if (this.cached_hash != 0) return this.cached_hash;
|
|
var hasher = std.hash.Wyhash.init(0);
|
|
inline for (comptime std.meta.fields(SSLConfig)) |field| {
|
|
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
|
|
const value = @field(this, field.name);
|
|
switch (field.type) {
|
|
?[*:0]const u8 => {
|
|
if (value) |s| {
|
|
hasher.update(bun.asByteSlice(s));
|
|
}
|
|
hasher.update(&.{0});
|
|
},
|
|
?[][*:0]const u8 => {
|
|
if (value) |slice| {
|
|
for (slice) |s| {
|
|
hasher.update(bun.asByteSlice(s));
|
|
hasher.update(&.{0});
|
|
}
|
|
}
|
|
hasher.update(&.{0});
|
|
},
|
|
else => {
|
|
hasher.update(std.mem.asBytes(&value));
|
|
},
|
|
}
|
|
}
|
|
const hash = hasher.final();
|
|
// Avoid 0 since it's the sentinel for "not computed"
|
|
this.cached_hash = if (hash == 0) 1 else hash;
|
|
return this.cached_hash;
|
|
}
|
|
|
|
/// Called by the RC mixin when refcount reaches 0.
|
|
fn destroy(this: *SSLConfig) void {
|
|
GlobalRegistry.remove(this);
|
|
this.deinit();
|
|
bun.default_allocator.destroy(this);
|
|
}
|
|
|
|
pub const GlobalRegistry = struct {
|
|
const MapContext = struct {
|
|
pub fn hash(_: @This(), key: *SSLConfig) u32 {
|
|
return @truncate(key.contentHash());
|
|
}
|
|
pub fn eql(_: @This(), a: *SSLConfig, b: *SSLConfig, _: usize) bool {
|
|
return a.isSame(b);
|
|
}
|
|
};
|
|
|
|
var mutex: bun.Mutex = .{};
|
|
var configs: std.ArrayHashMapUnmanaged(*SSLConfig, void, MapContext, true) = .empty;
|
|
|
|
/// Takes ownership of a heap-allocated SSLConfig.
|
|
/// If an identical config already exists in the registry, the new one is freed
|
|
/// and the existing one is returned (with refcount incremented).
|
|
/// If no match, the new config is registered and returned.
|
|
pub fn intern(new_config: *SSLConfig) *SSLConfig {
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
|
|
// Look up by content hash/equality
|
|
const gop = bun.handleOom(configs.getOrPutContext(bun.default_allocator, new_config, .{}));
|
|
if (gop.found_existing) {
|
|
// Identical config already exists - free the new one, return existing
|
|
const existing = gop.key_ptr.*;
|
|
new_config.ref_count.clearWithoutDestructor();
|
|
new_config.deinit();
|
|
bun.default_allocator.destroy(new_config);
|
|
existing.ref();
|
|
return existing;
|
|
}
|
|
|
|
// New config - it's already inserted by getOrPut
|
|
// refcount is already 1 from initialization
|
|
return new_config;
|
|
}
|
|
|
|
/// Remove a config from the registry. Called when refcount reaches 0.
|
|
fn remove(config: *SSLConfig) void {
|
|
mutex.lock();
|
|
defer mutex.unlock();
|
|
_ = configs.swapRemoveContext(config, .{});
|
|
}
|
|
};
|
|
|
|
pub const zero = SSLConfig{};
|
|
|
|
pub fn fromJS(
|
|
vm: *jsc.VirtualMachine,
|
|
global: *jsc.JSGlobalObject,
|
|
value: jsc.JSValue,
|
|
) bun.JSError!?SSLConfig {
|
|
var generated: jsc.generated.SSLConfig = try .fromJS(global, value);
|
|
defer generated.deinit();
|
|
return .fromGenerated(vm, global, &generated);
|
|
}
|
|
|
|
pub fn fromGenerated(
|
|
vm: *jsc.VirtualMachine,
|
|
global: *jsc.JSGlobalObject,
|
|
generated: *const jsc.generated.SSLConfig,
|
|
) bun.JSError!?SSLConfig {
|
|
var result: SSLConfig = zero;
|
|
errdefer result.deinit();
|
|
var any = false;
|
|
|
|
if (generated.passphrase.get()) |passphrase| {
|
|
result.passphrase = passphrase.toOwnedSliceZ(bun.default_allocator);
|
|
any = true;
|
|
}
|
|
if (generated.dh_params_file.get()) |dh_params_file| {
|
|
result.dh_params_file_name = try handlePath(global, "dhParamsFile", dh_params_file);
|
|
any = true;
|
|
}
|
|
if (generated.server_name.get()) |server_name| {
|
|
result.server_name = server_name.toOwnedSliceZ(bun.default_allocator);
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
|
|
result.low_memory_mode = generated.low_memory_mode;
|
|
result.reject_unauthorized = @intFromBool(
|
|
generated.reject_unauthorized orelse vm.getTLSRejectUnauthorized(),
|
|
);
|
|
result.request_cert = @intFromBool(generated.request_cert);
|
|
result.secure_options = generated.secure_options;
|
|
any = any or
|
|
result.low_memory_mode or
|
|
generated.reject_unauthorized != null or
|
|
generated.request_cert or
|
|
result.secure_options != 0;
|
|
|
|
result.ca = try handleFileForField(global, "ca", &generated.ca);
|
|
result.cert = try handleFileForField(global, "cert", &generated.cert);
|
|
result.key = try handleFileForField(global, "key", &generated.key);
|
|
result.requires_custom_request_ctx = result.requires_custom_request_ctx or
|
|
result.ca != null or
|
|
result.cert != null or
|
|
result.key != null;
|
|
|
|
if (generated.key_file.get()) |key_file| {
|
|
result.key_file_name = try handlePath(global, "keyFile", key_file);
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
if (generated.cert_file.get()) |cert_file| {
|
|
result.cert_file_name = try handlePath(global, "certFile", cert_file);
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
if (generated.ca_file.get()) |ca_file| {
|
|
result.ca_file_name = try handlePath(global, "caFile", ca_file);
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
|
|
const protocols = switch (generated.alpn_protocols) {
|
|
.none => null,
|
|
.string => |*val| val.get().toOwnedSliceZ(bun.default_allocator),
|
|
.buffer => |*val| blk: {
|
|
const buffer: jsc.ArrayBuffer = val.get().asArrayBuffer();
|
|
break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice());
|
|
},
|
|
};
|
|
if (protocols) |some_protocols| {
|
|
result.protos = some_protocols;
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
if (generated.ciphers.get()) |ciphers| {
|
|
result.ssl_ciphers = ciphers.toOwnedSliceZ(bun.default_allocator);
|
|
result.is_using_default_ciphers = false;
|
|
result.requires_custom_request_ctx = true;
|
|
}
|
|
|
|
result.client_renegotiation_limit = generated.client_renegotiation_limit;
|
|
result.client_renegotiation_window = generated.client_renegotiation_window;
|
|
any = any or
|
|
result.requires_custom_request_ctx or
|
|
result.client_renegotiation_limit != 0 or
|
|
generated.client_renegotiation_window != 0;
|
|
|
|
// We don't need to deinit `result` if `any` is false.
|
|
return if (any) result else null;
|
|
}
|
|
|
|
fn handlePath(
|
|
global: *jsc.JSGlobalObject,
|
|
comptime field: []const u8,
|
|
string: bun.string.WTFStringImpl,
|
|
) bun.JSError![:0]const u8 {
|
|
const name = string.toOwnedSliceZ(bun.default_allocator);
|
|
errdefer bun.freeSensitive(bun.default_allocator, name);
|
|
if (std.posix.system.access(name, std.posix.F_OK) != 0) {
|
|
return global.throwInvalidArguments(
|
|
std.fmt.comptimePrint("Unable to access {s} path", .{field}),
|
|
.{},
|
|
);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
fn handleFileForField(
|
|
global: *jsc.JSGlobalObject,
|
|
comptime field: []const u8,
|
|
file: *const jsc.generated.SSLConfigFile,
|
|
) bun.JSError!?[][*:0]const u8 {
|
|
return handleFile(global, file) catch |err| switch (err) {
|
|
error.JSError => return error.JSError,
|
|
error.OutOfMemory => return error.OutOfMemory,
|
|
error.JSTerminated => return error.JSTerminated,
|
|
error.EmptyFile => return global.throwInvalidArguments(
|
|
std.fmt.comptimePrint("TLSOptions.{s} is an empty file", .{field}),
|
|
.{},
|
|
),
|
|
error.NullStore, error.NotAFile => return global.throwInvalidArguments(
|
|
std.fmt.comptimePrint(
|
|
"TLSOptions.{s} is not a valid BunFile (non-BunFile `Blob`s are not supported)",
|
|
.{field},
|
|
),
|
|
.{},
|
|
),
|
|
};
|
|
}
|
|
|
|
fn handleFile(
|
|
global: *jsc.JSGlobalObject,
|
|
file: *const jsc.generated.SSLConfigFile,
|
|
) ReadFromBlobError!?[][*:0]const u8 {
|
|
const single = try handleSingleFile(global, switch (file.*) {
|
|
.none => return null,
|
|
.string => |*val| .{ .string = val.get() },
|
|
.buffer => |*val| .{ .buffer = val.get() },
|
|
.file => |*val| .{ .file = val.get() },
|
|
.array => |*list| return try handleFileArray(global, list.items()),
|
|
});
|
|
errdefer bun.freeSensitive(bun.default_allocator, single);
|
|
const result = try bun.default_allocator.alloc([*:0]const u8, 1);
|
|
result[0] = single;
|
|
return result;
|
|
}
|
|
|
|
fn handleFileArray(
|
|
global: *jsc.JSGlobalObject,
|
|
elements: []const jsc.generated.SSLConfigSingleFile,
|
|
) ReadFromBlobError!?[][*:0]const u8 {
|
|
if (elements.len == 0) return null;
|
|
var result: bun.collections.ArrayListDefault([*:0]const u8) = try .initCapacity(elements.len);
|
|
errdefer {
|
|
for (result.items()) |string| {
|
|
bun.freeSensitive(bun.default_allocator, std.mem.span(string));
|
|
}
|
|
result.deinit();
|
|
}
|
|
for (elements) |*elem| {
|
|
result.appendAssumeCapacity(try handleSingleFile(global, switch (elem.*) {
|
|
.string => |*val| .{ .string = val.get() },
|
|
.buffer => |*val| .{ .buffer = val.get() },
|
|
.file => |*val| .{ .file = val.get() },
|
|
}));
|
|
}
|
|
return try result.toOwnedSlice();
|
|
}
|
|
|
|
fn handleSingleFile(
|
|
global: *jsc.JSGlobalObject,
|
|
file: union(enum) {
|
|
string: bun.string.WTFStringImpl,
|
|
buffer: *jsc.JSCArrayBuffer,
|
|
file: *bun.webcore.Blob,
|
|
},
|
|
) ReadFromBlobError![:0]const u8 {
|
|
return switch (file) {
|
|
.string => |string| string.toOwnedSliceZ(bun.default_allocator),
|
|
.buffer => |jsc_buffer| blk: {
|
|
const buffer: jsc.ArrayBuffer = jsc_buffer.asArrayBuffer();
|
|
break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice());
|
|
},
|
|
.file => |blob| try readFromBlob(global, blob),
|
|
};
|
|
}
|
|
|
|
pub fn takeProtos(this: *SSLConfig) ?[]const u8 {
|
|
defer this.protos = null;
|
|
const protos = this.protos orelse return null;
|
|
return bun.handleOom(bun.memory.dropSentinel(protos, bun.default_allocator));
|
|
}
|
|
|
|
pub fn takeServerName(this: *SSLConfig) ?[]const u8 {
|
|
defer this.server_name = null;
|
|
const server_name = this.server_name orelse return null;
|
|
return bun.handleOom(bun.memory.dropSentinel(server_name, bun.default_allocator));
|
|
}
|
|
|
|
const std = @import("std");
|
|
|
|
const bun = @import("bun");
|
|
const strings = bun.strings;
|
|
const uws = bun.uws;
|
|
|
|
const jsc = bun.jsc;
|
|
const JSGlobalObject = jsc.JSGlobalObject;
|
|
const JSValue = jsc.JSValue;
|
|
const VirtualMachine = jsc.VirtualMachine;
|