| ... | @@ -25,27 +25,7 @@ next_https_rescan_certs: bool = true, | ... | @@ -25,27 +25,7 @@ next_https_rescan_certs: bool = true, |
| 25 | /// The pool of connections that can be reused (and currently in use). | 25 | /// The pool of connections that can be reused (and currently in use). |
| 26 | connection_pool: ConnectionPool = .{}, | 26 | connection_pool: ConnectionPool = .{}, |
| 27 | | 27 | |
| 28 | pub const ExtraError = union(enum) { | 28 | proxy: ?HttpProxy = null, |
| 29 | pub const TcpConnectError = std.net.TcpConnectToHostError; | | |
| 30 | pub const TlsError = std.crypto.tls.Client.InitError(net.Stream); | | |
| 31 | pub const WriteError = BufferedConnection.WriteError; | | |
| 32 | pub const ReadError = BufferedConnection.ReadError || error{HttpChunkInvalid}; | | |
| 33 | pub const CaBundleError = std.crypto.Certificate.Bundle.RescanError; | | |
| 34 | | | |
| 35 | pub const ZlibInitError = error{ BadHeader, InvalidCompression, InvalidWindowSize, Unsupported, EndOfStream, OutOfMemory } || Request.TransferReadError; | | |
| 36 | pub const GzipInitError = error{ BadHeader, InvalidCompression, OutOfMemory, WrongChecksum, EndOfStream, StreamTooLong } || Request.TransferReadError; | | |
| 37 | // pub const DecompressError = Compression.DeflateDecompressor.Error || Compression.GzipDecompressor.Error || Compression.ZstdDecompressor.Error; | | |
| 38 | pub const DecompressError = anyerror; // FIXME: the above line causes a false positive dependency loop | | |
| 39 | | | |
| 40 | zlib_init: ZlibInitError, // error.CompressionInitializationFailed | | |
| 41 | gzip_init: GzipInitError, // error.CompressionInitializationFailed | | |
| 42 | connect: TcpConnectError, // error.ConnectionFailed | | |
| 43 | ca_bundle: CaBundleError, // error.CertificateAuthorityBundleFailed | | |
| 44 | tls: TlsError, // error.TlsInitializationFailed | | |
| 45 | write: WriteError, // error.WriteFailed | | |
| 46 | read: ReadError, // error.ReadFailed | | |
| 47 | decompress: DecompressError, // error.ReadFailed | | |
| 48 | }; | | |
| 49 | | 29 | |
| 50 | /// A set of linked lists of connections that can be reused. | 30 | /// A set of linked lists of connections that can be reused. |
| 51 | pub const ConnectionPool = struct { | 31 | pub const ConnectionPool = struct { |
| ... | @@ -61,6 +41,7 @@ pub const ConnectionPool = struct { | ... | @@ -61,6 +41,7 @@ pub const ConnectionPool = struct { |
| 61 | host: []u8, | 41 | host: []u8, |
| 62 | port: u16, | 42 | port: u16, |
| 63 | | 43 | |
| | 44 | proxied: bool = false, |
| 64 | closing: bool = false, | 45 | closing: bool = false, |
| 65 | | 46 | |
| 66 | pub fn deinit(self: *StoredConnection, client: *Client) void { | 47 | pub fn deinit(self: *StoredConnection, client: *Client) void { |
| ... | @@ -137,7 +118,12 @@ pub const ConnectionPool = struct { | ... | @@ -137,7 +118,12 @@ pub const ConnectionPool = struct { |
| 137 | return client.allocator.destroy(popped); | 118 | return client.allocator.destroy(popped); |
| 138 | } | 119 | } |
| 139 | | 120 | |
| 140 | pool.free.append(node); | 121 | if (node.data.proxied) { |
| | 122 | pool.free.prepend(node); // proxied connections go to the end of the queue, always try direct connections first |
| | 123 | } else { |
| | 124 | pool.free.append(node); |
| | 125 | } |
| | 126 | |
| 141 | pool.free_len += 1; | 127 | pool.free_len += 1; |
| 142 | } | 128 | } |
| 143 | | 129 | |
| ... | @@ -546,9 +532,10 @@ pub const Request = struct { | ... | @@ -546,9 +532,10 @@ pub const Request = struct { |
| 546 | if (!req.response.parser.done) { | 532 | if (!req.response.parser.done) { |
| 547 | // If the response wasn't fully read, then we need to close the connection. | 533 | // If the response wasn't fully read, then we need to close the connection. |
| 548 | req.connection.data.closing = true; | 534 | req.connection.data.closing = true; |
| 549 | req.client.connection_pool.release(req.client, req.connection); | | |
| 550 | } | 535 | } |
| 551 | | 536 | |
| | 537 | req.client.connection_pool.release(req.client, req.connection); |
| | 538 | |
| 552 | req.arena.deinit(); | 539 | req.arena.deinit(); |
| 553 | req.* = undefined; | 540 | req.* = undefined; |
| 554 | } | 541 | } |
| ... | @@ -557,30 +544,20 @@ pub const Request = struct { | ... | @@ -557,30 +544,20 @@ pub const Request = struct { |
| 557 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); | 544 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 558 | const w = buffered.writer(); | 545 | const w = buffered.writer(); |
| 559 | | 546 | |
| 560 | const escaped_path = try Uri.escapePath(req.client.allocator, uri.path); | | |
| 561 | defer req.client.allocator.free(escaped_path); | | |
| 562 | | | |
| 563 | const escaped_query = if (uri.query) |q| try Uri.escapeQuery(req.client.allocator, q) else null; | | |
| 564 | defer if (escaped_query) |q| req.client.allocator.free(q); | | |
| 565 | | | |
| 566 | const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(req.client.allocator, f) else null; | | |
| 567 | defer if (escaped_fragment) |f| req.client.allocator.free(f); | | |
| 568 | | | |
| 569 | try w.writeAll(@tagName(headers.method)); | 547 | try w.writeAll(@tagName(headers.method)); |
| 570 | try w.writeByte(' '); | 548 | try w.writeByte(' '); |
| 571 | if (escaped_path.len == 0) { | 549 | |
| 572 | try w.writeByte('/'); | 550 | if (req.headers.method == .CONNECT) { |
| | 551 | try w.writeAll(uri.host.?); |
| | 552 | try w.writeByte(':'); |
| | 553 | try w.print("{}", .{uri.port.?}); |
| | 554 | } else if (req.connection.data.proxied) { |
| | 555 | // proxied connections require the full uri |
| | 556 | try w.print("{+/}", .{uri}); |
| 573 | } else { | 557 | } else { |
| 574 | try w.writeAll(escaped_path); | 558 | try w.print("{/}", .{uri}); |
| 575 | } | | |
| 576 | if (escaped_query) |q| { | | |
| 577 | try w.writeByte('?'); | | |
| 578 | try w.writeAll(q); | | |
| 579 | } | | |
| 580 | if (escaped_fragment) |f| { | | |
| 581 | try w.writeByte('#'); | | |
| 582 | try w.writeAll(f); | | |
| 583 | } | 559 | } |
| | 560 | |
| 584 | try w.writeByte(' '); | 561 | try w.writeByte(' '); |
| 585 | try w.writeAll(@tagName(headers.version)); | 562 | try w.writeAll(@tagName(headers.version)); |
| 586 | try w.writeAll("\r\nHost: "); | 563 | try w.writeAll("\r\nHost: "); |
| ... | @@ -659,6 +636,12 @@ pub const Request = struct { | ... | @@ -659,6 +636,12 @@ pub const Request = struct { |
| 659 | req.response.parser.done = true; | 636 | req.response.parser.done = true; |
| 660 | } | 637 | } |
| 661 | | 638 | |
| | 639 | if (req.headers.method == .CONNECT and req.response.headers.status == .ok) { |
| | 640 | req.connection.data.closing = false; |
| | 641 | req.connection.data.proxied = true; |
| | 642 | req.response.parser.done = true; |
| | 643 | } |
| | 644 | |
| 662 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | 645 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { |
| 663 | req.connection.data.closing = false; | 646 | req.connection.data.closing = false; |
| 664 | } else { | 647 | } else { |
| ... | @@ -802,7 +785,7 @@ pub const Request = struct { | ... | @@ -802,7 +785,7 @@ pub const Request = struct { |
| 802 | } | 785 | } |
| 803 | } | 786 | } |
| 804 | | 787 | |
| 805 | pub const FinishError = WriteError || error{ MessageNotCompleted }; | 788 | pub const FinishError = WriteError || error{MessageNotCompleted}; |
| 806 | | 789 | |
| 807 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 790 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 808 | pub fn finish(req: *Request) FinishError!void { | 791 | pub fn finish(req: *Request) FinishError!void { |
| ... | @@ -817,6 +800,20 @@ pub const Request = struct { | ... | @@ -817,6 +800,20 @@ pub const Request = struct { |
| 817 | } | 800 | } |
| 818 | }; | 801 | }; |
| 819 | | 802 | |
| | 803 | pub const HttpProxy = struct { |
| | 804 | pub const ProxyAuthentication = union(enum) { |
| | 805 | basic: []const u8, |
| | 806 | custom: []const u8, |
| | 807 | }; |
| | 808 | |
| | 809 | protocol: Connection.Protocol, |
| | 810 | host: []const u8, |
| | 811 | port: ?u16 = null, |
| | 812 | |
| | 813 | /// The value for the Proxy-Authorization header. |
| | 814 | auth: ?ProxyAuthentication = null, |
| | 815 | }; |
| | 816 | |
| 820 | /// Release all associated resources with the client. | 817 | /// Release all associated resources with the client. |
| 821 | /// TODO: currently leaks all request allocated data | 818 | /// TODO: currently leaks all request allocated data |
| 822 | pub fn deinit(client: *Client) void { | 819 | pub fn deinit(client: *Client) void { |
| ... | @@ -826,11 +823,11 @@ pub fn deinit(client: *Client) void { | ... | @@ -826,11 +823,11 @@ pub fn deinit(client: *Client) void { |
| 826 | client.* = undefined; | 823 | client.* = undefined; |
| 827 | } | 824 | } |
| 828 | | 825 | |
| 829 | pub const ConnectError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; | 826 | pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; |
| 830 | | 827 | |
| 831 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. | 828 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 832 | /// This function is threadsafe. | 829 | /// This function is threadsafe. |
| 833 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { | 830 | pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node { |
| 834 | if (client.connection_pool.findConnection(.{ | 831 | if (client.connection_pool.findConnection(.{ |
| 835 | .host = host, | 832 | .host = host, |
| 836 | .port = port, | 833 | .port = port, |
| ... | @@ -884,7 +881,34 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio | ... | @@ -884,7 +881,34 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 884 | return conn; | 881 | return conn; |
| 885 | } | 882 | } |
| 886 | | 883 | |
| 887 | pub const RequestError = ConnectError || BufferedConnection.WriteError || error{ | 884 | // Prevents a dependency loop in request() |
| | 885 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; |
| | 886 | pub const ConnectError = ConnectErrorPartial || RequestError; |
| | 887 | |
| | 888 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| | 889 | if (client.connection_pool.findConnection(.{ |
| | 890 | .host = host, |
| | 891 | .port = port, |
| | 892 | .is_tls = protocol == .tls, |
| | 893 | })) |node| |
| | 894 | return node; |
| | 895 | |
| | 896 | if (client.proxy) |proxy| { |
| | 897 | const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) { |
| | 898 | .plain => 80, |
| | 899 | .tls => 443, |
| | 900 | }; |
| | 901 | |
| | 902 | const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol); |
| | 903 | conn.data.proxied = true; |
| | 904 | |
| | 905 | return conn; |
| | 906 | } else { |
| | 907 | return client.connectUnproxied(host, port, protocol); |
| | 908 | } |
| | 909 | } |
| | 910 | |
| | 911 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{ |
| 888 | UnsupportedUrlScheme, | 912 | UnsupportedUrlScheme, |
| 889 | UriMissingHost, | 913 | UriMissingHost, |
| 890 | | 914 | |
| ... | @@ -896,6 +920,9 @@ pub const Options = struct { | ... | @@ -896,6 +920,9 @@ pub const Options = struct { |
| 896 | max_redirects: u32 = 3, | 920 | max_redirects: u32 = 3, |
| 897 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, | 921 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| 898 | | 922 | |
| | 923 | /// Must be an already acquired connection. |
| | 924 | connection: ?*ConnectionPool.Node = null, |
| | 925 | |
| 899 | pub const HeaderStrategy = union(enum) { | 926 | pub const HeaderStrategy = union(enum) { |
| 900 | /// In this case, the client's Allocator will be used to store the | 927 | /// In this case, the client's Allocator will be used to store the |
| 901 | /// entire HTTP header. This value is the maximum total size of | 928 | /// entire HTTP header. This value is the maximum total size of |
| ... | @@ -939,10 +966,12 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt | ... | @@ -939,10 +966,12 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 939 | } | 966 | } |
| 940 | } | 967 | } |
| 941 | | 968 | |
| | 969 | const conn = options.connection orelse try client.connect(host, port, protocol); |
| | 970 | |
| 942 | var req: Request = .{ | 971 | var req: Request = .{ |
| 943 | .uri = uri, | 972 | .uri = uri, |
| 944 | .client = client, | 973 | .client = client, |
| 945 | .connection = try client.connect(host, port, protocol), | 974 | .connection = conn, |
| 946 | .headers = headers, | 975 | .headers = headers, |
| 947 | .redirects_left = options.max_redirects, | 976 | .redirects_left = options.max_redirects, |
| 948 | .handle_redirects = options.handle_redirects, | 977 | .handle_redirects = options.handle_redirects, |