| author | |
| committer | |
| log | aa090a49d94155c4804644377db110f3b13f0500 |
| tree | 06226127b3e279414c6a8f1ca2a12f7c6b75f308 |
| parent | 5d40338f21b468c82d4bc2a1ac0a35c643126e74 |
| signature |
4 files changed, 156 insertions(+), 42 deletions(-)
lib/std/http/Client.zig+40-5| ... | ... | @@ -478,6 +478,7 @@ pub const Request = struct { |
| 478 | 478 | .zstd => |*zstd| zstd.deinit(), |
| 479 | 479 | } |
| 480 | 480 | |
| 481 | req.headers.deinit(); | |
| 481 | 482 | req.response.headers.deinit(); |
| 482 | 483 | |
| 483 | 484 | if (req.response.parser.header_bytes_owned) { |
| ... | ... | @@ -667,17 +668,19 @@ pub const Request = struct { |
| 667 | 668 | |
| 668 | 669 | try req.response.parse(req.response.parser.header_bytes.items, false); |
| 669 | 670 | |
| 670 | if (req.response.status == .switching_protocols) { | |
| 671 | req.connection.?.data.closing = false; | |
| 672 | req.response.parser.done = true; | |
| 671 | if (req.response.status == .@"continue") { | |
| 672 | req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response | |
| 673 | req.response.parser.reset(); | |
| 674 | break; | |
| 673 | 675 | } |
| 674 | 676 | |
| 675 | if (req.method == .CONNECT and req.response.status == .ok) { | |
| 677 | // we're switching protocols, so this connection is no longer doing http | |
| 678 | if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) { | |
| 676 | 679 | req.connection.?.data.closing = false; |
| 677 | 680 | req.response.parser.done = true; |
| 678 | 681 | } |
| 679 | 682 | |
| 680 | // we default to using keep-alive if not provided | |
| 683 | // we default to using keep-alive if not provided in the client if the server asks for it | |
| 681 | 684 | const req_connection = req.headers.getFirstValue("connection"); |
| 682 | 685 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); |
| 683 | 686 | |
| ... | ... | @@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: |
| 955 | 958 | return conn; |
| 956 | 959 | } |
| 957 | 960 | |
| 961 | pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{NameTooLong} || std.os.ConnectError; | |
| 962 | ||
| 963 | pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node { | |
| 964 | if (client.connection_pool.findConnection(.{ | |
| 965 | .host = path, | |
| 966 | .port = 0, | |
| 967 | .is_tls = false, | |
| 968 | })) |node| | |
| 969 | return node; | |
| 970 | ||
| 971 | const conn = try client.allocator.create(ConnectionPool.Node); | |
| 972 | errdefer client.allocator.destroy(conn); | |
| 973 | conn.* = .{ .data = undefined }; | |
| 974 | ||
| 975 | const stream = try std.net.connectUnixSocket(path); | |
| 976 | errdefer stream.close(); | |
| 977 | ||
| 978 | conn.data = .{ | |
| 979 | .stream = stream, | |
| 980 | .tls_client = undefined, | |
| 981 | .protocol = .plain, | |
| 982 | ||
| 983 | .host = try client.allocator.dupe(u8, path), | |
| 984 | .port = 0, | |
| 985 | }; | |
| 986 | errdefer client.allocator.free(conn.data.host); | |
| 987 | ||
| 988 | client.connection_pool.addUsed(conn); | |
| 989 | ||
| 990 | return conn; | |
| 991 | } | |
| 992 | ||
| 958 | 993 | // Prevents a dependency loop in request() |
| 959 | 994 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; |
| 960 | 995 | pub const ConnectError = ConnectErrorPartial || RequestError; |
lib/std/http/Server.zig+41-33| ... | ... | @@ -411,48 +411,52 @@ pub const Response = struct { |
| 411 | 411 | } |
| 412 | 412 | try w.writeAll("\r\n"); |
| 413 | 413 | |
| 414 | if (!res.headers.contains("server")) { | |
| 415 | try w.writeAll("Server: zig (std.http)\r\n"); | |
| 416 | } | |
| 414 | if (res.status == .@"continue") { | |
| 415 | res.state = .waited; // we still need to send another request after this | |
| 416 | } else { | |
| 417 | if (!res.headers.contains("server")) { | |
| 418 | try w.writeAll("Server: zig (std.http)\r\n"); | |
| 419 | } | |
| 417 | 420 | |
| 418 | if (!res.headers.contains("connection")) { | |
| 419 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 420 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 421 | if (!res.headers.contains("connection")) { | |
| 422 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 423 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 421 | 424 | |
| 422 | if (req_keepalive) { | |
| 423 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 424 | } else { | |
| 425 | try w.writeAll("Connection: close\r\n"); | |
| 425 | if (req_keepalive) { | |
| 426 | try w.writeAll("Connection: keep-alive\r\n"); | |
| 427 | } else { | |
| 428 | try w.writeAll("Connection: close\r\n"); | |
| 429 | } | |
| 426 | 430 | } |
| 427 | } | |
| 428 | 431 | |
| 429 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); | |
| 430 | const has_content_length = res.headers.contains("content-length"); | |
| 432 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); | |
| 433 | const has_content_length = res.headers.contains("content-length"); | |
| 431 | 434 | |
| 432 | if (!has_transfer_encoding and !has_content_length) { | |
| 433 | switch (res.transfer_encoding) { | |
| 434 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | |
| 435 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | |
| 436 | .none => {}, | |
| 437 | } | |
| 438 | } else { | |
| 439 | if (has_content_length) { | |
| 440 | const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; | |
| 441 | ||
| 442 | res.transfer_encoding = .{ .content_length = content_length }; | |
| 443 | } else if (has_transfer_encoding) { | |
| 444 | const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?; | |
| 445 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | |
| 446 | res.transfer_encoding = .chunked; | |
| 447 | } else { | |
| 448 | return error.UnsupportedTransferEncoding; | |
| 435 | if (!has_transfer_encoding and !has_content_length) { | |
| 436 | switch (res.transfer_encoding) { | |
| 437 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | |
| 438 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | |
| 439 | .none => {}, | |
| 449 | 440 | } |
| 450 | 441 | } else { |
| 451 | res.transfer_encoding = .none; | |
| 442 | if (has_content_length) { | |
| 443 | const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; | |
| 444 | ||
| 445 | res.transfer_encoding = .{ .content_length = content_length }; | |
| 446 | } else if (has_transfer_encoding) { | |
| 447 | const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?; | |
| 448 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | |
| 449 | res.transfer_encoding = .chunked; | |
| 450 | } else { | |
| 451 | return error.UnsupportedTransferEncoding; | |
| 452 | } | |
| 453 | } else { | |
| 454 | res.transfer_encoding = .none; | |
| 455 | } | |
| 452 | 456 | } |
| 453 | } | |
| 454 | 457 | |
| 455 | try w.print("{}", .{res.headers}); | |
| 458 | try w.print("{}", .{res.headers}); | |
| 459 | } | |
| 456 | 460 | |
| 457 | 461 | try w.writeAll("\r\n"); |
| 458 | 462 | |
| ... | ... | @@ -516,6 +520,10 @@ pub const Response = struct { |
| 516 | 520 | res.request.parser.done = true; |
| 517 | 521 | } |
| 518 | 522 | |
| 523 | if (res.request.method == .HEAD) { | |
| 524 | res.request.parser.done = true; | |
| 525 | } | |
| 526 | ||
| 519 | 527 | if (!res.request.parser.done) { |
| 520 | 528 | if (res.request.transfer_compression) |tc| switch (tc) { |
| 521 | 529 | .compress => return error.CompressionNotSupported, |
lib/std/http/protocol.zig+6-3| ... | ... | @@ -534,9 +534,9 @@ pub const HeadersParser = struct { |
| 534 | 534 | |
| 535 | 535 | if (r.next_chunk_length == 0) r.done = true; |
| 536 | 536 | |
| 537 | return 0; | |
| 538 | } else { | |
| 539 | const out_avail = buffer.len; | |
| 537 | return out_index; | |
| 538 | } else if (out_index < buffer.len) { | |
| 539 | const out_avail = buffer.len - out_index; | |
| 540 | 540 | |
| 541 | 541 | const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); |
| 542 | 542 | const nread = try conn.read(buffer[0..can_read]); |
| ... | ... | @@ -545,6 +545,8 @@ pub const HeadersParser = struct { |
| 545 | 545 | if (r.next_chunk_length == 0) r.done = true; |
| 546 | 546 | |
| 547 | 547 | return nread; |
| 548 | } else { | |
| 549 | return out_index; | |
| 548 | 550 | } |
| 549 | 551 | }, |
| 550 | 552 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { |
| ... | ... | @@ -558,6 +560,7 @@ pub const HeadersParser = struct { |
| 558 | 560 | .chunk_data => if (r.next_chunk_length == 0) { |
| 559 | 561 | if (std.mem.eql(u8, conn.peek(), "\r\n")) { |
| 560 | 562 | r.state = .finished; |
| 563 | r.done = true; | |
| 561 | 564 | } else { |
| 562 | 565 | // The trailer section is formatted identically to the header section. |
| 563 | 566 | r.state = .seen_rn; |
test/standalone/http.zig+69-1| ... | ... | @@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void { |
| 22 | 22 | |
| 23 | 23 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); |
| 24 | 24 | |
| 25 | if (res.request.headers.contains("expect")) { | |
| 26 | if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) { | |
| 27 | res.status = .@"continue"; | |
| 28 | try res.do(); | |
| 29 | res.status = .ok; | |
| 30 | } else { | |
| 31 | res.status = .expectation_failed; | |
| 32 | try res.do(); | |
| 33 | return; | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 25 | 37 | const body = try res.reader().readAllAlloc(salloc, 8192); |
| 26 | 38 | defer salloc.free(body); |
| 27 | 39 | |
| ... | ... | @@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void { |
| 62 | 74 | } |
| 63 | 75 | |
| 64 | 76 | try res.finish(); |
| 65 | } else if (mem.eql(u8, res.request.target, "/echo-content")) { | |
| 77 | } else if (mem.startsWith(u8, res.request.target, "/echo-content")) { | |
| 66 | 78 | try testing.expectEqualStrings("Hello, World!\n", body); |
| 67 | 79 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); |
| 68 | 80 | |
| ... | ... | @@ -592,6 +604,62 @@ pub fn main() !void { |
| 592 | 604 | try testing.expectEqualStrings("Hello, World!\n", res.body.?); |
| 593 | 605 | } |
| 594 | 606 | |
| 607 | { // expect: 100-continue | |
| 608 | var h = http.Headers{ .allocator = calloc }; | |
| 609 | defer h.deinit(); | |
| 610 | ||
| 611 | try h.append("expect", "100-continue"); | |
| 612 | try h.append("content-type", "text/plain"); | |
| 613 | ||
| 614 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port}); | |
| 615 | defer calloc.free(location); | |
| 616 | const uri = try std.Uri.parse(location); | |
| 617 | ||
| 618 | log.info("{s}", .{location}); | |
| 619 | var req = try client.request(.POST, uri, h, .{}); | |
| 620 | defer req.deinit(); | |
| 621 | ||
| 622 | req.transfer_encoding = .chunked; | |
| 623 | ||
| 624 | try req.start(); | |
| 625 | try req.wait(); | |
| 626 | try testing.expectEqual(http.Status.@"continue", req.response.status); | |
| 627 | ||
| 628 | try req.writeAll("Hello, "); | |
| 629 | try req.writeAll("World!\n"); | |
| 630 | try req.finish(); | |
| 631 | ||
| 632 | try req.wait(); | |
| 633 | try testing.expectEqual(http.Status.ok, req.response.status); | |
| 634 | ||
| 635 | const body = try req.reader().readAllAlloc(calloc, 8192); | |
| 636 | defer calloc.free(body); | |
| 637 | ||
| 638 | try testing.expectEqualStrings("Hello, World!\n", body); | |
| 639 | } | |
| 640 | ||
| 641 | { // expect: garbage | |
| 642 | var h = http.Headers{ .allocator = calloc }; | |
| 643 | defer h.deinit(); | |
| 644 | ||
| 645 | try h.append("content-type", "text/plain"); | |
| 646 | try h.append("expect", "garbage"); | |
| 647 | ||
| 648 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port}); | |
| 649 | defer calloc.free(location); | |
| 650 | const uri = try std.Uri.parse(location); | |
| 651 | ||
| 652 | log.info("{s}", .{location}); | |
| 653 | var req = try client.request(.POST, uri, h, .{}); | |
| 654 | defer req.deinit(); | |
| 655 | ||
| 656 | req.transfer_encoding = .chunked; | |
| 657 | ||
| 658 | try req.start(); | |
| 659 | try req.wait(); | |
| 660 | try testing.expectEqual(http.Status.expectation_failed, req.response.status); | |
| 661 | } | |
| 662 | ||
| 595 | 663 | { // issue 16282 *** This test leaves the client in an invalid state, it must be last *** |
| 596 | 664 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); |
| 597 | 665 | defer calloc.free(location); |