| ... | ... | @@ -29,9 +29,10 @@ const ConnectionPool = std.TailQueue(Connection); |
| 29 | 29 | const ConnectionNode = ConnectionPool.Node; |
| 30 | 30 | |
| 31 | 31 | /// Acquires an existing connection from the connection pool. This function is threadsafe. |
| 32 | | pub fn acquire(client: *Client, node: *ConnectionNode) void { |
| 33 | | client.connection_mutex.lock(); |
| 34 | | defer client.connection_mutex.unlock(); |
| 32 | /// If the caller already holds the connection mutex, it should pass `true` for `held`. |
| 33 | pub fn acquire(client: *Client, node: *ConnectionNode, held: bool) void { |
| 34 | if (!held) client.connection_mutex.lock(); |
| 35 | defer if (!held) client.connection_mutex.unlock(); |
| 35 | 36 | |
| 36 | 37 | client.connection_pool.remove(node); |
| 37 | 38 | client.connection_used.append(node); |
| ... | ... | @@ -40,16 +41,17 @@ pub fn acquire(client: *Client, node: *ConnectionNode) void { |
| 40 | 41 | /// Tries to release a connection back to the connection pool. This function is threadsafe. |
| 41 | 42 | /// If the connection is marked as closing, it will be closed instead. |
| 42 | 43 | pub fn release(client: *Client, node: *ConnectionNode) void { |
| 44 | client.connection_mutex.lock(); |
| 45 | defer client.connection_mutex.unlock(); |
| 46 | |
| 47 | client.connection_used.remove(node); |
| 48 | |
| 43 | 49 | if (node.data.closing) { |
| 44 | 50 | node.data.close(client); |
| 45 | 51 | |
| 46 | 52 | return client.allocator.destroy(node); |
| 47 | 53 | } |
| 48 | 54 | |
| 49 | | client.connection_mutex.lock(); |
| 50 | | defer client.connection_mutex.unlock(); |
| 51 | | |
| 52 | | client.connection_used.remove(node); |
| 53 | 55 | client.connection_pool.append(node); |
| 54 | 56 | } |
| 55 | 57 | |
| ... | ... | @@ -83,7 +85,7 @@ pub const Connection = struct { |
| 83 | 85 | } |
| 84 | 86 | } |
| 85 | 87 | |
| 86 | | pub const ReadError = std.net.Stream.ReadError || error{ |
| 88 | pub const ReadError = net.Stream.ReadError || error{ |
| 87 | 89 | TlsConnectionTruncated, |
| 88 | 90 | TlsRecordOverflow, |
| 89 | 91 | TlsDecodeError, |
| ... | ... | @@ -115,7 +117,7 @@ pub const Connection = struct { |
| 115 | 117 | } |
| 116 | 118 | } |
| 117 | 119 | |
| 118 | | pub const WriteError = std.net.Stream.WriteError || error{}; |
| 120 | pub const WriteError = net.Stream.WriteError || error{}; |
| 119 | 121 | pub const Writer = std.io.Writer(*Connection, WriteError, write); |
| 120 | 122 | |
| 121 | 123 | pub fn writer(conn: *Connection) Writer { |
| ... | ... | @@ -139,14 +141,21 @@ pub const Request = struct { |
| 139 | 141 | const read_buffer_size = 8192; |
| 140 | 142 | const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size); |
| 141 | 143 | |
| 144 | uri: Uri, |
| 142 | 145 | client: *Client, |
| 143 | 146 | connection: *ConnectionNode, |
| 144 | | redirects_left: u32, |
| 145 | 147 | response: Response, |
| 146 | 148 | /// These are stored in Request so that they are available when following |
| 147 | 149 | /// redirects. |
| 148 | 150 | headers: Headers, |
| 149 | 151 | |
| 152 | redirects_left: u32, |
| 153 | handle_redirects: bool, |
| 154 | compression_init: bool, |
| 155 | |
| 156 | /// Used as a allocator for resolving redirects locations. |
| 157 | arena: std.heap.ArenaAllocator, |
| 158 | |
| 150 | 159 | /// Read buffer for the connection. This is used to pull in large amounts of data from the connection even if the user asks for a small amount. This can probably be removed with careful planning. |
| 151 | 160 | read_buffer: [read_buffer_size]u8 = undefined, |
| 152 | 161 | read_buffer_start: ReadBufferIndex = 0, |
| ... | ... | @@ -661,6 +670,7 @@ pub const Request = struct { |
| 661 | 670 | pub const Headers = struct { |
| 662 | 671 | version: http.Version = .@"HTTP/1.1", |
| 663 | 672 | method: http.Method = .GET, |
| 673 | user_agent: []const u8 = "Zig (std.http)", |
| 664 | 674 | connection: http.Connection = .keep_alive, |
| 665 | 675 | transfer_encoding: RequestTransfer = .none, |
| 666 | 676 | |
| ... | ... | @@ -668,6 +678,7 @@ pub const Request = struct { |
| 668 | 678 | }; |
| 669 | 679 | |
| 670 | 680 | pub const Options = struct { |
| 681 | handle_redirects: bool = true, |
| 671 | 682 | max_redirects: u32 = 3, |
| 672 | 683 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| 673 | 684 | |
| ... | ... | @@ -703,10 +714,11 @@ pub const Request = struct { |
| 703 | 714 | req.client.release(req.connection); |
| 704 | 715 | } |
| 705 | 716 | |
| 717 | req.arena.deinit(); |
| 706 | 718 | req.* = undefined; |
| 707 | 719 | } |
| 708 | 720 | |
| 709 | | const ReadRawError = Connection.ReadError || std.Uri.ParseError || RequestError || error{ |
| 721 | const ReadRawError = Connection.ReadError || Uri.ParseError || RequestError || error{ |
| 710 | 722 | UnexpectedEndOfStream, |
| 711 | 723 | TooManyHttpRedirects, |
| 712 | 724 | HttpRedirectMissingLocation, |
| ... | ... | @@ -723,9 +735,7 @@ pub const Request = struct { |
| 723 | 735 | var index: usize = 0; |
| 724 | 736 | while (index == 0) { |
| 725 | 737 | const amt = try req.readRawAdvanced(buffer[index..]); |
| 726 | | const zero_means_end = req.response.done and req.response.headers.status.class() != .redirect; |
| 727 | | |
| 728 | | if (amt == 0 and zero_means_end) break; |
| 738 | if (amt == 0 and req.response.done) break; |
| 729 | 739 | index += amt; |
| 730 | 740 | } |
| 731 | 741 | |
| ... | ... | @@ -769,6 +779,8 @@ pub const Request = struct { |
| 769 | 779 | } |
| 770 | 780 | } else if (req.response.headers.content_length) |content_length| { |
| 771 | 781 | req.response.next_chunk_length = content_length; |
| 782 | |
| 783 | if (content_length == 0) req.response.done = true; |
| 772 | 784 | } else { |
| 773 | 785 | req.response.done = true; |
| 774 | 786 | } |
| ... | ... | @@ -779,7 +791,7 @@ pub const Request = struct { |
| 779 | 791 | return 0; |
| 780 | 792 | } |
| 781 | 793 | |
| 782 | | pub const WaitForCompleteHeadError = ReadRawError || error { |
| 794 | pub const WaitForCompleteHeadError = ReadRawError || error{ |
| 783 | 795 | UnexpectedEndOfStream, |
| 784 | 796 | |
| 785 | 797 | HttpHeadersExceededSizeLimit, |
| ... | ... | @@ -810,27 +822,8 @@ pub const Request = struct { |
| 810 | 822 | |
| 811 | 823 | /// This one can return 0 without meaning EOF. |
| 812 | 824 | fn readRawAdvanced(req: *Request, buffer: []u8) !usize { |
| 813 | | if (req.response.done) { |
| 814 | | if (req.response.headers.status.class() == .redirect) { |
| 815 | | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 816 | | |
| 817 | | const location = req.response.headers.location orelse |
| 818 | | return error.HttpRedirectMissingLocation; |
| 819 | | const new_url = try std.Uri.parse(location); |
| 820 | | const new_req = try req.client.request(new_url, req.headers, .{ |
| 821 | | .max_redirects = req.redirects_left - 1, |
| 822 | | .header_strategy = if (req.response.header_bytes_owned) .{ |
| 823 | | .dynamic = req.response.max_header_bytes, |
| 824 | | } else .{ |
| 825 | | .static = req.response.header_bytes.unusedCapacitySlice(), |
| 826 | | }, |
| 827 | | }); |
| 828 | | req.deinit(); |
| 829 | | req.* = new_req; |
| 830 | | } else { |
| 831 | | return 0; |
| 832 | | } |
| 833 | | } |
| 825 | assert(req.response.state.isContent()); |
| 826 | if (req.response.done) return 0; |
| 834 | 827 | |
| 835 | 828 | // var in: []const u8 = undefined; |
| 836 | 829 | if (req.read_buffer_start == req.read_buffer_len) { |
| ... | ... | @@ -851,7 +844,7 @@ pub const Request = struct { |
| 851 | 844 | const data_avail = req.response.next_chunk_length; |
| 852 | 845 | const out_avail = buffer.len; |
| 853 | 846 | |
| 854 | | if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) { |
| 847 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { |
| 855 | 848 | const can_read = @intCast(usize, @min(buf_avail, data_avail)); |
| 856 | 849 | req.response.next_chunk_length -= can_read; |
| 857 | 850 | |
| ... | ... | @@ -859,7 +852,6 @@ pub const Request = struct { |
| 859 | 852 | req.client.release(req.connection); |
| 860 | 853 | req.connection = undefined; |
| 861 | 854 | req.response.done = true; |
| 862 | | continue; |
| 863 | 855 | } |
| 864 | 856 | |
| 865 | 857 | return 0; // skip over as much data as possible |
| ... | ... | @@ -943,7 +935,7 @@ pub const Request = struct { |
| 943 | 935 | const data_avail = req.response.next_chunk_length; |
| 944 | 936 | const out_avail = buffer.len - out_index; |
| 945 | 937 | |
| 946 | | if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) { |
| 938 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { |
| 947 | 939 | const can_read = @intCast(usize, @min(buf_avail, data_avail)); |
| 948 | 940 | req.response.next_chunk_length -= can_read; |
| 949 | 941 | |
| ... | ... | @@ -990,9 +982,41 @@ pub const Request = struct { |
| 990 | 982 | } |
| 991 | 983 | |
| 992 | 984 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { |
| 993 | | if (!req.response.state.isContent()) try req.waitForCompleteHead(); |
| 985 | while (true) { |
| 986 | if (!req.response.state.isContent()) try req.waitForCompleteHead(); |
| 987 | |
| 988 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { |
| 989 | assert(try req.readRaw(buffer) == 0); |
| 990 | |
| 991 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 992 | |
| 993 | const location = req.response.headers.location orelse |
| 994 | return error.HttpRedirectMissingLocation; |
| 995 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); |
| 996 | |
| 997 | var new_arena = std.heap.ArenaAllocator.init(req.client.allocator); |
| 998 | const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator()); |
| 999 | errdefer new_arena.deinit(); |
| 1000 | |
| 1001 | req.arena.deinit(); |
| 1002 | req.arena = new_arena; |
| 1003 | |
| 1004 | const new_req = try req.client.request(resolved_url, req.headers, .{ |
| 1005 | .max_redirects = req.redirects_left - 1, |
| 1006 | .header_strategy = if (req.response.header_bytes_owned) .{ |
| 1007 | .dynamic = req.response.max_header_bytes, |
| 1008 | } else .{ |
| 1009 | .static = req.response.header_bytes.unusedCapacitySlice(), |
| 1010 | }, |
| 1011 | }); |
| 1012 | req.deinit(); |
| 1013 | req.* = new_req; |
| 1014 | } else { |
| 1015 | break; |
| 1016 | } |
| 1017 | } |
| 994 | 1018 | |
| 995 | | if (req.response.compression == .none and req.response.state.isContent()) { |
| 1019 | if (req.response.compression == .none) { |
| 996 | 1020 | if (req.response.headers.transfer_compression) |compression| { |
| 997 | 1021 | switch (compression) { |
| 998 | 1022 | .compress => unreachable, |
| ... | ... | @@ -1084,6 +1108,8 @@ pub const Request = struct { |
| 1084 | 1108 | }; |
| 1085 | 1109 | |
| 1086 | 1110 | pub fn deinit(client: *Client) void { |
| 1111 | client.connection_mutex.lock(); |
| 1112 | |
| 1087 | 1113 | var next = client.connection_pool.first; |
| 1088 | 1114 | while (next) |node| { |
| 1089 | 1115 | next = node.next; |
| ... | ... | @@ -1106,7 +1132,7 @@ pub fn deinit(client: *Client) void { |
| 1106 | 1132 | client.* = undefined; |
| 1107 | 1133 | } |
| 1108 | 1134 | |
| 1109 | | pub const ConnectError = std.mem.Allocator.Error || std.net.TcpConnectToHostError || std.crypto.tls.Client.InitError(std.net.Stream); |
| 1135 | pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); |
| 1110 | 1136 | |
| 1111 | 1137 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionNode { |
| 1112 | 1138 | { // Search through the connection pool for a potential connection. |
| ... | ... | @@ -1120,7 +1146,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 1120 | 1146 | const same_protocol = node.data.protocol == protocol; |
| 1121 | 1147 | |
| 1122 | 1148 | if (same_host and same_port and same_protocol) { |
| 1123 | | client.acquire(node); |
| 1149 | client.acquire(node, true); |
| 1124 | 1150 | return node; |
| 1125 | 1151 | } |
| 1126 | 1152 | |
| ... | ... | @@ -1168,6 +1194,7 @@ pub const RequestError = ConnectError || Connection.WriteError || error{ |
| 1168 | 1194 | InvalidPadding, |
| 1169 | 1195 | MissingEndCertificateMarker, |
| 1170 | 1196 | Unseekable, |
| 1197 | EndOfStream, |
| 1171 | 1198 | }; |
| 1172 | 1199 | |
| 1173 | 1200 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) RequestError!Request { |
| ... | ... | @@ -1196,27 +1223,52 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req |
| 1196 | 1223 | } |
| 1197 | 1224 | |
| 1198 | 1225 | var req: Request = .{ |
| 1226 | .uri = uri, |
| 1199 | 1227 | .client = client, |
| 1200 | 1228 | .headers = headers, |
| 1201 | 1229 | .connection = try client.connect(host, port, protocol), |
| 1202 | 1230 | .redirects_left = options.max_redirects, |
| 1231 | .handle_redirects = options.handle_redirects, |
| 1232 | .compression_init = false, |
| 1203 | 1233 | .response = switch (options.header_strategy) { |
| 1204 | 1234 | .dynamic => |max| Request.Response.initDynamic(max), |
| 1205 | 1235 | .static => |buf| Request.Response.initStatic(buf), |
| 1206 | 1236 | }, |
| 1237 | .arena = undefined, |
| 1207 | 1238 | }; |
| 1208 | 1239 | |
| 1240 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 1241 | |
| 1209 | 1242 | { |
| 1210 | 1243 | var buffered = std.io.bufferedWriter(req.connection.data.writer()); |
| 1211 | 1244 | const writer = buffered.writer(); |
| 1212 | 1245 | |
| 1246 | const escaped_path = try Uri.escapePath(client.allocator, uri.path); |
| 1247 | defer client.allocator.free(escaped_path); |
| 1248 | |
| 1249 | const escaped_query = if (uri.query) |q| try Uri.escapeQuery(client.allocator, q) else null; |
| 1250 | defer if (escaped_query) |q| client.allocator.free(q); |
| 1251 | |
| 1252 | const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(client.allocator, f) else null; |
| 1253 | defer if (escaped_fragment) |f| client.allocator.free(f); |
| 1254 | |
| 1213 | 1255 | try writer.writeAll(@tagName(headers.method)); |
| 1214 | 1256 | try writer.writeByte(' '); |
| 1215 | | try writer.writeAll(uri.path); |
| 1257 | try writer.writeAll(escaped_path); |
| 1258 | if (escaped_query) |q| { |
| 1259 | try writer.writeByte('?'); |
| 1260 | try writer.writeAll(q); |
| 1261 | } |
| 1262 | if (escaped_fragment) |f| { |
| 1263 | try writer.writeByte('#'); |
| 1264 | try writer.writeAll(f); |
| 1265 | } |
| 1216 | 1266 | try writer.writeByte(' '); |
| 1217 | 1267 | try writer.writeAll(@tagName(headers.version)); |
| 1218 | 1268 | try writer.writeAll("\r\nHost: "); |
| 1219 | 1269 | try writer.writeAll(host); |
| 1270 | try writer.writeAll("\r\nUser-Agent: "); |
| 1271 | try writer.writeAll(headers.user_agent); |
| 1220 | 1272 | if (headers.connection == .close) { |
| 1221 | 1273 | try writer.writeAll("\r\nConnection: close"); |
| 1222 | 1274 | } else { |