aboutsummaryrefslogtreecommitdiff
path: root/lib/std
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2024-03-02 14:34:43 -0800
committerGitHub <noreply@github.com>2024-03-02 14:34:43 -0800
commit671c2acf47d17551266f3760698181c83cd96090 (patch)
tree1ed92c6eccf5916e905ba956b137e3fa30c4987d /lib/std
parent9d500bda2d09fe67c39ee98067c1e53c58adbd5e (diff)
parente62b0773cca027e720db1b18ce3905b7fe24ca96 (diff)
downloadzig-671c2acf47d17551266f3760698181c83cd96090.tar.gz
zig-671c2acf47d17551266f3760698181c83cd96090.zip
Merge pull request #19094 from truemedian/std-http-fields
std.http: fix http field parsing
Diffstat (limited to 'lib/std')
-rw-r--r--lib/std/http/Client.zig102
-rw-r--r--lib/std/http/HeaderIterator.zig55
-rw-r--r--lib/std/http/Server.zig88
-rw-r--r--lib/std/http/test.zig4
4 files changed, 230 insertions, 19 deletions
diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig
index 7eade117d0..10088e7627 100644
--- a/lib/std/http/Client.zig
+++ b/lib/std/http/Client.zig
@@ -489,9 +489,9 @@ pub const Response = struct {
else => {},
}
- var line_it = mem.splitSequence(u8, line, ": ");
+ var line_it = mem.splitScalar(u8, line, ':');
const header_name = line_it.next().?;
- const header_value = line_it.rest();
+ const header_value = mem.trim(u8, line_it.rest(), " \t");
if (header_name.len == 0) return error.HttpHeadersInvalid;
if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
@@ -551,6 +551,43 @@ pub const Response = struct {
return error.HttpHeadersInvalid; // missing empty line
}
+ test parse {
+ const response_bytes = "HTTP/1.1 200 OK\r\n" ++
+ "LOcation:url\r\n" ++
+ "content-tYpe: text/plain\r\n" ++
+ "content-disposition:attachment; filename=example.txt \r\n" ++
+ "content-Length:10\r\n" ++
+ "TRansfer-encoding:\tdeflate, chunked \r\n" ++
+ "connectioN:\t keep-alive \r\n\r\n";
+
+ var header_buffer: [1024]u8 = undefined;
+ var res = Response{
+ .status = undefined,
+ .reason = undefined,
+ .version = undefined,
+ .keep_alive = false,
+ .parser = proto.HeadersParser.init(&header_buffer),
+ };
+
+ @memcpy(header_buffer[0..response_bytes.len], response_bytes);
+ res.parser.header_bytes_len = response_bytes.len;
+
+ try res.parse(response_bytes);
+
+ try testing.expectEqual(.@"HTTP/1.1", res.version);
+ try testing.expectEqualStrings("OK", res.reason);
+ try testing.expectEqual(.ok, res.status);
+
+ try testing.expectEqualStrings("url", res.location.?);
+ try testing.expectEqualStrings("text/plain", res.content_type.?);
+ try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?);
+
+ try testing.expectEqual(true, res.keep_alive);
+ try testing.expectEqual(10, res.content_length.?);
+ try testing.expectEqual(.chunked, res.transfer_encoding);
+ try testing.expectEqual(.deflate, res.transfer_compression);
+ }
+
inline fn int64(array: *const [8]u8) u64 {
return @bitCast(array.*);
}
@@ -575,6 +612,67 @@ pub const Response = struct {
pub fn iterateHeaders(r: Response) http.HeaderIterator {
return http.HeaderIterator.init(r.parser.get());
}
+
+ test iterateHeaders {
+ const response_bytes = "HTTP/1.1 200 OK\r\n" ++
+ "LOcation:url\r\n" ++
+ "content-tYpe: text/plain\r\n" ++
+ "content-disposition:attachment; filename=example.txt \r\n" ++
+ "content-Length:10\r\n" ++
+ "TRansfer-encoding:\tdeflate, chunked \r\n" ++
+ "connectioN:\t keep-alive \r\n\r\n";
+
+ var header_buffer: [1024]u8 = undefined;
+ var res = Response{
+ .status = undefined,
+ .reason = undefined,
+ .version = undefined,
+ .keep_alive = false,
+ .parser = proto.HeadersParser.init(&header_buffer),
+ };
+
+ @memcpy(header_buffer[0..response_bytes.len], response_bytes);
+ res.parser.header_bytes_len = response_bytes.len;
+
+ var it = res.iterateHeaders();
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("LOcation", header.name);
+ try testing.expectEqualStrings("url", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("content-tYpe", header.name);
+ try testing.expectEqualStrings("text/plain", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("content-disposition", header.name);
+ try testing.expectEqualStrings("attachment; filename=example.txt", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("content-Length", header.name);
+ try testing.expectEqualStrings("10", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("TRansfer-encoding", header.name);
+ try testing.expectEqualStrings("deflate, chunked", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("connectioN", header.name);
+ try testing.expectEqualStrings("keep-alive", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ try testing.expectEqual(null, it.next());
+ }
};
/// A HTTP request that has been sent.
diff --git a/lib/std/http/HeaderIterator.zig b/lib/std/http/HeaderIterator.zig
index 515058859d..518950a9ab 100644
--- a/lib/std/http/HeaderIterator.zig
+++ b/lib/std/http/HeaderIterator.zig
@@ -12,30 +12,43 @@ pub fn init(bytes: []const u8) HeaderIterator {
pub fn next(it: *HeaderIterator) ?std.http.Header {
const end = std.mem.indexOfPosLinear(u8, it.bytes, it.index, "\r\n").?;
- var kv_it = std.mem.splitSequence(u8, it.bytes[it.index..end], ": ");
- const name = kv_it.next().?;
- const value = kv_it.rest();
- if (name.len == 0 and value.len == 0) {
+ if (it.index == end) { // found the trailer boundary (\r\n\r\n)
if (it.is_trailer) return null;
+
const next_end = std.mem.indexOfPosLinear(u8, it.bytes, end + 2, "\r\n") orelse
return null;
+
+ var kv_it = std.mem.splitScalar(u8, it.bytes[end + 2 .. next_end], ':');
+ const name = kv_it.first();
+ const value = kv_it.rest();
+
it.is_trailer = true;
it.index = next_end + 2;
- kv_it = std.mem.splitSequence(u8, it.bytes[end + 2 .. next_end], ": ");
+ if (name.len == 0)
+ return null;
+
+ return .{
+ .name = name,
+ .value = std.mem.trim(u8, value, " \t"),
+ };
+ } else { // normal header
+ var kv_it = std.mem.splitScalar(u8, it.bytes[it.index..end], ':');
+ const name = kv_it.first();
+ const value = kv_it.rest();
+
+ it.index = end + 2;
+ if (name.len == 0)
+ return null;
+
return .{
- .name = kv_it.next().?,
- .value = kv_it.rest(),
+ .name = name,
+ .value = std.mem.trim(u8, value, " \t"),
};
}
- it.index = end + 2;
- return .{
- .name = name,
- .value = value,
- };
}
test next {
- var it = HeaderIterator.init("200 OK\r\na: b\r\nc: \r\nd: e\r\n\r\nf: g\r\n\r\n");
+ var it = HeaderIterator.init("200 OK\r\na: b\r\nc: \r\nd:e\r\n\r\nf: g\r\n\r\n");
try std.testing.expect(!it.is_trailer);
{
const header = it.next().?;
@@ -62,7 +75,23 @@ test next {
try std.testing.expectEqualStrings("g", header.value);
}
try std.testing.expectEqual(null, it.next());
+
+ it = HeaderIterator.init("200 OK\r\n: ss\r\n\r\n");
+ try std.testing.expect(!it.is_trailer);
+ try std.testing.expectEqual(null, it.next());
+
+ it = HeaderIterator.init("200 OK\r\na:b\r\n\r\n: ss\r\n\r\n");
+ try std.testing.expect(!it.is_trailer);
+ {
+ const header = it.next().?;
+ try std.testing.expect(!it.is_trailer);
+ try std.testing.expectEqualStrings("a", header.name);
+ try std.testing.expectEqualStrings("b", header.value);
+ }
+ try std.testing.expectEqual(null, it.next());
+ try std.testing.expect(it.is_trailer);
}
const HeaderIterator = @This();
const std = @import("../std.zig");
+const assert = std.debug.assert;
diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig
index bfc97157f6..5290241b6e 100644
--- a/lib/std/http/Server.zig
+++ b/lib/std/http/Server.zig
@@ -211,9 +211,9 @@ pub const Request = struct {
else => {},
}
- var line_it = mem.splitSequence(u8, line, ": ");
+ var line_it = mem.splitScalar(u8, line, ':');
const header_name = line_it.next().?;
- const header_value = line_it.rest();
+ const header_value = mem.trim(u8, line_it.rest(), " \t");
if (header_name.len == 0) return error.HttpHeadersInvalid;
if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
@@ -271,6 +271,29 @@ pub const Request = struct {
return error.MissingFinalNewline;
}
+ test parse {
+ const request_bytes = "GET /hi HTTP/1.0\r\n" ++
+ "content-tYpe: text/plain\r\n" ++
+ "content-Length:10\r\n" ++
+ "expeCt: 100-continue \r\n" ++
+ "TRansfer-encoding:\tdeflate, chunked \r\n" ++
+ "connectioN:\t keep-alive \r\n\r\n";
+
+ const req = try parse(request_bytes);
+
+ try testing.expectEqual(.GET, req.method);
+ try testing.expectEqual(.@"HTTP/1.0", req.version);
+ try testing.expectEqualStrings("/hi", req.target);
+
+ try testing.expectEqualStrings("text/plain", req.content_type.?);
+ try testing.expectEqualStrings("100-continue", req.expect.?);
+
+ try testing.expectEqual(true, req.keep_alive);
+ try testing.expectEqual(10, req.content_length.?);
+ try testing.expectEqual(.chunked, req.transfer_encoding);
+ try testing.expectEqual(.deflate, req.transfer_compression);
+ }
+
inline fn int64(array: *const [8]u8) u64 {
return @bitCast(array.*);
}
@@ -280,6 +303,66 @@ pub const Request = struct {
return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]);
}
+ test iterateHeaders {
+ const request_bytes = "GET /hi HTTP/1.0\r\n" ++
+ "content-tYpe: text/plain\r\n" ++
+ "content-Length:10\r\n" ++
+ "expeCt: 100-continue \r\n" ++
+ "TRansfer-encoding:\tdeflate, chunked \r\n" ++
+ "connectioN:\t keep-alive \r\n\r\n";
+
+ var read_buffer: [500]u8 = undefined;
+ @memcpy(read_buffer[0..request_bytes.len], request_bytes);
+
+ var server: Server = .{
+ .connection = undefined,
+ .state = .ready,
+ .read_buffer = &read_buffer,
+ .read_buffer_len = request_bytes.len,
+ .next_request_start = 0,
+ };
+
+ var request: Request = .{
+ .server = &server,
+ .head_end = request_bytes.len,
+ .head = undefined,
+ .reader_state = undefined,
+ };
+
+ var it = request.iterateHeaders();
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("content-tYpe", header.name);
+ try testing.expectEqualStrings("text/plain", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("content-Length", header.name);
+ try testing.expectEqualStrings("10", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("expeCt", header.name);
+ try testing.expectEqualStrings("100-continue", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("TRansfer-encoding", header.name);
+ try testing.expectEqualStrings("deflate, chunked", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ {
+ const header = it.next().?;
+ try testing.expectEqualStrings("connectioN", header.name);
+ try testing.expectEqualStrings("keep-alive", header.value);
+ try testing.expect(!it.is_trailer);
+ }
+ try testing.expectEqual(null, it.next());
+ }
+
pub const RespondOptions = struct {
version: http.Version = .@"HTTP/1.1",
status: http.Status = .ok,
@@ -1060,5 +1143,6 @@ const mem = std.mem;
const net = std.net;
const Uri = std.Uri;
const assert = std.debug.assert;
+const testing = std.testing;
const Server = @This();
diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig
index 15e7542d19..e2aa810d58 100644
--- a/lib/std/http/test.zig
+++ b/lib/std/http/test.zig
@@ -328,8 +328,8 @@ test "receiving arbitrary http headers from the client" {
defer test_server.destroy();
const request_bytes = "GET /bar HTTP/1.1\r\n" ++
- "CoNneCtIoN: close\r\n" ++
- "aoeu: asdf\r\n" ++
+ "CoNneCtIoN:close\r\n" ++
+ "aoeu: asdf \r\n" ++
"\r\n";
const gpa = std.testing.allocator;
const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());