| ... | ... | @@ -21,11 +21,27 @@ ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| 21 | 21 | /// it will first rescan the system for root certificates. |
| 22 | 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 | 35 | pub const Connection = struct { |
| 25 | 36 | stream: net.Stream, |
| 26 | 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 | 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 | 46 | pub const Protocol = enum { plain, tls }; |
| 31 | 47 | |
| ... | ... | @@ -56,6 +72,17 @@ pub const Connection = struct { |
| 56 | 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 | 88 | /// TODO: emit error.UnexpectedEndOfStream or something like that when the read |
| ... | ... | @@ -63,7 +90,7 @@ pub const Connection = struct { |
| 63 | 90 | /// close_notify protection on underlying TLS streams. |
| 64 | 91 | pub const Request = struct { |
| 65 | 92 | client: *Client, |
| 66 | | connection: Connection, |
| 93 | connection: *ConnectionNode, |
| 67 | 94 | redirects_left: u32, |
| 68 | 95 | response: Response, |
| 69 | 96 | /// These are stored in Request so that they are available when following |
| ... | ... | @@ -79,6 +106,7 @@ pub const Request = struct { |
| 79 | 106 | header_bytes: std.ArrayListUnmanaged(u8), |
| 80 | 107 | max_header_bytes: usize, |
| 81 | 108 | next_chunk_length: u64, |
| 109 | done: bool, |
| 82 | 110 | |
| 83 | 111 | pub const Headers = struct { |
| 84 | 112 | status: http.Status, |
| ... | ... | @@ -86,6 +114,7 @@ pub const Request = struct { |
| 86 | 114 | location: ?[]const u8 = null, |
| 87 | 115 | content_length: ?u64 = null, |
| 88 | 116 | transfer_encoding: ?http.TransferEncoding = null, |
| 117 | connection_close: bool = true, |
| 89 | 118 | |
| 90 | 119 | pub fn parse(bytes: []const u8) !Response.Headers { |
| 91 | 120 | var it = mem.split(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| ... | ... | @@ -126,6 +155,14 @@ pub const Request = struct { |
| 126 | 155 | if (headers.transfer_encoding != null) return error.HttpHeadersInvalid; |
| 127 | 156 | headers.transfer_encoding = std.meta.stringToEnum(http.TransferEncoding, header_value) orelse |
| 128 | 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 | 222 | chunk_r, |
| 186 | 223 | chunk_data, |
| 187 | 224 | |
| 188 | | pub fn zeroMeansEnd(state: State) bool { |
| 189 | | return switch (state) { |
| 190 | | .finished, .chunk_data => true, |
| 191 | | else => false, |
| 225 | pub fn isContent(self: State) bool { |
| 226 | return switch (self) { |
| 227 | .invalid, .start, .seen_r, .seen_rn, .seen_rnr => 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 | 238 | .max_header_bytes = max, |
| 202 | 239 | .header_bytes_owned = true, |
| 203 | 240 | .next_chunk_length = undefined, |
| 241 | .done = false, |
| 204 | 242 | }; |
| 205 | 243 | } |
| 206 | 244 | |
| ... | ... | @@ -212,6 +250,7 @@ pub const Request = struct { |
| 212 | 250 | .max_header_bytes = buf.len, |
| 213 | 251 | .header_bytes_owned = false, |
| 214 | 252 | .next_chunk_length = undefined, |
| 253 | .done = false, |
| 215 | 254 | }; |
| 216 | 255 | } |
| 217 | 256 | |
| ... | ... | @@ -501,6 +540,7 @@ pub const Request = struct { |
| 501 | 540 | pub const Headers = struct { |
| 502 | 541 | version: http.Version = .@"HTTP/1.1", |
| 503 | 542 | method: http.Method = .GET, |
| 543 | connection_close: bool = false, |
| 504 | 544 | }; |
| 505 | 545 | |
| 506 | 546 | pub const Options = struct { |
| ... | ... | @@ -545,6 +585,7 @@ pub const Request = struct { |
| 545 | 585 | HttpHeadersExceededSizeLimit, |
| 546 | 586 | HttpRedirectMissingLocation, |
| 547 | 587 | HttpTransferEncodingUnsupported, |
| 588 | HttpConnectionHeaderUnsupported, |
| 548 | 589 | HttpContentLengthUnknown, |
| 549 | 590 | TooManyHttpRedirects, |
| 550 | 591 | ShortHttpStatusLine, |
| ... | ... | @@ -669,8 +710,9 @@ pub const Request = struct { |
| 669 | 710 | assert(len <= buffer.len); |
| 670 | 711 | var index: usize = 0; |
| 671 | 712 | while (index < len) { |
| 672 | | const zero_means_end = req.response.state.zeroMeansEnd(); |
| 673 | 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 | 716 | if (amt == 0 and zero_means_end) break; |
| 675 | 717 | index += amt; |
| 676 | 718 | } |
| ... | ... | @@ -680,7 +722,29 @@ pub const Request = struct { |
| 680 | 722 | /// This one can return 0 without meaning EOF. |
| 681 | 723 | /// TODO change to readvAdvanced |
| 682 | 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 | 748 | var out_index: usize = 0; |
| 685 | 749 | while (true) { |
| 686 | 750 | switch (req.response.state) { |
| ... | ... | @@ -698,24 +762,10 @@ pub const Request = struct { |
| 698 | 762 | if (req.response.state == .finished) { |
| 699 | 763 | req.response.headers = try Response.Headers.parse(req.response.header_bytes.items); |
| 700 | 764 | |
| 701 | | if (req.response.headers.status.class() == .redirect) { |
| 702 | | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 703 | | const location = req.response.headers.location orelse |
| 704 | | return error.HttpRedirectMissingLocation; |
| 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; |
| 765 | if (req.response.headers.connection_close == true) { |
| 766 | req.connection.data.unusable = true; |
| 767 | } else { |
| 768 | req.connection.data.unusable = false; |
| 719 | 769 | } |
| 720 | 770 | |
| 721 | 771 | if (req.response.headers.transfer_encoding) |transfer_encoding| { |
| ... | ... | @@ -742,11 +792,29 @@ pub const Request = struct { |
| 742 | 792 | return 0; |
| 743 | 793 | }, |
| 744 | 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 | 813 | if (in.ptr == buffer.ptr) { |
| 746 | | return in.len; |
| 814 | return sub_amt; |
| 747 | 815 | } else { |
| 748 | | mem.copy(u8, buffer[out_index..], in); |
| 749 | | return out_index + in.len; |
| 816 | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| 817 | return out_index + sub_amt; |
| 750 | 818 | } |
| 751 | 819 | }, |
| 752 | 820 | .chunk_size_prefix_r => switch (in.len) { |
| ... | ... | @@ -793,7 +861,10 @@ pub const Request = struct { |
| 793 | 861 | .invalid => return error.HttpHeadersInvalid, |
| 794 | 862 | .chunk_data => { |
| 795 | 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 | 868 | return out_index; |
| 798 | 869 | } |
| 799 | 870 | in = in[i..]; |
| ... | ... | @@ -807,20 +878,27 @@ pub const Request = struct { |
| 807 | 878 | // TODO https://github.com/ziglang/zig/issues/14039 |
| 808 | 879 | const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len)); |
| 809 | 880 | req.response.next_chunk_length -= sub_amt; |
| 810 | | if (req.response.next_chunk_length > 0) { |
| 811 | | if (in.ptr == buffer.ptr) { |
| 812 | | return sub_amt; |
| 813 | | } else { |
| 814 | | mem.copy(u8, buffer[out_index..], in[0..sub_amt]); |
| 815 | | out_index += sub_amt; |
| 816 | | return out_index; |
| 817 | | } |
| 881 | |
| 882 | if (req.response.next_chunk_length == 0) { |
| 883 | req.response.state = .chunk_size_prefix_r; |
| 884 | in = in[sub_amt..]; |
| 885 | |
| 886 | if (req.response.headers.status.class() == .redirect) continue; |
| 887 | |
| 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 | 922 | }; |
| 845 | 923 | |
| 846 | 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 | 934 | client.ca_bundle.deinit(client.allocator); |
| 848 | 935 | client.* = undefined; |
| 849 | 936 | } |
| 850 | 937 | |
| 851 | | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !Connection { |
| 852 | | var conn: Connection = .{ |
| 938 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !*ConnectionNode { |
| 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 | 957 | .stream = try net.tcpConnectToHost(client.allocator, host, port), |
| 854 | 958 | .tls_client = undefined, |
| 855 | 959 | .protocol = protocol, |
| 856 | | }; |
| 960 | .host = try client.allocator.dupe(u8, host), |
| 961 | .port = port, |
| 962 | } }; |
| 857 | 963 | |
| 858 | 964 | switch (protocol) { |
| 859 | 965 | .plain => {}, |
| 860 | 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 | 968 | // This is appropriate for HTTPS because the HTTP headers contain |
| 863 | 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 | 1014 | try h.appendSlice(@tagName(headers.version)); |
| 909 | 1015 | try h.appendSlice("\r\nHost: "); |
| 910 | 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 | 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 | 1028 | return req; |