| author | |
| committer | |
| log | 671c2acf47d17551266f3760698181c83cd96090 |
| tree | 1ed92c6eccf5916e905ba956b137e3fa30c4987d |
| parent | 9d500bda2d09fe67c39ee98067c1e53c58adbd5e |
| parent | e62b0773cca027e720db1b18ce3905b7fe24ca96 |
| signature |
std.http: fix http field parsing4 files changed, 230 insertions(+), 19 deletions(-)
lib/std/http/Client.zig+100-2| ... | ... | @@ -489,9 +489,9 @@ pub const Response = struct { |
| 489 | 489 | else => {}, |
| 490 | 490 | } |
| 491 | 491 | |
| 492 | var line_it = mem.splitSequence(u8, line, ": "); | |
| 492 | var line_it = mem.splitScalar(u8, line, ':'); | |
| 493 | 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 | 495 | if (header_name.len == 0) return error.HttpHeadersInvalid; |
| 496 | 496 | |
| 497 | 497 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| ... | ... | @@ -551,6 +551,43 @@ pub const Response = struct { |
| 551 | 551 | return error.HttpHeadersInvalid; // missing empty line |
| 552 | 552 | } |
| 553 | 553 | |
| 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 | 591 | inline fn int64(array: *const [8]u8) u64 { |
| 555 | 592 | return @bitCast(array.*); |
| 556 | 593 | } |
| ... | ... | @@ -575,6 +612,67 @@ pub const Response = struct { |
| 575 | 612 | pub fn iterateHeaders(r: Response) http.HeaderIterator { |
| 576 | 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 | }; |
| 579 | 677 | |
| 580 | 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 | 12 | |
| 13 | 13 | pub fn next(it: *HeaderIterator) ?std.http.Header { |
| 14 | 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], ": "); | |
| 16 | const name = kv_it.next().?; | |
| 17 | const value = kv_it.rest(); | |
| 18 | if (name.len == 0 and value.len == 0) { | |
| 15 | if (it.index == end) { // found the trailer boundary (\r\n\r\n) | |
| 19 | 16 | if (it.is_trailer) return null; |
| 17 | ||
| 20 | 18 | const next_end = std.mem.indexOfPosLinear(u8, it.bytes, end + 2, "\r\n") orelse |
| 21 | 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 | 25 | it.is_trailer = true; |
| 23 | 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 | 43 | return .{ |
| 26 | .name = kv_it.next().?, | |
| 27 | .value = kv_it.rest(), | |
| 44 | .name = name, | |
| 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 | } |
| 36 | 49 | |
| 37 | 50 | test 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 | 52 | try std.testing.expect(!it.is_trailer); |
| 40 | 53 | { |
| 41 | 54 | const header = it.next().?; |
| ... | ... | @@ -62,7 +75,23 @@ test next { |
| 62 | 75 | try std.testing.expectEqualStrings("g", header.value); |
| 63 | 76 | } |
| 64 | 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 | } |
| 66 | 94 | |
| 67 | 95 | const HeaderIterator = @This(); |
| 68 | 96 | const std = @import("../std.zig"); |
| 97 | const assert = std.debug.assert; |
lib/std/http/Server.zig+86-2| ... | ... | @@ -211,9 +211,9 @@ pub const Request = struct { |
| 211 | 211 | else => {}, |
| 212 | 212 | } |
| 213 | 213 | |
| 214 | var line_it = mem.splitSequence(u8, line, ": "); | |
| 214 | var line_it = mem.splitScalar(u8, line, ':'); | |
| 215 | 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 | 217 | if (header_name.len == 0) return error.HttpHeadersInvalid; |
| 218 | 218 | |
| 219 | 219 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| ... | ... | @@ -271,6 +271,29 @@ pub const Request = struct { |
| 271 | 271 | return error.MissingFinalNewline; |
| 272 | 272 | } |
| 273 | 273 | |
| 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 | 297 | inline fn int64(array: *const [8]u8) u64 { |
| 275 | 298 | return @bitCast(array.*); |
| 276 | 299 | } |
| ... | ... | @@ -280,6 +303,66 @@ pub const Request = struct { |
| 280 | 303 | return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]); |
| 281 | 304 | } |
| 282 | 305 | |
| 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 | 366 | pub const RespondOptions = struct { |
| 284 | 367 | version: http.Version = .@"HTTP/1.1", |
| 285 | 368 | status: http.Status = .ok, |
| ... | ... | @@ -1060,5 +1143,6 @@ const mem = std.mem; |
| 1060 | 1143 | const net = std.net; |
| 1061 | 1144 | const Uri = std.Uri; |
| 1062 | 1145 | const assert = std.debug.assert; |
| 1146 | const testing = std.testing; | |
| 1063 | 1147 | |
| 1064 | 1148 | const Server = @This(); |
lib/std/http/test.zig+2-2| ... | ... | @@ -328,8 +328,8 @@ test "receiving arbitrary http headers from the client" { |
| 328 | 328 | defer test_server.destroy(); |
| 329 | 329 | |
| 330 | 330 | const request_bytes = "GET /bar HTTP/1.1\r\n" ++ |
| 331 | "CoNneCtIoN: close\r\n" ++ | |
| 332 | "aoeu: asdf\r\n" ++ | |
| 331 | "CoNneCtIoN:close\r\n" ++ | |
| 332 | "aoeu: asdf \r\n" ++ | |
| 333 | 333 | "\r\n"; |
| 334 | 334 | const gpa = std.testing.allocator; |
| 335 | 335 | const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); |