From ee30e8660c673d32c410281dad92daa8ebab554c Mon Sep 17 00:00:00 2001 From: Ludvig Hozman Date: Tue, 11 Jun 2024 15:36:32 +0200 Subject: [PATCH] feat(https/fetch): Support custom ca/cert/key in fetch (#11322) Co-authored-by: Liz3 (Yann HN) --- src/bun.js/api/server.zig | 83 +++++++++++++ src/bun.js/webcore/response.zig | 44 +++++-- src/http.zig | 65 ++++++++++ src/js/node/http.ts | 52 +++++++- .../node/http/fixtures/openssl_localhost.crt | 26 ++++ .../node/http/fixtures/openssl_localhost.key | 28 +++++ .../http/fixtures/openssl_localhost_ca.pem | 29 +++++ test/js/node/http/node-http.test.ts | 115 +++++++++++++++++- test/js/node/tls/renegotiation.test.ts | 4 +- .../js/web/fetch/fetch-leak-test-fixture-2.js | 16 ++- test/js/web/fetch/fetch-leak.test.js | 11 +- 11 files changed, 453 insertions(+), 20 deletions(-) create mode 100644 test/js/node/http/fixtures/openssl_localhost.crt create mode 100644 test/js/node/http/fixtures/openssl_localhost.key create mode 100644 test/js/node/http/fixtures/openssl_localhost_ca.pem diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 7a50d897a4..6a72b6b785 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -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 { diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 3a51889e16..0ff3e0aba0 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -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), diff --git a/src/http.zig b/src/http.zig index 4fb8c35dd9..ef008f4c36 100644 --- a/src/http.zig +++ b/src/http.zig @@ -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 diff --git a/src/js/node/http.ts b/src/js/node/http.ts index eeedcd945d..8c62b424c0 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -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) { diff --git a/test/js/node/http/fixtures/openssl_localhost.crt b/test/js/node/http/fixtures/openssl_localhost.crt new file mode 100644 index 0000000000..ffef6e4b86 --- /dev/null +++ b/test/js/node/http/fixtures/openssl_localhost.crt @@ -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----- diff --git a/test/js/node/http/fixtures/openssl_localhost.key b/test/js/node/http/fixtures/openssl_localhost.key new file mode 100644 index 0000000000..d293a804bc --- /dev/null +++ b/test/js/node/http/fixtures/openssl_localhost.key @@ -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----- diff --git a/test/js/node/http/fixtures/openssl_localhost_ca.pem b/test/js/node/http/fixtures/openssl_localhost_ca.pem new file mode 100644 index 0000000000..d4668a6069 --- /dev/null +++ b/test/js/node/http/fixtures/openssl_localhost_ca.pem @@ -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----- diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index 4658956d27..b12af1142c 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -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(); +}); diff --git a/test/js/node/tls/renegotiation.test.ts b/test/js/node/tls/renegotiation.test.ts index 14393034a0..ca390baa7f 100644 --- a/test/js/node/tls/renegotiation.test.ts +++ b/test/js/node/tls/renegotiation.test.ts @@ -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"); diff --git a/test/js/web/fetch/fetch-leak-test-fixture-2.js b/test/js/web/fetch/fetch-leak-test-fixture-2.js index 26cbdfdb7b..3fcc6de3dd 100644 --- a/test/js/web/fetch/fetch-leak-test-fixture-2.js +++ b/test/js/web/fetch/fetch-leak-test-fixture-2.js @@ -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; })(); } diff --git a/test/js/web/fetch/fetch-leak.test.js b/test/js/web/fetch/fetch-leak.test.js index a248273a87..dfcadeb51c 100644 --- a/test/js/web/fetch/fetch-leak.test.js +++ b/test/js/web/fetch/fetch-leak.test.js @@ -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); }); }