authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-10-02 19:57:43-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-10-21 20:52:58-05:00
log1afeada2d95e50efe651bd6227719ca4003dad96
tree0799938787b302420653064771942bb589546b36
parent7d50634e0ad4355e339bc243a2e2842693e133f9
signaturelock-open Commit is signed but in an unrecognized format.

std.http.Client: enhance proxy support

adds connectTunnel to form a HTTP CONNECT tunnel to the desired host. Primarily implemented for proxies, but like connectUnix may be called by any user. adds loadDefaultProxies to load proxy information from common environment variables (http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, all_proxy, ALL_PROXY). - no_proxy and NO_PROXY are currently unsupported. splits proxy into http_proxy and https_proxy, adds headers field for arbitrary headers to each proxy.

3 files changed, 357 insertions(+), 115 deletions(-)

lib/std/Uri.zig+79-33
......@@ -208,24 +208,45 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
208208 return uri;
209209}
210210
211pub fn format(
211pub const WriteToStreamOptions = struct {
212 /// When true, include the scheme part of the URI.
213 scheme: bool = false,
214
215 /// When true, include the user and password part of the URI. Ignored if `authority` is false.
216 authentication: bool = false,
217
218 /// When true, include the authority part of the URI.
219 authority: bool = false,
220
221 /// When true, include the path part of the URI.
222 path: bool = false,
223
224 /// When true, include the query part of the URI. Ignored when `path` is false.
225 query: bool = false,
226
227 /// When true, include the fragment part of the URI. Ignored when `path` is false.
228 fragment: bool = false,
229
230 /// When true, do not escape any part of the URI.
231 raw: bool = false,
232};
233
234pub fn writeToStream(
212235 uri: Uri,
213 comptime fmt: []const u8,
214 options: std.fmt.FormatOptions,
236 options: WriteToStreamOptions,
215237 writer: anytype,
216238) @TypeOf(writer).Error!void {
217 _ = options;
218
219 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
220 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
221 const raw_uri = comptime std.mem.indexOf(u8, fmt, "r") != null;
222 const needs_fragment = comptime std.mem.indexOf(u8, fmt, "#") != null;
223
224 if (needs_absolute) {
239 if (options.scheme) {
225240 try writer.writeAll(uri.scheme);
226241 try writer.writeAll(":");
227 if (uri.host) |host| {
242
243 if (options.authority and uri.host != null) {
228244 try writer.writeAll("//");
245 }
246 }
247
248 if (options.authority) {
249 if (options.authentication and uri.host != null) {
229250 if (uri.user) |user| {
230251 try writer.writeAll(user);
231252 if (uri.password) |password| {
......@@ -234,7 +255,9 @@ pub fn format(
234255 }
235256 try writer.writeAll("@");
236257 }
258 }
237259
260 if (uri.host) |host| {
238261 try writer.writeAll(host);
239262
240263 if (uri.port) |port| {
......@@ -244,39 +267,62 @@ pub fn format(
244267 }
245268 }
246269
247 if (needs_path) {
270 if (options.path) {
248271 if (uri.path.len == 0) {
249272 try writer.writeAll("/");
273 } else if (options.raw) {
274 try writer.writeAll(uri.path);
250275 } else {
251 if (raw_uri) {
252 try writer.writeAll(uri.path);
253 } else {
254 try Uri.writeEscapedPath(writer, uri.path);
255 }
276 try writeEscapedPath(writer, uri.path);
256277 }
257278
258 if (uri.query) |q| {
279 if (options.query) if (uri.query) |q| {
259280 try writer.writeAll("?");
260 if (raw_uri) {
281 if (options.raw) {
261282 try writer.writeAll(q);
262283 } else {
263 try Uri.writeEscapedQuery(writer, q);
284 try writeEscapedQuery(writer, q);
264285 }
265 }
286 };
266287
267 if (needs_fragment) {
268 if (uri.fragment) |f| {
269 try writer.writeAll("#");
270 if (raw_uri) {
271 try writer.writeAll(f);
272 } else {
273 try Uri.writeEscapedQuery(writer, f);
274 }
288 if (options.fragment) if (uri.fragment) |f| {
289 try writer.writeAll("#");
290 if (options.raw) {
291 try writer.writeAll(f);
292 } else {
293 try writeEscapedQuery(writer, f);
275294 }
276 }
295 };
277296 }
278297}
279298
299pub fn format(
300 uri: Uri,
301 comptime fmt: []const u8,
302 options: std.fmt.FormatOptions,
303 writer: anytype,
304) @TypeOf(writer).Error!void {
305 _ = options;
306
307 const scheme = comptime std.mem.indexOf(u8, fmt, ":") != null or fmt.len == 0;
308 const authentication = comptime std.mem.indexOf(u8, fmt, "@") != null or fmt.len == 0;
309 const authority = comptime std.mem.indexOf(u8, fmt, "+") != null or fmt.len == 0;
310 const path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
311 const query = comptime std.mem.indexOf(u8, fmt, "?") != null or fmt.len == 0;
312 const fragment = comptime std.mem.indexOf(u8, fmt, "#") != null or fmt.len == 0;
313 const raw = comptime std.mem.indexOf(u8, fmt, "r") != null or fmt.len == 0;
314
315 return writeToStream(uri, .{
316 .scheme = scheme,
317 .authentication = authentication,
318 .authority = authority,
319 .path = path,
320 .query = query,
321 .fragment = fragment,
322 .raw = raw,
323 }, writer);
324}
325
280326/// Parses the URI or returns an error.
281327/// The return value will contain unescaped strings pointing into the
282328/// original `text`. Each component that is provided, will be non-`null`.
......@@ -709,7 +755,7 @@ test "URI query escaping" {
709755 const parsed = try Uri.parse(address);
710756
711757 // format the URI to escape it
712 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{}", .{parsed});
758 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{/?}", .{parsed});
713759 defer std.testing.allocator.free(formatted_uri);
714760 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
715761}
......@@ -727,6 +773,6 @@ test "format" {
727773 };
728774 var buf = std.ArrayList(u8).init(std.testing.allocator);
729775 defer buf.deinit();
730 try uri.format("+/", .{}, buf.writer());
776 try uri.format(":/?#", .{}, buf.writer());
731777 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
732778}
lib/std/http/Client.zig+258-68
......@@ -18,6 +18,7 @@ pub const connection_pool_size = std.options.http_connection_pool_size;
1818allocator: Allocator,
1919ca_bundle: std.crypto.Certificate.Bundle = .{},
2020ca_bundle_mutex: std.Thread.Mutex = .{},
21
2122/// When this is `true`, the next time this client performs an HTTPS request,
2223/// it will first rescan the system for root certificates.
2324next_https_rescan_certs: bool = true,
......@@ -25,7 +26,11 @@ next_https_rescan_certs: bool = true,
2526/// The pool of connections that can be reused (and currently in use).
2627connection_pool: ConnectionPool = .{},
2728
28proxy: ?HttpProxy = null,
29/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections.
30http_proxy: ?ProxyInformation = null,
31
32/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections.
33https_proxy: ?ProxyInformation = null,
2934
3035/// A set of linked lists of connections that can be reused.
3136pub const ConnectionPool = struct {
......@@ -33,7 +38,7 @@ pub const ConnectionPool = struct {
3338 pub const Criteria = struct {
3439 host: []const u8,
3540 port: u16,
36 is_tls: bool,
41 protocol: Connection.Protocol,
3742 };
3843
3944 const Queue = std.DoublyLinkedList(Connection);
......@@ -55,9 +60,9 @@ pub const ConnectionPool = struct {
5560
5661 var next = pool.free.last;
5762 while (next) |node| : (next = node.prev) {
58 if ((node.data.protocol == .tls) != criteria.is_tls) continue;
63 if (node.data.protocol != criteria.protocol) continue;
5964 if (node.data.port != criteria.port) continue;
60 if (!mem.eql(u8, node.data.host, criteria.host)) continue;
65 if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue;
6166
6267 pool.acquireUnsafe(node);
6368 return node;
......@@ -84,23 +89,23 @@ pub const ConnectionPool = struct {
8489
8590 /// Tries to release a connection back to the connection pool. This function is threadsafe.
8691 /// If the connection is marked as closing, it will be closed instead.
87 pub fn release(pool: *ConnectionPool, client: *Client, node: *Node) void {
92 pub fn release(pool: *ConnectionPool, allocator: Allocator, node: *Node) void {
8893 pool.mutex.lock();
8994 defer pool.mutex.unlock();
9095
9196 pool.used.remove(node);
9297
93 if (node.data.closing) {
94 node.data.deinit(client);
95 return client.allocator.destroy(node);
98 if (node.data.closing or pool.free_size == 0) {
99 node.data.close(allocator);
100 return allocator.destroy(node);
96101 }
97102
98103 if (pool.free_len >= pool.free_size) {
99104 const popped = pool.free.popFirst() orelse unreachable;
100105 pool.free_len -= 1;
101106
102 popped.data.deinit(client);
103 client.allocator.destroy(popped);
107 popped.data.close(allocator);
108 allocator.destroy(popped);
104109 }
105110
106111 if (node.data.proxied) {
......@@ -128,7 +133,7 @@ pub const ConnectionPool = struct {
128133 defer client.allocator.destroy(node);
129134 next = node.next;
130135
131 node.data.deinit(client);
136 node.data.close(client.allocator);
132137 }
133138
134139 next = pool.used.first;
......@@ -136,7 +141,7 @@ pub const ConnectionPool = struct {
136141 defer client.allocator.destroy(node);
137142 next = node.next;
138143
139 node.data.deinit(client);
144 node.data.close(client.allocator);
140145 }
141146
142147 pool.* = undefined;
......@@ -283,19 +288,15 @@ pub const Connection = struct {
283288 return Writer{ .context = conn };
284289 }
285290
286 pub fn close(conn: *Connection, client: *const Client) void {
291 pub fn close(conn: *Connection, allocator: Allocator) void {
287292 if (conn.protocol == .tls) {
288293 // try to cleanly close the TLS connection, for any server that cares.
289294 _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
290 client.allocator.destroy(conn.tls_client);
295 allocator.destroy(conn.tls_client);
291296 }
292297
293298 conn.stream.close();
294 }
295
296 pub fn deinit(conn: *Connection, client: *const Client) void {
297 conn.close(client);
298 client.allocator.free(conn.host);
299 allocator.free(conn.host);
299300 }
300301};
301302
......@@ -490,7 +491,7 @@ pub const Request = struct {
490491 // If the response wasn't fully read, then we need to close the connection.
491492 connection.data.closing = true;
492493 }
493 req.client.connection_pool.release(req.client, connection);
494 req.client.connection_pool.release(req.client.allocator, connection);
494495 }
495496
496497 req.arena.deinit();
......@@ -509,7 +510,7 @@ pub const Request = struct {
509510 .zstd => |*zstd| zstd.deinit(),
510511 }
511512
512 req.client.connection_pool.release(req.client, req.connection.?);
513 req.client.connection_pool.release(req.client.allocator, req.connection.?);
513514 req.connection = null;
514515
515516 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
......@@ -554,24 +555,16 @@ pub const Request = struct {
554555 try w.writeByte(' ');
555556
556557 if (req.method == .CONNECT) {
557 try w.writeAll(req.uri.host.?);
558 try w.writeByte(':');
559 try w.print("{}", .{req.uri.port.?});
558 try req.uri.writeToStream(.{ .authority = true }, w);
560559 } else {
561 if (req.connection.?.data.proxied) {
562 // proxied connections require the full uri
563 if (options.raw_uri) {
564 try w.print("{+/r}", .{req.uri});
565 } else {
566 try w.print("{+/}", .{req.uri});
567 }
568 } else {
569 if (options.raw_uri) {
570 try w.print("{/r}", .{req.uri});
571 } else {
572 try w.print("{/}", .{req.uri});
573 }
574 }
560 try req.uri.writeToStream(.{
561 .scheme = req.connection.?.data.proxied,
562 .authentication = req.connection.?.data.proxied,
563 .authority = req.connection.?.data.proxied,
564 .path = true,
565 .query = true,
566 .raw = options.raw_uri,
567 }, w);
575568 }
576569 try w.writeByte(' ');
577570 try w.writeAll(@tagName(req.version));
......@@ -579,7 +572,7 @@ pub const Request = struct {
579572
580573 if (!req.headers.contains("host")) {
581574 try w.writeAll("Host: ");
582 try w.writeAll(req.uri.host.?);
575 try req.uri.writeToStream(.{ .authority = true }, w);
583576 try w.writeAll("\r\n");
584577 }
585578
......@@ -636,6 +629,24 @@ pub const Request = struct {
636629 try w.writeAll("\r\n");
637630 }
638631
632 if (req.connection.?.data.proxied) {
633 const proxy_headers: ?http.Headers = switch (req.connection.?.data.protocol) {
634 .plain => if (req.client.http_proxy) |proxy| proxy.headers else null,
635 .tls => if (req.client.https_proxy) |proxy| proxy.headers else null,
636 };
637
638 if (proxy_headers) |headers| {
639 for (headers.list.items) |entry| {
640 if (entry.value.len == 0) continue;
641
642 try w.writeAll(entry.name);
643 try w.writeAll(": ");
644 try w.writeAll(entry.value);
645 try w.writeAll("\r\n");
646 }
647 }
648 }
649
639650 try w.writeAll("\r\n");
640651
641652 try buffered.flush();
......@@ -893,18 +904,15 @@ pub const Request = struct {
893904 }
894905};
895906
896pub const HttpProxy = struct {
897 pub const ProxyAuthentication = union(enum) {
898 basic: []const u8,
899 custom: []const u8,
900 };
907pub const ProxyInformation = struct {
908 allocator: Allocator,
909 headers: http.Headers,
901910
902911 protocol: Connection.Protocol,
903912 host: []const u8,
904 port: ?u16 = null,
913 port: u16,
905914
906 /// The value for the Proxy-Authorization header.
907 auth: ?ProxyAuthentication = null,
915 supports_connect: bool = true,
908916};
909917
910918/// Release all associated resources with the client.
......@@ -912,19 +920,115 @@ pub const HttpProxy = struct {
912920pub fn deinit(client: *Client) void {
913921 client.connection_pool.deinit(client);
914922
923 if (client.http_proxy) |*proxy| {
924 proxy.allocator.free(proxy.host);
925 proxy.headers.deinit();
926 }
927
928 if (client.https_proxy) |*proxy| {
929 proxy.allocator.free(proxy.host);
930 proxy.headers.deinit();
931 }
932
915933 client.ca_bundle.deinit(client.allocator);
916934 client.* = undefined;
917935}
918936
919pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
937/// Uses the *_proxy environment variable to set any unset proxies for the client.
938/// This function *must not* be called when the client has any active connections.
939pub fn loadDefaultProxies(client: *Client) !void {
940 if (client.http_proxy == null) http: {
941 const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy"))
942 try std.process.getEnvVarOwned(client.allocator, "http_proxy")
943 else if (std.process.hasEnvVarConstant("HTTP_PROXY"))
944 try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY")
945 else if (std.process.hasEnvVarConstant("all_proxy"))
946 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
947 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
948 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
949 else
950 break :http;
951 defer client.allocator.free(content);
952
953 const uri = try Uri.parse(content);
954
955 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
956 client.http_proxy = .{
957 .allocator = client.allocator,
958 .headers = .{ .allocator = client.allocator },
959
960 .protocol = protocol,
961 .host = if (uri.host) |host| try client.allocator.dupe(u8, host) else return error.UriMissingHost,
962 .port = uri.port orelse switch (protocol) {
963 .plain => 80,
964 .tls => 443,
965 },
966 };
967
968 if (uri.user != null and uri.password != null) {
969 const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
970 defer client.allocator.free(unencoded);
971
972 const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len));
973 defer client.allocator.free(buffer);
974
975 const result = std.base64.standard.Encoder.encode(buffer, unencoded);
976
977 try client.http_proxy.?.headers.append("proxy-authorization", result);
978 }
979 }
980
981 if (client.https_proxy == null) https: {
982 const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy"))
983 try std.process.getEnvVarOwned(client.allocator, "https_proxy")
984 else if (std.process.hasEnvVarConstant("HTTPS_PROXY"))
985 try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY")
986 else if (std.process.hasEnvVarConstant("all_proxy"))
987 try std.process.getEnvVarOwned(client.allocator, "all_proxy")
988 else if (std.process.hasEnvVarConstant("ALL_PROXY"))
989 try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY")
990 else
991 break :https;
992 defer client.allocator.free(content);
993
994 const uri = try Uri.parse(content);
995
996 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
997 client.http_proxy = .{
998 .allocator = client.allocator,
999 .headers = .{ .allocator = client.allocator },
1000
1001 .protocol = protocol,
1002 .host = if (uri.host) |host| try client.allocator.dupe(u8, host) else return error.UriMissingHost,
1003 .port = uri.port orelse switch (protocol) {
1004 .plain => 80,
1005 .tls => 443,
1006 },
1007 };
1008
1009 if (uri.user != null and uri.password != null) {
1010 const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? });
1011 defer client.allocator.free(unencoded);
1012
1013 const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len));
1014 defer client.allocator.free(buffer);
1015
1016 const result = std.base64.standard.Encoder.encode(buffer, unencoded);
1017
1018 try client.https_proxy.?.headers.append("proxy-authorization", result);
1019 }
1020 }
1021}
1022
1023pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
9201024
9211025/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
9221026/// This function is threadsafe.
923pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node {
1027pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*ConnectionPool.Node {
9241028 if (client.connection_pool.findConnection(.{
9251029 .host = host,
9261030 .port = port,
927 .is_tls = protocol == .tls,
1031 .protocol = protocol,
9281032 })) |node|
9291033 return node;
9301034
......@@ -948,8 +1052,8 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol:
9481052 conn.data = .{
9491053 .stream = stream,
9501054 .tls_client = undefined,
951 .protocol = protocol,
9521055
1056 .protocol = protocol,
9531057 .host = try client.allocator.dupe(u8, host),
9541058 .port = port,
9551059 };
......@@ -981,7 +1085,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
9811085 if (client.connection_pool.findConnection(.{
9821086 .host = path,
9831087 .port = 0,
984 .is_tls = false,
1088 .protocol = .plain,
9851089 })) |node|
9861090 return node;
9871091
......@@ -1007,34 +1111,120 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
10071111 return conn;
10081112}
10091113
1010// Prevents a dependency loop in request()
1011const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };
1012pub const ConnectError = ConnectErrorPartial || RequestError;
1114pub fn connectTunnel(
1115 client: *Client,
1116 proxy: *ProxyInformation,
1117 tunnel_host: []const u8,
1118 tunnel_port: u16,
1119) !*ConnectionPool.Node {
1120 if (!proxy.supports_connect) return error.TunnelNotSupported;
10131121
1014pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
10151122 if (client.connection_pool.findConnection(.{
1016 .host = host,
1017 .port = port,
1018 .is_tls = protocol == .tls,
1123 .host = tunnel_host,
1124 .port = tunnel_port,
1125 .protocol = proxy.protocol,
10191126 })) |node|
10201127 return node;
10211128
1022 if (client.proxy) |proxy| {
1023 const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) {
1024 .plain => 80,
1025 .tls => 443,
1129 var maybe_valid = false;
1130 _ = tunnel: {
1131 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1132 errdefer {
1133 conn.data.closing = true;
1134 client.connection_pool.release(client.allocator, conn);
1135 }
1136
1137 const uri = Uri{
1138 .scheme = "http",
1139 .user = null,
1140 .password = null,
1141 .host = tunnel_host,
1142 .port = tunnel_port,
1143 .path = "",
1144 .query = null,
1145 .fragment = null,
10261146 };
10271147
1028 const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol);
1029 conn.data.proxied = true;
1148 // we can use a small buffer here because a CONNECT response should be very small
1149 var buffer: [8096]u8 = undefined;
1150
1151 var req = client.request(.CONNECT, uri, proxy.headers, .{
1152 .handle_redirects = false,
1153 .connection = conn,
1154 .header_strategy = .{ .static = buffer[0..] },
1155 }) catch |err| {
1156 std.log.debug("err {}", .{err});
1157 break :tunnel err;
1158 };
1159 defer req.deinit();
1160
1161 req.start(.{ .raw_uri = true }) catch |err| break :tunnel err;
1162 req.wait() catch |err| break :tunnel err;
1163
1164 if (req.response.status.class() == .server_error) {
1165 maybe_valid = true;
1166 break :tunnel error.ServerError;
1167 }
1168
1169 if (req.response.status != .ok) break :tunnel error.ConnectionRefused;
10301170
1171 // 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.
1172 req.connection = null;
1173
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);
1177
1178 conn.data.port = tunnel_port;
1179 conn.data.closing = false;
1180
1181 return conn;
1182 } catch {
1183 // something went wrong with the tunnel
1184 proxy.supports_connect = maybe_valid;
1185 return error.TunnelNotSupported;
1186 };
1187}
1188
1189// Prevents a dependency loop in request()
1190const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
1191pub const ConnectError = ConnectErrorPartial || RequestError;
1192
1193pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
1194 // pointer required so that `supports_connect` can be updated if a CONNECT fails
1195 const potential_proxy: ?*ProxyInformation = switch (protocol) {
1196 .plain => if (client.http_proxy) |*proxy_info| proxy_info else null,
1197 .tls => if (client.https_proxy) |*proxy_info| proxy_info else null,
1198 };
1199
1200 if (potential_proxy) |proxy| {
1201 // don't attempt to proxy the proxy thru itself.
1202 if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) {
1203 return client.connectTcp(host, port, protocol);
1204 }
1205
1206 _ = if (proxy.supports_connect) tunnel: {
1207 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {
1208 error.TunnelNotSupported => break :tunnel,
1209 else => |e| return e,
1210 };
1211 };
1212
1213 // fall back to using the proxy as a normal http proxy
1214 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1215 errdefer {
1216 conn.data.closing = true;
1217 client.connection_pool.release(conn);
1218 }
1219
1220 conn.data.proxied = true;
10311221 return conn;
1032 } else {
1033 return client.connectUnproxied(host, port, protocol);
10341222 }
1223
1224 return client.connectTcp(host, port, protocol);
10351225}
10361226
1037pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{
1227pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{
10381228 UnsupportedUrlScheme,
10391229 UriMissingHost,
10401230
test/standalone/http.zig+20-14
......@@ -226,8 +226,11 @@ pub fn main() !void {
226226 const server_thread = try std.Thread.spawn(.{}, serverThread, .{&server});
227227
228228 var client = Client{ .allocator = calloc };
229 errdefer client.deinit();
229230 // defer client.deinit(); handled below
230231
232 try client.loadDefaultProxies();
233
231234 { // read content-length response
232235 var h = http.Headers{ .allocator = calloc };
233236 defer h.deinit();
......@@ -251,7 +254,7 @@ pub fn main() !void {
251254 }
252255
253256 // connection has been kept alive
254 try testing.expect(client.connection_pool.free_len == 1);
257 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
255258
256259 { // read large content-length response
257260 var h = http.Headers{ .allocator = calloc };
......@@ -275,7 +278,7 @@ pub fn main() !void {
275278 }
276279
277280 // connection has been kept alive
278 try testing.expect(client.connection_pool.free_len == 1);
281 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
279282
280283 { // send head request and not read chunked
281284 var h = http.Headers{ .allocator = calloc };
......@@ -301,7 +304,7 @@ pub fn main() !void {
301304 }
302305
303306 // connection has been kept alive
304 try testing.expect(client.connection_pool.free_len == 1);
307 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
305308
306309 { // read chunked response
307310 var h = http.Headers{ .allocator = calloc };
......@@ -326,7 +329,7 @@ pub fn main() !void {
326329 }
327330
328331 // connection has been kept alive
329 try testing.expect(client.connection_pool.free_len == 1);
332 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
330333
331334 { // send head request and not read chunked
332335 var h = http.Headers{ .allocator = calloc };
......@@ -352,7 +355,7 @@ pub fn main() !void {
352355 }
353356
354357 // connection has been kept alive
355 try testing.expect(client.connection_pool.free_len == 1);
358 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
356359
357360 { // check trailing headers
358361 var h = http.Headers{ .allocator = calloc };
......@@ -377,7 +380,7 @@ pub fn main() !void {
377380 }
378381
379382 // connection has been kept alive
380 try testing.expect(client.connection_pool.free_len == 1);
383 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
381384
382385 { // send content-length request
383386 var h = http.Headers{ .allocator = calloc };
......@@ -409,7 +412,7 @@ pub fn main() !void {
409412 }
410413
411414 // connection has been kept alive
412 try testing.expect(client.connection_pool.free_len == 1);
415 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
413416
414417 { // read content-length response with connection close
415418 var h = http.Headers{ .allocator = calloc };
......@@ -468,7 +471,7 @@ pub fn main() !void {
468471 }
469472
470473 // connection has been kept alive
471 try testing.expect(client.connection_pool.free_len == 1);
474 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
472475
473476 { // relative redirect
474477 var h = http.Headers{ .allocator = calloc };
......@@ -492,7 +495,7 @@ pub fn main() !void {
492495 }
493496
494497 // connection has been kept alive
495 try testing.expect(client.connection_pool.free_len == 1);
498 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
496499
497500 { // redirect from root
498501 var h = http.Headers{ .allocator = calloc };
......@@ -516,7 +519,7 @@ pub fn main() !void {
516519 }
517520
518521 // connection has been kept alive
519 try testing.expect(client.connection_pool.free_len == 1);
522 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
520523
521524 { // absolute redirect
522525 var h = http.Headers{ .allocator = calloc };
......@@ -540,7 +543,7 @@ pub fn main() !void {
540543 }
541544
542545 // connection has been kept alive
543 try testing.expect(client.connection_pool.free_len == 1);
546 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
544547
545548 { // too many redirects
546549 var h = http.Headers{ .allocator = calloc };
......@@ -562,7 +565,7 @@ pub fn main() !void {
562565 }
563566
564567 // connection has been kept alive
565 try testing.expect(client.connection_pool.free_len == 1);
568 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
566569
567570 { // check client without segfault by connection error after redirection
568571 var h = http.Headers{ .allocator = calloc };
......@@ -579,11 +582,14 @@ pub fn main() !void {
579582 try req.start(.{});
580583 const result = req.wait();
581584
582 try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
585 // a proxy without an upstream is likely to return a 5xx status.
586 if (client.http_proxy == null) {
587 try testing.expectError(error.ConnectionRefused, result); // expects not segfault but the regular error
588 }
583589 }
584590
585591 // connection has been kept alive
586 try testing.expect(client.connection_pool.free_len == 1);
592 try testing.expect(client.http_proxy != null or client.connection_pool.free_len == 1);
587593
588594 { // Client.fetch()
589595 var h = http.Headers{ .allocator = calloc };