| ... | @@ -21,11 +21,27 @@ ca_bundle: std.crypto.Certificate.Bundle = .{}, | ... | @@ -21,11 +21,27 @@ ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| 21 | /// it will first rescan the system for root certificates. | 21 | /// it will first rescan the system for root certificates. |
| 22 | next_https_rescan_certs: bool = true, | 22 | next_https_rescan_certs: bool = true, |
| 23 | | 23 | |
| | 24 | connection_pool: std.TailQueue(Connection) = .{}, |
| | 25 | |
| | 26 | const ConnectionPool = std.TailQueue(Connection); |
| | 27 | const ConnectionNode = ConnectionPool.Node; |
| | 28 | |
| | 29 | pub fn release(client: *Client, node: *ConnectionNode) void { |
| | 30 | if (node.data.unusable) return node.data.close(client); |
| | 31 | |
| | 32 | client.connection_pool.append(node); |
| | 33 | } |
| | 34 | |
| 24 | pub const Connection = struct { | 35 | pub const Connection = struct { |
| 25 | stream: net.Stream, | 36 | stream: net.Stream, |
| 26 | /// undefined unless protocol is tls. | 37 | /// undefined unless protocol is tls. |
| 27 | tls_client: std.crypto.tls.Client, | 38 | tls_client: std.crypto.tls.Client, // TODO: allocate this, it's currently 16 KB. |
| 28 | protocol: Protocol, | 39 | protocol: Protocol, |
| | 40 | host: []u8, |
| | 41 | port: u16, |
| | 42 | |
| | 43 | // This connection has been part of a non keepalive request and cannot be added to the pool. |
| | 44 | unusable: bool = false, |
| 29 | | 45 | |
| 30 | pub const Protocol = enum { plain, tls }; | 46 | pub const Protocol = enum { plain, tls }; |
| 31 | | 47 | |
| ... | @@ -56,6 +72,17 @@ pub const Connection = struct { | ... | @@ -56,6 +72,17 @@ pub const Connection = struct { |
| 56 | .tls => return conn.tls_client.write(conn.stream, buffer), | 72 | .tls => return conn.tls_client.write(conn.stream, buffer), |
| 57 | } | 73 | } |
| 58 | } | 74 | } |
| | 75 | |
| | 76 | pub fn close(conn: *Connection, client: *const Client) void { |
| | 77 | if (conn.protocol == .tls) { |
| | 78 | // try to cleanly close the TLS connection, for any server that cares. |
| | 79 | _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {}; |
| | 80 | } |
| | 81 | |
| | 82 | conn.stream.close(); |
| | 83 | |
| | 84 | client.allocator.free(conn.host); |
| | 85 | } |
| 59 | }; | 86 | }; |
| 60 | | 87 | |
| 61 | /// TODO: emit error.UnexpectedEndOfStream or something like that when the read | 88 | /// TODO: emit error.UnexpectedEndOfStream or something like that when the read |
| ... | @@ -63,7 +90,7 @@ pub const Connection = struct { | ... | @@ -63,7 +90,7 @@ pub const Connection = struct { |
| 63 | /// close_notify protection on underlying TLS streams. | 90 | /// close_notify protection on underlying TLS streams. |
| 64 | pub const Request = struct { | 91 | pub const Request = struct { |
| 65 | client: *Client, | 92 | client: *Client, |
| 66 | connection: Connection, | 93 | connection: *ConnectionNode, |
| 67 | redirects_left: u32, | 94 | redirects_left: u32, |
| 68 | response: Response, | 95 | response: Response, |
| 69 | /// These are stored in Request so that they are available when following | 96 | /// These are stored in Request so that they are available when following |
| ... | @@ -79,6 +106,7 @@ pub const Request = struct { | ... | @@ -79,6 +106,7 @@ pub const Request = struct { |
| 79 | header_bytes: std.ArrayListUnmanaged(u8), | 106 | header_bytes: std.ArrayListUnmanaged(u8), |
| 80 | max_header_bytes: usize, | 107 | max_header_bytes: usize, |
| 81 | next_chunk_length: u64, | 108 | next_chunk_length: u64, |
| | 109 | done: bool, |
| 82 | | 110 | |
| 83 | pub const Headers = struct { | 111 | pub const Headers = struct { |
| 84 | status: http.Status, | 112 | status: http.Status, |
| ... | @@ -86,6 +114,7 @@ pub const Request = struct { | ... | @@ -86,6 +114,7 @@ pub const Request = struct { |
| 86 | location: ?[]const u8 = null, | 114 | location: ?[]const u8 = null, |
| 87 | content_length: ?u64 = null, | 115 | content_length: ?u64 = null, |
| 88 | transfer_encoding: ?http.TransferEncoding = null, | 116 | transfer_encoding: ?http.TransferEncoding = null, |
| | 117 | connection_close: bool = true, |
| 89 | | 118 | |
| 90 | pub fn parse(bytes: []const u8) !Response.Headers { | 119 | pub fn parse(bytes: []const u8) !Response.Headers { |
| 91 | var it = mem.split(u8, bytes[0 .. bytes.len - 4], "\r\n"); | 120 | var it = mem.split(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| ... | @@ -126,6 +155,14 @@ pub const Request = struct { | ... | @@ -126,6 +155,14 @@ pub const Request = struct { |
| 126 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; | 155 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; |
| 127 | headers.transfer_encoding = std.meta.stringToEnum(http.TransferEncoding, header_value) orelse | 156 | headers.transfer_encoding = std.meta.stringToEnum(http.TransferEncoding, header_value) orelse |
| 128 | return error.HttpTransferEncodingUnsupported; | 157 | return error.HttpTransferEncodingUnsupported; |
| | 158 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { |
| | 159 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { |
| | 160 | headers.connection_close = false; |
| | 161 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { |
| | 162 | headers.connection_close = true; |
| | 163 | } else { |
| | 164 | return error.HttpConnectionHeaderUnsupported; |
| | 165 | } |
| 129 | } | 166 | } |
| 130 | } | 167 | } |
| 131 | | 168 | |
| ... | @@ -185,10 +222,10 @@ pub const Request = struct { | ... | @@ -185,10 +222,10 @@ pub const Request = struct { |
| 185 | chunk_r, | 222 | chunk_r, |
| 186 | chunk_data, | 223 | chunk_data, |
| 187 | | 224 | |
| 188 | pub fn zeroMeansEnd(state: State) bool { | 225 | pub fn isContent(self: State) bool { |
| 189 | return switch (state) { | 226 | return switch (self) { |
| 190 | .finished, .chunk_data => true, | 227 | .invalid, .start, .seen_r, .seen_rn, .seen_rnr => false, |
| 191 | else => false, | 228 | .finished, .chunk_size_prefix_r, .chunk_size_prefix_n, .chunk_size, .chunk_r, .chunk_data => true, |
| 192 | }; | 229 | }; |
| 193 | } | 230 | } |
| 194 | }; | 231 | }; |
| ... | @@ -201,6 +238,7 @@ pub const Request = struct { | ... | @@ -201,6 +238,7 @@ pub const Request = struct { |
| 201 | .max_header_bytes = max, | 238 | .max_header_bytes = max, |
| 202 | .header_bytes_owned = true, | 239 | .header_bytes_owned = true, |
| 203 | .next_chunk_length = undefined, | 240 | .next_chunk_length = undefined, |
| | 241 | .done = false, |
| 204 | }; | 242 | }; |
| 205 | } | 243 | } |
| 206 | | 244 | |
| ... | @@ -212,6 +250,7 @@ pub const Request = struct { | ... | @@ -212,6 +250,7 @@ pub const Request = struct { |
| 212 | .max_header_bytes = buf.len, | 250 | .max_header_bytes = buf.len, |
| 213 | .header_bytes_owned = false, | 251 | .header_bytes_owned = false, |
| 214 | .next_chunk_length = undefined, | 252 | .next_chunk_length = undefined, |
| | 253 | .done = false, |
| 215 | }; | 254 | }; |
| 216 | } | 255 | } |
| 217 | | 256 | |
| ... | @@ -501,6 +540,7 @@ pub const Request = struct { | ... | @@ -501,6 +540,7 @@ pub const Request = struct { |
| 501 | pub const Headers = struct { | 540 | pub const Headers = struct { |
| 502 | version: http.Version = .@"HTTP/1.1", | 541 | version: http.Version = .@"HTTP/1.1", |
| 503 | method: http.Method = .GET, | 542 | method: http.Method = .GET, |
| | 543 | connection_close: bool = false, |
| 504 | }; | 544 | }; |
| 505 | | 545 | |
| 506 | pub const Options = struct { | 546 | pub const Options = struct { |
| ... | @@ -545,6 +585,7 @@ pub const Request = struct { | ... | @@ -545,6 +585,7 @@ pub const Request = struct { |
| 545 | HttpHeadersExceededSizeLimit, | 585 | HttpHeadersExceededSizeLimit, |
| 546 | HttpRedirectMissingLocation, | 586 | HttpRedirectMissingLocation, |
| 547 | HttpTransferEncodingUnsupported, | 587 | HttpTransferEncodingUnsupported, |
| | 588 | HttpConnectionHeaderUnsupported, |
| 548 | HttpContentLengthUnknown, | 589 | HttpContentLengthUnknown, |
| 549 | TooManyHttpRedirects, | 590 | TooManyHttpRedirects, |
| 550 | ShortHttpStatusLine, | 591 | ShortHttpStatusLine, |
| ... | @@ -669,8 +710,9 @@ pub const Request = struct { | ... | @@ -669,8 +710,9 @@ pub const Request = struct { |
| 669 | assert(len <= buffer.len); | 710 | assert(len <= buffer.len); |
| 670 | var index: usize = 0; | 711 | var index: usize = 0; |
| 671 | while (index < len) { | 712 | while (index < len) { |
| 672 | const zero_means_end = req.response.state.zeroMeansEnd(); | | |
| 673 | const amt = try readAdvanced(req, buffer[index..]); | 713 | const amt = try readAdvanced(req, buffer[index..]); |
| | 714 | const zero_means_end = req.response.done and req.response.headers.status.class() != .redirect; |
| | 715 | |
| 674 | if (amt == 0 and zero_means_end) break; | 716 | if (amt == 0 and zero_means_end) break; |
| 675 | index += amt; | 717 | index += amt; |
| 676 | } | 718 | } |
| ... | @@ -680,7 +722,29 @@ pub const Request = struct { | ... | @@ -680,7 +722,29 @@ pub const Request = struct { |
| 680 | /// This one can return 0 without meaning EOF. | 722 | /// This one can return 0 without meaning EOF. |
| 681 | /// TODO change to readvAdvanced | 723 | /// TODO change to readvAdvanced |
| 682 | pub fn readAdvanced(req: *Request, buffer: []u8) !usize { | 724 | pub fn readAdvanced(req: *Request, buffer: []u8) !usize { |
| 683 | var in = buffer[0..try req.connection.read(buffer)]; | 725 | if (req.response.done) { |
| | 726 | if (req.response.headers.status.class() == .redirect) { |
| | 727 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| | 728 | |
| | 729 | const location = req.response.headers.location orelse |
| | 730 | return error.HttpRedirectMissingLocation; |
| | 731 | const new_url = try std.Uri.parse(location); |
| | 732 | const new_req = try req.client.request(new_url, req.headers, .{ |
| | 733 | .max_redirects = req.redirects_left - 1, |
| | 734 | .header_strategy = if (req.response.header_bytes_owned) .{ |
| | 735 | .dynamic = req.response.max_header_bytes, |
| | 736 | } else .{ |
| | 737 | .static = req.response.header_bytes.unusedCapacitySlice(), |
| | 738 | }, |
| | 739 | }); |
| | 740 | req.deinit(); |
| | 741 | req.* = new_req; |
| | 742 | } else { |
| | 743 | return 0; |
| | 744 | } |
| | 745 | } |
| | 746 | |
| | 747 | var in = buffer[0..try req.connection.data.read(buffer)]; |
| 684 | var out_index: usize = 0; | 748 | var out_index: usize = 0; |
| 685 | while (true) { | 749 | while (true) { |
| 686 | switch (req.response.state) { | 750 | switch (req.response.state) { |
| ... | @@ -698,24 +762,10 @@ pub const Request = struct { | ... | @@ -698,24 +762,10 @@ pub const Request = struct { |
| 698 | if (req.response.state == .finished) { | 762 | if (req.response.state == .finished) { |
| 699 | req.response.headers = try Response.Headers.parse(req.response.header_bytes.items); | 763 | req.response.headers = try Response.Headers.parse(req.response.header_bytes.items); |
| 700 | | 764 | |
| 701 | if (req.response.headers.status.class() == .redirect) { | 765 | if (req.response.headers.connection_close == true) { |
| 702 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | 766 | req.connection.data.unusable = true; |
| 703 | const location = req.response.headers.location orelse | 767 | } else { |
| 704 | return error.HttpRedirectMissingLocation; | 768 | req.connection.data.unusable = false; |
| 705 | const new_url = try std.Uri.parse(location); | | |
| 706 | const new_req = try req.client.request(new_url, req.headers, .{ | | |
| 707 | .max_redirects = req.redirects_left - 1, | | |
| 708 | .header_strategy = if (req.response.header_bytes_owned) .{ | | |
| 709 | .dynamic = req.response.max_header_bytes, | | |
| 710 | } else .{ | | |
| 711 | .static = req.response.header_bytes.unusedCapacitySlice(), | | |
| 712 | }, | | |
| 713 | }); | | |
| 714 | req.deinit(); | | |
| 715 | req.* = new_req; | | |
| 716 | assert(out_index == 0); | | |
| 717 | in = buffer[0..try req.connection.read(buffer)]; | | |
| 718 | continue; | | |
| 719 | } | 769 | } |
| 720 | | 770 | |
| 721 | if (req.response.headers.transfer_encoding) |transfer_encoding| { | 771 | if (req.response.headers.transfer_encoding) |transfer_encoding| { |
| ... | @@ -742,11 +792,29 @@ pub const Request = struct { | ... | @@ -742,11 +792,29 @@ pub const Request = struct { |
| 742 | return 0; | 792 | return 0; |
| 743 | }, | 793 | }, |
| 744 | .finished => { | 794 | .finished => { |
| | 795 | const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len)); |
| | 796 | req.response.next_chunk_length -= sub_amt; |
| | 797 | |
| | 798 | if (req.response.next_chunk_length == 0) { |
| | 799 | req.client.release(req.connection); |
| | 800 | req.connection = undefined; |
| | 801 | |
| | 802 | req.response.done = true; |
| | 803 | assert(in.len == sub_amt); // TODO: figure out how to not read more than necessary. |
| | 804 | |
| | 805 | if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) return 0; |
| | 806 | |
| | 807 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| | 808 | return out_index + sub_amt; |
| | 809 | } |
| | 810 | |
| | 811 | if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) return 0; |
| | 812 | |
| 745 | if (in.ptr == buffer.ptr) { | 813 | if (in.ptr == buffer.ptr) { |
| 746 | return in.len; | 814 | return sub_amt; |
| 747 | } else { | 815 | } else { |
| 748 | mem.copy(u8, buffer[out_index..], in); | 816 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| 749 | return out_index + in.len; | 817 | return out_index + sub_amt; |
| 750 | } | 818 | } |
| 751 | }, | 819 | }, |
| 752 | .chunk_size_prefix_r => switch (in.len) { | 820 | .chunk_size_prefix_r => switch (in.len) { |
| ... | @@ -793,7 +861,10 @@ pub const Request = struct { | ... | @@ -793,7 +861,10 @@ pub const Request = struct { |
| 793 | .invalid => return error.HttpHeadersInvalid, | 861 | .invalid => return error.HttpHeadersInvalid, |
| 794 | .chunk_data => { | 862 | .chunk_data => { |
| 795 | if (req.response.next_chunk_length == 0) { | 863 | if (req.response.next_chunk_length == 0) { |
| 796 | req.response.state = .start; | 864 | req.response.done = true; |
| | 865 | req.client.release(req.connection); |
| | 866 | req.connection = undefined; |
| | 867 | |
| 797 | return out_index; | 868 | return out_index; |
| 798 | } | 869 | } |
| 799 | in = in[i..]; | 870 | in = in[i..]; |
| ... | @@ -807,20 +878,27 @@ pub const Request = struct { | ... | @@ -807,20 +878,27 @@ pub const Request = struct { |
| 807 | // TODO https://github.com/ziglang/zig/issues/14039 | 878 | // TODO https://github.com/ziglang/zig/issues/14039 |
| 808 | const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len)); | 879 | const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len)); |
| 809 | req.response.next_chunk_length -= sub_amt; | 880 | req.response.next_chunk_length -= sub_amt; |
| 810 | if (req.response.next_chunk_length > 0) { | 881 | |
| 811 | if (in.ptr == buffer.ptr) { | 882 | if (req.response.next_chunk_length == 0) { |
| 812 | return sub_amt; | 883 | req.response.state = .chunk_size_prefix_r; |
| 813 | } else { | 884 | in = in[sub_amt..]; |
| 814 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); | 885 | |
| 815 | out_index += sub_amt; | 886 | if (req.response.headers.status.class() == .redirect) continue; |
| 816 | return out_index; | 887 | |
| 817 | } | 888 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| | 889 | out_index += sub_amt; |
| | 890 | continue; |
| | 891 | } |
| | 892 | |
| | 893 | if (req.response.headers.status.class() == .redirect) return 0; |
| | 894 | |
| | 895 | if (in.ptr == buffer.ptr) { |
| | 896 | return sub_amt; |
| | 897 | } else { |
| | 898 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| | 899 | out_index += sub_amt; |
| | 900 | return out_index; |
| 818 | } | 901 | } |
| 819 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); | | |
| 820 | out_index += sub_amt; | | |
| 821 | req.response.state = .chunk_size_prefix_r; | | |
| 822 | in = in[sub_amt..]; | | |
| 823 | continue; | | |
| 824 | }, | 902 | }, |
| 825 | } | 903 | } |
| 826 | } | 904 | } |
| ... | @@ -844,24 +922,52 @@ pub const Request = struct { | ... | @@ -844,24 +922,52 @@ pub const Request = struct { |
| 844 | }; | 922 | }; |
| 845 | | 923 | |
| 846 | pub fn deinit(client: *Client) void { | 924 | pub fn deinit(client: *Client) void { |
| | 925 | var next = client.connection_pool.first; |
| | 926 | while (next) |node| { |
| | 927 | next = node.next; |
| | 928 | |
| | 929 | node.data.close(client); |
| | 930 | |
| | 931 | client.allocator.destroy(node); |
| | 932 | } |
| | 933 | |
| 847 | client.ca_bundle.deinit(client.allocator); | 934 | client.ca_bundle.deinit(client.allocator); |
| 848 | client.* = undefined; | 935 | client.* = undefined; |
| 849 | } | 936 | } |
| 850 | | 937 | |
| 851 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !Connection { | 938 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !*ConnectionNode { |
| 852 | var conn: Connection = .{ | 939 | var potential = client.connection_pool.last; |
| | 940 | while (potential) |node| { |
| | 941 | const same_host = mem.eql(u8, node.data.host, host); |
| | 942 | const same_port = node.data.port == port; |
| | 943 | const same_protocol = node.data.protocol == protocol; |
| | 944 | |
| | 945 | if (same_host and same_port and same_protocol) { |
| | 946 | client.connection_pool.remove(node); |
| | 947 | return node; |
| | 948 | } |
| | 949 | |
| | 950 | potential = node.prev; |
| | 951 | } |
| | 952 | |
| | 953 | const conn = try client.allocator.create(ConnectionNode); |
| | 954 | errdefer client.allocator.destroy(conn); |
| | 955 | |
| | 956 | conn.* = .{ .data = .{ |
| 853 | .stream = try net.tcpConnectToHost(client.allocator, host, port), | 957 | .stream = try net.tcpConnectToHost(client.allocator, host, port), |
| 854 | .tls_client = undefined, | 958 | .tls_client = undefined, |
| 855 | .protocol = protocol, | 959 | .protocol = protocol, |
| 856 | }; | 960 | .host = try client.allocator.dupe(u8, host), |
| | 961 | .port = port, |
| | 962 | } }; |
| 857 | | 963 | |
| 858 | switch (protocol) { | 964 | switch (protocol) { |
| 859 | .plain => {}, | 965 | .plain => {}, |
| 860 | .tls => { | 966 | .tls => { |
| 861 | conn.tls_client = try std.crypto.tls.Client.init(conn.stream, client.ca_bundle, host); | 967 | conn.data.tls_client = try std.crypto.tls.Client.init(conn.data.stream, client.ca_bundle, host); |
| 862 | // This is appropriate for HTTPS because the HTTP headers contain | 968 | // This is appropriate for HTTPS because the HTTP headers contain |
| 863 | // the content length which is used to detect truncation attacks. | 969 | // the content length which is used to detect truncation attacks. |
| 864 | conn.tls_client.allow_truncation_attacks = true; | 970 | conn.data.tls_client.allow_truncation_attacks = true; |
| 865 | }, | 971 | }, |
| 866 | } | 972 | } |
| 867 | | 973 | |
| ... | @@ -908,10 +1014,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req | ... | @@ -908,10 +1014,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 908 | try h.appendSlice(@tagName(headers.version)); | 1014 | try h.appendSlice(@tagName(headers.version)); |
| 909 | try h.appendSlice("\r\nHost: "); | 1015 | try h.appendSlice("\r\nHost: "); |
| 910 | try h.appendSlice(host); | 1016 | try h.appendSlice(host); |
| 911 | try h.appendSlice("\r\nConnection: close\r\n\r\n"); | 1017 | if (headers.connection_close) { |
| | 1018 | try h.appendSlice("\r\nConnection: close"); |
| | 1019 | } else { |
| | 1020 | try h.appendSlice("\r\nConnection: keep-alive"); |
| | 1021 | } |
| | 1022 | try h.appendSlice("\r\n\r\n"); |
| 912 | | 1023 | |
| 913 | const header_bytes = h.slice(); | 1024 | const header_bytes = h.slice(); |
| 914 | try req.connection.writeAll(header_bytes); | 1025 | try req.connection.data.writeAll(header_bytes); |
| 915 | } | 1026 | } |
| 916 | | 1027 | |
| 917 | return req; | 1028 | return req; |