authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-10-03 14:26:06-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-10-21 20:52:58-05:00
loge1c37f70d4ae9a7bfa6de92dcb26e7cfdffc17c2
tree26aa7274ade1c3fd5ffecdb477e0f83dd606b25b
parent1afeada2d95e50efe651bd6227719ca4003dad96
signaturelock-open Commit is signed but in an unrecognized format.

std.http.Client: store *Connection instead of a pool node, buffer writes


4 files changed, 110 insertions(+), 96 deletions(-)

lib/std/crypto/tls/Client.zig+1-1
......@@ -881,7 +881,7 @@ pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize {
881881/// The `iovecs` parameter is mutable because this function needs to mutate the fields in
882882/// order to handle partial reads from the underlying stream layer.
883883pub fn readv(c: *Client, stream: anytype, iovecs: []std.os.iovec) !usize {
884 return readvAtLeast(c, stream, iovecs);
884 return readvAtLeast(c, stream, iovecs, 1);
885885}
886886
887887/// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`.
lib/std/http/Client.zig+105-91
......@@ -54,7 +54,7 @@ pub const ConnectionPool = struct {
5454
5555 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.
5656 /// If no connection is found, null is returned.
57 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Node {
57 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection {
5858 pool.mutex.lock();
5959 defer pool.mutex.unlock();
6060
......@@ -65,7 +65,7 @@ pub const ConnectionPool = struct {
6565 if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue;
6666
6767 pool.acquireUnsafe(node);
68 return node;
68 return &node.data;
6969 }
7070
7171 return null;
......@@ -89,10 +89,12 @@ pub const ConnectionPool = struct {
8989
9090 /// Tries to release a connection back to the connection pool. This function is threadsafe.
9191 /// If the connection is marked as closing, it will be closed instead.
92 pub fn release(pool: *ConnectionPool, allocator: Allocator, node: *Node) void {
92 pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void {
9393 pool.mutex.lock();
9494 defer pool.mutex.unlock();
9595
96 const node = @fieldParentPtr(Node, "data", connection);
97
9698 pool.used.remove(node);
9799
98100 if (node.data.closing or pool.free_size == 0) {
......@@ -151,6 +153,8 @@ pub const ConnectionPool = struct {
151153/// An interface to either a plain or TLS connection.
152154pub const Connection = struct {
153155 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
156 const BufferSize = std.math.IntFittingRange(0, buffer_size);
157
154158 pub const Protocol = enum { plain, tls };
155159
156160 stream: net.Stream,
......@@ -164,14 +168,16 @@ pub const Connection = struct {
164168 proxied: bool = false,
165169 closing: bool = false,
166170
167 read_start: u16 = 0,
168 read_end: u16 = 0,
171 read_start: BufferSize = 0,
172 read_end: BufferSize = 0,
173 write_end: BufferSize = 0,
169174 read_buf: [buffer_size]u8 = undefined,
175 write_buf: [buffer_size]u8 = undefined,
170176
171 pub fn rawReadAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
177 pub fn readvDirect(conn: *Connection, buffers: []std.os.iovec) ReadError!usize {
172178 return switch (conn.protocol) {
173 .plain => conn.stream.readAtLeast(buffer, len),
174 .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len),
179 .plain => conn.stream.readv(buffers),
180 .tls => conn.tls_client.readv(conn.stream, buffers),
175181 } catch |err| {
176182 // TODO: https://github.com/ziglang/zig/issues/2473
177183 if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert;
......@@ -188,58 +194,52 @@ pub const Connection = struct {
188194 pub fn fill(conn: *Connection) ReadError!void {
189195 if (conn.read_end != conn.read_start) return;
190196
191 const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1);
197 var iovecs = [1]std.os.iovec{
198 .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
199 };
200 const nread = try conn.readvDirect(&iovecs);
192201 if (nread == 0) return error.EndOfStream;
193202 conn.read_start = 0;
194 conn.read_end = @as(u16, @intCast(nread));
203 conn.read_end = @intCast(nread);
195204 }
196205
197206 pub fn peek(conn: *Connection) []const u8 {
198207 return conn.read_buf[conn.read_start..conn.read_end];
199208 }
200209
201 pub fn drop(conn: *Connection, num: u16) void {
210 pub fn drop(conn: *Connection, num: BufferSize) void {
202211 conn.read_start += num;
203212 }
204213
205 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
206 assert(len <= buffer.len);
207
208 var out_index: u16 = 0;
209 while (out_index < len) {
210 const available_read = conn.read_end - conn.read_start;
211 const available_buffer = buffer.len - out_index;
212
213 if (available_read > available_buffer) { // partially read buffered data
214 @memcpy(buffer[out_index..], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
215 out_index += @as(u16, @intCast(available_buffer));
216 conn.read_start += @as(u16, @intCast(available_buffer));
214 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
215 const available_read = conn.read_end - conn.read_start;
216 const available_buffer = buffer.len;
217217
218 break;
219 } else if (available_read > 0) { // fully read buffered data
220 @memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]);
221 out_index += available_read;
222 conn.read_start += available_read;
218 if (available_read > available_buffer) { // partially read buffered data
219 @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);
220 conn.read_start += @intCast(available_buffer);
223221
224 if (out_index >= len) break;
225 }
222 return available_buffer;
223 } else if (available_read > 0) { // fully read buffered data
224 @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]);
225 conn.read_start += available_read;
226226
227 const leftover_buffer = available_buffer - available_read;
228 const leftover_len = len - out_index;
227 return available_read;
228 }
229229
230 if (leftover_buffer > conn.read_buf.len) {
231 // skip the buffer if the output is large enough
232 return conn.rawReadAtLeast(buffer[out_index..], leftover_len);
233 }
230 var iovecs = [2]std.os.iovec{
231 .{ .iov_base = buffer.ptr, .iov_len = buffer.len },
232 .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len },
233 };
234 const nread = try conn.readvDirect(&iovecs);
234235
235 try conn.fill();
236 if (nread > buffer.len) {
237 conn.read_start = 0;
238 conn.read_end = @intCast(nread - buffer.len);
239 return buffer.len;
236240 }
237241
238 return out_index;
239 }
240
241 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
242 return conn.readAtLeast(buffer, 1);
242 return nread;
243243 }
244244
245245 pub const ReadError = error{
......@@ -257,7 +257,7 @@ pub const Connection = struct {
257257 return Reader{ .context = conn };
258258 }
259259
260 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
260 pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void {
261261 return switch (conn.protocol) {
262262 .plain => conn.stream.writeAll(buffer),
263263 .tls => conn.tls_client.writeAll(conn.stream, buffer),
......@@ -267,14 +267,27 @@ pub const Connection = struct {
267267 };
268268 }
269269
270 pub fn write(conn: *Connection, buffer: []const u8) !usize {
271 return switch (conn.protocol) {
272 .plain => conn.stream.write(buffer),
273 .tls => conn.tls_client.write(conn.stream, buffer),
274 } catch |err| switch (err) {
275 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
276 else => return error.UnexpectedWriteFailure,
277 };
270 pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
271 if (conn.write_end + buffer.len > conn.write_buf.len) {
272 try conn.flush();
273
274 if (buffer.len > conn.write_buf.len) {
275 try conn.writeAllDirect(buffer);
276 return buffer.len;
277 }
278 }
279
280 @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer);
281 conn.write_end += @intCast(buffer.len);
282
283 return buffer.len;
284 }
285
286 pub fn flush(conn: *Connection) WriteError!void {
287 if (conn.write_end == 0) return;
288
289 try conn.writeAllDirect(conn.write_buf[0..conn.write_end]);
290 conn.write_end = 0;
278291 }
279292
280293 pub const WriteError = error{
......@@ -455,7 +468,7 @@ pub const Request = struct {
455468 uri: Uri,
456469 client: *Client,
457470 /// is null when this connection is released
458 connection: ?*ConnectionPool.Node,
471 connection: ?*Connection,
459472
460473 method: http.Method,
461474 version: http.Version = .@"HTTP/1.1",
......@@ -489,7 +502,7 @@ pub const Request = struct {
489502 if (req.connection) |connection| {
490503 if (!req.response.parser.done) {
491504 // If the response wasn't fully read, then we need to close the connection.
492 connection.data.closing = true;
505 connection.closing = true;
493506 }
494507 req.client.connection_pool.release(req.client.allocator, connection);
495508 }
......@@ -548,8 +561,7 @@ pub const Request = struct {
548561 pub fn start(req: *Request, options: StartOptions) StartError!void {
549562 if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding;
550563
551 var buffered = std.io.bufferedWriter(req.connection.?.data.writer());
552 const w = buffered.writer();
564 const w = req.connection.?.writer();
553565
554566 try req.method.write(w);
555567 try w.writeByte(' ');
......@@ -558,9 +570,9 @@ pub const Request = struct {
558570 try req.uri.writeToStream(.{ .authority = true }, w);
559571 } else {
560572 try req.uri.writeToStream(.{
561 .scheme = req.connection.?.data.proxied,
562 .authentication = req.connection.?.data.proxied,
563 .authority = req.connection.?.data.proxied,
573 .scheme = req.connection.?.proxied,
574 .authentication = req.connection.?.proxied,
575 .authority = req.connection.?.proxied,
564576 .path = true,
565577 .query = true,
566578 .raw = options.raw_uri,
......@@ -629,8 +641,8 @@ pub const Request = struct {
629641 try w.writeAll("\r\n");
630642 }
631643
632 if (req.connection.?.data.proxied) {
633 const proxy_headers: ?http.Headers = switch (req.connection.?.data.protocol) {
644 if (req.connection.?.proxied) {
645 const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) {
634646 .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,
635647 .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,
636648 };
......@@ -649,7 +661,7 @@ pub const Request = struct {
649661
650662 try w.writeAll("\r\n");
651663
652 try buffered.flush();
664 try req.connection.?.flush();
653665 }
654666
655667 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
......@@ -665,7 +677,7 @@ pub const Request = struct {
665677
666678 var index: usize = 0;
667679 while (index == 0) {
668 const amt = try req.response.parser.read(&req.connection.?.data, buf[index..], req.response.skip);
680 const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip);
669681 if (amt == 0 and req.response.parser.done) break;
670682 index += amt;
671683 }
......@@ -683,10 +695,10 @@ pub const Request = struct {
683695 pub fn wait(req: *Request) WaitError!void {
684696 while (true) { // handle redirects
685697 while (true) { // read headers
686 try req.connection.?.data.fill();
698 try req.connection.?.fill();
687699
688 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek());
689 req.connection.?.data.drop(@as(u16, @intCast(nchecked)));
700 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
701 req.connection.?.drop(@intCast(nchecked));
690702
691703 if (req.response.parser.state.isContent()) break;
692704 }
......@@ -701,7 +713,7 @@ pub const Request = struct {
701713
702714 // we're switching protocols, so this connection is no longer doing http
703715 if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) {
704 req.connection.?.data.closing = false;
716 req.connection.?.closing = false;
705717 req.response.parser.done = true;
706718 }
707719
......@@ -712,9 +724,9 @@ pub const Request = struct {
712724 const res_connection = req.response.headers.getFirstValue("connection");
713725 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
714726 if (res_keepalive and (req_keepalive or req_connection == null)) {
715 req.connection.?.data.closing = false;
727 req.connection.?.closing = false;
716728 } else {
717 req.connection.?.data.closing = true;
729 req.connection.?.closing = true;
718730 }
719731
720732 if (req.response.transfer_encoding) |te| {
......@@ -827,10 +839,10 @@ pub const Request = struct {
827839 const has_trail = !req.response.parser.state.isContent();
828840
829841 while (!req.response.parser.state.isContent()) { // read trailing headers
830 try req.connection.?.data.fill();
842 try req.connection.?.fill();
831843
832 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek());
833 req.connection.?.data.drop(@as(u16, @intCast(nchecked)));
844 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek());
845 req.connection.?.drop(@intCast(nchecked));
834846 }
835847
836848 if (has_trail) {
......@@ -868,16 +880,16 @@ pub const Request = struct {
868880 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
869881 switch (req.transfer_encoding) {
870882 .chunked => {
871 try req.connection.?.data.writer().print("{x}\r\n", .{bytes.len});
872 try req.connection.?.data.writeAll(bytes);
873 try req.connection.?.data.writeAll("\r\n");
883 try req.connection.?.writer().print("{x}\r\n", .{bytes.len});
884 try req.connection.?.writer().writeAll(bytes);
885 try req.connection.?.writer().writeAll("\r\n");
874886
875887 return bytes.len;
876888 },
877889 .content_length => |*len| {
878890 if (len.* < bytes.len) return error.MessageTooLong;
879891
880 const amt = try req.connection.?.data.write(bytes);
892 const amt = try req.connection.?.write(bytes);
881893 len.* -= amt;
882894 return amt;
883895 },
......@@ -897,10 +909,12 @@ pub const Request = struct {
897909 /// Finish the body of a request. This notifies the server that you have no more data to send.
898910 pub fn finish(req: *Request) FinishError!void {
899911 switch (req.transfer_encoding) {
900 .chunked => try req.connection.?.data.writeAll("0\r\n\r\n"),
912 .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"),
901913 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
902914 .none => {},
903915 }
916
917 try req.connection.?.flush();
904918 }
905919};
906920
......@@ -1024,7 +1038,7 @@ pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, Network
10241038
10251039/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
10261040/// This function is threadsafe.
1027pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*ConnectionPool.Node {
1041pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection {
10281042 if (client.connection_pool.findConnection(.{
10291043 .host = host,
10301044 .port = port,
......@@ -1074,12 +1088,12 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
10741088
10751089 client.connection_pool.addUsed(conn);
10761090
1077 return conn;
1091 return &conn.data;
10781092}
10791093
10801094pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError;
10811095
1082pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node {
1096pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection {
10831097 if (!net.has_unix_sockets) return error.Unsupported;
10841098
10851099 if (client.connection_pool.findConnection(.{
......@@ -1108,7 +1122,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
11081122
11091123 client.connection_pool.addUsed(conn);
11101124
1111 return conn;
1125 return &conn.data;
11121126}
11131127
11141128pub fn connectTunnel(
......@@ -1116,7 +1130,7 @@ pub fn connectTunnel(
11161130 proxy: *ProxyInformation,
11171131 tunnel_host: []const u8,
11181132 tunnel_port: u16,
1119) !*ConnectionPool.Node {
1133) !*Connection {
11201134 if (!proxy.supports_connect) return error.TunnelNotSupported;
11211135
11221136 if (client.connection_pool.findConnection(.{
......@@ -1130,7 +1144,7 @@ pub fn connectTunnel(
11301144 _ = tunnel: {
11311145 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
11321146 errdefer {
1133 conn.data.closing = true;
1147 conn.closing = true;
11341148 client.connection_pool.release(client.allocator, conn);
11351149 }
11361150
......@@ -1171,12 +1185,12 @@ pub fn connectTunnel(
11711185 // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized.
11721186 req.connection = null;
11731187
1174 client.allocator.free(conn.data.host);
1175 conn.data.host = try client.allocator.dupe(u8, tunnel_host);
1176 errdefer client.allocator.free(conn.data.host);
1188 client.allocator.free(conn.host);
1189 conn.host = try client.allocator.dupe(u8, tunnel_host);
1190 errdefer client.allocator.free(conn.host);
11771191
1178 conn.data.port = tunnel_port;
1179 conn.data.closing = false;
1192 conn.port = tunnel_port;
1193 conn.closing = false;
11801194
11811195 return conn;
11821196 } catch {
......@@ -1190,7 +1204,7 @@ pub fn connectTunnel(
11901204const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
11911205pub const ConnectError = ConnectErrorPartial || RequestError;
11921206
1193pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
1207pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection {
11941208 // pointer required so that `supports_connect` can be updated if a CONNECT fails
11951209 const potential_proxy: ?*ProxyInformation = switch (protocol) {
11961210 .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,
......@@ -1213,11 +1227,11 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
12131227 // fall back to using the proxy as a normal http proxy
12141228 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
12151229 errdefer {
1216 conn.data.closing = true;
1230 conn.closing = true;
12171231 client.connection_pool.release(conn);
12181232 }
12191233
1220 conn.data.proxied = true;
1234 conn.proxied = true;
12211235 return conn;
12221236 }
12231237
......@@ -1240,7 +1254,7 @@ pub const RequestOptions = struct {
12401254 header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 },
12411255
12421256 /// Must be an already acquired connection.
1243 connection: ?*ConnectionPool.Node = null,
1257 connection: ?*Connection = null,
12441258
12451259 pub const StorageStrategy = union(enum) {
12461260 /// In this case, the client's Allocator will be used to store the
lib/std/http/protocol.zig+3-3
......@@ -529,7 +529,7 @@ pub const HeadersParser = struct {
529529 try conn.fill();
530530
531531 const nread = @min(conn.peek().len, data_avail);
532 conn.drop(@as(u16, @intCast(nread)));
532 conn.drop(@intCast(nread));
533533 r.next_chunk_length -= nread;
534534
535535 if (r.next_chunk_length == 0) r.done = true;
......@@ -553,7 +553,7 @@ pub const HeadersParser = struct {
553553 try conn.fill();
554554
555555 const i = r.findChunkedLen(conn.peek());
556 conn.drop(@as(u16, @intCast(i)));
556 conn.drop(@intCast(i));
557557
558558 switch (r.state) {
559559 .invalid => return error.HttpChunkInvalid,
......@@ -582,7 +582,7 @@ pub const HeadersParser = struct {
582582 try conn.fill();
583583
584584 const nread = @min(conn.peek().len, data_avail);
585 conn.drop(@as(u16, @intCast(nread)));
585 conn.drop(@intCast(nread));
586586 r.next_chunk_length -= nread;
587587 } else if (out_avail > 0) {
588588 const can_read: usize = @intCast(@min(data_avail, out_avail));
test/standalone/http.zig+1-1
......@@ -680,7 +680,7 @@ pub fn main() !void {
680680 for (0..total_connections) |i| {
681681 var req = try client.request(.GET, uri, .{ .allocator = calloc }, .{});
682682 req.response.parser.done = true;
683 req.connection.?.data.closing = false;
683 req.connection.?.closing = false;
684684 requests[i] = req;
685685 }
686686