diff --git a/src/Config.zig b/src/Config.zig index 452c1221..709c70c3 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -103,6 +103,13 @@ pub fn httpMaxRedirects(_: *const Config) u8 { return 10; } +pub fn httpMaxResponseSize(self: *const Config) ?usize { + return switch (self.mode) { + inline .serve, .fetch => |opts| opts.common.http_max_response_size, + else => unreachable, + }; +} + pub fn logLevel(self: *const Config) ?log.Level { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.log_level, @@ -164,6 +171,7 @@ pub const Common = struct { http_max_host_open: ?u8 = null, http_timeout: ?u31 = null, http_connect_timeout: ?u31 = null, + http_max_response_size: ?usize = null, tls_verify_host: bool = true, log_level: ?log.Level = null, log_format: ?log.Format = null, @@ -249,6 +257,11 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void { \\ to complete. 0 means it never times out. \\ Defaults to 10000. \\ + \\--http_max_response_size + \\ Limits the acceptable response size for any request + \\ (e.g. XHR, fetch, script loading, ...). + \\ Defaults to no limit. + \\ \\--log_level The log level: debug, info, warn, error or fatal. \\ Defaults to ++ (if (builtin.mode == .Debug) " info." else "warn.") ++ @@ -683,6 +696,19 @@ fn parseCommonArg( return true; } + if (std.mem.eql(u8, "--http_max_response_size", opt)) { + const str = args.next() orelse { + log.fatal(.app, "missing argument value", .{ .arg = "--http_max_response_size" }); + return error.InvalidArgument; + }; + + common.http_max_response_size = std.fmt.parseInt(usize, str, 10) catch |err| { + log.fatal(.app, "invalid argument value", .{ .arg = "--http_max_response_size", .err = err }); + return error.InvalidArgument; + }; + return true; + } + if (std.mem.eql(u8, "--log_level", opt)) { const str = args.next() orelse { log.fatal(.app, "missing argument value", .{ .arg = "--log_level" }); diff --git a/src/http/Client.zig b/src/http/Client.zig index 22b280c1..4124e26c 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -366,17 +366,16 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer { .req = req, .ctx = req.ctx, .client = self, + .max_response_size = self.config.httpMaxResponseSize(), }; return transfer; } fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback: bool) void { - // this shouldn't happen, we'll crash in debug mode. But in release, we'll - // just noop this state. - if (comptime IS_DEBUG) { - std.debug.assert(transfer._notified_fail == false); - } if (transfer._notified_fail) { + // we can force a failed request within a callback, which will eventually + // result in this being called again in the more general loop. We do this + // because we can raise a more specific error inside a callback in some cases return; } @@ -787,6 +786,7 @@ pub const Request = struct { resource_type: ResourceType, credentials: ?[:0]const u8 = null, notification: *Notification, + max_response_size: ?usize = null, // This is only relevant for intercepted requests. If a request is flagged // as blocking AND is intercepted, then it'll be up to us to wait until @@ -877,6 +877,8 @@ pub const Transfer = struct { // the headers, and the [encoded] body. bytes_received: usize = 0, + max_response_size: ?usize = null, + // We'll store the response header here response_header: ?ResponseHeader = null, @@ -1125,6 +1127,14 @@ pub const Transfer = struct { } } + if (transfer.max_response_size) |max_size| { + if (transfer.getContentLength()) |cl| { + if (cl > max_size) { + return error.ResponseTooLarge; + } + } + } + const proceed = transfer.req.header_callback(transfer) catch |err| { log.err(.http, "header_callback", .{ .err = err, .req = transfer }); return err; @@ -1276,6 +1286,13 @@ pub const Transfer = struct { } transfer.bytes_received += chunk_len; + if (transfer.max_response_size) |max_size| { + if (transfer.bytes_received > max_size) { + requestFailed(transfer, error.ResponseTooLarge, true); + return -1; + } + } + const chunk = buffer[0..chunk_len]; transfer.req.data_callback(transfer, chunk) catch |err| { log.err(.http, "data_callback", .{ .err = err, .req = transfer });