diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 4af518744556a..1bfcd540e5bb8 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -370,6 +370,19 @@ myorg = { username = "myusername", password = "$npm_password", url = "https://re myorg = { token = "$npm_token", url = "https://registry.myorg.com/" } ``` +### `install.ca` and `install.cafile` + +To configure a CA certificate, use `install.ca` or `install.cafile` to specify a path to a CA certificate file. + +```toml +[install] +# The CA certificate as a string +ca = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" + +# A path to a CA certificate file. The file can contain multiple certificates. +cafile = "path/to/cafile" +``` + ### `install.cache` To configure the cache behavior: diff --git a/packages/bun-usockets/src/context.c b/packages/bun-usockets/src/context.c index a59c80e83a0c5..664f7dabdd477 100644 --- a/packages/bun-usockets/src/context.c +++ b/packages/bun-usockets/src/context.c @@ -278,11 +278,11 @@ struct us_socket_context_t *us_create_socket_context(int ssl, struct us_loop_t * return context; } -struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options) { +struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, int context_ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err) { #ifndef LIBUS_NO_SSL if (ssl) { /* This function will call us, again, with SSL = false and a bigger ext_size */ - return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options); + return (struct us_socket_context_t *) us_internal_bun_create_ssl_socket_context(loop, context_ext_size, options, err); } #endif diff --git a/packages/bun-usockets/src/crypto/openssl.c b/packages/bun-usockets/src/crypto/openssl.c index 232d5f8ff9c16..2c0420109595b 100644 --- a/packages/bun-usockets/src/crypto/openssl.c +++ b/packages/bun-usockets/src/crypto/openssl.c @@ -1104,7 +1104,8 @@ int us_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { } SSL_CTX *create_ssl_context_from_bun_options( - struct us_bun_socket_context_options_t options) { + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err) { /* Create the context */ SSL_CTX *ssl_context = SSL_CTX_new(TLS_method()); @@ -1174,6 +1175,7 @@ SSL_CTX *create_ssl_context_from_bun_options( STACK_OF(X509_NAME) * ca_list; ca_list = SSL_load_client_CA_file(options.ca_file_name); if (ca_list == NULL) { + *err = CREATE_BUN_SOCKET_ERROR_LOAD_CA_FILE; free_ssl_context(ssl_context); return NULL; } @@ -1181,6 +1183,7 @@ SSL_CTX *create_ssl_context_from_bun_options( SSL_CTX_set_client_CA_list(ssl_context, ca_list); if (SSL_CTX_load_verify_locations(ssl_context, options.ca_file_name, NULL) != 1) { + *err = CREATE_BUN_SOCKET_ERROR_INVALID_CA_FILE; free_ssl_context(ssl_context); return NULL; } @@ -1203,6 +1206,7 @@ SSL_CTX *create_ssl_context_from_bun_options( } if (!add_ca_cert_to_ctx_store(ssl_context, options.ca[i], cert_store)) { + *err = CREATE_BUN_SOCKET_ERROR_INVALID_CA; free_ssl_context(ssl_context); return NULL; } @@ -1338,7 +1342,8 @@ void us_bun_internal_ssl_socket_context_add_server_name( struct us_bun_socket_context_options_t options, void *user) { /* Try and construct an SSL_CTX from options */ - SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options, &err); /* Attach the user data to this context */ if (1 != SSL_CTX_set_ex_data(ssl_context, 0, user)) { @@ -1468,14 +1473,15 @@ struct us_internal_ssl_socket_context_t *us_internal_create_ssl_socket_context( struct us_internal_ssl_socket_context_t * us_internal_bun_create_ssl_socket_context( struct us_loop_t *loop, int context_ext_size, - struct us_bun_socket_context_options_t options) { + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err) { /* If we haven't initialized the loop data yet, do so . * This is needed because loop data holds shared OpenSSL data and * the function is also responsible for initializing OpenSSL */ us_internal_init_loop_ssl_data(loop); /* First of all we try and create the SSL context from options */ - SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options); + SSL_CTX *ssl_context = create_ssl_context_from_bun_options(options, err); if (!ssl_context) { /* We simply fail early if we cannot even create the OpenSSL context */ return NULL; @@ -1487,7 +1493,7 @@ us_internal_bun_create_ssl_socket_context( (struct us_internal_ssl_socket_context_t *)us_create_bun_socket_context( 0, loop, sizeof(struct us_internal_ssl_socket_context_t) + context_ext_size, - options); + options, err); /* I guess this is the only optional callback */ context->on_server_name = NULL; @@ -1983,9 +1989,10 @@ struct us_internal_ssl_socket_t *us_internal_ssl_socket_wrap_with_tls( struct us_socket_context_t *old_context = us_socket_context(0, s); us_socket_context_ref(0,old_context); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; struct us_socket_context_t *context = us_create_bun_socket_context( 1, old_context->loop, sizeof(struct us_wrapped_socket_context_t), - options); + options, &err); // Handle SSL context creation failure if (UNLIKELY(!context)) { diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index 8c6c717504729..abc24a4e8300c 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -330,7 +330,8 @@ struct us_internal_ssl_socket_context_t *us_internal_create_ssl_socket_context( struct us_internal_ssl_socket_context_t * us_internal_bun_create_ssl_socket_context( struct us_loop_t *loop, int context_ext_size, - struct us_bun_socket_context_options_t options); + struct us_bun_socket_context_options_t options, + enum create_bun_socket_error_t *err); void us_internal_ssl_socket_context_free( us_internal_ssl_socket_context_r context); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index b939af53efb20..e4a568cea1ca9 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -246,8 +246,16 @@ void *us_socket_context_get_native_handle(int ssl, us_socket_context_r context); /* A socket context holds shared callbacks and user data extension for associated sockets */ struct us_socket_context_t *us_create_socket_context(int ssl, us_loop_r loop, int ext_size, struct us_socket_context_options_t options) nonnull_fn_decl; + +enum create_bun_socket_error_t { + CREATE_BUN_SOCKET_ERROR_NONE = 0, + CREATE_BUN_SOCKET_ERROR_LOAD_CA_FILE, + CREATE_BUN_SOCKET_ERROR_INVALID_CA_FILE, + CREATE_BUN_SOCKET_ERROR_INVALID_CA, +}; + struct us_socket_context_t *us_create_bun_socket_context(int ssl, struct us_loop_t *loop, - int ext_size, struct us_bun_socket_context_options_t options); + int ext_size, struct us_bun_socket_context_options_t options, enum create_bun_socket_error_t *err); /* Delete resources allocated at creation time (will call unref now and only free when ref count == 0). */ void us_socket_context_free(int ssl, us_socket_context_r context) nonnull_fn_decl; diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index 338683f816890..0081779bdaf08 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -433,7 +433,8 @@ struct HttpContext { static HttpContext *create(Loop *loop, us_bun_socket_context_options_t options = {}) { HttpContext *httpContext; - httpContext = (HttpContext *) us_create_bun_socket_context(SSL, (us_loop_t *) loop, sizeof(HttpContextData), options); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + httpContext = (HttpContext *) us_create_bun_socket_context(SSL, (us_loop_t *) loop, sizeof(HttpContextData), options, &err); if (!httpContext) { return nullptr; diff --git a/src/api/schema.zig b/src/api/schema.zig index a7b958c8a5ee5..1c3679be8d434 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2974,6 +2974,13 @@ pub const Api = struct { /// concurrent_scripts concurrent_scripts: ?u32 = null, + cafile: ?[]const u8 = null, + + ca: ?union(enum) { + str: []const u8, + list: []const []const u8, + } = null, + pub fn decode(reader: anytype) anyerror!BunInstall { var this = std.mem.zeroes(BunInstall); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index d2f1d43c03d80..7d38576bc1c46 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -642,15 +642,20 @@ pub const Listener = struct { } } } - const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl); + const ctx_opts: uws.us_bun_socket_context_options_t = if (ssl != null) + JSC.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + else + .{}; vm.eventLoop().ensureWaker(); + var create_err: uws.create_bun_socket_error_t = .none; const socket_context = uws.us_create_bun_socket_context( @intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts, + &create_err, ) orelse { var err = globalObject.createErrorInstance("Failed to listen on {s}:{d}", .{ hostname_or_unix.slice(), port orelse 0 }); defer { @@ -1172,9 +1177,13 @@ pub const Listener = struct { } } - const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(socket_config.ssl); + const ctx_opts: uws.us_bun_socket_context_options_t = if (ssl != null) + JSC.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + else + .{}; - const socket_context = uws.us_create_bun_socket_context(@intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts) orelse { + var create_err: uws.create_bun_socket_error_t = .none; + const socket_context = uws.us_create_bun_socket_context(@intFromBool(ssl_enabled), uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err) orelse { const err = JSC.SystemError{ .message = bun.String.static("Failed to connect"), .syscall = bun.String.static("connect"), diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 755a6a9d4c5d1..5b176dba47e3c 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -583,41 +583,39 @@ pub const ServerConfig = struct { const log = Output.scoped(.SSLConfig, false); - pub fn asUSockets(this_: ?SSLConfig) uws.us_bun_socket_context_options_t { + pub fn asUSockets(this: SSLConfig) uws.us_bun_socket_context_options_t { var ctx_opts: uws.us_bun_socket_context_options_t = .{}; - if (this_) |ssl_config| { - if (ssl_config.key_file_name != null) - ctx_opts.key_file_name = ssl_config.key_file_name; - if (ssl_config.cert_file_name != null) - ctx_opts.cert_file_name = ssl_config.cert_file_name; - if (ssl_config.ca_file_name != null) - ctx_opts.ca_file_name = ssl_config.ca_file_name; - if (ssl_config.dh_params_file_name != null) - ctx_opts.dh_params_file_name = ssl_config.dh_params_file_name; - if (ssl_config.passphrase != null) - ctx_opts.passphrase = ssl_config.passphrase; - ctx_opts.ssl_prefer_low_memory_usage = @intFromBool(ssl_config.low_memory_mode); + 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 (ssl_config.key) |key| { - ctx_opts.key = key.ptr; - ctx_opts.key_count = ssl_config.key_count; - } - if (ssl_config.cert) |cert| { - ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = ssl_config.cert_count; - } - if (ssl_config.ca) |ca| { - ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = ssl_config.ca_count; - } + if (this.key) |key| { + ctx_opts.key = key.ptr; + ctx_opts.key_count = this.key_count; + } + if (this.cert) |cert| { + ctx_opts.cert = cert.ptr; + ctx_opts.cert_count = this.cert_count; + } + if (this.ca) |ca| { + ctx_opts.ca = ca.ptr; + ctx_opts.ca_count = this.ca_count; + } - if (ssl_config.ssl_ciphers != null) { - ctx_opts.ssl_ciphers = ssl_config.ssl_ciphers; - } - ctx_opts.request_cert = ssl_config.request_cert; - ctx_opts.reject_unauthorized = ssl_config.reject_unauthorized; + 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; } diff --git a/src/bun.js/bindings/ScriptExecutionContext.cpp b/src/bun.js/bindings/ScriptExecutionContext.cpp index 06e5b7ddba82c..34534d6369b04 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.cpp +++ b/src/bun.js/bindings/ScriptExecutionContext.cpp @@ -60,7 +60,8 @@ us_socket_context_t* ScriptExecutionContext::webSocketContextSSL() opts.request_cert = true; // but do not reject unauthorized opts.reject_unauthorized = false; - this->m_ssl_client_websockets_ctx = us_create_bun_socket_context(1, loop, sizeof(size_t), opts); + enum create_bun_socket_error_t err = CREATE_BUN_SOCKET_ERROR_NONE; + this->m_ssl_client_websockets_ctx = us_create_bun_socket_context(1, loop, sizeof(size_t), opts, &err); void** ptr = reinterpret_cast(us_socket_context_ext(1, m_ssl_client_websockets_ctx)); *ptr = this; registerHTTPContextForWebSocket(this, m_ssl_client_websockets_ctx, loop); diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 76d7d07aaa5e7..184c1f9cbbc06 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -1797,7 +1797,7 @@ pub const Fetch = struct { fetch_options: FetchOptions, promise: JSC.JSPromise.Strong, ) !*FetchTasklet { - http.HTTPThread.init(); + http.HTTPThread.init(&.{}); var node = try get( allocator, global, diff --git a/src/bun.zig b/src/bun.zig index efbdce6653379..2453cdcb4d057 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -718,7 +718,7 @@ pub const Analytics = @import("./analytics/analytics_thread.zig"); pub usingnamespace @import("./tagged_pointer.zig"); -pub fn once(comptime function: anytype, comptime ReturnType: type) ReturnType { +pub fn onceUnsafe(comptime function: anytype, comptime ReturnType: type) ReturnType { const Result = struct { var value: ReturnType = undefined; var ran = false; @@ -3938,3 +3938,43 @@ pub fn indexOfPointerInSlice(comptime T: type, slice: []const T, item: *const T) const index = @divExact(offset, @sizeOf(T)); return index; } + +/// Copied from zig std. Modified to accept arguments. +pub fn once(comptime f: anytype) Once(f) { + return Once(f){}; +} + +/// Copied from zig std. Modified to accept arguments. +/// +/// An object that executes the function `f` just once. +/// It is undefined behavior if `f` re-enters the same Once instance. +pub fn Once(comptime f: anytype) type { + return struct { + done: bool = false, + mutex: std.Thread.Mutex = std.Thread.Mutex{}, + + /// Call the function `f`. + /// If `call` is invoked multiple times `f` will be executed only the + /// first time. + /// The invocations are thread-safe. + pub fn call(self: *@This(), args: std.meta.ArgsTuple(@TypeOf(f))) void { + if (@atomicLoad(bool, &self.done, .acquire)) + return; + + return self.callSlow(args); + } + + fn callSlow(self: *@This(), args: std.meta.ArgsTuple(@TypeOf(f))) void { + @setCold(true); + + self.mutex.lock(); + defer self.mutex.unlock(); + + // The first thread to acquire the mutex gets to run the initializer + if (!self.done) { + @call(.auto, f, args); + @atomicStore(bool, &self.done, true, .release); + } + } + }; +} diff --git a/src/bun_js.zig b/src/bun_js.zig index e5eff889cef6f..bb8b1e8c48f7f 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -127,7 +127,7 @@ pub const Run = struct { fn doPreconnect(preconnect: []const string) void { if (preconnect.len == 0) return; - bun.HTTPThread.init(); + bun.HTTPThread.init(&.{}); for (preconnect) |url_str| { const url = bun.URL.parse(url_str); diff --git a/src/bunfig.zig b/src/bunfig.zig index f141edcd2fec0..0ebfb9cb5dfa2 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -336,15 +336,15 @@ pub const Bunfig = struct { } if (comptime cmd.isNPMRelated() or cmd == .RunCommand or cmd == .AutoCommand) { - if (json.get("install")) |_bun| { + if (json.getObject("install")) |install_obj| { var install: *Api.BunInstall = this.ctx.install orelse brk: { - const install_ = try this.allocator.create(Api.BunInstall); - install_.* = std.mem.zeroes(Api.BunInstall); - this.ctx.install = install_; - break :brk install_; + const install = try this.allocator.create(Api.BunInstall); + install.* = std.mem.zeroes(Api.BunInstall); + this.ctx.install = install; + break :brk install; }; - if (_bun.get("auto")) |auto_install_expr| { + if (install_obj.get("auto")) |auto_install_expr| { if (auto_install_expr.data == .e_string) { this.ctx.debug.global_cache = options.GlobalCache.Map.get(auto_install_expr.asString(this.allocator) orelse "") orelse { try this.addError(auto_install_expr.loc, "Invalid auto install setting, must be one of true, false, or \"force\" \"fallback\" \"disable\""); @@ -361,13 +361,46 @@ pub const Bunfig = struct { } } - if (_bun.get("exact")) |exact| { + if (install_obj.get("cafile")) |cafile| { + install.cafile = try cafile.asStringCloned(allocator) orelse { + try this.addError(cafile.loc, "Invalid cafile. Expected a string."); + return; + }; + } + + if (install_obj.get("ca")) |ca| { + switch (ca.data) { + .e_array => |arr| { + var list = try allocator.alloc([]const u8, arr.items.len); + for (arr.items.slice(), 0..) |item, i| { + list[i] = try item.asStringCloned(allocator) orelse { + try this.addError(item.loc, "Invalid CA. Expected a string."); + return; + }; + } + install.ca = .{ + .list = list, + }; + }, + .e_string => |str| { + install.ca = .{ + .str = try str.stringCloned(allocator), + }; + }, + else => { + try this.addError(ca.loc, "Invalid CA. Expected a string or an array of strings."); + return; + }, + } + } + + if (install_obj.get("exact")) |exact| { if (exact.asBool()) |value| { install.exact = value; } } - if (_bun.get("prefer")) |prefer_expr| { + if (install_obj.get("prefer")) |prefer_expr| { try this.expectString(prefer_expr); if (Prefer.get(prefer_expr.asString(bun.default_allocator) orelse "")) |setting| { @@ -377,11 +410,11 @@ pub const Bunfig = struct { } } - if (_bun.get("registry")) |registry| { + if (install_obj.get("registry")) |registry| { install.default_registry = try this.parseRegistry(registry); } - if (_bun.get("scopes")) |scopes| { + if (install_obj.get("scopes")) |scopes| { var registry_map = install.scoped orelse Api.NpmRegistryMap{}; try this.expect(scopes, .e_object); @@ -399,32 +432,32 @@ pub const Bunfig = struct { install.scoped = registry_map; } - if (_bun.get("dryRun")) |dry_run| { + if (install_obj.get("dryRun")) |dry_run| { if (dry_run.asBool()) |value| { install.dry_run = value; } } - if (_bun.get("production")) |production| { + if (install_obj.get("production")) |production| { if (production.asBool()) |value| { install.production = value; } } - if (_bun.get("frozenLockfile")) |frozen_lockfile| { + if (install_obj.get("frozenLockfile")) |frozen_lockfile| { if (frozen_lockfile.asBool()) |value| { install.frozen_lockfile = value; } } - if (_bun.get("concurrentScripts")) |jobs| { + if (install_obj.get("concurrentScripts")) |jobs| { if (jobs.data == .e_number) { install.concurrent_scripts = jobs.data.e_number.toU32(); if (install.concurrent_scripts.? == 0) install.concurrent_scripts = null; } } - if (_bun.get("lockfile")) |lockfile_expr| { + if (install_obj.get("lockfile")) |lockfile_expr| { if (lockfile_expr.get("print")) |lockfile| { try this.expectString(lockfile); if (lockfile.asString(this.allocator)) |value| { @@ -457,41 +490,41 @@ pub const Bunfig = struct { } } - if (_bun.get("optional")) |optional| { + if (install_obj.get("optional")) |optional| { if (optional.asBool()) |value| { install.save_optional = value; } } - if (_bun.get("peer")) |optional| { + if (install_obj.get("peer")) |optional| { if (optional.asBool()) |value| { install.save_peer = value; } } - if (_bun.get("dev")) |optional| { + if (install_obj.get("dev")) |optional| { if (optional.asBool()) |value| { install.save_dev = value; } } - if (_bun.get("globalDir")) |dir| { + if (install_obj.get("globalDir")) |dir| { if (dir.asString(allocator)) |value| { install.global_dir = value; } } - if (_bun.get("globalBinDir")) |dir| { + if (install_obj.get("globalBinDir")) |dir| { if (dir.asString(allocator)) |value| { install.global_bin_dir = value; } } - if (_bun.get("logLevel")) |expr| { + if (install_obj.get("logLevel")) |expr| { try this.loadLogLevel(expr); } - if (_bun.get("cache")) |cache| { + if (install_obj.get("cache")) |cache| { load: { if (cache.asBool()) |value| { if (!value) { diff --git a/src/cli/create_command.zig b/src/cli/create_command.zig index 16ac76623ecc3..2d6577a4be0d2 100644 --- a/src/cli/create_command.zig +++ b/src/cli/create_command.zig @@ -241,7 +241,7 @@ pub const CreateCommand = struct { @setCold(true); Global.configureAllocator(.{ .long_running = false }); - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var create_options = try CreateOptions.parse(ctx); const positionals = create_options.positionals; diff --git a/src/cli/init_command.zig b/src/cli/init_command.zig index 16d4d407a701e..86f6efd224c11 100644 --- a/src/cli/init_command.zig +++ b/src/cli/init_command.zig @@ -21,10 +21,9 @@ const initializeStore = @import("./create_command.zig").initializeStore; const lex = bun.js_lexer; const logger = bun.logger; const JSPrinter = bun.js_printer; +const exists = bun.sys.exists; +const existsZ = bun.sys.existsZ; -fn exists(path: anytype) bool { - return bun.sys.exists(path); -} pub const InitCommand = struct { pub fn prompt( alloc: std.mem.Allocator, @@ -210,7 +209,7 @@ pub const InitCommand = struct { }; for (paths_to_try) |path| { - if (exists(path)) { + if (existsZ(path)) { fields.entry_point = bun.asByteSlice(path); break :infer; } @@ -279,16 +278,16 @@ pub const InitCommand = struct { var steps = Steps{}; - steps.write_gitignore = !exists(".gitignore"); + steps.write_gitignore = !existsZ(".gitignore"); - steps.write_readme = !exists("README.md") and !exists("README") and !exists("README.txt") and !exists("README.mdx"); + steps.write_readme = !existsZ("README.md") and !existsZ("README") and !existsZ("README.txt") and !existsZ("README.mdx"); steps.write_tsconfig = brk: { - if (exists("tsconfig.json")) { + if (existsZ("tsconfig.json")) { break :brk false; } - if (exists("jsconfig.json")) { + if (existsZ("jsconfig.json")) { break :brk false; } @@ -444,7 +443,7 @@ pub const InitCommand = struct { Output.flush(); - if (exists("package.json")) { + if (existsZ("package.json")) { var process = std.process.Child.init( &.{ try bun.selfExePath(), diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig index b3b2604d77f78..b0f1000b5bd45 100644 --- a/src/cli/test_command.zig +++ b/src/cli/test_command.zig @@ -741,7 +741,7 @@ pub const TestCommand = struct { break :brk loader; }; bun.JSC.initialize(false); - HTTPThread.init(); + HTTPThread.init(&.{}); var snapshot_file_buf = std.ArrayList(u8).init(ctx.allocator); var snapshot_values = Snapshots.ValuesHashMap.init(ctx.allocator); diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index c75452a0fdfd4..b89d1777addb0 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -133,7 +133,7 @@ pub const UpgradeCheckerThread = struct { std.time.sleep(std.time.ns_per_ms * delay); Output.Source.configureThread(); - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); defer { js_ast.Expr.Data.Store.deinit(); @@ -440,7 +440,7 @@ pub const UpgradeCommand = struct { } fn _exec(ctx: Command.Context) !void { - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var filesystem = try fs.FileSystem.init(null); var env_loader: DotEnv.Loader = brk: { diff --git a/src/compile_target.zig b/src/compile_target.zig index a6ec5f076c7ae..bd060d24bb281 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -137,7 +137,7 @@ const HTTP = bun.http; const MutableString = bun.MutableString; const Global = bun.Global; pub fn downloadToPath(this: *const CompileTarget, env: *bun.DotEnv.Loader, allocator: std.mem.Allocator, dest_z: [:0]const u8) !void { - HTTP.HTTPThread.init(); + HTTP.HTTPThread.init(&.{}); var refresher = bun.Progress{}; { diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 102858501ac26..3e3f92adf773d 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -2539,6 +2539,13 @@ pub const us_bun_socket_context_options_t = extern struct { }; 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(i32) { + none = 0, + load_ca_file, + invalid_ca_file, + invalid_ca, +}; + pub const us_bun_verify_error_t = extern struct { error_no: i32 = 0, code: [*c]const u8 = null, @@ -2568,7 +2575,7 @@ pub extern fn us_socket_context_remove_server_name(ssl: i32, context: ?*SocketCo extern fn us_socket_context_on_server_name(ssl: i32, context: ?*SocketContext, cb: ?*const fn (?*SocketContext, [*c]const u8) callconv(.C) void) void; extern fn us_socket_context_get_native_handle(ssl: i32, context: ?*SocketContext) ?*anyopaque; pub extern fn us_create_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_socket_context_options_t) ?*SocketContext; -pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t) ?*SocketContext; +pub extern fn us_create_bun_socket_context(ssl: i32, loop: ?*Loop, ext_size: i32, options: us_bun_socket_context_options_t, err: *create_bun_socket_error_t) ?*SocketContext; pub extern fn us_bun_socket_context_add_server_name(ssl: i32, context: ?*SocketContext, hostname_pattern: [*c]const u8, options: us_bun_socket_context_options_t, ?*anyopaque) void; pub extern fn us_socket_context_free(ssl: i32, context: ?*SocketContext) void; pub extern fn us_socket_context_ref(ssl: i32, context: ?*SocketContext) void; diff --git a/src/http.zig b/src/http.zig index c2c0da7ba577b..de3a58fbec4d4 100644 --- a/src/http.zig +++ b/src/http.zig @@ -516,6 +516,13 @@ pub const HTTPCertError = struct { reason: [:0]const u8 = "", }; +pub const InitError = error{ + FailedToOpenSocket, + LoadCAFile, + InvalidCAFile, + InvalidCA, +}; + fn NewHTTPContext(comptime ssl: bool) type { return struct { const pool_size = 64; @@ -585,16 +592,30 @@ fn NewHTTPContext(comptime ssl: bool) type { bun.default_allocator.destroy(this); } - pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) !void { + pub fn initWithClientConfig(this: *@This(), client: *HTTPClient) InitError!void { if (!comptime ssl) { - unreachable; + @compileError("ssl only"); } 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); + try this.initWithOpts(&opts); + } + + fn initWithOpts(this: *@This(), opts: *const uws.us_bun_socket_context_options_t) InitError!void { + if (!comptime ssl) { + @compileError("ssl only"); + } + + var err: uws.create_bun_socket_error_t = .none; + const socket = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts.*, &err); if (socket == null) { - return error.FailedToOpenSocket; + return switch (err) { + .load_ca_file => error.LoadCAFile, + .invalid_ca_file => error.InvalidCAFile, + .invalid_ca => error.InvalidCA, + else => error.FailedToOpenSocket, + }; } this.us_socket_context = socket.?; this.sslCtx().setup(); @@ -607,7 +628,21 @@ fn NewHTTPContext(comptime ssl: bool) type { ); } - pub fn init(this: *@This()) !void { + pub fn initWithThreadOpts(this: *@This(), init_opts: *const HTTPThread.InitOpts) InitError!void { + if (!comptime ssl) { + @compileError("ssl only"); + } + var opts: uws.us_bun_socket_context_options_t = .{ + .ca = if (init_opts.ca.len > 0) @ptrCast(init_opts.ca) else null, + .ca_count = @intCast(init_opts.ca.len), + .ca_file_name = if (init_opts.abs_ca_file_name.len > 0) init_opts.abs_ca_file_name else null, + .request_cert = 1, + }; + + try this.initWithOpts(&opts); + } + + pub fn init(this: *@This()) void { if (comptime ssl) { const opts: uws.us_bun_socket_context_options_t = .{ // we request the cert so we load root certs and can verify it @@ -615,7 +650,8 @@ fn NewHTTPContext(comptime ssl: bool) type { // we manually abort the connection if the hostname doesn't match .reject_unauthorized = 0, }; - this.us_socket_context = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts).?; + var err: uws.create_bun_socket_error_t = .none; + this.us_socket_context = uws.us_create_bun_socket_context(ssl_int, http_thread.loop.loop, @sizeOf(usize), opts, &err).?; this.sslCtx().setup(); } else { @@ -1005,7 +1041,37 @@ pub const HTTPThread = struct { return this.lazy_libdeflater.?; } - fn initOnce() void { + fn onInitErrorNoop(err: InitError, opts: InitOpts) noreturn { + switch (err) { + error.LoadCAFile => { + if (!bun.sys.existsZ(opts.abs_ca_file_name)) { + Output.err("HTTPThread", "failed to find CA file: '{s}'", .{opts.abs_ca_file_name}); + } else { + Output.err("HTTPThread", "failed to load CA file: '{s}'", .{opts.abs_ca_file_name}); + } + }, + error.InvalidCAFile => { + Output.err("HTTPThread", "the CA file is invalid: '{s}'", .{opts.abs_ca_file_name}); + }, + error.InvalidCA => { + Output.err("HTTPThread", "the provided CA is invalid", .{}); + }, + error.FailedToOpenSocket => { + Output.errGeneric("failed to start HTTP client thread", .{}); + }, + } + Global.crash(); + } + + pub const InitOpts = struct { + ca: []stringZ = &.{}, + abs_ca_file_name: stringZ = &.{}, + for_install: bool = false, + + onInitError: *const fn (err: InitError, opts: InitOpts) noreturn = &onInitErrorNoop, + }; + + fn initOnce(opts: *const InitOpts) void { http_thread = .{ .loop = undefined, .http_context = .{ @@ -1022,17 +1088,17 @@ pub const HTTPThread = struct { .stack_size = bun.default_thread_stack_size, }, onStart, - .{}, + .{opts.*}, ) catch |err| Output.panic("Failed to start HTTP Client thread: {s}", .{@errorName(err)}); thread.detach(); } - var init_once = std.once(initOnce); + var init_once = bun.once(initOnce); - pub fn init() void { - init_once.call(); + pub fn init(opts: *const InitOpts) void { + init_once.call(.{opts}); } - pub fn onStart() void { + pub fn onStart(opts: InitOpts) void { Output.Source.configureNamedThread("HTTP Client"); default_arena = Arena.init() catch unreachable; default_allocator = default_arena.allocator(); @@ -1046,8 +1112,8 @@ pub const HTTPThread = struct { } http_thread.loop = loop; - http_thread.http_context.init() catch @panic("Failed to init http context"); - http_thread.https_context.init() catch @panic("Failed to init https context"); + http_thread.http_context.init(); + http_thread.https_context.initWithThreadOpts(&opts) catch |err| opts.onInitError(err, opts); http_thread.has_awoken.store(true, .monotonic); http_thread.processEvents(); } @@ -1084,7 +1150,14 @@ pub const HTTPThread = struct { requested_config.deinit(); bun.default_allocator.destroy(requested_config); bun.default_allocator.destroy(custom_context); - return err; + + // TODO: these error names reach js. figure out how they should be handled + return switch (err) { + error.FailedToOpenSocket => |e| e, + error.InvalidCA => error.FailedToOpenSocket, + error.InvalidCAFile => error.FailedToOpenSocket, + error.LoadCAFile => error.FailedToOpenSocket, + }; }; 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 @@ -2479,7 +2552,7 @@ pub const AsyncHTTP = struct { } pub fn sendSync(this: *AsyncHTTP) anyerror!picohttp.Response { - HTTPThread.init(); + HTTPThread.init(&.{}); var ctx = try bun.default_allocator.create(SingleHTTPChannel); ctx.* = SingleHTTPChannel.init(); diff --git a/src/ini.zig b/src/ini.zig index 0a2e9cb564999..cc9deecd0bbf4 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -962,6 +962,32 @@ pub fn loadNpmrc( } } + if (out.asProperty("ca")) |query| { + if (query.expr.asUtf8StringLiteral()) |str| { + install.ca = .{ + .str = str, + }; + } else if (query.expr.isArray()) { + const arr = query.expr.data.e_array; + var list = try allocator.alloc([]const u8, arr.items.len); + var i: usize = 0; + for (arr.items.slice()) |item| { + list[i] = try item.asStringCloned(allocator) orelse continue; + i += 1; + } + + install.ca = .{ + .list = list, + }; + } + } + + if (out.asProperty("cafile")) |query| { + if (try query.expr.asStringCloned(allocator)) |cafile| { + install.cafile = cafile; + } + } + var registry_map = install.scoped orelse bun.Schema.Api.NpmRegistryMap{}; // Process scopes diff --git a/src/install/install.zig b/src/install/install.zig index 95e6cb674801a..5ea3f2d8b5c70 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -6943,6 +6943,9 @@ pub const PackageManager = struct { publish_config: PublishConfig = .{}, + ca: []const string = &.{}, + ca_file_name: string = &.{}, + pub const PublishConfig = struct { access: ?Access = null, tag: string = "", @@ -7087,8 +7090,8 @@ pub const PackageManager = struct { .password = "", .token = "", }; - if (bun_install_) |bun_install| { - if (bun_install.default_registry) |registry| { + if (bun_install_) |config| { + if (config.default_registry) |registry| { base = registry; } } @@ -7097,8 +7100,8 @@ pub const PackageManager = struct { defer { this.did_override_default_scope = this.scope.url_hash != Npm.Registry.default_url_hash; } - if (bun_install_) |bun_install| { - if (bun_install.scoped) |scoped| { + if (bun_install_) |config| { + if (config.scoped) |scoped| { for (scoped.scopes.keys(), scoped.scopes.values()) |name, *registry_| { var registry = registry_.*; if (registry.url.len == 0) registry.url = base.url; @@ -7106,42 +7109,57 @@ pub const PackageManager = struct { } } - if (bun_install.disable_cache orelse false) { + if (config.ca) |ca| { + switch (ca) { + .list => |ca_list| { + this.ca = ca_list; + }, + .str => |ca_str| { + this.ca = &.{ca_str}; + }, + } + } + + if (config.cafile) |cafile| { + this.ca_file_name = cafile; + } + + if (config.disable_cache orelse false) { this.enable.cache = false; } - if (bun_install.disable_manifest_cache orelse false) { + if (config.disable_manifest_cache orelse false) { this.enable.manifest_cache = false; } - if (bun_install.force orelse false) { + if (config.force orelse false) { this.enable.manifest_cache_control = false; this.enable.force_install = true; } - if (bun_install.save_yarn_lockfile orelse false) { + if (config.save_yarn_lockfile orelse false) { this.do.save_yarn_lock = true; } - if (bun_install.save_lockfile) |save_lockfile| { + if (config.save_lockfile) |save_lockfile| { this.do.save_lockfile = save_lockfile; this.enable.force_save_lockfile = true; } - if (bun_install.save_dev) |save| { + if (config.save_dev) |save| { this.local_package_features.dev_dependencies = save; } - if (bun_install.save_peer) |save| { + if (config.save_peer) |save| { this.do.install_peer_dependencies = save; this.remote_package_features.peer_dependencies = save; } - if (bun_install.exact) |exact| { + if (config.exact) |exact| { this.enable.exact_versions = exact; } - if (bun_install.production) |production| { + if (config.production) |production| { if (production) { this.local_package_features.dev_dependencies = false; this.enable.fail_early = true; @@ -7150,22 +7168,22 @@ pub const PackageManager = struct { } } - if (bun_install.frozen_lockfile) |frozen_lockfile| { + if (config.frozen_lockfile) |frozen_lockfile| { if (frozen_lockfile) { this.enable.frozen_lockfile = true; } } - if (bun_install.concurrent_scripts) |jobs| { + if (config.concurrent_scripts) |jobs| { this.max_concurrent_lifecycle_scripts = jobs; } - if (bun_install.save_optional) |save| { + if (config.save_optional) |save| { this.remote_package_features.optional_dependencies = save; this.local_package_features.optional_dependencies = save; } - this.explicit_global_directory = bun_install.global_dir orelse this.explicit_global_directory; + this.explicit_global_directory = config.global_dir orelse this.explicit_global_directory; } const default_disable_progress_bar: bool = brk: { @@ -7392,6 +7410,13 @@ pub const PackageManager = struct { if (cli.publish_config.auth_type) |auth_type| { this.publish_config.auth_type = auth_type; } + + if (cli.ca.len > 0) { + this.ca = cli.ca; + } + if (cli.ca_file_name.len > 0) { + this.ca_file_name = cli.ca_file_name; + } } else { this.log_level = if (default_disable_progress_bar) LogLevel.default_no_progress else LogLevel.default; PackageManager.verbose_install = false; @@ -8329,14 +8354,33 @@ pub const PackageManager = struct { } }; + fn httpThreadOnInitError(err: HTTP.InitError, opts: HTTP.HTTPThread.InitOpts) noreturn { + switch (err) { + error.LoadCAFile => { + if (!bun.sys.existsZ(opts.abs_ca_file_name)) { + Output.err("HTTPThread", "could not find CA file: '{s}'", .{opts.abs_ca_file_name}); + } else { + Output.err("HTTPThread", "invalid CA file: '{s}'", .{opts.abs_ca_file_name}); + } + }, + error.InvalidCAFile => { + Output.err("HTTPThread", "invalid CA file: '{s}'", .{opts.abs_ca_file_name}); + }, + error.InvalidCA => { + Output.err("HTTPThread", "the CA is invalid", .{}); + }, + error.FailedToOpenSocket => { + Output.errGeneric("failed to start HTTP client thread", .{}); + }, + } + Global.crash(); + } + pub fn init( ctx: Command.Context, cli: CommandLineArguments, subcommand: Subcommand, ) !struct { *PackageManager, string } { - // assume that spawning a thread will take a lil so we do that asap - HTTP.HTTPThread.init(); - if (cli.global) { var explicit_global_dir: string = ""; if (ctx.install) |opts| { @@ -8677,6 +8721,36 @@ pub const PackageManager = struct { subcommand, ); + var ca: []stringZ = &.{}; + if (manager.options.ca.len > 0) { + ca = try manager.allocator.alloc(stringZ, manager.options.ca.len); + for (ca, manager.options.ca) |*z, s| { + z.* = try manager.allocator.dupeZ(u8, s); + } + } + + var abs_ca_file_name: stringZ = &.{}; + if (manager.options.ca_file_name.len > 0) { + // resolve with original cwd + if (std.fs.path.isAbsolute(manager.options.ca_file_name)) { + abs_ca_file_name = try manager.allocator.dupeZ(u8, manager.options.ca_file_name); + } else { + var path_buf: bun.PathBuffer = undefined; + abs_ca_file_name = try manager.allocator.dupeZ(u8, bun.path.joinAbsStringBuf( + original_cwd_clone, + &path_buf, + &.{manager.options.ca_file_name}, + .auto, + )); + } + } + + HTTP.HTTPThread.init(&.{ + .ca = ca, + .abs_ca_file_name = abs_ca_file_name, + .onInitError = &httpThreadOnInitError, + }); + manager.timestamp_for_manifest_cache_control = brk: { if (comptime bun.Environment.allow_assert) { if (env.get("BUN_CONFIG_MANIFEST_CACHE_CONTROL_TIMESTAMP")) |cache_control| { @@ -9207,6 +9281,8 @@ pub const PackageManager = struct { clap.parseParam("-p, --production Don't install devDependencies") catch unreachable, clap.parseParam("--no-save Don't update package.json or save a lockfile") catch unreachable, clap.parseParam("--save Save to package.json (true by default)") catch unreachable, + clap.parseParam("--ca ... Provide a Certificate Authority signing certificate") catch unreachable, + clap.parseParam("--cafile The same as `--ca`, but is a file path to the certificate") catch unreachable, clap.parseParam("--dry-run Don't install anything") catch unreachable, clap.parseParam("--frozen-lockfile Disallow changes to lockfile") catch unreachable, clap.parseParam("-f, --force Always request the latest versions from the registry & reinstall all dependencies") catch unreachable, @@ -9349,6 +9425,9 @@ pub const PackageManager = struct { publish_config: Options.PublishConfig = .{}, + ca: []const string = &.{}, + ca_file_name: string = "", + const PatchOpts = union(enum) { nothing: struct {}, patch: struct {}, @@ -9688,6 +9767,11 @@ pub const PackageManager = struct { cli.ignore_scripts = args.flag("--ignore-scripts"); cli.trusted = args.flag("--trust"); cli.no_summary = args.flag("--no-summary"); + cli.ca = args.options("--ca"); + + if (args.option("--cafile")) |ca_file_name| { + cli.ca_file_name = ca_file_name; + } // commands that support --filter if (comptime subcommand.supportsWorkspaceFiltering()) { diff --git a/src/js_ast.zig b/src/js_ast.zig index 363a000e6537d..d815627c45c80 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -3436,6 +3436,15 @@ pub const Expr = struct { return if (asProperty(expr, name)) |query| query.expr else null; } + pub fn getObject(expr: *const Expr, name: string) ?Expr { + if (expr.asProperty(name)) |query| { + if (query.expr.isObject()) { + return query.expr; + } + } + return null; + } + pub fn getString(expr: *const Expr, allocator: std.mem.Allocator, name: string) OOM!?struct { string, logger.Loc } { if (asProperty(expr, name)) |q| { if (q.expr.asString(allocator)) |str| { diff --git a/src/napi/napi.zig b/src/napi/napi.zig index baa675eb31998..39145743e314a 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -822,7 +822,7 @@ pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value // We don't want to fail to load the library because of that // so we instead return an error and warn the user fn notImplementedYet(comptime name: []const u8) void { - bun.once( + bun.onceUnsafe( struct { pub fn warn() void { if (JSC.VirtualMachine.get().log.level.atLeast(.warn)) { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 725a6ea480188..be558fb331810 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -563,7 +563,7 @@ pub const Resolver = struct { pub fn getPackageManager(this: *Resolver) *PackageManager { return this.package_manager orelse brk: { - bun.HTTPThread.init(); + bun.HTTPThread.init(&.{}); const pm = PackageManager.initWithRuntime( this.log, this.opts.install, diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 93168b63e51cb..40b556ab707a7 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -3095,7 +3095,8 @@ pub const PostgresSQLConnection = struct { defer hostname.deinit(); if (tls_object.isEmptyOrUndefinedOrNull()) { const ctx = vm.rareData().postgresql_context.tcp orelse brk: { - const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), uws.us_bun_socket_context_options_t{}).?; + var err: uws.create_bun_socket_error_t = .none; + const ctx_ = uws.us_create_bun_socket_context(0, vm.uwsLoop(), @sizeOf(*PostgresSQLConnection), uws.us_bun_socket_context_options_t{}, &err).?; uws.NewSocketHandler(false).configure(ctx_, true, *PostgresSQLConnection, SocketHandler(false)); vm.rareData().postgresql_context.tcp = ctx_; break :brk ctx_; diff --git a/src/sys.zig b/src/sys.zig index 731b8aa649ac7..c31f67d4a4e09 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2480,6 +2480,16 @@ pub fn exists(path: []const u8) bool { @compileError("TODO: existsOSPath"); } +pub fn existsZ(path: [:0]const u8) bool { + if (comptime Environment.isPosix) { + return system.access(path, 0) == 0; + } + + if (comptime Environment.isWindows) { + return getFileAttributes(path) != null; + } +} + pub fn faccessat(dir_: anytype, subpath: anytype) JSC.Maybe(bool) { const has_sentinel = std.meta.sentinel(@TypeOf(subpath)) != null; const dir_fd = bun.toFD(dir_); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index bd1e915a12b98..7c2d32126ef7e 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -22,6 +22,7 @@ import { toMatchNodeModulesAt, writeShebangScript, stderrForInstall, + tls, } from "harness"; import { join, resolve, sep } from "path"; import { readdirSorted } from "../dummy.registry"; @@ -514,6 +515,231 @@ ${Object.keys(opts) ); }); +describe("certificate authority", () => { + const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { + return async function (req: Request) { + if (req.url.includes("no-deps")) { + return new Response(Bun.file(join(import.meta.dir, "packages", "no-deps", "no-deps-1.0.0.tgz"))); + } + return new Response("OK", { status: 200 }); + }; + }; + test("valid --cafile", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + write(join(packageDir, "cafile"), tls.cert), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "cafile"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + const err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("ConnectionClosed"); + expect(err).not.toContain("error:"); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(0); + }); + test("valid --ca", async () => { + using server = Bun.serve({ + port: 0, + fetch: mockRegistryFetch(), + ...tls, + }); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.1.1", + dependencies: { + "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://localhost:${server.port}/"`, + ), + ]); + + // first without ca, should fail + let { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + let out = await Bun.readableStreamToText(stdout); + let err = await Bun.readableStreamToText(stderr); + expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(await exited).toBe(1); + + // now with a valid ca + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", tls.cert], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + })); + out = await Bun.readableStreamToText(stdout); + expect(out).toContain("+ no-deps@"); + err = await Bun.readableStreamToText(stderr); + expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + }); + test(`non-existent --cafile`, async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", "does-not-exist"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + + test("cafile from bunfig does not exist", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://localhost:${port}/" + cafile = "does-not-exist"`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); + expect(await exited).toBe(1); + }); + test("invalid cafile", async () => { + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ), + write( + join(packageDir, "invalid-cafile"), + `-----BEGIN CERTIFICATE----- +jlwkjekfjwlejlgldjfljlkwjef +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +ljelkjwelkgjw;lekj;lkejflkj +-----END CERTIFICATE-----`, + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cafile", join(packageDir, "invalid-cafile")], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain(`HTTPThread: invalid CA file: '${join(packageDir, "invalid-cafile")}'`); + expect(await exited).toBe(1); + }); + test("invalid --ca", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + dependencies: { + "no-deps": "1.1.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--ca", "not-valid"], + cwd: packageDir, + stderr: "pipe", + stdout: "pipe", + env, + }); + + const out = await Bun.readableStreamToText(stdout); + expect(out).not.toContain("no-deps"); + const err = await Bun.readableStreamToText(stderr); + expect(err).toContain("HTTPThread: the CA is invalid"); + expect(await exited).toBe(1); + }); +}); + export async function publish( env: any, cwd: string,