diff options
-rw-r--r-- | src/deps/uws.zig | 14 | ||||
-rw-r--r-- | src/javascript/jsc/api/server.zig | 116 | ||||
-rw-r--r-- | src/javascript/jsc/webcore/response.zig | 32 |
3 files changed, 132 insertions, 30 deletions
diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 2f590606c..792c11aa2 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -158,13 +158,15 @@ pub const Request = opaque { var ptr: [*]const u8 = undefined; return ptr[0..req.uws_req_get_method(&ptr)]; } - pub fn header(req: *Request, name: []const u8) []const u8 { + pub fn header(req: *Request, name: []const u8) ?[]const u8 { var ptr: [*]const u8 = undefined; - return ptr[0..req.uws_req_get_header(req, name.ptr, name.len, &ptr)]; + const len = req.uws_req_get_header(name.ptr, name.len, &ptr); + if (len == 0) return null; + return ptr[0..len]; } pub fn query(req: *Request, name: []const u8) []const u8 { var ptr: [*]const u8 = undefined; - return ptr[0..req.uws_req_get_query(req, name.ptr, name.len, &ptr)]; + return ptr[0..req.uws_req_get_query(name.ptr, name.len, &ptr)]; } pub fn parameter(req: *Request, index: u16) []const u8 { var ptr: [*]const u8 = undefined; @@ -173,7 +175,7 @@ pub const Request = opaque { pub fn cloneHeaders( req: *Request, - ctx: *anyopaque, + ctx: *anyopaque, ) void { uws_req_clone_headers(req, ctx); } @@ -190,7 +192,7 @@ pub const Request = opaque { extern fn uws_req_set_field(res: *Request, yield: bool) void; extern fn uws_req_get_url(res: *Request, dest: *[*]const u8) usize; extern fn uws_req_get_method(res: *Request, dest: *[*]const u8) usize; - extern fn uws_req_get_header(res: *Request, lower_case_header: *[*]const u8, lower_case_header_length: usize, dest: *[*]const u8) usize; + extern fn uws_req_get_header(res: *Request, lower_case_header: [*]const u8, lower_case_header_length: usize, dest: *[*]const u8) usize; extern fn uws_req_get_query(res: *Request, key: [*c]const u8, key_length: usize, dest: *[*]const u8) usize; extern fn uws_req_get_parameter(res: *Request, index: c_ushort, dest: *[*]const u8) usize; }; @@ -511,7 +513,7 @@ pub fn NewApp(comptime ssl: bool) type { pub fn onData( res: *Response, comptime UserDataType: type, - comptime handler: fn (*Response, chunk: []const u8, last: bool, UserDataType) void, + comptime handler: fn (UserDataType, *Response, chunk: []const u8, last: bool) void, opcional_data: UserDataType, ) void { const Wrapper = struct { diff --git a/src/javascript/jsc/api/server.zig b/src/javascript/jsc/api/server.zig index d7e7ccd64..f0cb7af03 100644 --- a/src/javascript/jsc/api/server.zig +++ b/src/javascript/jsc/api/server.zig @@ -91,7 +91,8 @@ const linux = std.os.linux; pub const ServerConfig = struct { port: u16 = 0, - hostnames: std.ArrayList([*:0]const u8) = .{}, + hostnames: std.ArrayListUnmanaged([*:0]const u8) = .{}, + max_request_body_size: usize = 1024 * 1024 * 128, }; pub fn NewServer(comptime ssl_enabled: bool) type { @@ -108,7 +109,7 @@ pub fn NewServer(comptime ssl_enabled: bool) type { globalThis: *JSGlobalObject, default_server: URL = URL{ .host = "localhost", .port = "3000" }, response_objects_pool: JSC.WebCore.Response.Pool = JSC.WebCore.Response.Pool{}, - + config: ServerConfig = ServerConfig{}, request_pool_allocator: std.mem.Allocator = undefined, pub fn init(port: u16, callback: JSC.JSValue, globalThis: *JSGlobalObject) *ThisServer { @@ -152,6 +153,8 @@ pub fn NewServer(comptime ssl_enabled: bool) type { has_sendfile_ctx: bool = false, sendfile: SendfileContext = undefined, request_js_object: JSC.C.JSObjectRef = null, + request_body_buf: std.ArrayListUnmanaged(u8) = .{}, + pub threadlocal var pool: *RequestContextStackAllocator = undefined; pub fn setAbortHandler(this: *RequestContext) void { @@ -177,7 +180,8 @@ pub fn NewServer(comptime ssl_enabled: bool) type { } var response = arguments[0].as(JSC.WebCore.Response) orelse { - Output.prettyErrorln("Expected serverless to return a Response", .{}); + Output.prettyErrorln("Expected a Response object", .{}); + Output.flush(); ctx.req.setYield(true); ctx.finalize(); return; @@ -190,12 +194,13 @@ pub fn NewServer(comptime ssl_enabled: bool) type { _: *JSC.JSGlobalObject, arguments: []const JSC.JSValue, ) void { + JSC.VirtualMachine.vm.defaultErrorHandler(arguments[0], null); + if (ctx.aborted) { ctx.finalize(); return; } - JSC.VirtualMachine.vm.defaultErrorHandler(arguments[0], null); ctx.req.setYield(true); ctx.finalize(); } @@ -212,22 +217,31 @@ pub fn NewServer(comptime ssl_enabled: bool) type { pub fn onAbort(this: *RequestContext, _: *App.Response) void { this.aborted = true; - this.req = undefined; - if (!this.response_jsvalue.isEmpty()) { - this.server.response_objects_pool.push(this.server.globalThis, this.response_jsvalue); - this.response_jsvalue = JSC.JSValue.zero; - } + this.finalizeWithoutDeinit(); } - pub fn finalize(this: *RequestContext) void { + // 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(); + this.request_body_buf.clearAndFree(bun.default_allocator); + 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) { + // User called .blob(), .json(), text(), or .arrayBuffer() on the Request object + // but we received nothing if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + if (req.body == .Locked and req.body.Locked.action != .none and req.body.Locked.promise != null) { + var old_body = req.body; + req.body = JSC.WebCore.Body.Value.empty; + old_body.Locked.callback = null; + old_body.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + } req.uws_request = null; JSC.C.JSValueUnprotect(this.server.globalThis.ref(), this.request_js_object); this.request_js_object = null; @@ -243,6 +257,9 @@ pub fn NewServer(comptime ssl_enabled: bool) type { this.response_headers.?.deref(); this.response_headers = null; } + } + pub fn finalize(this: *RequestContext) void { + this.finalizeWithoutDeinit(); this.server.request_pool_allocator.destroy(this); } @@ -469,6 +486,76 @@ pub fn NewServer(comptime ssl_enabled: bool) type { // this.resp.runCorked(*RequestContext, doRender, this); this.doRender(); } + + pub fn resolveRequestBody(this: *RequestContext) void { + if (this.aborted) + return; + if (JSC.JSValue.fromRef(this.request_js_object).as(Request)) |req| { + var bytes = this.request_body_buf.toOwnedSlice(bun.default_allocator); + var old = req.body; + req.body = .{ + .Blob = Blob.init(bytes, bun.default_allocator, this.server.globalThis), + }; + old.resolve(&req.body, this.server.globalThis); + VirtualMachine.vm.tick(); + return; + } + } + + pub fn onBodyChunk(this: *RequestContext, _: *App.Response, chunk: []const u8, last: bool) void { + if (this.aborted) return; + this.request_body_buf.appendSlice(bun.default_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(bun.default_allocator); + this.request_body_buf = .{}; + } + } + } + + pub fn onRequestData(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(bun.default_allocator, len) catch @panic("Out of memory while allocating request body buffer"); + } + this.setAbortHandler(); + + this.resp.onData(*RequestContext, onBodyChunk, this); + } + + pub fn onRequestDataCallback(this: *anyopaque) void { + onRequestData(bun.cast(*RequestContext, this)); + } }; pub fn onRequest(this: *ThisServer, req: *uws.Request, resp: *App.Response) void { @@ -481,6 +568,13 @@ pub fn NewServer(comptime ssl_enabled: bool) type { .url = JSC.ZigString.init(ctx.url), .method = ctx.method, .uws_request = req, + .body = .{ + .Locked = .{ + .task = ctx, + .global = this.globalThis, + .onRequestData = RequestContext.onRequestDataCallback, + }, + }, }; // 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)}; @@ -494,7 +588,7 @@ pub fn NewServer(comptime ssl_enabled: bool) type { return; } - if (ctx.response_jsvalue.isUndefinedOrNull()) { + if (ctx.response_jsvalue.isUndefinedOrNull() or ctx.response_jsvalue.isError() or ctx.response_jsvalue.isAggregateError(this.globalThis) or ctx.response_jsvalue.isException(this.globalThis.vm())) { req.setYield(true); ctx.finalize(); return; diff --git a/src/javascript/jsc/webcore/response.zig b/src/javascript/jsc/webcore/response.zig index b9cfdc415..407d1fc6f 100644 --- a/src/javascript/jsc/webcore/response.zig +++ b/src/javascript/jsc/webcore/response.zig @@ -4111,10 +4111,24 @@ pub const Body = struct { promise: ?JSValue = null, global: *JSGlobalObject, task: ?*anyopaque = null, + /// runs after the data is available. callback: ?fn (ctx: *anyopaque, value: *Value) void = null, + /// conditionally runs when requesting data + /// used in HTTP server to ignore request bodies unless asked for it + onRequestData: ?fn (ctx: *anyopaque) void = null, deinit: bool = false, action: Action = Action.none, + pub fn setPromise(value: *PendingValue, globalThis: *JSC.JSGlobalObject, action: Action) void { + value.action = action; + var promise = JSC.JSPromise.create(globalThis); + value.promise = promise.asValue(globalThis); + if (value.onRequestData) |onRequestData| { + value.onRequestData = null; + onRequestData(value.task.?); + } + } + pub const Action = enum { none, getText, @@ -4150,6 +4164,7 @@ pub const Body = struct { } if (locked.promise) |promise| { + locked.promise = null; var blob = new.use(); switch (locked.action) { @@ -4177,7 +4192,6 @@ pub const Body = struct { }, } JSC.C.JSValueUnprotect(global.ref(), promise.asObjectRef()); - locked.promise = null; } } } @@ -4718,9 +4732,7 @@ fn BlobInterface(comptime Type: type) type { ) js.JSValueRef { var value = this.getBodyValue(); if (value.* == .Locked) { - value.Locked.action = .getText; - var promise = JSC.JSPromise.create(ctx.ptr()); - value.Locked.promise = promise.asValue(ctx.ptr()); + value.Locked.setPromise(ctx.ptr(), .getText); return value.Locked.promise.?.asObjectRef(); } @@ -4738,9 +4750,7 @@ fn BlobInterface(comptime Type: type) type { ) js.JSValueRef { var value = this.getBodyValue(); if (value.* == .Locked) { - value.Locked.action = .getJSON; - var promise = JSC.JSPromise.create(ctx.ptr()); - value.Locked.promise = promise.asValue(ctx.ptr()); + value.Locked.setPromise(ctx.ptr(), .getJSON); return value.Locked.promise.?.asObjectRef(); } @@ -4758,9 +4768,7 @@ fn BlobInterface(comptime Type: type) type { var value = this.getBodyValue(); if (value.* == .Locked) { - value.Locked.action = .getArrayBuffer; - var promise = JSC.JSPromise.create(ctx.ptr()); - value.Locked.promise = promise.asValue(ctx.ptr()); + value.Locked.setPromise(ctx.ptr(), .getArrayBuffer); return value.Locked.promise.?.asObjectRef(); } @@ -4778,9 +4786,7 @@ fn BlobInterface(comptime Type: type) type { ) js.JSValueRef { var value = this.getBodyValue(); if (value.* == .Locked) { - value.Locked.action = .getBlob; - var promise = JSC.JSPromise.create(ctx.ptr()); - value.Locked.promise = promise.asValue(ctx.ptr()); + value.Locked.setPromise(ctx.ptr(), .getBlob); return value.Locked.promise.?.asObjectRef(); } |