diff options
Diffstat (limited to 'src/bun.js/api/server.zig')
-rw-r--r-- | src/bun.js/api/server.zig | 1844 |
1 files changed, 1844 insertions, 0 deletions
diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig new file mode 100644 index 000000000..cb3c4387a --- /dev/null +++ b/src/bun.js/api/server.zig @@ -0,0 +1,1844 @@ +const Bun = @This(); +const default_allocator = @import("../../global.zig").default_allocator; +const bun = @import("../../global.zig"); +const Environment = bun.Environment; +const NetworkThread = @import("http").NetworkThread; +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = @import("../../global.zig").Output; +const MutableString = @import("../../global.zig").MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const IdentityContext = @import("../../identity_context.zig").IdentityContext; +const Fs = @import("../../fs.zig"); +const Resolver = @import("../../resolver/resolver.zig"); +const ast = @import("../../import_record.zig"); +const NodeModuleBundle = @import("../../node_module_bundle.zig").NodeModuleBundle; +const MacroEntryPoint = @import("../../bundler.zig").MacroEntryPoint; +const logger = @import("../../logger.zig"); +const Api = @import("../../api/schema.zig").Api; +const options = @import("../../options.zig"); +const Bundler = @import("../../bundler.zig").Bundler; +const ServerEntryPoint = @import("../../bundler.zig").ServerEntryPoint; +const js_printer = @import("../../js_printer.zig"); +const js_parser = @import("../../js_parser.zig"); +const js_ast = @import("../../js_ast.zig"); +const hash_map = @import("../../hash_map.zig"); +const http = @import("../../http.zig"); +const NodeFallbackModules = @import("../../node_fallbacks.zig"); +const ImportKind = ast.ImportKind; +const Analytics = @import("../../analytics/analytics_thread.zig"); +const ZigString = @import("../../jsc.zig").ZigString; +const Runtime = @import("../../runtime.zig"); +const Router = @import("./router.zig"); +const ImportRecord = ast.ImportRecord; +const DotEnv = @import("../../env_loader.zig"); +const ParseResult = @import("../../bundler.zig").ParseResult; +const PackageJSON = @import("../../resolver/package_json.zig").PackageJSON; +const MacroRemap = @import("../../resolver/package_json.zig").MacroMap; +const WebCore = @import("../../jsc.zig").WebCore; +const Request = WebCore.Request; +const Response = WebCore.Response; +const Headers = WebCore.Headers; +const Fetch = WebCore.Fetch; +const HTTP = @import("http"); +const FetchEvent = WebCore.FetchEvent; +const js = @import("../../jsc.zig").C; +const JSC = @import("../../jsc.zig"); +const JSError = @import("../base.zig").JSError; +const MarkedArrayBuffer = @import("../base.zig").MarkedArrayBuffer; +const getAllocator = @import("../base.zig").getAllocator; +const JSValue = @import("../../jsc.zig").JSValue; +const NewClass = @import("../base.zig").NewClass; +const Microtask = @import("../../jsc.zig").Microtask; +const JSGlobalObject = @import("../../jsc.zig").JSGlobalObject; +const ExceptionValueRef = @import("../../jsc.zig").ExceptionValueRef; +const JSPrivateDataPtr = @import("../../jsc.zig").JSPrivateDataPtr; +const ZigConsoleClient = @import("../../jsc.zig").ZigConsoleClient; +const Node = @import("../../jsc.zig").Node; +const ZigException = @import("../../jsc.zig").ZigException; +const ZigStackTrace = @import("../../jsc.zig").ZigStackTrace; +const ErrorableResolvedSource = @import("../../jsc.zig").ErrorableResolvedSource; +const ResolvedSource = @import("../../jsc.zig").ResolvedSource; +const JSPromise = @import("../../jsc.zig").JSPromise; +const JSInternalPromise = @import("../../jsc.zig").JSInternalPromise; +const JSModuleLoader = @import("../../jsc.zig").JSModuleLoader; +const JSPromiseRejectionOperation = @import("../../jsc.zig").JSPromiseRejectionOperation; +const Exception = @import("../../jsc.zig").Exception; +const ErrorableZigString = @import("../../jsc.zig").ErrorableZigString; +const ZigGlobalObject = @import("../../jsc.zig").ZigGlobalObject; +const VM = @import("../../jsc.zig").VM; +const JSFunction = @import("../../jsc.zig").JSFunction; +const Config = @import("../config.zig"); +const URL = @import("../../url.zig").URL; +const Transpiler = @import("./transpiler.zig"); +const VirtualMachine = @import("../javascript.zig").VirtualMachine; +const IOTask = JSC.IOTask; +const is_bindgen = JSC.is_bindgen; +const uws = @import("uws"); +const Fallback = Runtime.Fallback; +const MimeType = HTTP.MimeType; +const Blob = JSC.WebCore.Blob; +const BoringSSL = @import("boringssl"); +const Arena = @import("../../mimalloc_arena.zig").Arena; +const SendfileContext = struct { + fd: i32, + socket_fd: i32 = 0, + remain: Blob.SizeType = 0, + offset: Blob.SizeType = 0, + has_listener: bool = false, + has_set_on_writable: bool = false, + auto_close: bool = false, +}; +const DateTime = @import("datetime"); +const linux = std.os.linux; + +pub const ServerConfig = struct { + port: u16 = 0, + hostname: [*:0]const u8 = "0.0.0.0", + + // TODO: use webkit URL parser instead of bun's + base_url: URL = URL{}, + base_uri: string = "", + + ssl_config: ?SSLConfig = null, + max_request_body_size: usize = 1024 * 1024 * 128, + development: bool = false, + + onError: JSC.JSValue = JSC.JSValue.zero, + onRequest: JSC.JSValue = JSC.JSValue.zero, + + pub const SSLConfig = struct { + server_name: [*c]const u8 = null, + + key_file_name: [*c]const u8 = null, + cert_file_name: [*c]const u8 = null, + + ca_file_name: [*c]const u8 = null, + dh_params_file_name: [*c]const u8 = null, + + passphrase: [*c]const u8 = null, + low_memory_mode: bool = false, + + pub fn deinit(this: *SSLConfig) void { + const fields = .{ + "server_name", + "key_file_name", + "cert_file_name", + "ca_file_name", + "dh_params_file_name", + "passphrase", + }; + + inline for (fields) |field| { + const slice = std.mem.span(@field(this, field)); + if (slice.len > 0) { + bun.default_allocator.free(slice); + } + } + } + + const zero = SSLConfig{}; + + pub fn inJS(global: *JSC.JSGlobalObject, obj: JSC.JSValue, exception: JSC.C.ExceptionRef) ?SSLConfig { + var result = zero; + var any = false; + + // Required + if (obj.getTruthy(global, "keyFile")) |key_file_name| { + var sliced = key_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.key_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.key_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Unable to access keyFile path", .{}, global.ref(), exception); + result.deinit(); + + return null; + } + any = true; + } + } + if (obj.getTruthy(global, "certFile")) |cert_file_name| { + var sliced = cert_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.cert_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.cert_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Unable to access certFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + any = true; + } + } + + // Optional + if (any) { + if (obj.getTruthy(global, "serverName")) |key_file_name| { + var sliced = key_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.server_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + } + } + + if (obj.getTruthy(global, "caFile")) |ca_file_name| { + var sliced = ca_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.ca_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.ca_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Invalid caFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + } + } + if (obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { + var sliced = dh_params_file_name.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.dh_params_file_name = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + if (std.os.system.access(result.dh_params_file_name, std.os.F_OK) != 0) { + JSC.throwInvalidArguments("Invalid dhParamsFile path", .{}, global.ref(), exception); + result.deinit(); + return null; + } + } + } + + if (obj.getTruthy(global, "passphrase")) |passphrase| { + var sliced = passphrase.toSlice(global, bun.default_allocator); + defer sliced.deinit(); + if (sliced.len > 0) { + result.passphrase = bun.default_allocator.dupeZ(u8, sliced.slice()) catch unreachable; + } + } + + if (obj.get(global, "lowMemoryMode")) |low_memory_mode| { + result.low_memory_mode = low_memory_mode.toBoolean(); + any = true; + } + } + + if (!any) + return null; + return result; + } + + pub fn fromJS(global: *JSC.JSGlobalObject, arguments: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) ?SSLConfig { + if (arguments.next()) |arg| { + return SSLConfig.inJS(global, arg, exception); + } + + return null; + } + }; + + pub fn fromJS(global: *JSC.JSGlobalObject, arguments: *JSC.Node.ArgumentsSlice, exception: JSC.C.ExceptionRef) ServerConfig { + var env = arguments.vm.bundler.env; + + var args = ServerConfig{ + .port = 3000, + .hostname = "0.0.0.0", + .development = true, + }; + var has_hostname = false; + if (strings.eqlComptime(env.get("NODE_ENV") orelse "", "production")) { + args.development = false; + } + + if (arguments.vm.bundler.options.production) { + args.development = false; + } + + const PORT_ENV = .{ "PORT", "BUN_PORT", "NODE_PORT" }; + + inline for (PORT_ENV) |PORT| { + if (env.get(PORT)) |port| { + if (std.fmt.parseInt(u16, port, 10)) |_port| { + args.port = _port; + } else |_| {} + } + } + + if (arguments.vm.bundler.options.transform_options.port) |port| { + args.port = port; + } + + if (arguments.vm.bundler.options.transform_options.origin) |origin| { + args.base_uri = origin; + } + + if (arguments.next()) |arg| { + if (arg.isUndefinedOrNull() or !arg.isObject()) { + JSC.throwInvalidArguments("Bun.serve expects an object", .{}, global.ref(), exception); + return args; + } + + if (arg.getTruthy(global, "port")) |port_| { + args.port = @intCast(u16, @minimum(@maximum(0, port_.toInt32()), std.math.maxInt(u16))); + } + + if (arg.getTruthy(global, "baseURI")) |baseURI| { + var sliced = baseURI.toSlice(global, bun.default_allocator); + + if (sliced.len > 0) { + defer sliced.deinit(); + args.base_uri = bun.default_allocator.dupe(u8, sliced.slice()) catch unreachable; + } + } + + if (arg.getTruthy(global, "hostname") orelse arg.getTruthy(global, "host")) |host| { + const host_str = host.toSlice( + global, + bun.default_allocator, + ); + if (host_str.len > 0) { + args.hostname = bun.default_allocator.dupeZ(u8, host_str.slice()) catch unreachable; + has_hostname = true; + } + } + + if (arg.get(global, "development")) |dev| { + args.development = dev.toBoolean(); + } + + if (SSLConfig.fromJS(global, arguments, exception)) |ssl_config| { + args.ssl_config = ssl_config; + } + + if (exception.* != null) { + return args; + } + + if (arg.getTruthy(global, "maxRequestBodySize")) |max_request_body_size| { + args.max_request_body_size = @intCast(u64, @maximum(0, max_request_body_size.toInt64())); + } + + if (arg.getTruthy(global, "error")) |onError| { + if (!onError.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected error to be a function", .{}, global.ref(), exception); + if (args.ssl_config) |*conf| { + conf.deinit(); + } + return args; + } + JSC.C.JSValueProtect(global.ref(), onError.asObjectRef()); + args.onError = onError; + } + + if (arg.getTruthy(global, "fetch")) |onRequest| { + if (!onRequest.isCallable(global.vm())) { + JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global.ref(), exception); + return args; + } + JSC.C.JSValueProtect(global.ref(), onRequest.asObjectRef()); + args.onRequest = onRequest; + } else { + JSC.throwInvalidArguments("Expected fetch() to be a function", .{}, global.ref(), exception); + if (args.ssl_config) |*conf| { + conf.deinit(); + } + return args; + } + } + + if (args.port == 0) { + JSC.throwInvalidArguments("Invalid port: must be > 0", .{}, global.ref(), exception); + } + + if (args.base_uri.len > 0) { + args.base_url = URL.parse(args.base_uri); + if (args.base_url.hostname.len == 0) { + JSC.throwInvalidArguments("baseURI must have a hostname", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (!strings.isAllASCII(args.base_uri)) { + JSC.throwInvalidArguments("Unicode baseURI must already be encoded for now.\nnew URL(baseuRI).toString() should do the trick.", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (args.base_url.protocol.len == 0) { + const protocol: string = if (args.ssl_config != null) "https" else "http"; + + args.base_uri = (if ((args.port == 80 and args.ssl_config == null) or (args.port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/{s}", .{ + protocol, + args.base_url.hostname, + strings.trimLeadingChar(args.base_url.pathname, '/'), + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/{s}", .{ + protocol, + args.base_url.hostname, + args.port, + strings.trimLeadingChar(args.base_url.pathname, '/'), + })) catch unreachable; + + args.base_url = URL.parse(args.base_uri); + } + } else { + const hostname: string = + if (has_hostname and std.mem.span(args.hostname).len > 0) std.mem.span(args.hostname) else "localhost"; + const protocol: string = if (args.ssl_config != null) "https" else "http"; + + args.base_uri = (if ((args.port == 80 and args.ssl_config == null) or (args.port == 443 and args.ssl_config != null)) + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}/", .{ + protocol, + hostname, + }) + else + std.fmt.allocPrint(bun.default_allocator, "{s}://{s}:{d}/", .{ protocol, hostname, args.port })) catch unreachable; + + if (!strings.isAllASCII(hostname)) { + JSC.throwInvalidArguments("Unicode hostnames must already be encoded for now.\nnew URL(input).hostname should do the trick.", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + args.base_url = URL.parse(args.base_uri); + } + + // I don't think there's a case where this can happen + // but let's check anyway, just in case + if (args.base_url.hostname.len == 0) { + JSC.throwInvalidArguments("baseURI must have a hostname", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + if (args.base_url.username.len > 0 or args.base_url.password.len > 0) { + JSC.throwInvalidArguments("baseURI can't have a username or password", .{}, global.ref(), exception); + bun.default_allocator.free(bun.constStrToU8(args.base_uri)); + args.base_uri = ""; + return args; + } + + return args; + } +}; + +pub fn NewRequestContextStackAllocator(comptime RequestContext: type, comptime count: usize) type { + // Pre-allocate up to 2048 requests + // use a bitset to track which ones are used + return struct { + buf: [count]RequestContext = undefined, + unused: Set = undefined, + fallback_allocator: std.mem.Allocator = undefined, + + pub const Set = std.bit_set.ArrayBitSet(usize, count); + + pub fn get(this: *@This()) std.mem.Allocator { + this.unused = Set.initFull(); + return std.mem.Allocator.init(this, alloc, resize, free); + } + + fn alloc(self: *@This(), a: usize, b: u29, c: u29, d: usize) ![]u8 { + if (self.unused.findFirstSet()) |i| { + self.unused.unset(i); + return std.mem.asBytes(&self.buf[i]); + } + + return try self.fallback_allocator.rawAlloc(a, b, c, d); + } + + fn resize( + _: *@This(), + _: []u8, + _: u29, + _: usize, + _: u29, + _: usize, + ) ?usize { + unreachable; + } + + fn sliceContainsSlice(container: []u8, slice: []u8) bool { + return @ptrToInt(slice.ptr) >= @ptrToInt(container.ptr) and + (@ptrToInt(slice.ptr) + slice.len) <= (@ptrToInt(container.ptr) + container.len); + } + + fn free( + self: *@This(), + buf: []u8, + buf_align: u29, + return_address: usize, + ) void { + _ = buf_align; + _ = return_address; + const bytes = std.mem.asBytes(&self.buf); + if (sliceContainsSlice(bytes, buf)) { + const index = if (bytes[0..buf.len].ptr != buf.ptr) + (@ptrToInt(buf.ptr) - @ptrToInt(bytes)) / @sizeOf(RequestContext) + else + @as(usize, 0); + + if (comptime Environment.allow_assert) { + std.debug.assert(@intToPtr(*RequestContext, @ptrToInt(buf.ptr)) == &self.buf[index]); + std.debug.assert(!self.unused.isSet(index)); + } + + self.unused.set(index); + } else { + self.fallback_allocator.rawFree(buf, buf_align, return_address); + } + } + }; +} + +// This is defined separately partially to work-around an LLVM debugger bug. +fn NewRequestContext(comptime ssl_enabled: bool, comptime debug_mode: bool, comptime ThisServer: type) type { + return struct { + const RequestContext = @This(); + const App = uws.NewApp(ssl_enabled); + pub threadlocal var pool: ?*RequestContext.RequestContextStackAllocator = null; + pub threadlocal var pool_allocator: std.mem.Allocator = undefined; + + pub const RequestContextStackAllocator = NewRequestContextStackAllocator(RequestContext, 2048); + pub const name = "HTTPRequestContext" ++ (if (debug_mode) "Debug" else "") ++ (if (ThisServer.ssl_enabled) "TLS" else ""); + pub const shim = JSC.Shimmer("Bun", name, @This()); + + server: *ThisServer, + resp: *App.Response, + /// thread-local default heap allocator + /// this prevents an extra pthread_getspecific() call which shows up in profiling + allocator: std.mem.Allocator, + req: *uws.Request, + url: string, + method: HTTP.Method, + aborted: bool = false, + finalized: bun.DebugOnly(bool) = bun.DebugOnlyDefault(false), + + /// We can only safely free once the request body promise is finalized + /// and the response is rejected + pending_promises_for_abort: u8 = 0, + + has_marked_complete: bool = false, + response_jsvalue: JSC.JSValue = JSC.JSValue.zero, + response_ptr: ?*JSC.WebCore.Response = null, + blob: JSC.WebCore.Blob = JSC.WebCore.Blob{}, + promise: ?*JSC.JSValue = null, + response_headers: ?*JSC.FetchHeaders = null, + has_abort_handler: bool = false, + has_sendfile_ctx: bool = false, + has_called_error_handler: bool = false, + needs_content_length: bool = false, + sendfile: SendfileContext = undefined, + request_js_object: JSC.C.JSObjectRef = null, + request_body_buf: std.ArrayListUnmanaged(u8) = .{}, + + /// Used either for temporary blob data or fallback + /// When the response body is a temporary value + response_buf_owned: std.ArrayListUnmanaged(u8) = .{}, + + // TODO: support builtin compression + const can_sendfile = !ssl_enabled; + + pub const thenables = shim.thenables(.{ + PromiseHandler, + }); + + pub const lazy_static_functions = thenables; + pub const Export = lazy_static_functions; + + const PromiseHandler = JSC.Thenable(RequestContext, onResolve, onReject); + + pub fn setAbortHandler(this: *RequestContext) void { + if (this.has_abort_handler) return; + this.has_abort_handler = true; + this.resp.onAborted(*RequestContext, RequestContext.onAbort, this); + } + + pub fn onResolve( + ctx: *RequestContext, + _: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + ) void { + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + + if (arguments.len == 0) { + ctx.renderMissing(); + return; + } + + handleResolve(ctx, arguments[0]); + } + + fn handleResolve(ctx: *RequestContext, value: JSC.JSValue) void { + if (value.isEmptyOrUndefinedOrNull()) { + ctx.renderMissing(); + return; + } + + var response = value.as(JSC.WebCore.Response) orelse { + Output.prettyErrorln("Expected a Response object", .{}); + Output.flush(); + ctx.renderMissing(); + return; + }; + ctx.response_jsvalue = value; + JSC.C.JSValueProtect(ctx.server.globalThis.ref(), value.asObjectRef()); + + ctx.render(response); + } + + pub fn finalizeForAbort(this: *RequestContext) void { + this.pending_promises_for_abort -|= 1; + if (this.pending_promises_for_abort == 0) this.finalize(); + } + + pub fn onReject( + ctx: *RequestContext, + _: *JSC.JSGlobalObject, + arguments: []const JSC.JSValue, + ) void { + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + handleReject(ctx, if (arguments.len > 0) arguments[0] else JSC.JSValue.jsUndefined()); + } + + fn handleReject(ctx: *RequestContext, value: JSC.JSValue) void { + ctx.runErrorHandler( + value, + ); + + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + if (!ctx.resp.hasResponded()) { + ctx.renderMissing(); + } + } + + pub fn renderMissing(ctx: *RequestContext) void { + if (comptime !debug_mode) { + ctx.resp.writeStatus("204 No Content"); + ctx.resp.endWithoutBody(); + ctx.finalize(); + } else { + ctx.resp.writeStatus("200 OK"); + ctx.resp.end("Welcome to Bun! To get started, return a Response object.", false); + ctx.finalize(); + } + } + + pub fn renderDefaultError( + this: *RequestContext, + log: *logger.Log, + err: anyerror, + exceptions: []Api.JsException, + comptime fmt: string, + args: anytype, + ) void { + this.resp.writeStatus("500 Internal Server Error"); + this.resp.writeHeader("content-type", MimeType.html.value); + + const allocator = this.allocator; + + var fallback_container = allocator.create(Api.FallbackMessageContainer) catch unreachable; + defer allocator.destroy(fallback_container); + fallback_container.* = Api.FallbackMessageContainer{ + .message = std.fmt.allocPrint(allocator, comptime Output.prettyFmt(fmt, false), args) catch unreachable, + .router = null, + .reason = .fetch_event_handler, + .cwd = VirtualMachine.vm.bundler.fs.top_level_dir, + .problems = Api.Problems{ + .code = @truncate(u16, @errorToInt(err)), + .name = @errorName(err), + .exceptions = exceptions, + .build = log.toAPI(allocator) catch unreachable, + }, + }; + + if (comptime fmt.len > 0) Output.prettyErrorln(fmt, args); + Output.flush(); + + var bb = std.ArrayList(u8).init(allocator); + var bb_writer = bb.writer(); + + Fallback.renderBackend( + allocator, + fallback_container, + @TypeOf(bb_writer), + bb_writer, + ) catch unreachable; + if (this.resp.tryEnd(bb.items, bb.items.len)) { + bb.clearAndFree(); + this.finalizeWithoutDeinit(); + return; + } + + this.response_buf_owned = std.ArrayListUnmanaged(u8){ .items = bb.items, .capacity = bb.capacity }; + this.renderResponseBuffer(); + } + + pub fn renderResponseBuffer(this: *RequestContext) void { + this.resp.onWritable(*RequestContext, onWritableResponseBuffer, this); + } + + pub fn onWritableResponseBuffer(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool { + std.debug.assert(this.resp == resp); + if (this.aborted) { + this.finalizeForAbort(); + return false; + } + return this.sendWritableBytes(this.response_buf_owned.items, write_offset, resp); + } + + pub fn create(this: *RequestContext, server: *ThisServer, req: *uws.Request, resp: *App.Response) void { + this.* = .{ + .allocator = server.allocator, + .resp = resp, + .req = req, + // this memory is owned by the Request object + .url = strings.append(this.allocator, server.base_url_string_for_joining, req.url()) catch + @panic("Out of memory while joining the URL path?"), + .method = HTTP.Method.which(req.method()) orelse .GET, + .server = server, + }; + } + + pub fn isDeadRequest(this: *RequestContext) bool { + if (this.pending_promises_for_abort > 0) return false; + + if (this.promise != null) { + return false; + } + + if (this.request_js_object) |obj| { + if (obj.value().as(Request)) |req| { + if (req.body == .Locked) { + return false; + } + } + } + + return true; + } + + pub fn onAbort(this: *RequestContext, resp: *App.Response) void { + std.debug.assert(this.resp == resp); + std.debug.assert(!this.aborted); + this.aborted = true; + + // if we can, free the request now. + if (this.isDeadRequest()) { + this.finalizeWithoutDeinit(); + this.markComplete(); + this.deinit(); + } else { + this.pending_promises_for_abort = 0; + + // if we cannot, we have to reject pending promises + // first, we reject the request body promise + if (this.request_js_object != null) { + var request_js = this.request_js_object.?.value(); + request_js.ensureStillAlive(); + + this.request_js_object = null; + defer request_js.ensureStillAlive(); + defer JSC.C.JSValueUnprotect(this.server.globalThis.ref(), request_js.asObjectRef()); + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + if (request_js.as(Request)) |req| { + // the promise is pending + if (req.body == .Locked and (req.body.Locked.action != .none or req.body.Locked.promise != null)) { + this.pending_promises_for_abort += 1; + req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + } + req.uws_request = null; + } + } + + // then, we reject the response promise + if (this.promise) |promise| { + this.pending_promises_for_abort += 1; + this.promise = null; + promise.asPromise().?.reject(this.server.globalThis, JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis)); + } + + if (this.pending_promises_for_abort > 0) { + this.server.vm.tick(); + } + } + } + + pub fn markComplete(this: *RequestContext) void { + if (!this.has_marked_complete) this.server.onRequestComplete(); + this.has_marked_complete = true; + } + + // This function may be called multiple times + // so it's important that we can safely do that + pub fn finalizeWithoutDeinit(this: *RequestContext) void { + this.blob.detach(); + + if (comptime Environment.allow_assert) { + std.debug.assert(!this.finalized); + this.finalized = true; + } + + if (!this.response_jsvalue.isEmpty()) { + this.server.response_objects_pool.push(this.server.globalThis, this.response_jsvalue); + this.response_jsvalue = JSC.JSValue.zero; + } + + if (this.request_js_object != null) { + var request_js = this.request_js_object.?.value(); + request_js.ensureStillAlive(); + + this.request_js_object = null; + defer request_js.ensureStillAlive(); + defer JSC.C.JSValueUnprotect(this.server.globalThis.ref(), request_js.asObjectRef()); + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing or the connection was aborted + if (request_js.as(Request)) |req| { + // the promise is pending + if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) { + req.body.toErrorInstance(JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis), this.server.globalThis); + } + req.uws_request = null; + } + } + + if (this.promise) |promise| { + this.promise = null; + + if (promise.asInternalPromise()) |prom| { + prom.rejectAsHandled(this.server.globalThis, (JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis))); + } else if (promise.asPromise()) |prom| { + prom.rejectAsHandled(this.server.globalThis, (JSC.toTypeError(.ABORT_ERR, "Request aborted", .{}, this.server.globalThis))); + } + JSC.C.JSValueUnprotect(this.server.globalThis.ref(), promise.asObjectRef()); + } + + if (this.response_headers != null) { + this.response_headers.?.deref(); + this.response_headers = null; + } + } + pub fn finalize(this: *RequestContext) void { + this.finalizeWithoutDeinit(); + this.markComplete(); + this.deinit(); + } + + pub fn deinit(this: *RequestContext) void { + if (comptime Environment.allow_assert) + std.debug.assert(this.finalized); + + if (comptime Environment.allow_assert) + std.debug.assert(this.has_marked_complete); + + var server = this.server; + this.request_body_buf.clearAndFree(this.allocator); + this.response_buf_owned.clearAndFree(this.allocator); + + server.request_pool_allocator.destroy(this); + } + + fn writeHeaders( + this: *RequestContext, + headers: *JSC.FetchHeaders, + ) void { + headers.remove(&ZigString.init("content-length")); + headers.remove(&ZigString.init("transfer-encoding")); + if (!ssl_enabled) headers.remove(&ZigString.init("strict-transport-security")); + headers.toUWSResponse(ssl_enabled, this.resp); + } + + pub fn writeStatus(this: *RequestContext, status: u16) void { + var status_text_buf: [48]u8 = undefined; + + if (status == 302) { + this.resp.writeStatus("302 Found"); + } else { + this.resp.writeStatus(std.fmt.bufPrint(&status_text_buf, "{d} HM", .{status}) catch unreachable); + } + } + + fn cleanupAndFinalizeAfterSendfile(this: *RequestContext) void { + this.resp.setWriteOffset(this.sendfile.offset); + this.resp.endWithoutBody(); + // use node syscall so that we don't segfault on BADF + if (this.sendfile.auto_close) + _ = JSC.Node.Syscall.close(this.sendfile.fd); + this.sendfile = undefined; + this.finalize(); + } + const separator: string = "\r\n"; + const separator_iovec = [1]std.os.iovec_const{.{ + .iov_base = separator.ptr, + .iov_len = separator.len, + }}; + + pub fn onSendfile(this: *RequestContext) bool { + if (this.aborted) { + this.cleanupAndFinalizeAfterSendfile(); + return false; + } + + const adjusted_count_temporary = @minimum(@as(u64, this.sendfile.remain), @as(u63, std.math.maxInt(u63))); + // TODO we should not need this int cast; improve the return type of `@minimum` + const adjusted_count = @intCast(u63, adjusted_count_temporary); + + if (Environment.isLinux) { + var signed_offset = @intCast(i64, this.sendfile.offset); + const start = this.sendfile.offset; + const val = + // this does the syscall directly, without libc + linux.sendfile(this.sendfile.socket_fd, this.sendfile.fd, &signed_offset, this.sendfile.remain); + this.sendfile.offset = @intCast(Blob.SizeType, signed_offset); + + const errcode = linux.getErrno(val); + + this.sendfile.remain -= @intCast(Blob.SizeType, this.sendfile.offset - start); + + if (errcode != .SUCCESS or this.aborted or this.sendfile.remain == 0 or val == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode != .SUCCESS; + } + } else { + var sbytes: std.os.off_t = adjusted_count; + const signed_offset = @bitCast(i64, @as(u64, this.sendfile.offset)); + + const errcode = std.c.getErrno(std.c.sendfile( + this.sendfile.fd, + this.sendfile.socket_fd, + + signed_offset, + &sbytes, + null, + 0, + )); + const wrote = @intCast(Blob.SizeType, sbytes); + this.sendfile.offset += wrote; + this.sendfile.remain -= wrote; + if (errcode != .AGAIN or this.aborted or this.sendfile.remain == 0 or sbytes == 0) { + if (errcode != .AGAIN and errcode != .SUCCESS and errcode != .PIPE) { + Output.prettyErrorln("Error: {s}", .{@tagName(errcode)}); + Output.flush(); + } + this.cleanupAndFinalizeAfterSendfile(); + return errcode == .SUCCESS; + } + } + + if (!this.sendfile.has_set_on_writable) { + this.sendfile.has_set_on_writable = true; + this.resp.onWritable(*RequestContext, onWritableSendfile, this); + } + + this.setAbortHandler(); + this.resp.markNeedsMore(); + + return true; + } + + pub fn onWritableBytes(this: *RequestContext, write_offset: c_ulong, resp: *App.Response) callconv(.C) bool { + std.debug.assert(this.resp == resp); + if (this.aborted) { + this.finalizeForAbort(); + return false; + } + + var bytes = this.blob.sharedView(); + return this.sendWritableBytes(bytes, write_offset, resp); + } + + pub fn sendWritableBytes(this: *RequestContext, bytes_: []const u8, write_offset: c_ulong, resp: *App.Response) bool { + std.debug.assert(this.resp == resp); + + var bytes = bytes_[@minimum(bytes_.len, @truncate(usize, write_offset))..]; + if (resp.tryEnd(bytes, bytes_.len)) { + this.finalize(); + return true; + } else { + this.resp.onWritable(*RequestContext, onWritableBytes, this); + return true; + } + } + + pub fn onWritableSendfile(this: *RequestContext, _: c_ulong, _: *App.Response) callconv(.C) bool { + return this.onSendfile(); + } + + // We tried open() in another thread for this + // it was not faster due to the mountain of syscalls + pub fn renderSendFile(this: *RequestContext, blob: JSC.WebCore.Blob) void { + this.blob = blob; + const file = &this.blob.store.?.data.file; + var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const auto_close = file.pathlike != .fd; + const fd = if (!auto_close) + file.pathlike.fd + else switch (JSC.Node.Syscall.open(file.pathlike.path.sliceZ(&file_buf), std.os.O.RDONLY | std.os.O.NONBLOCK | std.os.O.CLOEXEC, 0)) { + .result => |_fd| _fd, + .err => |err| return this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( + this.server.globalThis, + )), + }; + + // stat only blocks if the target is a file descriptor + const stat: std.os.Stat = switch (JSC.Node.Syscall.fstat(fd)) { + .result => |result| result, + .err => |err| { + this.runErrorHandler(err.withPath(file.pathlike.path.slice()).toSystemError().toErrorInstance( + this.server.globalThis, + )); + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + return; + }, + }; + + if (Environment.isMac) { + if (!std.os.S.ISREG(stat.mode)) { + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + + var err = JSC.Node.Syscall.Error{ + .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(std.os.E.INVAL)), + .path = file.pathlike.path.slice(), + .syscall = .sendfile, + }; + var sys = err.toSystemError(); + sys.message = ZigString.init("MacOS does not support sending non-regular files"); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); + return; + } + } + + if (Environment.isLinux) { + if (!(std.os.S.ISREG(stat.mode) or std.os.S.ISFIFO(stat.mode))) { + if (auto_close) { + _ = JSC.Node.Syscall.close(fd); + } + + var err = JSC.Node.Syscall.Error{ + .errno = @intCast(JSC.Node.Syscall.Error.Int, @enumToInt(std.os.E.INVAL)), + .path = file.pathlike.path.slice(), + .syscall = .sendfile, + }; + var sys = err.toSystemError(); + sys.message = ZigString.init("File must be regular or FIFO"); + this.runErrorHandler(sys.toErrorInstance( + this.server.globalThis, + )); + return; + } + } + + this.blob.size = @intCast(Blob.SizeType, stat.size); + this.needs_content_length = true; + + this.sendfile = .{ + .fd = fd, + .remain = this.blob.size, + .auto_close = auto_close, + .socket_fd = if (!this.aborted) this.resp.getNativeHandle() else -999, + }; + + this.resp.runCorked(*RequestContext, renderMetadataAndNewline, this); + + if (this.blob.size == 0) { + this.cleanupAndFinalizeAfterSendfile(); + return; + } + + _ = this.onSendfile(); + } + + pub fn renderMetadataAndNewline(this: *RequestContext) void { + this.renderMetadata(); + this.resp.prepareForSendfile(); + } + + pub fn doSendfile(this: *RequestContext, blob: Blob) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (this.has_sendfile_ctx) return; + + this.has_sendfile_ctx = true; + + if (comptime can_sendfile) { + return this.renderSendFile(blob); + } + + this.setAbortHandler(); + this.blob.doReadFileInternal(*RequestContext, this, onReadFile, this.server.globalThis); + } + + pub fn onReadFile(this: *RequestContext, result: Blob.Store.ReadFile.ResultType) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (result == .err) { + this.runErrorHandler(result.err.toErrorInstance(this.server.globalThis)); + return; + } + + const is_temporary = result.result.is_temporary; + if (!is_temporary) { + this.blob.resolveSize(); + this.doRenderBlob(); + } else { + this.blob.size = @truncate(Blob.SizeType, result.result.buf.len); + this.response_buf_owned = .{ .items = result.result.buf, .capacity = result.result.buf.len }; + this.renderResponseBuffer(); + } + } + + pub fn doRenderWithBodyLocked(this: *anyopaque, value: *JSC.WebCore.Body.Value) void { + doRenderWithBody(bun.cast(*RequestContext, this), value); + } + + pub fn doRenderWithBody(this: *RequestContext, value: *JSC.WebCore.Body.Value) void { + switch (value.*) { + .Error => { + const err = value.Error; + _ = value.use(); + if (this.aborted) { + this.finalizeForAbort(); + return; + } + this.runErrorHandler(err); + return; + }, + .Blob => { + this.blob = value.use(); + + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (this.blob.needsToReadFile()) { + this.req.setYield(false); + if (!this.has_sendfile_ctx) + this.doSendfile(this.blob); + return; + } + }, + // TODO: this needs to support streaming! + .Locked => |*lock| { + lock.callback = doRenderWithBodyLocked; + lock.task = this; + return; + }, + else => {}, + } + + this.doRenderBlob(); + } + + pub fn doRenderBlob(this: *RequestContext) void { + if (this.has_abort_handler) + this.resp.runCorked(*RequestContext, renderMetadata, this) + else + this.renderMetadata(); + + this.renderBytes(); + } + + pub fn doRender(this: *RequestContext) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + var response = this.response_ptr.?; + this.doRenderWithBody(&response.body.value); + } + + pub fn renderProductionError(this: *RequestContext, status: u16) void { + switch (status) { + 404 => { + this.resp.writeStatus("404 Not Found"); + this.resp.endWithoutBody(); + }, + else => { + this.resp.writeStatus("500 Internal Server Error"); + this.resp.writeHeader("content-type", "text/plain"); + this.resp.end("Something went wrong!", true); + }, + } + + this.finalize(); + } + + pub fn runErrorHandler( + this: *RequestContext, + value: JSC.JSValue, + ) void { + runErrorHandlerWithStatusCode(this, value, 500); + } + + pub fn runErrorHandlerWithStatusCode( + this: *RequestContext, + value: JSC.JSValue, + status: u16, + ) void { + JSC.markBinding(); + if (this.resp.hasResponded()) return; + + var exception_list: std.ArrayList(Api.JsException) = std.ArrayList(Api.JsException).init(this.allocator); + defer exception_list.deinit(); + if (!this.server.config.onError.isEmpty() and !this.has_called_error_handler) { + this.has_called_error_handler = true; + var args = [_]JSC.C.JSValueRef{value.asObjectRef()}; + const result = JSC.C.JSObjectCallAsFunctionReturnValue(this.server.globalThis.ref(), this.server.config.onError.asObjectRef(), this.server.thisObject.asObjectRef(), 1, &args); + + if (!result.isEmptyOrUndefinedOrNull()) { + if (result.isError() or result.isAggregateError(this.server.globalThis)) { + this.runErrorHandler(result); + return; + } else if (result.as(Response)) |response| { + this.render(response); + return; + } + } + } + + if (comptime debug_mode) { + JSC.VirtualMachine.vm.defaultErrorHandler(value, &exception_list); + + this.renderDefaultError( + JSC.VirtualMachine.vm.log, + error.ExceptionOcurred, + exception_list.toOwnedSlice(), + "<r><red>{s}<r> - <b>{s}<r> failed", + .{ std.mem.span(@tagName(this.method)), this.url }, + ); + } else { + if (status != 404) + JSC.VirtualMachine.vm.defaultErrorHandler(value, &exception_list); + this.renderProductionError(status); + } + JSC.VirtualMachine.vm.log.reset(); + return; + } + + pub fn renderMetadata(this: *RequestContext) void { + var response: *JSC.WebCore.Response = this.response_ptr.?; + var status = response.statusCode(); + const size = this.blob.size; + status = if (status == 200 and size == 0) + 204 + else + status; + + this.writeStatus(status); + var needs_content_type = true; + const content_type: MimeType = brk: { + if (response.body.init.headers) |headers_| { + if (headers_.get("content-type")) |content| { + needs_content_type = false; + break :brk MimeType.init(content); + } + } + break :brk if (this.blob.content_type.len > 0) + MimeType.init(this.blob.content_type) + else if (MimeType.sniff(this.blob.sharedView())) |content| + content + else if (this.blob.is_all_ascii orelse false) + MimeType.text + else + MimeType.other; + }; + + var has_content_disposition = false; + + if (response.body.init.headers) |headers_| { + this.writeHeaders(headers_); + has_content_disposition = headers_.has(&ZigString.init("content-disposition")); + response.body.init.headers = null; + headers_.deref(); + } + + if (needs_content_type) { + this.resp.writeHeader("content-type", content_type.value); + } + + // automatically include the filename when: + // 1. Bun.file("foo") + // 2. The content-disposition header is not present + if (!has_content_disposition and content_type.category.autosetFilename()) { + if (this.blob.store) |store| { + if (store.data == .file) { + if (store.data.file.pathlike == .path) { + const basename = std.fs.path.basename(store.data.file.pathlike.path.slice()); + if (basename.len > 0) { + var filename_buf: [1024]u8 = undefined; + + this.resp.writeHeader( + "content-disposition", + std.fmt.bufPrint(&filename_buf, "filename=\"{s}\"", .{basename[0..@minimum(basename.len, 1024 - 32)]}) catch "", + ); + } + } + } + } + } + + if (this.needs_content_length) { + this.resp.writeHeaderInt("content-length", size); + this.needs_content_length = false; + } + } + + pub fn renderBytes(this: *RequestContext) void { + const bytes = this.blob.sharedView(); + + if (!this.resp.tryEnd( + bytes, + bytes.len, + )) { + this.resp.onWritable(*RequestContext, onWritableBytes, this); + return; + } + + this.finalize(); + } + + pub fn render(this: *RequestContext, response: *JSC.WebCore.Response) void { + this.response_ptr = response; + + this.doRender(); + } + + pub fn resolveRequestBody(this: *RequestContext) void { + if (this.aborted) { + this.finalizeForAbort(); + return; + } + + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var bytes = this.request_body_buf.toOwnedSlice(this.allocator); + var old = req.body; + req.body = .{ + .Blob = if (bytes.len > 0) + Blob.init(bytes, this.allocator, this.server.globalThis) + else + Blob.initEmpty(this.server.globalThis), + }; + old.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + } + + pub fn onBodyChunk(this: *RequestContext, resp: *App.Response, chunk: []const u8, last: bool) void { + std.debug.assert(this.resp == resp); + + if (this.aborted) return; + this.request_body_buf.appendSlice(this.allocator, chunk) catch @panic("Out of memory while allocating request body"); + if (last) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request) != null) { + uws.Loop.get().?.nextTick(*RequestContext, this, resolveRequestBody); + } else { + this.request_body_buf.deinit(this.allocator); + this.request_body_buf = .{}; + } + } + } + + pub fn onPull(this: *RequestContext) void { + if (this.req.header("content-length")) |content_length| { + const len = std.fmt.parseInt(usize, content_length, 10) catch 0; + if (len == 0) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var old = req.body; + old.Locked.callback = null; + req.body = .{ .Empty = .{} }; + old.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + } + + if (len >= this.server.config.max_request_body_size) { + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var old = req.body; + old.Locked.callback = null; + req.body = .{ .Empty = .{} }; + old.toError(error.RequestBodyTooLarge, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + + this.resp.writeStatus("413 Request Entity Too Large"); + this.resp.endWithoutBody(); + this.finalize(); + return; + } + + this.request_body_buf.ensureTotalCapacityPrecise(this.allocator, len) catch @panic("Out of memory while allocating request body buffer"); + } + this.setAbortHandler(); + + this.resp.onData(*RequestContext, onBodyChunk, this); + } + + pub fn onPullCallback(this: *anyopaque) void { + onPull(bun.cast(*RequestContext, this)); + } + + comptime { + if (!JSC.is_bindgen) { + @export(PromiseHandler.resolve, .{ + .name = Export[0].symbol_name, + }); + @export(PromiseHandler.reject, .{ + .name = Export[1].symbol_name, + }); + } + } + }; +} + +pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type { + return struct { + pub const ssl_enabled = ssl_enabled_; + const debug_mode = debug_mode_; + + const ThisServer = @This(); + pub const RequestContext = NewRequestContext(ssl_enabled, debug_mode, @This()); + + pub const App = uws.NewApp(ssl_enabled); + + listener: ?*App.ListenSocket = null, + thisObject: JSC.JSValue = JSC.JSValue.zero, + app: *App = undefined, + vm: *JSC.VirtualMachine = undefined, + globalThis: *JSGlobalObject, + base_url_string_for_joining: string = "", + response_objects_pool: JSC.WebCore.Response.Pool = JSC.WebCore.Response.Pool{}, + config: ServerConfig = ServerConfig{}, + pending_requests: usize = 0, + request_pool_allocator: std.mem.Allocator = undefined, + has_js_deinited: bool = false, + listen_callback: JSC.AnyTask = undefined, + allocator: std.mem.Allocator, + + pub const Class = JSC.NewClass( + ThisServer, + .{ .name = "Server" }, + .{ + .stop = .{ + .rfn = JSC.wrapSync(ThisServer, "stopFromJS"), + }, + .finalize = .{ + .rfn = finalize, + }, + }, + .{ + .port = .{ + .get = JSC.getterWrap(ThisServer, "getPort"), + }, + .hostname = .{ + .get = JSC.getterWrap(ThisServer, "getHostname"), + }, + .development = .{ + .get = JSC.getterWrap(ThisServer, "getDevelopment"), + }, + .pendingRequests = .{ + .get = JSC.getterWrap(ThisServer, "getPendingRequests"), + }, + }, + ); + + pub fn stopFromJS(this: *ThisServer) JSC.JSValue { + if (this.listener != null) { + JSC.C.JSValueUnprotect(this.globalThis.ref(), this.thisObject.asObjectRef()); + this.thisObject = JSC.JSValue.jsUndefined(); + this.stop(); + } + + return JSC.JSValue.jsUndefined(); + } + + pub fn getPort(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(this.config.port); + } + + pub fn getPendingRequests(this: *ThisServer) JSC.JSValue { + return JSC.JSValue.jsNumber(@intCast(i32, @truncate(u31, this.pending_requests))); + } + + pub fn getHostname(this: *ThisServer, globalThis: *JSGlobalObject) JSC.JSValue { + return ZigString.init(this.config.base_uri).toValue(globalThis); + } + + pub fn getDevelopment( + _: *ThisServer, + ) JSC.JSValue { + return JSC.JSValue.jsBoolean(debug_mode); + } + + pub fn onRequestComplete(this: *ThisServer) void { + this.pending_requests -= 1; + this.deinitIfWeCan(); + } + + pub fn finalize(this: *ThisServer) void { + this.has_js_deinited = true; + this.deinitIfWeCan(); + } + + pub fn deinitIfWeCan(this: *ThisServer) void { + if (this.pending_requests == 0 and this.listener == null and this.has_js_deinited) + this.deinit(); + } + + pub fn stop(this: *ThisServer) void { + if (this.listener) |listener| { + listener.close(); + this.listener = null; + this.vm.disable_run_us_loop = false; + } + + this.deinitIfWeCan(); + } + + pub fn deinit(this: *ThisServer) void { + if (this.vm.response_objects_pool) |pool| { + if (pool == &this.response_objects_pool) { + this.vm.response_objects_pool = null; + } + } + + this.app.destroy(); + const allocator = this.allocator; + allocator.destroy(this); + } + + pub fn init(config: ServerConfig, globalThis: *JSGlobalObject) *ThisServer { + var server = bun.default_allocator.create(ThisServer) catch @panic("Out of memory!"); + server.* = .{ + .globalThis = globalThis, + .config = config, + .base_url_string_for_joining = strings.trim(config.base_url.href, "/"), + .vm = JSC.VirtualMachine.vm, + .allocator = Arena.getThreadlocalDefault(), + }; + if (RequestContext.pool == null) { + RequestContext.pool = server.allocator.create(RequestContext.RequestContextStackAllocator) catch @panic("Out of memory!"); + RequestContext.pool.?.* = .{ + .fallback_allocator = server.allocator, + }; + server.request_pool_allocator = RequestContext.pool.?.get(); + RequestContext.pool_allocator = server.request_pool_allocator; + } else { + server.request_pool_allocator = RequestContext.pool_allocator; + } + + return server; + } + + noinline fn onListenFailed(this: *ThisServer) void { + var zig_str: ZigString = ZigString.init("Failed to start server"); + if (comptime ssl_enabled) { + var output_buf: [4096]u8 = undefined; + output_buf[0] = 0; + var written: usize = 0; + var ssl_error = BoringSSL.ERR_get_error(); + while (ssl_error != 0 and written < output_buf.len) : (ssl_error = BoringSSL.ERR_get_error()) { + if (written > 0) { + output_buf[written] = '\n'; + written += 1; + } + + if (BoringSSL.ERR_reason_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len == 0) { + break; + } + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + + if (BoringSSL.ERR_func_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len > 0) { + output_buf[written..][0.." via ".len].* = " via ".*; + written += " via ".len; + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + } + + if (BoringSSL.ERR_lib_error_string( + ssl_error, + )) |reason_ptr| { + const reason = std.mem.span(reason_ptr); + if (reason.len > 0) { + output_buf[written..][0] = ' '; + written += 1; + @memcpy(output_buf[written..].ptr, reason.ptr, reason.len); + written += reason.len; + } + } + } + + if (written > 0) { + var message = output_buf[0..written]; + zig_str = ZigString.init(std.fmt.allocPrint(bun.default_allocator, "OpenSSL {s}", .{message}) catch unreachable); + zig_str.withEncoding().mark(); + } + } + // store the exception in here + this.thisObject = zig_str.toErrorInstance(this.globalThis); + return; + } + + pub fn onListen(this: *ThisServer, socket: ?*App.ListenSocket, _: uws.uws_app_listen_config_t) void { + if (socket == null) { + return this.onListenFailed(); + } + + this.listener = socket; + const needs_post_handler = this.vm.uws_event_loop == null; + this.vm.uws_event_loop = uws.Loop.get(); + this.vm.response_objects_pool = &this.response_objects_pool; + this.listen_callback = JSC.AnyTask.New(ThisServer, run).init(this); + this.vm.eventLoop().enqueueTask(JSC.Task.init(&this.listen_callback)); + if (needs_post_handler) { + _ = this.vm.uws_event_loop.?.addPostHandler(*JSC.EventLoop, this.vm.eventLoop(), JSC.EventLoop.tick); + } + } + + pub fn run(this: *ThisServer) void { + // this.app.addServerName(hostname_pattern: [*:0]const u8) + + // we do not increment the reference count here + // uWS manages running the loop, so it is unnecessary + // this.vm.us_loop_reference_count +|= 1; + this.vm.disable_run_us_loop = true; + + this.app.run(); + } + + pub fn onBunInfoRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + defer this.pending_requests -= 1; + req.setYield(false); + var stack_fallback = std.heap.stackFallback(8096, this.allocator); + var allocator = stack_fallback.get(); + + var buffer_writer = js_printer.BufferWriter.init(allocator) catch unreachable; + var writer = js_printer.BufferPrinter.init(buffer_writer); + defer writer.ctx.buffer.deinit(); + var source = logger.Source.initEmptyFile("info.json"); + _ = js_printer.printJSON( + *js_printer.BufferPrinter, + &writer, + bun.Global.BunInfo.generate(*Bundler, &JSC.VirtualMachine.vm.bundler, allocator) catch unreachable, + &source, + ) catch unreachable; + + resp.writeStatus("200 OK"); + resp.writeHeader("Content-Type", MimeType.json.value); + resp.writeHeader("Cache-Control", "public, max-age=3600"); + resp.writeHeaderInt("Age", 0); + const buffer = writer.ctx.written; + resp.end(buffer, false); + } + + pub fn onSrcRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + defer this.pending_requests -= 1; + req.setYield(false); + if (req.header("open-in-editor") == null) { + resp.writeStatus("501 Not Implemented"); + resp.end("Viewing source without opening in editor is not implemented yet!", false); + return; + } + + var ctx = &JSC.VirtualMachine.vm.rareData().editor_context; + ctx.autoDetectEditor(JSC.VirtualMachine.vm.bundler.env); + var line: ?string = req.header("editor-line"); + var column: ?string = req.header("editor-column"); + + if (ctx.editor) |editor| { + resp.writeStatus("200 Opened"); + resp.end("Opened in editor", false); + var url = req.url()["/src:".len..]; + if (strings.indexOfChar(url, ':')) |colon| { + url = url[0..colon]; + } + editor.open(ctx.path, url, line, column, this.allocator) catch Output.prettyErrorln("Failed to open editor", .{}); + } else { + resp.writeStatus("500 Missing Editor :("); + resp.end("Please set your editor in bunfig.toml", false); + } + } + + pub fn onRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { + JSC.markBinding(); + this.pending_requests += 1; + var vm = this.vm; + req.setYield(false); + var ctx = this.request_pool_allocator.create(RequestContext) catch @panic("ran out of memory"); + ctx.create(this, req, resp); + + var request_object = this.allocator.create(JSC.WebCore.Request) catch unreachable; + request_object.* = .{ + .url = JSC.ZigString.init(ctx.url), + .method = ctx.method, + .uws_request = req, + .body = .{ + .Locked = .{ + .task = ctx, + .global = this.globalThis, + .onPull = RequestContext.onPullCallback, + }, + }, + }; + request_object.url.mark(); + // We keep the Request object alive for the duration of the request so that we can remove the pointer to the UWS request object. + var args = [_]JSC.C.JSValueRef{JSC.WebCore.Request.Class.make(this.globalThis.ref(), request_object)}; + ctx.request_js_object = args[0]; + JSC.C.JSValueProtect(this.globalThis.ref(), args[0]); + const response_value = JSC.C.JSObjectCallAsFunctionReturnValue(this.globalThis.ref(), this.config.onRequest.asObjectRef(), this.thisObject.asObjectRef(), 1, &args); + + if (ctx.aborted) { + ctx.finalizeForAbort(); + return; + } + if (response_value.isEmptyOrUndefinedOrNull() and !ctx.resp.hasResponded()) { + ctx.renderMissing(); + return; + } + + if (response_value.isError() or response_value.isAggregateError(this.globalThis) or response_value.isException(this.globalThis.vm())) { + ctx.runErrorHandler(response_value); + return; + } + + if (response_value.as(JSC.WebCore.Response)) |response| { + JSC.C.JSValueProtect(this.globalThis.ref(), response_value.asObjectRef()); + ctx.response_jsvalue = response_value; + + ctx.render(response); + return; + } + + var wait_for_promise = false; + + if (response_value.asPromise()) |promise| { + // If we immediately have the value available, we can skip the extra event loop tick + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + // I don't think this case should happen + // But I'm uncertain + } else if (response_value.asInternalPromise()) |promise| { + switch (promise.status(vm.global.vm())) { + .Pending => {}, + .Fulfilled => { + ctx.handleResolve(promise.result(vm.global.vm())); + return; + }, + .Rejected => { + ctx.handleReject(promise.result(vm.global.vm())); + return; + }, + } + wait_for_promise = true; + } + + if (wait_for_promise) { + ctx.setAbortHandler(); + + RequestContext.PromiseHandler.then(ctx, response_value, this.globalThis); + return; + } + + // The user returned something that wasn't a promise or a promise with a response + if (!ctx.resp.hasResponded()) ctx.renderMissing(); + } + + pub fn listen(this: *ThisServer) void { + if (ssl_enabled) { + BoringSSL.load(); + const ssl_config = this.config.ssl_config orelse @panic("Assertion failure: ssl_config"); + this.app = App.create(.{ + .key_file_name = ssl_config.key_file_name, + .cert_file_name = ssl_config.cert_file_name, + .passphrase = ssl_config.passphrase, + .dh_params_file_name = ssl_config.dh_params_file_name, + .ca_file_name = ssl_config.ca_file_name, + .ssl_prefer_low_memory_usage = @as(c_int, @boolToInt(ssl_config.low_memory_mode)), + }); + + if (ssl_config.server_name != null and std.mem.span(ssl_config.server_name).len > 0) { + this.app.addServerName(ssl_config.server_name); + } + } else { + this.app = App.create(.{}); + } + + this.app.any("/*", *ThisServer, this, onRequest); + + if (comptime debug_mode) { + this.app.get("/bun:info", *ThisServer, this, onBunInfoRequest); + this.app.get("/src:/*", *ThisServer, this, onSrcRequest); + } + + this.app.listenWithConfig(*ThisServer, this, onListen, .{ + .port = this.config.port, + .host = this.config.hostname, + .options = 0, + }); + } + }; +} + +pub const Server = NewServer(false, false); +pub const SSLServer = NewServer(true, false); +pub const DebugServer = NewServer(false, true); +pub const DebugSSLServer = NewServer(true, true); |