diff options
Diffstat (limited to 'src/javascript/jsc/api/server.zig')
-rw-r--r-- | src/javascript/jsc/api/server.zig | 1844 |
1 files changed, 0 insertions, 1844 deletions
diff --git a/src/javascript/jsc/api/server.zig b/src/javascript/jsc/api/server.zig deleted file mode 100644 index 711329a95..000000000 --- a/src/javascript/jsc/api/server.zig +++ /dev/null @@ -1,1844 +0,0 @@ -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); |