authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-08-22 10:05:03-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-08-29 21:42:53-05:00
logaa090a49d94155c4804644377db110f3b13f0500
tree06226127b3e279414c6a8f1ca2a12f7c6b75f308
parent5d40338f21b468c82d4bc2a1ac0a35c643126e74
signaturelock-open Commit is signed but in an unrecognized format.

std.http: handle expect:100-continue and continue responses


4 files changed, 156 insertions(+), 42 deletions(-)

lib/std/http/Client.zig+40-5
......@@ -478,6 +478,7 @@ pub const Request = struct {
478478 .zstd => |*zstd| zstd.deinit(),
479479 }
480480
481 req.headers.deinit();
481482 req.response.headers.deinit();
482483
483484 if (req.response.parser.header_bytes_owned) {
......@@ -667,17 +668,19 @@ pub const Request = struct {
667668
668669 try req.response.parse(req.response.parser.header_bytes.items, false);
669670
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;
673675 }
674676
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)) {
676679 req.connection.?.data.closing = false;
677680 req.response.parser.done = true;
678681 }
679682
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
681684 const req_connection = req.headers.getFirstValue("connection");
682685 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
683686
......@@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:
955958 return conn;
956959}
957960
961pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{NameTooLong} || std.os.ConnectError;
962
963pub 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
958993// Prevents a dependency loop in request()
959994const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };
960995pub const ConnectError = ConnectErrorPartial || RequestError;
lib/std/http/Server.zig+41-33
......@@ -411,48 +411,52 @@ pub const Response = struct {
411411 }
412412 try w.writeAll("\r\n");
413413
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 }
417420
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.?);
421424
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 }
426430 }
427 }
428431
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");
431434
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 => {},
449440 }
450441 } 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 }
452456 }
453 }
454457
455 try w.print("{}", .{res.headers});
458 try w.print("{}", .{res.headers});
459 }
456460
457461 try w.writeAll("\r\n");
458462
......@@ -516,6 +520,10 @@ pub const Response = struct {
516520 res.request.parser.done = true;
517521 }
518522
523 if (res.request.method == .HEAD) {
524 res.request.parser.done = true;
525 }
526
519527 if (!res.request.parser.done) {
520528 if (res.request.transfer_compression) |tc| switch (tc) {
521529 .compress => return error.CompressionNotSupported,
lib/std/http/protocol.zig+6-3
......@@ -534,9 +534,9 @@ pub const HeadersParser = struct {
534534
535535 if (r.next_chunk_length == 0) r.done = true;
536536
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;
540540
541541 const can_read = @as(usize, @intCast(@min(data_avail, out_avail)));
542542 const nread = try conn.read(buffer[0..can_read]);
......@@ -545,6 +545,8 @@ pub const HeadersParser = struct {
545545 if (r.next_chunk_length == 0) r.done = true;
546546
547547 return nread;
548 } else {
549 return out_index;
548550 }
549551 },
550552 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {
......@@ -558,6 +560,7 @@ pub const HeadersParser = struct {
558560 .chunk_data => if (r.next_chunk_length == 0) {
559561 if (std.mem.eql(u8, conn.peek(), "\r\n")) {
560562 r.state = .finished;
563 r.done = true;
561564 } else {
562565 // The trailer section is formatted identically to the header section.
563566 r.state = .seen_rn;
test/standalone/http.zig+69-1
......@@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void {
2222
2323 log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target });
2424
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
2537 const body = try res.reader().readAllAlloc(salloc, 8192);
2638 defer salloc.free(body);
2739
......@@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void {
6274 }
6375
6476 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")) {
6678 try testing.expectEqualStrings("Hello, World!\n", body);
6779 try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?);
6880
......@@ -592,6 +604,62 @@ pub fn main() !void {
592604 try testing.expectEqualStrings("Hello, World!\n", res.body.?);
593605 }
594606
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
595663 { // issue 16282 *** This test leaves the client in an invalid state, it must be last ***
596664 const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port});
597665 defer calloc.free(location);