| 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,9 +489,9 @@ pub const Response = struct { |
| 489 | else => {}, | 489 | else => {}, |
| 490 | } | 490 | } |
| 491 | 491 | ||
| 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; |
| 496 | 496 | ||
| 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 line | 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 | 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 | }; |
| 579 | 677 | ||
| 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 { |
| 12 | 12 | ||
| 13 | pub fn next(it: *HeaderIterator) ?std.http.Header { | 13 | pub 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") orelse | 18 | 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 | } |
| 36 | 49 | ||
| 37 | test next { | 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 | 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 | } |
| 66 | 94 | ||
| 67 | const HeaderIterator = @This(); | 95 | const HeaderIterator = @This(); |
| 68 | const std = @import("../std.zig"); | 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,9 +211,9 @@ pub const Request = struct { |
| 211 | else => {}, | 211 | else => {}, |
| 212 | } | 212 | } |
| 213 | 213 | ||
| 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; |
| 218 | 218 | ||
| 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 | } |
| 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 | 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 | } |
| 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 | 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; |
| 1060 | const net = std.net; | 1143 | const net = std.net; |
| 1061 | const Uri = std.Uri; | 1144 | const Uri = std.Uri; |
| 1062 | const assert = std.debug.assert; | 1145 | const assert = std.debug.assert; |
| 1146 | const testing = std.testing; | ||
| 1063 | 1147 | ||
| 1064 | const Server = @This(); | 1148 | const 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(); |
| 329 | 329 | ||
| 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()); |