authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-03-02 14:34:43-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-03-02 14:34:43-08:00
log671c2acf47d17551266f3760698181c83cd96090
tree1ed92c6eccf5916e905ba956b137e3fa30c4987d
parent9d500bda2d09fe67c39ee98067c1e53c58adbd5e
parente62b0773cca027e720db1b18ce3905b7fe24ca96
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #19094 from truemedian/std-http-fields

std.http: fix http field parsing

4 files changed, 230 insertions(+), 19 deletions(-)

lib/std/http/Client.zig+100-2
...@@ -489,9 +489,9 @@ pub const Response = struct {...@@ -489,9 +489,9 @@ pub const Response = struct {
489 else => {},489 else => {},
490 }490 }
491491
492 var line_it = mem.splitSequence(u8, line, ": ");492 var line_it = mem.splitScalar(u8, line, ':');
493 const header_name = line_it.next().?;493 const header_name = line_it.next().?;
494 const header_value = line_it.rest();494 const header_value = mem.trim(u8, line_it.rest(), " \t");
495 if (header_name.len == 0) return error.HttpHeadersInvalid;495 if (header_name.len == 0) return error.HttpHeadersInvalid;
496496
497 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {497 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
...@@ -551,6 +551,43 @@ pub const Response = struct {...@@ -551,6 +551,43 @@ pub const Response = struct {
551 return error.HttpHeadersInvalid; // missing empty line551 return error.HttpHeadersInvalid; // missing empty line
552 }552 }
553553
554 test parse {
555 const response_bytes = "HTTP/1.1 200 OK\r\n" ++
556 "LOcation:url\r\n" ++
557 "content-tYpe: text/plain\r\n" ++
558 "content-disposition:attachment; filename=example.txt \r\n" ++
559 "content-Length:10\r\n" ++
560 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
561 "connectioN:\t keep-alive \r\n\r\n";
562
563 var header_buffer: [1024]u8 = undefined;
564 var res = Response{
565 .status = undefined,
566 .reason = undefined,
567 .version = undefined,
568 .keep_alive = false,
569 .parser = proto.HeadersParser.init(&header_buffer),
570 };
571
572 @memcpy(header_buffer[0..response_bytes.len], response_bytes);
573 res.parser.header_bytes_len = response_bytes.len;
574
575 try res.parse(response_bytes);
576
577 try testing.expectEqual(.@"HTTP/1.1", res.version);
578 try testing.expectEqualStrings("OK", res.reason);
579 try testing.expectEqual(.ok, res.status);
580
581 try testing.expectEqualStrings("url", res.location.?);
582 try testing.expectEqualStrings("text/plain", res.content_type.?);
583 try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?);
584
585 try testing.expectEqual(true, res.keep_alive);
586 try testing.expectEqual(10, res.content_length.?);
587 try testing.expectEqual(.chunked, res.transfer_encoding);
588 try testing.expectEqual(.deflate, res.transfer_compression);
589 }
590
554 inline fn int64(array: *const [8]u8) u64 {591 inline fn int64(array: *const [8]u8) u64 {
555 return @bitCast(array.*);592 return @bitCast(array.*);
556 }593 }
...@@ -575,6 +612,67 @@ pub const Response = struct {...@@ -575,6 +612,67 @@ pub const Response = struct {
575 pub fn iterateHeaders(r: Response) http.HeaderIterator {612 pub fn iterateHeaders(r: Response) http.HeaderIterator {
576 return http.HeaderIterator.init(r.parser.get());613 return http.HeaderIterator.init(r.parser.get());
577 }614 }
615
616 test iterateHeaders {
617 const response_bytes = "HTTP/1.1 200 OK\r\n" ++
618 "LOcation:url\r\n" ++
619 "content-tYpe: text/plain\r\n" ++
620 "content-disposition:attachment; filename=example.txt \r\n" ++
621 "content-Length:10\r\n" ++
622 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
623 "connectioN:\t keep-alive \r\n\r\n";
624
625 var header_buffer: [1024]u8 = undefined;
626 var res = Response{
627 .status = undefined,
628 .reason = undefined,
629 .version = undefined,
630 .keep_alive = false,
631 .parser = proto.HeadersParser.init(&header_buffer),
632 };
633
634 @memcpy(header_buffer[0..response_bytes.len], response_bytes);
635 res.parser.header_bytes_len = response_bytes.len;
636
637 var it = res.iterateHeaders();
638 {
639 const header = it.next().?;
640 try testing.expectEqualStrings("LOcation", header.name);
641 try testing.expectEqualStrings("url", header.value);
642 try testing.expect(!it.is_trailer);
643 }
644 {
645 const header = it.next().?;
646 try testing.expectEqualStrings("content-tYpe", header.name);
647 try testing.expectEqualStrings("text/plain", header.value);
648 try testing.expect(!it.is_trailer);
649 }
650 {
651 const header = it.next().?;
652 try testing.expectEqualStrings("content-disposition", header.name);
653 try testing.expectEqualStrings("attachment; filename=example.txt", header.value);
654 try testing.expect(!it.is_trailer);
655 }
656 {
657 const header = it.next().?;
658 try testing.expectEqualStrings("content-Length", header.name);
659 try testing.expectEqualStrings("10", header.value);
660 try testing.expect(!it.is_trailer);
661 }
662 {
663 const header = it.next().?;
664 try testing.expectEqualStrings("TRansfer-encoding", header.name);
665 try testing.expectEqualStrings("deflate, chunked", header.value);
666 try testing.expect(!it.is_trailer);
667 }
668 {
669 const header = it.next().?;
670 try testing.expectEqualStrings("connectioN", header.name);
671 try testing.expectEqualStrings("keep-alive", header.value);
672 try testing.expect(!it.is_trailer);
673 }
674 try testing.expectEqual(null, it.next());
675 }
578};676};
579677
580/// A HTTP request that has been sent.678/// A HTTP request that has been sent.
lib/std/http/HeaderIterator.zig+42-13
...@@ -12,30 +12,43 @@ pub fn init(bytes: []const u8) HeaderIterator {...@@ -12,30 +12,43 @@ pub fn init(bytes: []const u8) HeaderIterator {
1212
13pub fn next(it: *HeaderIterator) ?std.http.Header {13pub fn next(it: *HeaderIterator) ?std.http.Header {
14 const end = std.mem.indexOfPosLinear(u8, it.bytes, it.index, "\r\n").?;14 const end = std.mem.indexOfPosLinear(u8, it.bytes, it.index, "\r\n").?;
15 var kv_it = std.mem.splitSequence(u8, it.bytes[it.index..end], ": ");15 if (it.index == end) { // found the trailer boundary (\r\n\r\n)
16 const name = kv_it.next().?;
17 const value = kv_it.rest();
18 if (name.len == 0 and value.len == 0) {
19 if (it.is_trailer) return null;16 if (it.is_trailer) return null;
17
20 const next_end = std.mem.indexOfPosLinear(u8, it.bytes, end + 2, "\r\n") orelse18 const next_end = std.mem.indexOfPosLinear(u8, it.bytes, end + 2, "\r\n") orelse
21 return null;19 return null;
20
21 var kv_it = std.mem.splitScalar(u8, it.bytes[end + 2 .. next_end], ':');
22 const name = kv_it.first();
23 const value = kv_it.rest();
24
22 it.is_trailer = true;25 it.is_trailer = true;
23 it.index = next_end + 2;26 it.index = next_end + 2;
24 kv_it = std.mem.splitSequence(u8, it.bytes[end + 2 .. next_end], ": ");27 if (name.len == 0)
28 return null;
29
30 return .{
31 .name = name,
32 .value = std.mem.trim(u8, value, " \t"),
33 };
34 } else { // normal header
35 var kv_it = std.mem.splitScalar(u8, it.bytes[it.index..end], ':');
36 const name = kv_it.first();
37 const value = kv_it.rest();
38
39 it.index = end + 2;
40 if (name.len == 0)
41 return null;
42
25 return .{43 return .{
26 .name = kv_it.next().?,44 .name = name,
27 .value = kv_it.rest(),45 .value = std.mem.trim(u8, value, " \t"),
28 };46 };
29 }47 }
30 it.index = end + 2;
31 return .{
32 .name = name,
33 .value = value,
34 };
35}48}
3649
37test next {50test next {
38 var it = HeaderIterator.init("200 OK\r\na: b\r\nc: \r\nd: e\r\n\r\nf: g\r\n\r\n");51 var it = HeaderIterator.init("200 OK\r\na: b\r\nc: \r\nd:e\r\n\r\nf: g\r\n\r\n");
39 try std.testing.expect(!it.is_trailer);52 try std.testing.expect(!it.is_trailer);
40 {53 {
41 const header = it.next().?;54 const header = it.next().?;
...@@ -62,7 +75,23 @@ test next {...@@ -62,7 +75,23 @@ test next {
62 try std.testing.expectEqualStrings("g", header.value);75 try std.testing.expectEqualStrings("g", header.value);
63 }76 }
64 try std.testing.expectEqual(null, it.next());77 try std.testing.expectEqual(null, it.next());
78
79 it = HeaderIterator.init("200 OK\r\n: ss\r\n\r\n");
80 try std.testing.expect(!it.is_trailer);
81 try std.testing.expectEqual(null, it.next());
82
83 it = HeaderIterator.init("200 OK\r\na:b\r\n\r\n: ss\r\n\r\n");
84 try std.testing.expect(!it.is_trailer);
85 {
86 const header = it.next().?;
87 try std.testing.expect(!it.is_trailer);
88 try std.testing.expectEqualStrings("a", header.name);
89 try std.testing.expectEqualStrings("b", header.value);
90 }
91 try std.testing.expectEqual(null, it.next());
92 try std.testing.expect(it.is_trailer);
65}93}
6694
67const HeaderIterator = @This();95const HeaderIterator = @This();
68const std = @import("../std.zig");96const std = @import("../std.zig");
97const assert = std.debug.assert;
lib/std/http/Server.zig+86-2
...@@ -211,9 +211,9 @@ pub const Request = struct {...@@ -211,9 +211,9 @@ pub const Request = struct {
211 else => {},211 else => {},
212 }212 }
213213
214 var line_it = mem.splitSequence(u8, line, ": ");214 var line_it = mem.splitScalar(u8, line, ':');
215 const header_name = line_it.next().?;215 const header_name = line_it.next().?;
216 const header_value = line_it.rest();216 const header_value = mem.trim(u8, line_it.rest(), " \t");
217 if (header_name.len == 0) return error.HttpHeadersInvalid;217 if (header_name.len == 0) return error.HttpHeadersInvalid;
218218
219 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {219 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
...@@ -271,6 +271,29 @@ pub const Request = struct {...@@ -271,6 +271,29 @@ pub const Request = struct {
271 return error.MissingFinalNewline;271 return error.MissingFinalNewline;
272 }272 }
273273
274 test parse {
275 const request_bytes = "GET /hi HTTP/1.0\r\n" ++
276 "content-tYpe: text/plain\r\n" ++
277 "content-Length:10\r\n" ++
278 "expeCt: 100-continue \r\n" ++
279 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
280 "connectioN:\t keep-alive \r\n\r\n";
281
282 const req = try parse(request_bytes);
283
284 try testing.expectEqual(.GET, req.method);
285 try testing.expectEqual(.@"HTTP/1.0", req.version);
286 try testing.expectEqualStrings("/hi", req.target);
287
288 try testing.expectEqualStrings("text/plain", req.content_type.?);
289 try testing.expectEqualStrings("100-continue", req.expect.?);
290
291 try testing.expectEqual(true, req.keep_alive);
292 try testing.expectEqual(10, req.content_length.?);
293 try testing.expectEqual(.chunked, req.transfer_encoding);
294 try testing.expectEqual(.deflate, req.transfer_compression);
295 }
296
274 inline fn int64(array: *const [8]u8) u64 {297 inline fn int64(array: *const [8]u8) u64 {
275 return @bitCast(array.*);298 return @bitCast(array.*);
276 }299 }
...@@ -280,6 +303,66 @@ pub const Request = struct {...@@ -280,6 +303,66 @@ pub const Request = struct {
280 return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]);303 return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]);
281 }304 }
282305
306 test iterateHeaders {
307 const request_bytes = "GET /hi HTTP/1.0\r\n" ++
308 "content-tYpe: text/plain\r\n" ++
309 "content-Length:10\r\n" ++
310 "expeCt: 100-continue \r\n" ++
311 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
312 "connectioN:\t keep-alive \r\n\r\n";
313
314 var read_buffer: [500]u8 = undefined;
315 @memcpy(read_buffer[0..request_bytes.len], request_bytes);
316
317 var server: Server = .{
318 .connection = undefined,
319 .state = .ready,
320 .read_buffer = &read_buffer,
321 .read_buffer_len = request_bytes.len,
322 .next_request_start = 0,
323 };
324
325 var request: Request = .{
326 .server = &server,
327 .head_end = request_bytes.len,
328 .head = undefined,
329 .reader_state = undefined,
330 };
331
332 var it = request.iterateHeaders();
333 {
334 const header = it.next().?;
335 try testing.expectEqualStrings("content-tYpe", header.name);
336 try testing.expectEqualStrings("text/plain", header.value);
337 try testing.expect(!it.is_trailer);
338 }
339 {
340 const header = it.next().?;
341 try testing.expectEqualStrings("content-Length", header.name);
342 try testing.expectEqualStrings("10", header.value);
343 try testing.expect(!it.is_trailer);
344 }
345 {
346 const header = it.next().?;
347 try testing.expectEqualStrings("expeCt", header.name);
348 try testing.expectEqualStrings("100-continue", header.value);
349 try testing.expect(!it.is_trailer);
350 }
351 {
352 const header = it.next().?;
353 try testing.expectEqualStrings("TRansfer-encoding", header.name);
354 try testing.expectEqualStrings("deflate, chunked", header.value);
355 try testing.expect(!it.is_trailer);
356 }
357 {
358 const header = it.next().?;
359 try testing.expectEqualStrings("connectioN", header.name);
360 try testing.expectEqualStrings("keep-alive", header.value);
361 try testing.expect(!it.is_trailer);
362 }
363 try testing.expectEqual(null, it.next());
364 }
365
283 pub const RespondOptions = struct {366 pub const RespondOptions = struct {
284 version: http.Version = .@"HTTP/1.1",367 version: http.Version = .@"HTTP/1.1",
285 status: http.Status = .ok,368 status: http.Status = .ok,
...@@ -1060,5 +1143,6 @@ const mem = std.mem;...@@ -1060,5 +1143,6 @@ const mem = std.mem;
1060const net = std.net;1143const net = std.net;
1061const Uri = std.Uri;1144const Uri = std.Uri;
1062const assert = std.debug.assert;1145const assert = std.debug.assert;
1146const testing = std.testing;
10631147
1064const Server = @This();1148const Server = @This();
lib/std/http/test.zig+2-2
...@@ -328,8 +328,8 @@ test "receiving arbitrary http headers from the client" {...@@ -328,8 +328,8 @@ test "receiving arbitrary http headers from the client" {
328 defer test_server.destroy();328 defer test_server.destroy();
329329
330 const request_bytes = "GET /bar HTTP/1.1\r\n" ++330 const request_bytes = "GET /bar HTTP/1.1\r\n" ++
331 "CoNneCtIoN: close\r\n" ++331 "CoNneCtIoN:close\r\n" ++
332 "aoeu: asdf\r\n" ++332 "aoeu: asdf \r\n" ++
333 "\r\n";333 "\r\n";
334 const gpa = std.testing.allocator;334 const gpa = std.testing.allocator;
335 const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());335 const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());