| ... | ... | @@ -25,27 +25,7 @@ next_https_rescan_certs: bool = true, |
| 25 | 25 | /// The pool of connections that can be reused (and currently in use). |
| 26 | 26 | connection_pool: ConnectionPool = .{}, |
| 27 | 27 | |
| 28 | | pub const ExtraError = union(enum) { |
| 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 | | }; |
| 28 | proxy: ?HttpProxy = null, |
| 49 | 29 | |
| 50 | 30 | /// A set of linked lists of connections that can be reused. |
| 51 | 31 | pub const ConnectionPool = struct { |
| ... | ... | @@ -61,6 +41,7 @@ pub const ConnectionPool = struct { |
| 61 | 41 | host: []u8, |
| 62 | 42 | port: u16, |
| 63 | 43 | |
| 44 | proxied: bool = false, |
| 64 | 45 | closing: bool = false, |
| 65 | 46 | |
| 66 | 47 | pub fn deinit(self: *StoredConnection, client: *Client) void { |
| ... | ... | @@ -137,7 +118,12 @@ pub const ConnectionPool = struct { |
| 137 | 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 | 127 | pool.free_len += 1; |
| 142 | 128 | } |
| 143 | 129 | |
| ... | ... | @@ -546,9 +532,10 @@ pub const Request = struct { |
| 546 | 532 | if (!req.response.parser.done) { |
| 547 | 533 | // If the response wasn't fully read, then we need to close the connection. |
| 548 | 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 | 539 | req.arena.deinit(); |
| 553 | 540 | req.* = undefined; |
| 554 | 541 | } |
| ... | ... | @@ -557,30 +544,20 @@ pub const Request = struct { |
| 557 | 544 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 558 | 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 | 547 | try w.writeAll(@tagName(headers.method)); |
| 570 | 548 | try w.writeByte(' '); |
| 571 | | if (escaped_path.len == 0) { |
| 572 | | try w.writeByte('/'); |
| 549 | |
| 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 | 557 | } else { |
| 574 | | try w.writeAll(escaped_path); |
| 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); |
| 558 | try w.print("{/}", .{uri}); |
| 583 | 559 | } |
| 560 | |
| 584 | 561 | try w.writeByte(' '); |
| 585 | 562 | try w.writeAll(@tagName(headers.version)); |
| 586 | 563 | try w.writeAll("\r\nHost: "); |
| ... | ... | @@ -659,6 +636,12 @@ pub const Request = struct { |
| 659 | 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 | 645 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { |
| 663 | 646 | req.connection.data.closing = false; |
| 664 | 647 | } else { |
| ... | ... | @@ -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 | 790 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 808 | 791 | pub fn finish(req: *Request) FinishError!void { |
| ... | ... | @@ -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 | 817 | /// Release all associated resources with the client. |
| 821 | 818 | /// TODO: currently leaks all request allocated data |
| 822 | 819 | pub fn deinit(client: *Client) void { |
| ... | ... | @@ -826,11 +823,11 @@ pub fn deinit(client: *Client) void { |
| 826 | 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 | 828 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 832 | 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 | 831 | if (client.connection_pool.findConnection(.{ |
| 835 | 832 | .host = host, |
| 836 | 833 | .port = port, |
| ... | ... | @@ -884,7 +881,34 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 884 | 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 | 912 | UnsupportedUrlScheme, |
| 889 | 913 | UriMissingHost, |
| 890 | 914 | |
| ... | ... | @@ -896,6 +920,9 @@ pub const Options = struct { |
| 896 | 920 | max_redirects: u32 = 3, |
| 897 | 921 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| 898 | 922 | |
| 923 | /// Must be an already acquired connection. |
| 924 | connection: ?*ConnectionPool.Node = null, |
| 925 | |
| 899 | 926 | pub const HeaderStrategy = union(enum) { |
| 900 | 927 | /// In this case, the client's Allocator will be used to store the |
| 901 | 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 | 966 | } |
| 940 | 967 | } |
| 941 | 968 | |
| 969 | const conn = options.connection orelse try client.connect(host, port, protocol); |
| 970 | |
| 942 | 971 | var req: Request = .{ |
| 943 | 972 | .uri = uri, |
| 944 | 973 | .client = client, |
| 945 | | .connection = try client.connect(host, port, protocol), |
| 974 | .connection = conn, |
| 946 | 975 | .headers = headers, |
| 947 | 976 | .redirects_left = options.max_redirects, |
| 948 | 977 | .handle_redirects = options.handle_redirects, |