From 2d090f61be89d0738b9d2644236717e0fada7e4d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 29 Dec 2022 18:57:53 -0700 Subject: [PATCH] add std.http.Headers This is a streaming HTTP header parser. All it currently does is detect the end of headers. This will be a non-allocating parser where one can bring supply their own buffer if they want to handle custom headers. This commit also improves std.http.Client to not return the HTTP headers with the read functions. --- lib/std/http.zig | 50 +++++++++++++++++++++++++++++++++++++++++ lib/std/http/Client.zig | 50 +++++++++++++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lib/std/http.zig b/lib/std/http.zig index cf92b462b801f869c9799a4ba1957c146a530218..944271df274cc9b125af463151724c6288f764c4 100644 --- a/lib/std/http.zig +++ b/lib/std/http.zig @@ -242,10 +242,60 @@ pub const Status = enum(u10) { } }; +pub const Headers = struct { + state: State = .start, + invalid_index: u32 = undefined, + + pub const State = enum { invalid, start, line, nl_r, nl_n, nl2_r, finished }; + + /// Returns how many bytes are processed into headers. Always less than or + /// equal to bytes.len. If the amount returned is less than bytes.len, it + /// means the headers ended and the first byte after the double \r\n\r\n is + /// located at `bytes[result]`. + pub fn feed(h: *Headers, bytes: []const u8) usize { + for (bytes) |b, i| { + switch (h.state) { + .start => switch (b) { + '\r' => h.state = .nl_r, + '\n' => return invalid(h, i), + else => {}, + }, + .nl_r => switch (b) { + '\n' => h.state = .nl_n, + else => return invalid(h, i), + }, + .nl_n => switch (b) { + '\r' => h.state = .nl2_r, + else => h.state = .line, + }, + .nl2_r => switch (b) { + '\n' => h.state = .finished, + else => return invalid(h, i), + }, + .line => switch (b) { + '\r' => h.state = .nl_r, + '\n' => return invalid(h, i), + else => {}, + }, + .invalid => return i, + .finished => return i, + } + } + return bytes.len; + } + + fn invalid(h: *Headers, i: usize) usize { + h.invalid_index = @intCast(u32, i); + h.state = .invalid; + return i; + } +}; + const std = @import("std.zig"); test { _ = Client; _ = Method; _ = Status; + _ = Headers; } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index d27d8796632ad2b0df56eea02a4ebe6d057bd7be..efae62680d3ae0b8e2bf0e55f86343b282641434 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -16,6 +16,7 @@ pub const Request = struct { headers: std.ArrayListUnmanaged(u8) = .{}, tls_client: std.crypto.tls.Client, protocol: Protocol, + response_headers: http.Headers = .{}, pub const Protocol = enum { http, https }; @@ -51,18 +52,53 @@ pub const Request = struct { } } - pub fn read(req: *Request, buffer: []u8) !usize { - switch (req.protocol) { - .http => return req.stream.read(buffer), - .https => return req.tls_client.read(req.stream, buffer), - } - } - pub fn readAll(req: *Request, buffer: []u8) !usize { return readAtLeast(req, buffer, buffer.len); } + pub fn read(req: *Request, buffer: []u8) !usize { + return readAtLeast(req, buffer, 1); + } + pub fn readAtLeast(req: *Request, buffer: []u8, len: usize) !usize { + assert(len <= buffer.len); + var index: usize = 0; + while (index < len) { + const headers_finished = req.response_headers.state == .finished; + const amt = try readAdvanced(req, buffer[index..]); + if (amt == 0 and headers_finished) break; + index += amt; + } + return index; + } + + /// This one can return 0 without meaning EOF. + /// TODO change to readvAdvanced + pub fn readAdvanced(req: *Request, buffer: []u8) !usize { + if (req.response_headers.state == .finished) return readRaw(req, buffer); + + const amt = try readRaw(req, buffer); + const data = buffer[0..amt]; + const i = req.response_headers.feed(data); + if (req.response_headers.state == .invalid) return error.InvalidHttpHeaders; + if (i < data.len) { + const rest = data[i..]; + std.mem.copy(u8, buffer, rest); + return rest.len; + } + return 0; + } + + /// Only abstracts over http/https. + fn readRaw(req: *Request, buffer: []u8) !usize { + switch (req.protocol) { + .http => return req.stream.read(buffer), + .https => return req.tls_client.read(req.stream, buffer), + } + } + + /// Only abstracts over http/https. + fn readAtLeastRaw(req: *Request, buffer: []u8, len: usize) !usize { switch (req.protocol) { .http => return req.stream.readAtLeast(buffer, len), .https => return req.tls_client.readAtLeast(req.stream, buffer, len), -- 2.54.0