authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-04-25 19:58:26-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:28-07:00
log81b0d14e2ba43732c80a259b578997f167c23cdd
treed64899ee9d0e9529fae47576a0111a3c3ba46679
parent396464ee6b8f5534947e5f457dbd2a26a458cb0f

std.http: rewrite

WIP

9 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.
33
4const std = @import("std.zig");
5const testing = std.testing;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8
9const Uri = @This();
10
4scheme: []const u8,11scheme: []const u8,
5user: ?Component = null,12user: ?Component = null,
6password: ?Component = null,13password: ?Component = null,
...@@ -10,6 +17,32 @@ path: Component = Component.empty,...@@ -10,6 +17,32 @@ path: Component = Component.empty,
10query: ?Component = null,17query: ?Component = null,
11fragment: ?Component = null,18fragment: ?Component = null,
1219
20pub 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`
28pub 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`
39pub 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
13pub const Component = union(enum) {46pub const Component = union(enum) {
14 /// Invalid characters in this component must be percent encoded47 /// 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 }
2861
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`.
146pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri {190pub 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;
150193
151 if (reader.peekPrefix("//")) a: { // authority part194 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 }
159201
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 }
206248
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] };
208252
209 if ((reader.peek() orelse 0) == '?') { // query part253 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 }
213258
214 if ((reader.peek() orelse 0) == '#') { // fragment part259 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 }
218262
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}
293337
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 the339/// Each component that is provided will be non-`null`.
296/// original `text`. Each component that is provided, will be non-`null`.
297pub fn parse(text: []const u8) ParseError!Uri {340pub 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;
300343 } else text.len;
301 // after the scheme, a ':' must appear344 // 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}
311349
312pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft};350pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft};
313351
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 stored357/// 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 modified358/// 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.
320pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri {360pub 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);
330366
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}
440476
441const 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 / "+" / "-" / "." )
495fn isSchemeChar(c: u8) bool {478fn 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}
501484
502/// reserved = gen-delims / sub-delims
503fn isReserved(c: u8) bool {
504 return isGenLimit(c) or isSubLimit(c);
505}
506
507/// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
508fn isGenLimit(c: u8) bool {
509 return switch (c) {
510 ':', ',', '?', '#', '[', ']', '@' => true,
511 else => false,
512 };
513}
514
515/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"485/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
516/// / "*" / "+" / "," / ";" / "="486/// / "*" / "+" / "," / ";" / "="
517fn isSubLimit(c: u8) bool {487fn isSubLimit(c: u8) bool {
...@@ -551,26 +521,8 @@ fn isQueryChar(c: u8) bool {...@@ -551,26 +521,8 @@ fn isQueryChar(c: u8) bool {
551521
552const isFragmentChar = isQueryChar;522const isFragmentChar = isQueryChar;
553523
554fn isAuthoritySeparator(c: u8) bool {524const authority_sep: [3]u8 = .{ '/', '?', '#' };
555 return switch (c) {525const path_sep: [2]u8 = .{ '?', '#' };
556 '/', '?', '#' => true,
557 else => false,
558 };
559}
560
561fn isPathSeparator(c: u8) bool {
562 return switch (c) {
563 '?', '#' => true,
564 else => false,
565 };
566}
567
568fn isQuerySeparator(c: u8) bool {
569 return switch (c) {
570 '#' => true,
571 else => false,
572 };
573}
574526
575test "basic" {527test "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
855const std = @import("std.zig");
856const testing = std.testing;
857const Uri = @This();
lib/std/http.zig+740-12
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1const builtin = @import("builtin");
2const std = @import("std.zig");
3const assert = std.debug.assert;
4
1pub const Client = @import("http/Client.zig");5pub const Client = @import("http/Client.zig");
2pub const Server = @import("http/Server.zig");6pub const Server = @import("http/Server.zig");
3pub const protocol = @import("http/protocol.zig");
4pub const HeadParser = @import("http/HeadParser.zig");7pub const HeadParser = @import("http/HeadParser.zig");
5pub const ChunkParser = @import("http/ChunkParser.zig");8pub const ChunkParser = @import("http/ChunkParser.zig");
6pub const HeaderIterator = @import("http/HeaderIterator.zig");9pub 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 }
7982
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/Idempotent87 /// 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 }
9297
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/cacheable101 /// 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};
284290
291/// compression is intentionally omitted here since it is handled in `ContentEncoding`.
285pub const TransferEncoding = enum {292pub 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};
290296
291pub const ContentEncoding = enum {297pub 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};
310316
311const builtin = @import("builtin");317pub const Reader = struct {
312const 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.
681pub 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};
3131040
314test {1041test {
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.
22
3const ChunkParser = @This();
4const std = @import("std");
5
3state: State,6state: State,
4chunk_len: u64,7chunk_len: u64,
58
...@@ -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}
99102
100const ChunkParser = @This();
101const std = @import("std");
102
103test feed {103test feed {
104 const testing = std.testing;104 const testing = std.testing;
105105
lib/std/http/Client.zig+487-661
...@@ -15,7 +15,6 @@ const Allocator = mem.Allocator;...@@ -15,7 +15,6 @@ const Allocator = mem.Allocator;
15const assert = std.debug.assert;15const assert = std.debug.assert;
1616
17const Client = @This();17const Client = @This();
18const proto = @import("protocol.zig");
1918
20pub const disable_tls = std.options.http_disable_tls;19pub const disable_tls = std.options.http_disable_tls;
2120
...@@ -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 };
7372
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};
203202
203pub 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
204pub const Connection = struct {229pub 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,
217242
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};
413436
414/// The mode of transport for requests.
415pub 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.
422pub const Compression = union(enum) {438pub 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};
434450
435/// A HTTP response originating from a server.
436pub const Response = struct {451pub 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`.
440455 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,
447462 location: ?[]const u8 = null,
448 keep_alive: bool,463 content_type: ?[]const u8 = null,
449464 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 };
452484
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");
455494
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 }
458499
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 };
461516
462 /// Whether the response body should be skipped. Any data read from the517 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 }
465523
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 }
474552
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, " ");
477555
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 }
482563
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 };
499567
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 }
506569
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 }
535573
536 if (next) |second| {574 const trimmed = mem.trim(u8, header_value, " ");
537 const trimmed_second = mem.trim(u8, second, " ");
538575
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 supported577 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 };
586585
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" ++
589588 "LOcation:url\r\n" ++
590 try res.parse(response_bytes);589 "content-tYpe: text/plain\r\n" ++
591590 "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";
595594
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);
599598 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.?);
605604
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);
609608 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 }
623610
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 }
627614
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";
636623
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 };
645632
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;
648635
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};
689701
690pub const Request = struct {702pub 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,
696714
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,
701719
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,
704722
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,
712725
...@@ -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,
722735
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 }
773792
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 }
808798
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 is800 pub fn sendBodilessUnflushed(r: *Request) std.io.Writer.Error!void {
811 // still in a valid state, but the redirect must be handled801 assert(r.transfer_encoding == .none);
812 // manually.802 assert(!r.method.requestHasBody());
813 return error.RedirectRequiresResend;803 try sendHead(r);
814 }804 }
815805
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());
820810 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 }
829821
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;
833825 const connection = r.connection.?;
834 const connection = req.connection.?;
835 const w = &connection.writer;826 const w = &connection.writer;
836827
837 try req.method.write(w);828 try r.method.write(w);
838 try w.writeByte(' ');829 try w.writeByte(' ');
839830
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");
854845
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 }
860851
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 }
868859
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 }
874865
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 }
882873
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/18937875 // 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 }
888879
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 }
894885
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 because887 // 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 }
899890
900 for (req.extra_headers) |header| {891 for (r.extra_headers) |header| {
901 assert(header.name.len != 0);892 assert(header.name.len != 0);
902893
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 {
908899
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;
914905
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 }
920911
921 try w.writeAll("\r\n");912 try w.writeAll("\r\n");
922
923 try connection.writer.flush();
924 }913 }
925914
926 /// Returns true if the default behavior is required, otherwise handles915 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,
940929 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 };
974931
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 this932 /// If handling redirects and the request has no payload, then this
979 /// function will automatically follow redirects. If a request payload is933 /// function will automatically follow redirects.
980 /// present, then this function will error with934 ///
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 request938 /// 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's950 try r.reader.receiveHead();
988 // connection may be different than the previous iteration. However, it951 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,
991954 };
992 while (true) { // read headers955 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;
1011956
957 if (head.status == .@"continue") {
958 if (r.handle_continue) continue;
1012 return; // we're not handling the 100-continue959 return; // we're not handling the 100-continue
1013 }960 }
1014961
1015 // we're switching protocols, so this connection is no longer doing http962 // 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 }
1021972
1022 connection.closing = !req.response.keep_alive or !req.keep_alive;973 connection.closing = !head.keep_alive or !r.keep_alive;
1023974
1024 // Any response to a HEAD request and any response with a 1xx975 // Any response to a HEAD request and any response with a 1xx
1025 // (Informational), 204 (No Content), or 304 (Not Modified) status976 // (Informational), 204 (No Content), or 304 (Not Modified) status
1026 // code is always terminated by the first empty line after the977 // code is always terminated by the first empty line after the
1027 // header fields, regardless of the header fields present in the978 // 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 or980 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 }
1035985
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 }
1040993
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 closed996 .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 }
10521009 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 }
11011012
1102 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError ||1013 pub const RedirectError = error{
1103 error{ DecompressionFailure, InvalidTrailers };1014 HttpRedirectLocationOversize,
11041015 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 }
11101017
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/189371023 /// `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);
11211043
1122 while (!req.response.parser.state.isContent()) { // read trailing headers1044 const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme;
1123 try req.connection.?.fill();1045 const old_connection = r.connection.?;
11241046 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);
11281052
1129 return 0;1053 r.client.connection_pool.release(r.client.allocator, old_connection);
1130 }1054 r.connection = null;
11311055
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 }
11421060
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 }
12171071
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 }
12241078
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 }
12381085
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};
12511101
1252pub const Proxy = struct {1102pub 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;
13081158
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 };
13151162
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;
13211168
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),
16061453
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,
16141456
...@@ -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};
16261468
1627fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } {1469fn 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
1643fn uriPort(uri: Uri, protocol: Connection.Protocol) u16 {
1644 return uri.port orelse switch (protocol) {
1645 .plain => 80,
1646 .tls => 443,
1647 };
1648}1471}
16491472
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 }
16771498
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());
16801500
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 }
16941514
1695 const conn = options.connection orelse1515 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 };
16971520
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}
17231536
1724pub const FetchOptions = struct {1537pub 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}
18301643
1644pub 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
1651test 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
1831test {1658test {
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.
32
4const std = @import("../std.zig");3const std = @import("../std.zig");
5const http = std.http;4const http = std.http;
6const mem = std.mem;5const mem = std.mem;
7const net = std.net;
8const Uri = std.Uri;6const Uri = std.Uri;
9const assert = std.debug.assert;7const assert = std.debug.assert;
10const testing = std.testing;8const testing = std.testing;
119
12const Server = @This();10const Server = @This();
1311
14/// The reader's buffer must be large enough to store the client's entire HTTP
15/// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`.
16in: *std.io.BufferedReader,
17/// Data from the HTTP server to the HTTP client.12/// Data from the HTTP server to the HTTP client.
18out: *std.io.BufferedWriter,13out: *std.io.BufferedWriter,
19/// Keeps track of whether the Server is ready to accept a new request on the14/// Internal state managed by this abstraction.
20/// same connection, and makes invalid API usage cause assertion failures15reader: http.Reader,
21/// rather than HTTP protocol violations.
22state: State,
23/// Populated when `receiveHead` returns `ReceiveHeadError.HttpHeadersInvalid`.
24head_parse_err: ?Request.Head.ParseError = null,
25
26pub 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};
4016
41/// Initialize an HTTP server that can respond to multiple requests on the same17/// 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.
45pub fn init(in: *std.io.BufferedReader, out: *std.io.BufferedWriter) Server {24pub 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}
5233
53pub const ReceiveHeadError = error{34pub 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 code36 ///
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};
7041
71/// The header bytes reference the internal storage of `in`, which are42pub fn receiveHead(s: *Server) http.Reader.HeadError!Request {
72/// invalidated with the next call to `receiveHead`.43 try s.reader.receiveHead();
73pub 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.
7748 .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}
10351
104pub const Request = struct {52pub 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 };
14057
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 };
309226
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 }
313230
314 test iterateHeaders {231 test iterateHeaders {
...@@ -332,10 +249,8 @@ pub const Request = struct {...@@ -332,10 +249,8 @@ pub const Request = struct {
332249
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 };
340255
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 };
513428
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. If433 /// 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 the437 /// HEAD requests are handled transparently by setting the
522 /// `Response.elide_body` flag on the returned `Response`, causing438 /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing
523 /// the response stream to omit the body. However, it may be worth noticing439 /// 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 to440 /// 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 };
574490
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 }
586502
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 }
852529 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 }
880531
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 matters543 // 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 };
904556
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
919pub 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,
10request: *std.http.Server.Request,10request: *std.http.Server.Request,
11recv_fifo: std.fifo.LinearFifo(u8, .Slice),11recv_fifo: std.fifo.LinearFifo(u8, .Slice),
12reader: std.io.BufferedReader,12reader: std.io.BufferedReader,
13response: std.http.Server.Response,13body_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.
15outstanding_len: usize,15outstanding_len: usize,
1616
...@@ -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 };
238238
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 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const testing = std.testing;
4const mem = std.mem;
5
6const assert = std.debug.assert;
7
8pub 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
38pub 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
270inline fn int16(array: *const [2]u8) u16 {
271 return @as(u16, @bitCast(array.*));
272}
273
274inline fn int24(array: *const [3]u8) u24 {
275 return @as(u24, @bitCast(array.*));
276}
277
278inline fn int32(array: *const [4]u8) u32 {
279 return @as(u32, @bitCast(array.*));
280}
281
282inline 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.
290const 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
354test "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
384test "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
413test "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);
6262
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();
6966
70 try req.send();67 try req.sendBodiless();
71 try req.wait();68 var response = try req.receiveHead(&.{});
7269
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);
7572
76 try expectEqualStrings("Hello, World!\n", body);73 try expectEqualStrings("Hello, World!\n", body);
7774
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);
566563
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();
573568
574 try req.send();569 try req.sendBodiless();
575 try req.wait();570 var response = try req.receiveHead(&redirect_buffer);
576571
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);
579574
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 }
583578
584 // connection has been kept alive579 // 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);
591586
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();
598591
599 try req.send();592 try req.sendBodiless();
600 try req.wait();593 var response = try req.receiveHead(&redirect_buffer);
601594
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);
604597
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);
615608
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();
622613
623 try req.send();614 try req.sendBodiless();
624 try req.wait();615 var response = try req.receiveHead(&redirect_buffer);
625616
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);
628619
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 }
633624
634 // connection has been kept alive625 // 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);
641632
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();
648637
649 try req.send();638 try req.sendBodiless();
650 try req.wait();639 var response = try req.receiveHead(&redirect_buffer);
651640
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);
654643
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 }
658647
659 // connection has been kept alive648 // 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);
666655
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();
673660
674 try req.send();661 try req.sendBodiless();
675 try req.wait();662 try req.receiveHead(&redirect_buffer);
676663
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);
692679
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();
700686
701 try req.send();687 try req.sendBodiless();
702 try req.wait();688 try req.receiveHead(&redirect_buffer);
703689
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);
718704
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();
728713
729 try req.send();714 try req.sendBodiless();
730 try req.wait();715 try req.receiveHead(&redirect_buffer);
731716
732 try std.testing.expectEqual(.ok, req.response.status);717 try std.testing.expectEqual(.ok, req.response.status);
733718
...@@ -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);
762747
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();
769752
770 try req.send();753 try req.sendBodiless();
771 try req.wait();754 try req.receiveHead(&redirect_buffer);
772755
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);
786769
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();
793774
794 try req.send();775 try req.sendBodiless();
795 try req.wait();776 try req.receiveHead(&redirect_buffer);
796777
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);
810791
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();
817796
818 try req.send();797 try req.sendBodiless();
819 try req.wait();798 try req.receiveHead(&redirect_buffer);
820799
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);
834813
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();
841818
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);
853830
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();
860835
861 try req.send();836 try req.sendBodiless();
862 try req.wait();837 try req.receiveHead(&redirect_buffer);
863838
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);
877852
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();
884857
885 try req.send();858 try req.sendBodiless();
886 const result = req.wait();859 const result = req.receiveHead(&redirect_buffer);
887860
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();
980951
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();
991960
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);
995964
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");
999968
1000 try req.finish();969 try req.finish();
1001970
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);
1004973
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);
1016985
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 {
1025993
1026 req.transfer_encoding = .{ .content_length = 14 };994 req.transfer_encoding = .{ .content_length = 14 };
1027995
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();
10331001
1034 try req.wait();1002 var response = try req.receiveHead(&redirect_buffer);
10351003
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);
10381006
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 ));
10511019
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 {
10601027
1061 req.transfer_encoding = .chunked;1028 req.transfer_encoding = .chunked;
10621029
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();
10681035
1069 try req.wait();1036 var response = try req.receiveHead(&redirect_buffer);
10701037
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);
10731040
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);
11051072
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 {
11151081
1116 req.transfer_encoding = .chunked;1082 req.transfer_encoding = .chunked;
11171083
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();
11231089
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);
11261092
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);
11291095
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);
11371103
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 {
11471112
1148 req.transfer_encoding = .chunked;1113 req.transfer_encoding = .chunked;
11491114
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 }
11541121
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);
12561223
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();
12631228
1264 try req.send();1229 try req.sendBodiless();
1265 try req.wait();1230 var response = try req.receiveHead(&redirect_buffer);
12661231
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);
12691234
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}
921921
922/// Left-aligns data such that `br.seek` becomes zero.
923pub 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.
939pub 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.
950pub 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
922test initFixed {956test initFixed {
923 var br: BufferedReader = undefined;957 var br: BufferedReader = undefined;
924 br.initFixed("a\x02");958 br.initFixed("a\x02");