feat(https/fetch): Support custom ca/cert/key in fetch (#11322)

Co-authored-by: Liz3 (Yann HN) <accs@liz3.net>
This commit is contained in:
Ludvig Hozman
2024-06-11 15:36:32 +02:00
committed by GitHub
parent 1b8a72e724
commit ee30e8660c
11 changed files with 453 additions and 20 deletions

View File

@@ -211,6 +211,7 @@ pub const ServerConfig = struct {
}
pub const SSLConfig = struct {
requires_custom_request_ctx: bool = false,
server_name: [*c]const u8 = null,
key_file_name: [*c]const u8 = null,
@@ -281,6 +282,71 @@ pub const ServerConfig = struct {
return ctx_opts;
}
pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool {
{ //strings
const fields = .{
"server_name",
"key_file_name",
"cert_file_name",
"ca_file_name",
"dh_params_file_name",
"passphrase",
"ssl_ciphers",
"protos",
};
inline for (fields) |field| {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
if (lhs != null and rhs != null) {
if (!stringsEqual(lhs, rhs))
return false;
} else if (lhs != null or rhs != null) {
return false;
}
}
}
{
//numbers
const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" };
inline for (fields) |field| {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
if (lhs != rhs)
return false;
}
}
{
// complex fields
const fields = .{ "key", "ca", "cert" };
inline for (fields) |field| {
const lhs_count = @field(thisConfig, field ++ "_count");
const rhs_count = @field(otherConfig, field ++ "_count");
if (lhs_count != rhs_count)
return false;
if (lhs_count > 0) {
const lhs = @field(thisConfig, field);
const rhs = @field(otherConfig, field);
for (0..lhs_count) |i| {
if (!stringsEqual(lhs.?[i], rhs.?[i]))
return false;
}
}
}
}
return true;
}
fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool {
const lhs = bun.asByteSlice(a);
const rhs = bun.asByteSlice(b);
return strings.eqlLong(lhs, rhs, true);
}
pub fn deinit(this: *SSLConfig) void {
const fields = .{
"server_name",
@@ -367,6 +433,7 @@ pub const ServerConfig = struct {
return null;
}
any = true;
result.requires_custom_request_ctx = true;
}
}
@@ -386,11 +453,13 @@ pub const ServerConfig = struct {
native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (BlobFileContentResult.init("key", item, global, exception)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
result.requires_custom_request_ctx = true;
any = true;
} else {
// mark and free all CA's
@@ -422,6 +491,7 @@ pub const ServerConfig = struct {
result.key = native_array;
result.key_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
@@ -434,6 +504,7 @@ pub const ServerConfig = struct {
if (sliced.len > 0) {
native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
any = true;
result.requires_custom_request_ctx = true;
result.key = native_array;
result.key_count = 1;
} else {
@@ -460,6 +531,7 @@ pub const ServerConfig = struct {
return null;
}
any = true;
result.requires_custom_request_ctx = true;
}
}
@@ -473,6 +545,7 @@ pub const ServerConfig = struct {
}
any = true;
result.requires_custom_request_ctx = true;
} else {
global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{});
result.deinit();
@@ -496,11 +569,13 @@ pub const ServerConfig = struct {
native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (BlobFileContentResult.init("cert", item, global, exception)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
result.requires_custom_request_ctx = true;
any = true;
} else {
// mark and free all CA's
@@ -532,6 +607,7 @@ pub const ServerConfig = struct {
result.cert = native_array;
result.cert_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
@@ -544,6 +620,7 @@ pub const ServerConfig = struct {
if (sliced.len > 0) {
native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
any = true;
result.requires_custom_request_ctx = true;
result.cert = native_array;
result.cert_count = 1;
} else {
@@ -587,6 +664,7 @@ pub const ServerConfig = struct {
if (sliced.len > 0) {
result.ssl_ciphers = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable;
any = true;
result.requires_custom_request_ctx = true;
}
}
@@ -596,6 +674,7 @@ pub const ServerConfig = struct {
if (sliced.len > 0) {
result.server_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable;
any = true;
result.requires_custom_request_ctx = true;
}
}
@@ -615,12 +694,14 @@ pub const ServerConfig = struct {
native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
}
} else if (BlobFileContentResult.init("ca", item, global, exception)) |content| {
if (content.data.len > 0) {
native_array[valid_count] = content.data.ptr;
valid_count += 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
// mark and free all CA's
result.cert = native_array;
@@ -651,6 +732,7 @@ pub const ServerConfig = struct {
result.ca = native_array;
result.ca_count = 1;
any = true;
result.requires_custom_request_ctx = true;
} else {
result.deinit();
return null;
@@ -663,6 +745,7 @@ pub const ServerConfig = struct {
if (sliced.len > 0) {
native_array[0] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable;
any = true;
result.requires_custom_request_ctx = true;
result.ca = native_array;
result.ca_count = 1;
} else {

View File

@@ -35,6 +35,8 @@ const JSGlobalObject = JSC.JSGlobalObject;
const NullableAllocator = bun.NullableAllocator;
const DataURL = @import("../../resolver/data_url.zig").DataURL;
const SSLConfig = @import("../api/server.zig").ServerConfig.SSLConfig;
const VirtualMachine = JSC.VirtualMachine;
const Task = JSC.Task;
const JSPrinter = bun.js_printer;
@@ -1635,6 +1637,7 @@ pub const Fetch = struct {
.disable_decompression = fetch_options.disable_decompression,
.reject_unauthorized = fetch_options.reject_unauthorized,
.verbose = fetch_options.verbose,
.tls_props = fetch_options.ssl_config,
},
);
@@ -1693,6 +1696,7 @@ pub const Fetch = struct {
memory_reporter: *JSC.MemoryReportingAllocator,
check_server_identity: JSC.Strong = .{},
unix_socket_path: ZigString.Slice,
ssl_config: ?*SSLConfig = null,
};
pub fn queue(
@@ -1899,6 +1903,8 @@ pub const Fetch = struct {
blob,
};
var url_type = URLType.remote;
var ssl_config: ?*SSLConfig = null;
var reject_unauthorized = script_ctx.bundler.env.getTLSRejectUnauthorized();
var check_server_identity: JSValue = .zero;
@@ -2079,6 +2085,15 @@ pub const Fetch = struct {
if (options.get(ctx, "tls")) |tls| {
if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) {
if (SSLConfig.inJS(globalThis, tls, exception)) |config| {
if (ssl_config) |existing_conf| {
existing_conf.deinit();
bun.default_allocator.destroy(existing_conf);
ssl_config = null;
}
ssl_config = bun.default_allocator.create(SSLConfig) catch bun.outOfMemory();
ssl_config.?.* = config;
}
if (tls.get(ctx, "rejectUnauthorized")) |reject| {
if (reject.isBoolean()) {
reject_unauthorized = reject.asBoolean();
@@ -2086,7 +2101,6 @@ pub const Fetch = struct {
reject_unauthorized = reject.to(i32) != 0;
}
}
if (tls.get(ctx, "checkServerIdentity")) |checkServerIdentity| {
if (checkServerIdentity.isCell() and checkServerIdentity.isCallable(globalThis.vm())) {
check_server_identity = checkServerIdentity;
@@ -2100,9 +2114,13 @@ pub const Fetch = struct {
var href = JSC.URL.hrefFromJS(proxy_arg, globalThis);
if (href.tag == .Dead) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() proxy URL is invalid", .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
// clean hostname and tls props if any
if (ssl_config) |conf| {
conf.deinit();
bun.default_allocator.destroy(conf);
}
if (hostname) |hn| {
bun.default_allocator.free(hn);
hostname = null;
}
allocator.free(url_proxy_buffer);
@@ -2231,8 +2249,8 @@ pub const Fetch = struct {
if (str.isEmpty()) {
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, fetch_error_blank_url, .{}, ctx);
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
if (hostname) |hn| {
bun.default_allocator.free(hn);
hostname = null;
}
return JSPromise.rejectedPromiseValue(globalThis, err);
@@ -2253,8 +2271,8 @@ pub const Fetch = struct {
url = ZigURL.fromString(allocator, str) catch {
// clean hostname if any
if (hostname) |host| {
allocator.free(host);
if (hostname) |hn| {
bun.default_allocator.free(hn);
hostname = null;
}
const err = JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "fetch() URL is invalid", .{}, ctx);
@@ -2363,6 +2381,10 @@ pub const Fetch = struct {
if (options.get(ctx, "tls")) |tls| {
if (!tls.isEmptyOrUndefinedOrNull() and tls.isObject()) {
if (SSLConfig.inJS(globalThis, tls, exception)) |config| {
ssl_config = bun.default_allocator.create(SSLConfig) catch bun.outOfMemory();
ssl_config.?.* = config;
}
if (tls.get(ctx, "rejectUnauthorized")) |reject| {
if (reject.isBoolean()) {
reject_unauthorized = reject.asBoolean();
@@ -2389,6 +2411,11 @@ pub const Fetch = struct {
allocator.free(host);
hostname = null;
}
if (ssl_config) |conf| {
conf.deinit();
bun.default_allocator.destroy(conf);
}
allocator.free(url_proxy_buffer);
is_error = true;
return JSPromise.rejectedPromiseValue(globalThis, err);
@@ -2708,6 +2735,7 @@ pub const Fetch = struct {
.url_proxy_buffer = url_proxy_buffer,
.signal = signal,
.globalThis = globalThis,
.ssl_config = ssl_config,
.hostname = hostname,
.memory_reporter = memory_reporter,
.check_server_identity = if (check_server_identity.isEmptyOrUndefinedOrNull()) .{} else JSC.Strong.create(check_server_identity, globalThis),

View File

@@ -29,6 +29,8 @@ const SOCK = os.SOCK;
const Arena = @import("./mimalloc_arena.zig").Arena;
const ZlibPool = @import("./http/zlib.zig");
const BoringSSL = bun.BoringSSL;
const X509 = @import("./bun.js/api/bun/x509.zig");
const SSLConfig = @import("./bun.js/api/server.zig").ServerConfig.SSLConfig;
const URLBufferPool = ObjectPool([8192]u8, null, false, 10);
const uws = bun.uws;
@@ -47,6 +49,8 @@ var dead_socket = @as(*DeadSocket, @ptrFromInt(1));
var socket_async_http_abort_tracker = std.AutoArrayHashMap(u32, uws.InternalSocket).init(bun.default_allocator);
var async_http_id: std.atomic.Value(u32) = std.atomic.Value(u32).init(0);
const MAX_REDIRECT_URL_LENGTH = 128 * 1024;
var custom_ssl_context_map = std.AutoArrayHashMap(*SSLConfig, *NewHTTPContext(true)).init(bun.default_allocator);
const print_every = 0;
var print_every_i: usize = 0;
@@ -377,6 +381,34 @@ fn NewHTTPContext(comptime ssl: bool) type {
return @as(*BoringSSL.SSL_CTX, @ptrCast(this.us_socket_context.getNativeHandle(true)));
}
pub fn deinit(this: *@This()) void {
this.us_socket_context.deinit(ssl);
uws.us_socket_context_free(@as(c_int, @intFromBool(ssl)), this.us_socket_context);
bun.default_allocator.destroy(this);
}
pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) !void {
if (!comptime ssl) {
unreachable;
}
var opts = client.tls_props.?.asUSockets();
opts.request_cert = 1;
opts.reject_unauthorized = 0;
const socket = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts);
if (socket == null) {
return error.FailedToOpenSocket;
}
this.us_socket_context = socket.?;
this.sslCtx().setup();
HTTPSocket.configure(
this.us_socket_context,
false,
anyopaque,
Handler,
);
}
pub fn init(this: *@This()) !void {
if (comptime ssl) {
const opts: uws.us_bun_socket_context_options_t = .{
@@ -770,6 +802,34 @@ pub const HTTPThread = struct {
return try this.context(is_ssl).connectSocket(client, client.unix_socket_path.slice());
}
if (comptime is_ssl) {
const needs_own_context = client.tls_props != null and client.tls_props.?.requires_custom_request_ctx;
if (needs_own_context) {
var requested_config = client.tls_props.?;
for (custom_ssl_context_map.keys()) |other_config| {
if (requested_config.isSame(other_config)) {
// we free the callers config since we have a existing one
requested_config.deinit();
bun.default_allocator.destroy(requested_config);
client.tls_props = other_config;
return try custom_ssl_context_map.get(other_config).?.connect(client, client.url.hostname, client.url.getPortAuto());
}
}
// we need the config so dont free it
var custom_context = try bun.default_allocator.create(NewHTTPContext(is_ssl));
custom_context.initWithClientConfig(client) catch |err| {
requested_config.deinit();
client.tls_props = null;
bun.default_allocator.destroy(custom_context);
return err;
};
try custom_ssl_context_map.put(requested_config, custom_context);
// We might deinit the socket context, so we disable keepalive to make sure we don't
// free it while in use.
client.disable_keepalive = true;
return try custom_context.connect(client, client.url.hostname, client.url.getPortAuto());
}
}
if (client.http_proxy) |url| {
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
}
@@ -1490,6 +1550,7 @@ disable_keepalive: bool = false,
disable_decompression: bool = false,
state: InternalState = .{},
tls_props: ?*SSLConfig = null,
result_callback: HTTPClientResult.Callback = undefined,
/// Some HTTP servers (such as npm) report Last-Modified times but ignore If-Modified-Since.
@@ -1749,6 +1810,7 @@ pub const AsyncHTTP = struct {
disable_keepalive: ?bool = null,
disable_decompression: ?bool = null,
reject_unauthorized: ?bool = null,
tls_props: ?*SSLConfig = null,
};
pub fn init(
@@ -1811,6 +1873,9 @@ pub const AsyncHTTP = struct {
if (options.reject_unauthorized) |val| {
this.client.reject_unauthorized = val;
}
if (options.tls_props) |val| {
this.client.tls_props = val;
}
if (options.http_proxy) |proxy| {
// Username between 0 and 4096 chars

View File

@@ -1388,6 +1388,7 @@ class ClientRequest extends OutgoingMessage {
#protocol;
#method;
#port;
#tls = null;
#useDefaultPort;
#joinDuplicateHeaders;
#maxHeaderSize;
@@ -1402,7 +1403,6 @@ class ClientRequest extends OutgoingMessage {
#timeoutTimer?: Timer = undefined;
#options;
#finished;
#tls;
_httpMessage;
@@ -1459,6 +1459,11 @@ class ClientRequest extends OutgoingMessage {
callback();
}
_ensureTls() {
if (this.#tls === null) this.#tls = {};
return this.#tls;
}
_final(callback) {
this.#finished = true;
this[kAbortController] = new AbortController();
@@ -1482,6 +1487,8 @@ class ClientRequest extends OutgoingMessage {
} else {
url = `${this.#protocol}//${this.#host}${this.#useDefaultPort ? "" : ":" + this.#port}${this.#path}`;
}
const tls =
this.#protocol === "https:" && this.#tls ? { ...this.#tls, serverName: this.#tls.servername } : undefined;
try {
const fetchOptions: any = {
method,
@@ -1489,7 +1496,6 @@ class ClientRequest extends OutgoingMessage {
body: body && method !== "GET" && method !== "HEAD" && method !== "OPTIONS" ? body : undefined,
redirect: "manual",
signal: this[kAbortController].signal,
// Timeouts are handled via this.setTimeout.
timeout: false,
// Disable auto gzip/deflate
@@ -1694,7 +1700,48 @@ class ClientRequest extends OutgoingMessage {
}
this.#joinDuplicateHeaders = _joinDuplicateHeaders;
if (options.pfx) {
throw new Error("pfx is not supported");
}
if (options.rejectUnauthorized !== undefined) this._ensureTls().rejectUnauthorized = options.rejectUnauthorized;
if (options.ca) {
if (!isValidTLSArray(options.ca))
throw new TypeError(
"ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().ca = options.ca;
}
if (options.cert) {
if (!isValidTLSArray(options.cert))
throw new TypeError(
"cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().cert = options.cert;
}
if (options.key) {
if (!isValidTLSArray(options.key))
throw new TypeError(
"key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile",
);
this._ensureTls().key = options.key;
}
if (options.passphrase) {
if (typeof options.passphrase !== "string") throw new TypeError("passphrase argument must be a string");
this._ensureTls().passphrase = options.passphrase;
}
if (options.ciphers) {
if (typeof options.ciphers !== "string") throw new TypeError("ciphers argument must be a string");
this._ensureTls().ciphers = options.ciphers;
}
if (options.servername) {
if (typeof options.servername !== "string") throw new TypeError("servername argument must be a string");
this._ensureTls().servername = options.servername;
}
if (options.secureOptions) {
if (typeof options.secureOptions !== "number") throw new TypeError("secureOptions argument must be a string");
this._ensureTls().secureOptions = options.secureOptions;
}
this.#path = options.path || "/";
if (cb) {
this.once("response", cb);
@@ -1723,7 +1770,6 @@ class ClientRequest extends OutgoingMessage {
this.#reusedSocket = false;
this.#host = host;
this.#protocol = protocol;
this.#tls = options.tls;
const timeout = options.timeout;
if (timeout !== undefined && timeout !== 0) {

View File

@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEazCCAtOgAwIBAgIRAOmVTIueDOoEBqjyAsX9r+wwDQYJKoZIhvcNAQELBQAw
gZ0xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE5MDcGA1UECwwwbHVk
dmlnQEx1ZHZpZ3MtTWFjQm9vay1Qcm8ubG9jYWwgKEx1ZHZpZyBIb3ptYW4pMUAw
PgYDVQQDDDdta2NlcnQgbHVkdmlnQEx1ZHZpZ3MtTWFjQm9vay1Qcm8ubG9jYWwg
KEx1ZHZpZyBIb3ptYW4pMB4XDTI0MDUyNTEyMzIyN1oXDTI2MDgyNTEyMzIyN1ow
ZDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTkwNwYD
VQQLDDBsdWR2aWdATHVkdmlncy1NYWNCb29rLVByby5sb2NhbCAoTHVkdmlnIEhv
em1hbikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt6FinGWRCnrNN
T8xJfV1ldr3yfrgOun7D4+88xVDErI3/CA2EdbME8d51V8hRjv6aVG+Wgyo82AE0
W96wmDtDmToNO4UJO5NPuMCqwdCfph7QGKMt9GRDKLC5qwwBHnrDxAUyTqxSpDWn
zoNUsGf80Cxr9wzzv95cnxpHiuJL1KnHCgkAXwNdWh0Rg63oa5+xilBxV1UdzaKY
VAMIO5s0w9d4nK6U+6w+u8YrH3nTFceUTJguo4Jv9jGPl6fLL4BE++fljMQK3imi
/afvXoic9UpKMcEeZ2TxVEuaTqLplvoWe9TUentge4i7OGWSlBeHIs7B1ujL3CcC
e0uwZcgvAgMBAAGjXjBcMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEF
BQcDATAfBgNVHSMEGDAWgBRm53Lp693sneRIcClR8vuEt7zJMDAUBgNVHREEDTAL
gglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggGBACHrhzpedmGWVSGtmIwYZ3W7
Y4nwWi5V4uhgRc3f7zb4xHIqmILWGO1nijoKrqN9/L4Ac1ePrlZBjl5eSC6maev1
C021cbm1WczMIpg5ua3rj1X511AtX2tHv8fDiiDMZS2KqnJ7lErcL6PiCtRowUBN
dmJ32PgFPdSWVZ0r3QUaqjE7kURx4F65Q1M1dYcpSfZlluEdDnqmKyG6Cdn7+7le
d3FnUEnBS1yovg/l04nIxwNQNhAO7/bqLYC7yPHK39yFG+9YuSe6Wu9pkIT+a0xb
Lhv/UkAZB3EwZYFQgjbS+lmRgNT+jIjipm8MM1Awn+a9+s+Twc9dXqAD9lv5wPhh
pJG7rlj7zNd7oSAUzbAbOvVivH2IpYtwhc4vrKFZHGH6YsZBR7l8vLdCRHqaUc5h
9D2FXVvYA0WY3tX3clYo/p4UsNeYayAT1gWT6Vmx8IPi6fvDqUO8bAsvEHE+YJ4B
ABqWvmOc7wU/rLpLiBmIy4Fmk33mSk2FyEVmDucGWw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDt6FinGWRCnrNN
T8xJfV1ldr3yfrgOun7D4+88xVDErI3/CA2EdbME8d51V8hRjv6aVG+Wgyo82AE0
W96wmDtDmToNO4UJO5NPuMCqwdCfph7QGKMt9GRDKLC5qwwBHnrDxAUyTqxSpDWn
zoNUsGf80Cxr9wzzv95cnxpHiuJL1KnHCgkAXwNdWh0Rg63oa5+xilBxV1UdzaKY
VAMIO5s0w9d4nK6U+6w+u8YrH3nTFceUTJguo4Jv9jGPl6fLL4BE++fljMQK3imi
/afvXoic9UpKMcEeZ2TxVEuaTqLplvoWe9TUentge4i7OGWSlBeHIs7B1ujL3CcC
e0uwZcgvAgMBAAECggEALhd6vXz84K9Qe6T/JinEo3i62jVUwX2+O4N4gSSVPlVT
+Vn9DHGlKksV11QXej2i9BFxwQ5Oa5VJvnQiE8KakMEp7xBd+Ojy5Fod8bc1DQkp
JRXw32Fe32gNvRr3a2wVSsI6Y4G8fxJTVtx6sziuHNvUD2LAvqSolvc4Jy4wI5KD
YrmFCwY7T4iUPa7tAzzU1il+iXyQympsjZtWau4JwyPbrCeN0t68yzsp0tRmfWKJ
xK+T86F01Lf1gsXUQ+xzx5GMZHkRBjWBK4GwQhDkz6ap69q905bhgBPWyrKLWq5v
yuusyIFbhIRCR7G7ZsJzjSdYivopMKpprbH6aAjWiQKBgQD6wL5iFuaQbyXTp8us
nsrpONjxpQKeO3kxklSjxbr9ZYGVcqp8cMl2FDJcepRTDVoqm0knIMdOttUkuPs7
E6RvoL/McN/2OgDgL9ovr2ow49l/QB4MD6HgMq4V4rVEtJqPlmG9AjmmpUEfHnCn
c/BHo151/udJtfDy3ZJX1rI3gwKBgQDy4sqtxr7ecLd3hMsVqxhfy0p34WDTiPdp
3X5p9eWYXLmKoJDVNH7/BecgN/LcDjIF31HcIbELqEHuHdE1arsTyVOjKtoO5N/1
+lRJifJjsD9trX+uuFeIHNclb3jTOV2MU0/pfB4HzgtG/qrtyyF/Sf3bBfDtuMwg
cYiOB+Ng5QKBgEaWlcGlMri8IUjo9oQcm4B1+VRlIEyM73wN9ne4BQCqX4VDp0yq
r3vnCZpRA4oxuw09c6VpK9Iz0+KnlEm4KNUnynZx3ApDn9V8gw5jciBbM/IHia3Z
hLdJbQpKLL8vnEcJjXAYvUP1R1TMS+hH0f9ItSHAZTmx1yd3Smghzz+jAoGAFxAf
7LZVg2uykB/E5O7VJqt4C8AT4KI91AibK1aVEY2kdJxghE4yzOZzluSZI/oZF+On
sz5jwFaexAyCxA65atyQG4tDH2zuMz4s6Lq3kG246CI0YJPSg/MxHrXiBDSLRHrY
uLP3aghPm9Msyd2i9aJB/50lznzgrSf6rnnjRl0CgYEA32d7xxNG8bThR2JFzbO3
1NuK2UEIkKWUgMNy+hOuSHXXVzMCYT9lj2oMDPN4wnF0SnOcQm1xfQO21m0UxHyz
h/q6Fxmg+wl60zO9/oq5hU55oja3Sx1DqubicEWSaFdYuD9tMtHxWZAuUIkD3zX1
3XlcuJOjhCuZxRoS9O1Otro=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFCzCCA3OgAwIBAgIQBg0eUuH8A64LETs9IrQIbzANBgkqhkiG9w0BAQsFADCB
nTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTkwNwYDVQQLDDBsdWR2
aWdATHVkdmlncy1NYWNCb29rLVByby5sb2NhbCAoTHVkdmlnIEhvem1hbikxQDA+
BgNVBAMMN21rY2VydCBsdWR2aWdATHVkdmlncy1NYWNCb29rLVByby5sb2NhbCAo
THVkdmlnIEhvem1hbikwHhcNMjQwNTI1MTIzMjI3WhcNMzQwNTI1MTIzMjI3WjCB
nTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTkwNwYDVQQLDDBsdWR2
aWdATHVkdmlncy1NYWNCb29rLVByby5sb2NhbCAoTHVkdmlnIEhvem1hbikxQDA+
BgNVBAMMN21rY2VydCBsdWR2aWdATHVkdmlncy1NYWNCb29rLVByby5sb2NhbCAo
THVkdmlnIEhvem1hbikwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDT
vKduL//b9hSVZOCrRFPFjpARpB3uAr1sjGd7TVeEdEkeJapO5BrQ4I8Unbtqo5JC
2U1lZv5Gl6Odlyc7m60c/F1py15zH6vMggUUshmtSdCxmVmXPBsbYXmuaDkEhxcH
+sE/60IfdkX/jw8cVNa5grIy7WbCpHsRxnUIFjij32kfOuvVY5UylEy+j0x6flGH
fl+a7nOO4qq6tZXaeBmagg0pAPVK3la6bFZDXPyO5KjwfjIIqF7H9nB5+YlIIIAg
GoCLU+1wOMsOzHgQFJcNecoX0k86v0gP9K5SD0+vgW3xbJ6xBdOBWCulWhWMY8Im
f66lMBYkJYnVFg6MnNOjl7wIToyy0nNEZvkwwSBhETjXaKyMF1+vEHxYLtbucla9
JkVYDC0yU7AhZNKbsyiI+V/M0FMCKW3QZip2q7trst8GnA0vURWXOyj5iZ96nh7X
BbNFSkuY0wBBNwbr0p/pTHE/FF6BlBPXl6XQdpXM6/YVvrqj3dOW7P5WUIIU10cC
AwEAAaNFMEMwDgYDVR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
VR0OBBYEFGbncunr3eyd5EhwKVHy+4S3vMkwMA0GCSqGSIb3DQEBCwUAA4IBgQC6
h20ry+Z7ma8G4XPGcEKhbwAROGSfYCnygmGC5V1j/Wshcro4/qrts9qDtq6MtCzC
5vMB40xSo60EWtDaNQbRhRZHvA1Agkzyi5NnFHQARKn+eSyNV+7wmDWRy9nb5bGH
A48mWREOTaQLi6BY6OPvLr376+dzdMx8GL/uMHz/1rQDU1/4e6lRxYPzrSuT8SPe
Zb112wpkbJuT69HvbT3mrYQVsagX5qJ1NML2/6+ichB9ou08ZIyksVd+8TKLP/zn
QSYhzrgcI5pTnyi2AybKRy07EjcAFNBzKiHP42S4+AudOUYUzdeNxMpgelgTiHjU
kkYncgeQ6qXzA3uC4ODTBZWGnslzSATY0IuLvn9/ZcgZmj1GcEeRyaxpkdE7JaX0
KIpPD6WIFHSB/6VwjFTUxf49+yW9U9bdaPlWOcHXUtOfoikC/EK1OXfX+sAd4OhE
8iyfiWz4jpOK9oBhqGsJLooaU4TXLzfMXYWyIjOOIoZX3QECUFQ4Zw3rJ9oV1A8=
-----END CERTIFICATE-----

View File

@@ -12,7 +12,7 @@ import http, {
IncomingMessage,
OutgoingMessage,
} from "node:http";
import https from "node:https";
import https, { createServer as createHttpsServer } from "node:https";
import { EventEmitter } from "node:events";
import { createServer as createHttpsServer } from "node:https";
import { createTest } from "node-harness";
@@ -829,6 +829,60 @@ describe("node:http", () => {
});
});
describe("https.request with custom tls options", () => {
const createServer = () =>
new Promise(resolve => {
const server = createHttpsServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
rejectUnauthorized: true,
},
(req, res) => {
res.writeHead(200);
res.end("hello world");
},
);
listen(server, "https").then(url => {
resolve({
server,
close: () => server.close(),
url,
});
});
});
it("supports custom tls args", async done => {
const { url, close } = await createServer();
try {
const options: https.RequestOptions = {
method: "GET",
url,
port: url.port,
ca: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost_ca.pem")),
};
const req = https.request(options, res => {
res.on("data", () => null);
res.on("end", () => {
close();
done();
});
});
req.on("error", error => {
close();
done(error);
});
req.end();
} catch (e) {
close();
throw e;
}
});
});
describe("signal", () => {
it("should abort and close the server", done => {
const server = createServer((req, res) => {
@@ -2039,3 +2093,62 @@ it("ServerResponse ClientRequest field exposes agent getter", async () => {
server.close();
}
});
it("should accept custom certs when provided", async () => {
const server = https.createServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
passphrase: "123123123",
},
(req, res) => {
res.write("Hello from https server");
res.end();
},
);
server.listen(0, "localhost");
const address = server.address();
let url_address = address.address;
const res = await fetch(`https://localhost:${address.port}`, {
tls: {
rejectUnauthorized: true,
ca: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost_ca.pem")),
},
});
const t = await res.text();
expect(t).toEqual("Hello from https server");
server.close();
});
it("should error with faulty args", async () => {
const server = https.createServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
passphrase: "123123123",
},
(req, res) => {
res.write("Hello from https server");
res.end();
},
);
server.listen(0, "localhost");
const address = server.address();
try {
let url_address = address.address;
const res = await fetch(`https://localhost:${address.port}`, {
tls: {
rejectUnauthorized: true,
ca: "some invalid value for a ca",
},
});
await res.text();
expect(true).toBe("unreacheable");
} catch (err) {
expect(err.code).toBe("FailedToOpenSocket");
expect(err.message).toBe("Was there a typo in the url or port?");
}
server.close();
});

View File

@@ -49,7 +49,7 @@ it("allow renegotiation in https module", async () => {
path: url.pathname,
method: "GET",
keepalive: false,
tls: { rejectUnauthorized: false },
rejectUnauthorized: false,
},
(res: IncomingMessage) => {
res.setEncoding("utf8");
@@ -79,7 +79,7 @@ it("should fail if renegotiation fails using https", async () => {
path: url.pathname,
method: "GET",
keepalive: false,
tls: { rejectUnauthorized: true },
rejectUnauthorized: true,
},
(res: IncomingMessage) => {
res.setEncoding("utf8");

View File

@@ -11,7 +11,13 @@ var oks = 0;
var textLength = 0;
Bun.gc(true);
const baseline = await (async function runAll() {
const resp = await fetch(SERVER);
const tls =
process.env.NAME === "tls-with-client"
? {
cert: "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKLdQVPy90jjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTkwMjAzMTQ0OTM1WhcNMjAwMjAzMTQ0OTM1WjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7i7IIEdICTiSTVx+ma6xHxOtcbd6wGW3nkxlCkJ1UuV8NmY5ovMsGnGD\nhJJtUQ2j5ig5BcJUf3tezqCNW4tKnSOgSISfEAKvpn2BPvaFq3yx2Yjz0ruvcGKp\nDMZBXmB/AAtGyN/UFXzkrcfppmLHJTaBYGG6KnmU43gPkSDy4iw46CJFUOupc51A\nFIz7RsE7mbT1plCM8e75gfqaZSn2k+Wmy+8n1HGyYHhVISRVvPqkS7gVLSVEdTea\nUtKP1Vx/818/HDWk3oIvDVWI9CFH73elNxBkMH5zArSNIBTehdnehyAevjY4RaC/\nkK8rslO3e4EtJ9SnA4swOjCiqAIQEwIDAQABo1AwTjAdBgNVHQ4EFgQUv5rc9Smm\n9c4YnNf3hR49t4rH4yswHwYDVR0jBBgwFoAUv5rc9Smm9c4YnNf3hR49t4rH4ysw\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATcL9CAAXg0u//eYUAlQa\nL+l8yKHS1rsq1sdmx7pvsmfZ2g8ONQGfSF3TkzkI2OOnCBokeqAYuyT8awfdNUtE\nEHOihv4ZzhK2YZVuy0fHX2d4cCFeQpdxno7aN6B37qtsLIRZxkD8PU60Dfu9ea5F\nDDynnD0TUabna6a0iGn77yD8GPhjaJMOz3gMYjQFqsKL252isDVHEDbpVxIzxPmN\nw1+WK8zRNdunAcHikeoKCuAPvlZ83gDQHp07dYdbuZvHwGj0nfxBLc9qt90XsBtC\n4IYR7c/bcLMmKXYf0qoQ4OzngsnPI5M+v9QEHvYWaKVwFY4CTcSNJEwfXw+BAeO5\nOA==\n-----END CERTIFICATE-----",
}
: null;
const resp = await fetch(SERVER, { tls });
textLength = Number(resp.headers.get("Content-Length"));
if (!textLength) {
throw new Error("Content-Length header is not set");
@@ -24,7 +30,13 @@ Bun.gc(true);
for (let j = 0; j < COUNT; j++) {
await (async function runAll() {
oks += !!(await (await fetch(SERVER)).arrayBuffer())?.byteLength;
const tls =
process.env.NAME === "tls-with-client"
? {
cert: "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAKLdQVPy90jjMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTkwMjAzMTQ0OTM1WhcNMjAwMjAzMTQ0OTM1WjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7i7IIEdICTiSTVx+ma6xHxOtcbd6wGW3nkxlCkJ1UuV8NmY5ovMsGnGD\nhJJtUQ2j5ig5BcJUf3tezqCNW4tKnSOgSISfEAKvpn2BPvaFq3yx2Yjz0ruvcGKp\nDMZBXmB/AAtGyN/UFXzkrcfppmLHJTaBYGG6KnmU43gPkSDy4iw46CJFUOupc51A\nFIz7RsE7mbT1plCM8e75gfqaZSn2k+Wmy+8n1HGyYHhVISRVvPqkS7gVLSVEdTea\nUtKP1Vx/818/HDWk3oIvDVWI9CFH73elNxBkMH5zArSNIBTehdnehyAevjY4RaC/\nkK8rslO3e4EtJ9SnA4swOjCiqAIQEwIDAQABo1AwTjAdBgNVHQ4EFgQUv5rc9Smm\n9c4YnNf3hR49t4rH4yswHwYDVR0jBBgwFoAUv5rc9Smm9c4YnNf3hR49t4rH4ysw\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATcL9CAAXg0u//eYUAlQa\nL+l8yKHS1rsq1sdmx7pvsmfZ2g8ONQGfSF3TkzkI2OOnCBokeqAYuyT8awfdNUtE\nEHOihv4ZzhK2YZVuy0fHX2d4cCFeQpdxno7aN6B37qtsLIRZxkD8PU60Dfu9ea5F\nDDynnD0TUabna6a0iGn77yD8GPhjaJMOz3gMYjQFqsKL252isDVHEDbpVxIzxPmN\nw1+WK8zRNdunAcHikeoKCuAPvlZ83gDQHp07dYdbuZvHwGj0nfxBLc9qt90XsBtC\n4IYR7c/bcLMmKXYf0qoQ4OzngsnPI5M+v9QEHvYWaKVwFY4CTcSNJEwfXw+BAeO5\nOA==\n-----END CERTIFICATE-----",
}
: null;
oks += !!(await (await fetch(SERVER, { tls })).arrayBuffer())?.byteLength;
})();
}

View File

@@ -32,10 +32,12 @@ describe("fetch doesn't leak", () => {
});
// This tests for body leakage and Response object leakage.
async function runTest(compressed, tls) {
async function runTest(compressed, name) {
const body = !compressed
? new Blob(["some body in here!".repeat(2000000)])
: new Blob([Bun.deflateSync(crypto.getRandomValues(new Buffer(65123)))]);
const tls = name.includes("tls");
const headers = {
"Content-Type": "application/octet-stream",
};
@@ -60,6 +62,7 @@ describe("fetch doesn't leak", () => {
...bunEnv,
SERVER: `${tls ? "https" : "http"}://${server.hostname}:${server.port}`,
BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString("10"),
NAME: name,
};
if (tls) {
@@ -83,10 +86,10 @@ describe("fetch doesn't leak", () => {
for (let compressed of [true, false]) {
describe(compressed ? "compressed" : "uncompressed", () => {
for (let tls of [true, false]) {
describe(tls ? "tls" : "tcp", () => {
for (let name of ["tcp", "tls", "tls-with-client"]) {
describe(name, () => {
test("fixture #2", async () => {
await runTest(compressed, tls);
await runTest(compressed, name);
}, 100000);
});
}