diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 412918ad0c91eca3bc86ebc73e90e86dbad8693c..ae8484a15dd668de58119e0334c722d76d95cc8c 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -1,6 +1,13 @@ //! Uniform Resource Identifier (URI) parsing roughly adhering to . //! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild. +const std = @import("std.zig"); +const testing = std.testing; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const Uri = @This(); + scheme: []const u8, user: ?Component = null, password: ?Component = null, @@ -10,6 +17,32 @@ path: Component = Component.empty, query: ?Component = null, fragment: ?Component = null, +pub const host_name_max = 255; + +/// Returned value may point into `buffer` or be the original string. +/// +/// Suggested buffer length: `host_name_max`. +/// +/// See also: +/// * `getHostAlloc` +pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 { + const component = uri.host orelse return error.UriMissingHost; + return component.toRaw(buffer) catch |err| switch (err) { + error.NoSpaceLeft => return error.UriHostTooLong, + }; +} + +/// Returned value may point into `buffer` or be the original string. +/// +/// See also: +/// * `getHost` +pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 { + const component = uri.host orelse return error.UriMissingHost; + const result = try component.toRawMaybeAlloc(arena); + if (result.len > host_name_max) return error.UriHostTooLong; + return result; +} + pub const Component = union(enum) { /// Invalid characters in this component must be percent encoded /// before being printed as part of a URI. @@ -26,11 +59,22 @@ pub const Component = union(enum) { }; } + /// Returned value may point into `buffer` or be the original string. + pub fn toRaw(component: Component, buffer: []u8) error{NoSpaceLeft}![]const u8 { + return switch (component) { + .raw => |raw| raw, + .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| + try std.fmt.bufPrint(buffer, "{fraw}", .{component}) + else + percent_encoded, + }; + } + /// Allocates the result with `arena` only if needed, so the result should not be freed. pub fn toRawMaybeAlloc( component: Component, - arena: std.mem.Allocator, - ) std.mem.Allocator.Error![]const u8 { + arena: Allocator, + ) Allocator.Error![]const u8 { return switch (component) { .raw => |raw| raw, .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| @@ -144,17 +188,15 @@ pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; /// The return value will contain strings pointing into the original `text`. /// Each component that is provided, will be non-`null`. pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { - var reader = SliceReader{ .slice = text }; - var uri: Uri = .{ .scheme = scheme, .path = undefined }; + var i: usize = 0; - if (reader.peekPrefix("//")) a: { // authority part - std.debug.assert(reader.get().? == '/'); - std.debug.assert(reader.get().? == '/'); - - const authority = reader.readUntil(isAuthoritySeparator); + if (std.mem.startsWith(u8, text, "//")) a: { + i = std.mem.indexOfAnyPos(u8, text, 2, &authority_sep) orelse text.len; + const authority = text[2..i]; if (authority.len == 0) { - if (reader.peekPrefix("/")) break :a else return error.InvalidFormat; + if (!std.mem.startsWith(u8, text[2..], "/")) return error.InvalidFormat; + break :a; } var start_of_host: usize = 0; @@ -204,16 +246,18 @@ pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] }; } - uri.path = .{ .percent_encoded = reader.readUntil(isPathSeparator) }; + const path_start = i; + i = std.mem.indexOfAnyPos(u8, text, path_start, &path_sep) orelse text.len; + uri.path = .{ .percent_encoded = text[path_start..i] }; - if ((reader.peek() orelse 0) == '?') { // query part - std.debug.assert(reader.get().? == '?'); - uri.query = .{ .percent_encoded = reader.readUntil(isQuerySeparator) }; + if (std.mem.startsWith(u8, text[i..], "?")) { + const query_start = i + 1; + i = std.mem.indexOfScalarPos(u8, text, query_start, '#') orelse text.len; + uri.query = .{ .percent_encoded = text[query_start..i] }; } - if ((reader.peek() orelse 0) == '#') { // fragment part - std.debug.assert(reader.get().? == '#'); - uri.fragment = .{ .percent_encoded = reader.readUntilEof() }; + if (std.mem.startsWith(u8, text[i..], "#")) { + uri.fragment = .{ .percent_encoded = text[i + 1 ..] }; } return uri; @@ -291,41 +335,33 @@ pub fn format(uri: Uri, bw: *std.io.BufferedWriter, comptime fmt: []const u8) st }, bw); } -/// Parses the URI or returns an error. -/// The return value will contain strings pointing into the -/// original `text`. Each component that is provided, will be non-`null`. +/// The return value will contain strings pointing into the original `text`. +/// Each component that is provided will be non-`null`. pub fn parse(text: []const u8) ParseError!Uri { - var reader: SliceReader = .{ .slice = text }; - const scheme = reader.readWhile(isSchemeChar); - - // after the scheme, a ':' must appear - if (reader.get()) |c| { - if (c != ':') - return error.UnexpectedCharacter; - } else { - return error.InvalidFormat; - } - - return parseAfterScheme(scheme, reader.readUntilEof()); + const end = for (text, 0..) |byte, i| { + if (!isSchemeChar(byte)) break i; + } else text.len; + // After the scheme, a ':' must appear. + if (end >= text.len) return error.InvalidFormat; + if (text[end] != ':') return error.UnexpectedCharacter; + return parseAfterScheme(text[0..end], text[end + 1 ..]); } pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft}; /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. -/// Copies `new` to the beginning of `aux_buf.*`, allowing the slices to overlap, -/// then parses `new` as a URI, and then resolves the path in place. +/// +/// Assumes new location is already copied to the beginning of `aux_buf.*`. +/// Parses that new location as a URI, and then resolves the path in place. +/// /// If a merge needs to take place, the newly constructed path will be stored -/// in `aux_buf.*` just after the copied `new`, and `aux_buf.*` will be modified -/// to only contain the remaining unused space. -pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri { - std.mem.copyForwards(u8, aux_buf.*, new); - // At this point, new is an invalid pointer. - const new_mut = aux_buf.*[0..new.len]; - aux_buf.* = aux_buf.*[new.len..]; - - const new_parsed = parse(new_mut) catch |err| - (parseAfterScheme("", new_mut) catch return err); - // As you can see above, `new_mut` is not a const pointer. +/// in `aux_buf.*` just after the copied location, and `aux_buf.*` will be +/// modified to only contain the remaining unused space. +pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceError!Uri { + const new = aux_buf.*[0..new_len]; + const new_parsed = parse(new) catch |err| (parseAfterScheme("", new) catch return err); + aux_buf.* = aux_buf.*[new_len..]; + // As you can see above, `new` is not a const pointer. const new_path: []u8 = @constCast(new_parsed.path.percent_encoded); if (new_parsed.scheme.len > 0) return .{ @@ -438,59 +474,6 @@ fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Co return merged_path; } -const SliceReader = struct { - const Self = @This(); - - slice: []const u8, - offset: usize = 0, - - fn get(self: *Self) ?u8 { - if (self.offset >= self.slice.len) - return null; - const c = self.slice[self.offset]; - self.offset += 1; - return c; - } - - fn peek(self: Self) ?u8 { - if (self.offset >= self.slice.len) - return null; - return self.slice[self.offset]; - } - - fn readWhile(self: *Self, comptime predicate: fn (u8) bool) []const u8 { - const start = self.offset; - var end = start; - while (end < self.slice.len and predicate(self.slice[end])) { - end += 1; - } - self.offset = end; - return self.slice[start..end]; - } - - fn readUntil(self: *Self, comptime predicate: fn (u8) bool) []const u8 { - const start = self.offset; - var end = start; - while (end < self.slice.len and !predicate(self.slice[end])) { - end += 1; - } - self.offset = end; - return self.slice[start..end]; - } - - fn readUntilEof(self: *Self) []const u8 { - const start = self.offset; - self.offset = self.slice.len; - return self.slice[start..]; - } - - fn peekPrefix(self: Self, prefix: []const u8) bool { - if (self.offset + prefix.len > self.slice.len) - return false; - return std.mem.eql(u8, self.slice[self.offset..][0..prefix.len], prefix); - } -}; - /// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) fn isSchemeChar(c: u8) bool { return switch (c) { @@ -499,19 +482,6 @@ fn isSchemeChar(c: u8) bool { }; } -/// reserved = gen-delims / sub-delims -fn isReserved(c: u8) bool { - return isGenLimit(c) or isSubLimit(c); -} - -/// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" -fn isGenLimit(c: u8) bool { - return switch (c) { - ':', ',', '?', '#', '[', ']', '@' => true, - else => false, - }; -} - /// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" /// / "*" / "+" / "," / ";" / "=" fn isSubLimit(c: u8) bool { @@ -551,26 +521,8 @@ fn isQueryChar(c: u8) bool { const isFragmentChar = isQueryChar; -fn isAuthoritySeparator(c: u8) bool { - return switch (c) { - '/', '?', '#' => true, - else => false, - }; -} - -fn isPathSeparator(c: u8) bool { - return switch (c) { - '?', '#' => true, - else => false, - }; -} - -fn isQuerySeparator(c: u8) bool { - return switch (c) { - '#' => true, - else => false, - }; -} +const authority_sep: [3]u8 = .{ '/', '?', '#' }; +const path_sep: [2]u8 = .{ '?', '#' }; test "basic" { const parsed = try parse("https://ziglang.org/download"); @@ -851,7 +803,3 @@ test "URI malformed input" { try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@[")); try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q")); } - -const std = @import("std.zig"); -const testing = std.testing; -const Uri = @This(); diff --git a/lib/std/http.zig b/lib/std/http.zig index d5d5583299e258bae2096b6885b2453ba8da63d3..e808cd201ec6e9b2e95846e5e3dfe9a0b0011b43 100644 --- a/lib/std/http.zig +++ b/lib/std/http.zig @@ -1,6 +1,9 @@ +const builtin = @import("builtin"); +const std = @import("std.zig"); +const assert = std.debug.assert; + pub const Client = @import("http/Client.zig"); pub const Server = @import("http/Server.zig"); -pub const protocol = @import("http/protocol.zig"); pub const HeadParser = @import("http/HeadParser.zig"); pub const ChunkParser = @import("http/ChunkParser.zig"); pub const HeaderIterator = @import("http/HeaderIterator.zig"); @@ -77,7 +80,9 @@ pub const Method = enum(u64) { }; } - /// An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. + /// An HTTP method is idempotent if an identical request can be made once + /// or several times in a row with the same effect while leaving the server + /// in the same state. /// /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent /// @@ -90,7 +95,8 @@ pub const Method = enum(u64) { }; } - /// A cacheable response is an HTTP response that can be cached, that is stored to be retrieved and used later, saving a new request to the server. + /// A cacheable response can be stored to be retrieved and used later, + /// saving a new request to the server. /// /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable /// @@ -282,10 +288,10 @@ pub const Status = enum(u10) { } }; +/// compression is intentionally omitted here since it is handled in `ContentEncoding`. pub const TransferEncoding = enum { chunked, none, - // compression is intentionally omitted here, as std.http.Client stores it as content-encoding }; pub const ContentEncoding = enum { @@ -308,18 +314,740 @@ pub const Header = struct { value: []const u8, }; -const builtin = @import("builtin"); -const std = @import("std.zig"); +pub const Reader = struct { + in: *std.io.BufferedReader, + /// Keeps track of whether the stream is ready to accept a new request, + /// making invalid API usage cause assertion failures rather than HTTP + /// protocol violations. + state: State, + /// Number of bytes of HTTP trailers. These are at the end of a + /// transfer-encoding: chunked message. + trailers_len: usize = 0, + body_state: union { + none: void, + remaining_content_length: u64, + remaining_chunk_len: RemainingChunkLen, + }, + body_err: ?BodyError = null, + /// Stolen from `in`. + head_buffer: []u8 = &.{}, + + pub const max_chunk_header_len = 22; + + pub const RemainingChunkLen = enum(u64) { + head = 0, + n = 1, + rn = 2, + done = std.math.maxInt(u64), + _, + + pub fn init(integer: u64) RemainingChunkLen { + return @enumFromInt(integer); + } + + pub fn int(rcl: RemainingChunkLen) u64 { + return @intFromEnum(rcl); + } + }; + + pub const State = enum { + /// The stream is available to be used for the first time, or reused. + ready, + receiving_head, + received_head, + receiving_body, + /// The stream would be eligible for another HTTP request, however the + /// client and server did not negotiate a persistent connection. + closing, + }; + + pub const BodyError = error{ + HttpChunkInvalid, + HttpHeadersOversize, + }; + + pub const HeadError = error{ + /// Too many bytes of HTTP headers. + /// + /// The HTTP specification suggests to respond with a 431 status code + /// before closing the connection. + HttpHeadersOversize, + /// Partial HTTP request was received but the connection was closed + /// before fully receiving the headers. + HttpRequestTruncated, + /// The client sent 0 bytes of headers before closing the stream. This + /// happens when a keep-alive connection is finally closed. + HttpConnectionClosing, + /// Transitive error occurred reading from `in`. + ReadFailed, + }; + + /// Buffers the entire head into `head_buffer`, invalidating the previous + /// `head_buffer`, if any. + pub fn receiveHead(reader: *Reader) HeadError!void { + const in = reader.in; + in.restitute(reader.head_buffer.len); + in.rebase(); + var hp: HeadParser = .{}; + var head_end: usize = 0; + while (true) { + if (head_end >= in.buffer.len) return error.HttpHeadersOversize; + const buf = in.peekGreedy(head_end + 1) catch |err| switch (err) { + error.EndOfStream => switch (head_end) { + 0 => return error.HttpConnectionClosing, + else => return error.HttpRequestTruncated, + }, + error.ReadFailed => return error.ReadFailed, + }; + head_end += hp.feed(buf[head_end..]); + if (hp.state == .finished) { + reader.head_buffer = in.steal(head_end); + return; + } + } + } + + /// Asserts only called once and after `receiveHead`. + pub fn interface(reader: *Reader, transfer_encoding: TransferEncoding, content_length: ?u64) std.io.Reader { + assert(reader.state == .received_head); + reader.state = .receiving_body; + switch (transfer_encoding) { + .chunked => { + reader.body_state = .{ .remaining_chunk_len = .head }; + return .{ + .context = reader, + .vtable = &.{ + .read = &chunkedRead, + .readVec = &chunkedReadVec, + .discard = &chunkedDiscard, + }, + }; + }, + .none => { + if (content_length) |len| { + reader.body_state = .{ .remaining_content_length = len }; + return .{ + .context = reader, + .vtable = &.{ + .read = &contentLengthRead, + .readVec = &contentLengthReadVec, + .discard = &contentLengthDiscard, + }, + }; + } else { + return reader.in.reader(); + } + }, + } + } + + fn contentLengthRead( + ctx: ?*anyopaque, + bw: *std.io.BufferedWriter, + limit: std.io.Reader.Limit, + ) std.io.Reader.RwError!usize { + const reader: *Reader = @alignCast(@ptrCast(ctx)); + const remaining_content_length = &reader.body_state.remaining_content_length; + const remaining = remaining_content_length.*; + if (remaining == 0) { + reader.state = .ready; + return error.EndOfStream; + } + const n = try reader.in.read(bw, limit.min(.limited(remaining))); + remaining_content_length.* = remaining - n; + return n; + } + + fn contentLengthReadVec(context: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { + const reader: *Reader = @alignCast(@ptrCast(context)); + const remaining_content_length = &reader.body_state.remaining_content_length; + const remaining = remaining_content_length.*; + if (remaining == 0) { + reader.state = .ready; + return error.EndOfStream; + } + const n = try reader.in.readVecLimit(data, .limited(remaining)); + remaining_content_length.* = remaining - n; + return n; + } + + fn contentLengthDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { + const reader: *Reader = @alignCast(@ptrCast(ctx)); + const remaining_content_length = &reader.body_state.remaining_content_length; + const remaining = remaining_content_length.*; + if (remaining == 0) { + reader.state = .ready; + return error.EndOfStream; + } + const n = try reader.in.discard(limit.min(.limited(remaining))); + remaining_content_length.* = remaining - n; + return n; + } + + fn chunkedRead( + ctx: ?*anyopaque, + bw: *std.io.BufferedWriter, + limit: std.io.Reader.Limit, + ) std.io.Reader.RwError!usize { + const reader: *Reader = @alignCast(@ptrCast(ctx)); + const chunk_len_ptr = &reader.body_state.remaining_chunk_len; + const in = reader.in; + len: switch (chunk_len_ptr.*) { + .head => { + var cp: ChunkParser = .init; + const i = cp.feed(in.bufferContents()); + switch (cp.state) { + .invalid => return reader.failBody(error.HttpChunkInvalid), + .data => { + if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(i); + }, + else => { + try in.fill(max_chunk_header_len); + const next_i = cp.feed(in.bufferContents()[i..]); + if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); + const header_len = i + next_i; + if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(header_len); + }, + } + if (cp.chunk_len == 0) return parseTrailers(reader, 0); + const n = try in.read(bw, limit.min(.limited(cp.chunk_len))); + chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); + return n; + }, + .n => { + if ((try in.peekByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); + in.toss(1); + continue :len .head; + }, + .rn => { + const rn = try in.peekArray(2); + if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); + in.toss(2); + continue :len .head; + }, + else => |remaining_chunk_len| { + const n = try in.read(bw, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); + chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); + return n; + }, + .done => return error.EndOfStream, + } + } + + fn chunkedReadVec(ctx: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { + const reader: *Reader = @alignCast(@ptrCast(ctx)); + const chunk_len_ptr = &reader.body_state.remaining_chunk_len; + const in = reader.in; + var already_requested_more = false; + var amt_read: usize = 0; + data: for (data) |d| { + len: switch (chunk_len_ptr.*) { + .head => { + var cp: ChunkParser = .init; + const available_buffer = in.bufferContents(); + const i = cp.feed(available_buffer); + if (cp.state == .invalid) return reader.failBody(error.HttpChunkInvalid); + if (i == available_buffer.len) { + if (already_requested_more) { + chunk_len_ptr.* = .head; + return amt_read; + } + already_requested_more = true; + try in.fill(max_chunk_header_len); + const next_i = cp.feed(in.bufferContents()[i..]); + if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); + const header_len = i + next_i; + if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(header_len); + } else { + if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(i); + } + if (cp.chunk_len == 0) return parseTrailers(reader, amt_read); + continue :len .init(cp.chunk_len + 2); + }, + .n => { + if (in.bufferContents().len < 1) already_requested_more = true; + if ((try in.takeByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); + continue :len .head; + }, + .rn => { + if (in.bufferContents().len < 2) already_requested_more = true; + const rn = try in.takeArray(2); + if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); + continue :len .head; + }, + else => |remaining_chunk_len| { + const available_buffer = in.bufferContents(); + const copy_len = @min(available_buffer.len, d.len, remaining_chunk_len.int() - 2); + @memcpy(d[0..copy_len], available_buffer[0..copy_len]); + amt_read += copy_len; + in.toss(copy_len); + const next_chunk_len: RemainingChunkLen = .init(remaining_chunk_len.int() - copy_len); + if (copy_len == d.len) { + chunk_len_ptr.* = next_chunk_len; + continue :data; + } + if (already_requested_more) { + chunk_len_ptr.* = next_chunk_len; + return amt_read; + } + already_requested_more = true; + try in.fill(3); + continue :len next_chunk_len; + }, + .done => return error.EndOfStream, + } + } + return amt_read; + } + + fn chunkedDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { + const reader: *Reader = @alignCast(@ptrCast(ctx)); + const chunk_len_ptr = &reader.body_state.remaining_chunk_len; + const in = reader.in; + len: switch (chunk_len_ptr.*) { + .head => { + var cp: ChunkParser = .init; + const i = cp.feed(in.bufferContents()); + switch (cp.state) { + .invalid => return reader.failBody(error.HttpChunkInvalid), + .data => { + if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(i); + }, + else => { + try in.fill(max_chunk_header_len); + const next_i = cp.feed(in.bufferContents()[i..]); + if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); + const header_len = i + next_i; + if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); + in.toss(header_len); + }, + } + if (cp.chunk_len == 0) return parseTrailers(reader, 0); + const n = try in.discard(limit.min(.limited(cp.chunk_len))); + chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); + return n; + }, + .n => { + if ((try in.peekByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); + in.toss(1); + continue :len .head; + }, + .rn => { + const rn = try in.peekArray(2); + if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); + in.toss(2); + continue :len .head; + }, + else => |remaining_chunk_len| { + const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2))); + chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); + return n; + }, + .done => return error.EndOfStream, + } + } + + /// Called when next bytes in the stream are trailers, or "\r\n" to indicate + /// end of chunked body. + fn parseTrailers(reader: *Reader, amt_read: usize) std.io.Reader.Error!usize { + const in = reader.in; + var hp: HeadParser = .{}; + var trailers_len: usize = 0; + while (true) { + if (trailers_len >= in.buffer.len) return reader.failBody(error.HttpHeadersOversize); + try in.fill(trailers_len + 1); + trailers_len += hp.feed(in.bufferContents()[trailers_len..]); + if (hp.state == .finished) { + reader.body_state.remaining_chunk_len = .done; + reader.state = .ready; + reader.trailers_len = trailers_len; + return amt_read; + } + } + } + + fn failBody(r: *Reader, err: BodyError) error{ReadFailed} { + r.body_err = err; + return error.ReadFailed; + } +}; + +/// Request or response body. +pub const BodyWriter = struct { + /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the + /// state of this other than via methods of `BodyWriter`. + http_protocol_output: *std.io.BufferedWriter, + state: State, + elide: bool, + err: Error!void = {}, + + pub const Error = error{ + /// Attempted to write a file to the stream, an expensive operation + /// that should be avoided when `elide` is true. + UnableToElideBody, + }; + pub const WriteError = std.io.Writer.Error; + + /// How many zeroes to reserve for hex-encoded chunk length. + const chunk_len_digits = 8; + const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; + const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; + + comptime { + assert(max_chunk_len == std.math.maxInt(u32)); + } + + pub const State = union(enum) { + /// End of connection signals the end of the stream. + none, + /// As a debugging utility, counts down to zero as bytes are written. + content_length: u64, + /// Each chunk is wrapped in a header and trailer. + chunked: Chunked, + /// Cleanly finished stream; connection can be reused. + end, + + pub const Chunked = union(enum) { + /// Index of the hex-encoded chunk length in the chunk header + /// within the buffer of `BodyWriter.http_protocol_output`. + offset: usize, + /// We are in the middle of a chunk and this is how many bytes are + /// left until the next header. This includes +2 for "\r"\n", and + /// is zero for the beginning of the stream. + chunk_len: usize, + + pub const init: Chunked = .{ .chunk_len = 0 }; + }; + }; + + /// Sends all buffered data across `BodyWriter.http_protocol_output`. + /// + /// Some buffered data will remain if transfer-encoding is chunked and the + /// BodyWriter is mid-chunk. + pub fn flush(w: *BodyWriter) WriteError!void { + switch (w.state) { + .none, .content_length => return w.http_protocol_output.flush(), + .chunked => |*chunked| switch (chunked.*) { + .offset => |*offset| { + try w.http_protocol_output.flushLimit(.limited(w.http_protocol_output.end - offset.*)); + offset.* = 0; + }, + .chunk_len => return w.http_protocol_output.flush(), + }, + } + } + + /// When using content-length, asserts that the amount of data sent matches + /// the value sent in the header, then flushes. + /// + /// When using transfer-encoding: chunked, writes the end-of-stream message + /// with empty trailers, then flushes the stream to the system. Asserts any + /// started chunk has been completely finished. + /// + /// Respects the value of `elide` to omit all data after the headers. + /// + /// See also: + /// * `endUnflushed` + /// * `endChunked` + pub fn end(w: *BodyWriter) WriteError!void { + try endUnflushed(w); + try w.http_protocol_output.flush(); + } + + /// When using content-length, asserts that the amount of data sent matches + /// the value sent in the header. + /// + /// Otherwise, transfer-encoding: chunked is being used, and it writes the + /// end-of-stream message with empty trailers. + /// + /// Respects the value of `elide` to omit all data after the headers. + /// + /// See also: + /// * `end` + /// * `endChunked` + pub fn endUnflushed(w: *BodyWriter) WriteError!void { + switch (w.state) { + .content_length => |len| { + assert(len == 0); // Trips when end() called before all bytes written. + w.state = .end; + }, + .none => {}, + .chunked => return endChunked(w, .{}), + } + } + + pub const EndChunkedOptions = struct { + trailers: []const Header = &.{}, + }; + + /// Writes the end-of-stream message and any optional trailers. + /// + /// Does not flush. + /// + /// Asserts that the BodyWriter is using transfer-encoding: chunked. + /// + /// Respects the value of `elide` to omit all data after the headers. + /// + /// See also: + /// * `end` + /// * `endUnflushed` + pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) WriteError!void { + const chunked = &w.state.chunked; + if (w.elide) { + w.state = .end; + return; + } + const bw = w.http_protocol_output; + switch (chunked.*) { + .offset => |offset| { + const chunk_len = bw.end - offset - chunk_header_template.len; + writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); + try bw.writeAll("\r\n"); + }, + .chunk_len => |chunk_len| switch (chunk_len) { + 0 => {}, + 1 => try bw.writeByte('\n'), + 2 => try bw.writeAll("\r\n"), + else => unreachable, // An earlier write call indicated more data would follow. + }, + } + if (options.trailers.len > 0) { + try bw.writeAll("0\r\n"); + for (options.trailers) |trailer| { + try bw.writeAll(trailer.name); + try bw.writeAll(": "); + try bw.writeAll(trailer.value); + try bw.writeAll("\r\n"); + } + try bw.writeAll("\r\n"); + } + w.state = .end; + } + + fn contentLengthWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { + const w: *BodyWriter = @alignCast(@ptrCast(context)); + const n = if (w.elide) countSplat(data, splat) else try w.http_protocol_output.writeSplat(data, splat); + w.state.content_length -= n; + return n; + } + + fn noneWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { + const w: *BodyWriter = @alignCast(@ptrCast(context)); + if (w.elide) return countSplat(data, splat); + return w.http_protocol_output.writeSplat(data, splat); + } + + fn countSplat(data: []const []const u8, splat: usize) usize { + if (data.len == 0) return 0; + var total: usize = 0; + for (data[0 .. data.len - 1]) |buf| total += buf.len; + total += data[data.len - 1].len * splat; + return total; + } + + fn elideWriteFile( + w: *BodyWriter, + offset: std.io.Writer.Offset, + limit: std.io.Writer.Limit, + headers_and_trailers: []const []const u8, + ) WriteError!usize { + if (offset != .none) { + if (countWriteFile(limit, headers_and_trailers)) |n| { + return n; + } + } + w.err = error.UnableToElideBody; + return error.WriteFailed; + } + + /// Returns `null` if size cannot be computed without making any syscalls. + fn countWriteFile(limit: std.io.Writer.Limit, headers_and_trailers: []const []const u8) ?usize { + var total: usize = limit.toInt() orelse return null; + for (headers_and_trailers) |buf| total += buf.len; + return total; + } + + fn noneWriteFile( + context: ?*anyopaque, + file: std.fs.File, + offset: std.io.Writer.Offset, + limit: std.io.Writer.Limit, + headers_and_trailers: []const []const u8, + headers_len: usize, + ) std.io.Writer.FileError!usize { + if (limit == .nothing) return noneWriteSplat(context, headers_and_trailers, 1); + const w: *BodyWriter = @alignCast(@ptrCast(context)); + if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); + return w.http_protocol_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); + } + + fn contentLengthWriteFile( + context: ?*anyopaque, + file: std.fs.File, + offset: std.io.Writer.Offset, + limit: std.io.Writer.Limit, + headers_and_trailers: []const []const u8, + headers_len: usize, + ) std.io.Writer.FileError!usize { + if (limit == .nothing) return contentLengthWriteSplat(context, headers_and_trailers, 1); + const w: *BodyWriter = @alignCast(@ptrCast(context)); + if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); + const n = try w.http_protocol_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); + w.state.content_length -= n; + return n; + } + + fn chunkedWriteFile( + context: ?*anyopaque, + file: std.fs.File, + offset: std.io.Writer.Offset, + limit: std.io.Writer.Limit, + headers_and_trailers: []const []const u8, + headers_len: usize, + ) std.io.Writer.FileError!usize { + if (limit == .nothing) return chunkedWriteSplat(context, headers_and_trailers, 1); + const w: *BodyWriter = @alignCast(@ptrCast(context)); + if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); + const data_len = countWriteFile(limit, headers_and_trailers) orelse @panic("TODO"); + const bw = w.http_protocol_output; + const chunked = &w.state.chunked; + state: switch (chunked.*) { + .offset => |off| { + // TODO: is it better perf to read small files into the buffer? + const buffered_len = bw.end - off - chunk_header_template.len; + const chunk_len = data_len + buffered_len; + writeHex(bw.buffer[off..][0..chunk_len_digits], chunk_len); + const n = try bw.writeFile(file, offset, limit, headers_and_trailers, headers_len); + chunked.* = .{ .chunk_len = data_len + 2 - n }; + return n; + }, + .chunk_len => |chunk_len| l: switch (chunk_len) { + 0 => { + const header_buf = try bw.writableArray(chunk_header_template.len); + const off = bw.end; + @memcpy(header_buf, chunk_header_template); + chunked.* = .{ .offset = off }; + continue :state .{ .offset = off }; + }, + 1 => { + try bw.writeByte('\n'); + chunked.chunk_len = 0; + continue :l 0; + }, + 2 => { + try bw.writeByte('\r'); + chunked.chunk_len = 1; + continue :l 1; + }, + else => { + const new_limit = limit.min(.limited(chunk_len - 2)); + const n = try bw.writeFile(file, offset, new_limit, headers_and_trailers, headers_len); + chunked.chunk_len = chunk_len - n; + return n; + }, + }, + } + } + + fn chunkedWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { + const w: *BodyWriter = @alignCast(@ptrCast(context)); + const data_len = countSplat(data, splat); + if (w.elide) return data_len; + + const bw = w.http_protocol_output; + const chunked = &w.state.chunked; + + state: switch (chunked.*) { + .offset => |offset| { + if (bw.unusedCapacitySlice().len >= data_len) { + assert(data_len == (bw.writeSplat(data, splat) catch unreachable)); + return data_len; + } + const buffered_len = bw.end - offset - chunk_header_template.len; + const chunk_len = data_len + buffered_len; + writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); + const n = try bw.writeSplat(data, splat); + chunked.* = .{ .chunk_len = data_len + 2 - n }; + return n; + }, + .chunk_len => |chunk_len| l: switch (chunk_len) { + 0 => { + const header_buf = try bw.writableArray(chunk_header_template.len); + const offset = bw.end; + @memcpy(header_buf, chunk_header_template); + chunked.* = .{ .offset = offset }; + continue :state .{ .offset = offset }; + }, + 1 => { + try bw.writeByte('\n'); + chunked.chunk_len = 0; + continue :l 0; + }, + 2 => { + try bw.writeByte('\r'); + chunked.chunk_len = 1; + continue :l 1; + }, + else => { + const n = try bw.writeSplatLimit(data, splat, .limited(chunk_len - 2)); + chunked.chunk_len = chunk_len - n; + return n; + }, + }, + } + } + + /// Writes an integer as base 16 to `buf`, right-aligned, assuming the + /// buffer has already been filled with zeroes. + fn writeHex(buf: []u8, x: usize) void { + assert(std.mem.allEqual(u8, buf, '0')); + const base = 16; + var index: usize = buf.len; + var a = x; + while (a > 0) { + const digit = a % base; + index -= 1; + buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); + a /= base; + } + } + + pub fn interface(w: *BodyWriter) std.io.Writer { + return .{ + .context = w, + .vtable = switch (w.state) { + .none => &.{ + .writeSplat = noneWriteSplat, + .writeFile = noneWriteFile, + }, + .content_length => &.{ + .writeSplat = contentLengthWriteSplat, + .writeFile = contentLengthWriteFile, + }, + .chunked => &.{ + .writeSplat = chunkedWriteSplat, + .writeFile = chunkedWriteFile, + }, + }, + }; + } +}; test { + _ = Server; + _ = Status; + _ = Method; + _ = ChunkParser; + _ = HeadParser; + _ = WebSocket; + if (builtin.os.tag != .wasi) { _ = Client; - _ = Method; - _ = Server; - _ = Status; - _ = HeadParser; - _ = ChunkParser; - _ = WebSocket; _ = @import("http/test.zig"); } } diff --git a/lib/std/http/ChunkParser.zig b/lib/std/http/ChunkParser.zig index adcdc74bc7be7b5a04d5b4bc5d67a90d695f9ff8..7c628ec327771efbf5d0109c2dfb7f8bd5cab08d 100644 --- a/lib/std/http/ChunkParser.zig +++ b/lib/std/http/ChunkParser.zig @@ -1,5 +1,8 @@ //! Parser for transfer-encoding: chunked. +const ChunkParser = @This(); +const std = @import("std"); + state: State, chunk_len: u64, @@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize { return bytes.len; } -const ChunkParser = @This(); -const std = @import("std"); - test feed { const testing = std.testing; diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 82409e57408d4b2976422af08420876e6393f0f8..fe9beb5da7d18fce7ac332ebaa671b52266408fc 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -15,7 +15,6 @@ const Allocator = mem.Allocator; const assert = std.debug.assert; const Client = @This(); -const proto = @import("protocol.zig"); pub const disable_tls = std.options.http_disable_tls; @@ -68,7 +67,7 @@ pub const ConnectionPool = struct { pub const Criteria = struct { host: []const u8, port: u16, - protocol: Connection.Protocol, + protocol: Protocol, }; /// Finds and acquires a connection from the connection pool matching the criteria. @@ -201,6 +200,32 @@ pub const ConnectionPool = struct { } }; +pub const Protocol = enum { + plain, + tls, + + fn port(protocol: Protocol) u16 { + return switch (protocol) { + .plain => 80, + .tls => 443, + }; + } + + pub fn fromScheme(scheme: []const u8) ?Protocol { + const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ + .{ "http", .plain }, + .{ "ws", .plain }, + .{ "https", .tls }, + .{ "wss", .tls }, + }); + return protocol_map.get(scheme); + } + + pub fn fromUri(uri: Uri) ?Protocol { + return fromScheme(uri.scheme); + } +}; + pub const Connection = struct { client: *Client, stream: net.Stream, @@ -215,8 +240,6 @@ pub const Connection = struct { closing: bool, protocol: Protocol, - pub const Protocol = enum { plain, tls }; - const Plain = struct { /// Data from `Connection.stream`. reader: std.io.BufferedReader, @@ -411,13 +434,6 @@ pub const Connection = struct { } }; -/// The mode of transport for requests. -pub const RequestTransfer = union(enum) { - content_length: u64, - chunked: void, - none: void, -}; - /// The decompressor for response messages. pub const Compression = union(enum) { pub const DeflateDecompressor = std.compress.zlib.Decompressor; @@ -432,281 +448,278 @@ pub const Compression = union(enum) { none: void, }; -/// A HTTP response originating from a server. pub const Response = struct { - version: http.Version, - status: http.Status, - reason: []const u8, - - /// Points into the user-provided `server_header_buffer`. - location: ?[]const u8 = null, - /// Points into the user-provided `server_header_buffer`. - content_type: ?[]const u8 = null, - /// Points into the user-provided `server_header_buffer`. - content_disposition: ?[]const u8 = null, - - keep_alive: bool, - - /// If present, the number of bytes in the response body. - content_length: ?u64 = null, - - /// If present, the transfer encoding of the response body, otherwise none. - transfer_encoding: http.TransferEncoding = .none, - - /// If present, the compression of the response body, otherwise identity (no compression). - transfer_compression: http.ContentEncoding = .identity, - - parser: proto.HeadersParser, - compression: Compression = .none, - - /// Whether the response body should be skipped. Any data read from the - /// response body will be discarded. - skip: bool = false, - - pub const ParseError = error{ - HttpHeadersInvalid, - HttpHeaderContinuationsUnsupported, - HttpTransferEncodingUnsupported, - HttpConnectionHeaderUnsupported, - InvalidContentLength, - CompressionUnsupported, - }; - - pub fn parse(res: *Response, bytes: []const u8) ParseError!void { - var it = mem.splitSequence(u8, bytes, "\r\n"); - - const first_line = it.next().?; - if (first_line.len < 12) { - return error.HttpHeadersInvalid; - } - - const version: http.Version = switch (int64(first_line[0..8])) { - int64("HTTP/1.0") => .@"HTTP/1.0", - int64("HTTP/1.1") => .@"HTTP/1.1", - else => return error.HttpHeadersInvalid, - }; - if (first_line[8] != ' ') return error.HttpHeadersInvalid; - const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); - const reason = mem.trimStart(u8, first_line[12..], " "); - - res.version = version; - res.status = status; - res.reason = reason; - res.keep_alive = switch (version) { - .@"HTTP/1.0" => false, - .@"HTTP/1.1" => true, + request: *Request, + /// Pointers in this struct are invalidated with the next call to + /// `receiveHead`. + head: Head, + + pub const Head = struct { + bytes: []const u8, + version: http.Version, + status: http.Status, + reason: []const u8, + location: ?[]const u8 = null, + content_type: ?[]const u8 = null, + content_disposition: ?[]const u8 = null, + + keep_alive: bool, + + /// If present, the number of bytes in the response body. + content_length: ?u64 = null, + + transfer_encoding: http.TransferEncoding = .none, + transfer_compression: http.ContentEncoding = .identity, + + compression: Compression = .none, + + pub const ParseError = error{ + HttpHeadersInvalid, + HttpHeaderContinuationsUnsupported, + HttpTransferEncodingUnsupported, + HttpConnectionHeaderUnsupported, + InvalidContentLength, + CompressionUnsupported, }; - while (it.next()) |line| { - if (line.len == 0) return; - switch (line[0]) { - ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, - else => {}, + pub fn parse(bytes: []const u8) ParseError!Head { + var res: Head = .{ + .bytes = bytes, + .status = undefined, + .reason = undefined, + .version = undefined, + .keep_alive = false, + }; + var it = mem.splitSequence(u8, bytes, "\r\n"); + + const first_line = it.next().?; + if (first_line.len < 12) { + return error.HttpHeadersInvalid; } - var line_it = mem.splitScalar(u8, line, ':'); - const header_name = line_it.next().?; - const header_value = mem.trim(u8, line_it.rest(), " \t"); - if (header_name.len == 0) return error.HttpHeadersInvalid; + const version: http.Version = switch (int64(first_line[0..8])) { + int64("HTTP/1.0") => .@"HTTP/1.0", + int64("HTTP/1.1") => .@"HTTP/1.1", + else => return error.HttpHeadersInvalid, + }; + if (first_line[8] != ' ') return error.HttpHeadersInvalid; + const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); + const reason = mem.trimLeft(u8, first_line[12..], " "); - if (std.ascii.eqlIgnoreCase(header_name, "connection")) { - res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); - } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { - res.content_type = header_value; - } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { - res.location = header_value; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { - res.content_disposition = header_value; - } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { - // Transfer-Encoding: second, first - // Transfer-Encoding: deflate, chunked - var iter = mem.splitBackwardsScalar(u8, header_value, ','); + res.version = version; + res.status = status; + res.reason = reason; + res.keep_alive = switch (version) { + .@"HTTP/1.0" => false, + .@"HTTP/1.1" => true, + }; - const first = iter.first(); - const trimmed_first = mem.trim(u8, first, " "); - - var next: ?[]const u8 = first; - if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { - if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding - res.transfer_encoding = transfer; - - next = iter.next(); + while (it.next()) |line| { + if (line.len == 0) return res; + switch (line[0]) { + ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, + else => {}, } - if (next) |second| { - const trimmed_second = mem.trim(u8, second, " "); + var line_it = mem.splitScalar(u8, line, ':'); + const header_name = line_it.next().?; + const header_value = mem.trim(u8, line_it.rest(), " \t"); + if (header_name.len == 0) return error.HttpHeadersInvalid; - if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { - if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported - res.transfer_compression = transfer; + if (std.ascii.eqlIgnoreCase(header_name, "connection")) { + res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); + } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { + res.content_type = header_value; + } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { + res.location = header_value; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { + res.content_disposition = header_value; + } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { + // Transfer-Encoding: second, first + // Transfer-Encoding: deflate, chunked + var iter = mem.splitBackwardsScalar(u8, header_value, ','); + + const first = iter.first(); + const trimmed_first = mem.trim(u8, first, " "); + + var next: ?[]const u8 = first; + if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { + if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding + res.transfer_encoding = transfer; + + next = iter.next(); + } + + if (next) |second| { + const trimmed_second = mem.trim(u8, second, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { + if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported + res.transfer_compression = transfer; + } else { + return error.HttpTransferEncodingUnsupported; + } + } + + if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { + const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; + + if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; + + res.content_length = content_length; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { + if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; + + const trimmed = mem.trim(u8, header_value, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + res.transfer_compression = ce; } else { return error.HttpTransferEncodingUnsupported; } } - - if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { - const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; - - if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; - - res.content_length = content_length; - } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { - if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; - - const trimmed = mem.trim(u8, header_value, " "); - - if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { - res.transfer_compression = ce; - } else { - return error.HttpTransferEncodingUnsupported; - } } + return error.HttpHeadersInvalid; // missing empty line } - return error.HttpHeadersInvalid; // missing empty line - } - - test parse { - const response_bytes = "HTTP/1.1 200 OK\r\n" ++ - "LOcation:url\r\n" ++ - "content-tYpe: text/plain\r\n" ++ - "content-disposition:attachment; filename=example.txt \r\n" ++ - "content-Length:10\r\n" ++ - "TRansfer-encoding:\tdeflate, chunked \r\n" ++ - "connectioN:\t keep-alive \r\n\r\n"; - - var header_buffer: [1024]u8 = undefined; - var res = Response{ - .status = undefined, - .reason = undefined, - .version = undefined, - .keep_alive = false, - .parser = .init(&header_buffer), - }; - - @memcpy(header_buffer[0..response_bytes.len], response_bytes); - res.parser.header_bytes_len = response_bytes.len; - - try res.parse(response_bytes); - - try testing.expectEqual(.@"HTTP/1.1", res.version); - try testing.expectEqualStrings("OK", res.reason); - try testing.expectEqual(.ok, res.status); - - try testing.expectEqualStrings("url", res.location.?); - try testing.expectEqualStrings("text/plain", res.content_type.?); - try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?); - - try testing.expectEqual(true, res.keep_alive); - try testing.expectEqual(10, res.content_length.?); - try testing.expectEqual(.chunked, res.transfer_encoding); - try testing.expectEqual(.deflate, res.transfer_compression); - } - - inline fn int64(array: *const [8]u8) u64 { - return @bitCast(array.*); - } - - fn parseInt3(text: *const [3]u8) u10 { - const nnn: @Vector(3, u8) = text.*; - const zero: @Vector(3, u8) = .{ '0', '0', '0' }; - const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; - return @reduce(.Add, (nnn -% zero) *% mmm); - } - - test parseInt3 { - const expectEqual = testing.expectEqual; - try expectEqual(@as(u10, 0), parseInt3("000")); - try expectEqual(@as(u10, 418), parseInt3("418")); - try expectEqual(@as(u10, 999), parseInt3("999")); - } - - pub fn iterateHeaders(r: Response) http.HeaderIterator { - return .init(r.parser.get()); - } - test iterateHeaders { - const response_bytes = "HTTP/1.1 200 OK\r\n" ++ - "LOcation:url\r\n" ++ - "content-tYpe: text/plain\r\n" ++ - "content-disposition:attachment; filename=example.txt \r\n" ++ - "content-Length:10\r\n" ++ - "TRansfer-encoding:\tdeflate, chunked \r\n" ++ - "connectioN:\t keep-alive \r\n\r\n"; - - var header_buffer: [1024]u8 = undefined; - var res = Response{ - .status = undefined, - .reason = undefined, - .version = undefined, - .keep_alive = false, - .parser = .init(&header_buffer), - }; - - @memcpy(header_buffer[0..response_bytes.len], response_bytes); - res.parser.header_bytes_len = response_bytes.len; - - var it = res.iterateHeaders(); - { - const header = it.next().?; - try testing.expectEqualStrings("LOcation", header.name); - try testing.expectEqualStrings("url", header.value); - try testing.expect(!it.is_trailer); + test parse { + const response_bytes = "HTTP/1.1 200 OK\r\n" ++ + "LOcation:url\r\n" ++ + "content-tYpe: text/plain\r\n" ++ + "content-disposition:attachment; filename=example.txt \r\n" ++ + "content-Length:10\r\n" ++ + "TRansfer-encoding:\tdeflate, chunked \r\n" ++ + "connectioN:\t keep-alive \r\n\r\n"; + + const head = Head.parse(response_bytes); + + try testing.expectEqual(.@"HTTP/1.1", head.version); + try testing.expectEqualStrings("OK", head.reason); + try testing.expectEqual(.ok, head.status); + + try testing.expectEqualStrings("url", head.location.?); + try testing.expectEqualStrings("text/plain", head.content_type.?); + try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); + + try testing.expectEqual(true, head.keep_alive); + try testing.expectEqual(10, head.content_length.?); + try testing.expectEqual(.chunked, head.transfer_encoding); + try testing.expectEqual(.deflate, head.transfer_compression); } - { - const header = it.next().?; - try testing.expectEqualStrings("content-tYpe", header.name); - try testing.expectEqualStrings("text/plain", header.value); - try testing.expect(!it.is_trailer); + + pub fn iterateHeaders(h: Head) http.HeaderIterator { + return .init(h.bytes); } - { - const header = it.next().?; - try testing.expectEqualStrings("content-disposition", header.name); - try testing.expectEqualStrings("attachment; filename=example.txt", header.value); - try testing.expect(!it.is_trailer); + + test iterateHeaders { + const response_bytes = "HTTP/1.1 200 OK\r\n" ++ + "LOcation:url\r\n" ++ + "content-tYpe: text/plain\r\n" ++ + "content-disposition:attachment; filename=example.txt \r\n" ++ + "content-Length:10\r\n" ++ + "TRansfer-encoding:\tdeflate, chunked \r\n" ++ + "connectioN:\t keep-alive \r\n\r\n"; + + var header_buffer: [1024]u8 = undefined; + var res = Response{ + .status = undefined, + .reason = undefined, + .version = undefined, + .keep_alive = false, + .parser = .init(&header_buffer), + }; + + @memcpy(header_buffer[0..response_bytes.len], response_bytes); + res.parser.header_bytes_len = response_bytes.len; + + var it = res.iterateHeaders(); + { + const header = it.next().?; + try testing.expectEqualStrings("LOcation", header.name); + try testing.expectEqualStrings("url", header.value); + try testing.expect(!it.is_trailer); + } + { + const header = it.next().?; + try testing.expectEqualStrings("content-tYpe", header.name); + try testing.expectEqualStrings("text/plain", header.value); + try testing.expect(!it.is_trailer); + } + { + const header = it.next().?; + try testing.expectEqualStrings("content-disposition", header.name); + try testing.expectEqualStrings("attachment; filename=example.txt", header.value); + try testing.expect(!it.is_trailer); + } + { + const header = it.next().?; + try testing.expectEqualStrings("content-Length", header.name); + try testing.expectEqualStrings("10", header.value); + try testing.expect(!it.is_trailer); + } + { + const header = it.next().?; + try testing.expectEqualStrings("TRansfer-encoding", header.name); + try testing.expectEqualStrings("deflate, chunked", header.value); + try testing.expect(!it.is_trailer); + } + { + const header = it.next().?; + try testing.expectEqualStrings("connectioN", header.name); + try testing.expectEqualStrings("keep-alive", header.value); + try testing.expect(!it.is_trailer); + } + try testing.expectEqual(null, it.next()); } - { - const header = it.next().?; - try testing.expectEqualStrings("content-Length", header.name); - try testing.expectEqualStrings("10", header.value); - try testing.expect(!it.is_trailer); + + inline fn int64(array: *const [8]u8) u64 { + return @bitCast(array.*); } - { - const header = it.next().?; - try testing.expectEqualStrings("TRansfer-encoding", header.name); - try testing.expectEqualStrings("deflate, chunked", header.value); - try testing.expect(!it.is_trailer); + + fn parseInt3(text: *const [3]u8) u10 { + const nnn: @Vector(3, u8) = text.*; + const zero: @Vector(3, u8) = .{ '0', '0', '0' }; + const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; + return @reduce(.Add, (nnn -% zero) *% mmm); } - { - const header = it.next().?; - try testing.expectEqualStrings("connectioN", header.name); - try testing.expectEqualStrings("keep-alive", header.value); - try testing.expect(!it.is_trailer); + + test parseInt3 { + const expectEqual = testing.expectEqual; + try expectEqual(@as(u10, 0), parseInt3("000")); + try expectEqual(@as(u10, 418), parseInt3("418")); + try expectEqual(@as(u10, 999), parseInt3("999")); } - try testing.expectEqual(null, it.next()); + }; + + /// Asserts that this function is only called once. + pub fn reader(response: *Response) std.io.Reader { + const head = &response.head; + return response.request.reader.interface(head.transfer_encoding, head.content_length); } }; pub const Request = struct { + /// This field is provided so that clients can observe redirected URIs. + /// + /// Its backing memory is externally provided by API users when creating a + /// request, and then again provided externally via `redirect_buffer` to + /// `receiveHead`. uri: Uri, client: *Client, /// This is null when the connection is released. connection: ?*Connection, + reader: http.Reader, keep_alive: bool, method: http.Method, version: http.Version = .@"HTTP/1.1", - transfer_encoding: RequestTransfer, + transfer_encoding: TransferEncoding, redirect_behavior: RedirectBehavior, /// Whether the request should handle a 100-continue response before sending the request body. handle_continue: bool, - /// The response associated with this request. - /// - /// This field is undefined until `wait` is called. - response: Response, - /// Standard headers that have default, but overridable, behavior. headers: Headers, @@ -720,6 +733,12 @@ pub const Request = struct { /// Externally-owned; must outlive the Request. privileged_headers: []const http.Header, + pub const TransferEncoding = union(enum) { + content_length: u64, + chunked: void, + none: void, + }; + pub const Headers = struct { host: Value = .default, authorization: Value = .default, @@ -771,76 +790,48 @@ pub const Request = struct { req.* = undefined; } - // This function must deallocate all resources associated with the request, - // or keep those which will be used. - // This needs to be kept in sync with deinit and request. - fn redirect(req: *Request, uri: Uri) !void { - assert(req.response.parser.done); + /// Sends and flushes a complete request as only HTTP head, no body. + pub fn sendBodiless(r: *Request) std.io.Writer.Error!void { + try sendBodilessUnflushed(r); + try r.connection.?.writer.flush(); + } - req.client.connection_pool.release(req.client.allocator, req.connection.?); - req.connection = null; + /// Sends but does not flush a complete request as only HTTP head, no body. + pub fn sendBodilessUnflushed(r: *Request) std.io.Writer.Error!void { + assert(r.transfer_encoding == .none); + assert(!r.method.requestHasBody()); + try sendHead(r); + } - var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer); - defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..]; - const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); - - const new_host = valid_uri.host.?.raw; - const prev_host = req.uri.host.?.raw; - const keep_privileged_headers = - std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and - std.ascii.endsWithIgnoreCase(new_host, prev_host) and - (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.'); - if (!keep_privileged_headers) { - // When redirecting to a different domain, strip privileged headers. - req.privileged_headers = &.{}; - } - - if (switch (req.response.status) { - .see_other => true, - .moved_permanently, .found => req.method == .POST, - else => false, - }) { - // A redirect to a GET must change the method and remove the body. - req.method = .GET; - req.transfer_encoding = .none; - req.headers.content_type = .omit; - } - - if (req.transfer_encoding != .none) { - // The request body has already been sent. The request is - // still in a valid state, but the redirect must be handled - // manually. - return error.RedirectRequiresResend; - } - - req.uri = valid_uri; - req.connection = try req.client.connect(new_host, uriPort(valid_uri, protocol), protocol); - req.redirect_behavior.subtractOne(); - req.response.parser.reset(); - - req.response = .{ - .version = undefined, - .status = undefined, - .reason = undefined, - .keep_alive = undefined, - .parser = req.response.parser, + /// Transfers the HTTP head over the connection, which is not flushed until + /// `BodyWriter.flush` or `BodyWriter.end` is called. + pub fn sendBody(r: *Request) std.io.Writer.Error!http.BodyWriter { + assert(r.method.requestHasBody()); + try sendHead(r); + return .{ + .http_protocol_output = &r.connection.?.writer, + .transfer_encoding = if (r.transfer_encoding) |te| switch (te) { + .chunked => .{ .chunked = .init }, + .content_length => |len| .{ .content_length = len }, + .none => .none, + } else .{ .chunked = .init }, + .elide_body = false, }; } - /// Send the HTTP request headers to the server. - pub fn send(req: *Request) std.io.Writer.Error!void { - assert(req.transfer_encoding == .none or req.method.requestHasBody()); - - const connection = req.connection.?; + /// Sends HTTP headers without flushing. + fn sendHead(r: *Request) std.io.Writer.Error!void { + const uri = r.uri; + const connection = r.connection.?; const w = &connection.writer; - try req.method.write(w); + try r.method.write(w); try w.writeByte(' '); - if (req.method == .CONNECT) { - try req.uri.writeToStream(.{ .authority = true }, w); + if (r.method == .CONNECT) { + try uri.writeToStream(.{ .authority = true }, w); } else { - try req.uri.writeToStream(.{ + try uri.writeToStream(.{ .scheme = connection.proxied, .authentication = connection.proxied, .authority = connection.proxied, @@ -849,55 +840,55 @@ pub const Request = struct { }, w); } try w.writeByte(' '); - try w.writeAll(@tagName(req.version)); + try w.writeAll(@tagName(r.version)); try w.writeAll("\r\n"); - if (try emitOverridableHeader("host: ", req.headers.host, w)) { + if (try emitOverridableHeader("host: ", r.headers.host, w)) { try w.writeAll("host: "); - try req.uri.writeToStream(.{ .authority = true }, w); + try uri.writeToStream(.{ .authority = true }, w); try w.writeAll("\r\n"); } - if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) { - if (req.uri.user != null or req.uri.password != null) { + if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { + if (uri.user != null or uri.password != null) { try w.writeAll("authorization: "); - try basic_authorization.write(req.uri, w); + try basic_authorization.write(uri, w); try w.writeAll("\r\n"); } } - if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) { + if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { try w.writeAll("user-agent: zig/"); try w.writeAll(builtin.zig_version_string); try w.writeAll(" (std.http)\r\n"); } - if (try emitOverridableHeader("connection: ", req.headers.connection, w)) { - if (req.keep_alive) { + if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { + if (r.keep_alive) { try w.writeAll("connection: keep-alive\r\n"); } else { try w.writeAll("connection: close\r\n"); } } - if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) { + if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { // https://github.com/ziglang/zig/issues/18937 //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n"); try w.writeAll("accept-encoding: gzip, deflate\r\n"); } - switch (req.transfer_encoding) { + switch (r.transfer_encoding) { .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), .none => {}, } - if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) { + if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { // The default is to omit content-type if not provided because // "application/octet-stream" is redundant. } - for (req.extra_headers) |header| { + for (r.extra_headers) |header| { assert(header.name.len != 0); try w.writeAll(header.name); @@ -908,8 +899,8 @@ pub const Request = struct { if (connection.proxied) proxy: { const proxy = switch (connection.protocol) { - .plain => req.client.http_proxy, - .tls => req.client.https_proxy, + .plain => r.client.http_proxy, + .tls => r.client.https_proxy, } orelse break :proxy; const authorization = proxy.authorization orelse break :proxy; @@ -919,338 +910,197 @@ pub const Request = struct { } try w.writeAll("\r\n"); - - try connection.writer.flush(); - } - - /// Returns true if the default behavior is required, otherwise handles - /// writing (or not writing) the header. - fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool { - switch (v) { - .default => return true, - .omit => return false, - .override => |x| { - try w.writeAll(prefix); - try w.writeAll(x); - try w.writeAll("\r\n"); - return false; - }, - } - } - - const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; - - const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); - - fn transferReader(req: *Request) TransferReader { - return .{ .context = req }; - } - - fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { - if (req.response.parser.done) return 0; - - var index: usize = 0; - while (index == 0) { - const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); - if (amt == 0 and req.response.parser.done) break; - index += amt; - } - - return index; } - /// TODO collapse each error set into its own meta error code, and store - /// the underlying error code as a field on Request - pub const WaitError = RequestError || std.io.Writer.Error || TransferReadError || - proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || - error{ - TooManyHttpRedirects, - RedirectRequiresResend, - HttpRedirectLocationMissing, - HttpRedirectLocationInvalid, - CompressionInitializationFailed, - CompressionUnsupported, - }; + pub const ReceiveHeadError = http.Reader.HeadError || error{ + /// Server sent headers that did not conform to the HTTP protocol. + /// + /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be + /// passed directly to `Request.Head.parse`. + HttpHeadersInvalid, + TooManyHttpRedirects, + /// This can be avoided by calling `receiveHead` before sending the + /// request body. + RedirectRequiresResend, + HttpRedirectLocationMissing, + HttpRedirectLocationOversize, + HttpRedirectLocationInvalid, + CompressionInitializationFailed, + CompressionUnsupported, + }; - /// Waits for a response from the server and parses any headers that are sent. - /// This function will block until the final response is received. - /// /// If handling redirects and the request has no payload, then this - /// function will automatically follow redirects. If a request payload is - /// present, then this function will error with - /// error.RedirectRequiresResend. + /// function will automatically follow redirects. /// - /// Must be called after `send` and, if any data was written to the request - /// body, then also after `finish`. - pub fn wait(req: *Request) WaitError!void { + /// If a request payload is present, then this function will error with + /// `error.RedirectRequiresResend`. + /// + /// This function takes an auxiliary buffer to store the arbitrarily large + /// URI which may need to be merged with the previous URI, and that data + /// needs to survive across different connections, which is where the input + /// buffer lives. + /// + /// `redirect_buffer` must outlive accesses to `Request.uri`. If this + /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` + /// is returned instead. This buffer may be empty if no redirects are to be + /// handled. + pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { + var aux_buf = redirect_buffer; while (true) { + try r.reader.receiveHead(); + const response: Response = .{ + .request = r, + .head = Response.Head.parse(r.reader.head_buffer) catch return error.HttpHeadersInvalid, + }; + const head = &response.head; + + if (head.status == .@"continue") { + if (r.handle_continue) continue; + return; // we're not handling the 100-continue + } + // This while loop is for handling redirects, which means the request's // connection may be different than the previous iteration. However, it // is still guaranteed to be non-null with each iteration of this loop. - const connection = req.connection.?; + const connection = r.connection.?; - while (true) { // read headers - try connection.fill(); - - const nchecked = try req.response.parser.checkCompleteHead(connection.peek()); - connection.drop(@intCast(nchecked)); - - if (req.response.parser.state.isContent()) break; - } - - try req.response.parse(req.response.parser.get()); - - if (req.response.status == .@"continue") { - // We're done parsing the continue response; reset to prepare - // for the real response. - req.response.parser.done = true; - req.response.parser.reset(); - - if (req.handle_continue) - continue; - - return; // we're not handling the 100-continue - } - - // we're switching protocols, so this connection is no longer doing http - if (req.method == .CONNECT and req.response.status.class() == .success) { + if (r.method == .CONNECT and head.status.class() == .success) { + // This connection is no longer doing HTTP. connection.closing = false; - req.response.parser.done = true; - return; // the connection is not HTTP past this point + return response; } - connection.closing = !req.response.keep_alive or !req.keep_alive; + connection.closing = !head.keep_alive or !r.keep_alive; // Any response to a HEAD request and any response with a 1xx // (Informational), 204 (No Content), or 304 (Not Modified) status // code is always terminated by the first empty line after the // header fields, regardless of the header fields present in the // message. - if (req.method == .HEAD or req.response.status.class() == .informational or - req.response.status == .no_content or req.response.status == .not_modified) + if (r.method == .HEAD or head.status.class() == .informational or + head.status == .no_content or head.status == .not_modified) { - req.response.parser.done = true; - return; // The response is empty; no further setup or redirection is necessary. + return response; } - switch (req.response.transfer_encoding) { - .none => { - if (req.response.content_length) |cl| { - req.response.parser.next_chunk_length = cl; + if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { + if (r.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; + const location = head.location orelse return error.HttpRedirectLocationMissing; + try r.redirect(location, &aux_buf); + try r.send(); + continue; + } - if (cl == 0) req.response.parser.done = true; - } else { - // read until the connection is closed - req.response.parser.next_chunk_length = std.math.maxInt(u64); - } + switch (head.transfer_compression) { + .identity => response.compression = .none, + .compress, .@"x-compress" => return error.CompressionUnsupported, + .deflate => response.compression = .{ + .deflate = std.compress.zlib.decompressor(r.transferReader()), }, - .chunked => { - req.response.parser.next_chunk_length = 0; - req.response.parser.state = .chunk_head_size; + .gzip, .@"x-gzip" => response.compression = .{ + .gzip = std.compress.gzip.decompressor(r.transferReader()), }, + // https://github.com/ziglang/zig/issues/18937 + //.zstd => response.compression = .{ + // .zstd = std.compress.zstd.decompressStream(r.client.allocator, r.transferReader()), + //}, + .zstd => return error.CompressionUnsupported, } - - if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) { - // skip the body of the redirect response, this will at least - // leave the connection in a known good state. - req.response.skip = true; - assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary - - if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; - - const location = req.response.location orelse - return error.HttpRedirectLocationMissing; - - // This mutates the beginning of header_bytes_buffer and uses that - // for the backing memory of the returned Uri. - try req.redirect(req.uri.resolve_inplace( - location, - &req.response.parser.header_bytes_buffer, - ) catch |err| switch (err) { - error.UnexpectedCharacter, - error.InvalidFormat, - error.InvalidPort, - => return error.HttpRedirectLocationInvalid, - error.NoSpaceLeft => return error.HttpHeadersOversize, - }); - try req.send(); - } else { - req.response.skip = false; - if (!req.response.parser.done) { - switch (req.response.transfer_compression) { - .identity => req.response.compression = .none, - .compress, .@"x-compress" => return error.CompressionUnsupported, - .deflate => req.response.compression = .{ - .deflate = std.compress.zlib.decompressor(req.transferReader()), - }, - .gzip, .@"x-gzip" => req.response.compression = .{ - .gzip = std.compress.gzip.decompressor(req.transferReader()), - }, - // https://github.com/ziglang/zig/issues/18937 - //.zstd => req.response.compression = .{ - // .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), - //}, - .zstd => return error.CompressionUnsupported, - } - } - - break; - } + return response; } } - pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || - error{ DecompressionFailure, InvalidTrailers }; - - pub const Reader = std.io.Reader(*Request, ReadError, read); - - pub fn reader(req: *Request) Reader { - return .{ .context = req }; - } - - /// Reads data from the response body. Must be called after `wait`. - pub fn read(req: *Request, buffer: []u8) ReadError!usize { - const out_index = switch (req.response.compression) { - .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, - .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, - // https://github.com/ziglang/zig/issues/18937 - //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure, - else => try req.transferRead(buffer), + pub const RedirectError = error{ + HttpRedirectLocationOversize, + HttpRedirectLocationInvalid, + }; + + /// This function takes an auxiliary buffer to store the arbitrarily large + /// URI which may need to be merged with the previous URI, and that data + /// needs to survive across different connections, which is where the input + /// buffer lives. + /// + /// `aux_buf` must outlive accesses to `Request.uri`. + fn redirect(r: *Request, new_location: []const u8, aux_buf: *[]u8) RedirectError!void { + if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; + const location = aux_buf.*[0..new_location.len]; + @memcpy(location, new_location); + { + // Skip the body of the redirect response to leave the connection in + // the correct state. This causes `new_location` to be invalidated. + var reader = r.reader.interface(); + _ = reader.discardRemaining() catch |err| switch (err) { + error.ReadFailed => return r.reader.err.?, + }; + } + const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { + error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, + error.InvalidFormat => return error.HttpRedirectLocationInvalid, + error.InvalidPort => return error.HttpRedirectLocationInvalid, + error.NoSpaceLeft => return error.HttpRedirectLocationOversize, }; - if (out_index > 0) return out_index; + const resolved_len = location.len + (aux_buf.*.ptr - location.ptr); - while (!req.response.parser.state.isContent()) { // read trailing headers - try req.connection.?.fill(); + const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; + const old_connection = r.connection.?; + const old_host = old_connection.host(); + var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; + const new_host = try new_uri.getHost(&new_host_name_buffer); + const keep_privileged_headers = + std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and + sameParentDomain(old_host, new_host); - const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek()); - req.connection.?.drop(@intCast(nchecked)); + r.client.connection_pool.release(r.client.allocator, old_connection); + r.connection = null; + + if (!keep_privileged_headers) { + // When redirecting to a different domain, strip privileged headers. + r.privileged_headers = &.{}; } - return 0; - } + if (switch (r.response.status) { + .see_other => true, + .moved_permanently, .found => r.method == .POST, + else => false, + }) { + // A redirect to a GET must change the method and remove the body. + r.method = .GET; + r.transfer_encoding = .none; + r.headers.content_type = .omit; + } - /// Reads data from the response body. Must be called after `wait`. - pub fn readAll(req: *Request, buffer: []u8) !usize { - var index: usize = 0; - while (index < buffer.len) { - const amt = try read(req, buffer[index..]); - if (amt == 0) break; - index += amt; + if (r.transfer_encoding != .none) { + // The request body has already been sent. The request is + // still in a valid state, but the redirect must be handled + // manually. + return error.RedirectRequiresResend; } - return index; + + const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); + r.uri = new_uri; + r.stolen_bytes_len = resolved_len; + r.connection = new_connection; + r.redirect_behavior.subtractOne(); } - /// Resulting `std.io.Writer` must used after `send` and before `finish`. - pub fn writer(req: *Request) std.io.Writer { - return .{ - .context = req, - .vtable = switch (req.transfer_encoding) { - .chunked => &.{ - .writeSplat = chunked_writeSplat, - .writeFile = chunked_writeFile, - }, - .content_length => &.{ - .writeSplat = cl_writeSplat, - .writeFile = cl_writeFile, - }, - .none => unreachable, + /// Returns true if the default behavior is required, otherwise handles + /// writing (or not writing) the header. + fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *std.io.BufferedWriter) std.io.Writer.Error!bool { + switch (v) { + .default => return true, + .omit => return false, + .override => |x| { + try bw.writeAll(prefix); + try bw.writeAll(x); + try bw.writeAll("\r\n"); + return false; }, - }; - } - - fn chunked_writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { - const req: *Request = @ptrCast(@alignCast(context)); - var total: usize = 0; - for (data) |bytes| total += bytes.len; - if (total == 0) return 0; - var iovecs: [max_buffers_len][]const u8 = undefined; - var header_buffer: [30]u8 = undefined; - var header_buffer_writer: std.io.BufferedWriter = undefined; - header_buffer_writer.initFixed(&header_buffer); - header_buffer_writer.print("{x}\r\n", .{total}) catch unreachable; - iovecs[0] = header_buffer_writer.getWritten(); - @memcpy(iovecs[1..][0..data.len], data); - iovecs[data.len + 1] = "\r\n"; - // TODO: only 1 underlying write call - // TODO: don't rely on max_buffers_len exceeding the caller - // TODO: handle splat - _ = splat; - const w = &req.connection.?.writer; - try w.writevAll(iovecs[0 .. data.len + 2]); - return total; - } - - const max_buffers_len = 16; - - pub fn chunked_writeFile( - context: *anyopaque, - file: std.fs.File, - offset: u64, - len: std.io.Writer.FileLen, - headers_and_trailers: []const []const u8, - headers_len: usize, - ) std.io.Writer.Error!usize { - if (len == .entire_file) return error.Unimplemented; - const req: *Request = @ptrCast(@alignCast(context)); - var total: usize = len.int(); - for (headers_and_trailers) |bytes| total += bytes.len; - if (total == 0) return 0; - var iovecs: [max_buffers_len][]const u8 = undefined; - var header_buffer: [30]u8 = undefined; - var header_buffer_writer: std.io.BufferedWriter = undefined; - header_buffer_writer.initFixed(&header_buffer); - header_buffer_writer.print("{x}\r\n", .{total}) catch unreachable; - iovecs[0] = header_buffer_writer.getWritten(); - @memcpy(iovecs[1..][0..headers_and_trailers.len], headers_and_trailers); - iovecs[headers_and_trailers.len + 1] = "\r\n"; - // TODO: only 1 underlying write call - // TODO: don't rely on max_buffers_len exceeding the caller - const w = &req.connection.?.writer; - try w.writeFileAll(file, .{ - .offset = offset, - .len = len, - .headers_and_trailers = iovecs[0 .. headers_and_trailers.len + 2], - .headers_len = headers_len + 1, - }); - return total; - } - - fn cl_writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { - const req: *Request = @ptrCast(@alignCast(context)); - const n = try req.connection.?.writer.writeSplat(data, splat); - req.transfer_encoding.content_length -= n; - return n; - } - - pub fn cl_writeFile( - context: *anyopaque, - file: std.fs.File, - offset: u64, - len: std.io.Writer.FileLen, - headers_and_trailers: []const []const u8, - headers_len: usize, - ) std.io.Writer.Error!usize { - const req: *Request = @ptrCast(@alignCast(context)); - const n = try req.connection.?.writer.writeFile(file, offset, len, headers_and_trailers, headers_len); - req.transfer_encoding.content_length -= n; - return n; - } - - /// Finish the body of a request. This notifies the server that you have no more data to send. - /// Must be called after `send`. - pub fn finish(req: *Request) std.io.Writer.Error!void { - switch (req.transfer_encoding) { - .chunked => try req.connection.?.writer.writeAll("0\r\n\r\n"), - .content_length => |len| assert(len == 0), - .none => {}, } - - try req.connection.?.writer.flush(); } }; pub const Proxy = struct { - protocol: Connection.Protocol, + protocol: Protocol, host: []const u8, authorization: ?[]const u8, port: u16, @@ -1307,24 +1157,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !? } else return null; const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); - const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) { - error.UnsupportedUriScheme => return null, - error.UriMissingHost => return error.HttpProxyMissingHost, - error.OutOfMemory => |e| return e, - }; + const protocol = Protocol.fromUri(uri) orelse return null; + const raw_host = try uri.getHostAlloc(arena); - const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: { - const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri)); - assert(basic_authorization.value(valid_uri, authorization).len == authorization.len); + const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { + const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); + assert(basic_authorization.value(uri, authorization).len == authorization.len); break :a authorization; } else null; const proxy = try arena.create(Proxy); proxy.* = .{ .protocol = protocol, - .host = valid_uri.host.?.raw, + .host = raw_host, .authorization = authorization, - .port = uriPort(valid_uri, protocol), + .port = uriPort(uri, protocol), .supports_connect = true, }; return proxy; @@ -1385,7 +1232,7 @@ pub fn connectTcp( client: *Client, host: []const u8, port: u16, - protocol: Connection.Protocol, + protocol: Protocol, ) ConnectTcpError!*Connection { if (client.connection_pool.findConnection(.{ .host = host, @@ -1540,7 +1387,7 @@ pub fn connect( client: *Client, host: []const u8, port: u16, - protocol: Connection.Protocol, + protocol: Protocol, ) ConnectError!*Connection { const proxy = switch (protocol) { .plain => client.http_proxy, @@ -1604,11 +1451,6 @@ pub const RequestOptions = struct { /// payload or the server has acknowledged the payload). redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), - /// Externally-owned memory used to store the server's entire HTTP header. - /// `error.HttpHeadersOversize` is returned from read() when a - /// client sends too many bytes of HTTP headers. - server_header_buffer: []u8, - /// Must be an already acquired connection. connection: ?*Connection = null, @@ -1624,33 +1466,12 @@ pub const RequestOptions = struct { privileged_headers: []const http.Header = &.{}, }; -fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } { - const protocol_map = std.StaticStringMap(Connection.Protocol).initComptime(.{ - .{ "http", .plain }, - .{ "ws", .plain }, - .{ "https", .tls }, - .{ "wss", .tls }, - }); - const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme; - var valid_uri = uri; - // The host is always going to be needed as a raw string for hostname resolution anyway. - valid_uri.host = .{ - .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena), - }; - return .{ protocol, valid_uri }; -} - -fn uriPort(uri: Uri, protocol: Connection.Protocol) u16 { - return uri.port orelse switch (protocol) { - .plain => 80, - .tls => 443, - }; +fn uriPort(uri: Uri, protocol: Protocol) u16 { + return uri.port orelse protocol.port(); } /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. /// -/// `uri` must remain alive during the entire request. -/// /// The caller is responsible for calling `deinit()` on the `Request`. /// This function is threadsafe. /// @@ -1675,8 +1496,7 @@ pub fn open( } } - var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer); - const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); + const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; if (protocol == .tls) { if (disable_tls) unreachable; @@ -1692,33 +1512,26 @@ pub fn open( } } - const conn = options.connection orelse - try client.connect(valid_uri.host.?.raw, uriPort(valid_uri, protocol), protocol); + const connection = options.connection orelse c: { + var host_name_buffer: [Uri.host_name_max]u8 = undefined; + const host_name = try uri.getHost(&host_name_buffer); + break :c try client.connect(host_name, uriPort(uri, protocol), protocol); + }; - var req: Request = .{ - .uri = valid_uri, + return .{ + .uri = uri, .client = client, - .connection = conn, + .connection = connection, .keep_alive = options.keep_alive, .method = method, .version = options.version, .transfer_encoding = .none, .redirect_behavior = options.redirect_behavior, .handle_continue = options.handle_continue, - .response = .{ - .version = undefined, - .status = undefined, - .reason = undefined, - .keep_alive = undefined, - .parser = .init(server_header.buffer[server_header.end_index..]), - }, .headers = options.headers, .extra_headers = options.extra_headers, .privileged_headers = options.privileged_headers, }; - errdefer req.deinit(); - - return req; } pub const FetchOptions = struct { @@ -1828,7 +1641,20 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { }; } +pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { + if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; + if (child_host.len == parent_host.len) return true; + if (parent_host.len > child_host.len) return false; + return child_host[child_host.len - parent_host.len - 1] == '.'; +} + +test sameParentDomain { + try testing.expect(!sameParentDomain("foo.com", "bar.com")); + try testing.expect(sameParentDomain("foo.com", "foo.com")); + try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); + try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); +} + test { _ = Response; - _ = &initDefaultProxies; } diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig index 43b45c463db8ab2bb98a7afd2149e9d31f7ffa41..e0c224c560a5971260f774ccdc2283b0d1e8262e 100644 --- a/lib/std/http/Server.zig +++ b/lib/std/http/Server.zig @@ -1,142 +1,59 @@ -//! Blocking HTTP server implementation. -//! Handles a single connection's lifecycle. +//! Handles a single connection lifecycle. const std = @import("../std.zig"); const http = std.http; const mem = std.mem; -const net = std.net; const Uri = std.Uri; const assert = std.debug.assert; const testing = std.testing; const Server = @This(); -/// The reader's buffer must be large enough to store the client's entire HTTP -/// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. -in: *std.io.BufferedReader, /// Data from the HTTP server to the HTTP client. out: *std.io.BufferedWriter, -/// Keeps track of whether the Server is ready to accept a new request on the -/// same connection, and makes invalid API usage cause assertion failures -/// rather than HTTP protocol violations. -state: State, -/// Populated when `receiveHead` returns `ReceiveHeadError.HttpHeadersInvalid`. -head_parse_err: ?Request.Head.ParseError = null, - -pub const State = enum { - /// The connection is available to be used for the first time, or reused. - ready, - /// An error occurred in `receiveHead`. - receiving_head, - /// A Request object has been obtained and from there a Response can be - /// opened. - received_head, - /// The client is uploading something to this Server. - receiving_body, - /// The connection is eligible for another HTTP request, however the client - /// and server did not negotiate a persistent connection. - closing, -}; +/// Internal state managed by this abstraction. +reader: http.Reader, /// Initialize an HTTP server that can respond to multiple requests on the same /// connection. /// +/// The buffer of `in` must be large enough to store the client's entire HTTP +/// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. +/// /// The returned `Server` is ready for `receiveHead` to be called. pub fn init(in: *std.io.BufferedReader, out: *std.io.BufferedWriter) Server { return .{ - .in = in, + .reader = .{ + .in = in, + .state = .ready, + }, .out = out, - .state = .ready, }; } -pub const ReceiveHeadError = error{ - /// Client sent too many bytes of HTTP headers. - /// The HTTP specification suggests to respond with a 431 status code - /// before closing the connection. - HttpHeadersOversize, - /// Client sent headers that did not conform to the HTTP protocol; - /// `head_parse_err` is populated. +pub const ReceiveHeadError = http.Reader.HeadError || error{ + /// Client sent headers that did not conform to the HTTP protocol. + /// + /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be + /// passed directly to `Request.Head.parse`. HttpHeadersInvalid, - /// Partial HTTP request was received but the connection was closed before - /// fully receiving the headers. - HttpRequestTruncated, - /// The client sent 0 bytes of headers before closing the stream. - /// In other words, a keep-alive connection was finally closed. - HttpConnectionClosing, - /// Transitive error occurred reading from `in`. - ReadFailed, }; -/// The header bytes reference the internal storage of `in`, which are -/// invalidated with the next call to `receiveHead`. -pub fn receiveHead(s: *Server) ReceiveHeadError!Request { - assert(s.state == .ready); - s.state = .received_head; - errdefer s.state = .receiving_head; - - const in = s.in; - var hp: http.HeadParser = .{}; - var head_end: usize = 0; - - while (true) { - if (head_end >= in.buffer.len) return error.HttpHeadersOversize; - const buf = in.peekGreedy(head_end + 1) catch |err| switch (err) { - error.EndOfStream => switch (head_end) { - 0 => return error.HttpConnectionClosing, - else => return error.HttpRequestTruncated, - }, - error.ReadFailed => return error.ReadFailed, - }; - head_end += hp.feed(buf[head_end..]); - if (hp.state == .finished) return .{ - .server = s, - .head_end = head_end, - .head = Request.Head.parse(buf[0..head_end]) catch |err| { - s.head_parse_err = err; - return error.HttpHeadersInvalid; - }, - .reader_state = undefined, - }; - } +pub fn receiveHead(s: *Server) http.Reader.HeadError!Request { + try s.reader.receiveHead(); + return .{ + .server = s, + // No need to track the returned error here since users can repeat the + // parse with the header buffer to get detailed diagnostics. + .head = Request.Head.parse(s.reader.head_buffer) catch return error.HttpHeadersInvalid, + }; } pub const Request = struct { server: *Server, - /// Index into `Server.in` internal buffer. - head_end: usize, - /// Number of bytes of HTTP trailers. These are at the end of a - /// transfer-encoding: chunked message. - trailers_len: usize = 0, + /// Pointers in this struct are invalidated with the next call to + /// `receiveHead`. head: Head, - reader_state: union { - remaining_content_length: u64, - remaining_chunk_len: RemainingChunkLen, - }, - read_err: ?ReadError = null, - - pub const ReadError = error{ - HttpChunkInvalid, - HttpHeadersOversize, - }; - - pub const max_chunk_header_len = 22; - - pub const RemainingChunkLen = enum(u64) { - head = 0, - n = 1, - rn = 2, - done = std.math.maxInt(u64), - _, - - pub fn init(integer: u64) RemainingChunkLen { - return @enumFromInt(integer); - } - - pub fn int(rcl: RemainingChunkLen) u64 { - return @intFromEnum(rcl); - } - }; pub const Compression = union(enum) { deflate: std.compress.zlib.Decompressor, @@ -308,7 +225,7 @@ pub const Request = struct { }; pub fn iterateHeaders(r: *Request) http.HeaderIterator { - return http.HeaderIterator.init(r.server.in.bufferContents()[0..r.head_end]); + return http.HeaderIterator.init(r.server.reader.head_buffer); } test iterateHeaders { @@ -332,10 +249,8 @@ pub const Request = struct { var request: Request = .{ .server = &server, - .head_end = request_bytes.len, .trailers_len = 0, .head = undefined, - .reader_state = undefined, }; var it = request.iterateHeaders(); @@ -511,7 +426,8 @@ pub const Request = struct { respond_options: RespondOptions = .{}, }; - /// The header is not guaranteed to be sent until `Response.flush` is called. + /// The header is not guaranteed to be sent until `BodyWriter.flush` or + /// `BodyWriter.end` is called. /// /// If the request contains a body and the connection is to be reused, /// discards the request body, leaving the Server in the `ready` state. If @@ -519,13 +435,13 @@ pub const Request = struct { /// no error is surfaced. /// /// HEAD requests are handled transparently by setting the - /// `Response.elide_body` flag on the returned `Response`, causing + /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing /// the response stream to omit the body. However, it may be worth noticing /// that flag and skipping any expensive work that would otherwise need to /// be done to satisfy the request. /// /// Asserts status is not `continue`. - pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) std.io.Writer.Error!Response { + pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) std.io.Writer.Error!http.BodyWriter { const o = options.respond_options; assert(o.status != .@"continue"); const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; @@ -573,7 +489,7 @@ pub const Request = struct { }; return .{ - .server_output = request.server.out, + .http_protocol_output = request.server.out, .transfer_encoding = if (o.transfer_encoding) |te| switch (te) { .chunked => .{ .chunked = .init }, .none => .none, @@ -584,242 +500,6 @@ pub const Request = struct { }; } - fn contentLengthRead( - ctx: ?*anyopaque, - bw: *std.io.BufferedWriter, - limit: std.io.Reader.Limit, - ) std.io.Reader.RwError!usize { - const request: *Request = @alignCast(@ptrCast(ctx)); - const remaining_content_length = &request.reader_state.remaining_content_length; - const remaining = remaining_content_length.*; - const server = request.server; - if (remaining == 0) { - server.state = .ready; - return error.EndOfStream; - } - const n = try server.in.read(bw, limit.min(.limited(remaining))); - const new_remaining = remaining - n; - remaining_content_length.* = new_remaining; - return n; - } - - fn contentLengthReadVec(context: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { - const request: *Request = @alignCast(@ptrCast(context)); - const remaining_content_length = &request.reader_state.remaining_content_length; - const server = request.server; - const remaining = remaining_content_length.*; - if (remaining == 0) { - server.state = .ready; - return error.EndOfStream; - } - const n = try server.in.readVecLimit(data, .limited(remaining)); - const new_remaining = remaining - n; - remaining_content_length.* = new_remaining; - return n; - } - - fn contentLengthDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { - const request: *Request = @alignCast(@ptrCast(ctx)); - const remaining_content_length = &request.reader_state.remaining_content_length; - const server = request.server; - const remaining = remaining_content_length.*; - if (remaining == 0) { - server.state = .ready; - return error.EndOfStream; - } - const n = try server.in.discard(limit.min(.limited(remaining))); - const new_remaining = remaining - n; - remaining_content_length.* = new_remaining; - return n; - } - - fn chunkedRead( - ctx: ?*anyopaque, - bw: *std.io.BufferedWriter, - limit: std.io.Reader.Limit, - ) std.io.Reader.RwError!usize { - const request: *Request = @alignCast(@ptrCast(ctx)); - const chunk_len_ptr = &request.reader_state.remaining_chunk_len; - const in = request.server.in; - len: switch (chunk_len_ptr.*) { - .head => { - var cp: http.ChunkParser = .init; - const i = cp.feed(in.bufferContents()); - switch (cp.state) { - .invalid => return request.failRead(error.HttpChunkInvalid), - .data => { - if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(i); - }, - else => { - try in.fill(max_chunk_header_len); - const next_i = cp.feed(in.bufferContents()[i..]); - if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); - const header_len = i + next_i; - if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(header_len); - }, - } - if (cp.chunk_len == 0) return parseTrailers(request, 0); - const n = try in.read(bw, limit.min(.limited(cp.chunk_len))); - chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); - return n; - }, - .n => { - if ((try in.peekByte()) != '\n') return request.failRead(error.HttpChunkInvalid); - in.toss(1); - continue :len .head; - }, - .rn => { - const rn = try in.peekArray(2); - if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); - in.toss(2); - continue :len .head; - }, - else => |remaining_chunk_len| { - const n = try in.read(bw, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); - chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); - return n; - }, - .done => return error.EndOfStream, - } - } - - fn chunkedReadVec(ctx: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { - const request: *Request = @alignCast(@ptrCast(ctx)); - const chunk_len_ptr = &request.reader_state.remaining_chunk_len; - const in = request.server.in; - var already_requested_more = false; - var amt_read: usize = 0; - data: for (data) |d| { - len: switch (chunk_len_ptr.*) { - .head => { - var cp: http.ChunkParser = .init; - const available_buffer = in.bufferContents(); - const i = cp.feed(available_buffer); - if (cp.state == .invalid) return request.failRead(error.HttpChunkInvalid); - if (i == available_buffer.len) { - if (already_requested_more) { - chunk_len_ptr.* = .head; - return amt_read; - } - already_requested_more = true; - try in.fill(max_chunk_header_len); - const next_i = cp.feed(in.bufferContents()[i..]); - if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); - const header_len = i + next_i; - if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(header_len); - } else { - if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(i); - } - if (cp.chunk_len == 0) return parseTrailers(request, amt_read); - continue :len .init(cp.chunk_len + 2); - }, - .n => { - if (in.bufferContents().len < 1) already_requested_more = true; - if ((try in.takeByte()) != '\n') return request.failRead(error.HttpChunkInvalid); - continue :len .head; - }, - .rn => { - if (in.bufferContents().len < 2) already_requested_more = true; - const rn = try in.takeArray(2); - if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); - continue :len .head; - }, - else => |remaining_chunk_len| { - const available_buffer = in.bufferContents(); - const copy_len = @min(available_buffer.len, d.len, remaining_chunk_len.int() - 2); - @memcpy(d[0..copy_len], available_buffer[0..copy_len]); - amt_read += copy_len; - in.toss(copy_len); - const next_chunk_len: RemainingChunkLen = .init(remaining_chunk_len.int() - copy_len); - if (copy_len == d.len) { - chunk_len_ptr.* = next_chunk_len; - continue :data; - } - if (already_requested_more) { - chunk_len_ptr.* = next_chunk_len; - return amt_read; - } - already_requested_more = true; - try in.fill(3); - continue :len next_chunk_len; - }, - .done => return error.EndOfStream, - } - } - return amt_read; - } - - fn chunkedDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { - const request: *Request = @alignCast(@ptrCast(ctx)); - const chunk_len_ptr = &request.reader_state.remaining_chunk_len; - const in = request.server.in; - len: switch (chunk_len_ptr.*) { - .head => { - var cp: http.ChunkParser = .init; - const i = cp.feed(in.bufferContents()); - switch (cp.state) { - .invalid => return request.failRead(error.HttpChunkInvalid), - .data => { - if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(i); - }, - else => { - try in.fill(max_chunk_header_len); - const next_i = cp.feed(in.bufferContents()[i..]); - if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); - const header_len = i + next_i; - if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); - in.toss(header_len); - }, - } - if (cp.chunk_len == 0) return parseTrailers(request, 0); - const n = try in.discard(limit.min(.limited(cp.chunk_len))); - chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); - return n; - }, - .n => { - if ((try in.peekByte()) != '\n') return request.failRead(error.HttpChunkInvalid); - in.toss(1); - continue :len .head; - }, - .rn => { - const rn = try in.peekArray(2); - if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); - in.toss(2); - continue :len .head; - }, - else => |remaining_chunk_len| { - const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2))); - chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); - return n; - }, - .done => return error.EndOfStream, - } - } - - /// Called when next bytes in the stream are trailers, or "\r\n" to indicate - /// end of chunked body. - fn parseTrailers(request: *Request, amt_read: usize) std.io.Reader.Error!usize { - const in = request.server.in; - var hp: http.HeadParser = .{}; - var trailers_len: usize = 0; - while (true) { - if (trailers_len >= in.buffer.len) return request.failRead(error.HttpHeadersOversize); - try in.fill(trailers_len + 1); - trailers_len += hp.feed(in.bufferContents()[trailers_len..]); - if (hp.state == .finished) { - request.reader_state.remaining_chunk_len = .done; - request.server.state = .ready; - request.trailers_len = trailers_len; - return amt_read; - } - } - } - pub const ReaderError = error{ /// Failed to write "100-continue" to the stream. WriteFailed, @@ -837,10 +517,7 @@ pub const Request = struct { /// /// Asserts that this function is only called once. pub fn reader(request: *Request) ReaderError!std.io.Reader { - const s = request.server; - assert(s.state == .received_head); - s.state = .receiving_body; - + assert(request.server.reader.state == .received_head); if (request.head.expect) |expect| { if (mem.eql(u8, expect, "100-continue")) { try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); @@ -849,36 +526,11 @@ pub const Request = struct { return error.HttpExpectationFailed; } } - - switch (request.head.transfer_encoding) { - .chunked => { - request.reader_state = .{ .remaining_chunk_len = .head }; - return .{ - .context = request, - .vtable = &.{ - .read = &chunkedRead, - .readVec = &chunkedReadVec, - .discard = &chunkedDiscard, - }, - }; - }, - .none => { - request.reader_state = .{ - .remaining_content_length = request.head.content_length orelse 0, - }; - return .{ - .context = request, - .vtable = &.{ - .read = &contentLengthRead, - .readVec = &contentLengthReadVec, - .discard = &contentLengthDiscard, - }, - }; - }, - } + return request.server.reader.interface(request.head.transfer_encoding, request.head.content_length); } /// Returns whether the connection should remain persistent. + /// /// If it would fail, it instead sets the Server state to `receiving_body` /// and returns false. fn discardBody(request: *Request, keep_alive: bool) bool { @@ -890,12 +542,12 @@ pub const Request = struct { // or the request body. // If the connection won't be kept alive, then none of this matters // because the connection will be severed after the response is sent. - const s = request.server; - if (keep_alive and request.head.keep_alive) switch (s.state) { + const r = &request.server.reader; + if (keep_alive and request.head.keep_alive) switch (r.state) { .received_head => { - const r = request.reader() catch return false; - _ = r.discardRemaining() catch return false; - assert(s.state == .ready); + const reader_interface = request.reader() catch return false; + _ = reader_interface.discardRemaining() catch return false; + assert(r.state == .ready); return true; }, .receiving_body, .ready => return true, @@ -903,378 +555,10 @@ pub const Request = struct { }; // Avoid clobbering the state in case a reading stream already exists. - switch (s.state) { - .received_head => s.state = .closing, + switch (r.state) { + .received_head => r.state = .closing, else => {}, } return false; } - - fn failRead(r: *Request, err: ReadError) error{ReadFailed} { - r.read_err = err; - return error.ReadFailed; - } -}; - -pub const Response = struct { - /// HTTP protocol to the client. - /// - /// This is the underlying stream; use `buffered` to create a - /// `BufferedWriter` for this `Response`. - /// - /// Until the lifetime of `Response` ends, it is illegal to modify the - /// state of this other than via methods of `Response`. - server_output: *std.io.BufferedWriter, - /// `null` means transfer-encoding: chunked. - /// As a debugging utility, counts down to zero as bytes are written. - transfer_encoding: TransferEncoding, - elide_body: bool, - err: Error!void = {}, - - pub const Error = error{ - /// Attempted to write a file to the stream, an expensive operation - /// that should be avoided when `elide_body` is true. - UnableToElideBody, - }; - pub const WriteError = std.io.Writer.Error; - - /// How many zeroes to reserve for hex-encoded chunk length. - const chunk_len_digits = 8; - const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; - const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; - - comptime { - assert(max_chunk_len == std.math.maxInt(u32)); - } - - pub const TransferEncoding = union(enum) { - /// End of connection signals the end of the stream. - none, - /// As a debugging utility, counts down to zero as bytes are written. - content_length: u64, - /// Each chunk is wrapped in a header and trailer. - chunked: Chunked, - - pub const Chunked = union(enum) { - /// Index of the hex-encoded chunk length in the chunk header - /// within the buffer of `Response.server_output`. - offset: usize, - /// We are in the middle of a chunk and this is how many bytes are - /// left until the next header. This includes +2 for "\r"\n", and - /// is zero for the beginning of the stream. - chunk_len: usize, - - pub const init: Chunked = .{ .chunk_len = 0 }; - }; - }; - - /// Sends all buffered data across `Response.server_output`. - /// - /// Some buffered data will remain if transfer-encoding is chunked and the - /// response is mid-chunk. - pub fn flush(r: *Response) WriteError!void { - switch (r.transfer_encoding) { - .none, .content_length => return r.server_output.flush(), - .chunked => |*chunked| switch (chunked.*) { - .offset => |*offset| { - try r.server_output.flushLimit(.limited(r.server_output.end - offset.*)); - offset.* = 0; - }, - .chunk_len => return r.server_output.flush(), - }, - } - } - - /// When using content-length, asserts that the amount of data sent matches - /// the value sent in the header, then flushes. Asserts the amount of bytes - /// sent matches the content-length value provided in the HTTP header. - /// - /// When using transfer-encoding: chunked, writes the end-of-stream message - /// with empty trailers, then flushes the stream to the system. Asserts any - /// started chunk has been completely finished. - /// - /// Respects the value of `elide_body` to omit all data after the headers. - /// - /// Sets `r` to undefined. - /// - /// See also: - /// * `endUnflushed` - /// * `endChunked` - pub fn end(r: *Response) WriteError!void { - try endUnflushed(r); - try r.server_output.flush(); - r.* = undefined; - } - - /// When using content-length, asserts that the amount of data sent matches - /// the value sent in the header. - /// - /// Otherwise, transfer-encoding: chunked is being used, and it writes the - /// end-of-stream message with empty trailers. - /// - /// Respects the value of `elide_body` to omit all data after the headers. - /// - /// See also: - /// * `end` - /// * `endChunked` - pub fn endUnflushed(r: *Response) WriteError!void { - switch (r.transfer_encoding) { - .content_length => |len| assert(len == 0), // Trips when end() called before all bytes written. - .none => {}, - .chunked => try endChunked(r, .{}), - } - } - - pub const EndChunkedOptions = struct { - trailers: []const http.Header = &.{}, - }; - - /// Writes the end-of-stream message and any optional trailers. - /// - /// Does not flush. - /// - /// Asserts that the Response is using transfer-encoding: chunked. - /// - /// Respects the value of `elide_body` to omit all data after the headers. - /// - /// See also: - /// * `end` - /// * `endUnflushed` - pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void { - const chunked = &r.transfer_encoding.chunked; - if (r.elide_body) return; - const bw = r.server_output; - switch (chunked.*) { - .offset => |offset| { - const chunk_len = bw.end - offset - chunk_header_template.len; - writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); - try bw.writeAll("\r\n"); - }, - .chunk_len => |chunk_len| switch (chunk_len) { - 0 => {}, - 1 => try bw.writeByte('\n'), - 2 => try bw.writeAll("\r\n"), - else => unreachable, // An earlier write call indicated more data would follow. - }, - } - if (options.trailers.len > 0) { - try bw.writeAll("0\r\n"); - for (options.trailers) |trailer| { - try bw.writeAll(trailer.name); - try bw.writeAll(": "); - try bw.writeAll(trailer.value); - try bw.writeAll("\r\n"); - } - try bw.writeAll("\r\n"); - } - r.* = undefined; - } - - fn contentLengthWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { - const r: *Response = @alignCast(@ptrCast(context)); - const n = if (r.elide_body) countSplat(data, splat) else try r.server_output.writeSplat(data, splat); - r.transfer_encoding.content_length -= n; - return n; - } - - fn noneWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { - const r: *Response = @alignCast(@ptrCast(context)); - if (r.elide_body) return countSplat(data, splat); - return r.server_output.writeSplat(data, splat); - } - - fn countSplat(data: []const []const u8, splat: usize) usize { - if (data.len == 0) return 0; - var total: usize = 0; - for (data[0 .. data.len - 1]) |buf| total += buf.len; - total += data[data.len - 1].len * splat; - return total; - } - - fn elideWriteFile( - r: *Response, - offset: std.io.Writer.Offset, - limit: std.io.Writer.Limit, - headers_and_trailers: []const []const u8, - ) WriteError!usize { - if (offset != .none) { - if (countWriteFile(limit, headers_and_trailers)) |n| { - return n; - } - } - r.err = error.UnableToElideBody; - return error.WriteFailed; - } - - /// Returns `null` if size cannot be computed without making any syscalls. - fn countWriteFile(limit: std.io.Writer.Limit, headers_and_trailers: []const []const u8) ?usize { - var total: usize = limit.toInt() orelse return null; - for (headers_and_trailers) |buf| total += buf.len; - return total; - } - - fn noneWriteFile( - context: ?*anyopaque, - file: std.fs.File, - offset: std.io.Writer.Offset, - limit: std.io.Writer.Limit, - headers_and_trailers: []const []const u8, - headers_len: usize, - ) std.io.Writer.FileError!usize { - if (limit == .nothing) return noneWriteSplat(context, headers_and_trailers, 1); - const r: *Response = @alignCast(@ptrCast(context)); - if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); - return r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); - } - - fn contentLengthWriteFile( - context: ?*anyopaque, - file: std.fs.File, - offset: std.io.Writer.Offset, - limit: std.io.Writer.Limit, - headers_and_trailers: []const []const u8, - headers_len: usize, - ) std.io.Writer.FileError!usize { - if (limit == .nothing) return contentLengthWriteSplat(context, headers_and_trailers, 1); - const r: *Response = @alignCast(@ptrCast(context)); - if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); - const n = try r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); - r.transfer_encoding.content_length -= n; - return n; - } - - fn chunkedWriteFile( - context: ?*anyopaque, - file: std.fs.File, - offset: std.io.Writer.Offset, - limit: std.io.Writer.Limit, - headers_and_trailers: []const []const u8, - headers_len: usize, - ) std.io.Writer.FileError!usize { - if (limit == .nothing) return chunkedWriteSplat(context, headers_and_trailers, 1); - const r: *Response = @alignCast(@ptrCast(context)); - if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); - const data_len = countWriteFile(limit, headers_and_trailers) orelse @panic("TODO"); - const bw = r.server_output; - const chunked = &r.transfer_encoding.chunked; - state: switch (chunked.*) { - .offset => |off| { - // TODO: is it better perf to read small files into the buffer? - const buffered_len = bw.end - off - chunk_header_template.len; - const chunk_len = data_len + buffered_len; - writeHex(bw.buffer[off..][0..chunk_len_digits], chunk_len); - const n = try bw.writeFile(file, offset, limit, headers_and_trailers, headers_len); - chunked.* = .{ .chunk_len = data_len + 2 - n }; - return n; - }, - .chunk_len => |chunk_len| l: switch (chunk_len) { - 0 => { - const header_buf = try bw.writableArray(chunk_header_template.len); - const off = bw.end; - @memcpy(header_buf, chunk_header_template); - chunked.* = .{ .offset = off }; - continue :state .{ .offset = off }; - }, - 1 => { - try bw.writeByte('\n'); - chunked.chunk_len = 0; - continue :l 0; - }, - 2 => { - try bw.writeByte('\r'); - chunked.chunk_len = 1; - continue :l 1; - }, - else => { - const new_limit = limit.min(.limited(chunk_len - 2)); - const n = try bw.writeFile(file, offset, new_limit, headers_and_trailers, headers_len); - chunked.chunk_len = chunk_len - n; - return n; - }, - }, - } - } - - fn chunkedWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { - const r: *Response = @alignCast(@ptrCast(context)); - const data_len = countSplat(data, splat); - if (r.elide_body) return data_len; - - const bw = r.server_output; - const chunked = &r.transfer_encoding.chunked; - - state: switch (chunked.*) { - .offset => |offset| { - if (bw.unusedCapacitySlice().len >= data_len) { - assert(data_len == (bw.writeSplat(data, splat) catch unreachable)); - return data_len; - } - const buffered_len = bw.end - offset - chunk_header_template.len; - const chunk_len = data_len + buffered_len; - writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); - const n = try bw.writeSplat(data, splat); - chunked.* = .{ .chunk_len = data_len + 2 - n }; - return n; - }, - .chunk_len => |chunk_len| l: switch (chunk_len) { - 0 => { - const header_buf = try bw.writableArray(chunk_header_template.len); - const offset = bw.end; - @memcpy(header_buf, chunk_header_template); - chunked.* = .{ .offset = offset }; - continue :state .{ .offset = offset }; - }, - 1 => { - try bw.writeByte('\n'); - chunked.chunk_len = 0; - continue :l 0; - }, - 2 => { - try bw.writeByte('\r'); - chunked.chunk_len = 1; - continue :l 1; - }, - else => { - const n = try bw.writeSplatLimit(data, splat, .limited(chunk_len - 2)); - chunked.chunk_len = chunk_len - n; - return n; - }, - }, - } - } - - /// Writes an integer as base 16 to `buf`, right-aligned, assuming the - /// buffer has already been filled with zeroes. - fn writeHex(buf: []u8, x: usize) void { - assert(std.mem.allEqual(u8, buf, '0')); - const base = 16; - var index: usize = buf.len; - var a = x; - while (a > 0) { - const digit = a % base; - index -= 1; - buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); - a /= base; - } - } - - pub fn writer(r: *Response) std.io.Writer { - return .{ - .context = r, - .vtable = switch (r.transfer_encoding) { - .none => &.{ - .writeSplat = noneWriteSplat, - .writeFile = noneWriteFile, - }, - .content_length => &.{ - .writeSplat = contentLengthWriteSplat, - .writeFile = contentLengthWriteFile, - }, - .chunked => &.{ - .writeSplat = chunkedWriteSplat, - .writeFile = chunkedWriteFile, - }, - }, - }; - } }; diff --git a/lib/std/http/WebSocket.zig b/lib/std/http/WebSocket.zig index db92e6b0d302f2abd762e104d62d4b6ecb12e739..59b9659b3a0d0880799635f3260fd6a0d9664729 100644 --- a/lib/std/http/WebSocket.zig +++ b/lib/std/http/WebSocket.zig @@ -10,7 +10,7 @@ key: []const u8, request: *std.http.Server.Request, recv_fifo: std.fifo.LinearFifo(u8, .Slice), reader: std.io.BufferedReader, -response: std.http.Server.Response, +body_writer: std.http.BodyWriter, /// Number of bytes that have been peeked but not discarded yet. outstanding_len: usize, @@ -58,7 +58,7 @@ pub fn init( .key = key, .recv_fifo = .init(recv_buffer), .reader = (try request.reader()).unbuffered(), - .response = try request.respondStreaming(.{ + .body_writer = try request.respondStreaming(.{ .respond_options = .{ .status = .switching_protocols, .extra_headers = &.{ @@ -236,7 +236,7 @@ pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opc }, }; - var bw = ws.response.writer().unbuffered(); + var bw = ws.body_writer.interface().unbuffered(); try bw.writeAll(header); for (message) |iovec| try bw.writeAll(iovec.base[0..iovec.len]); try bw.flush(); diff --git a/lib/std/http/protocol.zig b/lib/std/http/protocol.zig deleted file mode 100644 index a7b0cbc5d64044c10d629f363d34b4991495fb65..0000000000000000000000000000000000000000 --- a/lib/std/http/protocol.zig +++ /dev/null @@ -1,449 +0,0 @@ -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const testing = std.testing; -const mem = std.mem; - -const assert = std.debug.assert; - -pub const State = enum { - invalid, - - // Begin header and trailer parsing states. - - start, - seen_n, - seen_r, - seen_rn, - seen_rnr, - finished, - - // Begin transfer-encoding: chunked parsing states. - - chunk_head_size, - chunk_head_ext, - chunk_head_r, - chunk_data, - chunk_data_suffix, - chunk_data_suffix_r, - - /// Returns true if the parser is in a content state (ie. not waiting for more headers). - pub fn isContent(self: State) bool { - return switch (self) { - .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false, - .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true, - }; - } -}; - -pub const HeadersParser = struct { - state: State = .start, - /// A fixed buffer of len `max_header_bytes`. - /// Pointers into this buffer are not stable until after a message is complete. - header_bytes_buffer: []u8, - header_bytes_len: u32, - next_chunk_length: u64, - /// `false`: headers. `true`: trailers. - done: bool, - - /// Initializes the parser with a provided buffer `buf`. - pub fn init(buf: []u8) HeadersParser { - return .{ - .header_bytes_buffer = buf, - .header_bytes_len = 0, - .done = false, - .next_chunk_length = 0, - }; - } - - /// Reinitialize the parser. - /// Asserts the parser is in the "done" state. - pub fn reset(hp: *HeadersParser) void { - assert(hp.done); - hp.* = .{ - .state = .start, - .header_bytes_buffer = hp.header_bytes_buffer, - .header_bytes_len = 0, - .done = false, - .next_chunk_length = 0, - }; - } - - pub fn get(hp: HeadersParser) []u8 { - return hp.header_bytes_buffer[0..hp.header_bytes_len]; - } - - pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 { - var hp: std.http.HeadParser = .{ - .state = switch (r.state) { - .start => .start, - .seen_n => .seen_n, - .seen_r => .seen_r, - .seen_rn => .seen_rn, - .seen_rnr => .seen_rnr, - .finished => .finished, - else => unreachable, - }, - }; - const result = hp.feed(bytes); - r.state = switch (hp.state) { - .start => .start, - .seen_n => .seen_n, - .seen_r => .seen_r, - .seen_rn => .seen_rn, - .seen_rnr => .seen_rnr, - .finished => .finished, - }; - return @intCast(result); - } - - pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 { - var cp: std.http.ChunkParser = .{ - .state = switch (r.state) { - .chunk_head_size => .head_size, - .chunk_head_ext => .head_ext, - .chunk_head_r => .head_r, - .chunk_data => .data, - .chunk_data_suffix => .data_suffix, - .chunk_data_suffix_r => .data_suffix_r, - .invalid => .invalid, - else => unreachable, - }, - .chunk_len = r.next_chunk_length, - }; - const result = cp.feed(bytes); - r.state = switch (cp.state) { - .head_size => .chunk_head_size, - .head_ext => .chunk_head_ext, - .head_r => .chunk_head_r, - .data => .chunk_data, - .data_suffix => .chunk_data_suffix, - .data_suffix_r => .chunk_data_suffix_r, - .invalid => .invalid, - }; - r.next_chunk_length = cp.chunk_len; - return @intCast(result); - } - - /// Returns whether or not the parser has finished parsing a complete - /// message. A message is only complete after the entire body has been read - /// and any trailing headers have been parsed. - pub fn isComplete(r: *HeadersParser) bool { - return r.done and r.state == .finished; - } - - pub const CheckCompleteHeadError = error{HttpHeadersOversize}; - - /// Pushes `in` into the parser. Returns the number of bytes consumed by - /// the header. Any header bytes are appended to `header_bytes_buffer`. - pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 { - if (hp.state.isContent()) return 0; - - const i = hp.findHeadersEnd(in); - const data = in[0..i]; - if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len) - return error.HttpHeadersOversize; - - @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data); - hp.header_bytes_len += @intCast(data.len); - - return i; - } - - pub const ReadError = error{ - HttpChunkInvalid, - }; - - /// Reads the body of the message into `buffer`. Returns the number of - /// bytes placed in the buffer. - /// - /// If `skip` is true, the buffer will be unused and the body will be skipped. - /// - /// See `std.http.Client.Connection for an example of `conn`. - pub fn read(r: *HeadersParser, conn: anytype, buffer: []u8, skip: bool) !usize { - assert(r.state.isContent()); - if (r.done) return 0; - - var out_index: usize = 0; - while (true) { - switch (r.state) { - .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable, - .finished => { - const data_avail = r.next_chunk_length; - - if (skip) { - conn.fill() catch |err| switch (err) { - error.EndOfStream => { - r.done = true; - return 0; - }, - else => |e| return e, - }; - - const nread = @min(conn.peek().len, data_avail); - conn.drop(@intCast(nread)); - r.next_chunk_length -= nread; - - if (r.next_chunk_length == 0 or nread == 0) r.done = true; - - return out_index; - } else if (out_index < buffer.len) { - const out_avail = buffer.len - out_index; - - const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); - const nread = try conn.read(buffer[0..can_read]); - r.next_chunk_length -= nread; - - if (r.next_chunk_length == 0 or nread == 0) r.done = true; - - return nread; - } else { - return out_index; - } - }, - .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { - conn.fill() catch |err| switch (err) { - error.EndOfStream => { - r.done = true; - return 0; - }, - else => |e| return e, - }; - - const i = r.findChunkedLen(conn.peek()); - conn.drop(@intCast(i)); - - switch (r.state) { - .invalid => return error.HttpChunkInvalid, - .chunk_data => if (r.next_chunk_length == 0) { - if (std.mem.eql(u8, conn.peek(), "\r\n")) { - r.state = .finished; - conn.drop(2); - } else { - // The trailer section is formatted identically - // to the header section. - r.state = .seen_rn; - } - r.done = true; - - return out_index; - }, - else => return out_index, - } - - continue; - }, - .chunk_data => { - const data_avail = r.next_chunk_length; - const out_avail = buffer.len - out_index; - - if (skip) { - conn.fill() catch |err| switch (err) { - error.EndOfStream => { - r.done = true; - return 0; - }, - else => |e| return e, - }; - - const nread = @min(conn.peek().len, data_avail); - conn.drop(@intCast(nread)); - r.next_chunk_length -= nread; - } else if (out_avail > 0) { - const can_read: usize = @intCast(@min(data_avail, out_avail)); - const nread = try conn.read(buffer[out_index..][0..can_read]); - r.next_chunk_length -= nread; - out_index += nread; - } - - if (r.next_chunk_length == 0) { - r.state = .chunk_data_suffix; - continue; - } - - return out_index; - }, - } - } - } -}; - -inline fn int16(array: *const [2]u8) u16 { - return @as(u16, @bitCast(array.*)); -} - -inline fn int24(array: *const [3]u8) u24 { - return @as(u24, @bitCast(array.*)); -} - -inline fn int32(array: *const [4]u8) u32 { - return @as(u32, @bitCast(array.*)); -} - -inline fn intShift(comptime T: type, x: anytype) T { - switch (@import("builtin").cpu.arch.endian()) { - .little => return @as(T, @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T)))), - .big => return @as(T, @truncate(x)), - } -} - -/// A buffered (and peekable) Connection. -const MockBufferedConnection = struct { - pub const buffer_size = 0x2000; - - conn: std.io.FixedBufferStream, - buf: [buffer_size]u8 = undefined, - start: u16 = 0, - end: u16 = 0, - - pub fn fill(conn: *MockBufferedConnection) ReadError!void { - if (conn.end != conn.start) return; - - const nread = try conn.conn.read(conn.buf[0..]); - if (nread == 0) return error.EndOfStream; - conn.start = 0; - conn.end = @as(u16, @truncate(nread)); - } - - pub fn peek(conn: *MockBufferedConnection) []const u8 { - return conn.buf[conn.start..conn.end]; - } - - pub fn drop(conn: *MockBufferedConnection, num: u16) void { - conn.start += num; - } - - pub fn readAtLeast(conn: *MockBufferedConnection, buffer: []u8, len: usize) ReadError!usize { - var out_index: u16 = 0; - while (out_index < len) { - const available = conn.end - conn.start; - const left = buffer.len - out_index; - - if (available > 0) { - const can_read = @as(u16, @truncate(@min(available, left))); - - @memcpy(buffer[out_index..][0..can_read], conn.buf[conn.start..][0..can_read]); - out_index += can_read; - conn.start += can_read; - - continue; - } - - if (left > conn.buf.len) { - // skip the buffer if the output is large enough - return conn.conn.read(buffer[out_index..]); - } - - try conn.fill(); - } - - return out_index; - } - - pub fn read(conn: *MockBufferedConnection, buffer: []u8) ReadError!usize { - return conn.readAtLeast(buffer, 1); - } - - pub const ReadError = std.io.FixedBufferStream.ReadError || error{EndOfStream}; - pub const Reader = std.io.Reader(*MockBufferedConnection, ReadError, read); - - pub fn reader(conn: *MockBufferedConnection) Reader { - return Reader{ .context = conn }; - } -}; - -test "HeadersParser.read length" { - // mock BufferedConnection for read - var headers_buf: [256]u8 = undefined; - - var r = HeadersParser.init(&headers_buf); - const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello"; - - var conn: MockBufferedConnection = .{ - .conn = .{ .buffer = data }, - }; - - while (true) { // read headers - try conn.fill(); - - const nchecked = try r.checkCompleteHead(conn.peek()); - conn.drop(@intCast(nchecked)); - - if (r.state.isContent()) break; - } - - var buf: [8]u8 = undefined; - - r.next_chunk_length = 5; - const len = try r.read(&conn, &buf, false); - try std.testing.expectEqual(@as(usize, 5), len); - try std.testing.expectEqualStrings("Hello", buf[0..len]); - - try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get()); -} - -test "HeadersParser.read chunked" { - // mock BufferedConnection for read - - var headers_buf: [256]u8 = undefined; - var r = HeadersParser.init(&headers_buf); - const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n"; - - var conn: MockBufferedConnection = .{ - .conn = .{ .buffer = data }, - }; - - while (true) { // read headers - try conn.fill(); - - const nchecked = try r.checkCompleteHead(conn.peek()); - conn.drop(@intCast(nchecked)); - - if (r.state.isContent()) break; - } - var buf: [8]u8 = undefined; - - r.state = .chunk_head_size; - const len = try r.read(&conn, &buf, false); - try std.testing.expectEqual(@as(usize, 5), len); - try std.testing.expectEqualStrings("Hello", buf[0..len]); - - try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get()); -} - -test "HeadersParser.read chunked trailer" { - // mock BufferedConnection for read - - var headers_buf: [256]u8 = undefined; - var r = HeadersParser.init(&headers_buf); - const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n"; - - var conn: MockBufferedConnection = .{ - .conn = .{ .buffer = data }, - }; - - while (true) { // read headers - try conn.fill(); - - const nchecked = try r.checkCompleteHead(conn.peek()); - conn.drop(@intCast(nchecked)); - - if (r.state.isContent()) break; - } - var buf: [8]u8 = undefined; - - r.state = .chunk_head_size; - const len = try r.read(&conn, &buf, false); - try std.testing.expectEqual(@as(usize, 5), len); - try std.testing.expectEqualStrings("Hello", buf[0..len]); - - while (true) { // read headers - try conn.fill(); - - const nchecked = try r.checkCompleteHead(conn.peek()); - conn.drop(@intCast(nchecked)); - - if (r.state.isContent()) break; - } - - try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get()); -} diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index 22b30bc3b2333830a00f79bd13f3c6bebac90668..378ae3a7fed8c6e8d6ae902e9ac918ea1f5f1081 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -61,21 +61,18 @@ test "trailers" { const uri = try std.Uri.parse(location); { - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&.{}); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); - var it = req.response.iterateHeaders(); + var it = response.iterateHeaders(); { const header = it.next().?; try expect(!it.is_trailer); @@ -565,20 +562,18 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); - try expectEqualStrings("text/plain", req.response.content_type.?); + try expectEqualStrings("text/plain", response.head.content_type.?); } // connection has been kept alive @@ -590,16 +585,14 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192 * 1024)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192 * 1024)); defer gpa.free(body); try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); @@ -614,21 +607,19 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.HEAD, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.HEAD, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("", body); - try expectEqualStrings("text/plain", req.response.content_type.?); - try expectEqual(14, req.response.content_length.?); + try expectEqualStrings("text/plain", response.content_type.?); + try expectEqual(14, response.head.content_length.?); } // connection has been kept alive @@ -640,20 +631,18 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); - try expectEqualStrings("text/plain", req.response.content_type.?); + try expectEqualStrings("text/plain", response.head.content_type.?); } // connection has been kept alive @@ -665,14 +654,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.HEAD, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.HEAD, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -691,15 +678,14 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, .keep_alive = false, }); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -717,17 +703,16 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "empty", .value = "" }, }, }); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); try std.testing.expectEqual(.ok, req.response.status); @@ -761,14 +746,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -785,14 +768,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -809,14 +790,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -833,14 +812,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - req.wait() catch |err| switch (err) { + try req.sendBodiless(); + req.receiveHead(&redirect_buffer) catch |err| switch (err) { error.TooManyHttpRedirects => {}, else => return err, }; @@ -852,14 +829,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + try req.receiveHead(&redirect_buffer); const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); @@ -876,14 +851,12 @@ test "general client/server API coverage" { const uri = try std.Uri.parse(location); log.info("{s}", .{location}); - var server_header_buffer: [1024]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [1024]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - const result = req.wait(); + try req.sendBodiless(); + const result = req.receiveHead(&redirect_buffer); // a proxy without an upstream is likely to return a 5xx status. if (client.http_proxy == null) { @@ -910,9 +883,7 @@ test "general client/server API coverage" { for (0..total_connections) |i| { const headers_buf = try gpa.alloc(u8, 1024); try header_bufs.append(headers_buf); - var req = try client.open(.GET, uri, .{ - .server_header_buffer = headers_buf, - }); + var req = try client.open(.GET, uri, .{}); req.response.parser.done = true; req.connection.?.closing = false; requests[i] = req; @@ -978,28 +949,26 @@ test "Server streams both reading and writing" { var client: http.Client = .{ .allocator = std.testing.allocator }; defer client.deinit(); - var server_header_buffer: [555]u8 = undefined; + var redirect_buffer: [555]u8 = undefined; var req = try client.open(.POST, .{ .scheme = "http", .host = .{ .raw = "127.0.0.1" }, .port = test_server.port(), .path = .{ .percent_encoded = "/" }, - }, .{ - .server_header_buffer = &server_header_buffer, - }); + }, .{}); defer req.deinit(); req.transfer_encoding = .chunked; - try req.send(); - try req.wait(); + var body_writer = try req.sendBody(); + var response = try req.receiveHead(&redirect_buffer); - var w = req.writer().unbuffered(); + var w = body_writer.interface().unbuffered(); try w.writeAll("one "); try w.writeAll("fish"); try req.finish(); - const body = try req.reader().readRemainingAlloc(std.testing.allocator, .limited(8192)); + const body = try response.reader().readRemainingAlloc(std.testing.allocator, .limited(8192)); defer std.testing.allocator.free(body); try expectEqualStrings("ONE FISH", body); @@ -1014,9 +983,8 @@ fn echoTests(client: *http.Client, port: u16) !void { defer gpa.free(location); const uri = try std.Uri.parse(location); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.POST, uri, .{ - .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "content-type", .value = "text/plain" }, }, @@ -1025,15 +993,15 @@ fn echoTests(client: *http.Client, port: u16) !void { req.transfer_encoding = .{ .content_length = 14 }; - try req.send(); - var w = req.writer().unbuffered(); + var body_writer = try req.sendBody(); + var w = body_writer.interface().unbuffered(); try w.writeAll("Hello, "); try w.writeAll("World!\n"); - try req.finish(); + try body_writer.end(); - try req.wait(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); @@ -1049,9 +1017,8 @@ fn echoTests(client: *http.Client, port: u16) !void { .{port}, )); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.POST, uri, .{ - .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "content-type", .value = "text/plain" }, }, @@ -1060,15 +1027,15 @@ fn echoTests(client: *http.Client, port: u16) !void { req.transfer_encoding = .chunked; - try req.send(); - var w = req.writer().unbuffered(); + var body_writer = try req.sendBody(); + var w = body_writer.interface().unbuffered(); try w.writeAll("Hello, "); try w.writeAll("World!\n"); - try req.finish(); + try body_writer.end(); - try req.wait(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); @@ -1103,9 +1070,8 @@ fn echoTests(client: *http.Client, port: u16) !void { defer gpa.free(location); const uri = try std.Uri.parse(location); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.POST, uri, .{ - .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "expect", .value = "100-continue" }, .{ .name = "content-type", .value = "text/plain" }, @@ -1115,16 +1081,16 @@ fn echoTests(client: *http.Client, port: u16) !void { req.transfer_encoding = .chunked; - try req.send(); - var w = req.writer().unbuffered(); + var body_writer = try req.sendBody(); + var w = body_writer.interface().unbuffered(); try w.writeAll("Hello, "); try w.writeAll("World!\n"); - try req.finish(); + try body_writer.end(); - try req.wait(); - try expectEqual(.ok, req.response.status); + var response = try req.receiveHead(&redirect_buffer); + try expectEqual(.ok, response.head.status); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("Hello, World!\n", body); @@ -1135,9 +1101,8 @@ fn echoTests(client: *http.Client, port: u16) !void { defer gpa.free(location); const uri = try std.Uri.parse(location); - var server_header_buffer: [1024]u8 = undefined; + var redirect_buffer: [1024]u8 = undefined; var req = try client.open(.POST, uri, .{ - .server_header_buffer = &server_header_buffer, .extra_headers = &.{ .{ .name = "content-type", .value = "text/plain" }, .{ .name = "expect", .value = "garbage" }, @@ -1147,9 +1112,11 @@ fn echoTests(client: *http.Client, port: u16) !void { req.transfer_encoding = .chunked; - try req.send(); - try req.wait(); - try expectEqual(.expectation_failed, req.response.status); + var body_writer = try req.sendBody(); + try body_writer.flush(); + var response = try req.receiveHead(&redirect_buffer); + try expectEqual(.expectation_failed, response.head.status); + _ = try response.reader().discardRemaining(); } _ = try client.fetch(.{ @@ -1255,16 +1222,14 @@ test "redirect to different connection" { const uri = try std.Uri.parse(location); { - var server_header_buffer: [666]u8 = undefined; - var req = try client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, - }); + var redirect_buffer: [666]u8 = undefined; + var req = try client.open(.GET, uri, .{}); defer req.deinit(); - try req.send(); - try req.wait(); + try req.sendBodiless(); + var response = try req.receiveHead(&redirect_buffer); - const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); + const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); defer gpa.free(body); try expectEqualStrings("good job, you pass", body); diff --git a/lib/std/io/BufferedReader.zig b/lib/std/io/BufferedReader.zig index d557c52c96ab7b58b6727e13039eda1556f63d57..4ff11d75a10f9a723aa2dedd1a243ff8a8159a77 100644 --- a/lib/std/io/BufferedReader.zig +++ b/lib/std/io/BufferedReader.zig @@ -919,6 +919,40 @@ fn takeMultipleOf7Leb128(br: *BufferedReader, comptime Result: type) TakeLeb128E } } +/// Left-aligns data such that `br.seek` becomes zero. +pub fn rebase(br: *BufferedReader) void { + const data = br.buffer[br.seek..br.end]; + const dest = br.buffer[0..data.len]; + std.mem.copyForwards(u8, dest, data); + br.seek = 0; + br.end = data.len; +} + +/// Advances the stream and decreases the size of the storage buffer by `n`, +/// returning the range of bytes no longer accessible by `br`. +/// +/// This action can be undone by `restitute`. +/// +/// Asserts there are at least `n` buffered bytes already. +/// +/// Asserts that `br.seek` is zero, i.e. the buffer is in a rebased state. +pub fn steal(br: *BufferedReader, n: usize) []u8 { + assert(br.seek == 0); + assert(n <= br.end); + const stolen = br.buffer[0..n]; + br.buffer = br.buffer[n..]; + br.end -= n; + return stolen; +} + +/// Expands the storage buffer, undoing the effects of `steal` +/// Assumes that `n` does not exceed the total number of stolen bytes. +pub fn restitute(br: *BufferedReader, n: usize) void { + br.buffer = (br.buffer.ptr - n)[0 .. br.buffer.len + n]; + br.end += n; + br.seek += n; +} + test initFixed { var br: BufferedReader = undefined; br.initFixed("a\x02");