authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-11 17:17:09-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:10-07:00
log90bd4f226e2ba03634d31c73df06bf0a90fa0231
tree0a5cc52fa31ac0e0d9f8f40bce9534c67c4bd89c
parentf1cf300c8fa9842ec9c812310bdc9f3aeeb75359

std.http: remove the ability to heap-allocate headers

The buffer for HTTP headers is now always provided via a static buffer. As a consequence, OutOfMemory is no longer a member of the read() error set, and the API and implementation of Client and Server are simplified. error.HttpHeadersExceededSizeLimit is renamed to error.HttpHeadersOversize.

5 files changed, 210 insertions(+), 179 deletions(-)

lib/std/http/Client.zig+78-54
......@@ -20,9 +20,7 @@ const proto = @import("protocol.zig");
2020
2121pub const disable_tls = std.options.http_disable_tls;
2222
23/// Allocator used for all allocations made by the client.
24///
25/// This allocator must be thread-safe.
23/// Used for all client allocations. Must be thread-safe.
2624allocator: Allocator,
2725
2826ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
......@@ -35,10 +33,12 @@ next_https_rescan_certs: bool = true,
3533/// The pool of connections that can be reused (and currently in use).
3634connection_pool: ConnectionPool = .{},
3735
38/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections.
36/// This is the proxy that will handle http:// connections. It *must not* be
37/// modified when the client has any active connections.
3938http_proxy: ?Proxy = null,
4039
41/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections.
40/// This is the proxy that will handle https:// connections. It *must not* be
41/// modified when the client has any active connections.
4242https_proxy: ?Proxy = null,
4343
4444/// A set of linked lists of connections that can be reused.
......@@ -609,10 +609,6 @@ pub const Request = struct {
609609 req.headers.deinit();
610610 req.response.headers.deinit();
611611
612 if (req.response.parser.header_bytes_owned) {
613 req.response.parser.header_bytes.deinit(req.client.allocator);
614 }
615
616612 if (req.connection) |connection| {
617613 if (!req.response.parser.done) {
618614 // If the response wasn't fully read, then we need to close the connection.
......@@ -810,27 +806,38 @@ pub const Request = struct {
810806 return index;
811807 }
812808
813 pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported };
809 pub const WaitError = RequestError || SendError || TransferReadError ||
810 proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError ||
811 error{ // TODO: file zig fmt issue for this bad indentation
812 TooManyHttpRedirects,
813 RedirectRequiresResend,
814 HttpRedirectMissingLocation,
815 CompressionInitializationFailed,
816 CompressionNotSupported,
817 };
814818
815819 /// Waits for a response from the server and parses any headers that are sent.
816820 /// This function will block until the final response is received.
817821 ///
818 /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow
819 /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend.
822 /// If `handle_redirects` is true and the request has no payload, then this
823 /// function will automatically follow redirects. If a request payload is
824 /// present, then this function will error with
825 /// error.RedirectRequiresResend.
820826 ///
821 /// Must be called after `send` and, if any data was written to the request body, then also after `finish`.
827 /// Must be called after `send` and, if any data was written to the request
828 /// body, then also after `finish`.
822829 pub fn wait(req: *Request) WaitError!void {
823830 while (true) { // handle redirects
824831 while (true) { // read headers
825832 try req.connection.?.fill();
826833
827 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
834 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
828835 req.connection.?.drop(@intCast(nchecked));
829836
830837 if (req.response.parser.state.isContent()) break;
831838 }
832839
833 try req.response.parse(req.response.parser.header_bytes.items, false);
840 try req.response.parse(req.response.parser.get(), false);
834841
835842 if (req.response.status == .@"continue") {
836843 req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response
......@@ -891,7 +898,8 @@ pub const Request = struct {
891898 if (req.response.status.class() == .redirect and req.handle_redirects) {
892899 req.response.skip = true;
893900
894 // skip the body of the redirect response, this will at least leave the connection in a known good state.
901 // skip the body of the redirect response, this will at least
902 // leave the connection in a known good state.
895903 const empty = @as([*]u8, undefined)[0..0];
896904 assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary
897905
......@@ -908,7 +916,10 @@ pub const Request = struct {
908916 const resolved_url = try req.uri.resolve(new_url, false, arena);
909917
910918 // is the redirect location on the same domain, or a subdomain of the original request?
911 const is_same_domain_or_subdomain = std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and (resolved_url.host.?.len == req.uri.host.?.len or resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');
919 const is_same_domain_or_subdomain =
920 std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and
921 (resolved_url.host.?.len == req.uri.host.?.len or
922 resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.');
912923
913924 if (resolved_url.host == null or !is_same_domain_or_subdomain or !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme)) {
914925 // we're redirecting to a different domain, strip privileged headers like cookies
......@@ -957,7 +968,8 @@ pub const Request = struct {
957968 }
958969 }
959970
960 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers };
971 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError ||
972 error{ DecompressionFailure, InvalidTrailers };
961973
962974 pub const Reader = std.io.Reader(*Request, ReadError, read);
963975
......@@ -980,14 +992,16 @@ pub const Request = struct {
980992 while (!req.response.parser.state.isContent()) { // read trailing headers
981993 try req.connection.?.fill();
982994
983 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
995 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
984996 req.connection.?.drop(@intCast(nchecked));
985997 }
986998
987999 if (has_trail) {
988 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
1000 // The response headers before the trailers are already
1001 // guaranteed to be valid, so they will always be parsed again
1002 // and cannot return an error.
9891003 // This will *only* fail for a malformed trailer.
990 req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers;
1004 req.response.parse(req.response.parser.get(), true) catch return error.InvalidTrailers;
9911005 }
9921006 }
9931007
......@@ -1362,13 +1376,11 @@ pub fn connectTunnel(
13621376 .fragment = null,
13631377 };
13641378
1365 // we can use a small buffer here because a CONNECT response should be very small
13661379 var buffer: [8096]u8 = undefined;
1367
13681380 var req = client.open(.CONNECT, uri, proxy.headers, .{
13691381 .handle_redirects = false,
13701382 .connection = conn,
1371 .header_strategy = .{ .static = &buffer },
1383 .server_header_buffer = &buffer,
13721384 }) catch |err| {
13731385 std.log.debug("err {}", .{err});
13741386 break :tunnel err;
......@@ -1445,7 +1457,9 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
14451457 return client.connectTcp(host, port, protocol);
14461458}
14471459
1448pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || std.fmt.ParseIntError || Connection.WriteError || error{
1460pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
1461 std.fmt.ParseIntError || Connection.WriteError ||
1462 error{ // TODO: file a zig fmt issue for this bad indentation
14491463 UnsupportedUrlScheme,
14501464 UriMissingHost,
14511465
......@@ -1456,36 +1470,29 @@ pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendE
14561470pub const RequestOptions = struct {
14571471 version: http.Version = .@"HTTP/1.1",
14581472
1459 /// Automatically ignore 100 Continue responses. This assumes you don't care, and will have sent the body before you
1460 /// wait for the response.
1473 /// Automatically ignore 100 Continue responses. This assumes you don't
1474 /// care, and will have sent the body before you wait for the response.
14611475 ///
1462 /// If this is not the case AND you know the server will send a 100 Continue, set this to false and wait for a
1463 /// response before sending the body. If you wait AND the server does not send a 100 Continue before you finish the
1464 /// request, then the request *will* deadlock.
1476 /// If this is not the case AND you know the server will send a 100
1477 /// Continue, set this to false and wait for a response before sending the
1478 /// body. If you wait AND the server does not send a 100 Continue before
1479 /// you finish the request, then the request *will* deadlock.
14651480 handle_continue: bool = true,
14661481
1467 /// Automatically follow redirects. This will only follow redirects for repeatable requests (ie. with no payload or the server has acknowledged the payload)
1482 /// Automatically follow redirects. This will only follow redirects for
1483 /// repeatable requests (ie. with no payload or the server has acknowledged
1484 /// the payload).
14681485 handle_redirects: bool = true,
14691486
14701487 /// How many redirects to follow before returning an error.
14711488 max_redirects: u32 = 3,
1472 header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },
1489 /// Externally-owned memory used to store the server's entire HTTP header.
1490 /// `error.HttpHeadersOversize` is returned from read() when a
1491 /// client sends too many bytes of HTTP headers.
1492 server_header_buffer: []u8,
14731493
14741494 /// Must be an already acquired connection.
14751495 connection: ?*Connection = null,
1476
1477 pub const StorageStrategy = union(enum) {
1478 /// In this case, the client's Allocator will be used to store the
1479 /// entire HTTP header. This value is the maximum total size of
1480 /// HTTP headers allowed, otherwise
1481 /// error.HttpHeadersExceededSizeLimit is returned from read().
1482 dynamic: usize,
1483 /// This is used to store the entire HTTP header. If the HTTP
1484 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
1485 /// is returned from read(). When this is used, `error.OutOfMemory`
1486 /// cannot be returned from `read()`.
1487 static: []u8,
1488 };
14891496};
14901497
14911498pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
......@@ -1502,7 +1509,13 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
15021509///
15031510/// The caller is responsible for calling `deinit()` on the `Request`.
15041511/// This function is threadsafe.
1505pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request {
1512pub fn open(
1513 client: *Client,
1514 method: http.Method,
1515 uri: Uri,
1516 headers: http.Headers,
1517 options: RequestOptions,
1518) RequestError!Request {
15061519 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
15071520
15081521 const port: u16 = uri.port orelse switch (protocol) {
......@@ -1541,10 +1554,7 @@ pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Header
15411554 .reason = undefined,
15421555 .version = undefined,
15431556 .headers = http.Headers{ .allocator = client.allocator, .owned = false },
1544 .parser = switch (options.header_strategy) {
1545 .dynamic => |max| proto.HeadersParser.initDynamic(max),
1546 .static => |buf| proto.HeadersParser.initStatic(buf),
1547 },
1557 .parser = proto.HeadersParser.init(options.server_header_buffer),
15481558 },
15491559 .arena = undefined,
15501560 };
......@@ -1568,17 +1578,30 @@ pub const FetchOptions = struct {
15681578 };
15691579
15701580 pub const ResponseStrategy = union(enum) {
1571 storage: RequestOptions.StorageStrategy,
1581 storage: StorageStrategy,
15721582 file: std.fs.File,
15731583 none,
15741584 };
15751585
1576 header_strategy: RequestOptions.StorageStrategy = .{ .dynamic = 16 * 1024 },
1586 pub const StorageStrategy = union(enum) {
1587 /// In this case, the client's Allocator will be used to store the
1588 /// entire HTTP header. This value is the maximum total size of
1589 /// HTTP headers allowed, otherwise
1590 /// error.HttpHeadersExceededSizeLimit is returned from read().
1591 dynamic: usize,
1592 /// This is used to store the entire HTTP header. If the HTTP
1593 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
1594 /// is returned from read(). When this is used, `error.OutOfMemory`
1595 /// cannot be returned from `read()`.
1596 static: []u8,
1597 };
1598
1599 server_header_buffer: ?[]u8 = null,
15771600 response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } },
15781601
15791602 location: Location,
15801603 method: http.Method = .GET,
1581 headers: http.Headers = http.Headers{ .allocator = std.heap.page_allocator, .owned = false },
1604 headers: http.Headers = .{ .allocator = std.heap.page_allocator, .owned = false },
15821605 payload: Payload = .none,
15831606 raw_uri: bool = false,
15841607};
......@@ -1613,9 +1636,10 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc
16131636 .url => |u| try Uri.parse(u),
16141637 .uri => |u| u,
16151638 };
1639 var server_header_buffer: [16 * 1024]u8 = undefined;
16161640
16171641 var req = try open(client, options.method, uri, options.headers, .{
1618 .header_strategy = options.header_strategy,
1642 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,
16191643 .handle_redirects = options.payload == .none,
16201644 });
16211645 defer req.deinit();
lib/std/http/Server.zig+35-42
......@@ -1,6 +1,7 @@
11//! HTTP Server implementation.
22//!
3//! This server assumes *all* clients are well behaved and standard compliant; it can and will deadlock if a client holds a connection open without sending a request.
3//! This server assumes clients are well behaved and standard compliant; it
4//! deadlocks if a client holds a connection open without sending a request.
45//!
56//! Example usage:
67//!
......@@ -17,7 +18,7 @@
1718//! while (res.reset() != .closing) {
1819//! res.wait() catch |err| switch (err) {
1920//! error.HttpHeadersInvalid => break,
20//! error.HttpHeadersExceededSizeLimit => {
21//! error.HttpHeadersOversize => {
2122//! res.status = .request_header_fields_too_large;
2223//! res.send() catch break;
2324//! break;
......@@ -39,6 +40,7 @@
3940//! }
4041//! ```
4142
43const builtin = @import("builtin");
4244const std = @import("../std.zig");
4345const testing = std.testing;
4446const http = std.http;
......@@ -86,7 +88,7 @@ pub const Connection = struct {
8688 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);
8789 if (nread == 0) return error.EndOfStream;
8890 conn.read_start = 0;
89 conn.read_end = @as(u16, @intCast(nread));
91 conn.read_end = @intCast(nread);
9092 }
9193
9294 pub fn peek(conn: *Connection) []const u8 {
......@@ -382,10 +384,6 @@ pub const Response = struct {
382384
383385 res.headers.deinit();
384386 res.request.headers.deinit();
385
386 if (res.request.parser.header_bytes_owned) {
387 res.request.parser.header_bytes.deinit(res.allocator);
388 }
389387 }
390388
391389 pub const ResetState = enum { reset, closing };
......@@ -548,17 +546,24 @@ pub const Response = struct {
548546 return index;
549547 }
550548
551 pub const WaitError = Connection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || error{ CompressionInitializationFailed, CompressionNotSupported };
549 pub const WaitError = Connection.ReadError ||
550 proto.HeadersParser.CheckCompleteHeadError || Request.ParseError ||
551 error{ CompressionInitializationFailed, CompressionNotSupported };
552552
553553 /// Wait for the client to send a complete request head.
554554 ///
555555 /// For correct behavior, the following rules must be followed:
556556 ///
557 /// * If this returns any error in `Connection.ReadError`, you MUST immediately close the connection by calling `deinit`.
558 /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close the connection by calling `deinit`.
559 /// * If this returns `error.HttpHeadersExceededSizeLimit`, you MUST respond with a 431 status code and then call `deinit`.
560 /// * If this returns any error in `Request.ParseError`, you MUST respond with a 400 status code and then call `deinit`.
561 /// * If this returns any other error, you MUST respond with a 400 status code and then call `deinit`.
557 /// * If this returns any error in `Connection.ReadError`, you MUST
558 /// immediately close the connection by calling `deinit`.
559 /// * If this returns `error.HttpHeadersInvalid`, you MAY immediately close
560 /// the connection by calling `deinit`.
561 /// * If this returns `error.HttpHeadersOversize`, you MUST
562 /// respond with a 431 status code and then call `deinit`.
563 /// * If this returns any error in `Request.ParseError`, you MUST respond
564 /// with a 400 status code and then call `deinit`.
565 /// * If this returns any other error, you MUST respond with a 400 status
566 /// code and then call `deinit`.
562567 /// * If the request has an Expect header containing 100-continue, you MUST either:
563568 /// * Respond with a 100 status code, then call `wait` again.
564569 /// * Respond with a 417 status code.
......@@ -571,14 +576,14 @@ pub const Response = struct {
571576 while (true) {
572577 try res.connection.fill();
573578
574 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());
575 res.connection.drop(@as(u16, @intCast(nchecked)));
579 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());
580 res.connection.drop(@intCast(nchecked));
576581
577582 if (res.request.parser.state.isContent()) break;
578583 }
579584
580585 res.request.headers = .{ .allocator = res.allocator, .owned = true };
581 try res.request.parse(res.request.parser.header_bytes.items);
586 try res.request.parse(res.request.parser.get());
582587
583588 if (res.request.transfer_encoding != .none) {
584589 switch (res.request.transfer_encoding) {
......@@ -641,16 +646,18 @@ pub const Response = struct {
641646 while (!res.request.parser.state.isContent()) { // read trailing headers
642647 try res.connection.fill();
643648
644 const nchecked = try res.request.parser.checkCompleteHead(res.allocator, res.connection.peek());
645 res.connection.drop(@as(u16, @intCast(nchecked)));
649 const nchecked = try res.request.parser.checkCompleteHead(res.connection.peek());
650 res.connection.drop(@intCast(nchecked));
646651 }
647652
648653 if (has_trail) {
649654 res.request.headers = http.Headers{ .allocator = res.allocator, .owned = false };
650655
651 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
656 // The response headers before the trailers are already
657 // guaranteed to be valid, so they will always be parsed again
658 // and cannot return an error.
652659 // This will *only* fail for a malformed trailer.
653 res.request.parse(res.request.parser.header_bytes.items) catch return error.InvalidTrailers;
660 res.request.parse(res.request.parser.get()) catch return error.InvalidTrailers;
654661 }
655662 }
656663
......@@ -751,29 +758,19 @@ pub fn listen(server: *Server, address: net.Address) ListenError!void {
751758
752759pub const AcceptError = net.StreamServer.AcceptError || Allocator.Error;
753760
754pub const HeaderStrategy = union(enum) {
755 /// In this case, the client's Allocator will be used to store the
756 /// entire HTTP header. This value is the maximum total size of
757 /// HTTP headers allowed, otherwise
758 /// error.HttpHeadersExceededSizeLimit is returned from read().
759 dynamic: usize,
760 /// This is used to store the entire HTTP header. If the HTTP
761 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
762 /// is returned from read(). When this is used, `error.OutOfMemory`
763 /// cannot be returned from `read()`.
764 static: []u8,
765};
766
767761pub const AcceptOptions = struct {
768762 allocator: Allocator,
769 header_strategy: HeaderStrategy = .{ .dynamic = 8192 },
763 /// Externally-owned memory used to store the client's entire HTTP header.
764 /// `error.HttpHeadersOversize` is returned from read() when a
765 /// client sends too many bytes of HTTP headers.
766 client_header_buffer: []u8,
770767};
771768
772769/// Accept a new connection.
773770pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
774771 const in = try server.socket.accept();
775772
776 return Response{
773 return .{
777774 .allocator = options.allocator,
778775 .address = in.address,
779776 .connection = .{
......@@ -786,17 +783,12 @@ pub fn accept(server: *Server, options: AcceptOptions) AcceptError!Response {
786783 .method = undefined,
787784 .target = undefined,
788785 .headers = .{ .allocator = options.allocator, .owned = false },
789 .parser = switch (options.header_strategy) {
790 .dynamic => |max| proto.HeadersParser.initDynamic(max),
791 .static => |buf| proto.HeadersParser.initStatic(buf),
792 },
786 .parser = proto.HeadersParser.init(options.client_header_buffer),
793787 },
794788 };
795789}
796790
797791test "HTTP server handles a chunked transfer coding request" {
798 const builtin = @import("builtin");
799
800792 // This test requires spawning threads.
801793 if (builtin.single_threaded) {
802794 return error.SkipZigTest;
......@@ -823,9 +815,10 @@ test "HTTP server handles a chunked transfer coding request" {
823815
824816 const server_thread = try std.Thread.spawn(.{}, (struct {
825817 fn apply(s: *std.http.Server) !void {
818 var header_buffer: [max_header_size]u8 = undefined;
826819 var res = try s.accept(.{
827820 .allocator = allocator,
828 .header_strategy = .{ .dynamic = max_header_size },
821 .client_header_buffer = &header_buffer,
829822 });
830823 defer res.deinit();
831824 defer _ = res.reset();
lib/std/http/protocol.zig+70-74
......@@ -34,54 +34,49 @@ pub const State = enum {
3434
3535pub const HeadersParser = struct {
3636 state: State = .start,
37 /// Whether or not `header_bytes` is allocated or was provided as a fixed buffer.
38 header_bytes_owned: bool,
39 /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.
37 /// A fixed buffer of len `max_header_bytes`.
4038 /// Pointers into this buffer are not stable until after a message is complete.
41 header_bytes: std.ArrayListUnmanaged(u8),
42 /// The maximum allowed size of `header_bytes`.
43 max_header_bytes: usize,
44 next_chunk_length: u64 = 0,
39 header_bytes_buffer: []u8,
40 header_bytes_len: u32,
41 next_chunk_length: u64,
4542 /// Whether this parser is done parsing a complete message.
4643 /// A message is only done when the entire payload has been read.
47 done: bool = false,
44 done: bool,
4845
49 /// Initializes the parser with a dynamically growing header buffer of up to `max` bytes.
50 pub fn initDynamic(max: usize) HeadersParser {
46 /// Initializes the parser with a provided buffer `buf`.
47 pub fn init(buf: []u8) HeadersParser {
5148 return .{
52 .header_bytes = .{},
53 .max_header_bytes = max,
54 .header_bytes_owned = true,
49 .header_bytes_buffer = buf,
50 .header_bytes_len = 0,
51 .done = false,
52 .next_chunk_length = 0,
5553 };
5654 }
5755
58 /// Initializes the parser with a provided buffer `buf`.
59 pub fn initStatic(buf: []u8) HeadersParser {
60 return .{
61 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },
62 .max_header_bytes = buf.len,
63 .header_bytes_owned = false,
56 /// Reinitialize the parser.
57 /// Asserts the parser is in the "done" state.
58 pub fn reset(hp: *HeadersParser) void {
59 assert(hp.done);
60 hp.* = .{
61 .state = .start,
62 .header_bytes_buffer = hp.header_bytes_buffer,
63 .header_bytes_len = 0,
64 .done = false,
65 .next_chunk_length = 0,
6466 };
6567 }
6668
67 /// Completely resets the parser to it's initial state.
68 /// This must be called after a message is complete.
69 pub fn reset(r: *HeadersParser) void {
70 assert(r.done); // The message must be completely read before reset, otherwise the parser is in an invalid state.
71
72 r.header_bytes.clearRetainingCapacity();
73
74 r.* = .{
75 .header_bytes = r.header_bytes,
76 .max_header_bytes = r.max_header_bytes,
77 .header_bytes_owned = r.header_bytes_owned,
78 };
69 pub fn get(hp: HeadersParser) []u8 {
70 return hp.header_bytes_buffer[0..hp.header_bytes_len];
7971 }
8072
81 /// Returns the number of bytes consumed by headers. This is always less than or equal to `bytes.len`.
82 /// You should check `r.state.isContent()` after this to check if the headers are done.
73 /// Returns the number of bytes consumed by headers. This is always less
74 /// than or equal to `bytes.len`.
75 /// You should check `r.state.isContent()` after this to check if the
76 /// headers are done.
8377 ///
84 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in a content state and the
78 /// If the amount returned is less than `bytes.len`, you may assume that
79 /// the parser is in a content state and the
8580 /// first byte of content is located at `bytes[result]`.
8681 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
8782 const vector_len: comptime_int = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
......@@ -410,11 +405,14 @@ pub const HeadersParser = struct {
410405 }
411406 }
412407
413 /// Returns the number of bytes consumed by the chunk size. This is always less than or equal to `bytes.len`.
414 /// You should check `r.state == .chunk_data` after this to check if the chunk size has been fully parsed.
408 /// Returns the number of bytes consumed by the chunk size. This is always
409 /// less than or equal to `bytes.len`.
410 /// You should check `r.state == .chunk_data` after this to check if the
411 /// chunk size has been fully parsed.
415412 ///
416 /// If the amount returned is less than `bytes.len`, you may assume that the parser is in the `chunk_data` state
417 /// and that the first byte of the chunk is at `bytes[result]`.
413 /// If the amount returned is less than `bytes.len`, you may assume that
414 /// the parser is in the `chunk_data` state and that the first byte of the
415 /// chunk is at `bytes[result]`.
418416 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
419417 const len = @as(u32, @intCast(bytes.len));
420418
......@@ -488,30 +486,27 @@ pub const HeadersParser = struct {
488486 return len;
489487 }
490488
491 /// Returns whether or not the parser has finished parsing a complete message. A message is only complete after the
492 /// entire body has been read and any trailing headers have been parsed.
489 /// Returns whether or not the parser has finished parsing a complete
490 /// message. A message is only complete after the entire body has been read
491 /// and any trailing headers have been parsed.
493492 pub fn isComplete(r: *HeadersParser) bool {
494493 return r.done and r.state == .finished;
495494 }
496495
497 pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};
496 pub const CheckCompleteHeadError = error{HttpHeadersOversize};
498497
499 /// Pushes `in` into the parser. Returns the number of bytes consumed by the header. Any header bytes are appended
500 /// to the `header_bytes` buffer.
501 ///
502 /// This function only uses `allocator` if `r.header_bytes_owned` is true, and may be undefined otherwise.
503 pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {
504 if (r.state.isContent()) return 0;
498 /// Pushes `in` into the parser. Returns the number of bytes consumed by
499 /// the header. Any header bytes are appended to `header_bytes_buffer`.
500 pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 {
501 if (hp.state.isContent()) return 0;
505502
506 const i = r.findHeadersEnd(in);
503 const i = hp.findHeadersEnd(in);
507504 const data = in[0..i];
508 if (r.header_bytes.items.len + data.len > r.max_header_bytes) {
509 return error.HttpHeadersExceededSizeLimit;
510 } else {
511 if (r.header_bytes_owned) try r.header_bytes.ensureUnusedCapacity(allocator, data.len);
505 if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len)
506 return error.HttpHeadersOversize;
512507
513 r.header_bytes.appendSliceAssumeCapacity(data);
514 }
508 @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data);
509 hp.header_bytes_len += @intCast(data.len);
515510
516511 return i;
517512 }
......@@ -520,7 +515,8 @@ pub const HeadersParser = struct {
520515 HttpChunkInvalid,
521516 };
522517
523 /// Reads the body of the message into `buffer`. Returns the number of bytes placed in the buffer.
518 /// Reads the body of the message into `buffer`. Returns the number of
519 /// bytes placed in the buffer.
524520 ///
525521 /// If `skip` is true, the buffer will be unused and the body will be skipped.
526522 ///
......@@ -718,7 +714,7 @@ test "HeadersParser.findHeadersEnd" {
718714 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
719715
720716 for (0..36) |i| {
721 r = HeadersParser.initDynamic(0);
717 r = HeadersParser.init(&.{});
722718 try std.testing.expectEqual(@as(u32, @intCast(i)), r.findHeadersEnd(data[0..i]));
723719 try std.testing.expectEqual(@as(u32, @intCast(35 - i)), r.findHeadersEnd(data[i..]));
724720 }
......@@ -728,7 +724,7 @@ test "HeadersParser.findChunkedLen" {
728724 var r: HeadersParser = undefined;
729725 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
730726
731 r = HeadersParser.initDynamic(0);
727 r = HeadersParser.init(&.{});
732728 r.state = .chunk_head_size;
733729 r.next_chunk_length = 0;
734730
......@@ -761,9 +757,9 @@ test "HeadersParser.findChunkedLen" {
761757
762758test "HeadersParser.read length" {
763759 // mock BufferedConnection for read
760 var headers_buf: [256]u8 = undefined;
764761
765 var r = HeadersParser.initDynamic(256);
766 defer r.header_bytes.deinit(std.testing.allocator);
762 var r = HeadersParser.init(&headers_buf);
767763 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
768764
769765 var conn: MockBufferedConnection = .{
......@@ -773,8 +769,8 @@ test "HeadersParser.read length" {
773769 while (true) { // read headers
774770 try conn.fill();
775771
776 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
777 conn.drop(@as(u16, @intCast(nchecked)));
772 const nchecked = try r.checkCompleteHead(conn.peek());
773 conn.drop(@intCast(nchecked));
778774
779775 if (r.state.isContent()) break;
780776 }
......@@ -786,14 +782,14 @@ test "HeadersParser.read length" {
786782 try std.testing.expectEqual(@as(usize, 5), len);
787783 try std.testing.expectEqualStrings("Hello", buf[0..len]);
788784
789 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.header_bytes.items);
785 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get());
790786}
791787
792788test "HeadersParser.read chunked" {
793789 // mock BufferedConnection for read
794790
795 var r = HeadersParser.initDynamic(256);
796 defer r.header_bytes.deinit(std.testing.allocator);
791 var headers_buf: [256]u8 = undefined;
792 var r = HeadersParser.init(&headers_buf);
797793 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";
798794
799795 var conn: MockBufferedConnection = .{
......@@ -803,8 +799,8 @@ test "HeadersParser.read chunked" {
803799 while (true) { // read headers
804800 try conn.fill();
805801
806 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
807 conn.drop(@as(u16, @intCast(nchecked)));
802 const nchecked = try r.checkCompleteHead(conn.peek());
803 conn.drop(@intCast(nchecked));
808804
809805 if (r.state.isContent()) break;
810806 }
......@@ -815,14 +811,14 @@ test "HeadersParser.read chunked" {
815811 try std.testing.expectEqual(@as(usize, 5), len);
816812 try std.testing.expectEqualStrings("Hello", buf[0..len]);
817813
818 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.header_bytes.items);
814 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get());
819815}
820816
821817test "HeadersParser.read chunked trailer" {
822818 // mock BufferedConnection for read
823819
824 var r = HeadersParser.initDynamic(256);
825 defer r.header_bytes.deinit(std.testing.allocator);
820 var headers_buf: [256]u8 = undefined;
821 var r = HeadersParser.init(&headers_buf);
826822 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";
827823
828824 var conn: MockBufferedConnection = .{
......@@ -832,8 +828,8 @@ test "HeadersParser.read chunked trailer" {
832828 while (true) { // read headers
833829 try conn.fill();
834830
835 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
836 conn.drop(@as(u16, @intCast(nchecked)));
831 const nchecked = try r.checkCompleteHead(conn.peek());
832 conn.drop(@intCast(nchecked));
837833
838834 if (r.state.isContent()) break;
839835 }
......@@ -847,11 +843,11 @@ test "HeadersParser.read chunked trailer" {
847843 while (true) { // read headers
848844 try conn.fill();
849845
850 const nchecked = try r.checkCompleteHead(std.testing.allocator, conn.peek());
851 conn.drop(@as(u16, @intCast(nchecked)));
846 const nchecked = try r.checkCompleteHead(conn.peek());
847 conn.drop(@intCast(nchecked));
852848
853849 if (r.state.isContent()) break;
854850 }
855851
856 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.header_bytes.items);
852 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get());
857853}
src/Package/Fetch.zig+14-7
......@@ -354,7 +354,8 @@ pub fn run(f: *Fetch) RunError!void {
354354 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
355355 ));
356356 };
357 var resource = try f.initResource(uri);
357 var server_header_buffer: [header_buffer_size]u8 = undefined;
358 var resource = try f.initResource(uri, &server_header_buffer);
358359 return runResource(f, uri.path, &resource, null);
359360 }
360361 },
......@@ -415,7 +416,8 @@ pub fn run(f: *Fetch) RunError!void {
415416 f.location_tok,
416417 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
417418 );
418 var resource = try f.initResource(uri);
419 var server_header_buffer: [header_buffer_size]u8 = undefined;
420 var resource = try f.initResource(uri, &server_header_buffer);
419421 return runResource(f, uri.path, &resource, remote.hash);
420422}
421423
......@@ -876,7 +878,9 @@ const FileType = enum {
876878 }
877879};
878880
879fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
881const header_buffer_size = 16 * 1024;
882
883fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource {
880884 const gpa = f.arena.child_allocator;
881885 const arena = f.arena.allocator();
882886 const eb = &f.error_bundle;
......@@ -894,10 +898,12 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
894898 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
895899 ascii.eqlIgnoreCase(uri.scheme, "https"))
896900 {
897 var h = std.http.Headers{ .allocator = gpa };
901 var h: std.http.Headers = .{ .allocator = gpa };
898902 defer h.deinit();
899903
900 var req = http_client.open(.GET, uri, h, .{}) catch |err| {
904 var req = http_client.open(.GET, uri, h, .{
905 .server_header_buffer = server_header_buffer,
906 }) catch |err| {
901907 return f.fail(f.location_tok, try eb.printString(
902908 "unable to connect to server: {s}",
903909 .{@errorName(err)},
......@@ -935,7 +941,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
935941 transport_uri.scheme = uri.scheme["git+".len..];
936942 var redirect_uri: []u8 = undefined;
937943 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
938 session.discoverCapabilities(gpa, &redirect_uri) catch |err| switch (err) {
944 session.discoverCapabilities(gpa, &redirect_uri, server_header_buffer) catch |err| switch (err) {
939945 error.Redirected => {
940946 defer gpa.free(redirect_uri);
941947 return f.fail(f.location_tok, try eb.printString(
......@@ -961,6 +967,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
961967 var ref_iterator = session.listRefs(gpa, .{
962968 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
963969 .include_peeled = true,
970 .server_header_buffer = server_header_buffer,
964971 }) catch |err| {
965972 return f.fail(f.location_tok, try eb.printString(
966973 "unable to list refs: {s}",
......@@ -1003,7 +1010,7 @@ fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
10031010 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
10041011 std.fmt.fmtSliceHexLower(&want_oid),
10051012 }) catch unreachable;
1006 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}) catch |err| {
1013 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}, server_header_buffer) catch |err| {
10071014 return f.fail(f.location_tok, try eb.printString(
10081015 "unable to create fetch stream: {s}",
10091016 .{@errorName(err)},
src/Package/Fetch/git.zig+13-2
......@@ -494,8 +494,9 @@ pub const Session = struct {
494494 session: *Session,
495495 allocator: Allocator,
496496 redirect_uri: *[]u8,
497 http_headers_buffer: []u8,
497498 ) !void {
498 var capability_iterator = try session.getCapabilities(allocator, redirect_uri);
499 var capability_iterator = try session.getCapabilities(allocator, redirect_uri, http_headers_buffer);
499500 defer capability_iterator.deinit();
500501 while (try capability_iterator.next()) |capability| {
501502 if (mem.eql(u8, capability.key, "agent")) {
......@@ -521,6 +522,7 @@ pub const Session = struct {
521522 session: Session,
522523 allocator: Allocator,
523524 redirect_uri: *[]u8,
525 http_headers_buffer: []u8,
524526 ) !CapabilityIterator {
525527 var info_refs_uri = session.uri;
526528 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });
......@@ -534,6 +536,7 @@ pub const Session = struct {
534536
535537 var request = try session.transport.open(.GET, info_refs_uri, headers, .{
536538 .max_redirects = 3,
539 .server_header_buffer = http_headers_buffer,
537540 });
538541 errdefer request.deinit();
539542 try request.send(.{});
......@@ -620,6 +623,7 @@ pub const Session = struct {
620623 include_symrefs: bool = false,
621624 /// Whether to include the peeled object ID for returned tag refs.
622625 include_peeled: bool = false,
626 server_header_buffer: []u8,
623627 };
624628
625629 /// Returns an iterator over refs known to the server.
......@@ -658,6 +662,7 @@ pub const Session = struct {
658662
659663 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
660664 .handle_redirects = false,
665 .server_header_buffer = options.server_header_buffer,
661666 });
662667 errdefer request.deinit();
663668 request.transfer_encoding = .{ .content_length = body.items.len };
......@@ -721,7 +726,12 @@ pub const Session = struct {
721726
722727 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
723728 /// performed if the server supports it.
724 pub fn fetch(session: Session, allocator: Allocator, wants: []const []const u8) !FetchStream {
729 pub fn fetch(
730 session: Session,
731 allocator: Allocator,
732 wants: []const []const u8,
733 http_headers_buffer: []u8,
734 ) !FetchStream {
725735 var upload_pack_uri = session.uri;
726736 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
727737 defer allocator.free(upload_pack_uri.path);
......@@ -758,6 +768,7 @@ pub const Session = struct {
758768
759769 var request = try session.transport.open(.POST, upload_pack_uri, headers, .{
760770 .handle_redirects = false,
771 .server_header_buffer = http_headers_buffer,
761772 });
762773 errdefer request.deinit();
763774 request.transfer_encoding = .{ .content_length = body.items.len };