| author | |
| committer | |
| log | 81b0d14e2ba43732c80a259b578997f167c23cdd |
| tree | d64899ee9d0e9529fae47576a0111a3c3ba46679 |
| parent | 396464ee6b8f5534947e5f457dbd2a26a458cb0f |
WIP9 files changed, 1491 insertions(+), 2155 deletions(-)
lib/std/Uri.zig+83-135| ... | @@ -1,6 +1,13 @@ | ... | @@ -1,6 +1,13 @@ |
| 1 | //! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>. | 1 | //! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>. |
| 2 | //! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild. | 2 | //! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild. |
| 3 | 3 | ||
| 4 | const std = @import("std.zig"); | ||
| 5 | const testing = std.testing; | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const assert = std.debug.assert; | ||
| 8 | |||
| 9 | const Uri = @This(); | ||
| 10 | |||
| 4 | scheme: []const u8, | 11 | scheme: []const u8, |
| 5 | user: ?Component = null, | 12 | user: ?Component = null, |
| 6 | password: ?Component = null, | 13 | password: ?Component = null, |
| ... | @@ -10,6 +17,32 @@ path: Component = Component.empty, | ... | @@ -10,6 +17,32 @@ path: Component = Component.empty, |
| 10 | query: ?Component = null, | 17 | query: ?Component = null, |
| 11 | fragment: ?Component = null, | 18 | fragment: ?Component = null, |
| 12 | 19 | ||
| 20 | pub const host_name_max = 255; | ||
| 21 | |||
| 22 | /// Returned value may point into `buffer` or be the original string. | ||
| 23 | /// | ||
| 24 | /// Suggested buffer length: `host_name_max`. | ||
| 25 | /// | ||
| 26 | /// See also: | ||
| 27 | /// * `getHostAlloc` | ||
| 28 | pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 { | ||
| 29 | const component = uri.host orelse return error.UriMissingHost; | ||
| 30 | return component.toRaw(buffer) catch |err| switch (err) { | ||
| 31 | error.NoSpaceLeft => return error.UriHostTooLong, | ||
| 32 | }; | ||
| 33 | } | ||
| 34 | |||
| 35 | /// Returned value may point into `buffer` or be the original string. | ||
| 36 | /// | ||
| 37 | /// See also: | ||
| 38 | /// * `getHost` | ||
| 39 | pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 { | ||
| 40 | const component = uri.host orelse return error.UriMissingHost; | ||
| 41 | const result = try component.toRawMaybeAlloc(arena); | ||
| 42 | if (result.len > host_name_max) return error.UriHostTooLong; | ||
| 43 | return result; | ||
| 44 | } | ||
| 45 | |||
| 13 | pub const Component = union(enum) { | 46 | pub const Component = union(enum) { |
| 14 | /// Invalid characters in this component must be percent encoded | 47 | /// Invalid characters in this component must be percent encoded |
| 15 | /// before being printed as part of a URI. | 48 | /// before being printed as part of a URI. |
| ... | @@ -26,11 +59,22 @@ pub const Component = union(enum) { | ... | @@ -26,11 +59,22 @@ pub const Component = union(enum) { |
| 26 | }; | 59 | }; |
| 27 | } | 60 | } |
| 28 | 61 | ||
| 62 | /// Returned value may point into `buffer` or be the original string. | ||
| 63 | pub fn toRaw(component: Component, buffer: []u8) error{NoSpaceLeft}![]const u8 { | ||
| 64 | return switch (component) { | ||
| 65 | .raw => |raw| raw, | ||
| 66 | .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| | ||
| 67 | try std.fmt.bufPrint(buffer, "{fraw}", .{component}) | ||
| 68 | else | ||
| 69 | percent_encoded, | ||
| 70 | }; | ||
| 71 | } | ||
| 72 | |||
| 29 | /// Allocates the result with `arena` only if needed, so the result should not be freed. | 73 | /// Allocates the result with `arena` only if needed, so the result should not be freed. |
| 30 | pub fn toRawMaybeAlloc( | 74 | pub fn toRawMaybeAlloc( |
| 31 | component: Component, | 75 | component: Component, |
| 32 | arena: std.mem.Allocator, | 76 | arena: Allocator, |
| 33 | ) std.mem.Allocator.Error![]const u8 { | 77 | ) Allocator.Error![]const u8 { |
| 34 | return switch (component) { | 78 | return switch (component) { |
| 35 | .raw => |raw| raw, | 79 | .raw => |raw| raw, |
| 36 | .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| | 80 | .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| |
| ... | @@ -144,17 +188,15 @@ pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; | ... | @@ -144,17 +188,15 @@ pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; |
| 144 | /// The return value will contain strings pointing into the original `text`. | 188 | /// The return value will contain strings pointing into the original `text`. |
| 145 | /// Each component that is provided, will be non-`null`. | 189 | /// Each component that is provided, will be non-`null`. |
| 146 | pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { | 190 | pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { |
| 147 | var reader = SliceReader{ .slice = text }; | ||
| 148 | |||
| 149 | var uri: Uri = .{ .scheme = scheme, .path = undefined }; | 191 | var uri: Uri = .{ .scheme = scheme, .path = undefined }; |
| 192 | var i: usize = 0; | ||
| 150 | 193 | ||
| 151 | if (reader.peekPrefix("//")) a: { // authority part | 194 | if (std.mem.startsWith(u8, text, "//")) a: { |
| 152 | std.debug.assert(reader.get().? == '/'); | 195 | i = std.mem.indexOfAnyPos(u8, text, 2, &authority_sep) orelse text.len; |
| 153 | std.debug.assert(reader.get().? == '/'); | 196 | const authority = text[2..i]; |
| 154 | |||
| 155 | const authority = reader.readUntil(isAuthoritySeparator); | ||
| 156 | if (authority.len == 0) { | 197 | if (authority.len == 0) { |
| 157 | if (reader.peekPrefix("/")) break :a else return error.InvalidFormat; | 198 | if (!std.mem.startsWith(u8, text[2..], "/")) return error.InvalidFormat; |
| 199 | break :a; | ||
| 158 | } | 200 | } |
| 159 | 201 | ||
| 160 | var start_of_host: usize = 0; | 202 | var start_of_host: usize = 0; |
| ... | @@ -204,16 +246,18 @@ pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { | ... | @@ -204,16 +246,18 @@ pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { |
| 204 | uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] }; | 246 | uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] }; |
| 205 | } | 247 | } |
| 206 | 248 | ||
| 207 | uri.path = .{ .percent_encoded = reader.readUntil(isPathSeparator) }; | 249 | const path_start = i; |
| 250 | i = std.mem.indexOfAnyPos(u8, text, path_start, &path_sep) orelse text.len; | ||
| 251 | uri.path = .{ .percent_encoded = text[path_start..i] }; | ||
| 208 | 252 | ||
| 209 | if ((reader.peek() orelse 0) == '?') { // query part | 253 | if (std.mem.startsWith(u8, text[i..], "?")) { |
| 210 | std.debug.assert(reader.get().? == '?'); | 254 | const query_start = i + 1; |
| 211 | uri.query = .{ .percent_encoded = reader.readUntil(isQuerySeparator) }; | 255 | i = std.mem.indexOfScalarPos(u8, text, query_start, '#') orelse text.len; |
| 256 | uri.query = .{ .percent_encoded = text[query_start..i] }; | ||
| 212 | } | 257 | } |
| 213 | 258 | ||
| 214 | if ((reader.peek() orelse 0) == '#') { // fragment part | 259 | if (std.mem.startsWith(u8, text[i..], "#")) { |
| 215 | std.debug.assert(reader.get().? == '#'); | 260 | uri.fragment = .{ .percent_encoded = text[i + 1 ..] }; |
| 216 | uri.fragment = .{ .percent_encoded = reader.readUntilEof() }; | ||
| 217 | } | 261 | } |
| 218 | 262 | ||
| 219 | return uri; | 263 | return uri; |
| ... | @@ -291,41 +335,33 @@ pub fn format(uri: Uri, bw: *std.io.BufferedWriter, comptime fmt: []const u8) st | ... | @@ -291,41 +335,33 @@ pub fn format(uri: Uri, bw: *std.io.BufferedWriter, comptime fmt: []const u8) st |
| 291 | }, bw); | 335 | }, bw); |
| 292 | } | 336 | } |
| 293 | 337 | ||
| 294 | /// Parses the URI or returns an error. | 338 | /// The return value will contain strings pointing into the original `text`. |
| 295 | /// The return value will contain strings pointing into the | 339 | /// Each component that is provided will be non-`null`. |
| 296 | /// original `text`. Each component that is provided, will be non-`null`. | ||
| 297 | pub fn parse(text: []const u8) ParseError!Uri { | 340 | pub fn parse(text: []const u8) ParseError!Uri { |
| 298 | var reader: SliceReader = .{ .slice = text }; | 341 | const end = for (text, 0..) |byte, i| { |
| 299 | const scheme = reader.readWhile(isSchemeChar); | 342 | if (!isSchemeChar(byte)) break i; |
| 300 | 343 | } else text.len; | |
| 301 | // after the scheme, a ':' must appear | 344 | // After the scheme, a ':' must appear. |
| 302 | if (reader.get()) |c| { | 345 | if (end >= text.len) return error.InvalidFormat; |
| 303 | if (c != ':') | 346 | if (text[end] != ':') return error.UnexpectedCharacter; |
| 304 | return error.UnexpectedCharacter; | 347 | return parseAfterScheme(text[0..end], text[end + 1 ..]); |
| 305 | } else { | ||
| 306 | return error.InvalidFormat; | ||
| 307 | } | ||
| 308 | |||
| 309 | return parseAfterScheme(scheme, reader.readUntilEof()); | ||
| 310 | } | 348 | } |
| 311 | 349 | ||
| 312 | pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft}; | 350 | pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft}; |
| 313 | 351 | ||
| 314 | /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. | 352 | /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. |
| 315 | /// Copies `new` to the beginning of `aux_buf.*`, allowing the slices to overlap, | 353 | /// |
| 316 | /// then parses `new` as a URI, and then resolves the path in place. | 354 | /// Assumes new location is already copied to the beginning of `aux_buf.*`. |
| 355 | /// Parses that new location as a URI, and then resolves the path in place. | ||
| 356 | /// | ||
| 317 | /// If a merge needs to take place, the newly constructed path will be stored | 357 | /// If a merge needs to take place, the newly constructed path will be stored |
| 318 | /// in `aux_buf.*` just after the copied `new`, and `aux_buf.*` will be modified | 358 | /// in `aux_buf.*` just after the copied location, and `aux_buf.*` will be |
| 319 | /// to only contain the remaining unused space. | 359 | /// modified to only contain the remaining unused space. |
| 320 | pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri { | 360 | pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceError!Uri { |
| 321 | std.mem.copyForwards(u8, aux_buf.*, new); | 361 | const new = aux_buf.*[0..new_len]; |
| 322 | // At this point, new is an invalid pointer. | 362 | const new_parsed = parse(new) catch |err| (parseAfterScheme("", new) catch return err); |
| 323 | const new_mut = aux_buf.*[0..new.len]; | 363 | aux_buf.* = aux_buf.*[new_len..]; |
| 324 | aux_buf.* = aux_buf.*[new.len..]; | 364 | // As you can see above, `new` is not a const pointer. |
| 325 | |||
| 326 | const new_parsed = parse(new_mut) catch |err| | ||
| 327 | (parseAfterScheme("", new_mut) catch return err); | ||
| 328 | // As you can see above, `new_mut` is not a const pointer. | ||
| 329 | const new_path: []u8 = @constCast(new_parsed.path.percent_encoded); | 365 | const new_path: []u8 = @constCast(new_parsed.path.percent_encoded); |
| 330 | 366 | ||
| 331 | if (new_parsed.scheme.len > 0) return .{ | 367 | if (new_parsed.scheme.len > 0) return .{ |
| ... | @@ -438,59 +474,6 @@ fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Co | ... | @@ -438,59 +474,6 @@ fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Co |
| 438 | return merged_path; | 474 | return merged_path; |
| 439 | } | 475 | } |
| 440 | 476 | ||
| 441 | const SliceReader = struct { | ||
| 442 | const Self = @This(); | ||
| 443 | |||
| 444 | slice: []const u8, | ||
| 445 | offset: usize = 0, | ||
| 446 | |||
| 447 | fn get(self: *Self) ?u8 { | ||
| 448 | if (self.offset >= self.slice.len) | ||
| 449 | return null; | ||
| 450 | const c = self.slice[self.offset]; | ||
| 451 | self.offset += 1; | ||
| 452 | return c; | ||
| 453 | } | ||
| 454 | |||
| 455 | fn peek(self: Self) ?u8 { | ||
| 456 | if (self.offset >= self.slice.len) | ||
| 457 | return null; | ||
| 458 | return self.slice[self.offset]; | ||
| 459 | } | ||
| 460 | |||
| 461 | fn readWhile(self: *Self, comptime predicate: fn (u8) bool) []const u8 { | ||
| 462 | const start = self.offset; | ||
| 463 | var end = start; | ||
| 464 | while (end < self.slice.len and predicate(self.slice[end])) { | ||
| 465 | end += 1; | ||
| 466 | } | ||
| 467 | self.offset = end; | ||
| 468 | return self.slice[start..end]; | ||
| 469 | } | ||
| 470 | |||
| 471 | fn readUntil(self: *Self, comptime predicate: fn (u8) bool) []const u8 { | ||
| 472 | const start = self.offset; | ||
| 473 | var end = start; | ||
| 474 | while (end < self.slice.len and !predicate(self.slice[end])) { | ||
| 475 | end += 1; | ||
| 476 | } | ||
| 477 | self.offset = end; | ||
| 478 | return self.slice[start..end]; | ||
| 479 | } | ||
| 480 | |||
| 481 | fn readUntilEof(self: *Self) []const u8 { | ||
| 482 | const start = self.offset; | ||
| 483 | self.offset = self.slice.len; | ||
| 484 | return self.slice[start..]; | ||
| 485 | } | ||
| 486 | |||
| 487 | fn peekPrefix(self: Self, prefix: []const u8) bool { | ||
| 488 | if (self.offset + prefix.len > self.slice.len) | ||
| 489 | return false; | ||
| 490 | return std.mem.eql(u8, self.slice[self.offset..][0..prefix.len], prefix); | ||
| 491 | } | ||
| 492 | }; | ||
| 493 | |||
| 494 | /// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) | 477 | /// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
| 495 | fn isSchemeChar(c: u8) bool { | 478 | fn isSchemeChar(c: u8) bool { |
| 496 | return switch (c) { | 479 | return switch (c) { |
| ... | @@ -499,19 +482,6 @@ fn isSchemeChar(c: u8) bool { | ... | @@ -499,19 +482,6 @@ fn isSchemeChar(c: u8) bool { |
| 499 | }; | 482 | }; |
| 500 | } | 483 | } |
| 501 | 484 | ||
| 502 | /// reserved = gen-delims / sub-delims | ||
| 503 | fn isReserved(c: u8) bool { | ||
| 504 | return isGenLimit(c) or isSubLimit(c); | ||
| 505 | } | ||
| 506 | |||
| 507 | /// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" | ||
| 508 | fn isGenLimit(c: u8) bool { | ||
| 509 | return switch (c) { | ||
| 510 | ':', ',', '?', '#', '[', ']', '@' => true, | ||
| 511 | else => false, | ||
| 512 | }; | ||
| 513 | } | ||
| 514 | |||
| 515 | /// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" | 485 | /// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
| 516 | /// / "*" / "+" / "," / ";" / "=" | 486 | /// / "*" / "+" / "," / ";" / "=" |
| 517 | fn isSubLimit(c: u8) bool { | 487 | fn isSubLimit(c: u8) bool { |
| ... | @@ -551,26 +521,8 @@ fn isQueryChar(c: u8) bool { | ... | @@ -551,26 +521,8 @@ fn isQueryChar(c: u8) bool { |
| 551 | 521 | ||
| 552 | const isFragmentChar = isQueryChar; | 522 | const isFragmentChar = isQueryChar; |
| 553 | 523 | ||
| 554 | fn isAuthoritySeparator(c: u8) bool { | 524 | const authority_sep: [3]u8 = .{ '/', '?', '#' }; |
| 555 | return switch (c) { | 525 | const path_sep: [2]u8 = .{ '?', '#' }; |
| 556 | '/', '?', '#' => true, | ||
| 557 | else => false, | ||
| 558 | }; | ||
| 559 | } | ||
| 560 | |||
| 561 | fn isPathSeparator(c: u8) bool { | ||
| 562 | return switch (c) { | ||
| 563 | '?', '#' => true, | ||
| 564 | else => false, | ||
| 565 | }; | ||
| 566 | } | ||
| 567 | |||
| 568 | fn isQuerySeparator(c: u8) bool { | ||
| 569 | return switch (c) { | ||
| 570 | '#' => true, | ||
| 571 | else => false, | ||
| 572 | }; | ||
| 573 | } | ||
| 574 | 526 | ||
| 575 | test "basic" { | 527 | test "basic" { |
| 576 | const parsed = try parse("https://ziglang.org/download"); | 528 | const parsed = try parse("https://ziglang.org/download"); |
| ... | @@ -851,7 +803,3 @@ test "URI malformed input" { | ... | @@ -851,7 +803,3 @@ test "URI malformed input" { |
| 851 | try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@[")); | 803 | try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@[")); |
| 852 | try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q")); | 804 | try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q")); |
| 853 | } | 805 | } |
| 854 | |||
| 855 | const std = @import("std.zig"); | ||
| 856 | const testing = std.testing; | ||
| 857 | const Uri = @This(); |
lib/std/http.zig+740-12| ... | @@ -1,6 +1,9 @@ | ... | @@ -1,6 +1,9 @@ |
| 1 | const builtin = @import("builtin"); | ||
| 2 | const std = @import("std.zig"); | ||
| 3 | const assert = std.debug.assert; | ||
| 4 | |||
| 1 | pub const Client = @import("http/Client.zig"); | 5 | pub const Client = @import("http/Client.zig"); |
| 2 | pub const Server = @import("http/Server.zig"); | 6 | pub const Server = @import("http/Server.zig"); |
| 3 | pub const protocol = @import("http/protocol.zig"); | ||
| 4 | pub const HeadParser = @import("http/HeadParser.zig"); | 7 | pub const HeadParser = @import("http/HeadParser.zig"); |
| 5 | pub const ChunkParser = @import("http/ChunkParser.zig"); | 8 | pub const ChunkParser = @import("http/ChunkParser.zig"); |
| 6 | pub const HeaderIterator = @import("http/HeaderIterator.zig"); | 9 | pub const HeaderIterator = @import("http/HeaderIterator.zig"); |
| ... | @@ -77,7 +80,9 @@ pub const Method = enum(u64) { | ... | @@ -77,7 +80,9 @@ pub const Method = enum(u64) { |
| 77 | }; | 80 | }; |
| 78 | } | 81 | } |
| 79 | 82 | ||
| 80 | /// 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. | 83 | /// An HTTP method is idempotent if an identical request can be made once |
| 84 | /// or several times in a row with the same effect while leaving the server | ||
| 85 | /// in the same state. | ||
| 81 | /// | 86 | /// |
| 82 | /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent | 87 | /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent |
| 83 | /// | 88 | /// |
| ... | @@ -90,7 +95,8 @@ pub const Method = enum(u64) { | ... | @@ -90,7 +95,8 @@ pub const Method = enum(u64) { |
| 90 | }; | 95 | }; |
| 91 | } | 96 | } |
| 92 | 97 | ||
| 93 | /// 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. | 98 | /// A cacheable response can be stored to be retrieved and used later, |
| 99 | /// saving a new request to the server. | ||
| 94 | /// | 100 | /// |
| 95 | /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable | 101 | /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable |
| 96 | /// | 102 | /// |
| ... | @@ -282,10 +288,10 @@ pub const Status = enum(u10) { | ... | @@ -282,10 +288,10 @@ pub const Status = enum(u10) { |
| 282 | } | 288 | } |
| 283 | }; | 289 | }; |
| 284 | 290 | ||
| 291 | /// compression is intentionally omitted here since it is handled in `ContentEncoding`. | ||
| 285 | pub const TransferEncoding = enum { | 292 | pub const TransferEncoding = enum { |
| 286 | chunked, | 293 | chunked, |
| 287 | none, | 294 | none, |
| 288 | // compression is intentionally omitted here, as std.http.Client stores it as content-encoding | ||
| 289 | }; | 295 | }; |
| 290 | 296 | ||
| 291 | pub const ContentEncoding = enum { | 297 | pub const ContentEncoding = enum { |
| ... | @@ -308,18 +314,740 @@ pub const Header = struct { | ... | @@ -308,18 +314,740 @@ pub const Header = struct { |
| 308 | value: []const u8, | 314 | value: []const u8, |
| 309 | }; | 315 | }; |
| 310 | 316 | ||
| 311 | const builtin = @import("builtin"); | 317 | pub const Reader = struct { |
| 312 | const std = @import("std.zig"); | 318 | in: *std.io.BufferedReader, |
| 319 | /// Keeps track of whether the stream is ready to accept a new request, | ||
| 320 | /// making invalid API usage cause assertion failures rather than HTTP | ||
| 321 | /// protocol violations. | ||
| 322 | state: State, | ||
| 323 | /// Number of bytes of HTTP trailers. These are at the end of a | ||
| 324 | /// transfer-encoding: chunked message. | ||
| 325 | trailers_len: usize = 0, | ||
| 326 | body_state: union { | ||
| 327 | none: void, | ||
| 328 | remaining_content_length: u64, | ||
| 329 | remaining_chunk_len: RemainingChunkLen, | ||
| 330 | }, | ||
| 331 | body_err: ?BodyError = null, | ||
| 332 | /// Stolen from `in`. | ||
| 333 | head_buffer: []u8 = &.{}, | ||
| 334 | |||
| 335 | pub const max_chunk_header_len = 22; | ||
| 336 | |||
| 337 | pub const RemainingChunkLen = enum(u64) { | ||
| 338 | head = 0, | ||
| 339 | n = 1, | ||
| 340 | rn = 2, | ||
| 341 | done = std.math.maxInt(u64), | ||
| 342 | _, | ||
| 343 | |||
| 344 | pub fn init(integer: u64) RemainingChunkLen { | ||
| 345 | return @enumFromInt(integer); | ||
| 346 | } | ||
| 347 | |||
| 348 | pub fn int(rcl: RemainingChunkLen) u64 { | ||
| 349 | return @intFromEnum(rcl); | ||
| 350 | } | ||
| 351 | }; | ||
| 352 | |||
| 353 | pub const State = enum { | ||
| 354 | /// The stream is available to be used for the first time, or reused. | ||
| 355 | ready, | ||
| 356 | receiving_head, | ||
| 357 | received_head, | ||
| 358 | receiving_body, | ||
| 359 | /// The stream would be eligible for another HTTP request, however the | ||
| 360 | /// client and server did not negotiate a persistent connection. | ||
| 361 | closing, | ||
| 362 | }; | ||
| 363 | |||
| 364 | pub const BodyError = error{ | ||
| 365 | HttpChunkInvalid, | ||
| 366 | HttpHeadersOversize, | ||
| 367 | }; | ||
| 368 | |||
| 369 | pub const HeadError = error{ | ||
| 370 | /// Too many bytes of HTTP headers. | ||
| 371 | /// | ||
| 372 | /// The HTTP specification suggests to respond with a 431 status code | ||
| 373 | /// before closing the connection. | ||
| 374 | HttpHeadersOversize, | ||
| 375 | /// Partial HTTP request was received but the connection was closed | ||
| 376 | /// before fully receiving the headers. | ||
| 377 | HttpRequestTruncated, | ||
| 378 | /// The client sent 0 bytes of headers before closing the stream. This | ||
| 379 | /// happens when a keep-alive connection is finally closed. | ||
| 380 | HttpConnectionClosing, | ||
| 381 | /// Transitive error occurred reading from `in`. | ||
| 382 | ReadFailed, | ||
| 383 | }; | ||
| 384 | |||
| 385 | /// Buffers the entire head into `head_buffer`, invalidating the previous | ||
| 386 | /// `head_buffer`, if any. | ||
| 387 | pub fn receiveHead(reader: *Reader) HeadError!void { | ||
| 388 | const in = reader.in; | ||
| 389 | in.restitute(reader.head_buffer.len); | ||
| 390 | in.rebase(); | ||
| 391 | var hp: HeadParser = .{}; | ||
| 392 | var head_end: usize = 0; | ||
| 393 | while (true) { | ||
| 394 | if (head_end >= in.buffer.len) return error.HttpHeadersOversize; | ||
| 395 | const buf = in.peekGreedy(head_end + 1) catch |err| switch (err) { | ||
| 396 | error.EndOfStream => switch (head_end) { | ||
| 397 | 0 => return error.HttpConnectionClosing, | ||
| 398 | else => return error.HttpRequestTruncated, | ||
| 399 | }, | ||
| 400 | error.ReadFailed => return error.ReadFailed, | ||
| 401 | }; | ||
| 402 | head_end += hp.feed(buf[head_end..]); | ||
| 403 | if (hp.state == .finished) { | ||
| 404 | reader.head_buffer = in.steal(head_end); | ||
| 405 | return; | ||
| 406 | } | ||
| 407 | } | ||
| 408 | } | ||
| 409 | |||
| 410 | /// Asserts only called once and after `receiveHead`. | ||
| 411 | pub fn interface(reader: *Reader, transfer_encoding: TransferEncoding, content_length: ?u64) std.io.Reader { | ||
| 412 | assert(reader.state == .received_head); | ||
| 413 | reader.state = .receiving_body; | ||
| 414 | switch (transfer_encoding) { | ||
| 415 | .chunked => { | ||
| 416 | reader.body_state = .{ .remaining_chunk_len = .head }; | ||
| 417 | return .{ | ||
| 418 | .context = reader, | ||
| 419 | .vtable = &.{ | ||
| 420 | .read = &chunkedRead, | ||
| 421 | .readVec = &chunkedReadVec, | ||
| 422 | .discard = &chunkedDiscard, | ||
| 423 | }, | ||
| 424 | }; | ||
| 425 | }, | ||
| 426 | .none => { | ||
| 427 | if (content_length) |len| { | ||
| 428 | reader.body_state = .{ .remaining_content_length = len }; | ||
| 429 | return .{ | ||
| 430 | .context = reader, | ||
| 431 | .vtable = &.{ | ||
| 432 | .read = &contentLengthRead, | ||
| 433 | .readVec = &contentLengthReadVec, | ||
| 434 | .discard = &contentLengthDiscard, | ||
| 435 | }, | ||
| 436 | }; | ||
| 437 | } else { | ||
| 438 | return reader.in.reader(); | ||
| 439 | } | ||
| 440 | }, | ||
| 441 | } | ||
| 442 | } | ||
| 443 | |||
| 444 | fn contentLengthRead( | ||
| 445 | ctx: ?*anyopaque, | ||
| 446 | bw: *std.io.BufferedWriter, | ||
| 447 | limit: std.io.Reader.Limit, | ||
| 448 | ) std.io.Reader.RwError!usize { | ||
| 449 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | ||
| 450 | const remaining_content_length = &reader.body_state.remaining_content_length; | ||
| 451 | const remaining = remaining_content_length.*; | ||
| 452 | if (remaining == 0) { | ||
| 453 | reader.state = .ready; | ||
| 454 | return error.EndOfStream; | ||
| 455 | } | ||
| 456 | const n = try reader.in.read(bw, limit.min(.limited(remaining))); | ||
| 457 | remaining_content_length.* = remaining - n; | ||
| 458 | return n; | ||
| 459 | } | ||
| 460 | |||
| 461 | fn contentLengthReadVec(context: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { | ||
| 462 | const reader: *Reader = @alignCast(@ptrCast(context)); | ||
| 463 | const remaining_content_length = &reader.body_state.remaining_content_length; | ||
| 464 | const remaining = remaining_content_length.*; | ||
| 465 | if (remaining == 0) { | ||
| 466 | reader.state = .ready; | ||
| 467 | return error.EndOfStream; | ||
| 468 | } | ||
| 469 | const n = try reader.in.readVecLimit(data, .limited(remaining)); | ||
| 470 | remaining_content_length.* = remaining - n; | ||
| 471 | return n; | ||
| 472 | } | ||
| 473 | |||
| 474 | fn contentLengthDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { | ||
| 475 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | ||
| 476 | const remaining_content_length = &reader.body_state.remaining_content_length; | ||
| 477 | const remaining = remaining_content_length.*; | ||
| 478 | if (remaining == 0) { | ||
| 479 | reader.state = .ready; | ||
| 480 | return error.EndOfStream; | ||
| 481 | } | ||
| 482 | const n = try reader.in.discard(limit.min(.limited(remaining))); | ||
| 483 | remaining_content_length.* = remaining - n; | ||
| 484 | return n; | ||
| 485 | } | ||
| 486 | |||
| 487 | fn chunkedRead( | ||
| 488 | ctx: ?*anyopaque, | ||
| 489 | bw: *std.io.BufferedWriter, | ||
| 490 | limit: std.io.Reader.Limit, | ||
| 491 | ) std.io.Reader.RwError!usize { | ||
| 492 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | ||
| 493 | const chunk_len_ptr = &reader.body_state.remaining_chunk_len; | ||
| 494 | const in = reader.in; | ||
| 495 | len: switch (chunk_len_ptr.*) { | ||
| 496 | .head => { | ||
| 497 | var cp: ChunkParser = .init; | ||
| 498 | const i = cp.feed(in.bufferContents()); | ||
| 499 | switch (cp.state) { | ||
| 500 | .invalid => return reader.failBody(error.HttpChunkInvalid), | ||
| 501 | .data => { | ||
| 502 | if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 503 | in.toss(i); | ||
| 504 | }, | ||
| 505 | else => { | ||
| 506 | try in.fill(max_chunk_header_len); | ||
| 507 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 508 | if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); | ||
| 509 | const header_len = i + next_i; | ||
| 510 | if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 511 | in.toss(header_len); | ||
| 512 | }, | ||
| 513 | } | ||
| 514 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | ||
| 515 | const n = try in.read(bw, limit.min(.limited(cp.chunk_len))); | ||
| 516 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | ||
| 517 | return n; | ||
| 518 | }, | ||
| 519 | .n => { | ||
| 520 | if ((try in.peekByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 521 | in.toss(1); | ||
| 522 | continue :len .head; | ||
| 523 | }, | ||
| 524 | .rn => { | ||
| 525 | const rn = try in.peekArray(2); | ||
| 526 | if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 527 | in.toss(2); | ||
| 528 | continue :len .head; | ||
| 529 | }, | ||
| 530 | else => |remaining_chunk_len| { | ||
| 531 | const n = try in.read(bw, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); | ||
| 532 | chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); | ||
| 533 | return n; | ||
| 534 | }, | ||
| 535 | .done => return error.EndOfStream, | ||
| 536 | } | ||
| 537 | } | ||
| 538 | |||
| 539 | fn chunkedReadVec(ctx: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { | ||
| 540 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | ||
| 541 | const chunk_len_ptr = &reader.body_state.remaining_chunk_len; | ||
| 542 | const in = reader.in; | ||
| 543 | var already_requested_more = false; | ||
| 544 | var amt_read: usize = 0; | ||
| 545 | data: for (data) |d| { | ||
| 546 | len: switch (chunk_len_ptr.*) { | ||
| 547 | .head => { | ||
| 548 | var cp: ChunkParser = .init; | ||
| 549 | const available_buffer = in.bufferContents(); | ||
| 550 | const i = cp.feed(available_buffer); | ||
| 551 | if (cp.state == .invalid) return reader.failBody(error.HttpChunkInvalid); | ||
| 552 | if (i == available_buffer.len) { | ||
| 553 | if (already_requested_more) { | ||
| 554 | chunk_len_ptr.* = .head; | ||
| 555 | return amt_read; | ||
| 556 | } | ||
| 557 | already_requested_more = true; | ||
| 558 | try in.fill(max_chunk_header_len); | ||
| 559 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 560 | if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); | ||
| 561 | const header_len = i + next_i; | ||
| 562 | if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 563 | in.toss(header_len); | ||
| 564 | } else { | ||
| 565 | if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 566 | in.toss(i); | ||
| 567 | } | ||
| 568 | if (cp.chunk_len == 0) return parseTrailers(reader, amt_read); | ||
| 569 | continue :len .init(cp.chunk_len + 2); | ||
| 570 | }, | ||
| 571 | .n => { | ||
| 572 | if (in.bufferContents().len < 1) already_requested_more = true; | ||
| 573 | if ((try in.takeByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 574 | continue :len .head; | ||
| 575 | }, | ||
| 576 | .rn => { | ||
| 577 | if (in.bufferContents().len < 2) already_requested_more = true; | ||
| 578 | const rn = try in.takeArray(2); | ||
| 579 | if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 580 | continue :len .head; | ||
| 581 | }, | ||
| 582 | else => |remaining_chunk_len| { | ||
| 583 | const available_buffer = in.bufferContents(); | ||
| 584 | const copy_len = @min(available_buffer.len, d.len, remaining_chunk_len.int() - 2); | ||
| 585 | @memcpy(d[0..copy_len], available_buffer[0..copy_len]); | ||
| 586 | amt_read += copy_len; | ||
| 587 | in.toss(copy_len); | ||
| 588 | const next_chunk_len: RemainingChunkLen = .init(remaining_chunk_len.int() - copy_len); | ||
| 589 | if (copy_len == d.len) { | ||
| 590 | chunk_len_ptr.* = next_chunk_len; | ||
| 591 | continue :data; | ||
| 592 | } | ||
| 593 | if (already_requested_more) { | ||
| 594 | chunk_len_ptr.* = next_chunk_len; | ||
| 595 | return amt_read; | ||
| 596 | } | ||
| 597 | already_requested_more = true; | ||
| 598 | try in.fill(3); | ||
| 599 | continue :len next_chunk_len; | ||
| 600 | }, | ||
| 601 | .done => return error.EndOfStream, | ||
| 602 | } | ||
| 603 | } | ||
| 604 | return amt_read; | ||
| 605 | } | ||
| 606 | |||
| 607 | fn chunkedDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { | ||
| 608 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | ||
| 609 | const chunk_len_ptr = &reader.body_state.remaining_chunk_len; | ||
| 610 | const in = reader.in; | ||
| 611 | len: switch (chunk_len_ptr.*) { | ||
| 612 | .head => { | ||
| 613 | var cp: ChunkParser = .init; | ||
| 614 | const i = cp.feed(in.bufferContents()); | ||
| 615 | switch (cp.state) { | ||
| 616 | .invalid => return reader.failBody(error.HttpChunkInvalid), | ||
| 617 | .data => { | ||
| 618 | if (i > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 619 | in.toss(i); | ||
| 620 | }, | ||
| 621 | else => { | ||
| 622 | try in.fill(max_chunk_header_len); | ||
| 623 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 624 | if (cp.state != .data) return reader.failBody(error.HttpChunkInvalid); | ||
| 625 | const header_len = i + next_i; | ||
| 626 | if (header_len > max_chunk_header_len) return reader.failBody(error.HttpChunkInvalid); | ||
| 627 | in.toss(header_len); | ||
| 628 | }, | ||
| 629 | } | ||
| 630 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | ||
| 631 | const n = try in.discard(limit.min(.limited(cp.chunk_len))); | ||
| 632 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | ||
| 633 | return n; | ||
| 634 | }, | ||
| 635 | .n => { | ||
| 636 | if ((try in.peekByte()) != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 637 | in.toss(1); | ||
| 638 | continue :len .head; | ||
| 639 | }, | ||
| 640 | .rn => { | ||
| 641 | const rn = try in.peekArray(2); | ||
| 642 | if (rn[0] != '\r' or rn[1] != '\n') return reader.failBody(error.HttpChunkInvalid); | ||
| 643 | in.toss(2); | ||
| 644 | continue :len .head; | ||
| 645 | }, | ||
| 646 | else => |remaining_chunk_len| { | ||
| 647 | const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2))); | ||
| 648 | chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); | ||
| 649 | return n; | ||
| 650 | }, | ||
| 651 | .done => return error.EndOfStream, | ||
| 652 | } | ||
| 653 | } | ||
| 654 | |||
| 655 | /// Called when next bytes in the stream are trailers, or "\r\n" to indicate | ||
| 656 | /// end of chunked body. | ||
| 657 | fn parseTrailers(reader: *Reader, amt_read: usize) std.io.Reader.Error!usize { | ||
| 658 | const in = reader.in; | ||
| 659 | var hp: HeadParser = .{}; | ||
| 660 | var trailers_len: usize = 0; | ||
| 661 | while (true) { | ||
| 662 | if (trailers_len >= in.buffer.len) return reader.failBody(error.HttpHeadersOversize); | ||
| 663 | try in.fill(trailers_len + 1); | ||
| 664 | trailers_len += hp.feed(in.bufferContents()[trailers_len..]); | ||
| 665 | if (hp.state == .finished) { | ||
| 666 | reader.body_state.remaining_chunk_len = .done; | ||
| 667 | reader.state = .ready; | ||
| 668 | reader.trailers_len = trailers_len; | ||
| 669 | return amt_read; | ||
| 670 | } | ||
| 671 | } | ||
| 672 | } | ||
| 673 | |||
| 674 | fn failBody(r: *Reader, err: BodyError) error{ReadFailed} { | ||
| 675 | r.body_err = err; | ||
| 676 | return error.ReadFailed; | ||
| 677 | } | ||
| 678 | }; | ||
| 679 | |||
| 680 | /// Request or response body. | ||
| 681 | pub const BodyWriter = struct { | ||
| 682 | /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the | ||
| 683 | /// state of this other than via methods of `BodyWriter`. | ||
| 684 | http_protocol_output: *std.io.BufferedWriter, | ||
| 685 | state: State, | ||
| 686 | elide: bool, | ||
| 687 | err: Error!void = {}, | ||
| 688 | |||
| 689 | pub const Error = error{ | ||
| 690 | /// Attempted to write a file to the stream, an expensive operation | ||
| 691 | /// that should be avoided when `elide` is true. | ||
| 692 | UnableToElideBody, | ||
| 693 | }; | ||
| 694 | pub const WriteError = std.io.Writer.Error; | ||
| 695 | |||
| 696 | /// How many zeroes to reserve for hex-encoded chunk length. | ||
| 697 | const chunk_len_digits = 8; | ||
| 698 | const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; | ||
| 699 | const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; | ||
| 700 | |||
| 701 | comptime { | ||
| 702 | assert(max_chunk_len == std.math.maxInt(u32)); | ||
| 703 | } | ||
| 704 | |||
| 705 | pub const State = union(enum) { | ||
| 706 | /// End of connection signals the end of the stream. | ||
| 707 | none, | ||
| 708 | /// As a debugging utility, counts down to zero as bytes are written. | ||
| 709 | content_length: u64, | ||
| 710 | /// Each chunk is wrapped in a header and trailer. | ||
| 711 | chunked: Chunked, | ||
| 712 | /// Cleanly finished stream; connection can be reused. | ||
| 713 | end, | ||
| 714 | |||
| 715 | pub const Chunked = union(enum) { | ||
| 716 | /// Index of the hex-encoded chunk length in the chunk header | ||
| 717 | /// within the buffer of `BodyWriter.http_protocol_output`. | ||
| 718 | offset: usize, | ||
| 719 | /// We are in the middle of a chunk and this is how many bytes are | ||
| 720 | /// left until the next header. This includes +2 for "\r"\n", and | ||
| 721 | /// is zero for the beginning of the stream. | ||
| 722 | chunk_len: usize, | ||
| 723 | |||
| 724 | pub const init: Chunked = .{ .chunk_len = 0 }; | ||
| 725 | }; | ||
| 726 | }; | ||
| 727 | |||
| 728 | /// Sends all buffered data across `BodyWriter.http_protocol_output`. | ||
| 729 | /// | ||
| 730 | /// Some buffered data will remain if transfer-encoding is chunked and the | ||
| 731 | /// BodyWriter is mid-chunk. | ||
| 732 | pub fn flush(w: *BodyWriter) WriteError!void { | ||
| 733 | switch (w.state) { | ||
| 734 | .none, .content_length => return w.http_protocol_output.flush(), | ||
| 735 | .chunked => |*chunked| switch (chunked.*) { | ||
| 736 | .offset => |*offset| { | ||
| 737 | try w.http_protocol_output.flushLimit(.limited(w.http_protocol_output.end - offset.*)); | ||
| 738 | offset.* = 0; | ||
| 739 | }, | ||
| 740 | .chunk_len => return w.http_protocol_output.flush(), | ||
| 741 | }, | ||
| 742 | } | ||
| 743 | } | ||
| 744 | |||
| 745 | /// When using content-length, asserts that the amount of data sent matches | ||
| 746 | /// the value sent in the header, then flushes. | ||
| 747 | /// | ||
| 748 | /// When using transfer-encoding: chunked, writes the end-of-stream message | ||
| 749 | /// with empty trailers, then flushes the stream to the system. Asserts any | ||
| 750 | /// started chunk has been completely finished. | ||
| 751 | /// | ||
| 752 | /// Respects the value of `elide` to omit all data after the headers. | ||
| 753 | /// | ||
| 754 | /// See also: | ||
| 755 | /// * `endUnflushed` | ||
| 756 | /// * `endChunked` | ||
| 757 | pub fn end(w: *BodyWriter) WriteError!void { | ||
| 758 | try endUnflushed(w); | ||
| 759 | try w.http_protocol_output.flush(); | ||
| 760 | } | ||
| 761 | |||
| 762 | /// When using content-length, asserts that the amount of data sent matches | ||
| 763 | /// the value sent in the header. | ||
| 764 | /// | ||
| 765 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the | ||
| 766 | /// end-of-stream message with empty trailers. | ||
| 767 | /// | ||
| 768 | /// Respects the value of `elide` to omit all data after the headers. | ||
| 769 | /// | ||
| 770 | /// See also: | ||
| 771 | /// * `end` | ||
| 772 | /// * `endChunked` | ||
| 773 | pub fn endUnflushed(w: *BodyWriter) WriteError!void { | ||
| 774 | switch (w.state) { | ||
| 775 | .content_length => |len| { | ||
| 776 | assert(len == 0); // Trips when end() called before all bytes written. | ||
| 777 | w.state = .end; | ||
| 778 | }, | ||
| 779 | .none => {}, | ||
| 780 | .chunked => return endChunked(w, .{}), | ||
| 781 | } | ||
| 782 | } | ||
| 783 | |||
| 784 | pub const EndChunkedOptions = struct { | ||
| 785 | trailers: []const Header = &.{}, | ||
| 786 | }; | ||
| 787 | |||
| 788 | /// Writes the end-of-stream message and any optional trailers. | ||
| 789 | /// | ||
| 790 | /// Does not flush. | ||
| 791 | /// | ||
| 792 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. | ||
| 793 | /// | ||
| 794 | /// Respects the value of `elide` to omit all data after the headers. | ||
| 795 | /// | ||
| 796 | /// See also: | ||
| 797 | /// * `end` | ||
| 798 | /// * `endUnflushed` | ||
| 799 | pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) WriteError!void { | ||
| 800 | const chunked = &w.state.chunked; | ||
| 801 | if (w.elide) { | ||
| 802 | w.state = .end; | ||
| 803 | return; | ||
| 804 | } | ||
| 805 | const bw = w.http_protocol_output; | ||
| 806 | switch (chunked.*) { | ||
| 807 | .offset => |offset| { | ||
| 808 | const chunk_len = bw.end - offset - chunk_header_template.len; | ||
| 809 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| 810 | try bw.writeAll("\r\n"); | ||
| 811 | }, | ||
| 812 | .chunk_len => |chunk_len| switch (chunk_len) { | ||
| 813 | 0 => {}, | ||
| 814 | 1 => try bw.writeByte('\n'), | ||
| 815 | 2 => try bw.writeAll("\r\n"), | ||
| 816 | else => unreachable, // An earlier write call indicated more data would follow. | ||
| 817 | }, | ||
| 818 | } | ||
| 819 | if (options.trailers.len > 0) { | ||
| 820 | try bw.writeAll("0\r\n"); | ||
| 821 | for (options.trailers) |trailer| { | ||
| 822 | try bw.writeAll(trailer.name); | ||
| 823 | try bw.writeAll(": "); | ||
| 824 | try bw.writeAll(trailer.value); | ||
| 825 | try bw.writeAll("\r\n"); | ||
| 826 | } | ||
| 827 | try bw.writeAll("\r\n"); | ||
| 828 | } | ||
| 829 | w.state = .end; | ||
| 830 | } | ||
| 831 | |||
| 832 | fn contentLengthWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 833 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 834 | const n = if (w.elide) countSplat(data, splat) else try w.http_protocol_output.writeSplat(data, splat); | ||
| 835 | w.state.content_length -= n; | ||
| 836 | return n; | ||
| 837 | } | ||
| 838 | |||
| 839 | fn noneWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 840 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 841 | if (w.elide) return countSplat(data, splat); | ||
| 842 | return w.http_protocol_output.writeSplat(data, splat); | ||
| 843 | } | ||
| 844 | |||
| 845 | fn countSplat(data: []const []const u8, splat: usize) usize { | ||
| 846 | if (data.len == 0) return 0; | ||
| 847 | var total: usize = 0; | ||
| 848 | for (data[0 .. data.len - 1]) |buf| total += buf.len; | ||
| 849 | total += data[data.len - 1].len * splat; | ||
| 850 | return total; | ||
| 851 | } | ||
| 852 | |||
| 853 | fn elideWriteFile( | ||
| 854 | w: *BodyWriter, | ||
| 855 | offset: std.io.Writer.Offset, | ||
| 856 | limit: std.io.Writer.Limit, | ||
| 857 | headers_and_trailers: []const []const u8, | ||
| 858 | ) WriteError!usize { | ||
| 859 | if (offset != .none) { | ||
| 860 | if (countWriteFile(limit, headers_and_trailers)) |n| { | ||
| 861 | return n; | ||
| 862 | } | ||
| 863 | } | ||
| 864 | w.err = error.UnableToElideBody; | ||
| 865 | return error.WriteFailed; | ||
| 866 | } | ||
| 867 | |||
| 868 | /// Returns `null` if size cannot be computed without making any syscalls. | ||
| 869 | fn countWriteFile(limit: std.io.Writer.Limit, headers_and_trailers: []const []const u8) ?usize { | ||
| 870 | var total: usize = limit.toInt() orelse return null; | ||
| 871 | for (headers_and_trailers) |buf| total += buf.len; | ||
| 872 | return total; | ||
| 873 | } | ||
| 874 | |||
| 875 | fn noneWriteFile( | ||
| 876 | context: ?*anyopaque, | ||
| 877 | file: std.fs.File, | ||
| 878 | offset: std.io.Writer.Offset, | ||
| 879 | limit: std.io.Writer.Limit, | ||
| 880 | headers_and_trailers: []const []const u8, | ||
| 881 | headers_len: usize, | ||
| 882 | ) std.io.Writer.FileError!usize { | ||
| 883 | if (limit == .nothing) return noneWriteSplat(context, headers_and_trailers, 1); | ||
| 884 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 885 | if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); | ||
| 886 | return w.http_protocol_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 887 | } | ||
| 888 | |||
| 889 | fn contentLengthWriteFile( | ||
| 890 | context: ?*anyopaque, | ||
| 891 | file: std.fs.File, | ||
| 892 | offset: std.io.Writer.Offset, | ||
| 893 | limit: std.io.Writer.Limit, | ||
| 894 | headers_and_trailers: []const []const u8, | ||
| 895 | headers_len: usize, | ||
| 896 | ) std.io.Writer.FileError!usize { | ||
| 897 | if (limit == .nothing) return contentLengthWriteSplat(context, headers_and_trailers, 1); | ||
| 898 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 899 | if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); | ||
| 900 | const n = try w.http_protocol_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 901 | w.state.content_length -= n; | ||
| 902 | return n; | ||
| 903 | } | ||
| 904 | |||
| 905 | fn chunkedWriteFile( | ||
| 906 | context: ?*anyopaque, | ||
| 907 | file: std.fs.File, | ||
| 908 | offset: std.io.Writer.Offset, | ||
| 909 | limit: std.io.Writer.Limit, | ||
| 910 | headers_and_trailers: []const []const u8, | ||
| 911 | headers_len: usize, | ||
| 912 | ) std.io.Writer.FileError!usize { | ||
| 913 | if (limit == .nothing) return chunkedWriteSplat(context, headers_and_trailers, 1); | ||
| 914 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 915 | if (w.elide) return elideWriteFile(w, offset, limit, headers_and_trailers); | ||
| 916 | const data_len = countWriteFile(limit, headers_and_trailers) orelse @panic("TODO"); | ||
| 917 | const bw = w.http_protocol_output; | ||
| 918 | const chunked = &w.state.chunked; | ||
| 919 | state: switch (chunked.*) { | ||
| 920 | .offset => |off| { | ||
| 921 | // TODO: is it better perf to read small files into the buffer? | ||
| 922 | const buffered_len = bw.end - off - chunk_header_template.len; | ||
| 923 | const chunk_len = data_len + buffered_len; | ||
| 924 | writeHex(bw.buffer[off..][0..chunk_len_digits], chunk_len); | ||
| 925 | const n = try bw.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 926 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| 927 | return n; | ||
| 928 | }, | ||
| 929 | .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 930 | 0 => { | ||
| 931 | const header_buf = try bw.writableArray(chunk_header_template.len); | ||
| 932 | const off = bw.end; | ||
| 933 | @memcpy(header_buf, chunk_header_template); | ||
| 934 | chunked.* = .{ .offset = off }; | ||
| 935 | continue :state .{ .offset = off }; | ||
| 936 | }, | ||
| 937 | 1 => { | ||
| 938 | try bw.writeByte('\n'); | ||
| 939 | chunked.chunk_len = 0; | ||
| 940 | continue :l 0; | ||
| 941 | }, | ||
| 942 | 2 => { | ||
| 943 | try bw.writeByte('\r'); | ||
| 944 | chunked.chunk_len = 1; | ||
| 945 | continue :l 1; | ||
| 946 | }, | ||
| 947 | else => { | ||
| 948 | const new_limit = limit.min(.limited(chunk_len - 2)); | ||
| 949 | const n = try bw.writeFile(file, offset, new_limit, headers_and_trailers, headers_len); | ||
| 950 | chunked.chunk_len = chunk_len - n; | ||
| 951 | return n; | ||
| 952 | }, | ||
| 953 | }, | ||
| 954 | } | ||
| 955 | } | ||
| 956 | |||
| 957 | fn chunkedWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 958 | const w: *BodyWriter = @alignCast(@ptrCast(context)); | ||
| 959 | const data_len = countSplat(data, splat); | ||
| 960 | if (w.elide) return data_len; | ||
| 961 | |||
| 962 | const bw = w.http_protocol_output; | ||
| 963 | const chunked = &w.state.chunked; | ||
| 964 | |||
| 965 | state: switch (chunked.*) { | ||
| 966 | .offset => |offset| { | ||
| 967 | if (bw.unusedCapacitySlice().len >= data_len) { | ||
| 968 | assert(data_len == (bw.writeSplat(data, splat) catch unreachable)); | ||
| 969 | return data_len; | ||
| 970 | } | ||
| 971 | const buffered_len = bw.end - offset - chunk_header_template.len; | ||
| 972 | const chunk_len = data_len + buffered_len; | ||
| 973 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| 974 | const n = try bw.writeSplat(data, splat); | ||
| 975 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| 976 | return n; | ||
| 977 | }, | ||
| 978 | .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 979 | 0 => { | ||
| 980 | const header_buf = try bw.writableArray(chunk_header_template.len); | ||
| 981 | const offset = bw.end; | ||
| 982 | @memcpy(header_buf, chunk_header_template); | ||
| 983 | chunked.* = .{ .offset = offset }; | ||
| 984 | continue :state .{ .offset = offset }; | ||
| 985 | }, | ||
| 986 | 1 => { | ||
| 987 | try bw.writeByte('\n'); | ||
| 988 | chunked.chunk_len = 0; | ||
| 989 | continue :l 0; | ||
| 990 | }, | ||
| 991 | 2 => { | ||
| 992 | try bw.writeByte('\r'); | ||
| 993 | chunked.chunk_len = 1; | ||
| 994 | continue :l 1; | ||
| 995 | }, | ||
| 996 | else => { | ||
| 997 | const n = try bw.writeSplatLimit(data, splat, .limited(chunk_len - 2)); | ||
| 998 | chunked.chunk_len = chunk_len - n; | ||
| 999 | return n; | ||
| 1000 | }, | ||
| 1001 | }, | ||
| 1002 | } | ||
| 1003 | } | ||
| 1004 | |||
| 1005 | /// Writes an integer as base 16 to `buf`, right-aligned, assuming the | ||
| 1006 | /// buffer has already been filled with zeroes. | ||
| 1007 | fn writeHex(buf: []u8, x: usize) void { | ||
| 1008 | assert(std.mem.allEqual(u8, buf, '0')); | ||
| 1009 | const base = 16; | ||
| 1010 | var index: usize = buf.len; | ||
| 1011 | var a = x; | ||
| 1012 | while (a > 0) { | ||
| 1013 | const digit = a % base; | ||
| 1014 | index -= 1; | ||
| 1015 | buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); | ||
| 1016 | a /= base; | ||
| 1017 | } | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | pub fn interface(w: *BodyWriter) std.io.Writer { | ||
| 1021 | return .{ | ||
| 1022 | .context = w, | ||
| 1023 | .vtable = switch (w.state) { | ||
| 1024 | .none => &.{ | ||
| 1025 | .writeSplat = noneWriteSplat, | ||
| 1026 | .writeFile = noneWriteFile, | ||
| 1027 | }, | ||
| 1028 | .content_length => &.{ | ||
| 1029 | .writeSplat = contentLengthWriteSplat, | ||
| 1030 | .writeFile = contentLengthWriteFile, | ||
| 1031 | }, | ||
| 1032 | .chunked => &.{ | ||
| 1033 | .writeSplat = chunkedWriteSplat, | ||
| 1034 | .writeFile = chunkedWriteFile, | ||
| 1035 | }, | ||
| 1036 | }, | ||
| 1037 | }; | ||
| 1038 | } | ||
| 1039 | }; | ||
| 313 | 1040 | ||
| 314 | test { | 1041 | test { |
| 1042 | _ = Server; | ||
| 1043 | _ = Status; | ||
| 1044 | _ = Method; | ||
| 1045 | _ = ChunkParser; | ||
| 1046 | _ = HeadParser; | ||
| 1047 | _ = WebSocket; | ||
| 1048 | |||
| 315 | if (builtin.os.tag != .wasi) { | 1049 | if (builtin.os.tag != .wasi) { |
| 316 | _ = Client; | 1050 | _ = Client; |
| 317 | _ = Method; | ||
| 318 | _ = Server; | ||
| 319 | _ = Status; | ||
| 320 | _ = HeadParser; | ||
| 321 | _ = ChunkParser; | ||
| 322 | _ = WebSocket; | ||
| 323 | _ = @import("http/test.zig"); | 1051 | _ = @import("http/test.zig"); |
| 324 | } | 1052 | } |
| 325 | } | 1053 | } |
lib/std/http/ChunkParser.zig+3-3| ... | @@ -1,5 +1,8 @@ | ... | @@ -1,5 +1,8 @@ |
| 1 | //! Parser for transfer-encoding: chunked. | 1 | //! Parser for transfer-encoding: chunked. |
| 2 | 2 | ||
| 3 | const ChunkParser = @This(); | ||
| 4 | const std = @import("std"); | ||
| 5 | |||
| 3 | state: State, | 6 | state: State, |
| 4 | chunk_len: u64, | 7 | chunk_len: u64, |
| 5 | 8 | ||
| ... | @@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize { | ... | @@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize { |
| 97 | return bytes.len; | 100 | return bytes.len; |
| 98 | } | 101 | } |
| 99 | 102 | ||
| 100 | const ChunkParser = @This(); | ||
| 101 | const std = @import("std"); | ||
| 102 | |||
| 103 | test feed { | 103 | test feed { |
| 104 | const testing = std.testing; | 104 | const testing = std.testing; |
| 105 | 105 |
lib/std/http/Client.zig+487-661| ... | @@ -15,7 +15,6 @@ const Allocator = mem.Allocator; | ... | @@ -15,7 +15,6 @@ const Allocator = mem.Allocator; |
| 15 | const assert = std.debug.assert; | 15 | const assert = std.debug.assert; |
| 16 | 16 | ||
| 17 | const Client = @This(); | 17 | const Client = @This(); |
| 18 | const proto = @import("protocol.zig"); | ||
| 19 | 18 | ||
| 20 | pub const disable_tls = std.options.http_disable_tls; | 19 | pub const disable_tls = std.options.http_disable_tls; |
| 21 | 20 | ||
| ... | @@ -68,7 +67,7 @@ pub const ConnectionPool = struct { | ... | @@ -68,7 +67,7 @@ pub const ConnectionPool = struct { |
| 68 | pub const Criteria = struct { | 67 | pub const Criteria = struct { |
| 69 | host: []const u8, | 68 | host: []const u8, |
| 70 | port: u16, | 69 | port: u16, |
| 71 | protocol: Connection.Protocol, | 70 | protocol: Protocol, |
| 72 | }; | 71 | }; |
| 73 | 72 | ||
| 74 | /// Finds and acquires a connection from the connection pool matching the criteria. | 73 | /// Finds and acquires a connection from the connection pool matching the criteria. |
| ... | @@ -201,6 +200,32 @@ pub const ConnectionPool = struct { | ... | @@ -201,6 +200,32 @@ pub const ConnectionPool = struct { |
| 201 | } | 200 | } |
| 202 | }; | 201 | }; |
| 203 | 202 | ||
| 203 | pub const Protocol = enum { | ||
| 204 | plain, | ||
| 205 | tls, | ||
| 206 | |||
| 207 | fn port(protocol: Protocol) u16 { | ||
| 208 | return switch (protocol) { | ||
| 209 | .plain => 80, | ||
| 210 | .tls => 443, | ||
| 211 | }; | ||
| 212 | } | ||
| 213 | |||
| 214 | pub fn fromScheme(scheme: []const u8) ?Protocol { | ||
| 215 | const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ | ||
| 216 | .{ "http", .plain }, | ||
| 217 | .{ "ws", .plain }, | ||
| 218 | .{ "https", .tls }, | ||
| 219 | .{ "wss", .tls }, | ||
| 220 | }); | ||
| 221 | return protocol_map.get(scheme); | ||
| 222 | } | ||
| 223 | |||
| 224 | pub fn fromUri(uri: Uri) ?Protocol { | ||
| 225 | return fromScheme(uri.scheme); | ||
| 226 | } | ||
| 227 | }; | ||
| 228 | |||
| 204 | pub const Connection = struct { | 229 | pub const Connection = struct { |
| 205 | client: *Client, | 230 | client: *Client, |
| 206 | stream: net.Stream, | 231 | stream: net.Stream, |
| ... | @@ -215,8 +240,6 @@ pub const Connection = struct { | ... | @@ -215,8 +240,6 @@ pub const Connection = struct { |
| 215 | closing: bool, | 240 | closing: bool, |
| 216 | protocol: Protocol, | 241 | protocol: Protocol, |
| 217 | 242 | ||
| 218 | pub const Protocol = enum { plain, tls }; | ||
| 219 | |||
| 220 | const Plain = struct { | 243 | const Plain = struct { |
| 221 | /// Data from `Connection.stream`. | 244 | /// Data from `Connection.stream`. |
| 222 | reader: std.io.BufferedReader, | 245 | reader: std.io.BufferedReader, |
| ... | @@ -411,13 +434,6 @@ pub const Connection = struct { | ... | @@ -411,13 +434,6 @@ pub const Connection = struct { |
| 411 | } | 434 | } |
| 412 | }; | 435 | }; |
| 413 | 436 | ||
| 414 | /// The mode of transport for requests. | ||
| 415 | pub const RequestTransfer = union(enum) { | ||
| 416 | content_length: u64, | ||
| 417 | chunked: void, | ||
| 418 | none: void, | ||
| 419 | }; | ||
| 420 | |||
| 421 | /// The decompressor for response messages. | 437 | /// The decompressor for response messages. |
| 422 | pub const Compression = union(enum) { | 438 | pub const Compression = union(enum) { |
| 423 | pub const DeflateDecompressor = std.compress.zlib.Decompressor; | 439 | pub const DeflateDecompressor = std.compress.zlib.Decompressor; |
| ... | @@ -432,281 +448,278 @@ pub const Compression = union(enum) { | ... | @@ -432,281 +448,278 @@ pub const Compression = union(enum) { |
| 432 | none: void, | 448 | none: void, |
| 433 | }; | 449 | }; |
| 434 | 450 | ||
| 435 | /// A HTTP response originating from a server. | ||
| 436 | pub const Response = struct { | 451 | pub const Response = struct { |
| 437 | version: http.Version, | 452 | request: *Request, |
| 438 | status: http.Status, | 453 | /// Pointers in this struct are invalidated with the next call to |
| 439 | reason: []const u8, | 454 | /// `receiveHead`. |
| 440 | 455 | head: Head, | |
| 441 | /// Points into the user-provided `server_header_buffer`. | 456 | |
| 442 | location: ?[]const u8 = null, | 457 | pub const Head = struct { |
| 443 | /// Points into the user-provided `server_header_buffer`. | 458 | bytes: []const u8, |
| 444 | content_type: ?[]const u8 = null, | 459 | version: http.Version, |
| 445 | /// Points into the user-provided `server_header_buffer`. | 460 | status: http.Status, |
| 446 | content_disposition: ?[]const u8 = null, | 461 | reason: []const u8, |
| 447 | 462 | location: ?[]const u8 = null, | |
| 448 | keep_alive: bool, | 463 | content_type: ?[]const u8 = null, |
| 449 | 464 | content_disposition: ?[]const u8 = null, | |
| 450 | /// If present, the number of bytes in the response body. | 465 | |
| 451 | content_length: ?u64 = null, | 466 | keep_alive: bool, |
| 467 | |||
| 468 | /// If present, the number of bytes in the response body. | ||
| 469 | content_length: ?u64 = null, | ||
| 470 | |||
| 471 | transfer_encoding: http.TransferEncoding = .none, | ||
| 472 | transfer_compression: http.ContentEncoding = .identity, | ||
| 473 | |||
| 474 | compression: Compression = .none, | ||
| 475 | |||
| 476 | pub const ParseError = error{ | ||
| 477 | HttpHeadersInvalid, | ||
| 478 | HttpHeaderContinuationsUnsupported, | ||
| 479 | HttpTransferEncodingUnsupported, | ||
| 480 | HttpConnectionHeaderUnsupported, | ||
| 481 | InvalidContentLength, | ||
| 482 | CompressionUnsupported, | ||
| 483 | }; | ||
| 452 | 484 | ||
| 453 | /// If present, the transfer encoding of the response body, otherwise none. | 485 | pub fn parse(bytes: []const u8) ParseError!Head { |
| 454 | transfer_encoding: http.TransferEncoding = .none, | 486 | var res: Head = .{ |
| 487 | .bytes = bytes, | ||
| 488 | .status = undefined, | ||
| 489 | .reason = undefined, | ||
| 490 | .version = undefined, | ||
| 491 | .keep_alive = false, | ||
| 492 | }; | ||
| 493 | var it = mem.splitSequence(u8, bytes, "\r\n"); | ||
| 455 | 494 | ||
| 456 | /// If present, the compression of the response body, otherwise identity (no compression). | 495 | const first_line = it.next().?; |
| 457 | transfer_compression: http.ContentEncoding = .identity, | 496 | if (first_line.len < 12) { |
| 497 | return error.HttpHeadersInvalid; | ||
| 498 | } | ||
| 458 | 499 | ||
| 459 | parser: proto.HeadersParser, | 500 | const version: http.Version = switch (int64(first_line[0..8])) { |
| 460 | compression: Compression = .none, | 501 | int64("HTTP/1.0") => .@"HTTP/1.0", |
| 502 | int64("HTTP/1.1") => .@"HTTP/1.1", | ||
| 503 | else => return error.HttpHeadersInvalid, | ||
| 504 | }; | ||
| 505 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | ||
| 506 | const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); | ||
| 507 | const reason = mem.trimLeft(u8, first_line[12..], " "); | ||
| 508 | |||
| 509 | res.version = version; | ||
| 510 | res.status = status; | ||
| 511 | res.reason = reason; | ||
| 512 | res.keep_alive = switch (version) { | ||
| 513 | .@"HTTP/1.0" => false, | ||
| 514 | .@"HTTP/1.1" => true, | ||
| 515 | }; | ||
| 461 | 516 | ||
| 462 | /// Whether the response body should be skipped. Any data read from the | 517 | while (it.next()) |line| { |
| 463 | /// response body will be discarded. | 518 | if (line.len == 0) return res; |
| 464 | skip: bool = false, | 519 | switch (line[0]) { |
| 520 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | ||
| 521 | else => {}, | ||
| 522 | } | ||
| 465 | 523 | ||
| 466 | pub const ParseError = error{ | 524 | var line_it = mem.splitScalar(u8, line, ':'); |
| 467 | HttpHeadersInvalid, | 525 | const header_name = line_it.next().?; |
| 468 | HttpHeaderContinuationsUnsupported, | 526 | const header_value = mem.trim(u8, line_it.rest(), " \t"); |
| 469 | HttpTransferEncodingUnsupported, | 527 | if (header_name.len == 0) return error.HttpHeadersInvalid; |
| 470 | HttpConnectionHeaderUnsupported, | 528 | |
| 471 | InvalidContentLength, | 529 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| 472 | CompressionUnsupported, | 530 | res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); |
| 473 | }; | 531 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { |
| 532 | res.content_type = header_value; | ||
| 533 | } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { | ||
| 534 | res.location = header_value; | ||
| 535 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { | ||
| 536 | res.content_disposition = header_value; | ||
| 537 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 538 | // Transfer-Encoding: second, first | ||
| 539 | // Transfer-Encoding: deflate, chunked | ||
| 540 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | ||
| 541 | |||
| 542 | const first = iter.first(); | ||
| 543 | const trimmed_first = mem.trim(u8, first, " "); | ||
| 544 | |||
| 545 | var next: ?[]const u8 = first; | ||
| 546 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { | ||
| 547 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding | ||
| 548 | res.transfer_encoding = transfer; | ||
| 549 | |||
| 550 | next = iter.next(); | ||
| 551 | } | ||
| 474 | 552 | ||
| 475 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { | 553 | if (next) |second| { |
| 476 | var it = mem.splitSequence(u8, bytes, "\r\n"); | 554 | const trimmed_second = mem.trim(u8, second, " "); |
| 477 | 555 | ||
| 478 | const first_line = it.next().?; | 556 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { |
| 479 | if (first_line.len < 12) { | 557 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported |
| 480 | return error.HttpHeadersInvalid; | 558 | res.transfer_compression = transfer; |
| 481 | } | 559 | } else { |
| 560 | return error.HttpTransferEncodingUnsupported; | ||
| 561 | } | ||
| 562 | } | ||
| 482 | 563 | ||
| 483 | const version: http.Version = switch (int64(first_line[0..8])) { | 564 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| 484 | int64("HTTP/1.0") => .@"HTTP/1.0", | 565 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 485 | int64("HTTP/1.1") => .@"HTTP/1.1", | 566 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; |
| 486 | else => return error.HttpHeadersInvalid, | ||
| 487 | }; | ||
| 488 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | ||
| 489 | const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); | ||
| 490 | const reason = mem.trimStart(u8, first_line[12..], " "); | ||
| 491 | |||
| 492 | res.version = version; | ||
| 493 | res.status = status; | ||
| 494 | res.reason = reason; | ||
| 495 | res.keep_alive = switch (version) { | ||
| 496 | .@"HTTP/1.0" => false, | ||
| 497 | .@"HTTP/1.1" => true, | ||
| 498 | }; | ||
| 499 | 567 | ||
| 500 | while (it.next()) |line| { | 568 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; |
| 501 | if (line.len == 0) return; | ||
| 502 | switch (line[0]) { | ||
| 503 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | ||
| 504 | else => {}, | ||
| 505 | } | ||
| 506 | 569 | ||
| 507 | var line_it = mem.splitScalar(u8, line, ':'); | 570 | res.content_length = content_length; |
| 508 | const header_name = line_it.next().?; | 571 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 509 | const header_value = mem.trim(u8, line_it.rest(), " \t"); | 572 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; |
| 510 | if (header_name.len == 0) return error.HttpHeadersInvalid; | ||
| 511 | |||
| 512 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | ||
| 513 | res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); | ||
| 514 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { | ||
| 515 | res.content_type = header_value; | ||
| 516 | } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { | ||
| 517 | res.location = header_value; | ||
| 518 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { | ||
| 519 | res.content_disposition = header_value; | ||
| 520 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | ||
| 521 | // Transfer-Encoding: second, first | ||
| 522 | // Transfer-Encoding: deflate, chunked | ||
| 523 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | ||
| 524 | |||
| 525 | const first = iter.first(); | ||
| 526 | const trimmed_first = mem.trim(u8, first, " "); | ||
| 527 | |||
| 528 | var next: ?[]const u8 = first; | ||
| 529 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { | ||
| 530 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding | ||
| 531 | res.transfer_encoding = transfer; | ||
| 532 | |||
| 533 | next = iter.next(); | ||
| 534 | } | ||
| 535 | 573 | ||
| 536 | if (next) |second| { | 574 | const trimmed = mem.trim(u8, header_value, " "); |
| 537 | const trimmed_second = mem.trim(u8, second, " "); | ||
| 538 | 575 | ||
| 539 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { | 576 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 540 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported | 577 | res.transfer_compression = ce; |
| 541 | res.transfer_compression = transfer; | ||
| 542 | } else { | 578 | } else { |
| 543 | return error.HttpTransferEncodingUnsupported; | 579 | return error.HttpTransferEncodingUnsupported; |
| 544 | } | 580 | } |
| 545 | } | 581 | } |
| 546 | |||
| 547 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | ||
| 548 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | ||
| 549 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | ||
| 550 | |||
| 551 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; | ||
| 552 | |||
| 553 | res.content_length = content_length; | ||
| 554 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | ||
| 555 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; | ||
| 556 | |||
| 557 | const trimmed = mem.trim(u8, header_value, " "); | ||
| 558 | |||
| 559 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | ||
| 560 | res.transfer_compression = ce; | ||
| 561 | } else { | ||
| 562 | return error.HttpTransferEncodingUnsupported; | ||
| 563 | } | ||
| 564 | } | 582 | } |
| 583 | return error.HttpHeadersInvalid; // missing empty line | ||
| 565 | } | 584 | } |
| 566 | return error.HttpHeadersInvalid; // missing empty line | ||
| 567 | } | ||
| 568 | |||
| 569 | test parse { | ||
| 570 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | ||
| 571 | "LOcation:url\r\n" ++ | ||
| 572 | "content-tYpe: text/plain\r\n" ++ | ||
| 573 | "content-disposition:attachment; filename=example.txt \r\n" ++ | ||
| 574 | "content-Length:10\r\n" ++ | ||
| 575 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | ||
| 576 | "connectioN:\t keep-alive \r\n\r\n"; | ||
| 577 | |||
| 578 | var header_buffer: [1024]u8 = undefined; | ||
| 579 | var res = Response{ | ||
| 580 | .status = undefined, | ||
| 581 | .reason = undefined, | ||
| 582 | .version = undefined, | ||
| 583 | .keep_alive = false, | ||
| 584 | .parser = .init(&header_buffer), | ||
| 585 | }; | ||
| 586 | 585 | ||
| 587 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); | 586 | test parse { |
| 588 | res.parser.header_bytes_len = response_bytes.len; | 587 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ |
| 589 | 588 | "LOcation:url\r\n" ++ | |
| 590 | try res.parse(response_bytes); | 589 | "content-tYpe: text/plain\r\n" ++ |
| 591 | 590 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 592 | try testing.expectEqual(.@"HTTP/1.1", res.version); | 591 | "content-Length:10\r\n" ++ |
| 593 | try testing.expectEqualStrings("OK", res.reason); | 592 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ |
| 594 | try testing.expectEqual(.ok, res.status); | 593 | "connectioN:\t keep-alive \r\n\r\n"; |
| 595 | 594 | ||
| 596 | try testing.expectEqualStrings("url", res.location.?); | 595 | const head = Head.parse(response_bytes); |
| 597 | try testing.expectEqualStrings("text/plain", res.content_type.?); | 596 | |
| 598 | try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?); | 597 | try testing.expectEqual(.@"HTTP/1.1", head.version); |
| 599 | 598 | try testing.expectEqualStrings("OK", head.reason); | |
| 600 | try testing.expectEqual(true, res.keep_alive); | 599 | try testing.expectEqual(.ok, head.status); |
| 601 | try testing.expectEqual(10, res.content_length.?); | 600 | |
| 602 | try testing.expectEqual(.chunked, res.transfer_encoding); | 601 | try testing.expectEqualStrings("url", head.location.?); |
| 603 | try testing.expectEqual(.deflate, res.transfer_compression); | 602 | try testing.expectEqualStrings("text/plain", head.content_type.?); |
| 604 | } | 603 | try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); |
| 605 | 604 | ||
| 606 | inline fn int64(array: *const [8]u8) u64 { | 605 | try testing.expectEqual(true, head.keep_alive); |
| 607 | return @bitCast(array.*); | 606 | try testing.expectEqual(10, head.content_length.?); |
| 608 | } | 607 | try testing.expectEqual(.chunked, head.transfer_encoding); |
| 609 | 608 | try testing.expectEqual(.deflate, head.transfer_compression); | |
| 610 | fn parseInt3(text: *const [3]u8) u10 { | 609 | } |
| 611 | const nnn: @Vector(3, u8) = text.*; | ||
| 612 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | ||
| 613 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | ||
| 614 | return @reduce(.Add, (nnn -% zero) *% mmm); | ||
| 615 | } | ||
| 616 | |||
| 617 | test parseInt3 { | ||
| 618 | const expectEqual = testing.expectEqual; | ||
| 619 | try expectEqual(@as(u10, 0), parseInt3("000")); | ||
| 620 | try expectEqual(@as(u10, 418), parseInt3("418")); | ||
| 621 | try expectEqual(@as(u10, 999), parseInt3("999")); | ||
| 622 | } | ||
| 623 | 610 | ||
| 624 | pub fn iterateHeaders(r: Response) http.HeaderIterator { | 611 | pub fn iterateHeaders(h: Head) http.HeaderIterator { |
| 625 | return .init(r.parser.get()); | 612 | return .init(h.bytes); |
| 626 | } | 613 | } |
| 627 | 614 | ||
| 628 | test iterateHeaders { | 615 | test iterateHeaders { |
| 629 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | 616 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ |
| 630 | "LOcation:url\r\n" ++ | 617 | "LOcation:url\r\n" ++ |
| 631 | "content-tYpe: text/plain\r\n" ++ | 618 | "content-tYpe: text/plain\r\n" ++ |
| 632 | "content-disposition:attachment; filename=example.txt \r\n" ++ | 619 | "content-disposition:attachment; filename=example.txt \r\n" ++ |
| 633 | "content-Length:10\r\n" ++ | 620 | "content-Length:10\r\n" ++ |
| 634 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | 621 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ |
| 635 | "connectioN:\t keep-alive \r\n\r\n"; | 622 | "connectioN:\t keep-alive \r\n\r\n"; |
| 636 | 623 | ||
| 637 | var header_buffer: [1024]u8 = undefined; | 624 | var header_buffer: [1024]u8 = undefined; |
| 638 | var res = Response{ | 625 | var res = Response{ |
| 639 | .status = undefined, | 626 | .status = undefined, |
| 640 | .reason = undefined, | 627 | .reason = undefined, |
| 641 | .version = undefined, | 628 | .version = undefined, |
| 642 | .keep_alive = false, | 629 | .keep_alive = false, |
| 643 | .parser = .init(&header_buffer), | 630 | .parser = .init(&header_buffer), |
| 644 | }; | 631 | }; |
| 645 | 632 | ||
| 646 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); | 633 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); |
| 647 | res.parser.header_bytes_len = response_bytes.len; | 634 | res.parser.header_bytes_len = response_bytes.len; |
| 648 | 635 | ||
| 649 | var it = res.iterateHeaders(); | 636 | var it = res.iterateHeaders(); |
| 650 | { | 637 | { |
| 651 | const header = it.next().?; | 638 | const header = it.next().?; |
| 652 | try testing.expectEqualStrings("LOcation", header.name); | 639 | try testing.expectEqualStrings("LOcation", header.name); |
| 653 | try testing.expectEqualStrings("url", header.value); | 640 | try testing.expectEqualStrings("url", header.value); |
| 654 | try testing.expect(!it.is_trailer); | 641 | try testing.expect(!it.is_trailer); |
| 655 | } | 642 | } |
| 656 | { | 643 | { |
| 657 | const header = it.next().?; | 644 | const header = it.next().?; |
| 658 | try testing.expectEqualStrings("content-tYpe", header.name); | 645 | try testing.expectEqualStrings("content-tYpe", header.name); |
| 659 | try testing.expectEqualStrings("text/plain", header.value); | 646 | try testing.expectEqualStrings("text/plain", header.value); |
| 660 | try testing.expect(!it.is_trailer); | 647 | try testing.expect(!it.is_trailer); |
| 661 | } | 648 | } |
| 662 | { | 649 | { |
| 663 | const header = it.next().?; | 650 | const header = it.next().?; |
| 664 | try testing.expectEqualStrings("content-disposition", header.name); | 651 | try testing.expectEqualStrings("content-disposition", header.name); |
| 665 | try testing.expectEqualStrings("attachment; filename=example.txt", header.value); | 652 | try testing.expectEqualStrings("attachment; filename=example.txt", header.value); |
| 666 | try testing.expect(!it.is_trailer); | 653 | try testing.expect(!it.is_trailer); |
| 654 | } | ||
| 655 | { | ||
| 656 | const header = it.next().?; | ||
| 657 | try testing.expectEqualStrings("content-Length", header.name); | ||
| 658 | try testing.expectEqualStrings("10", header.value); | ||
| 659 | try testing.expect(!it.is_trailer); | ||
| 660 | } | ||
| 661 | { | ||
| 662 | const header = it.next().?; | ||
| 663 | try testing.expectEqualStrings("TRansfer-encoding", header.name); | ||
| 664 | try testing.expectEqualStrings("deflate, chunked", header.value); | ||
| 665 | try testing.expect(!it.is_trailer); | ||
| 666 | } | ||
| 667 | { | ||
| 668 | const header = it.next().?; | ||
| 669 | try testing.expectEqualStrings("connectioN", header.name); | ||
| 670 | try testing.expectEqualStrings("keep-alive", header.value); | ||
| 671 | try testing.expect(!it.is_trailer); | ||
| 672 | } | ||
| 673 | try testing.expectEqual(null, it.next()); | ||
| 667 | } | 674 | } |
| 668 | { | 675 | |
| 669 | const header = it.next().?; | 676 | inline fn int64(array: *const [8]u8) u64 { |
| 670 | try testing.expectEqualStrings("content-Length", header.name); | 677 | return @bitCast(array.*); |
| 671 | try testing.expectEqualStrings("10", header.value); | ||
| 672 | try testing.expect(!it.is_trailer); | ||
| 673 | } | 678 | } |
| 674 | { | 679 | |
| 675 | const header = it.next().?; | 680 | fn parseInt3(text: *const [3]u8) u10 { |
| 676 | try testing.expectEqualStrings("TRansfer-encoding", header.name); | 681 | const nnn: @Vector(3, u8) = text.*; |
| 677 | try testing.expectEqualStrings("deflate, chunked", header.value); | 682 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; |
| 678 | try testing.expect(!it.is_trailer); | 683 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; |
| 684 | return @reduce(.Add, (nnn -% zero) *% mmm); | ||
| 679 | } | 685 | } |
| 680 | { | 686 | |
| 681 | const header = it.next().?; | 687 | test parseInt3 { |
| 682 | try testing.expectEqualStrings("connectioN", header.name); | 688 | const expectEqual = testing.expectEqual; |
| 683 | try testing.expectEqualStrings("keep-alive", header.value); | 689 | try expectEqual(@as(u10, 0), parseInt3("000")); |
| 684 | try testing.expect(!it.is_trailer); | 690 | try expectEqual(@as(u10, 418), parseInt3("418")); |
| 691 | try expectEqual(@as(u10, 999), parseInt3("999")); | ||
| 685 | } | 692 | } |
| 686 | try testing.expectEqual(null, it.next()); | 693 | }; |
| 694 | |||
| 695 | /// Asserts that this function is only called once. | ||
| 696 | pub fn reader(response: *Response) std.io.Reader { | ||
| 697 | const head = &response.head; | ||
| 698 | return response.request.reader.interface(head.transfer_encoding, head.content_length); | ||
| 687 | } | 699 | } |
| 688 | }; | 700 | }; |
| 689 | 701 | ||
| 690 | pub const Request = struct { | 702 | pub const Request = struct { |
| 703 | /// This field is provided so that clients can observe redirected URIs. | ||
| 704 | /// | ||
| 705 | /// Its backing memory is externally provided by API users when creating a | ||
| 706 | /// request, and then again provided externally via `redirect_buffer` to | ||
| 707 | /// `receiveHead`. | ||
| 691 | uri: Uri, | 708 | uri: Uri, |
| 692 | client: *Client, | 709 | client: *Client, |
| 693 | /// This is null when the connection is released. | 710 | /// This is null when the connection is released. |
| 694 | connection: ?*Connection, | 711 | connection: ?*Connection, |
| 712 | reader: http.Reader, | ||
| 695 | keep_alive: bool, | 713 | keep_alive: bool, |
| 696 | 714 | ||
| 697 | method: http.Method, | 715 | method: http.Method, |
| 698 | version: http.Version = .@"HTTP/1.1", | 716 | version: http.Version = .@"HTTP/1.1", |
| 699 | transfer_encoding: RequestTransfer, | 717 | transfer_encoding: TransferEncoding, |
| 700 | redirect_behavior: RedirectBehavior, | 718 | redirect_behavior: RedirectBehavior, |
| 701 | 719 | ||
| 702 | /// Whether the request should handle a 100-continue response before sending the request body. | 720 | /// Whether the request should handle a 100-continue response before sending the request body. |
| 703 | handle_continue: bool, | 721 | handle_continue: bool, |
| 704 | 722 | ||
| 705 | /// The response associated with this request. | ||
| 706 | /// | ||
| 707 | /// This field is undefined until `wait` is called. | ||
| 708 | response: Response, | ||
| 709 | |||
| 710 | /// Standard headers that have default, but overridable, behavior. | 723 | /// Standard headers that have default, but overridable, behavior. |
| 711 | headers: Headers, | 724 | headers: Headers, |
| 712 | 725 | ||
| ... | @@ -720,6 +733,12 @@ pub const Request = struct { | ... | @@ -720,6 +733,12 @@ pub const Request = struct { |
| 720 | /// Externally-owned; must outlive the Request. | 733 | /// Externally-owned; must outlive the Request. |
| 721 | privileged_headers: []const http.Header, | 734 | privileged_headers: []const http.Header, |
| 722 | 735 | ||
| 736 | pub const TransferEncoding = union(enum) { | ||
| 737 | content_length: u64, | ||
| 738 | chunked: void, | ||
| 739 | none: void, | ||
| 740 | }; | ||
| 741 | |||
| 723 | pub const Headers = struct { | 742 | pub const Headers = struct { |
| 724 | host: Value = .default, | 743 | host: Value = .default, |
| 725 | authorization: Value = .default, | 744 | authorization: Value = .default, |
| ... | @@ -771,76 +790,48 @@ pub const Request = struct { | ... | @@ -771,76 +790,48 @@ pub const Request = struct { |
| 771 | req.* = undefined; | 790 | req.* = undefined; |
| 772 | } | 791 | } |
| 773 | 792 | ||
| 774 | // This function must deallocate all resources associated with the request, | 793 | /// Sends and flushes a complete request as only HTTP head, no body. |
| 775 | // or keep those which will be used. | 794 | pub fn sendBodiless(r: *Request) std.io.Writer.Error!void { |
| 776 | // This needs to be kept in sync with deinit and request. | 795 | try sendBodilessUnflushed(r); |
| 777 | fn redirect(req: *Request, uri: Uri) !void { | 796 | try r.connection.?.writer.flush(); |
| 778 | assert(req.response.parser.done); | 797 | } |
| 779 | |||
| 780 | req.client.connection_pool.release(req.client.allocator, req.connection.?); | ||
| 781 | req.connection = null; | ||
| 782 | |||
| 783 | var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer); | ||
| 784 | defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..]; | ||
| 785 | const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); | ||
| 786 | |||
| 787 | const new_host = valid_uri.host.?.raw; | ||
| 788 | const prev_host = req.uri.host.?.raw; | ||
| 789 | const keep_privileged_headers = | ||
| 790 | std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and | ||
| 791 | std.ascii.endsWithIgnoreCase(new_host, prev_host) and | ||
| 792 | (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.'); | ||
| 793 | if (!keep_privileged_headers) { | ||
| 794 | // When redirecting to a different domain, strip privileged headers. | ||
| 795 | req.privileged_headers = &.{}; | ||
| 796 | } | ||
| 797 | |||
| 798 | if (switch (req.response.status) { | ||
| 799 | .see_other => true, | ||
| 800 | .moved_permanently, .found => req.method == .POST, | ||
| 801 | else => false, | ||
| 802 | }) { | ||
| 803 | // A redirect to a GET must change the method and remove the body. | ||
| 804 | req.method = .GET; | ||
| 805 | req.transfer_encoding = .none; | ||
| 806 | req.headers.content_type = .omit; | ||
| 807 | } | ||
| 808 | 798 | ||
| 809 | if (req.transfer_encoding != .none) { | 799 | /// Sends but does not flush a complete request as only HTTP head, no body. |
| 810 | // The request body has already been sent. The request is | 800 | pub fn sendBodilessUnflushed(r: *Request) std.io.Writer.Error!void { |
| 811 | // still in a valid state, but the redirect must be handled | 801 | assert(r.transfer_encoding == .none); |
| 812 | // manually. | 802 | assert(!r.method.requestHasBody()); |
| 813 | return error.RedirectRequiresResend; | 803 | try sendHead(r); |
| 814 | } | 804 | } |
| 815 | 805 | ||
| 816 | req.uri = valid_uri; | 806 | /// Transfers the HTTP head over the connection, which is not flushed until |
| 817 | req.connection = try req.client.connect(new_host, uriPort(valid_uri, protocol), protocol); | 807 | /// `BodyWriter.flush` or `BodyWriter.end` is called. |
| 818 | req.redirect_behavior.subtractOne(); | 808 | pub fn sendBody(r: *Request) std.io.Writer.Error!http.BodyWriter { |
| 819 | req.response.parser.reset(); | 809 | assert(r.method.requestHasBody()); |
| 820 | 810 | try sendHead(r); | |
| 821 | req.response = .{ | 811 | return .{ |
| 822 | .version = undefined, | 812 | .http_protocol_output = &r.connection.?.writer, |
| 823 | .status = undefined, | 813 | .transfer_encoding = if (r.transfer_encoding) |te| switch (te) { |
| 824 | .reason = undefined, | 814 | .chunked => .{ .chunked = .init }, |
| 825 | .keep_alive = undefined, | 815 | .content_length => |len| .{ .content_length = len }, |
| 826 | .parser = req.response.parser, | 816 | .none => .none, |
| 817 | } else .{ .chunked = .init }, | ||
| 818 | .elide_body = false, | ||
| 827 | }; | 819 | }; |
| 828 | } | 820 | } |
| 829 | 821 | ||
| 830 | /// Send the HTTP request headers to the server. | 822 | /// Sends HTTP headers without flushing. |
| 831 | pub fn send(req: *Request) std.io.Writer.Error!void { | 823 | fn sendHead(r: *Request) std.io.Writer.Error!void { |
| 832 | assert(req.transfer_encoding == .none or req.method.requestHasBody()); | 824 | const uri = r.uri; |
| 833 | 825 | const connection = r.connection.?; | |
| 834 | const connection = req.connection.?; | ||
| 835 | const w = &connection.writer; | 826 | const w = &connection.writer; |
| 836 | 827 | ||
| 837 | try req.method.write(w); | 828 | try r.method.write(w); |
| 838 | try w.writeByte(' '); | 829 | try w.writeByte(' '); |
| 839 | 830 | ||
| 840 | if (req.method == .CONNECT) { | 831 | if (r.method == .CONNECT) { |
| 841 | try req.uri.writeToStream(.{ .authority = true }, w); | 832 | try uri.writeToStream(.{ .authority = true }, w); |
| 842 | } else { | 833 | } else { |
| 843 | try req.uri.writeToStream(.{ | 834 | try uri.writeToStream(.{ |
| 844 | .scheme = connection.proxied, | 835 | .scheme = connection.proxied, |
| 845 | .authentication = connection.proxied, | 836 | .authentication = connection.proxied, |
| 846 | .authority = connection.proxied, | 837 | .authority = connection.proxied, |
| ... | @@ -849,55 +840,55 @@ pub const Request = struct { | ... | @@ -849,55 +840,55 @@ pub const Request = struct { |
| 849 | }, w); | 840 | }, w); |
| 850 | } | 841 | } |
| 851 | try w.writeByte(' '); | 842 | try w.writeByte(' '); |
| 852 | try w.writeAll(@tagName(req.version)); | 843 | try w.writeAll(@tagName(r.version)); |
| 853 | try w.writeAll("\r\n"); | 844 | try w.writeAll("\r\n"); |
| 854 | 845 | ||
| 855 | if (try emitOverridableHeader("host: ", req.headers.host, w)) { | 846 | if (try emitOverridableHeader("host: ", r.headers.host, w)) { |
| 856 | try w.writeAll("host: "); | 847 | try w.writeAll("host: "); |
| 857 | try req.uri.writeToStream(.{ .authority = true }, w); | 848 | try uri.writeToStream(.{ .authority = true }, w); |
| 858 | try w.writeAll("\r\n"); | 849 | try w.writeAll("\r\n"); |
| 859 | } | 850 | } |
| 860 | 851 | ||
| 861 | if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) { | 852 | if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { |
| 862 | if (req.uri.user != null or req.uri.password != null) { | 853 | if (uri.user != null or uri.password != null) { |
| 863 | try w.writeAll("authorization: "); | 854 | try w.writeAll("authorization: "); |
| 864 | try basic_authorization.write(req.uri, w); | 855 | try basic_authorization.write(uri, w); |
| 865 | try w.writeAll("\r\n"); | 856 | try w.writeAll("\r\n"); |
| 866 | } | 857 | } |
| 867 | } | 858 | } |
| 868 | 859 | ||
| 869 | if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) { | 860 | if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { |
| 870 | try w.writeAll("user-agent: zig/"); | 861 | try w.writeAll("user-agent: zig/"); |
| 871 | try w.writeAll(builtin.zig_version_string); | 862 | try w.writeAll(builtin.zig_version_string); |
| 872 | try w.writeAll(" (std.http)\r\n"); | 863 | try w.writeAll(" (std.http)\r\n"); |
| 873 | } | 864 | } |
| 874 | 865 | ||
| 875 | if (try emitOverridableHeader("connection: ", req.headers.connection, w)) { | 866 | if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { |
| 876 | if (req.keep_alive) { | 867 | if (r.keep_alive) { |
| 877 | try w.writeAll("connection: keep-alive\r\n"); | 868 | try w.writeAll("connection: keep-alive\r\n"); |
| 878 | } else { | 869 | } else { |
| 879 | try w.writeAll("connection: close\r\n"); | 870 | try w.writeAll("connection: close\r\n"); |
| 880 | } | 871 | } |
| 881 | } | 872 | } |
| 882 | 873 | ||
| 883 | if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) { | 874 | if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { |
| 884 | // https://github.com/ziglang/zig/issues/18937 | 875 | // https://github.com/ziglang/zig/issues/18937 |
| 885 | //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n"); | 876 | //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n"); |
| 886 | try w.writeAll("accept-encoding: gzip, deflate\r\n"); | 877 | try w.writeAll("accept-encoding: gzip, deflate\r\n"); |
| 887 | } | 878 | } |
| 888 | 879 | ||
| 889 | switch (req.transfer_encoding) { | 880 | switch (r.transfer_encoding) { |
| 890 | .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), | 881 | .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), |
| 891 | .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), | 882 | .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), |
| 892 | .none => {}, | 883 | .none => {}, |
| 893 | } | 884 | } |
| 894 | 885 | ||
| 895 | if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) { | 886 | if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { |
| 896 | // The default is to omit content-type if not provided because | 887 | // The default is to omit content-type if not provided because |
| 897 | // "application/octet-stream" is redundant. | 888 | // "application/octet-stream" is redundant. |
| 898 | } | 889 | } |
| 899 | 890 | ||
| 900 | for (req.extra_headers) |header| { | 891 | for (r.extra_headers) |header| { |
| 901 | assert(header.name.len != 0); | 892 | assert(header.name.len != 0); |
| 902 | 893 | ||
| 903 | try w.writeAll(header.name); | 894 | try w.writeAll(header.name); |
| ... | @@ -908,8 +899,8 @@ pub const Request = struct { | ... | @@ -908,8 +899,8 @@ pub const Request = struct { |
| 908 | 899 | ||
| 909 | if (connection.proxied) proxy: { | 900 | if (connection.proxied) proxy: { |
| 910 | const proxy = switch (connection.protocol) { | 901 | const proxy = switch (connection.protocol) { |
| 911 | .plain => req.client.http_proxy, | 902 | .plain => r.client.http_proxy, |
| 912 | .tls => req.client.https_proxy, | 903 | .tls => r.client.https_proxy, |
| 913 | } orelse break :proxy; | 904 | } orelse break :proxy; |
| 914 | 905 | ||
| 915 | const authorization = proxy.authorization orelse break :proxy; | 906 | const authorization = proxy.authorization orelse break :proxy; |
| ... | @@ -919,338 +910,197 @@ pub const Request = struct { | ... | @@ -919,338 +910,197 @@ pub const Request = struct { |
| 919 | } | 910 | } |
| 920 | 911 | ||
| 921 | try w.writeAll("\r\n"); | 912 | try w.writeAll("\r\n"); |
| 922 | |||
| 923 | try connection.writer.flush(); | ||
| 924 | } | 913 | } |
| 925 | 914 | ||
| 926 | /// Returns true if the default behavior is required, otherwise handles | 915 | pub const ReceiveHeadError = http.Reader.HeadError || error{ |
| 927 | /// writing (or not writing) the header. | 916 | /// Server sent headers that did not conform to the HTTP protocol. |
| 928 | fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool { | 917 | /// |
| 929 | switch (v) { | 918 | /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be |
| 930 | .default => return true, | 919 | /// passed directly to `Request.Head.parse`. |
| 931 | .omit => return false, | 920 | HttpHeadersInvalid, |
| 932 | .override => |x| { | 921 | TooManyHttpRedirects, |
| 933 | try w.writeAll(prefix); | 922 | /// This can be avoided by calling `receiveHead` before sending the |
| 934 | try w.writeAll(x); | 923 | /// request body. |
| 935 | try w.writeAll("\r\n"); | 924 | RedirectRequiresResend, |
| 936 | return false; | 925 | HttpRedirectLocationMissing, |
| 937 | }, | 926 | HttpRedirectLocationOversize, |
| 938 | } | 927 | HttpRedirectLocationInvalid, |
| 939 | } | 928 | CompressionInitializationFailed, |
| 940 | 929 | CompressionUnsupported, | |
| 941 | const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; | 930 | }; |
| 942 | |||
| 943 | const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); | ||
| 944 | |||
| 945 | fn transferReader(req: *Request) TransferReader { | ||
| 946 | return .{ .context = req }; | ||
| 947 | } | ||
| 948 | |||
| 949 | fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { | ||
| 950 | if (req.response.parser.done) return 0; | ||
| 951 | |||
| 952 | var index: usize = 0; | ||
| 953 | while (index == 0) { | ||
| 954 | const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); | ||
| 955 | if (amt == 0 and req.response.parser.done) break; | ||
| 956 | index += amt; | ||
| 957 | } | ||
| 958 | |||
| 959 | return index; | ||
| 960 | } | ||
| 961 | |||
| 962 | /// TODO collapse each error set into its own meta error code, and store | ||
| 963 | /// the underlying error code as a field on Request | ||
| 964 | pub const WaitError = RequestError || std.io.Writer.Error || TransferReadError || | ||
| 965 | proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || | ||
| 966 | error{ | ||
| 967 | TooManyHttpRedirects, | ||
| 968 | RedirectRequiresResend, | ||
| 969 | HttpRedirectLocationMissing, | ||
| 970 | HttpRedirectLocationInvalid, | ||
| 971 | CompressionInitializationFailed, | ||
| 972 | CompressionUnsupported, | ||
| 973 | }; | ||
| 974 | 931 | ||
| 975 | /// Waits for a response from the server and parses any headers that are sent. | ||
| 976 | /// This function will block until the final response is received. | ||
| 977 | /// | ||
| 978 | /// If handling redirects and the request has no payload, then this | 932 | /// If handling redirects and the request has no payload, then this |
| 979 | /// function will automatically follow redirects. If a request payload is | 933 | /// function will automatically follow redirects. |
| 980 | /// present, then this function will error with | 934 | /// |
| 981 | /// error.RedirectRequiresResend. | 935 | /// If a request payload is present, then this function will error with |
| 936 | /// `error.RedirectRequiresResend`. | ||
| 982 | /// | 937 | /// |
| 983 | /// Must be called after `send` and, if any data was written to the request | 938 | /// This function takes an auxiliary buffer to store the arbitrarily large |
| 984 | /// body, then also after `finish`. | 939 | /// URI which may need to be merged with the previous URI, and that data |
| 985 | pub fn wait(req: *Request) WaitError!void { | 940 | /// needs to survive across different connections, which is where the input |
| 941 | /// buffer lives. | ||
| 942 | /// | ||
| 943 | /// `redirect_buffer` must outlive accesses to `Request.uri`. If this | ||
| 944 | /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` | ||
| 945 | /// is returned instead. This buffer may be empty if no redirects are to be | ||
| 946 | /// handled. | ||
| 947 | pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { | ||
| 948 | var aux_buf = redirect_buffer; | ||
| 986 | while (true) { | 949 | while (true) { |
| 987 | // This while loop is for handling redirects, which means the request's | 950 | try r.reader.receiveHead(); |
| 988 | // connection may be different than the previous iteration. However, it | 951 | const response: Response = .{ |
| 989 | // is still guaranteed to be non-null with each iteration of this loop. | 952 | .request = r, |
| 990 | const connection = req.connection.?; | 953 | .head = Response.Head.parse(r.reader.head_buffer) catch return error.HttpHeadersInvalid, |
| 991 | 954 | }; | |
| 992 | while (true) { // read headers | 955 | const head = &response.head; |
| 993 | try connection.fill(); | ||
| 994 | |||
| 995 | const nchecked = try req.response.parser.checkCompleteHead(connection.peek()); | ||
| 996 | connection.drop(@intCast(nchecked)); | ||
| 997 | |||
| 998 | if (req.response.parser.state.isContent()) break; | ||
| 999 | } | ||
| 1000 | |||
| 1001 | try req.response.parse(req.response.parser.get()); | ||
| 1002 | |||
| 1003 | if (req.response.status == .@"continue") { | ||
| 1004 | // We're done parsing the continue response; reset to prepare | ||
| 1005 | // for the real response. | ||
| 1006 | req.response.parser.done = true; | ||
| 1007 | req.response.parser.reset(); | ||
| 1008 | |||
| 1009 | if (req.handle_continue) | ||
| 1010 | continue; | ||
| 1011 | 956 | ||
| 957 | if (head.status == .@"continue") { | ||
| 958 | if (r.handle_continue) continue; | ||
| 1012 | return; // we're not handling the 100-continue | 959 | return; // we're not handling the 100-continue |
| 1013 | } | 960 | } |
| 1014 | 961 | ||
| 1015 | // we're switching protocols, so this connection is no longer doing http | 962 | // This while loop is for handling redirects, which means the request's |
| 1016 | if (req.method == .CONNECT and req.response.status.class() == .success) { | 963 | // connection may be different than the previous iteration. However, it |
| 964 | // is still guaranteed to be non-null with each iteration of this loop. | ||
| 965 | const connection = r.connection.?; | ||
| 966 | |||
| 967 | if (r.method == .CONNECT and head.status.class() == .success) { | ||
| 968 | // This connection is no longer doing HTTP. | ||
| 1017 | connection.closing = false; | 969 | connection.closing = false; |
| 1018 | req.response.parser.done = true; | 970 | return response; |
| 1019 | return; // the connection is not HTTP past this point | ||
| 1020 | } | 971 | } |
| 1021 | 972 | ||
| 1022 | connection.closing = !req.response.keep_alive or !req.keep_alive; | 973 | connection.closing = !head.keep_alive or !r.keep_alive; |
| 1023 | 974 | ||
| 1024 | // Any response to a HEAD request and any response with a 1xx | 975 | // Any response to a HEAD request and any response with a 1xx |
| 1025 | // (Informational), 204 (No Content), or 304 (Not Modified) status | 976 | // (Informational), 204 (No Content), or 304 (Not Modified) status |
| 1026 | // code is always terminated by the first empty line after the | 977 | // code is always terminated by the first empty line after the |
| 1027 | // header fields, regardless of the header fields present in the | 978 | // header fields, regardless of the header fields present in the |
| 1028 | // message. | 979 | // message. |
| 1029 | if (req.method == .HEAD or req.response.status.class() == .informational or | 980 | if (r.method == .HEAD or head.status.class() == .informational or |
| 1030 | req.response.status == .no_content or req.response.status == .not_modified) | 981 | head.status == .no_content or head.status == .not_modified) |
| 1031 | { | 982 | { |
| 1032 | req.response.parser.done = true; | 983 | return response; |
| 1033 | return; // The response is empty; no further setup or redirection is necessary. | ||
| 1034 | } | 984 | } |
| 1035 | 985 | ||
| 1036 | switch (req.response.transfer_encoding) { | 986 | if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { |
| 1037 | .none => { | 987 | if (r.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; |
| 1038 | if (req.response.content_length) |cl| { | 988 | const location = head.location orelse return error.HttpRedirectLocationMissing; |
| 1039 | req.response.parser.next_chunk_length = cl; | 989 | try r.redirect(location, &aux_buf); |
| 990 | try r.send(); | ||
| 991 | continue; | ||
| 992 | } | ||
| 1040 | 993 | ||
| 1041 | if (cl == 0) req.response.parser.done = true; | 994 | switch (head.transfer_compression) { |
| 1042 | } else { | 995 | .identity => response.compression = .none, |
| 1043 | // read until the connection is closed | 996 | .compress, .@"x-compress" => return error.CompressionUnsupported, |
| 1044 | req.response.parser.next_chunk_length = std.math.maxInt(u64); | 997 | .deflate => response.compression = .{ |
| 1045 | } | 998 | .deflate = std.compress.zlib.decompressor(r.transferReader()), |
| 1046 | }, | 999 | }, |
| 1047 | .chunked => { | 1000 | .gzip, .@"x-gzip" => response.compression = .{ |
| 1048 | req.response.parser.next_chunk_length = 0; | 1001 | .gzip = std.compress.gzip.decompressor(r.transferReader()), |
| 1049 | req.response.parser.state = .chunk_head_size; | ||
| 1050 | }, | 1002 | }, |
| 1003 | // https://github.com/ziglang/zig/issues/18937 | ||
| 1004 | //.zstd => response.compression = .{ | ||
| 1005 | // .zstd = std.compress.zstd.decompressStream(r.client.allocator, r.transferReader()), | ||
| 1006 | //}, | ||
| 1007 | .zstd => return error.CompressionUnsupported, | ||
| 1051 | } | 1008 | } |
| 1052 | 1009 | return response; | |
| 1053 | if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) { | ||
| 1054 | // skip the body of the redirect response, this will at least | ||
| 1055 | // leave the connection in a known good state. | ||
| 1056 | req.response.skip = true; | ||
| 1057 | assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary | ||
| 1058 | |||
| 1059 | if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; | ||
| 1060 | |||
| 1061 | const location = req.response.location orelse | ||
| 1062 | return error.HttpRedirectLocationMissing; | ||
| 1063 | |||
| 1064 | // This mutates the beginning of header_bytes_buffer and uses that | ||
| 1065 | // for the backing memory of the returned Uri. | ||
| 1066 | try req.redirect(req.uri.resolve_inplace( | ||
| 1067 | location, | ||
| 1068 | &req.response.parser.header_bytes_buffer, | ||
| 1069 | ) catch |err| switch (err) { | ||
| 1070 | error.UnexpectedCharacter, | ||
| 1071 | error.InvalidFormat, | ||
| 1072 | error.InvalidPort, | ||
| 1073 | => return error.HttpRedirectLocationInvalid, | ||
| 1074 | error.NoSpaceLeft => return error.HttpHeadersOversize, | ||
| 1075 | }); | ||
| 1076 | try req.send(); | ||
| 1077 | } else { | ||
| 1078 | req.response.skip = false; | ||
| 1079 | if (!req.response.parser.done) { | ||
| 1080 | switch (req.response.transfer_compression) { | ||
| 1081 | .identity => req.response.compression = .none, | ||
| 1082 | .compress, .@"x-compress" => return error.CompressionUnsupported, | ||
| 1083 | .deflate => req.response.compression = .{ | ||
| 1084 | .deflate = std.compress.zlib.decompressor(req.transferReader()), | ||
| 1085 | }, | ||
| 1086 | .gzip, .@"x-gzip" => req.response.compression = .{ | ||
| 1087 | .gzip = std.compress.gzip.decompressor(req.transferReader()), | ||
| 1088 | }, | ||
| 1089 | // https://github.com/ziglang/zig/issues/18937 | ||
| 1090 | //.zstd => req.response.compression = .{ | ||
| 1091 | // .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), | ||
| 1092 | //}, | ||
| 1093 | .zstd => return error.CompressionUnsupported, | ||
| 1094 | } | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | break; | ||
| 1098 | } | ||
| 1099 | } | 1010 | } |
| 1100 | } | 1011 | } |
| 1101 | 1012 | ||
| 1102 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || | 1013 | pub const RedirectError = error{ |
| 1103 | error{ DecompressionFailure, InvalidTrailers }; | 1014 | HttpRedirectLocationOversize, |
| 1104 | 1015 | HttpRedirectLocationInvalid, | |
| 1105 | pub const Reader = std.io.Reader(*Request, ReadError, read); | 1016 | }; |
| 1106 | |||
| 1107 | pub fn reader(req: *Request) Reader { | ||
| 1108 | return .{ .context = req }; | ||
| 1109 | } | ||
| 1110 | 1017 | ||
| 1111 | /// Reads data from the response body. Must be called after `wait`. | 1018 | /// This function takes an auxiliary buffer to store the arbitrarily large |
| 1112 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | 1019 | /// URI which may need to be merged with the previous URI, and that data |
| 1113 | const out_index = switch (req.response.compression) { | 1020 | /// needs to survive across different connections, which is where the input |
| 1114 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, | 1021 | /// buffer lives. |
| 1115 | .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, | 1022 | /// |
| 1116 | // https://github.com/ziglang/zig/issues/18937 | 1023 | /// `aux_buf` must outlive accesses to `Request.uri`. |
| 1117 | //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure, | 1024 | fn redirect(r: *Request, new_location: []const u8, aux_buf: *[]u8) RedirectError!void { |
| 1118 | else => try req.transferRead(buffer), | 1025 | if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; |
| 1026 | const location = aux_buf.*[0..new_location.len]; | ||
| 1027 | @memcpy(location, new_location); | ||
| 1028 | { | ||
| 1029 | // Skip the body of the redirect response to leave the connection in | ||
| 1030 | // the correct state. This causes `new_location` to be invalidated. | ||
| 1031 | var reader = r.reader.interface(); | ||
| 1032 | _ = reader.discardRemaining() catch |err| switch (err) { | ||
| 1033 | error.ReadFailed => return r.reader.err.?, | ||
| 1034 | }; | ||
| 1035 | } | ||
| 1036 | const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { | ||
| 1037 | error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, | ||
| 1038 | error.InvalidFormat => return error.HttpRedirectLocationInvalid, | ||
| 1039 | error.InvalidPort => return error.HttpRedirectLocationInvalid, | ||
| 1040 | error.NoSpaceLeft => return error.HttpRedirectLocationOversize, | ||
| 1119 | }; | 1041 | }; |
| 1120 | if (out_index > 0) return out_index; | 1042 | const resolved_len = location.len + (aux_buf.*.ptr - location.ptr); |
| 1121 | 1043 | ||
| 1122 | while (!req.response.parser.state.isContent()) { // read trailing headers | 1044 | const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; |
| 1123 | try req.connection.?.fill(); | 1045 | const old_connection = r.connection.?; |
| 1124 | 1046 | const old_host = old_connection.host(); | |
| 1125 | const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek()); | 1047 | var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; |
| 1126 | req.connection.?.drop(@intCast(nchecked)); | 1048 | const new_host = try new_uri.getHost(&new_host_name_buffer); |
| 1127 | } | 1049 | const keep_privileged_headers = |
| 1050 | std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and | ||
| 1051 | sameParentDomain(old_host, new_host); | ||
| 1128 | 1052 | ||
| 1129 | return 0; | 1053 | r.client.connection_pool.release(r.client.allocator, old_connection); |
| 1130 | } | 1054 | r.connection = null; |
| 1131 | 1055 | ||
| 1132 | /// Reads data from the response body. Must be called after `wait`. | 1056 | if (!keep_privileged_headers) { |
| 1133 | pub fn readAll(req: *Request, buffer: []u8) !usize { | 1057 | // When redirecting to a different domain, strip privileged headers. |
| 1134 | var index: usize = 0; | 1058 | r.privileged_headers = &.{}; |
| 1135 | while (index < buffer.len) { | ||
| 1136 | const amt = try read(req, buffer[index..]); | ||
| 1137 | if (amt == 0) break; | ||
| 1138 | index += amt; | ||
| 1139 | } | 1059 | } |
| 1140 | return index; | ||
| 1141 | } | ||
| 1142 | 1060 | ||
| 1143 | /// Resulting `std.io.Writer` must used after `send` and before `finish`. | 1061 | if (switch (r.response.status) { |
| 1144 | pub fn writer(req: *Request) std.io.Writer { | 1062 | .see_other => true, |
| 1145 | return .{ | 1063 | .moved_permanently, .found => r.method == .POST, |
| 1146 | .context = req, | 1064 | else => false, |
| 1147 | .vtable = switch (req.transfer_encoding) { | 1065 | }) { |
| 1148 | .chunked => &.{ | 1066 | // A redirect to a GET must change the method and remove the body. |
| 1149 | .writeSplat = chunked_writeSplat, | 1067 | r.method = .GET; |
| 1150 | .writeFile = chunked_writeFile, | 1068 | r.transfer_encoding = .none; |
| 1151 | }, | 1069 | r.headers.content_type = .omit; |
| 1152 | .content_length => &.{ | 1070 | } |
| 1153 | .writeSplat = cl_writeSplat, | ||
| 1154 | .writeFile = cl_writeFile, | ||
| 1155 | }, | ||
| 1156 | .none => unreachable, | ||
| 1157 | }, | ||
| 1158 | }; | ||
| 1159 | } | ||
| 1160 | |||
| 1161 | fn chunked_writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { | ||
| 1162 | const req: *Request = @ptrCast(@alignCast(context)); | ||
| 1163 | var total: usize = 0; | ||
| 1164 | for (data) |bytes| total += bytes.len; | ||
| 1165 | if (total == 0) return 0; | ||
| 1166 | var iovecs: [max_buffers_len][]const u8 = undefined; | ||
| 1167 | var header_buffer: [30]u8 = undefined; | ||
| 1168 | var header_buffer_writer: std.io.BufferedWriter = undefined; | ||
| 1169 | header_buffer_writer.initFixed(&header_buffer); | ||
| 1170 | header_buffer_writer.print("{x}\r\n", .{total}) catch unreachable; | ||
| 1171 | iovecs[0] = header_buffer_writer.getWritten(); | ||
| 1172 | @memcpy(iovecs[1..][0..data.len], data); | ||
| 1173 | iovecs[data.len + 1] = "\r\n"; | ||
| 1174 | // TODO: only 1 underlying write call | ||
| 1175 | // TODO: don't rely on max_buffers_len exceeding the caller | ||
| 1176 | // TODO: handle splat | ||
| 1177 | _ = splat; | ||
| 1178 | const w = &req.connection.?.writer; | ||
| 1179 | try w.writevAll(iovecs[0 .. data.len + 2]); | ||
| 1180 | return total; | ||
| 1181 | } | ||
| 1182 | |||
| 1183 | const max_buffers_len = 16; | ||
| 1184 | |||
| 1185 | pub fn chunked_writeFile( | ||
| 1186 | context: *anyopaque, | ||
| 1187 | file: std.fs.File, | ||
| 1188 | offset: u64, | ||
| 1189 | len: std.io.Writer.FileLen, | ||
| 1190 | headers_and_trailers: []const []const u8, | ||
| 1191 | headers_len: usize, | ||
| 1192 | ) std.io.Writer.Error!usize { | ||
| 1193 | if (len == .entire_file) return error.Unimplemented; | ||
| 1194 | const req: *Request = @ptrCast(@alignCast(context)); | ||
| 1195 | var total: usize = len.int(); | ||
| 1196 | for (headers_and_trailers) |bytes| total += bytes.len; | ||
| 1197 | if (total == 0) return 0; | ||
| 1198 | var iovecs: [max_buffers_len][]const u8 = undefined; | ||
| 1199 | var header_buffer: [30]u8 = undefined; | ||
| 1200 | var header_buffer_writer: std.io.BufferedWriter = undefined; | ||
| 1201 | header_buffer_writer.initFixed(&header_buffer); | ||
| 1202 | header_buffer_writer.print("{x}\r\n", .{total}) catch unreachable; | ||
| 1203 | iovecs[0] = header_buffer_writer.getWritten(); | ||
| 1204 | @memcpy(iovecs[1..][0..headers_and_trailers.len], headers_and_trailers); | ||
| 1205 | iovecs[headers_and_trailers.len + 1] = "\r\n"; | ||
| 1206 | // TODO: only 1 underlying write call | ||
| 1207 | // TODO: don't rely on max_buffers_len exceeding the caller | ||
| 1208 | const w = &req.connection.?.writer; | ||
| 1209 | try w.writeFileAll(file, .{ | ||
| 1210 | .offset = offset, | ||
| 1211 | .len = len, | ||
| 1212 | .headers_and_trailers = iovecs[0 .. headers_and_trailers.len + 2], | ||
| 1213 | .headers_len = headers_len + 1, | ||
| 1214 | }); | ||
| 1215 | return total; | ||
| 1216 | } | ||
| 1217 | 1071 | ||
| 1218 | fn cl_writeSplat(context: *anyopaque, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { | 1072 | if (r.transfer_encoding != .none) { |
| 1219 | const req: *Request = @ptrCast(@alignCast(context)); | 1073 | // The request body has already been sent. The request is |
| 1220 | const n = try req.connection.?.writer.writeSplat(data, splat); | 1074 | // still in a valid state, but the redirect must be handled |
| 1221 | req.transfer_encoding.content_length -= n; | 1075 | // manually. |
| 1222 | return n; | 1076 | return error.RedirectRequiresResend; |
| 1223 | } | 1077 | } |
| 1224 | 1078 | ||
| 1225 | pub fn cl_writeFile( | 1079 | const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); |
| 1226 | context: *anyopaque, | 1080 | r.uri = new_uri; |
| 1227 | file: std.fs.File, | 1081 | r.stolen_bytes_len = resolved_len; |
| 1228 | offset: u64, | 1082 | r.connection = new_connection; |
| 1229 | len: std.io.Writer.FileLen, | 1083 | r.redirect_behavior.subtractOne(); |
| 1230 | headers_and_trailers: []const []const u8, | ||
| 1231 | headers_len: usize, | ||
| 1232 | ) std.io.Writer.Error!usize { | ||
| 1233 | const req: *Request = @ptrCast(@alignCast(context)); | ||
| 1234 | const n = try req.connection.?.writer.writeFile(file, offset, len, headers_and_trailers, headers_len); | ||
| 1235 | req.transfer_encoding.content_length -= n; | ||
| 1236 | return n; | ||
| 1237 | } | 1084 | } |
| 1238 | 1085 | ||
| 1239 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 1086 | /// Returns true if the default behavior is required, otherwise handles |
| 1240 | /// Must be called after `send`. | 1087 | /// writing (or not writing) the header. |
| 1241 | pub fn finish(req: *Request) std.io.Writer.Error!void { | 1088 | fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *std.io.BufferedWriter) std.io.Writer.Error!bool { |
| 1242 | switch (req.transfer_encoding) { | 1089 | switch (v) { |
| 1243 | .chunked => try req.connection.?.writer.writeAll("0\r\n\r\n"), | 1090 | .default => return true, |
| 1244 | .content_length => |len| assert(len == 0), | 1091 | .omit => return false, |
| 1245 | .none => {}, | 1092 | .override => |x| { |
| 1093 | try bw.writeAll(prefix); | ||
| 1094 | try bw.writeAll(x); | ||
| 1095 | try bw.writeAll("\r\n"); | ||
| 1096 | return false; | ||
| 1097 | }, | ||
| 1246 | } | 1098 | } |
| 1247 | |||
| 1248 | try req.connection.?.writer.flush(); | ||
| 1249 | } | 1099 | } |
| 1250 | }; | 1100 | }; |
| 1251 | 1101 | ||
| 1252 | pub const Proxy = struct { | 1102 | pub const Proxy = struct { |
| 1253 | protocol: Connection.Protocol, | 1103 | protocol: Protocol, |
| 1254 | host: []const u8, | 1104 | host: []const u8, |
| 1255 | authorization: ?[]const u8, | 1105 | authorization: ?[]const u8, |
| 1256 | port: u16, | 1106 | port: u16, |
| ... | @@ -1307,24 +1157,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !? | ... | @@ -1307,24 +1157,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !? |
| 1307 | } else return null; | 1157 | } else return null; |
| 1308 | 1158 | ||
| 1309 | const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); | 1159 | const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); |
| 1310 | const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) { | 1160 | const protocol = Protocol.fromUri(uri) orelse return null; |
| 1311 | error.UnsupportedUriScheme => return null, | 1161 | const raw_host = try uri.getHostAlloc(arena); |
| 1312 | error.UriMissingHost => return error.HttpProxyMissingHost, | ||
| 1313 | error.OutOfMemory => |e| return e, | ||
| 1314 | }; | ||
| 1315 | 1162 | ||
| 1316 | const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: { | 1163 | const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { |
| 1317 | const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri)); | 1164 | const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); |
| 1318 | assert(basic_authorization.value(valid_uri, authorization).len == authorization.len); | 1165 | assert(basic_authorization.value(uri, authorization).len == authorization.len); |
| 1319 | break :a authorization; | 1166 | break :a authorization; |
| 1320 | } else null; | 1167 | } else null; |
| 1321 | 1168 | ||
| 1322 | const proxy = try arena.create(Proxy); | 1169 | const proxy = try arena.create(Proxy); |
| 1323 | proxy.* = .{ | 1170 | proxy.* = .{ |
| 1324 | .protocol = protocol, | 1171 | .protocol = protocol, |
| 1325 | .host = valid_uri.host.?.raw, | 1172 | .host = raw_host, |
| 1326 | .authorization = authorization, | 1173 | .authorization = authorization, |
| 1327 | .port = uriPort(valid_uri, protocol), | 1174 | .port = uriPort(uri, protocol), |
| 1328 | .supports_connect = true, | 1175 | .supports_connect = true, |
| 1329 | }; | 1176 | }; |
| 1330 | return proxy; | 1177 | return proxy; |
| ... | @@ -1385,7 +1232,7 @@ pub fn connectTcp( | ... | @@ -1385,7 +1232,7 @@ pub fn connectTcp( |
| 1385 | client: *Client, | 1232 | client: *Client, |
| 1386 | host: []const u8, | 1233 | host: []const u8, |
| 1387 | port: u16, | 1234 | port: u16, |
| 1388 | protocol: Connection.Protocol, | 1235 | protocol: Protocol, |
| 1389 | ) ConnectTcpError!*Connection { | 1236 | ) ConnectTcpError!*Connection { |
| 1390 | if (client.connection_pool.findConnection(.{ | 1237 | if (client.connection_pool.findConnection(.{ |
| 1391 | .host = host, | 1238 | .host = host, |
| ... | @@ -1540,7 +1387,7 @@ pub fn connect( | ... | @@ -1540,7 +1387,7 @@ pub fn connect( |
| 1540 | client: *Client, | 1387 | client: *Client, |
| 1541 | host: []const u8, | 1388 | host: []const u8, |
| 1542 | port: u16, | 1389 | port: u16, |
| 1543 | protocol: Connection.Protocol, | 1390 | protocol: Protocol, |
| 1544 | ) ConnectError!*Connection { | 1391 | ) ConnectError!*Connection { |
| 1545 | const proxy = switch (protocol) { | 1392 | const proxy = switch (protocol) { |
| 1546 | .plain => client.http_proxy, | 1393 | .plain => client.http_proxy, |
| ... | @@ -1604,11 +1451,6 @@ pub const RequestOptions = struct { | ... | @@ -1604,11 +1451,6 @@ pub const RequestOptions = struct { |
| 1604 | /// payload or the server has acknowledged the payload). | 1451 | /// payload or the server has acknowledged the payload). |
| 1605 | redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), | 1452 | redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), |
| 1606 | 1453 | ||
| 1607 | /// Externally-owned memory used to store the server's entire HTTP header. | ||
| 1608 | /// `error.HttpHeadersOversize` is returned from read() when a | ||
| 1609 | /// client sends too many bytes of HTTP headers. | ||
| 1610 | server_header_buffer: []u8, | ||
| 1611 | |||
| 1612 | /// Must be an already acquired connection. | 1454 | /// Must be an already acquired connection. |
| 1613 | connection: ?*Connection = null, | 1455 | connection: ?*Connection = null, |
| 1614 | 1456 | ||
| ... | @@ -1624,33 +1466,12 @@ pub const RequestOptions = struct { | ... | @@ -1624,33 +1466,12 @@ pub const RequestOptions = struct { |
| 1624 | privileged_headers: []const http.Header = &.{}, | 1466 | privileged_headers: []const http.Header = &.{}, |
| 1625 | }; | 1467 | }; |
| 1626 | 1468 | ||
| 1627 | fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } { | 1469 | fn uriPort(uri: Uri, protocol: Protocol) u16 { |
| 1628 | const protocol_map = std.StaticStringMap(Connection.Protocol).initComptime(.{ | 1470 | return uri.port orelse protocol.port(); |
| 1629 | .{ "http", .plain }, | ||
| 1630 | .{ "ws", .plain }, | ||
| 1631 | .{ "https", .tls }, | ||
| 1632 | .{ "wss", .tls }, | ||
| 1633 | }); | ||
| 1634 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme; | ||
| 1635 | var valid_uri = uri; | ||
| 1636 | // The host is always going to be needed as a raw string for hostname resolution anyway. | ||
| 1637 | valid_uri.host = .{ | ||
| 1638 | .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena), | ||
| 1639 | }; | ||
| 1640 | return .{ protocol, valid_uri }; | ||
| 1641 | } | ||
| 1642 | |||
| 1643 | fn uriPort(uri: Uri, protocol: Connection.Protocol) u16 { | ||
| 1644 | return uri.port orelse switch (protocol) { | ||
| 1645 | .plain => 80, | ||
| 1646 | .tls => 443, | ||
| 1647 | }; | ||
| 1648 | } | 1471 | } |
| 1649 | 1472 | ||
| 1650 | /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. | 1473 | /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. |
| 1651 | /// | 1474 | /// |
| 1652 | /// `uri` must remain alive during the entire request. | ||
| 1653 | /// | ||
| 1654 | /// The caller is responsible for calling `deinit()` on the `Request`. | 1475 | /// The caller is responsible for calling `deinit()` on the `Request`. |
| 1655 | /// This function is threadsafe. | 1476 | /// This function is threadsafe. |
| 1656 | /// | 1477 | /// |
| ... | @@ -1675,8 +1496,7 @@ pub fn open( | ... | @@ -1675,8 +1496,7 @@ pub fn open( |
| 1675 | } | 1496 | } |
| 1676 | } | 1497 | } |
| 1677 | 1498 | ||
| 1678 | var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer); | 1499 | const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; |
| 1679 | const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); | ||
| 1680 | 1500 | ||
| 1681 | if (protocol == .tls) { | 1501 | if (protocol == .tls) { |
| 1682 | if (disable_tls) unreachable; | 1502 | if (disable_tls) unreachable; |
| ... | @@ -1692,33 +1512,26 @@ pub fn open( | ... | @@ -1692,33 +1512,26 @@ pub fn open( |
| 1692 | } | 1512 | } |
| 1693 | } | 1513 | } |
| 1694 | 1514 | ||
| 1695 | const conn = options.connection orelse | 1515 | const connection = options.connection orelse c: { |
| 1696 | try client.connect(valid_uri.host.?.raw, uriPort(valid_uri, protocol), protocol); | 1516 | var host_name_buffer: [Uri.host_name_max]u8 = undefined; |
| 1517 | const host_name = try uri.getHost(&host_name_buffer); | ||
| 1518 | break :c try client.connect(host_name, uriPort(uri, protocol), protocol); | ||
| 1519 | }; | ||
| 1697 | 1520 | ||
| 1698 | var req: Request = .{ | 1521 | return .{ |
| 1699 | .uri = valid_uri, | 1522 | .uri = uri, |
| 1700 | .client = client, | 1523 | .client = client, |
| 1701 | .connection = conn, | 1524 | .connection = connection, |
| 1702 | .keep_alive = options.keep_alive, | 1525 | .keep_alive = options.keep_alive, |
| 1703 | .method = method, | 1526 | .method = method, |
| 1704 | .version = options.version, | 1527 | .version = options.version, |
| 1705 | .transfer_encoding = .none, | 1528 | .transfer_encoding = .none, |
| 1706 | .redirect_behavior = options.redirect_behavior, | 1529 | .redirect_behavior = options.redirect_behavior, |
| 1707 | .handle_continue = options.handle_continue, | 1530 | .handle_continue = options.handle_continue, |
| 1708 | .response = .{ | ||
| 1709 | .version = undefined, | ||
| 1710 | .status = undefined, | ||
| 1711 | .reason = undefined, | ||
| 1712 | .keep_alive = undefined, | ||
| 1713 | .parser = .init(server_header.buffer[server_header.end_index..]), | ||
| 1714 | }, | ||
| 1715 | .headers = options.headers, | 1531 | .headers = options.headers, |
| 1716 | .extra_headers = options.extra_headers, | 1532 | .extra_headers = options.extra_headers, |
| 1717 | .privileged_headers = options.privileged_headers, | 1533 | .privileged_headers = options.privileged_headers, |
| 1718 | }; | 1534 | }; |
| 1719 | errdefer req.deinit(); | ||
| 1720 | |||
| 1721 | return req; | ||
| 1722 | } | 1535 | } |
| 1723 | 1536 | ||
| 1724 | pub const FetchOptions = struct { | 1537 | pub const FetchOptions = struct { |
| ... | @@ -1828,7 +1641,20 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { | ... | @@ -1828,7 +1641,20 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { |
| 1828 | }; | 1641 | }; |
| 1829 | } | 1642 | } |
| 1830 | 1643 | ||
| 1644 | pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { | ||
| 1645 | if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; | ||
| 1646 | if (child_host.len == parent_host.len) return true; | ||
| 1647 | if (parent_host.len > child_host.len) return false; | ||
| 1648 | return child_host[child_host.len - parent_host.len - 1] == '.'; | ||
| 1649 | } | ||
| 1650 | |||
| 1651 | test sameParentDomain { | ||
| 1652 | try testing.expect(!sameParentDomain("foo.com", "bar.com")); | ||
| 1653 | try testing.expect(sameParentDomain("foo.com", "foo.com")); | ||
| 1654 | try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); | ||
| 1655 | try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); | ||
| 1656 | } | ||
| 1657 | |||
| 1831 | test { | 1658 | test { |
| 1832 | _ = Response; | 1659 | _ = Response; |
| 1833 | _ = &initDefaultProxies; | ||
| 1834 | } | 1660 | } |
lib/std/http/Server.zig+41-757| ... | @@ -1,142 +1,59 @@ | ... | @@ -1,142 +1,59 @@ |
| 1 | //! Blocking HTTP server implementation. | 1 | //! Handles a single connection lifecycle. |
| 2 | //! Handles a single connection's lifecycle. | ||
| 3 | 2 | ||
| 4 | const std = @import("../std.zig"); | 3 | const std = @import("../std.zig"); |
| 5 | const http = std.http; | 4 | const http = std.http; |
| 6 | const mem = std.mem; | 5 | const mem = std.mem; |
| 7 | const net = std.net; | ||
| 8 | const Uri = std.Uri; | 6 | const Uri = std.Uri; |
| 9 | const assert = std.debug.assert; | 7 | const assert = std.debug.assert; |
| 10 | const testing = std.testing; | 8 | const testing = std.testing; |
| 11 | 9 | ||
| 12 | const Server = @This(); | 10 | const Server = @This(); |
| 13 | 11 | ||
| 14 | /// The reader's buffer must be large enough to store the client's entire HTTP | ||
| 15 | /// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. | ||
| 16 | in: *std.io.BufferedReader, | ||
| 17 | /// Data from the HTTP server to the HTTP client. | 12 | /// Data from the HTTP server to the HTTP client. |
| 18 | out: *std.io.BufferedWriter, | 13 | out: *std.io.BufferedWriter, |
| 19 | /// Keeps track of whether the Server is ready to accept a new request on the | 14 | /// Internal state managed by this abstraction. |
| 20 | /// same connection, and makes invalid API usage cause assertion failures | 15 | reader: http.Reader, |
| 21 | /// rather than HTTP protocol violations. | ||
| 22 | state: State, | ||
| 23 | /// Populated when `receiveHead` returns `ReceiveHeadError.HttpHeadersInvalid`. | ||
| 24 | head_parse_err: ?Request.Head.ParseError = null, | ||
| 25 | |||
| 26 | pub const State = enum { | ||
| 27 | /// The connection is available to be used for the first time, or reused. | ||
| 28 | ready, | ||
| 29 | /// An error occurred in `receiveHead`. | ||
| 30 | receiving_head, | ||
| 31 | /// A Request object has been obtained and from there a Response can be | ||
| 32 | /// opened. | ||
| 33 | received_head, | ||
| 34 | /// The client is uploading something to this Server. | ||
| 35 | receiving_body, | ||
| 36 | /// The connection is eligible for another HTTP request, however the client | ||
| 37 | /// and server did not negotiate a persistent connection. | ||
| 38 | closing, | ||
| 39 | }; | ||
| 40 | 16 | ||
| 41 | /// Initialize an HTTP server that can respond to multiple requests on the same | 17 | /// Initialize an HTTP server that can respond to multiple requests on the same |
| 42 | /// connection. | 18 | /// connection. |
| 43 | /// | 19 | /// |
| 20 | /// The buffer of `in` must be large enough to store the client's entire HTTP | ||
| 21 | /// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. | ||
| 22 | /// | ||
| 44 | /// The returned `Server` is ready for `receiveHead` to be called. | 23 | /// The returned `Server` is ready for `receiveHead` to be called. |
| 45 | pub fn init(in: *std.io.BufferedReader, out: *std.io.BufferedWriter) Server { | 24 | pub fn init(in: *std.io.BufferedReader, out: *std.io.BufferedWriter) Server { |
| 46 | return .{ | 25 | return .{ |
| 47 | .in = in, | 26 | .reader = .{ |
| 27 | .in = in, | ||
| 28 | .state = .ready, | ||
| 29 | }, | ||
| 48 | .out = out, | 30 | .out = out, |
| 49 | .state = .ready, | ||
| 50 | }; | 31 | }; |
| 51 | } | 32 | } |
| 52 | 33 | ||
| 53 | pub const ReceiveHeadError = error{ | 34 | pub const ReceiveHeadError = http.Reader.HeadError || error{ |
| 54 | /// Client sent too many bytes of HTTP headers. | 35 | /// Client sent headers that did not conform to the HTTP protocol. |
| 55 | /// The HTTP specification suggests to respond with a 431 status code | 36 | /// |
| 56 | /// before closing the connection. | 37 | /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be |
| 57 | HttpHeadersOversize, | 38 | /// passed directly to `Request.Head.parse`. |
| 58 | /// Client sent headers that did not conform to the HTTP protocol; | ||
| 59 | /// `head_parse_err` is populated. | ||
| 60 | HttpHeadersInvalid, | 39 | HttpHeadersInvalid, |
| 61 | /// Partial HTTP request was received but the connection was closed before | ||
| 62 | /// fully receiving the headers. | ||
| 63 | HttpRequestTruncated, | ||
| 64 | /// The client sent 0 bytes of headers before closing the stream. | ||
| 65 | /// In other words, a keep-alive connection was finally closed. | ||
| 66 | HttpConnectionClosing, | ||
| 67 | /// Transitive error occurred reading from `in`. | ||
| 68 | ReadFailed, | ||
| 69 | }; | 40 | }; |
| 70 | 41 | ||
| 71 | /// The header bytes reference the internal storage of `in`, which are | 42 | pub fn receiveHead(s: *Server) http.Reader.HeadError!Request { |
| 72 | /// invalidated with the next call to `receiveHead`. | 43 | try s.reader.receiveHead(); |
| 73 | pub fn receiveHead(s: *Server) ReceiveHeadError!Request { | 44 | return .{ |
| 74 | assert(s.state == .ready); | 45 | .server = s, |
| 75 | s.state = .received_head; | 46 | // No need to track the returned error here since users can repeat the |
| 76 | errdefer s.state = .receiving_head; | 47 | // parse with the header buffer to get detailed diagnostics. |
| 77 | 48 | .head = Request.Head.parse(s.reader.head_buffer) catch return error.HttpHeadersInvalid, | |
| 78 | const in = s.in; | 49 | }; |
| 79 | var hp: http.HeadParser = .{}; | ||
| 80 | var head_end: usize = 0; | ||
| 81 | |||
| 82 | while (true) { | ||
| 83 | if (head_end >= in.buffer.len) return error.HttpHeadersOversize; | ||
| 84 | const buf = in.peekGreedy(head_end + 1) catch |err| switch (err) { | ||
| 85 | error.EndOfStream => switch (head_end) { | ||
| 86 | 0 => return error.HttpConnectionClosing, | ||
| 87 | else => return error.HttpRequestTruncated, | ||
| 88 | }, | ||
| 89 | error.ReadFailed => return error.ReadFailed, | ||
| 90 | }; | ||
| 91 | head_end += hp.feed(buf[head_end..]); | ||
| 92 | if (hp.state == .finished) return .{ | ||
| 93 | .server = s, | ||
| 94 | .head_end = head_end, | ||
| 95 | .head = Request.Head.parse(buf[0..head_end]) catch |err| { | ||
| 96 | s.head_parse_err = err; | ||
| 97 | return error.HttpHeadersInvalid; | ||
| 98 | }, | ||
| 99 | .reader_state = undefined, | ||
| 100 | }; | ||
| 101 | } | ||
| 102 | } | 50 | } |
| 103 | 51 | ||
| 104 | pub const Request = struct { | 52 | pub const Request = struct { |
| 105 | server: *Server, | 53 | server: *Server, |
| 106 | /// Index into `Server.in` internal buffer. | 54 | /// Pointers in this struct are invalidated with the next call to |
| 107 | head_end: usize, | 55 | /// `receiveHead`. |
| 108 | /// Number of bytes of HTTP trailers. These are at the end of a | ||
| 109 | /// transfer-encoding: chunked message. | ||
| 110 | trailers_len: usize = 0, | ||
| 111 | head: Head, | 56 | head: Head, |
| 112 | reader_state: union { | ||
| 113 | remaining_content_length: u64, | ||
| 114 | remaining_chunk_len: RemainingChunkLen, | ||
| 115 | }, | ||
| 116 | read_err: ?ReadError = null, | ||
| 117 | |||
| 118 | pub const ReadError = error{ | ||
| 119 | HttpChunkInvalid, | ||
| 120 | HttpHeadersOversize, | ||
| 121 | }; | ||
| 122 | |||
| 123 | pub const max_chunk_header_len = 22; | ||
| 124 | |||
| 125 | pub const RemainingChunkLen = enum(u64) { | ||
| 126 | head = 0, | ||
| 127 | n = 1, | ||
| 128 | rn = 2, | ||
| 129 | done = std.math.maxInt(u64), | ||
| 130 | _, | ||
| 131 | |||
| 132 | pub fn init(integer: u64) RemainingChunkLen { | ||
| 133 | return @enumFromInt(integer); | ||
| 134 | } | ||
| 135 | |||
| 136 | pub fn int(rcl: RemainingChunkLen) u64 { | ||
| 137 | return @intFromEnum(rcl); | ||
| 138 | } | ||
| 139 | }; | ||
| 140 | 57 | ||
| 141 | pub const Compression = union(enum) { | 58 | pub const Compression = union(enum) { |
| 142 | deflate: std.compress.zlib.Decompressor, | 59 | deflate: std.compress.zlib.Decompressor, |
| ... | @@ -308,7 +225,7 @@ pub const Request = struct { | ... | @@ -308,7 +225,7 @@ pub const Request = struct { |
| 308 | }; | 225 | }; |
| 309 | 226 | ||
| 310 | pub fn iterateHeaders(r: *Request) http.HeaderIterator { | 227 | pub fn iterateHeaders(r: *Request) http.HeaderIterator { |
| 311 | return http.HeaderIterator.init(r.server.in.bufferContents()[0..r.head_end]); | 228 | return http.HeaderIterator.init(r.server.reader.head_buffer); |
| 312 | } | 229 | } |
| 313 | 230 | ||
| 314 | test iterateHeaders { | 231 | test iterateHeaders { |
| ... | @@ -332,10 +249,8 @@ pub const Request = struct { | ... | @@ -332,10 +249,8 @@ pub const Request = struct { |
| 332 | 249 | ||
| 333 | var request: Request = .{ | 250 | var request: Request = .{ |
| 334 | .server = &server, | 251 | .server = &server, |
| 335 | .head_end = request_bytes.len, | ||
| 336 | .trailers_len = 0, | 252 | .trailers_len = 0, |
| 337 | .head = undefined, | 253 | .head = undefined, |
| 338 | .reader_state = undefined, | ||
| 339 | }; | 254 | }; |
| 340 | 255 | ||
| 341 | var it = request.iterateHeaders(); | 256 | var it = request.iterateHeaders(); |
| ... | @@ -511,7 +426,8 @@ pub const Request = struct { | ... | @@ -511,7 +426,8 @@ pub const Request = struct { |
| 511 | respond_options: RespondOptions = .{}, | 426 | respond_options: RespondOptions = .{}, |
| 512 | }; | 427 | }; |
| 513 | 428 | ||
| 514 | /// The header is not guaranteed to be sent until `Response.flush` is called. | 429 | /// The header is not guaranteed to be sent until `BodyWriter.flush` or |
| 430 | /// `BodyWriter.end` is called. | ||
| 515 | /// | 431 | /// |
| 516 | /// If the request contains a body and the connection is to be reused, | 432 | /// If the request contains a body and the connection is to be reused, |
| 517 | /// discards the request body, leaving the Server in the `ready` state. If | 433 | /// discards the request body, leaving the Server in the `ready` state. If |
| ... | @@ -519,13 +435,13 @@ pub const Request = struct { | ... | @@ -519,13 +435,13 @@ pub const Request = struct { |
| 519 | /// no error is surfaced. | 435 | /// no error is surfaced. |
| 520 | /// | 436 | /// |
| 521 | /// HEAD requests are handled transparently by setting the | 437 | /// HEAD requests are handled transparently by setting the |
| 522 | /// `Response.elide_body` flag on the returned `Response`, causing | 438 | /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing |
| 523 | /// the response stream to omit the body. However, it may be worth noticing | 439 | /// the response stream to omit the body. However, it may be worth noticing |
| 524 | /// that flag and skipping any expensive work that would otherwise need to | 440 | /// that flag and skipping any expensive work that would otherwise need to |
| 525 | /// be done to satisfy the request. | 441 | /// be done to satisfy the request. |
| 526 | /// | 442 | /// |
| 527 | /// Asserts status is not `continue`. | 443 | /// Asserts status is not `continue`. |
| 528 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) std.io.Writer.Error!Response { | 444 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) std.io.Writer.Error!http.BodyWriter { |
| 529 | const o = options.respond_options; | 445 | const o = options.respond_options; |
| 530 | assert(o.status != .@"continue"); | 446 | assert(o.status != .@"continue"); |
| 531 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; | 447 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; |
| ... | @@ -573,7 +489,7 @@ pub const Request = struct { | ... | @@ -573,7 +489,7 @@ pub const Request = struct { |
| 573 | }; | 489 | }; |
| 574 | 490 | ||
| 575 | return .{ | 491 | return .{ |
| 576 | .server_output = request.server.out, | 492 | .http_protocol_output = request.server.out, |
| 577 | .transfer_encoding = if (o.transfer_encoding) |te| switch (te) { | 493 | .transfer_encoding = if (o.transfer_encoding) |te| switch (te) { |
| 578 | .chunked => .{ .chunked = .init }, | 494 | .chunked => .{ .chunked = .init }, |
| 579 | .none => .none, | 495 | .none => .none, |
| ... | @@ -584,242 +500,6 @@ pub const Request = struct { | ... | @@ -584,242 +500,6 @@ pub const Request = struct { |
| 584 | }; | 500 | }; |
| 585 | } | 501 | } |
| 586 | 502 | ||
| 587 | fn contentLengthRead( | ||
| 588 | ctx: ?*anyopaque, | ||
| 589 | bw: *std.io.BufferedWriter, | ||
| 590 | limit: std.io.Reader.Limit, | ||
| 591 | ) std.io.Reader.RwError!usize { | ||
| 592 | const request: *Request = @alignCast(@ptrCast(ctx)); | ||
| 593 | const remaining_content_length = &request.reader_state.remaining_content_length; | ||
| 594 | const remaining = remaining_content_length.*; | ||
| 595 | const server = request.server; | ||
| 596 | if (remaining == 0) { | ||
| 597 | server.state = .ready; | ||
| 598 | return error.EndOfStream; | ||
| 599 | } | ||
| 600 | const n = try server.in.read(bw, limit.min(.limited(remaining))); | ||
| 601 | const new_remaining = remaining - n; | ||
| 602 | remaining_content_length.* = new_remaining; | ||
| 603 | return n; | ||
| 604 | } | ||
| 605 | |||
| 606 | fn contentLengthReadVec(context: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { | ||
| 607 | const request: *Request = @alignCast(@ptrCast(context)); | ||
| 608 | const remaining_content_length = &request.reader_state.remaining_content_length; | ||
| 609 | const server = request.server; | ||
| 610 | const remaining = remaining_content_length.*; | ||
| 611 | if (remaining == 0) { | ||
| 612 | server.state = .ready; | ||
| 613 | return error.EndOfStream; | ||
| 614 | } | ||
| 615 | const n = try server.in.readVecLimit(data, .limited(remaining)); | ||
| 616 | const new_remaining = remaining - n; | ||
| 617 | remaining_content_length.* = new_remaining; | ||
| 618 | return n; | ||
| 619 | } | ||
| 620 | |||
| 621 | fn contentLengthDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { | ||
| 622 | const request: *Request = @alignCast(@ptrCast(ctx)); | ||
| 623 | const remaining_content_length = &request.reader_state.remaining_content_length; | ||
| 624 | const server = request.server; | ||
| 625 | const remaining = remaining_content_length.*; | ||
| 626 | if (remaining == 0) { | ||
| 627 | server.state = .ready; | ||
| 628 | return error.EndOfStream; | ||
| 629 | } | ||
| 630 | const n = try server.in.discard(limit.min(.limited(remaining))); | ||
| 631 | const new_remaining = remaining - n; | ||
| 632 | remaining_content_length.* = new_remaining; | ||
| 633 | return n; | ||
| 634 | } | ||
| 635 | |||
| 636 | fn chunkedRead( | ||
| 637 | ctx: ?*anyopaque, | ||
| 638 | bw: *std.io.BufferedWriter, | ||
| 639 | limit: std.io.Reader.Limit, | ||
| 640 | ) std.io.Reader.RwError!usize { | ||
| 641 | const request: *Request = @alignCast(@ptrCast(ctx)); | ||
| 642 | const chunk_len_ptr = &request.reader_state.remaining_chunk_len; | ||
| 643 | const in = request.server.in; | ||
| 644 | len: switch (chunk_len_ptr.*) { | ||
| 645 | .head => { | ||
| 646 | var cp: http.ChunkParser = .init; | ||
| 647 | const i = cp.feed(in.bufferContents()); | ||
| 648 | switch (cp.state) { | ||
| 649 | .invalid => return request.failRead(error.HttpChunkInvalid), | ||
| 650 | .data => { | ||
| 651 | if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 652 | in.toss(i); | ||
| 653 | }, | ||
| 654 | else => { | ||
| 655 | try in.fill(max_chunk_header_len); | ||
| 656 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 657 | if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); | ||
| 658 | const header_len = i + next_i; | ||
| 659 | if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 660 | in.toss(header_len); | ||
| 661 | }, | ||
| 662 | } | ||
| 663 | if (cp.chunk_len == 0) return parseTrailers(request, 0); | ||
| 664 | const n = try in.read(bw, limit.min(.limited(cp.chunk_len))); | ||
| 665 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | ||
| 666 | return n; | ||
| 667 | }, | ||
| 668 | .n => { | ||
| 669 | if ((try in.peekByte()) != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 670 | in.toss(1); | ||
| 671 | continue :len .head; | ||
| 672 | }, | ||
| 673 | .rn => { | ||
| 674 | const rn = try in.peekArray(2); | ||
| 675 | if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 676 | in.toss(2); | ||
| 677 | continue :len .head; | ||
| 678 | }, | ||
| 679 | else => |remaining_chunk_len| { | ||
| 680 | const n = try in.read(bw, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); | ||
| 681 | chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); | ||
| 682 | return n; | ||
| 683 | }, | ||
| 684 | .done => return error.EndOfStream, | ||
| 685 | } | ||
| 686 | } | ||
| 687 | |||
| 688 | fn chunkedReadVec(ctx: ?*anyopaque, data: []const []u8) std.io.Reader.Error!usize { | ||
| 689 | const request: *Request = @alignCast(@ptrCast(ctx)); | ||
| 690 | const chunk_len_ptr = &request.reader_state.remaining_chunk_len; | ||
| 691 | const in = request.server.in; | ||
| 692 | var already_requested_more = false; | ||
| 693 | var amt_read: usize = 0; | ||
| 694 | data: for (data) |d| { | ||
| 695 | len: switch (chunk_len_ptr.*) { | ||
| 696 | .head => { | ||
| 697 | var cp: http.ChunkParser = .init; | ||
| 698 | const available_buffer = in.bufferContents(); | ||
| 699 | const i = cp.feed(available_buffer); | ||
| 700 | if (cp.state == .invalid) return request.failRead(error.HttpChunkInvalid); | ||
| 701 | if (i == available_buffer.len) { | ||
| 702 | if (already_requested_more) { | ||
| 703 | chunk_len_ptr.* = .head; | ||
| 704 | return amt_read; | ||
| 705 | } | ||
| 706 | already_requested_more = true; | ||
| 707 | try in.fill(max_chunk_header_len); | ||
| 708 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 709 | if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); | ||
| 710 | const header_len = i + next_i; | ||
| 711 | if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 712 | in.toss(header_len); | ||
| 713 | } else { | ||
| 714 | if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 715 | in.toss(i); | ||
| 716 | } | ||
| 717 | if (cp.chunk_len == 0) return parseTrailers(request, amt_read); | ||
| 718 | continue :len .init(cp.chunk_len + 2); | ||
| 719 | }, | ||
| 720 | .n => { | ||
| 721 | if (in.bufferContents().len < 1) already_requested_more = true; | ||
| 722 | if ((try in.takeByte()) != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 723 | continue :len .head; | ||
| 724 | }, | ||
| 725 | .rn => { | ||
| 726 | if (in.bufferContents().len < 2) already_requested_more = true; | ||
| 727 | const rn = try in.takeArray(2); | ||
| 728 | if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 729 | continue :len .head; | ||
| 730 | }, | ||
| 731 | else => |remaining_chunk_len| { | ||
| 732 | const available_buffer = in.bufferContents(); | ||
| 733 | const copy_len = @min(available_buffer.len, d.len, remaining_chunk_len.int() - 2); | ||
| 734 | @memcpy(d[0..copy_len], available_buffer[0..copy_len]); | ||
| 735 | amt_read += copy_len; | ||
| 736 | in.toss(copy_len); | ||
| 737 | const next_chunk_len: RemainingChunkLen = .init(remaining_chunk_len.int() - copy_len); | ||
| 738 | if (copy_len == d.len) { | ||
| 739 | chunk_len_ptr.* = next_chunk_len; | ||
| 740 | continue :data; | ||
| 741 | } | ||
| 742 | if (already_requested_more) { | ||
| 743 | chunk_len_ptr.* = next_chunk_len; | ||
| 744 | return amt_read; | ||
| 745 | } | ||
| 746 | already_requested_more = true; | ||
| 747 | try in.fill(3); | ||
| 748 | continue :len next_chunk_len; | ||
| 749 | }, | ||
| 750 | .done => return error.EndOfStream, | ||
| 751 | } | ||
| 752 | } | ||
| 753 | return amt_read; | ||
| 754 | } | ||
| 755 | |||
| 756 | fn chunkedDiscard(ctx: ?*anyopaque, limit: std.io.Reader.Limit) std.io.Reader.Error!usize { | ||
| 757 | const request: *Request = @alignCast(@ptrCast(ctx)); | ||
| 758 | const chunk_len_ptr = &request.reader_state.remaining_chunk_len; | ||
| 759 | const in = request.server.in; | ||
| 760 | len: switch (chunk_len_ptr.*) { | ||
| 761 | .head => { | ||
| 762 | var cp: http.ChunkParser = .init; | ||
| 763 | const i = cp.feed(in.bufferContents()); | ||
| 764 | switch (cp.state) { | ||
| 765 | .invalid => return request.failRead(error.HttpChunkInvalid), | ||
| 766 | .data => { | ||
| 767 | if (i > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 768 | in.toss(i); | ||
| 769 | }, | ||
| 770 | else => { | ||
| 771 | try in.fill(max_chunk_header_len); | ||
| 772 | const next_i = cp.feed(in.bufferContents()[i..]); | ||
| 773 | if (cp.state != .data) return request.failRead(error.HttpChunkInvalid); | ||
| 774 | const header_len = i + next_i; | ||
| 775 | if (header_len > max_chunk_header_len) return request.failRead(error.HttpChunkInvalid); | ||
| 776 | in.toss(header_len); | ||
| 777 | }, | ||
| 778 | } | ||
| 779 | if (cp.chunk_len == 0) return parseTrailers(request, 0); | ||
| 780 | const n = try in.discard(limit.min(.limited(cp.chunk_len))); | ||
| 781 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | ||
| 782 | return n; | ||
| 783 | }, | ||
| 784 | .n => { | ||
| 785 | if ((try in.peekByte()) != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 786 | in.toss(1); | ||
| 787 | continue :len .head; | ||
| 788 | }, | ||
| 789 | .rn => { | ||
| 790 | const rn = try in.peekArray(2); | ||
| 791 | if (rn[0] != '\r' or rn[1] != '\n') return request.failRead(error.HttpChunkInvalid); | ||
| 792 | in.toss(2); | ||
| 793 | continue :len .head; | ||
| 794 | }, | ||
| 795 | else => |remaining_chunk_len| { | ||
| 796 | const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2))); | ||
| 797 | chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); | ||
| 798 | return n; | ||
| 799 | }, | ||
| 800 | .done => return error.EndOfStream, | ||
| 801 | } | ||
| 802 | } | ||
| 803 | |||
| 804 | /// Called when next bytes in the stream are trailers, or "\r\n" to indicate | ||
| 805 | /// end of chunked body. | ||
| 806 | fn parseTrailers(request: *Request, amt_read: usize) std.io.Reader.Error!usize { | ||
| 807 | const in = request.server.in; | ||
| 808 | var hp: http.HeadParser = .{}; | ||
| 809 | var trailers_len: usize = 0; | ||
| 810 | while (true) { | ||
| 811 | if (trailers_len >= in.buffer.len) return request.failRead(error.HttpHeadersOversize); | ||
| 812 | try in.fill(trailers_len + 1); | ||
| 813 | trailers_len += hp.feed(in.bufferContents()[trailers_len..]); | ||
| 814 | if (hp.state == .finished) { | ||
| 815 | request.reader_state.remaining_chunk_len = .done; | ||
| 816 | request.server.state = .ready; | ||
| 817 | request.trailers_len = trailers_len; | ||
| 818 | return amt_read; | ||
| 819 | } | ||
| 820 | } | ||
| 821 | } | ||
| 822 | |||
| 823 | pub const ReaderError = error{ | 503 | pub const ReaderError = error{ |
| 824 | /// Failed to write "100-continue" to the stream. | 504 | /// Failed to write "100-continue" to the stream. |
| 825 | WriteFailed, | 505 | WriteFailed, |
| ... | @@ -837,10 +517,7 @@ pub const Request = struct { | ... | @@ -837,10 +517,7 @@ pub const Request = struct { |
| 837 | /// | 517 | /// |
| 838 | /// Asserts that this function is only called once. | 518 | /// Asserts that this function is only called once. |
| 839 | pub fn reader(request: *Request) ReaderError!std.io.Reader { | 519 | pub fn reader(request: *Request) ReaderError!std.io.Reader { |
| 840 | const s = request.server; | 520 | assert(request.server.reader.state == .received_head); |
| 841 | assert(s.state == .received_head); | ||
| 842 | s.state = .receiving_body; | ||
| 843 | |||
| 844 | if (request.head.expect) |expect| { | 521 | if (request.head.expect) |expect| { |
| 845 | if (mem.eql(u8, expect, "100-continue")) { | 522 | if (mem.eql(u8, expect, "100-continue")) { |
| 846 | try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); | 523 | try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); |
| ... | @@ -849,36 +526,11 @@ pub const Request = struct { | ... | @@ -849,36 +526,11 @@ pub const Request = struct { |
| 849 | return error.HttpExpectationFailed; | 526 | return error.HttpExpectationFailed; |
| 850 | } | 527 | } |
| 851 | } | 528 | } |
| 852 | 529 | return request.server.reader.interface(request.head.transfer_encoding, request.head.content_length); | |
| 853 | switch (request.head.transfer_encoding) { | ||
| 854 | .chunked => { | ||
| 855 | request.reader_state = .{ .remaining_chunk_len = .head }; | ||
| 856 | return .{ | ||
| 857 | .context = request, | ||
| 858 | .vtable = &.{ | ||
| 859 | .read = &chunkedRead, | ||
| 860 | .readVec = &chunkedReadVec, | ||
| 861 | .discard = &chunkedDiscard, | ||
| 862 | }, | ||
| 863 | }; | ||
| 864 | }, | ||
| 865 | .none => { | ||
| 866 | request.reader_state = .{ | ||
| 867 | .remaining_content_length = request.head.content_length orelse 0, | ||
| 868 | }; | ||
| 869 | return .{ | ||
| 870 | .context = request, | ||
| 871 | .vtable = &.{ | ||
| 872 | .read = &contentLengthRead, | ||
| 873 | .readVec = &contentLengthReadVec, | ||
| 874 | .discard = &contentLengthDiscard, | ||
| 875 | }, | ||
| 876 | }; | ||
| 877 | }, | ||
| 878 | } | ||
| 879 | } | 530 | } |
| 880 | 531 | ||
| 881 | /// Returns whether the connection should remain persistent. | 532 | /// Returns whether the connection should remain persistent. |
| 533 | /// | ||
| 882 | /// If it would fail, it instead sets the Server state to `receiving_body` | 534 | /// If it would fail, it instead sets the Server state to `receiving_body` |
| 883 | /// and returns false. | 535 | /// and returns false. |
| 884 | fn discardBody(request: *Request, keep_alive: bool) bool { | 536 | fn discardBody(request: *Request, keep_alive: bool) bool { |
| ... | @@ -890,12 +542,12 @@ pub const Request = struct { | ... | @@ -890,12 +542,12 @@ pub const Request = struct { |
| 890 | // or the request body. | 542 | // or the request body. |
| 891 | // If the connection won't be kept alive, then none of this matters | 543 | // If the connection won't be kept alive, then none of this matters |
| 892 | // because the connection will be severed after the response is sent. | 544 | // because the connection will be severed after the response is sent. |
| 893 | const s = request.server; | 545 | const r = &request.server.reader; |
| 894 | if (keep_alive and request.head.keep_alive) switch (s.state) { | 546 | if (keep_alive and request.head.keep_alive) switch (r.state) { |
| 895 | .received_head => { | 547 | .received_head => { |
| 896 | const r = request.reader() catch return false; | 548 | const reader_interface = request.reader() catch return false; |
| 897 | _ = r.discardRemaining() catch return false; | 549 | _ = reader_interface.discardRemaining() catch return false; |
| 898 | assert(s.state == .ready); | 550 | assert(r.state == .ready); |
| 899 | return true; | 551 | return true; |
| 900 | }, | 552 | }, |
| 901 | .receiving_body, .ready => return true, | 553 | .receiving_body, .ready => return true, |
| ... | @@ -903,378 +555,10 @@ pub const Request = struct { | ... | @@ -903,378 +555,10 @@ pub const Request = struct { |
| 903 | }; | 555 | }; |
| 904 | 556 | ||
| 905 | // Avoid clobbering the state in case a reading stream already exists. | 557 | // Avoid clobbering the state in case a reading stream already exists. |
| 906 | switch (s.state) { | 558 | switch (r.state) { |
| 907 | .received_head => s.state = .closing, | 559 | .received_head => r.state = .closing, |
| 908 | else => {}, | 560 | else => {}, |
| 909 | } | 561 | } |
| 910 | return false; | 562 | return false; |
| 911 | } | 563 | } |
| 912 | |||
| 913 | fn failRead(r: *Request, err: ReadError) error{ReadFailed} { | ||
| 914 | r.read_err = err; | ||
| 915 | return error.ReadFailed; | ||
| 916 | } | ||
| 917 | }; | ||
| 918 | |||
| 919 | pub const Response = struct { | ||
| 920 | /// HTTP protocol to the client. | ||
| 921 | /// | ||
| 922 | /// This is the underlying stream; use `buffered` to create a | ||
| 923 | /// `BufferedWriter` for this `Response`. | ||
| 924 | /// | ||
| 925 | /// Until the lifetime of `Response` ends, it is illegal to modify the | ||
| 926 | /// state of this other than via methods of `Response`. | ||
| 927 | server_output: *std.io.BufferedWriter, | ||
| 928 | /// `null` means transfer-encoding: chunked. | ||
| 929 | /// As a debugging utility, counts down to zero as bytes are written. | ||
| 930 | transfer_encoding: TransferEncoding, | ||
| 931 | elide_body: bool, | ||
| 932 | err: Error!void = {}, | ||
| 933 | |||
| 934 | pub const Error = error{ | ||
| 935 | /// Attempted to write a file to the stream, an expensive operation | ||
| 936 | /// that should be avoided when `elide_body` is true. | ||
| 937 | UnableToElideBody, | ||
| 938 | }; | ||
| 939 | pub const WriteError = std.io.Writer.Error; | ||
| 940 | |||
| 941 | /// How many zeroes to reserve for hex-encoded chunk length. | ||
| 942 | const chunk_len_digits = 8; | ||
| 943 | const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; | ||
| 944 | const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; | ||
| 945 | |||
| 946 | comptime { | ||
| 947 | assert(max_chunk_len == std.math.maxInt(u32)); | ||
| 948 | } | ||
| 949 | |||
| 950 | pub const TransferEncoding = union(enum) { | ||
| 951 | /// End of connection signals the end of the stream. | ||
| 952 | none, | ||
| 953 | /// As a debugging utility, counts down to zero as bytes are written. | ||
| 954 | content_length: u64, | ||
| 955 | /// Each chunk is wrapped in a header and trailer. | ||
| 956 | chunked: Chunked, | ||
| 957 | |||
| 958 | pub const Chunked = union(enum) { | ||
| 959 | /// Index of the hex-encoded chunk length in the chunk header | ||
| 960 | /// within the buffer of `Response.server_output`. | ||
| 961 | offset: usize, | ||
| 962 | /// We are in the middle of a chunk and this is how many bytes are | ||
| 963 | /// left until the next header. This includes +2 for "\r"\n", and | ||
| 964 | /// is zero for the beginning of the stream. | ||
| 965 | chunk_len: usize, | ||
| 966 | |||
| 967 | pub const init: Chunked = .{ .chunk_len = 0 }; | ||
| 968 | }; | ||
| 969 | }; | ||
| 970 | |||
| 971 | /// Sends all buffered data across `Response.server_output`. | ||
| 972 | /// | ||
| 973 | /// Some buffered data will remain if transfer-encoding is chunked and the | ||
| 974 | /// response is mid-chunk. | ||
| 975 | pub fn flush(r: *Response) WriteError!void { | ||
| 976 | switch (r.transfer_encoding) { | ||
| 977 | .none, .content_length => return r.server_output.flush(), | ||
| 978 | .chunked => |*chunked| switch (chunked.*) { | ||
| 979 | .offset => |*offset| { | ||
| 980 | try r.server_output.flushLimit(.limited(r.server_output.end - offset.*)); | ||
| 981 | offset.* = 0; | ||
| 982 | }, | ||
| 983 | .chunk_len => return r.server_output.flush(), | ||
| 984 | }, | ||
| 985 | } | ||
| 986 | } | ||
| 987 | |||
| 988 | /// When using content-length, asserts that the amount of data sent matches | ||
| 989 | /// the value sent in the header, then flushes. Asserts the amount of bytes | ||
| 990 | /// sent matches the content-length value provided in the HTTP header. | ||
| 991 | /// | ||
| 992 | /// When using transfer-encoding: chunked, writes the end-of-stream message | ||
| 993 | /// with empty trailers, then flushes the stream to the system. Asserts any | ||
| 994 | /// started chunk has been completely finished. | ||
| 995 | /// | ||
| 996 | /// Respects the value of `elide_body` to omit all data after the headers. | ||
| 997 | /// | ||
| 998 | /// Sets `r` to undefined. | ||
| 999 | /// | ||
| 1000 | /// See also: | ||
| 1001 | /// * `endUnflushed` | ||
| 1002 | /// * `endChunked` | ||
| 1003 | pub fn end(r: *Response) WriteError!void { | ||
| 1004 | try endUnflushed(r); | ||
| 1005 | try r.server_output.flush(); | ||
| 1006 | r.* = undefined; | ||
| 1007 | } | ||
| 1008 | |||
| 1009 | /// When using content-length, asserts that the amount of data sent matches | ||
| 1010 | /// the value sent in the header. | ||
| 1011 | /// | ||
| 1012 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the | ||
| 1013 | /// end-of-stream message with empty trailers. | ||
| 1014 | /// | ||
| 1015 | /// Respects the value of `elide_body` to omit all data after the headers. | ||
| 1016 | /// | ||
| 1017 | /// See also: | ||
| 1018 | /// * `end` | ||
| 1019 | /// * `endChunked` | ||
| 1020 | pub fn endUnflushed(r: *Response) WriteError!void { | ||
| 1021 | switch (r.transfer_encoding) { | ||
| 1022 | .content_length => |len| assert(len == 0), // Trips when end() called before all bytes written. | ||
| 1023 | .none => {}, | ||
| 1024 | .chunked => try endChunked(r, .{}), | ||
| 1025 | } | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | pub const EndChunkedOptions = struct { | ||
| 1029 | trailers: []const http.Header = &.{}, | ||
| 1030 | }; | ||
| 1031 | |||
| 1032 | /// Writes the end-of-stream message and any optional trailers. | ||
| 1033 | /// | ||
| 1034 | /// Does not flush. | ||
| 1035 | /// | ||
| 1036 | /// Asserts that the Response is using transfer-encoding: chunked. | ||
| 1037 | /// | ||
| 1038 | /// Respects the value of `elide_body` to omit all data after the headers. | ||
| 1039 | /// | ||
| 1040 | /// See also: | ||
| 1041 | /// * `end` | ||
| 1042 | /// * `endUnflushed` | ||
| 1043 | pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void { | ||
| 1044 | const chunked = &r.transfer_encoding.chunked; | ||
| 1045 | if (r.elide_body) return; | ||
| 1046 | const bw = r.server_output; | ||
| 1047 | switch (chunked.*) { | ||
| 1048 | .offset => |offset| { | ||
| 1049 | const chunk_len = bw.end - offset - chunk_header_template.len; | ||
| 1050 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| 1051 | try bw.writeAll("\r\n"); | ||
| 1052 | }, | ||
| 1053 | .chunk_len => |chunk_len| switch (chunk_len) { | ||
| 1054 | 0 => {}, | ||
| 1055 | 1 => try bw.writeByte('\n'), | ||
| 1056 | 2 => try bw.writeAll("\r\n"), | ||
| 1057 | else => unreachable, // An earlier write call indicated more data would follow. | ||
| 1058 | }, | ||
| 1059 | } | ||
| 1060 | if (options.trailers.len > 0) { | ||
| 1061 | try bw.writeAll("0\r\n"); | ||
| 1062 | for (options.trailers) |trailer| { | ||
| 1063 | try bw.writeAll(trailer.name); | ||
| 1064 | try bw.writeAll(": "); | ||
| 1065 | try bw.writeAll(trailer.value); | ||
| 1066 | try bw.writeAll("\r\n"); | ||
| 1067 | } | ||
| 1068 | try bw.writeAll("\r\n"); | ||
| 1069 | } | ||
| 1070 | r.* = undefined; | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | fn contentLengthWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 1074 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1075 | const n = if (r.elide_body) countSplat(data, splat) else try r.server_output.writeSplat(data, splat); | ||
| 1076 | r.transfer_encoding.content_length -= n; | ||
| 1077 | return n; | ||
| 1078 | } | ||
| 1079 | |||
| 1080 | fn noneWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 1081 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1082 | if (r.elide_body) return countSplat(data, splat); | ||
| 1083 | return r.server_output.writeSplat(data, splat); | ||
| 1084 | } | ||
| 1085 | |||
| 1086 | fn countSplat(data: []const []const u8, splat: usize) usize { | ||
| 1087 | if (data.len == 0) return 0; | ||
| 1088 | var total: usize = 0; | ||
| 1089 | for (data[0 .. data.len - 1]) |buf| total += buf.len; | ||
| 1090 | total += data[data.len - 1].len * splat; | ||
| 1091 | return total; | ||
| 1092 | } | ||
| 1093 | |||
| 1094 | fn elideWriteFile( | ||
| 1095 | r: *Response, | ||
| 1096 | offset: std.io.Writer.Offset, | ||
| 1097 | limit: std.io.Writer.Limit, | ||
| 1098 | headers_and_trailers: []const []const u8, | ||
| 1099 | ) WriteError!usize { | ||
| 1100 | if (offset != .none) { | ||
| 1101 | if (countWriteFile(limit, headers_and_trailers)) |n| { | ||
| 1102 | return n; | ||
| 1103 | } | ||
| 1104 | } | ||
| 1105 | r.err = error.UnableToElideBody; | ||
| 1106 | return error.WriteFailed; | ||
| 1107 | } | ||
| 1108 | |||
| 1109 | /// Returns `null` if size cannot be computed without making any syscalls. | ||
| 1110 | fn countWriteFile(limit: std.io.Writer.Limit, headers_and_trailers: []const []const u8) ?usize { | ||
| 1111 | var total: usize = limit.toInt() orelse return null; | ||
| 1112 | for (headers_and_trailers) |buf| total += buf.len; | ||
| 1113 | return total; | ||
| 1114 | } | ||
| 1115 | |||
| 1116 | fn noneWriteFile( | ||
| 1117 | context: ?*anyopaque, | ||
| 1118 | file: std.fs.File, | ||
| 1119 | offset: std.io.Writer.Offset, | ||
| 1120 | limit: std.io.Writer.Limit, | ||
| 1121 | headers_and_trailers: []const []const u8, | ||
| 1122 | headers_len: usize, | ||
| 1123 | ) std.io.Writer.FileError!usize { | ||
| 1124 | if (limit == .nothing) return noneWriteSplat(context, headers_and_trailers, 1); | ||
| 1125 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1126 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); | ||
| 1127 | return r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 1128 | } | ||
| 1129 | |||
| 1130 | fn contentLengthWriteFile( | ||
| 1131 | context: ?*anyopaque, | ||
| 1132 | file: std.fs.File, | ||
| 1133 | offset: std.io.Writer.Offset, | ||
| 1134 | limit: std.io.Writer.Limit, | ||
| 1135 | headers_and_trailers: []const []const u8, | ||
| 1136 | headers_len: usize, | ||
| 1137 | ) std.io.Writer.FileError!usize { | ||
| 1138 | if (limit == .nothing) return contentLengthWriteSplat(context, headers_and_trailers, 1); | ||
| 1139 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1140 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); | ||
| 1141 | const n = try r.server_output.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 1142 | r.transfer_encoding.content_length -= n; | ||
| 1143 | return n; | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | fn chunkedWriteFile( | ||
| 1147 | context: ?*anyopaque, | ||
| 1148 | file: std.fs.File, | ||
| 1149 | offset: std.io.Writer.Offset, | ||
| 1150 | limit: std.io.Writer.Limit, | ||
| 1151 | headers_and_trailers: []const []const u8, | ||
| 1152 | headers_len: usize, | ||
| 1153 | ) std.io.Writer.FileError!usize { | ||
| 1154 | if (limit == .nothing) return chunkedWriteSplat(context, headers_and_trailers, 1); | ||
| 1155 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1156 | if (r.elide_body) return elideWriteFile(r, offset, limit, headers_and_trailers); | ||
| 1157 | const data_len = countWriteFile(limit, headers_and_trailers) orelse @panic("TODO"); | ||
| 1158 | const bw = r.server_output; | ||
| 1159 | const chunked = &r.transfer_encoding.chunked; | ||
| 1160 | state: switch (chunked.*) { | ||
| 1161 | .offset => |off| { | ||
| 1162 | // TODO: is it better perf to read small files into the buffer? | ||
| 1163 | const buffered_len = bw.end - off - chunk_header_template.len; | ||
| 1164 | const chunk_len = data_len + buffered_len; | ||
| 1165 | writeHex(bw.buffer[off..][0..chunk_len_digits], chunk_len); | ||
| 1166 | const n = try bw.writeFile(file, offset, limit, headers_and_trailers, headers_len); | ||
| 1167 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| 1168 | return n; | ||
| 1169 | }, | ||
| 1170 | .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 1171 | 0 => { | ||
| 1172 | const header_buf = try bw.writableArray(chunk_header_template.len); | ||
| 1173 | const off = bw.end; | ||
| 1174 | @memcpy(header_buf, chunk_header_template); | ||
| 1175 | chunked.* = .{ .offset = off }; | ||
| 1176 | continue :state .{ .offset = off }; | ||
| 1177 | }, | ||
| 1178 | 1 => { | ||
| 1179 | try bw.writeByte('\n'); | ||
| 1180 | chunked.chunk_len = 0; | ||
| 1181 | continue :l 0; | ||
| 1182 | }, | ||
| 1183 | 2 => { | ||
| 1184 | try bw.writeByte('\r'); | ||
| 1185 | chunked.chunk_len = 1; | ||
| 1186 | continue :l 1; | ||
| 1187 | }, | ||
| 1188 | else => { | ||
| 1189 | const new_limit = limit.min(.limited(chunk_len - 2)); | ||
| 1190 | const n = try bw.writeFile(file, offset, new_limit, headers_and_trailers, headers_len); | ||
| 1191 | chunked.chunk_len = chunk_len - n; | ||
| 1192 | return n; | ||
| 1193 | }, | ||
| 1194 | }, | ||
| 1195 | } | ||
| 1196 | } | ||
| 1197 | |||
| 1198 | fn chunkedWriteSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) WriteError!usize { | ||
| 1199 | const r: *Response = @alignCast(@ptrCast(context)); | ||
| 1200 | const data_len = countSplat(data, splat); | ||
| 1201 | if (r.elide_body) return data_len; | ||
| 1202 | |||
| 1203 | const bw = r.server_output; | ||
| 1204 | const chunked = &r.transfer_encoding.chunked; | ||
| 1205 | |||
| 1206 | state: switch (chunked.*) { | ||
| 1207 | .offset => |offset| { | ||
| 1208 | if (bw.unusedCapacitySlice().len >= data_len) { | ||
| 1209 | assert(data_len == (bw.writeSplat(data, splat) catch unreachable)); | ||
| 1210 | return data_len; | ||
| 1211 | } | ||
| 1212 | const buffered_len = bw.end - offset - chunk_header_template.len; | ||
| 1213 | const chunk_len = data_len + buffered_len; | ||
| 1214 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| 1215 | const n = try bw.writeSplat(data, splat); | ||
| 1216 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| 1217 | return n; | ||
| 1218 | }, | ||
| 1219 | .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 1220 | 0 => { | ||
| 1221 | const header_buf = try bw.writableArray(chunk_header_template.len); | ||
| 1222 | const offset = bw.end; | ||
| 1223 | @memcpy(header_buf, chunk_header_template); | ||
| 1224 | chunked.* = .{ .offset = offset }; | ||
| 1225 | continue :state .{ .offset = offset }; | ||
| 1226 | }, | ||
| 1227 | 1 => { | ||
| 1228 | try bw.writeByte('\n'); | ||
| 1229 | chunked.chunk_len = 0; | ||
| 1230 | continue :l 0; | ||
| 1231 | }, | ||
| 1232 | 2 => { | ||
| 1233 | try bw.writeByte('\r'); | ||
| 1234 | chunked.chunk_len = 1; | ||
| 1235 | continue :l 1; | ||
| 1236 | }, | ||
| 1237 | else => { | ||
| 1238 | const n = try bw.writeSplatLimit(data, splat, .limited(chunk_len - 2)); | ||
| 1239 | chunked.chunk_len = chunk_len - n; | ||
| 1240 | return n; | ||
| 1241 | }, | ||
| 1242 | }, | ||
| 1243 | } | ||
| 1244 | } | ||
| 1245 | |||
| 1246 | /// Writes an integer as base 16 to `buf`, right-aligned, assuming the | ||
| 1247 | /// buffer has already been filled with zeroes. | ||
| 1248 | fn writeHex(buf: []u8, x: usize) void { | ||
| 1249 | assert(std.mem.allEqual(u8, buf, '0')); | ||
| 1250 | const base = 16; | ||
| 1251 | var index: usize = buf.len; | ||
| 1252 | var a = x; | ||
| 1253 | while (a > 0) { | ||
| 1254 | const digit = a % base; | ||
| 1255 | index -= 1; | ||
| 1256 | buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); | ||
| 1257 | a /= base; | ||
| 1258 | } | ||
| 1259 | } | ||
| 1260 | |||
| 1261 | pub fn writer(r: *Response) std.io.Writer { | ||
| 1262 | return .{ | ||
| 1263 | .context = r, | ||
| 1264 | .vtable = switch (r.transfer_encoding) { | ||
| 1265 | .none => &.{ | ||
| 1266 | .writeSplat = noneWriteSplat, | ||
| 1267 | .writeFile = noneWriteFile, | ||
| 1268 | }, | ||
| 1269 | .content_length => &.{ | ||
| 1270 | .writeSplat = contentLengthWriteSplat, | ||
| 1271 | .writeFile = contentLengthWriteFile, | ||
| 1272 | }, | ||
| 1273 | .chunked => &.{ | ||
| 1274 | .writeSplat = chunkedWriteSplat, | ||
| 1275 | .writeFile = chunkedWriteFile, | ||
| 1276 | }, | ||
| 1277 | }, | ||
| 1278 | }; | ||
| 1279 | } | ||
| 1280 | }; | 564 | }; |
lib/std/http/WebSocket.zig+3-3| ... | @@ -10,7 +10,7 @@ key: []const u8, | ... | @@ -10,7 +10,7 @@ key: []const u8, |
| 10 | request: *std.http.Server.Request, | 10 | request: *std.http.Server.Request, |
| 11 | recv_fifo: std.fifo.LinearFifo(u8, .Slice), | 11 | recv_fifo: std.fifo.LinearFifo(u8, .Slice), |
| 12 | reader: std.io.BufferedReader, | 12 | reader: std.io.BufferedReader, |
| 13 | response: std.http.Server.Response, | 13 | body_writer: std.http.BodyWriter, |
| 14 | /// Number of bytes that have been peeked but not discarded yet. | 14 | /// Number of bytes that have been peeked but not discarded yet. |
| 15 | outstanding_len: usize, | 15 | outstanding_len: usize, |
| 16 | 16 | ||
| ... | @@ -58,7 +58,7 @@ pub fn init( | ... | @@ -58,7 +58,7 @@ pub fn init( |
| 58 | .key = key, | 58 | .key = key, |
| 59 | .recv_fifo = .init(recv_buffer), | 59 | .recv_fifo = .init(recv_buffer), |
| 60 | .reader = (try request.reader()).unbuffered(), | 60 | .reader = (try request.reader()).unbuffered(), |
| 61 | .response = try request.respondStreaming(.{ | 61 | .body_writer = try request.respondStreaming(.{ |
| 62 | .respond_options = .{ | 62 | .respond_options = .{ |
| 63 | .status = .switching_protocols, | 63 | .status = .switching_protocols, |
| 64 | .extra_headers = &.{ | 64 | .extra_headers = &.{ |
| ... | @@ -236,7 +236,7 @@ pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opc | ... | @@ -236,7 +236,7 @@ pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opc |
| 236 | }, | 236 | }, |
| 237 | }; | 237 | }; |
| 238 | 238 | ||
| 239 | var bw = ws.response.writer().unbuffered(); | 239 | var bw = ws.body_writer.interface().unbuffered(); |
| 240 | try bw.writeAll(header); | 240 | try bw.writeAll(header); |
| 241 | for (message) |iovec| try bw.writeAll(iovec.base[0..iovec.len]); | 241 | for (message) |iovec| try bw.writeAll(iovec.base[0..iovec.len]); |
| 242 | try bw.flush(); | 242 | try bw.flush(); |
lib/std/http/protocol.zig deleted-449| ... | @@ -1,449 +0,0 @@ | ||
| 1 | const std = @import("../std.zig"); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | const testing = std.testing; | ||
| 4 | const mem = std.mem; | ||
| 5 | |||
| 6 | const assert = std.debug.assert; | ||
| 7 | |||
| 8 | pub const State = enum { | ||
| 9 | invalid, | ||
| 10 | |||
| 11 | // Begin header and trailer parsing states. | ||
| 12 | |||
| 13 | start, | ||
| 14 | seen_n, | ||
| 15 | seen_r, | ||
| 16 | seen_rn, | ||
| 17 | seen_rnr, | ||
| 18 | finished, | ||
| 19 | |||
| 20 | // Begin transfer-encoding: chunked parsing states. | ||
| 21 | |||
| 22 | chunk_head_size, | ||
| 23 | chunk_head_ext, | ||
| 24 | chunk_head_r, | ||
| 25 | chunk_data, | ||
| 26 | chunk_data_suffix, | ||
| 27 | chunk_data_suffix_r, | ||
| 28 | |||
| 29 | /// Returns true if the parser is in a content state (ie. not waiting for more headers). | ||
| 30 | pub fn isContent(self: State) bool { | ||
| 31 | return switch (self) { | ||
| 32 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false, | ||
| 33 | .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true, | ||
| 34 | }; | ||
| 35 | } | ||
| 36 | }; | ||
| 37 | |||
| 38 | pub const HeadersParser = struct { | ||
| 39 | state: State = .start, | ||
| 40 | /// A fixed buffer of len `max_header_bytes`. | ||
| 41 | /// Pointers into this buffer are not stable until after a message is complete. | ||
| 42 | header_bytes_buffer: []u8, | ||
| 43 | header_bytes_len: u32, | ||
| 44 | next_chunk_length: u64, | ||
| 45 | /// `false`: headers. `true`: trailers. | ||
| 46 | done: bool, | ||
| 47 | |||
| 48 | /// Initializes the parser with a provided buffer `buf`. | ||
| 49 | pub fn init(buf: []u8) HeadersParser { | ||
| 50 | return .{ | ||
| 51 | .header_bytes_buffer = buf, | ||
| 52 | .header_bytes_len = 0, | ||
| 53 | .done = false, | ||
| 54 | .next_chunk_length = 0, | ||
| 55 | }; | ||
| 56 | } | ||
| 57 | |||
| 58 | /// Reinitialize the parser. | ||
| 59 | /// Asserts the parser is in the "done" state. | ||
| 60 | pub fn reset(hp: *HeadersParser) void { | ||
| 61 | assert(hp.done); | ||
| 62 | hp.* = .{ | ||
| 63 | .state = .start, | ||
| 64 | .header_bytes_buffer = hp.header_bytes_buffer, | ||
| 65 | .header_bytes_len = 0, | ||
| 66 | .done = false, | ||
| 67 | .next_chunk_length = 0, | ||
| 68 | }; | ||
| 69 | } | ||
| 70 | |||
| 71 | pub fn get(hp: HeadersParser) []u8 { | ||
| 72 | return hp.header_bytes_buffer[0..hp.header_bytes_len]; | ||
| 73 | } | ||
| 74 | |||
| 75 | pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 { | ||
| 76 | var hp: std.http.HeadParser = .{ | ||
| 77 | .state = switch (r.state) { | ||
| 78 | .start => .start, | ||
| 79 | .seen_n => .seen_n, | ||
| 80 | .seen_r => .seen_r, | ||
| 81 | .seen_rn => .seen_rn, | ||
| 82 | .seen_rnr => .seen_rnr, | ||
| 83 | .finished => .finished, | ||
| 84 | else => unreachable, | ||
| 85 | }, | ||
| 86 | }; | ||
| 87 | const result = hp.feed(bytes); | ||
| 88 | r.state = switch (hp.state) { | ||
| 89 | .start => .start, | ||
| 90 | .seen_n => .seen_n, | ||
| 91 | .seen_r => .seen_r, | ||
| 92 | .seen_rn => .seen_rn, | ||
| 93 | .seen_rnr => .seen_rnr, | ||
| 94 | .finished => .finished, | ||
| 95 | }; | ||
| 96 | return @intCast(result); | ||
| 97 | } | ||
| 98 | |||
| 99 | pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 { | ||
| 100 | var cp: std.http.ChunkParser = .{ | ||
| 101 | .state = switch (r.state) { | ||
| 102 | .chunk_head_size => .head_size, | ||
| 103 | .chunk_head_ext => .head_ext, | ||
| 104 | .chunk_head_r => .head_r, | ||
| 105 | .chunk_data => .data, | ||
| 106 | .chunk_data_suffix => .data_suffix, | ||
| 107 | .chunk_data_suffix_r => .data_suffix_r, | ||
| 108 | .invalid => .invalid, | ||
| 109 | else => unreachable, | ||
| 110 | }, | ||
| 111 | .chunk_len = r.next_chunk_length, | ||
| 112 | }; | ||
| 113 | const result = cp.feed(bytes); | ||
| 114 | r.state = switch (cp.state) { | ||
| 115 | .head_size => .chunk_head_size, | ||
| 116 | .head_ext => .chunk_head_ext, | ||
| 117 | .head_r => .chunk_head_r, | ||
| 118 | .data => .chunk_data, | ||
| 119 | .data_suffix => .chunk_data_suffix, | ||
| 120 | .data_suffix_r => .chunk_data_suffix_r, | ||
| 121 | .invalid => .invalid, | ||
| 122 | }; | ||
| 123 | r.next_chunk_length = cp.chunk_len; | ||
| 124 | return @intCast(result); | ||
| 125 | } | ||
| 126 | |||
| 127 | /// Returns whether or not the parser has finished parsing a complete | ||
| 128 | /// message. A message is only complete after the entire body has been read | ||
| 129 | /// and any trailing headers have been parsed. | ||
| 130 | pub fn isComplete(r: *HeadersParser) bool { | ||
| 131 | return r.done and r.state == .finished; | ||
| 132 | } | ||
| 133 | |||
| 134 | pub const CheckCompleteHeadError = error{HttpHeadersOversize}; | ||
| 135 | |||
| 136 | /// Pushes `in` into the parser. Returns the number of bytes consumed by | ||
| 137 | /// the header. Any header bytes are appended to `header_bytes_buffer`. | ||
| 138 | pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 { | ||
| 139 | if (hp.state.isContent()) return 0; | ||
| 140 | |||
| 141 | const i = hp.findHeadersEnd(in); | ||
| 142 | const data = in[0..i]; | ||
| 143 | if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len) | ||
| 144 | return error.HttpHeadersOversize; | ||
| 145 | |||
| 146 | @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data); | ||
| 147 | hp.header_bytes_len += @intCast(data.len); | ||
| 148 | |||
| 149 | return i; | ||
| 150 | } | ||
| 151 | |||
| 152 | pub const ReadError = error{ | ||
| 153 | HttpChunkInvalid, | ||
| 154 | }; | ||
| 155 | |||
| 156 | /// Reads the body of the message into `buffer`. Returns the number of | ||
| 157 | /// bytes placed in the buffer. | ||
| 158 | /// | ||
| 159 | /// If `skip` is true, the buffer will be unused and the body will be skipped. | ||
| 160 | /// | ||
| 161 | /// See `std.http.Client.Connection for an example of `conn`. | ||
| 162 | pub fn read(r: *HeadersParser, conn: anytype, buffer: []u8, skip: bool) !usize { | ||
| 163 | assert(r.state.isContent()); | ||
| 164 | if (r.done) return 0; | ||
| 165 | |||
| 166 | var out_index: usize = 0; | ||
| 167 | while (true) { | ||
| 168 | switch (r.state) { | ||
| 169 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable, | ||
| 170 | .finished => { | ||
| 171 | const data_avail = r.next_chunk_length; | ||
| 172 | |||
| 173 | if (skip) { | ||
| 174 | conn.fill() catch |err| switch (err) { | ||
| 175 | error.EndOfStream => { | ||
| 176 | r.done = true; | ||
| 177 | return 0; | ||
| 178 | }, | ||
| 179 | else => |e| return e, | ||
| 180 | }; | ||
| 181 | |||
| 182 | const nread = @min(conn.peek().len, data_avail); | ||
| 183 | conn.drop(@intCast(nread)); | ||
| 184 | r.next_chunk_length -= nread; | ||
| 185 | |||
| 186 | if (r.next_chunk_length == 0 or nread == 0) r.done = true; | ||
| 187 | |||
| 188 | return out_index; | ||
| 189 | } else if (out_index < buffer.len) { | ||
| 190 | const out_avail = buffer.len - out_index; | ||
| 191 | |||
| 192 | const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); | ||
| 193 | const nread = try conn.read(buffer[0..can_read]); | ||
| 194 | r.next_chunk_length -= nread; | ||
| 195 | |||
| 196 | if (r.next_chunk_length == 0 or nread == 0) r.done = true; | ||
| 197 | |||
| 198 | return nread; | ||
| 199 | } else { | ||
| 200 | return out_index; | ||
| 201 | } | ||
| 202 | }, | ||
| 203 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { | ||
| 204 | conn.fill() catch |err| switch (err) { | ||
| 205 | error.EndOfStream => { | ||
| 206 | r.done = true; | ||
| 207 | return 0; | ||
| 208 | }, | ||
| 209 | else => |e| return e, | ||
| 210 | }; | ||
| 211 | |||
| 212 | const i = r.findChunkedLen(conn.peek()); | ||
| 213 | conn.drop(@intCast(i)); | ||
| 214 | |||
| 215 | switch (r.state) { | ||
| 216 | .invalid => return error.HttpChunkInvalid, | ||
| 217 | .chunk_data => if (r.next_chunk_length == 0) { | ||
| 218 | if (std.mem.eql(u8, conn.peek(), "\r\n")) { | ||
| 219 | r.state = .finished; | ||
| 220 | conn.drop(2); | ||
| 221 | } else { | ||
| 222 | // The trailer section is formatted identically | ||
| 223 | // to the header section. | ||
| 224 | r.state = .seen_rn; | ||
| 225 | } | ||
| 226 | r.done = true; | ||
| 227 | |||
| 228 | return out_index; | ||
| 229 | }, | ||
| 230 | else => return out_index, | ||
| 231 | } | ||
| 232 | |||
| 233 | continue; | ||
| 234 | }, | ||
| 235 | .chunk_data => { | ||
| 236 | const data_avail = r.next_chunk_length; | ||
| 237 | const out_avail = buffer.len - out_index; | ||
| 238 | |||
| 239 | if (skip) { | ||
| 240 | conn.fill() catch |err| switch (err) { | ||
| 241 | error.EndOfStream => { | ||
| 242 | r.done = true; | ||
| 243 | return 0; | ||
| 244 | }, | ||
| 245 | else => |e| return e, | ||
| 246 | }; | ||
| 247 | |||
| 248 | const nread = @min(conn.peek().len, data_avail); | ||
| 249 | conn.drop(@intCast(nread)); | ||
| 250 | r.next_chunk_length -= nread; | ||
| 251 | } else if (out_avail > 0) { | ||
| 252 | const can_read: usize = @intCast(@min(data_avail, out_avail)); | ||
| 253 | const nread = try conn.read(buffer[out_index..][0..can_read]); | ||
| 254 | r.next_chunk_length -= nread; | ||
| 255 | out_index += nread; | ||
| 256 | } | ||
| 257 | |||
| 258 | if (r.next_chunk_length == 0) { | ||
| 259 | r.state = .chunk_data_suffix; | ||
| 260 | continue; | ||
| 261 | } | ||
| 262 | |||
| 263 | return out_index; | ||
| 264 | }, | ||
| 265 | } | ||
| 266 | } | ||
| 267 | } | ||
| 268 | }; | ||
| 269 | |||
| 270 | inline fn int16(array: *const [2]u8) u16 { | ||
| 271 | return @as(u16, @bitCast(array.*)); | ||
| 272 | } | ||
| 273 | |||
| 274 | inline fn int24(array: *const [3]u8) u24 { | ||
| 275 | return @as(u24, @bitCast(array.*)); | ||
| 276 | } | ||
| 277 | |||
| 278 | inline fn int32(array: *const [4]u8) u32 { | ||
| 279 | return @as(u32, @bitCast(array.*)); | ||
| 280 | } | ||
| 281 | |||
| 282 | inline fn intShift(comptime T: type, x: anytype) T { | ||
| 283 | switch (@import("builtin").cpu.arch.endian()) { | ||
| 284 | .little => return @as(T, @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T)))), | ||
| 285 | .big => return @as(T, @truncate(x)), | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | /// A buffered (and peekable) Connection. | ||
| 290 | const MockBufferedConnection = struct { | ||
| 291 | pub const buffer_size = 0x2000; | ||
| 292 | |||
| 293 | conn: std.io.FixedBufferStream, | ||
| 294 | buf: [buffer_size]u8 = undefined, | ||
| 295 | start: u16 = 0, | ||
| 296 | end: u16 = 0, | ||
| 297 | |||
| 298 | pub fn fill(conn: *MockBufferedConnection) ReadError!void { | ||
| 299 | if (conn.end != conn.start) return; | ||
| 300 | |||
| 301 | const nread = try conn.conn.read(conn.buf[0..]); | ||
| 302 | if (nread == 0) return error.EndOfStream; | ||
| 303 | conn.start = 0; | ||
| 304 | conn.end = @as(u16, @truncate(nread)); | ||
| 305 | } | ||
| 306 | |||
| 307 | pub fn peek(conn: *MockBufferedConnection) []const u8 { | ||
| 308 | return conn.buf[conn.start..conn.end]; | ||
| 309 | } | ||
| 310 | |||
| 311 | pub fn drop(conn: *MockBufferedConnection, num: u16) void { | ||
| 312 | conn.start += num; | ||
| 313 | } | ||
| 314 | |||
| 315 | pub fn readAtLeast(conn: *MockBufferedConnection, buffer: []u8, len: usize) ReadError!usize { | ||
| 316 | var out_index: u16 = 0; | ||
| 317 | while (out_index < len) { | ||
| 318 | const available = conn.end - conn.start; | ||
| 319 | const left = buffer.len - out_index; | ||
| 320 | |||
| 321 | if (available > 0) { | ||
| 322 | const can_read = @as(u16, @truncate(@min(available, left))); | ||
| 323 | |||
| 324 | @memcpy(buffer[out_index..][0..can_read], conn.buf[conn.start..][0..can_read]); | ||
| 325 | out_index += can_read; | ||
| 326 | conn.start += can_read; | ||
| 327 | |||
| 328 | continue; | ||
| 329 | } | ||
| 330 | |||
| 331 | if (left > conn.buf.len) { | ||
| 332 | // skip the buffer if the output is large enough | ||
| 333 | return conn.conn.read(buffer[out_index..]); | ||
| 334 | } | ||
| 335 | |||
| 336 | try conn.fill(); | ||
| 337 | } | ||
| 338 | |||
| 339 | return out_index; | ||
| 340 | } | ||
| 341 | |||
| 342 | pub fn read(conn: *MockBufferedConnection, buffer: []u8) ReadError!usize { | ||
| 343 | return conn.readAtLeast(buffer, 1); | ||
| 344 | } | ||
| 345 | |||
| 346 | pub const ReadError = std.io.FixedBufferStream.ReadError || error{EndOfStream}; | ||
| 347 | pub const Reader = std.io.Reader(*MockBufferedConnection, ReadError, read); | ||
| 348 | |||
| 349 | pub fn reader(conn: *MockBufferedConnection) Reader { | ||
| 350 | return Reader{ .context = conn }; | ||
| 351 | } | ||
| 352 | }; | ||
| 353 | |||
| 354 | test "HeadersParser.read length" { | ||
| 355 | // mock BufferedConnection for read | ||
| 356 | var headers_buf: [256]u8 = undefined; | ||
| 357 | |||
| 358 | var r = HeadersParser.init(&headers_buf); | ||
| 359 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello"; | ||
| 360 | |||
| 361 | var conn: MockBufferedConnection = .{ | ||
| 362 | .conn = .{ .buffer = data }, | ||
| 363 | }; | ||
| 364 | |||
| 365 | while (true) { // read headers | ||
| 366 | try conn.fill(); | ||
| 367 | |||
| 368 | const nchecked = try r.checkCompleteHead(conn.peek()); | ||
| 369 | conn.drop(@intCast(nchecked)); | ||
| 370 | |||
| 371 | if (r.state.isContent()) break; | ||
| 372 | } | ||
| 373 | |||
| 374 | var buf: [8]u8 = undefined; | ||
| 375 | |||
| 376 | r.next_chunk_length = 5; | ||
| 377 | const len = try r.read(&conn, &buf, false); | ||
| 378 | try std.testing.expectEqual(@as(usize, 5), len); | ||
| 379 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | ||
| 380 | |||
| 381 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get()); | ||
| 382 | } | ||
| 383 | |||
| 384 | test "HeadersParser.read chunked" { | ||
| 385 | // mock BufferedConnection for read | ||
| 386 | |||
| 387 | var headers_buf: [256]u8 = undefined; | ||
| 388 | var r = HeadersParser.init(&headers_buf); | ||
| 389 | 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"; | ||
| 390 | |||
| 391 | var conn: MockBufferedConnection = .{ | ||
| 392 | .conn = .{ .buffer = data }, | ||
| 393 | }; | ||
| 394 | |||
| 395 | while (true) { // read headers | ||
| 396 | try conn.fill(); | ||
| 397 | |||
| 398 | const nchecked = try r.checkCompleteHead(conn.peek()); | ||
| 399 | conn.drop(@intCast(nchecked)); | ||
| 400 | |||
| 401 | if (r.state.isContent()) break; | ||
| 402 | } | ||
| 403 | var buf: [8]u8 = undefined; | ||
| 404 | |||
| 405 | r.state = .chunk_head_size; | ||
| 406 | const len = try r.read(&conn, &buf, false); | ||
| 407 | try std.testing.expectEqual(@as(usize, 5), len); | ||
| 408 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | ||
| 409 | |||
| 410 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get()); | ||
| 411 | } | ||
| 412 | |||
| 413 | test "HeadersParser.read chunked trailer" { | ||
| 414 | // mock BufferedConnection for read | ||
| 415 | |||
| 416 | var headers_buf: [256]u8 = undefined; | ||
| 417 | var r = HeadersParser.init(&headers_buf); | ||
| 418 | 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"; | ||
| 419 | |||
| 420 | var conn: MockBufferedConnection = .{ | ||
| 421 | .conn = .{ .buffer = data }, | ||
| 422 | }; | ||
| 423 | |||
| 424 | while (true) { // read headers | ||
| 425 | try conn.fill(); | ||
| 426 | |||
| 427 | const nchecked = try r.checkCompleteHead(conn.peek()); | ||
| 428 | conn.drop(@intCast(nchecked)); | ||
| 429 | |||
| 430 | if (r.state.isContent()) break; | ||
| 431 | } | ||
| 432 | var buf: [8]u8 = undefined; | ||
| 433 | |||
| 434 | r.state = .chunk_head_size; | ||
| 435 | const len = try r.read(&conn, &buf, false); | ||
| 436 | try std.testing.expectEqual(@as(usize, 5), len); | ||
| 437 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | ||
| 438 | |||
| 439 | while (true) { // read headers | ||
| 440 | try conn.fill(); | ||
| 441 | |||
| 442 | const nchecked = try r.checkCompleteHead(conn.peek()); | ||
| 443 | conn.drop(@intCast(nchecked)); | ||
| 444 | |||
| 445 | if (r.state.isContent()) break; | ||
| 446 | } | ||
| 447 | |||
| 448 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get()); | ||
| 449 | } | ||
lib/std/http/test.zig+100-135| ... | @@ -61,21 +61,18 @@ test "trailers" { | ... | @@ -61,21 +61,18 @@ test "trailers" { |
| 61 | const uri = try std.Uri.parse(location); | 61 | const uri = try std.Uri.parse(location); |
| 62 | 62 | ||
| 63 | { | 63 | { |
| 64 | var server_header_buffer: [1024]u8 = undefined; | 64 | var req = try client.open(.GET, uri, .{}); |
| 65 | var req = try client.open(.GET, uri, .{ | ||
| 66 | .server_header_buffer = &server_header_buffer, | ||
| 67 | }); | ||
| 68 | defer req.deinit(); | 65 | defer req.deinit(); |
| 69 | 66 | ||
| 70 | try req.send(); | 67 | try req.sendBodiless(); |
| 71 | try req.wait(); | 68 | var response = try req.receiveHead(&.{}); |
| 72 | 69 | ||
| 73 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 70 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 74 | defer gpa.free(body); | 71 | defer gpa.free(body); |
| 75 | 72 | ||
| 76 | try expectEqualStrings("Hello, World!\n", body); | 73 | try expectEqualStrings("Hello, World!\n", body); |
| 77 | 74 | ||
| 78 | var it = req.response.iterateHeaders(); | 75 | var it = response.iterateHeaders(); |
| 79 | { | 76 | { |
| 80 | const header = it.next().?; | 77 | const header = it.next().?; |
| 81 | try expect(!it.is_trailer); | 78 | try expect(!it.is_trailer); |
| ... | @@ -565,20 +562,18 @@ test "general client/server API coverage" { | ... | @@ -565,20 +562,18 @@ test "general client/server API coverage" { |
| 565 | const uri = try std.Uri.parse(location); | 562 | const uri = try std.Uri.parse(location); |
| 566 | 563 | ||
| 567 | log.info("{s}", .{location}); | 564 | log.info("{s}", .{location}); |
| 568 | var server_header_buffer: [1024]u8 = undefined; | 565 | var redirect_buffer: [1024]u8 = undefined; |
| 569 | var req = try client.open(.GET, uri, .{ | 566 | var req = try client.open(.GET, uri, .{}); |
| 570 | .server_header_buffer = &server_header_buffer, | ||
| 571 | }); | ||
| 572 | defer req.deinit(); | 567 | defer req.deinit(); |
| 573 | 568 | ||
| 574 | try req.send(); | 569 | try req.sendBodiless(); |
| 575 | try req.wait(); | 570 | var response = try req.receiveHead(&redirect_buffer); |
| 576 | 571 | ||
| 577 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 572 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 578 | defer gpa.free(body); | 573 | defer gpa.free(body); |
| 579 | 574 | ||
| 580 | try expectEqualStrings("Hello, World!\n", body); | 575 | try expectEqualStrings("Hello, World!\n", body); |
| 581 | try expectEqualStrings("text/plain", req.response.content_type.?); | 576 | try expectEqualStrings("text/plain", response.head.content_type.?); |
| 582 | } | 577 | } |
| 583 | 578 | ||
| 584 | // connection has been kept alive | 579 | // connection has been kept alive |
| ... | @@ -590,16 +585,14 @@ test "general client/server API coverage" { | ... | @@ -590,16 +585,14 @@ test "general client/server API coverage" { |
| 590 | const uri = try std.Uri.parse(location); | 585 | const uri = try std.Uri.parse(location); |
| 591 | 586 | ||
| 592 | log.info("{s}", .{location}); | 587 | log.info("{s}", .{location}); |
| 593 | var server_header_buffer: [1024]u8 = undefined; | 588 | var redirect_buffer: [1024]u8 = undefined; |
| 594 | var req = try client.open(.GET, uri, .{ | 589 | var req = try client.open(.GET, uri, .{}); |
| 595 | .server_header_buffer = &server_header_buffer, | ||
| 596 | }); | ||
| 597 | defer req.deinit(); | 590 | defer req.deinit(); |
| 598 | 591 | ||
| 599 | try req.send(); | 592 | try req.sendBodiless(); |
| 600 | try req.wait(); | 593 | var response = try req.receiveHead(&redirect_buffer); |
| 601 | 594 | ||
| 602 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192 * 1024)); | 595 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192 * 1024)); |
| 603 | defer gpa.free(body); | 596 | defer gpa.free(body); |
| 604 | 597 | ||
| 605 | try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); | 598 | try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); |
| ... | @@ -614,21 +607,19 @@ test "general client/server API coverage" { | ... | @@ -614,21 +607,19 @@ test "general client/server API coverage" { |
| 614 | const uri = try std.Uri.parse(location); | 607 | const uri = try std.Uri.parse(location); |
| 615 | 608 | ||
| 616 | log.info("{s}", .{location}); | 609 | log.info("{s}", .{location}); |
| 617 | var server_header_buffer: [1024]u8 = undefined; | 610 | var redirect_buffer: [1024]u8 = undefined; |
| 618 | var req = try client.open(.HEAD, uri, .{ | 611 | var req = try client.open(.HEAD, uri, .{}); |
| 619 | .server_header_buffer = &server_header_buffer, | ||
| 620 | }); | ||
| 621 | defer req.deinit(); | 612 | defer req.deinit(); |
| 622 | 613 | ||
| 623 | try req.send(); | 614 | try req.sendBodiless(); |
| 624 | try req.wait(); | 615 | var response = try req.receiveHead(&redirect_buffer); |
| 625 | 616 | ||
| 626 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 617 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 627 | defer gpa.free(body); | 618 | defer gpa.free(body); |
| 628 | 619 | ||
| 629 | try expectEqualStrings("", body); | 620 | try expectEqualStrings("", body); |
| 630 | try expectEqualStrings("text/plain", req.response.content_type.?); | 621 | try expectEqualStrings("text/plain", response.content_type.?); |
| 631 | try expectEqual(14, req.response.content_length.?); | 622 | try expectEqual(14, response.head.content_length.?); |
| 632 | } | 623 | } |
| 633 | 624 | ||
| 634 | // connection has been kept alive | 625 | // connection has been kept alive |
| ... | @@ -640,20 +631,18 @@ test "general client/server API coverage" { | ... | @@ -640,20 +631,18 @@ test "general client/server API coverage" { |
| 640 | const uri = try std.Uri.parse(location); | 631 | const uri = try std.Uri.parse(location); |
| 641 | 632 | ||
| 642 | log.info("{s}", .{location}); | 633 | log.info("{s}", .{location}); |
| 643 | var server_header_buffer: [1024]u8 = undefined; | 634 | var redirect_buffer: [1024]u8 = undefined; |
| 644 | var req = try client.open(.GET, uri, .{ | 635 | var req = try client.open(.GET, uri, .{}); |
| 645 | .server_header_buffer = &server_header_buffer, | ||
| 646 | }); | ||
| 647 | defer req.deinit(); | 636 | defer req.deinit(); |
| 648 | 637 | ||
| 649 | try req.send(); | 638 | try req.sendBodiless(); |
| 650 | try req.wait(); | 639 | var response = try req.receiveHead(&redirect_buffer); |
| 651 | 640 | ||
| 652 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 641 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 653 | defer gpa.free(body); | 642 | defer gpa.free(body); |
| 654 | 643 | ||
| 655 | try expectEqualStrings("Hello, World!\n", body); | 644 | try expectEqualStrings("Hello, World!\n", body); |
| 656 | try expectEqualStrings("text/plain", req.response.content_type.?); | 645 | try expectEqualStrings("text/plain", response.head.content_type.?); |
| 657 | } | 646 | } |
| 658 | 647 | ||
| 659 | // connection has been kept alive | 648 | // connection has been kept alive |
| ... | @@ -665,14 +654,12 @@ test "general client/server API coverage" { | ... | @@ -665,14 +654,12 @@ test "general client/server API coverage" { |
| 665 | const uri = try std.Uri.parse(location); | 654 | const uri = try std.Uri.parse(location); |
| 666 | 655 | ||
| 667 | log.info("{s}", .{location}); | 656 | log.info("{s}", .{location}); |
| 668 | var server_header_buffer: [1024]u8 = undefined; | 657 | var redirect_buffer: [1024]u8 = undefined; |
| 669 | var req = try client.open(.HEAD, uri, .{ | 658 | var req = try client.open(.HEAD, uri, .{}); |
| 670 | .server_header_buffer = &server_header_buffer, | ||
| 671 | }); | ||
| 672 | defer req.deinit(); | 659 | defer req.deinit(); |
| 673 | 660 | ||
| 674 | try req.send(); | 661 | try req.sendBodiless(); |
| 675 | try req.wait(); | 662 | try req.receiveHead(&redirect_buffer); |
| 676 | 663 | ||
| 677 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 664 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 678 | defer gpa.free(body); | 665 | defer gpa.free(body); |
| ... | @@ -691,15 +678,14 @@ test "general client/server API coverage" { | ... | @@ -691,15 +678,14 @@ test "general client/server API coverage" { |
| 691 | const uri = try std.Uri.parse(location); | 678 | const uri = try std.Uri.parse(location); |
| 692 | 679 | ||
| 693 | log.info("{s}", .{location}); | 680 | log.info("{s}", .{location}); |
| 694 | var server_header_buffer: [1024]u8 = undefined; | 681 | var redirect_buffer: [1024]u8 = undefined; |
| 695 | var req = try client.open(.GET, uri, .{ | 682 | var req = try client.open(.GET, uri, .{ |
| 696 | .server_header_buffer = &server_header_buffer, | ||
| 697 | .keep_alive = false, | 683 | .keep_alive = false, |
| 698 | }); | 684 | }); |
| 699 | defer req.deinit(); | 685 | defer req.deinit(); |
| 700 | 686 | ||
| 701 | try req.send(); | 687 | try req.sendBodiless(); |
| 702 | try req.wait(); | 688 | try req.receiveHead(&redirect_buffer); |
| 703 | 689 | ||
| 704 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 690 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 705 | defer gpa.free(body); | 691 | defer gpa.free(body); |
| ... | @@ -717,17 +703,16 @@ test "general client/server API coverage" { | ... | @@ -717,17 +703,16 @@ test "general client/server API coverage" { |
| 717 | const uri = try std.Uri.parse(location); | 703 | const uri = try std.Uri.parse(location); |
| 718 | 704 | ||
| 719 | log.info("{s}", .{location}); | 705 | log.info("{s}", .{location}); |
| 720 | var server_header_buffer: [1024]u8 = undefined; | 706 | var redirect_buffer: [1024]u8 = undefined; |
| 721 | var req = try client.open(.GET, uri, .{ | 707 | var req = try client.open(.GET, uri, .{ |
| 722 | .server_header_buffer = &server_header_buffer, | ||
| 723 | .extra_headers = &.{ | 708 | .extra_headers = &.{ |
| 724 | .{ .name = "empty", .value = "" }, | 709 | .{ .name = "empty", .value = "" }, |
| 725 | }, | 710 | }, |
| 726 | }); | 711 | }); |
| 727 | defer req.deinit(); | 712 | defer req.deinit(); |
| 728 | 713 | ||
| 729 | try req.send(); | 714 | try req.sendBodiless(); |
| 730 | try req.wait(); | 715 | try req.receiveHead(&redirect_buffer); |
| 731 | 716 | ||
| 732 | try std.testing.expectEqual(.ok, req.response.status); | 717 | try std.testing.expectEqual(.ok, req.response.status); |
| 733 | 718 | ||
| ... | @@ -761,14 +746,12 @@ test "general client/server API coverage" { | ... | @@ -761,14 +746,12 @@ test "general client/server API coverage" { |
| 761 | const uri = try std.Uri.parse(location); | 746 | const uri = try std.Uri.parse(location); |
| 762 | 747 | ||
| 763 | log.info("{s}", .{location}); | 748 | log.info("{s}", .{location}); |
| 764 | var server_header_buffer: [1024]u8 = undefined; | 749 | var redirect_buffer: [1024]u8 = undefined; |
| 765 | var req = try client.open(.GET, uri, .{ | 750 | var req = try client.open(.GET, uri, .{}); |
| 766 | .server_header_buffer = &server_header_buffer, | ||
| 767 | }); | ||
| 768 | defer req.deinit(); | 751 | defer req.deinit(); |
| 769 | 752 | ||
| 770 | try req.send(); | 753 | try req.sendBodiless(); |
| 771 | try req.wait(); | 754 | try req.receiveHead(&redirect_buffer); |
| 772 | 755 | ||
| 773 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 756 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 774 | defer gpa.free(body); | 757 | defer gpa.free(body); |
| ... | @@ -785,14 +768,12 @@ test "general client/server API coverage" { | ... | @@ -785,14 +768,12 @@ test "general client/server API coverage" { |
| 785 | const uri = try std.Uri.parse(location); | 768 | const uri = try std.Uri.parse(location); |
| 786 | 769 | ||
| 787 | log.info("{s}", .{location}); | 770 | log.info("{s}", .{location}); |
| 788 | var server_header_buffer: [1024]u8 = undefined; | 771 | var redirect_buffer: [1024]u8 = undefined; |
| 789 | var req = try client.open(.GET, uri, .{ | 772 | var req = try client.open(.GET, uri, .{}); |
| 790 | .server_header_buffer = &server_header_buffer, | ||
| 791 | }); | ||
| 792 | defer req.deinit(); | 773 | defer req.deinit(); |
| 793 | 774 | ||
| 794 | try req.send(); | 775 | try req.sendBodiless(); |
| 795 | try req.wait(); | 776 | try req.receiveHead(&redirect_buffer); |
| 796 | 777 | ||
| 797 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 778 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 798 | defer gpa.free(body); | 779 | defer gpa.free(body); |
| ... | @@ -809,14 +790,12 @@ test "general client/server API coverage" { | ... | @@ -809,14 +790,12 @@ test "general client/server API coverage" { |
| 809 | const uri = try std.Uri.parse(location); | 790 | const uri = try std.Uri.parse(location); |
| 810 | 791 | ||
| 811 | log.info("{s}", .{location}); | 792 | log.info("{s}", .{location}); |
| 812 | var server_header_buffer: [1024]u8 = undefined; | 793 | var redirect_buffer: [1024]u8 = undefined; |
| 813 | var req = try client.open(.GET, uri, .{ | 794 | var req = try client.open(.GET, uri, .{}); |
| 814 | .server_header_buffer = &server_header_buffer, | ||
| 815 | }); | ||
| 816 | defer req.deinit(); | 795 | defer req.deinit(); |
| 817 | 796 | ||
| 818 | try req.send(); | 797 | try req.sendBodiless(); |
| 819 | try req.wait(); | 798 | try req.receiveHead(&redirect_buffer); |
| 820 | 799 | ||
| 821 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 800 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 822 | defer gpa.free(body); | 801 | defer gpa.free(body); |
| ... | @@ -833,14 +812,12 @@ test "general client/server API coverage" { | ... | @@ -833,14 +812,12 @@ test "general client/server API coverage" { |
| 833 | const uri = try std.Uri.parse(location); | 812 | const uri = try std.Uri.parse(location); |
| 834 | 813 | ||
| 835 | log.info("{s}", .{location}); | 814 | log.info("{s}", .{location}); |
| 836 | var server_header_buffer: [1024]u8 = undefined; | 815 | var redirect_buffer: [1024]u8 = undefined; |
| 837 | var req = try client.open(.GET, uri, .{ | 816 | var req = try client.open(.GET, uri, .{}); |
| 838 | .server_header_buffer = &server_header_buffer, | ||
| 839 | }); | ||
| 840 | defer req.deinit(); | 817 | defer req.deinit(); |
| 841 | 818 | ||
| 842 | try req.send(); | 819 | try req.sendBodiless(); |
| 843 | req.wait() catch |err| switch (err) { | 820 | req.receiveHead(&redirect_buffer) catch |err| switch (err) { |
| 844 | error.TooManyHttpRedirects => {}, | 821 | error.TooManyHttpRedirects => {}, |
| 845 | else => return err, | 822 | else => return err, |
| 846 | }; | 823 | }; |
| ... | @@ -852,14 +829,12 @@ test "general client/server API coverage" { | ... | @@ -852,14 +829,12 @@ test "general client/server API coverage" { |
| 852 | const uri = try std.Uri.parse(location); | 829 | const uri = try std.Uri.parse(location); |
| 853 | 830 | ||
| 854 | log.info("{s}", .{location}); | 831 | log.info("{s}", .{location}); |
| 855 | var server_header_buffer: [1024]u8 = undefined; | 832 | var redirect_buffer: [1024]u8 = undefined; |
| 856 | var req = try client.open(.GET, uri, .{ | 833 | var req = try client.open(.GET, uri, .{}); |
| 857 | .server_header_buffer = &server_header_buffer, | ||
| 858 | }); | ||
| 859 | defer req.deinit(); | 834 | defer req.deinit(); |
| 860 | 835 | ||
| 861 | try req.send(); | 836 | try req.sendBodiless(); |
| 862 | try req.wait(); | 837 | try req.receiveHead(&redirect_buffer); |
| 863 | 838 | ||
| 864 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 839 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 865 | defer gpa.free(body); | 840 | defer gpa.free(body); |
| ... | @@ -876,14 +851,12 @@ test "general client/server API coverage" { | ... | @@ -876,14 +851,12 @@ test "general client/server API coverage" { |
| 876 | const uri = try std.Uri.parse(location); | 851 | const uri = try std.Uri.parse(location); |
| 877 | 852 | ||
| 878 | log.info("{s}", .{location}); | 853 | log.info("{s}", .{location}); |
| 879 | var server_header_buffer: [1024]u8 = undefined; | 854 | var redirect_buffer: [1024]u8 = undefined; |
| 880 | var req = try client.open(.GET, uri, .{ | 855 | var req = try client.open(.GET, uri, .{}); |
| 881 | .server_header_buffer = &server_header_buffer, | ||
| 882 | }); | ||
| 883 | defer req.deinit(); | 856 | defer req.deinit(); |
| 884 | 857 | ||
| 885 | try req.send(); | 858 | try req.sendBodiless(); |
| 886 | const result = req.wait(); | 859 | const result = req.receiveHead(&redirect_buffer); |
| 887 | 860 | ||
| 888 | // a proxy without an upstream is likely to return a 5xx status. | 861 | // a proxy without an upstream is likely to return a 5xx status. |
| 889 | if (client.http_proxy == null) { | 862 | if (client.http_proxy == null) { |
| ... | @@ -910,9 +883,7 @@ test "general client/server API coverage" { | ... | @@ -910,9 +883,7 @@ test "general client/server API coverage" { |
| 910 | for (0..total_connections) |i| { | 883 | for (0..total_connections) |i| { |
| 911 | const headers_buf = try gpa.alloc(u8, 1024); | 884 | const headers_buf = try gpa.alloc(u8, 1024); |
| 912 | try header_bufs.append(headers_buf); | 885 | try header_bufs.append(headers_buf); |
| 913 | var req = try client.open(.GET, uri, .{ | 886 | var req = try client.open(.GET, uri, .{}); |
| 914 | .server_header_buffer = headers_buf, | ||
| 915 | }); | ||
| 916 | req.response.parser.done = true; | 887 | req.response.parser.done = true; |
| 917 | req.connection.?.closing = false; | 888 | req.connection.?.closing = false; |
| 918 | requests[i] = req; | 889 | requests[i] = req; |
| ... | @@ -978,28 +949,26 @@ test "Server streams both reading and writing" { | ... | @@ -978,28 +949,26 @@ test "Server streams both reading and writing" { |
| 978 | var client: http.Client = .{ .allocator = std.testing.allocator }; | 949 | var client: http.Client = .{ .allocator = std.testing.allocator }; |
| 979 | defer client.deinit(); | 950 | defer client.deinit(); |
| 980 | 951 | ||
| 981 | var server_header_buffer: [555]u8 = undefined; | 952 | var redirect_buffer: [555]u8 = undefined; |
| 982 | var req = try client.open(.POST, .{ | 953 | var req = try client.open(.POST, .{ |
| 983 | .scheme = "http", | 954 | .scheme = "http", |
| 984 | .host = .{ .raw = "127.0.0.1" }, | 955 | .host = .{ .raw = "127.0.0.1" }, |
| 985 | .port = test_server.port(), | 956 | .port = test_server.port(), |
| 986 | .path = .{ .percent_encoded = "/" }, | 957 | .path = .{ .percent_encoded = "/" }, |
| 987 | }, .{ | 958 | }, .{}); |
| 988 | .server_header_buffer = &server_header_buffer, | ||
| 989 | }); | ||
| 990 | defer req.deinit(); | 959 | defer req.deinit(); |
| 991 | 960 | ||
| 992 | req.transfer_encoding = .chunked; | 961 | req.transfer_encoding = .chunked; |
| 993 | try req.send(); | 962 | var body_writer = try req.sendBody(); |
| 994 | try req.wait(); | 963 | var response = try req.receiveHead(&redirect_buffer); |
| 995 | 964 | ||
| 996 | var w = req.writer().unbuffered(); | 965 | var w = body_writer.interface().unbuffered(); |
| 997 | try w.writeAll("one "); | 966 | try w.writeAll("one "); |
| 998 | try w.writeAll("fish"); | 967 | try w.writeAll("fish"); |
| 999 | 968 | ||
| 1000 | try req.finish(); | 969 | try req.finish(); |
| 1001 | 970 | ||
| 1002 | const body = try req.reader().readRemainingAlloc(std.testing.allocator, .limited(8192)); | 971 | const body = try response.reader().readRemainingAlloc(std.testing.allocator, .limited(8192)); |
| 1003 | defer std.testing.allocator.free(body); | 972 | defer std.testing.allocator.free(body); |
| 1004 | 973 | ||
| 1005 | try expectEqualStrings("ONE FISH", body); | 974 | try expectEqualStrings("ONE FISH", body); |
| ... | @@ -1014,9 +983,8 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1014,9 +983,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1014 | defer gpa.free(location); | 983 | defer gpa.free(location); |
| 1015 | const uri = try std.Uri.parse(location); | 984 | const uri = try std.Uri.parse(location); |
| 1016 | 985 | ||
| 1017 | var server_header_buffer: [1024]u8 = undefined; | 986 | var redirect_buffer: [1024]u8 = undefined; |
| 1018 | var req = try client.open(.POST, uri, .{ | 987 | var req = try client.open(.POST, uri, .{ |
| 1019 | .server_header_buffer = &server_header_buffer, | ||
| 1020 | .extra_headers = &.{ | 988 | .extra_headers = &.{ |
| 1021 | .{ .name = "content-type", .value = "text/plain" }, | 989 | .{ .name = "content-type", .value = "text/plain" }, |
| 1022 | }, | 990 | }, |
| ... | @@ -1025,15 +993,15 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1025,15 +993,15 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1025 | 993 | ||
| 1026 | req.transfer_encoding = .{ .content_length = 14 }; | 994 | req.transfer_encoding = .{ .content_length = 14 }; |
| 1027 | 995 | ||
| 1028 | try req.send(); | 996 | var body_writer = try req.sendBody(); |
| 1029 | var w = req.writer().unbuffered(); | 997 | var w = body_writer.interface().unbuffered(); |
| 1030 | try w.writeAll("Hello, "); | 998 | try w.writeAll("Hello, "); |
| 1031 | try w.writeAll("World!\n"); | 999 | try w.writeAll("World!\n"); |
| 1032 | try req.finish(); | 1000 | try body_writer.end(); |
| 1033 | 1001 | ||
| 1034 | try req.wait(); | 1002 | var response = try req.receiveHead(&redirect_buffer); |
| 1035 | 1003 | ||
| 1036 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 1004 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 1037 | defer gpa.free(body); | 1005 | defer gpa.free(body); |
| 1038 | 1006 | ||
| 1039 | try expectEqualStrings("Hello, World!\n", body); | 1007 | try expectEqualStrings("Hello, World!\n", body); |
| ... | @@ -1049,9 +1017,8 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1049,9 +1017,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1049 | .{port}, | 1017 | .{port}, |
| 1050 | )); | 1018 | )); |
| 1051 | 1019 | ||
| 1052 | var server_header_buffer: [1024]u8 = undefined; | 1020 | var redirect_buffer: [1024]u8 = undefined; |
| 1053 | var req = try client.open(.POST, uri, .{ | 1021 | var req = try client.open(.POST, uri, .{ |
| 1054 | .server_header_buffer = &server_header_buffer, | ||
| 1055 | .extra_headers = &.{ | 1022 | .extra_headers = &.{ |
| 1056 | .{ .name = "content-type", .value = "text/plain" }, | 1023 | .{ .name = "content-type", .value = "text/plain" }, |
| 1057 | }, | 1024 | }, |
| ... | @@ -1060,15 +1027,15 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1060,15 +1027,15 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1060 | 1027 | ||
| 1061 | req.transfer_encoding = .chunked; | 1028 | req.transfer_encoding = .chunked; |
| 1062 | 1029 | ||
| 1063 | try req.send(); | 1030 | var body_writer = try req.sendBody(); |
| 1064 | var w = req.writer().unbuffered(); | 1031 | var w = body_writer.interface().unbuffered(); |
| 1065 | try w.writeAll("Hello, "); | 1032 | try w.writeAll("Hello, "); |
| 1066 | try w.writeAll("World!\n"); | 1033 | try w.writeAll("World!\n"); |
| 1067 | try req.finish(); | 1034 | try body_writer.end(); |
| 1068 | 1035 | ||
| 1069 | try req.wait(); | 1036 | var response = try req.receiveHead(&redirect_buffer); |
| 1070 | 1037 | ||
| 1071 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 1038 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 1072 | defer gpa.free(body); | 1039 | defer gpa.free(body); |
| 1073 | 1040 | ||
| 1074 | try expectEqualStrings("Hello, World!\n", body); | 1041 | try expectEqualStrings("Hello, World!\n", body); |
| ... | @@ -1103,9 +1070,8 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1103,9 +1070,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1103 | defer gpa.free(location); | 1070 | defer gpa.free(location); |
| 1104 | const uri = try std.Uri.parse(location); | 1071 | const uri = try std.Uri.parse(location); |
| 1105 | 1072 | ||
| 1106 | var server_header_buffer: [1024]u8 = undefined; | 1073 | var redirect_buffer: [1024]u8 = undefined; |
| 1107 | var req = try client.open(.POST, uri, .{ | 1074 | var req = try client.open(.POST, uri, .{ |
| 1108 | .server_header_buffer = &server_header_buffer, | ||
| 1109 | .extra_headers = &.{ | 1075 | .extra_headers = &.{ |
| 1110 | .{ .name = "expect", .value = "100-continue" }, | 1076 | .{ .name = "expect", .value = "100-continue" }, |
| 1111 | .{ .name = "content-type", .value = "text/plain" }, | 1077 | .{ .name = "content-type", .value = "text/plain" }, |
| ... | @@ -1115,16 +1081,16 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1115,16 +1081,16 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1115 | 1081 | ||
| 1116 | req.transfer_encoding = .chunked; | 1082 | req.transfer_encoding = .chunked; |
| 1117 | 1083 | ||
| 1118 | try req.send(); | 1084 | var body_writer = try req.sendBody(); |
| 1119 | var w = req.writer().unbuffered(); | 1085 | var w = body_writer.interface().unbuffered(); |
| 1120 | try w.writeAll("Hello, "); | 1086 | try w.writeAll("Hello, "); |
| 1121 | try w.writeAll("World!\n"); | 1087 | try w.writeAll("World!\n"); |
| 1122 | try req.finish(); | 1088 | try body_writer.end(); |
| 1123 | 1089 | ||
| 1124 | try req.wait(); | 1090 | var response = try req.receiveHead(&redirect_buffer); |
| 1125 | try expectEqual(.ok, req.response.status); | 1091 | try expectEqual(.ok, response.head.status); |
| 1126 | 1092 | ||
| 1127 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 1093 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 1128 | defer gpa.free(body); | 1094 | defer gpa.free(body); |
| 1129 | 1095 | ||
| 1130 | try expectEqualStrings("Hello, World!\n", body); | 1096 | try expectEqualStrings("Hello, World!\n", body); |
| ... | @@ -1135,9 +1101,8 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1135,9 +1101,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1135 | defer gpa.free(location); | 1101 | defer gpa.free(location); |
| 1136 | const uri = try std.Uri.parse(location); | 1102 | const uri = try std.Uri.parse(location); |
| 1137 | 1103 | ||
| 1138 | var server_header_buffer: [1024]u8 = undefined; | 1104 | var redirect_buffer: [1024]u8 = undefined; |
| 1139 | var req = try client.open(.POST, uri, .{ | 1105 | var req = try client.open(.POST, uri, .{ |
| 1140 | .server_header_buffer = &server_header_buffer, | ||
| 1141 | .extra_headers = &.{ | 1106 | .extra_headers = &.{ |
| 1142 | .{ .name = "content-type", .value = "text/plain" }, | 1107 | .{ .name = "content-type", .value = "text/plain" }, |
| 1143 | .{ .name = "expect", .value = "garbage" }, | 1108 | .{ .name = "expect", .value = "garbage" }, |
| ... | @@ -1147,9 +1112,11 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1147,9 +1112,11 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1147 | 1112 | ||
| 1148 | req.transfer_encoding = .chunked; | 1113 | req.transfer_encoding = .chunked; |
| 1149 | 1114 | ||
| 1150 | try req.send(); | 1115 | var body_writer = try req.sendBody(); |
| 1151 | try req.wait(); | 1116 | try body_writer.flush(); |
| 1152 | try expectEqual(.expectation_failed, req.response.status); | 1117 | var response = try req.receiveHead(&redirect_buffer); |
| 1118 | try expectEqual(.expectation_failed, response.head.status); | ||
| 1119 | _ = try response.reader().discardRemaining(); | ||
| 1153 | } | 1120 | } |
| 1154 | 1121 | ||
| 1155 | _ = try client.fetch(.{ | 1122 | _ = try client.fetch(.{ |
| ... | @@ -1255,16 +1222,14 @@ test "redirect to different connection" { | ... | @@ -1255,16 +1222,14 @@ test "redirect to different connection" { |
| 1255 | const uri = try std.Uri.parse(location); | 1222 | const uri = try std.Uri.parse(location); |
| 1256 | 1223 | ||
| 1257 | { | 1224 | { |
| 1258 | var server_header_buffer: [666]u8 = undefined; | 1225 | var redirect_buffer: [666]u8 = undefined; |
| 1259 | var req = try client.open(.GET, uri, .{ | 1226 | var req = try client.open(.GET, uri, .{}); |
| 1260 | .server_header_buffer = &server_header_buffer, | ||
| 1261 | }); | ||
| 1262 | defer req.deinit(); | 1227 | defer req.deinit(); |
| 1263 | 1228 | ||
| 1264 | try req.send(); | 1229 | try req.sendBodiless(); |
| 1265 | try req.wait(); | 1230 | var response = try req.receiveHead(&redirect_buffer); |
| 1266 | 1231 | ||
| 1267 | const body = try req.reader().readRemainingAlloc(gpa, .limited(8192)); | 1232 | const body = try response.reader().readRemainingAlloc(gpa, .limited(8192)); |
| 1268 | defer gpa.free(body); | 1233 | defer gpa.free(body); |
| 1269 | 1234 | ||
| 1270 | try expectEqualStrings("good job, you pass", body); | 1235 | try expectEqualStrings("good job, you pass", body); |
lib/std/io/BufferedReader.zig+34| ... | @@ -919,6 +919,40 @@ fn takeMultipleOf7Leb128(br: *BufferedReader, comptime Result: type) TakeLeb128E | ... | @@ -919,6 +919,40 @@ fn takeMultipleOf7Leb128(br: *BufferedReader, comptime Result: type) TakeLeb128E |
| 919 | } | 919 | } |
| 920 | } | 920 | } |
| 921 | 921 | ||
| 922 | /// Left-aligns data such that `br.seek` becomes zero. | ||
| 923 | pub fn rebase(br: *BufferedReader) void { | ||
| 924 | const data = br.buffer[br.seek..br.end]; | ||
| 925 | const dest = br.buffer[0..data.len]; | ||
| 926 | std.mem.copyForwards(u8, dest, data); | ||
| 927 | br.seek = 0; | ||
| 928 | br.end = data.len; | ||
| 929 | } | ||
| 930 | |||
| 931 | /// Advances the stream and decreases the size of the storage buffer by `n`, | ||
| 932 | /// returning the range of bytes no longer accessible by `br`. | ||
| 933 | /// | ||
| 934 | /// This action can be undone by `restitute`. | ||
| 935 | /// | ||
| 936 | /// Asserts there are at least `n` buffered bytes already. | ||
| 937 | /// | ||
| 938 | /// Asserts that `br.seek` is zero, i.e. the buffer is in a rebased state. | ||
| 939 | pub fn steal(br: *BufferedReader, n: usize) []u8 { | ||
| 940 | assert(br.seek == 0); | ||
| 941 | assert(n <= br.end); | ||
| 942 | const stolen = br.buffer[0..n]; | ||
| 943 | br.buffer = br.buffer[n..]; | ||
| 944 | br.end -= n; | ||
| 945 | return stolen; | ||
| 946 | } | ||
| 947 | |||
| 948 | /// Expands the storage buffer, undoing the effects of `steal` | ||
| 949 | /// Assumes that `n` does not exceed the total number of stolen bytes. | ||
| 950 | pub fn restitute(br: *BufferedReader, n: usize) void { | ||
| 951 | br.buffer = (br.buffer.ptr - n)[0 .. br.buffer.len + n]; | ||
| 952 | br.end += n; | ||
| 953 | br.seek += n; | ||
| 954 | } | ||
| 955 | |||
| 922 | test initFixed { | 956 | test initFixed { |
| 923 | var br: BufferedReader = undefined; | 957 | var br: BufferedReader = undefined; |
| 924 | br.initFixed("a\x02"); | 958 | br.initFixed("a\x02"); |