| ... | @@ -13,12 +13,16 @@ const assert = std.debug.assert; | ... | @@ -13,12 +13,16 @@ const assert = std.debug.assert; |
| 13 | const Client = @This(); | 13 | const Client = @This(); |
| 14 | const proto = @import("protocol.zig"); | 14 | const proto = @import("protocol.zig"); |
| 15 | | 15 | |
| 16 | pub const default_connection_pool_size = 32; | 16 | pub const disable_tls = std.options.http_disable_tls; |
| 17 | pub const connection_pool_size = std.options.http_connection_pool_size; | | |
| 18 | | 17 | |
| | 18 | /// Allocator used for all allocations made by the client. |
| | 19 | /// |
| | 20 | /// This allocator must be thread-safe. |
| 19 | allocator: Allocator, | 21 | allocator: Allocator, |
| 20 | ca_bundle: std.crypto.Certificate.Bundle = .{}, | 22 | |
| | 23 | ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, |
| 21 | ca_bundle_mutex: std.Thread.Mutex = .{}, | 24 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| | 25 | |
| 22 | /// When this is `true`, the next time this client performs an HTTPS request, | 26 | /// When this is `true`, the next time this client performs an HTTPS request, |
| 23 | /// it will first rescan the system for root certificates. | 27 | /// it will first rescan the system for root certificates. |
| 24 | next_https_rescan_certs: bool = true, | 28 | next_https_rescan_certs: bool = true, |
| ... | @@ -26,7 +30,11 @@ next_https_rescan_certs: bool = true, | ... | @@ -26,7 +30,11 @@ next_https_rescan_certs: bool = true, |
| 26 | /// The pool of connections that can be reused (and currently in use). | 30 | /// The pool of connections that can be reused (and currently in use). |
| 27 | connection_pool: ConnectionPool = .{}, | 31 | connection_pool: ConnectionPool = .{}, |
| 28 | | 32 | |
| 29 | proxy: ?HttpProxy = null, | 33 | /// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections. |
| | 34 | http_proxy: ?Proxy = null, |
| | 35 | |
| | 36 | /// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections. |
| | 37 | https_proxy: ?Proxy = null, |
| 30 | | 38 | |
| 31 | /// A set of linked lists of connections that can be reused. | 39 | /// A set of linked lists of connections that can be reused. |
| 32 | pub const ConnectionPool = struct { | 40 | pub const ConnectionPool = struct { |
| ... | @@ -34,7 +42,7 @@ pub const ConnectionPool = struct { | ... | @@ -34,7 +42,7 @@ pub const ConnectionPool = struct { |
| 34 | pub const Criteria = struct { | 42 | pub const Criteria = struct { |
| 35 | host: []const u8, | 43 | host: []const u8, |
| 36 | port: u16, | 44 | port: u16, |
| 37 | is_tls: bool, | 45 | protocol: Connection.Protocol, |
| 38 | }; | 46 | }; |
| 39 | | 47 | |
| 40 | const Queue = std.DoublyLinkedList(Connection); | 48 | const Queue = std.DoublyLinkedList(Connection); |
| ... | @@ -46,22 +54,24 @@ pub const ConnectionPool = struct { | ... | @@ -46,22 +54,24 @@ pub const ConnectionPool = struct { |
| 46 | /// Open connections that are not currently in use. | 54 | /// Open connections that are not currently in use. |
| 47 | free: Queue = .{}, | 55 | free: Queue = .{}, |
| 48 | free_len: usize = 0, | 56 | free_len: usize = 0, |
| 49 | free_size: usize = connection_pool_size, | 57 | free_size: usize = 32, |
| 50 | | 58 | |
| 51 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. | 59 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. |
| 52 | /// If no connection is found, null is returned. | 60 | /// If no connection is found, null is returned. |
| 53 | pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Node { | 61 | pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection { |
| 54 | pool.mutex.lock(); | 62 | pool.mutex.lock(); |
| 55 | defer pool.mutex.unlock(); | 63 | defer pool.mutex.unlock(); |
| 56 | | 64 | |
| 57 | var next = pool.free.last; | 65 | var next = pool.free.last; |
| 58 | while (next) |node| : (next = node.prev) { | 66 | while (next) |node| : (next = node.prev) { |
| 59 | if ((node.data.protocol == .tls) != criteria.is_tls) continue; | 67 | if (node.data.protocol != criteria.protocol) continue; |
| 60 | if (node.data.port != criteria.port) continue; | 68 | if (node.data.port != criteria.port) continue; |
| 61 | if (!mem.eql(u8, node.data.host, criteria.host)) continue; | 69 | |
| | 70 | // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) |
| | 71 | if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue; |
| 62 | | 72 | |
| 63 | pool.acquireUnsafe(node); | 73 | pool.acquireUnsafe(node); |
| 64 | return node; | 74 | return &node.data; |
| 65 | } | 75 | } |
| 66 | | 76 | |
| 67 | return null; | 77 | return null; |
| ... | @@ -85,23 +95,28 @@ pub const ConnectionPool = struct { | ... | @@ -85,23 +95,28 @@ pub const ConnectionPool = struct { |
| 85 | | 95 | |
| 86 | /// Tries to release a connection back to the connection pool. This function is threadsafe. | 96 | /// Tries to release a connection back to the connection pool. This function is threadsafe. |
| 87 | /// If the connection is marked as closing, it will be closed instead. | 97 | /// If the connection is marked as closing, it will be closed instead. |
| 88 | pub fn release(pool: *ConnectionPool, client: *Client, node: *Node) void { | 98 | /// |
| | 99 | /// The allocator must be the owner of all nodes in this pool. |
| | 100 | /// The allocator must be the owner of all resources associated with the connection. |
| | 101 | pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void { |
| 89 | pool.mutex.lock(); | 102 | pool.mutex.lock(); |
| 90 | defer pool.mutex.unlock(); | 103 | defer pool.mutex.unlock(); |
| 91 | | 104 | |
| | 105 | const node = @fieldParentPtr(Node, "data", connection); |
| | 106 | |
| 92 | pool.used.remove(node); | 107 | pool.used.remove(node); |
| 93 | | 108 | |
| 94 | if (node.data.closing) { | 109 | if (node.data.closing or pool.free_size == 0) { |
| 95 | node.data.deinit(client); | 110 | node.data.close(allocator); |
| 96 | return client.allocator.destroy(node); | 111 | return allocator.destroy(node); |
| 97 | } | 112 | } |
| 98 | | 113 | |
| 99 | if (pool.free_len >= pool.free_size) { | 114 | if (pool.free_len >= pool.free_size) { |
| 100 | const popped = pool.free.popFirst() orelse unreachable; | 115 | const popped = pool.free.popFirst() orelse unreachable; |
| 101 | pool.free_len -= 1; | 116 | pool.free_len -= 1; |
| 102 | | 117 | |
| 103 | popped.data.deinit(client); | 118 | popped.data.close(allocator); |
| 104 | client.allocator.destroy(popped); | 119 | allocator.destroy(popped); |
| 105 | } | 120 | } |
| 106 | | 121 | |
| 107 | if (node.data.proxied) { | 122 | if (node.data.proxied) { |
| ... | @@ -121,23 +136,43 @@ pub const ConnectionPool = struct { | ... | @@ -121,23 +136,43 @@ pub const ConnectionPool = struct { |
| 121 | pool.used.append(node); | 136 | pool.used.append(node); |
| 122 | } | 137 | } |
| 123 | | 138 | |
| 124 | pub fn deinit(pool: *ConnectionPool, client: *Client) void { | 139 | /// Resizes the connection pool. This function is threadsafe. |
| | 140 | /// |
| | 141 | /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. |
| | 142 | pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { |
| | 143 | pool.mutex.lock(); |
| | 144 | defer pool.mutex.unlock(); |
| | 145 | |
| | 146 | var next = pool.free.first; |
| | 147 | _ = next; |
| | 148 | while (pool.free_len > new_size) { |
| | 149 | const popped = pool.free.popFirst() orelse unreachable; |
| | 150 | pool.free_len -= 1; |
| | 151 | |
| | 152 | popped.data.close(allocator); |
| | 153 | allocator.destroy(popped); |
| | 154 | } |
| | 155 | |
| | 156 | pool.free_size = new_size; |
| | 157 | } |
| | 158 | |
| | 159 | pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void { |
| 125 | pool.mutex.lock(); | 160 | pool.mutex.lock(); |
| 126 | | 161 | |
| 127 | var next = pool.free.first; | 162 | var next = pool.free.first; |
| 128 | while (next) |node| { | 163 | while (next) |node| { |
| 129 | defer client.allocator.destroy(node); | 164 | defer allocator.destroy(node); |
| 130 | next = node.next; | 165 | next = node.next; |
| 131 | | 166 | |
| 132 | node.data.deinit(client); | 167 | node.data.close(allocator); |
| 133 | } | 168 | } |
| 134 | | 169 | |
| 135 | next = pool.used.first; | 170 | next = pool.used.first; |
| 136 | while (next) |node| { | 171 | while (next) |node| { |
| 137 | defer client.allocator.destroy(node); | 172 | defer allocator.destroy(node); |
| 138 | next = node.next; | 173 | next = node.next; |
| 139 | | 174 | |
| 140 | node.data.deinit(client); | 175 | node.data.close(allocator); |
| 141 | } | 176 | } |
| 142 | | 177 | |
| 143 | pool.* = undefined; | 178 | pool.* = undefined; |
| ... | @@ -147,11 +182,13 @@ pub const ConnectionPool = struct { | ... | @@ -147,11 +182,13 @@ pub const ConnectionPool = struct { |
| 147 | /// An interface to either a plain or TLS connection. | 182 | /// An interface to either a plain or TLS connection. |
| 148 | pub const Connection = struct { | 183 | pub const Connection = struct { |
| 149 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; | 184 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; |
| | 185 | const BufferSize = std.math.IntFittingRange(0, buffer_size); |
| | 186 | |
| 150 | pub const Protocol = enum { plain, tls }; | 187 | pub const Protocol = enum { plain, tls }; |
| 151 | | 188 | |
| 152 | stream: net.Stream, | 189 | stream: net.Stream, |
| 153 | /// undefined unless protocol is tls. | 190 | /// undefined unless protocol is tls. |
| 154 | tls_client: *std.crypto.tls.Client, | 191 | tls_client: if (!disable_tls) *std.crypto.tls.Client else void, |
| 155 | | 192 | |
| 156 | protocol: Protocol, | 193 | protocol: Protocol, |
| 157 | host: []u8, | 194 | host: []u8, |
| ... | @@ -160,16 +197,15 @@ pub const Connection = struct { | ... | @@ -160,16 +197,15 @@ pub const Connection = struct { |
| 160 | proxied: bool = false, | 197 | proxied: bool = false, |
| 161 | closing: bool = false, | 198 | closing: bool = false, |
| 162 | | 199 | |
| 163 | read_start: u16 = 0, | 200 | read_start: BufferSize = 0, |
| 164 | read_end: u16 = 0, | 201 | read_end: BufferSize = 0, |
| | 202 | write_end: BufferSize = 0, |
| 165 | read_buf: [buffer_size]u8 = undefined, | 203 | read_buf: [buffer_size]u8 = undefined, |
| | 204 | write_buf: [buffer_size]u8 = undefined, |
| 166 | | 205 | |
| 167 | pub fn rawReadAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize { | 206 | pub fn readvDirectTls(conn: *Connection, buffers: []std.os.iovec) ReadError!usize { |
| 168 | return switch (conn.protocol) { | 207 | return conn.tls_client.readv(conn.stream, buffers) catch |err| { |
| 169 | .plain => conn.stream.readAtLeast(buffer, len), | 208 | // https://github.com/ziglang/zig/issues/2473 |
| 170 | .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len), | | |
| 171 | } catch |err| { | | |
| 172 | // TODO: https://github.com/ziglang/zig/issues/2473 | | |
| 173 | if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert; | 209 | if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert; |
| 174 | | 210 | |
| 175 | switch (err) { | 211 | switch (err) { |
| ... | @@ -181,61 +217,69 @@ pub const Connection = struct { | ... | @@ -181,61 +217,69 @@ pub const Connection = struct { |
| 181 | }; | 217 | }; |
| 182 | } | 218 | } |
| 183 | | 219 | |
| | 220 | pub fn readvDirect(conn: *Connection, buffers: []std.os.iovec) ReadError!usize { |
| | 221 | if (conn.protocol == .tls) { |
| | 222 | if (disable_tls) unreachable; |
| | 223 | |
| | 224 | return conn.readvDirectTls(buffers); |
| | 225 | } |
| | 226 | |
| | 227 | return conn.stream.readv(buffers) catch |err| switch (err) { |
| | 228 | error.ConnectionTimedOut => return error.ConnectionTimedOut, |
| | 229 | error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, |
| | 230 | else => return error.UnexpectedReadFailure, |
| | 231 | }; |
| | 232 | } |
| | 233 | |
| 184 | pub fn fill(conn: *Connection) ReadError!void { | 234 | pub fn fill(conn: *Connection) ReadError!void { |
| 185 | if (conn.read_end != conn.read_start) return; | 235 | if (conn.read_end != conn.read_start) return; |
| 186 | | 236 | |
| 187 | const nread = try conn.rawReadAtLeast(conn.read_buf[0..], 1); | 237 | var iovecs = [1]std.os.iovec{ |
| | 238 | .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len }, |
| | 239 | }; |
| | 240 | const nread = try conn.readvDirect(&iovecs); |
| 188 | if (nread == 0) return error.EndOfStream; | 241 | if (nread == 0) return error.EndOfStream; |
| 189 | conn.read_start = 0; | 242 | conn.read_start = 0; |
| 190 | conn.read_end = @as(u16, @intCast(nread)); | 243 | conn.read_end = @intCast(nread); |
| 191 | } | 244 | } |
| 192 | | 245 | |
| 193 | pub fn peek(conn: *Connection) []const u8 { | 246 | pub fn peek(conn: *Connection) []const u8 { |
| 194 | return conn.read_buf[conn.read_start..conn.read_end]; | 247 | return conn.read_buf[conn.read_start..conn.read_end]; |
| 195 | } | 248 | } |
| 196 | | 249 | |
| 197 | pub fn drop(conn: *Connection, num: u16) void { | 250 | pub fn drop(conn: *Connection, num: BufferSize) void { |
| 198 | conn.read_start += num; | 251 | conn.read_start += num; |
| 199 | } | 252 | } |
| 200 | | 253 | |
| 201 | pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize { | 254 | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { |
| 202 | assert(len <= buffer.len); | 255 | const available_read = conn.read_end - conn.read_start; |
| 203 | | 256 | const available_buffer = buffer.len; |
| 204 | var out_index: u16 = 0; | | |
| 205 | while (out_index < len) { | | |
| 206 | const available_read = conn.read_end - conn.read_start; | | |
| 207 | const available_buffer = buffer.len - out_index; | | |
| 208 | | | |
| 209 | if (available_read > available_buffer) { // partially read buffered data | | |
| 210 | @memcpy(buffer[out_index..], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]); | | |
| 211 | out_index += @as(u16, @intCast(available_buffer)); | | |
| 212 | conn.read_start += @as(u16, @intCast(available_buffer)); | | |
| 213 | | 257 | |
| 214 | break; | 258 | if (available_read > available_buffer) { // partially read buffered data |
| 215 | } else if (available_read > 0) { // fully read buffered data | 259 | @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]); |
| 216 | @memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]); | 260 | conn.read_start += @intCast(available_buffer); |
| 217 | out_index += available_read; | | |
| 218 | conn.read_start += available_read; | | |
| 219 | | 261 | |
| 220 | if (out_index >= len) break; | 262 | return available_buffer; |
| 221 | } | 263 | } else if (available_read > 0) { // fully read buffered data |
| | 264 | @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]); |
| | 265 | conn.read_start += available_read; |
| 222 | | 266 | |
| 223 | const leftover_buffer = available_buffer - available_read; | 267 | return available_read; |
| 224 | const leftover_len = len - out_index; | 268 | } |
| 225 | | 269 | |
| 226 | if (leftover_buffer > conn.read_buf.len) { | 270 | var iovecs = [2]std.os.iovec{ |
| 227 | // skip the buffer if the output is large enough | 271 | .{ .iov_base = buffer.ptr, .iov_len = buffer.len }, |
| 228 | return conn.rawReadAtLeast(buffer[out_index..], leftover_len); | 272 | .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len }, |
| 229 | } | 273 | }; |
| | 274 | const nread = try conn.readvDirect(&iovecs); |
| 230 | | 275 | |
| 231 | try conn.fill(); | 276 | if (nread > buffer.len) { |
| | 277 | conn.read_start = 0; |
| | 278 | conn.read_end = @intCast(nread - buffer.len); |
| | 279 | return buffer.len; |
| 232 | } | 280 | } |
| 233 | | 281 | |
| 234 | return out_index; | 282 | return nread; |
| 235 | } | | |
| 236 | | | |
| 237 | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { | | |
| 238 | return conn.readAtLeast(buffer, 1); | | |
| 239 | } | 283 | } |
| 240 | | 284 | |
| 241 | pub const ReadError = error{ | 285 | pub const ReadError = error{ |
| ... | @@ -253,26 +297,49 @@ pub const Connection = struct { | ... | @@ -253,26 +297,49 @@ pub const Connection = struct { |
| 253 | return Reader{ .context = conn }; | 297 | return Reader{ .context = conn }; |
| 254 | } | 298 | } |
| 255 | | 299 | |
| 256 | pub fn writeAll(conn: *Connection, buffer: []const u8) !void { | 300 | pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void { |
| 257 | return switch (conn.protocol) { | 301 | return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) { |
| 258 | .plain => conn.stream.writeAll(buffer), | | |
| 259 | .tls => conn.tls_client.writeAll(conn.stream, buffer), | | |
| 260 | } catch |err| switch (err) { | | |
| 261 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | 302 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, |
| 262 | else => return error.UnexpectedWriteFailure, | 303 | else => return error.UnexpectedWriteFailure, |
| 263 | }; | 304 | }; |
| 264 | } | 305 | } |
| 265 | | 306 | |
| 266 | pub fn write(conn: *Connection, buffer: []const u8) !usize { | 307 | pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void { |
| 267 | return switch (conn.protocol) { | 308 | if (conn.protocol == .tls) { |
| 268 | .plain => conn.stream.write(buffer), | 309 | if (disable_tls) unreachable; |
| 269 | .tls => conn.tls_client.write(conn.stream, buffer), | 310 | |
| 270 | } catch |err| switch (err) { | 311 | return conn.writeAllDirectTls(buffer); |
| | 312 | } |
| | 313 | |
| | 314 | return conn.stream.writeAll(buffer) catch |err| switch (err) { |
| 271 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | 315 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, |
| 272 | else => return error.UnexpectedWriteFailure, | 316 | else => return error.UnexpectedWriteFailure, |
| 273 | }; | 317 | }; |
| 274 | } | 318 | } |
| 275 | | 319 | |
| | 320 | pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize { |
| | 321 | if (conn.write_end + buffer.len > conn.write_buf.len) { |
| | 322 | try conn.flush(); |
| | 323 | |
| | 324 | if (buffer.len > conn.write_buf.len) { |
| | 325 | try conn.writeAllDirect(buffer); |
| | 326 | return buffer.len; |
| | 327 | } |
| | 328 | } |
| | 329 | |
| | 330 | @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer); |
| | 331 | conn.write_end += @intCast(buffer.len); |
| | 332 | |
| | 333 | return buffer.len; |
| | 334 | } |
| | 335 | |
| | 336 | pub fn flush(conn: *Connection) WriteError!void { |
| | 337 | if (conn.write_end == 0) return; |
| | 338 | |
| | 339 | try conn.writeAllDirect(conn.write_buf[0..conn.write_end]); |
| | 340 | conn.write_end = 0; |
| | 341 | } |
| | 342 | |
| 276 | pub const WriteError = error{ | 343 | pub const WriteError = error{ |
| 277 | ConnectionResetByPeer, | 344 | ConnectionResetByPeer, |
| 278 | UnexpectedWriteFailure, | 345 | UnexpectedWriteFailure, |
| ... | @@ -284,19 +351,17 @@ pub const Connection = struct { | ... | @@ -284,19 +351,17 @@ pub const Connection = struct { |
| 284 | return Writer{ .context = conn }; | 351 | return Writer{ .context = conn }; |
| 285 | } | 352 | } |
| 286 | | 353 | |
| 287 | pub fn close(conn: *Connection, client: *const Client) void { | 354 | pub fn close(conn: *Connection, allocator: Allocator) void { |
| 288 | if (conn.protocol == .tls) { | 355 | if (conn.protocol == .tls) { |
| | 356 | if (disable_tls) unreachable; |
| | 357 | |
| 289 | // try to cleanly close the TLS connection, for any server that cares. | 358 | // try to cleanly close the TLS connection, for any server that cares. |
| 290 | _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {}; | 359 | _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {}; |
| 291 | client.allocator.destroy(conn.tls_client); | 360 | allocator.destroy(conn.tls_client); |
| 292 | } | 361 | } |
| 293 | | 362 | |
| 294 | conn.stream.close(); | 363 | conn.stream.close(); |
| 295 | } | 364 | allocator.free(conn.host); |
| 296 | | | |
| 297 | pub fn deinit(conn: *Connection, client: *const Client) void { | | |
| 298 | conn.close(client); | | |
| 299 | client.allocator.free(conn.host); | | |
| 300 | } | 365 | } |
| 301 | }; | 366 | }; |
| 302 | | 367 | |
| ... | @@ -331,7 +396,7 @@ pub const Response = struct { | ... | @@ -331,7 +396,7 @@ pub const Response = struct { |
| 331 | }; | 396 | }; |
| 332 | | 397 | |
| 333 | pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void { | 398 | pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void { |
| 334 | var it = mem.tokenizeAny(u8, bytes[0 .. bytes.len - 4], "\r\n"); | 399 | var it = mem.tokenizeAny(u8, bytes, "\r\n"); |
| 335 | | 400 | |
| 336 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | 401 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 337 | if (first_line.len < 12) | 402 | if (first_line.len < 12) |
| ... | @@ -350,6 +415,8 @@ pub const Response = struct { | ... | @@ -350,6 +415,8 @@ pub const Response = struct { |
| 350 | res.status = status; | 415 | res.status = status; |
| 351 | res.reason = reason; | 416 | res.reason = reason; |
| 352 | | 417 | |
| | 418 | res.headers.clearRetainingCapacity(); |
| | 419 | |
| 353 | while (it.next()) |line| { | 420 | while (it.next()) |line| { |
| 354 | if (line.len == 0) return error.HttpHeadersInvalid; | 421 | if (line.len == 0) return error.HttpHeadersInvalid; |
| 355 | switch (line[0]) { | 422 | switch (line[0]) { |
| ... | @@ -365,46 +432,42 @@ pub const Response = struct { | ... | @@ -365,46 +432,42 @@ pub const Response = struct { |
| 365 | | 432 | |
| 366 | if (trailing) continue; | 433 | if (trailing) continue; |
| 367 | | 434 | |
| 368 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | 435 | if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { |
| 369 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | | |
| 370 | | | |
| 371 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; | | |
| 372 | | | |
| 373 | res.content_length = content_length; | | |
| 374 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | | |
| 375 | // Transfer-Encoding: second, first | 436 | // Transfer-Encoding: second, first |
| 376 | // Transfer-Encoding: deflate, chunked | 437 | // Transfer-Encoding: deflate, chunked |
| 377 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | 438 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); |
| 378 | | 439 | |
| 379 | if (iter.next()) |first| { | 440 | const first = iter.first(); |
| 380 | const trimmed = mem.trim(u8, first, " "); | 441 | const trimmed_first = mem.trim(u8, first, " "); |
| 381 | | 442 | |
| 382 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { | 443 | var next: ?[]const u8 = first; |
| 383 | if (res.transfer_encoding != null) return error.HttpHeadersInvalid; | 444 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { |
| 384 | res.transfer_encoding = te; | 445 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding |
| 385 | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 446 | res.transfer_encoding = transfer; |
| 386 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | | |
| 387 | res.transfer_compression = ce; | | |
| 388 | } else { | | |
| 389 | return error.HttpTransferEncodingUnsupported; | | |
| 390 | } | | |
| 391 | } | | |
| 392 | | 447 | |
| 393 | if (iter.next()) |second| { | 448 | next = iter.next(); |
| 394 | if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported; | 449 | } |
| 395 | | 450 | |
| 396 | const trimmed = mem.trim(u8, second, " "); | 451 | if (next) |second| { |
| | 452 | const trimmed_second = mem.trim(u8, second, " "); |
| 397 | | 453 | |
| 398 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | 454 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { |
| 399 | res.transfer_compression = ce; | 455 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported |
| | 456 | res.transfer_compression = transfer; |
| 400 | } else { | 457 | } else { |
| 401 | return error.HttpTransferEncodingUnsupported; | 458 | return error.HttpTransferEncodingUnsupported; |
| 402 | } | 459 | } |
| 403 | } | 460 | } |
| 404 | | 461 | |
| 405 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | 462 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; |
| | 463 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| | 464 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; |
| | 465 | |
| | 466 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; |
| | 467 | |
| | 468 | res.content_length = content_length; |
| 406 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | 469 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { |
| 407 | if (res.transfer_compression != null) return error.HttpHeadersInvalid; | 470 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; |
| 408 | | 471 | |
| 409 | const trimmed = mem.trim(u8, header_value, " "); | 472 | const trimmed = mem.trim(u8, header_value, " "); |
| 410 | | 473 | |
| ... | @@ -440,13 +503,21 @@ pub const Response = struct { | ... | @@ -440,13 +503,21 @@ pub const Response = struct { |
| 440 | status: http.Status, | 503 | status: http.Status, |
| 441 | reason: []const u8, | 504 | reason: []const u8, |
| 442 | | 505 | |
| | 506 | /// If present, the number of bytes in the response body. |
| 443 | content_length: ?u64 = null, | 507 | content_length: ?u64 = null, |
| 444 | transfer_encoding: ?http.TransferEncoding = null, | | |
| 445 | transfer_compression: ?http.ContentEncoding = null, | | |
| 446 | | 508 | |
| | 509 | /// If present, the transfer encoding of the response body, otherwise none. |
| | 510 | transfer_encoding: http.TransferEncoding = .none, |
| | 511 | |
| | 512 | /// If present, the compression of the response body, otherwise identity (no compression). |
| | 513 | transfer_compression: http.ContentEncoding = .identity, |
| | 514 | |
| | 515 | /// The headers received from the server. |
| 447 | headers: http.Headers, | 516 | headers: http.Headers, |
| 448 | parser: proto.HeadersParser, | 517 | parser: proto.HeadersParser, |
| 449 | compression: Compression = .none, | 518 | compression: Compression = .none, |
| | 519 | |
| | 520 | /// Whether the response body should be skipped. Any data read from the response body will be discarded. |
| 450 | skip: bool = false, | 521 | skip: bool = false, |
| 451 | }; | 522 | }; |
| 452 | | 523 | |
| ... | @@ -457,15 +528,18 @@ pub const Request = struct { | ... | @@ -457,15 +528,18 @@ pub const Request = struct { |
| 457 | uri: Uri, | 528 | uri: Uri, |
| 458 | client: *Client, | 529 | client: *Client, |
| 459 | /// is null when this connection is released | 530 | /// is null when this connection is released |
| 460 | connection: ?*ConnectionPool.Node, | 531 | connection: ?*Connection, |
| 461 | | 532 | |
| 462 | method: http.Method, | 533 | method: http.Method, |
| 463 | version: http.Version = .@"HTTP/1.1", | 534 | version: http.Version = .@"HTTP/1.1", |
| 464 | headers: http.Headers, | 535 | headers: http.Headers, |
| | 536 | |
| | 537 | /// The transfer encoding of the request body. |
| 465 | transfer_encoding: RequestTransfer = .none, | 538 | transfer_encoding: RequestTransfer = .none, |
| 466 | | 539 | |
| 467 | redirects_left: u32, | 540 | redirects_left: u32, |
| 468 | handle_redirects: bool, | 541 | handle_redirects: bool, |
| | 542 | handle_continue: bool, |
| 469 | | 543 | |
| 470 | response: Response, | 544 | response: Response, |
| 471 | | 545 | |
| ... | @@ -491,9 +565,9 @@ pub const Request = struct { | ... | @@ -491,9 +565,9 @@ pub const Request = struct { |
| 491 | if (req.connection) |connection| { | 565 | if (req.connection) |connection| { |
| 492 | if (!req.response.parser.done) { | 566 | if (!req.response.parser.done) { |
| 493 | // If the response wasn't fully read, then we need to close the connection. | 567 | // If the response wasn't fully read, then we need to close the connection. |
| 494 | connection.data.closing = true; | 568 | connection.closing = true; |
| 495 | } | 569 | } |
| 496 | req.client.connection_pool.release(req.client, connection); | 570 | req.client.connection_pool.release(req.client.allocator, connection); |
| 497 | } | 571 | } |
| 498 | | 572 | |
| 499 | req.arena.deinit(); | 573 | req.arena.deinit(); |
| ... | @@ -512,7 +586,7 @@ pub const Request = struct { | ... | @@ -512,7 +586,7 @@ pub const Request = struct { |
| 512 | .zstd => |*zstd| zstd.deinit(), | 586 | .zstd => |*zstd| zstd.deinit(), |
| 513 | } | 587 | } |
| 514 | | 588 | |
| 515 | req.client.connection_pool.release(req.client, req.connection.?); | 589 | req.client.connection_pool.release(req.client.allocator, req.connection.?); |
| 516 | req.connection = null; | 590 | req.connection = null; |
| 517 | | 591 | |
| 518 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; | 592 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| ... | @@ -539,42 +613,33 @@ pub const Request = struct { | ... | @@ -539,42 +613,33 @@ pub const Request = struct { |
| 539 | }; | 613 | }; |
| 540 | } | 614 | } |
| 541 | | 615 | |
| 542 | pub const StartError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; | 616 | pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; |
| 543 | | 617 | |
| 544 | pub const StartOptions = struct { | 618 | pub const SendOptions = struct { |
| 545 | /// Specifies that the uri should be used as is | 619 | /// Specifies that the uri should be used as is. You guarantee that the uri is already escaped. |
| 546 | raw_uri: bool = false, | 620 | raw_uri: bool = false, |
| 547 | }; | 621 | }; |
| 548 | | 622 | |
| 549 | /// Send the request to the server. | 623 | /// Send the HTTP request headers to the server. |
| 550 | pub fn start(req: *Request, options: StartOptions) StartError!void { | 624 | pub fn send(req: *Request, options: SendOptions) SendError!void { |
| 551 | if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding; | 625 | if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding; |
| 552 | | 626 | |
| 553 | var buffered = std.io.bufferedWriter(req.connection.?.data.writer()); | 627 | const w = req.connection.?.writer(); |
| 554 | const w = buffered.writer(); | | |
| 555 | | 628 | |
| 556 | try req.method.write(w); | 629 | try req.method.write(w); |
| 557 | try w.writeByte(' '); | 630 | try w.writeByte(' '); |
| 558 | | 631 | |
| 559 | if (req.method == .CONNECT) { | 632 | if (req.method == .CONNECT) { |
| 560 | try w.writeAll(req.uri.host.?); | 633 | try req.uri.writeToStream(.{ .authority = true }, w); |
| 561 | try w.writeByte(':'); | | |
| 562 | try w.print("{}", .{req.uri.port.?}); | | |
| 563 | } else { | 634 | } else { |
| 564 | if (req.connection.?.data.proxied) { | 635 | try req.uri.writeToStream(.{ |
| 565 | // proxied connections require the full uri | 636 | .scheme = req.connection.?.proxied, |
| 566 | if (options.raw_uri) { | 637 | .authentication = req.connection.?.proxied, |
| 567 | try w.print("{+/r}", .{req.uri}); | 638 | .authority = req.connection.?.proxied, |
| 568 | } else { | 639 | .path = true, |
| 569 | try w.print("{+/}", .{req.uri}); | 640 | .query = true, |
| 570 | } | 641 | .raw = options.raw_uri, |
| 571 | } else { | 642 | }, w); |
| 572 | if (options.raw_uri) { | | |
| 573 | try w.print("{/r}", .{req.uri}); | | |
| 574 | } else { | | |
| 575 | try w.print("{/}", .{req.uri}); | | |
| 576 | } | | |
| 577 | } | | |
| 578 | } | 643 | } |
| 579 | try w.writeByte(' '); | 644 | try w.writeByte(' '); |
| 580 | try w.writeAll(@tagName(req.version)); | 645 | try w.writeAll(@tagName(req.version)); |
| ... | @@ -582,7 +647,7 @@ pub const Request = struct { | ... | @@ -582,7 +647,7 @@ pub const Request = struct { |
| 582 | | 647 | |
| 583 | if (!req.headers.contains("host")) { | 648 | if (!req.headers.contains("host")) { |
| 584 | try w.writeAll("Host: "); | 649 | try w.writeAll("Host: "); |
| 585 | try w.writeAll(req.uri.host.?); | 650 | try req.uri.writeToStream(.{ .authority = true }, w); |
| 586 | try w.writeAll("\r\n"); | 651 | try w.writeAll("\r\n"); |
| 587 | } | 652 | } |
| 588 | | 653 | |
| ... | @@ -614,17 +679,17 @@ pub const Request = struct { | ... | @@ -614,17 +679,17 @@ pub const Request = struct { |
| 614 | .none => {}, | 679 | .none => {}, |
| 615 | } | 680 | } |
| 616 | } else { | 681 | } else { |
| 617 | if (has_content_length) { | 682 | if (has_transfer_encoding) { |
| 618 | const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; | | |
| 619 | | | |
| 620 | req.transfer_encoding = .{ .content_length = content_length }; | | |
| 621 | } else if (has_transfer_encoding) { | | |
| 622 | const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?; | 683 | const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?; |
| 623 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | 684 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { |
| 624 | req.transfer_encoding = .chunked; | 685 | req.transfer_encoding = .chunked; |
| 625 | } else { | 686 | } else { |
| 626 | return error.UnsupportedTransferEncoding; | 687 | return error.UnsupportedTransferEncoding; |
| 627 | } | 688 | } |
| | 689 | } else if (has_content_length) { |
| | 690 | const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; |
| | 691 | |
| | 692 | req.transfer_encoding = .{ .content_length = content_length }; |
| 628 | } else { | 693 | } else { |
| 629 | req.transfer_encoding = .none; | 694 | req.transfer_encoding = .none; |
| 630 | } | 695 | } |
| ... | @@ -639,9 +704,27 @@ pub const Request = struct { | ... | @@ -639,9 +704,27 @@ pub const Request = struct { |
| 639 | try w.writeAll("\r\n"); | 704 | try w.writeAll("\r\n"); |
| 640 | } | 705 | } |
| 641 | | 706 | |
| | 707 | if (req.connection.?.proxied) { |
| | 708 | const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) { |
| | 709 | .plain => if (req.client.http_proxy) |proxy| proxy.headers else null, |
| | 710 | .tls => if (req.client.https_proxy) |proxy| proxy.headers else null, |
| | 711 | }; |
| | 712 | |
| | 713 | if (proxy_headers) |headers| { |
| | 714 | for (headers.list.items) |entry| { |
| | 715 | if (entry.value.len == 0) continue; |
| | 716 | |
| | 717 | try w.writeAll(entry.name); |
| | 718 | try w.writeAll(": "); |
| | 719 | try w.writeAll(entry.value); |
| | 720 | try w.writeAll("\r\n"); |
| | 721 | } |
| | 722 | } |
| | 723 | } |
| | 724 | |
| 642 | try w.writeAll("\r\n"); | 725 | try w.writeAll("\r\n"); |
| 643 | | 726 | |
| 644 | try buffered.flush(); | 727 | try req.connection.?.flush(); |
| 645 | } | 728 | } |
| 646 | | 729 | |
| 647 | const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; | 730 | const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; |
| ... | @@ -657,7 +740,7 @@ pub const Request = struct { | ... | @@ -657,7 +740,7 @@ pub const Request = struct { |
| 657 | | 740 | |
| 658 | var index: usize = 0; | 741 | var index: usize = 0; |
| 659 | while (index == 0) { | 742 | while (index == 0) { |
| 660 | const amt = try req.response.parser.read(&req.connection.?.data, buf[index..], req.response.skip); | 743 | const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); |
| 661 | if (amt == 0 and req.response.parser.done) break; | 744 | if (amt == 0 and req.response.parser.done) break; |
| 662 | index += amt; | 745 | index += amt; |
| 663 | } | 746 | } |
| ... | @@ -665,20 +748,22 @@ pub const Request = struct { | ... | @@ -665,20 +748,22 @@ pub const Request = struct { |
| 665 | return index; | 748 | return index; |
| 666 | } | 749 | } |
| 667 | | 750 | |
| 668 | pub const WaitError = RequestError || StartError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported }; | 751 | pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported }; |
| 669 | | 752 | |
| 670 | /// Waits for a response from the server and parses any headers that are sent. | 753 | /// Waits for a response from the server and parses any headers that are sent. |
| 671 | /// This function will block until the final response is received. | 754 | /// This function will block until the final response is received. |
| 672 | /// | 755 | /// |
| 673 | /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow | 756 | /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow |
| 674 | /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend. | 757 | /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend. |
| | 758 | /// |
| | 759 | /// Must be called after `start` and, if any data was written to the request body, then also after `finish`. |
| 675 | pub fn wait(req: *Request) WaitError!void { | 760 | pub fn wait(req: *Request) WaitError!void { |
| 676 | while (true) { // handle redirects | 761 | while (true) { // handle redirects |
| 677 | while (true) { // read headers | 762 | while (true) { // read headers |
| 678 | try req.connection.?.data.fill(); | 763 | try req.connection.?.fill(); |
| 679 | | 764 | |
| 680 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek()); | 765 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); |
| 681 | req.connection.?.data.drop(@as(u16, @intCast(nchecked))); | 766 | req.connection.?.drop(@intCast(nchecked)); |
| 682 | | 767 | |
| 683 | if (req.response.parser.state.isContent()) break; | 768 | if (req.response.parser.state.isContent()) break; |
| 684 | } | 769 | } |
| ... | @@ -688,12 +773,16 @@ pub const Request = struct { | ... | @@ -688,12 +773,16 @@ pub const Request = struct { |
| 688 | if (req.response.status == .@"continue") { | 773 | if (req.response.status == .@"continue") { |
| 689 | req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response | 774 | req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response |
| 690 | req.response.parser.reset(); | 775 | req.response.parser.reset(); |
| | 776 | |
| | 777 | if (req.handle_continue) |
| | 778 | continue; |
| | 779 | |
| 691 | break; | 780 | break; |
| 692 | } | 781 | } |
| 693 | | 782 | |
| 694 | // we're switching protocols, so this connection is no longer doing http | 783 | // we're switching protocols, so this connection is no longer doing http |
| 695 | if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) { | 784 | if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) { |
| 696 | req.connection.?.data.closing = false; | 785 | req.connection.?.closing = false; |
| 697 | req.response.parser.done = true; | 786 | req.response.parser.done = true; |
| 698 | } | 787 | } |
| 699 | | 788 | |
| ... | @@ -704,13 +793,14 @@ pub const Request = struct { | ... | @@ -704,13 +793,14 @@ pub const Request = struct { |
| 704 | const res_connection = req.response.headers.getFirstValue("connection"); | 793 | const res_connection = req.response.headers.getFirstValue("connection"); |
| 705 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | 794 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); |
| 706 | if (res_keepalive and (req_keepalive or req_connection == null)) { | 795 | if (res_keepalive and (req_keepalive or req_connection == null)) { |
| 707 | req.connection.?.data.closing = false; | 796 | req.connection.?.closing = false; |
| 708 | } else { | 797 | } else { |
| 709 | req.connection.?.data.closing = true; | 798 | req.connection.?.closing = true; |
| 710 | } | 799 | } |
| 711 | | 800 | |
| 712 | if (req.response.transfer_encoding) |te| { | 801 | if (req.response.transfer_encoding != .none) { |
| 713 | switch (te) { | 802 | switch (req.response.transfer_encoding) { |
| | 803 | .none => unreachable, |
| 714 | .chunked => { | 804 | .chunked => { |
| 715 | req.response.parser.next_chunk_length = 0; | 805 | req.response.parser.next_chunk_length = 0; |
| 716 | req.response.parser.state = .chunk_head_size; | 806 | req.response.parser.state = .chunk_head_size; |
| ... | @@ -774,23 +864,23 @@ pub const Request = struct { | ... | @@ -774,23 +864,23 @@ pub const Request = struct { |
| 774 | | 864 | |
| 775 | try req.redirect(resolved_url); | 865 | try req.redirect(resolved_url); |
| 776 | | 866 | |
| 777 | try req.start(.{}); | 867 | try req.send(.{}); |
| 778 | } else { | 868 | } else { |
| 779 | req.response.skip = false; | 869 | req.response.skip = false; |
| 780 | if (!req.response.parser.done) { | 870 | if (!req.response.parser.done) { |
| 781 | if (req.response.transfer_compression) |tc| switch (tc) { | 871 | switch (req.response.transfer_compression) { |
| 782 | .identity => req.response.compression = .none, | 872 | .identity => req.response.compression = .none, |
| 783 | .compress => return error.CompressionNotSupported, | 873 | .compress, .@"x-compress" => return error.CompressionNotSupported, |
| 784 | .deflate => req.response.compression = .{ | 874 | .deflate => req.response.compression = .{ |
| 785 | .deflate = std.compress.zlib.decompressStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, | 875 | .deflate = std.compress.zlib.decompressStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, |
| 786 | }, | 876 | }, |
| 787 | .gzip => req.response.compression = .{ | 877 | .gzip, .@"x-gzip" => req.response.compression = .{ |
| 788 | .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, | 878 | .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, |
| 789 | }, | 879 | }, |
| 790 | .zstd => req.response.compression = .{ | 880 | .zstd => req.response.compression = .{ |
| 791 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), | 881 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), |
| 792 | }, | 882 | }, |
| 793 | }; | 883 | } |
| 794 | } | 884 | } |
| 795 | | 885 | |
| 796 | break; | 886 | break; |
| ... | @@ -806,7 +896,7 @@ pub const Request = struct { | ... | @@ -806,7 +896,7 @@ pub const Request = struct { |
| 806 | return .{ .context = req }; | 896 | return .{ .context = req }; |
| 807 | } | 897 | } |
| 808 | | 898 | |
| 809 | /// Reads data from the response body. Must be called after `do`. | 899 | /// Reads data from the response body. Must be called after `wait`. |
| 810 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | 900 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { |
| 811 | const out_index = switch (req.response.compression) { | 901 | const out_index = switch (req.response.compression) { |
| 812 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, | 902 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| ... | @@ -819,15 +909,13 @@ pub const Request = struct { | ... | @@ -819,15 +909,13 @@ pub const Request = struct { |
| 819 | const has_trail = !req.response.parser.state.isContent(); | 909 | const has_trail = !req.response.parser.state.isContent(); |
| 820 | | 910 | |
| 821 | while (!req.response.parser.state.isContent()) { // read trailing headers | 911 | while (!req.response.parser.state.isContent()) { // read trailing headers |
| 822 | try req.connection.?.data.fill(); | 912 | try req.connection.?.fill(); |
| 823 | | 913 | |
| 824 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.data.peek()); | 914 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); |
| 825 | req.connection.?.data.drop(@as(u16, @intCast(nchecked))); | 915 | req.connection.?.drop(@intCast(nchecked)); |
| 826 | } | 916 | } |
| 827 | | 917 | |
| 828 | if (has_trail) { | 918 | if (has_trail) { |
| 829 | req.response.headers.clearRetainingCapacity(); | | |
| 830 | | | |
| 831 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. | 919 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. |
| 832 | // This will *only* fail for a malformed trailer. | 920 | // This will *only* fail for a malformed trailer. |
| 833 | req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers; | 921 | req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers; |
| ... | @@ -837,7 +925,7 @@ pub const Request = struct { | ... | @@ -837,7 +925,7 @@ pub const Request = struct { |
| 837 | return out_index; | 925 | return out_index; |
| 838 | } | 926 | } |
| 839 | | 927 | |
| 840 | /// Reads data from the response body. Must be called after `do`. | 928 | /// Reads data from the response body. Must be called after `wait`. |
| 841 | pub fn readAll(req: *Request, buffer: []u8) !usize { | 929 | pub fn readAll(req: *Request, buffer: []u8) !usize { |
| 842 | var index: usize = 0; | 930 | var index: usize = 0; |
| 843 | while (index < buffer.len) { | 931 | while (index < buffer.len) { |
| ... | @@ -856,20 +944,21 @@ pub const Request = struct { | ... | @@ -856,20 +944,21 @@ pub const Request = struct { |
| 856 | return .{ .context = req }; | 944 | return .{ .context = req }; |
| 857 | } | 945 | } |
| 858 | | 946 | |
| 859 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. | 947 | /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. |
| | 948 | /// Must be called after `start` and before `finish`. |
| 860 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { | 949 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { |
| 861 | switch (req.transfer_encoding) { | 950 | switch (req.transfer_encoding) { |
| 862 | .chunked => { | 951 | .chunked => { |
| 863 | try req.connection.?.data.writer().print("{x}\r\n", .{bytes.len}); | 952 | try req.connection.?.writer().print("{x}\r\n", .{bytes.len}); |
| 864 | try req.connection.?.data.writeAll(bytes); | 953 | try req.connection.?.writer().writeAll(bytes); |
| 865 | try req.connection.?.data.writeAll("\r\n"); | 954 | try req.connection.?.writer().writeAll("\r\n"); |
| 866 | | 955 | |
| 867 | return bytes.len; | 956 | return bytes.len; |
| 868 | }, | 957 | }, |
| 869 | .content_length => |*len| { | 958 | .content_length => |*len| { |
| 870 | if (len.* < bytes.len) return error.MessageTooLong; | 959 | if (len.* < bytes.len) return error.MessageTooLong; |
| 871 | | 960 | |
| 872 | const amt = try req.connection.?.data.write(bytes); | 961 | const amt = try req.connection.?.write(bytes); |
| 873 | len.* -= amt; | 962 | len.* -= amt; |
| 874 | return amt; | 963 | return amt; |
| 875 | }, | 964 | }, |
| ... | @@ -877,6 +966,8 @@ pub const Request = struct { | ... | @@ -877,6 +966,8 @@ pub const Request = struct { |
| 877 | } | 966 | } |
| 878 | } | 967 | } |
| 879 | | 968 | |
| | 969 | /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. |
| | 970 | /// Must be called after `start` and before `finish`. |
| 880 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | 971 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { |
| 881 | var index: usize = 0; | 972 | var index: usize = 0; |
| 882 | while (index < bytes.len) { | 973 | while (index < bytes.len) { |
| ... | @@ -887,50 +978,169 @@ pub const Request = struct { | ... | @@ -887,50 +978,169 @@ pub const Request = struct { |
| 887 | pub const FinishError = WriteError || error{MessageNotCompleted}; | 978 | pub const FinishError = WriteError || error{MessageNotCompleted}; |
| 888 | | 979 | |
| 889 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 980 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| | 981 | /// Must be called after `start`. |
| 890 | pub fn finish(req: *Request) FinishError!void { | 982 | pub fn finish(req: *Request) FinishError!void { |
| 891 | switch (req.transfer_encoding) { | 983 | switch (req.transfer_encoding) { |
| 892 | .chunked => try req.connection.?.data.writeAll("0\r\n\r\n"), | 984 | .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"), |
| 893 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | 985 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 894 | .none => {}, | 986 | .none => {}, |
| 895 | } | 987 | } |
| | 988 | |
| | 989 | try req.connection.?.flush(); |
| 896 | } | 990 | } |
| 897 | }; | 991 | }; |
| 898 | | 992 | |
| 899 | pub const HttpProxy = struct { | 993 | pub const Proxy = struct { |
| 900 | pub const ProxyAuthentication = union(enum) { | 994 | allocator: Allocator, |
| 901 | basic: []const u8, | 995 | headers: http.Headers, |
| 902 | custom: []const u8, | | |
| 903 | }; | | |
| 904 | | 996 | |
| 905 | protocol: Connection.Protocol, | 997 | protocol: Connection.Protocol, |
| 906 | host: []const u8, | 998 | host: []const u8, |
| 907 | port: ?u16 = null, | 999 | port: u16, |
| 908 | | 1000 | |
| 909 | /// The value for the Proxy-Authorization header. | 1001 | supports_connect: bool = true, |
| 910 | auth: ?ProxyAuthentication = null, | | |
| 911 | }; | 1002 | }; |
| 912 | | 1003 | |
| 913 | /// Release all associated resources with the client. | 1004 | /// Release all associated resources with the client. |
| 914 | /// TODO: currently leaks all request allocated data | 1005 | /// |
| | 1006 | /// All pending requests must be de-initialized and all active connections released |
| | 1007 | /// before calling this function. |
| 915 | pub fn deinit(client: *Client) void { | 1008 | pub fn deinit(client: *Client) void { |
| 916 | client.connection_pool.deinit(client); | 1009 | assert(client.connection_pool.used.first == null); // There are still active requests. |
| | 1010 | |
| | 1011 | client.connection_pool.deinit(client.allocator); |
| | 1012 | |
| | 1013 | if (client.http_proxy) |*proxy| { |
| | 1014 | proxy.allocator.free(proxy.host); |
| | 1015 | proxy.headers.deinit(); |
| | 1016 | } |
| | 1017 | |
| | 1018 | if (client.https_proxy) |*proxy| { |
| | 1019 | proxy.allocator.free(proxy.host); |
| | 1020 | proxy.headers.deinit(); |
| | 1021 | } |
| | 1022 | |
| | 1023 | if (!disable_tls) |
| | 1024 | client.ca_bundle.deinit(client.allocator); |
| 917 | | 1025 | |
| 918 | client.ca_bundle.deinit(client.allocator); | | |
| 919 | client.* = undefined; | 1026 | client.* = undefined; |
| 920 | } | 1027 | } |
| 921 | | 1028 | |
| 922 | pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; | 1029 | /// Uses the *_proxy environment variable to set any unset proxies for the client. |
| | 1030 | /// This function *must not* be called when the client has any active connections. |
| | 1031 | pub fn loadDefaultProxies(client: *Client) !void { |
| | 1032 | // Prevent any new connections from being created. |
| | 1033 | client.connection_pool.mutex.lock(); |
| | 1034 | defer client.connection_pool.mutex.unlock(); |
| | 1035 | |
| | 1036 | assert(client.connection_pool.used.first == null); // There are still active requests. |
| | 1037 | |
| | 1038 | if (client.http_proxy == null) http: { |
| | 1039 | const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy")) |
| | 1040 | try std.process.getEnvVarOwned(client.allocator, "http_proxy") |
| | 1041 | else if (std.process.hasEnvVarConstant("HTTP_PROXY")) |
| | 1042 | try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY") |
| | 1043 | else if (std.process.hasEnvVarConstant("all_proxy")) |
| | 1044 | try std.process.getEnvVarOwned(client.allocator, "all_proxy") |
| | 1045 | else if (std.process.hasEnvVarConstant("ALL_PROXY")) |
| | 1046 | try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY") |
| | 1047 | else |
| | 1048 | break :http; |
| | 1049 | defer client.allocator.free(content); |
| | 1050 | |
| | 1051 | const uri = try Uri.parse(content); |
| | 1052 | |
| | 1053 | const protocol = protocol_map.get(uri.scheme) orelse break :http; // Unknown scheme, ignore |
| | 1054 | const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :http; // Missing host, ignore |
| | 1055 | client.http_proxy = .{ |
| | 1056 | .allocator = client.allocator, |
| | 1057 | .headers = .{ .allocator = client.allocator }, |
| | 1058 | |
| | 1059 | .protocol = protocol, |
| | 1060 | .host = host, |
| | 1061 | .port = uri.port orelse switch (protocol) { |
| | 1062 | .plain => 80, |
| | 1063 | .tls => 443, |
| | 1064 | }, |
| | 1065 | }; |
| | 1066 | |
| | 1067 | if (uri.user != null and uri.password != null) { |
| | 1068 | const prefix_len = "Basic ".len; |
| | 1069 | |
| | 1070 | const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? }); |
| | 1071 | defer client.allocator.free(unencoded); |
| | 1072 | |
| | 1073 | const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix_len); |
| | 1074 | defer client.allocator.free(buffer); |
| | 1075 | |
| | 1076 | const result = std.base64.standard.Encoder.encode(buffer[prefix_len..], unencoded); |
| | 1077 | @memcpy(buffer[0..prefix_len], "Basic "); |
| | 1078 | |
| | 1079 | try client.http_proxy.?.headers.append("proxy-authorization", result); |
| | 1080 | } |
| | 1081 | } |
| | 1082 | |
| | 1083 | if (client.https_proxy == null) https: { |
| | 1084 | const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy")) |
| | 1085 | try std.process.getEnvVarOwned(client.allocator, "https_proxy") |
| | 1086 | else if (std.process.hasEnvVarConstant("HTTPS_PROXY")) |
| | 1087 | try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY") |
| | 1088 | else if (std.process.hasEnvVarConstant("all_proxy")) |
| | 1089 | try std.process.getEnvVarOwned(client.allocator, "all_proxy") |
| | 1090 | else if (std.process.hasEnvVarConstant("ALL_PROXY")) |
| | 1091 | try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY") |
| | 1092 | else |
| | 1093 | break :https; |
| | 1094 | defer client.allocator.free(content); |
| | 1095 | |
| | 1096 | const uri = try Uri.parse(content); |
| | 1097 | |
| | 1098 | const protocol = protocol_map.get(uri.scheme) orelse break :https; // Unknown scheme, ignore |
| | 1099 | const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :https; // Missing host, ignore |
| | 1100 | client.http_proxy = .{ |
| | 1101 | .allocator = client.allocator, |
| | 1102 | .headers = .{ .allocator = client.allocator }, |
| | 1103 | |
| | 1104 | .protocol = protocol, |
| | 1105 | .host = host, |
| | 1106 | .port = uri.port orelse switch (protocol) { |
| | 1107 | .plain => 80, |
| | 1108 | .tls => 443, |
| | 1109 | }, |
| | 1110 | }; |
| | 1111 | |
| | 1112 | if (uri.user != null and uri.password != null) { |
| | 1113 | const prefix_len = "Basic ".len; |
| | 1114 | |
| | 1115 | const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? }); |
| | 1116 | defer client.allocator.free(unencoded); |
| | 1117 | |
| | 1118 | const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix_len); |
| | 1119 | defer client.allocator.free(buffer); |
| | 1120 | |
| | 1121 | const result = std.base64.standard.Encoder.encode(buffer[prefix_len..], unencoded); |
| | 1122 | @memcpy(buffer[0..prefix_len], "Basic "); |
| | 1123 | |
| | 1124 | try client.https_proxy.?.headers.append("proxy-authorization", result); |
| | 1125 | } |
| | 1126 | } |
| | 1127 | } |
| | 1128 | |
| | 1129 | pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; |
| 923 | | 1130 | |
| 924 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. | 1131 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 925 | /// This function is threadsafe. | 1132 | /// This function is threadsafe. |
| 926 | pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node { | 1133 | pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection { |
| 927 | if (client.connection_pool.findConnection(.{ | 1134 | if (client.connection_pool.findConnection(.{ |
| 928 | .host = host, | 1135 | .host = host, |
| 929 | .port = port, | 1136 | .port = port, |
| 930 | .is_tls = protocol == .tls, | 1137 | .protocol = protocol, |
| 931 | })) |node| | 1138 | })) |node| |
| 932 | return node; | 1139 | return node; |
| 933 | | 1140 | |
| | 1141 | if (disable_tls and protocol == .tls) |
| | 1142 | return error.TlsInitializationFailed; |
| | 1143 | |
| 934 | const conn = try client.allocator.create(ConnectionPool.Node); | 1144 | const conn = try client.allocator.create(ConnectionPool.Node); |
| 935 | errdefer client.allocator.destroy(conn); | 1145 | errdefer client.allocator.destroy(conn); |
| 936 | conn.* = .{ .data = undefined }; | 1146 | conn.* = .{ .data = undefined }; |
| ... | @@ -951,40 +1161,41 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: | ... | @@ -951,40 +1161,41 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: |
| 951 | conn.data = .{ | 1161 | conn.data = .{ |
| 952 | .stream = stream, | 1162 | .stream = stream, |
| 953 | .tls_client = undefined, | 1163 | .tls_client = undefined, |
| 954 | .protocol = protocol, | | |
| 955 | | 1164 | |
| | 1165 | .protocol = protocol, |
| 956 | .host = try client.allocator.dupe(u8, host), | 1166 | .host = try client.allocator.dupe(u8, host), |
| 957 | .port = port, | 1167 | .port = port, |
| 958 | }; | 1168 | }; |
| 959 | errdefer client.allocator.free(conn.data.host); | 1169 | errdefer client.allocator.free(conn.data.host); |
| 960 | | 1170 | |
| 961 | switch (protocol) { | 1171 | if (protocol == .tls) { |
| 962 | .plain => {}, | 1172 | if (disable_tls) unreachable; |
| 963 | .tls => { | | |
| 964 | conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client); | | |
| 965 | errdefer client.allocator.destroy(conn.data.tls_client); | | |
| 966 | | 1173 | |
| 967 | conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed; | 1174 | conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client); |
| 968 | // This is appropriate for HTTPS because the HTTP headers contain | 1175 | errdefer client.allocator.destroy(conn.data.tls_client); |
| 969 | // the content length which is used to detect truncation attacks. | 1176 | |
| 970 | conn.data.tls_client.allow_truncation_attacks = true; | 1177 | conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed; |
| 971 | }, | 1178 | // This is appropriate for HTTPS because the HTTP headers contain |
| | 1179 | // the content length which is used to detect truncation attacks. |
| | 1180 | conn.data.tls_client.allow_truncation_attacks = true; |
| 972 | } | 1181 | } |
| 973 | | 1182 | |
| 974 | client.connection_pool.addUsed(conn); | 1183 | client.connection_pool.addUsed(conn); |
| 975 | | 1184 | |
| 976 | return conn; | 1185 | return &conn.data; |
| 977 | } | 1186 | } |
| 978 | | 1187 | |
| 979 | pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError; | 1188 | pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError; |
| 980 | | 1189 | |
| 981 | pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node { | 1190 | /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. |
| | 1191 | /// This function is threadsafe. |
| | 1192 | pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { |
| 982 | if (!net.has_unix_sockets) return error.Unsupported; | 1193 | if (!net.has_unix_sockets) return error.Unsupported; |
| 983 | | 1194 | |
| 984 | if (client.connection_pool.findConnection(.{ | 1195 | if (client.connection_pool.findConnection(.{ |
| 985 | .host = path, | 1196 | .host = path, |
| 986 | .port = 0, | 1197 | .port = 0, |
| 987 | .is_tls = false, | 1198 | .protocol = .plain, |
| 988 | })) |node| | 1199 | })) |node| |
| 989 | return node; | 1200 | return node; |
| 990 | | 1201 | |
| ... | @@ -1007,37 +1218,130 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti | ... | @@ -1007,37 +1218,130 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti |
| 1007 | | 1218 | |
| 1008 | client.connection_pool.addUsed(conn); | 1219 | client.connection_pool.addUsed(conn); |
| 1009 | | 1220 | |
| 1010 | return conn; | 1221 | return &conn.data; |
| 1011 | } | 1222 | } |
| 1012 | | 1223 | |
| 1013 | // Prevents a dependency loop in request() | 1224 | /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open. |
| 1014 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; | 1225 | /// This function is threadsafe. |
| 1015 | pub const ConnectError = ConnectErrorPartial || RequestError; | 1226 | pub fn connectTunnel( |
| | 1227 | client: *Client, |
| | 1228 | proxy: *Proxy, |
| | 1229 | tunnel_host: []const u8, |
| | 1230 | tunnel_port: u16, |
| | 1231 | ) !*Connection { |
| | 1232 | if (!proxy.supports_connect) return error.TunnelNotSupported; |
| 1016 | | 1233 | |
| 1017 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { | | |
| 1018 | if (client.connection_pool.findConnection(.{ | 1234 | if (client.connection_pool.findConnection(.{ |
| 1019 | .host = host, | 1235 | .host = tunnel_host, |
| 1020 | .port = port, | 1236 | .port = tunnel_port, |
| 1021 | .is_tls = protocol == .tls, | 1237 | .protocol = proxy.protocol, |
| 1022 | })) |node| | 1238 | })) |node| |
| 1023 | return node; | 1239 | return node; |
| 1024 | | 1240 | |
| 1025 | if (client.proxy) |proxy| { | 1241 | var maybe_valid = false; |
| 1026 | const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) { | 1242 | (tunnel: { |
| 1027 | .plain => 80, | 1243 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); |
| 1028 | .tls => 443, | 1244 | errdefer { |
| | 1245 | conn.closing = true; |
| | 1246 | client.connection_pool.release(client.allocator, conn); |
| | 1247 | } |
| | 1248 | |
| | 1249 | const uri = Uri{ |
| | 1250 | .scheme = "http", |
| | 1251 | .user = null, |
| | 1252 | .password = null, |
| | 1253 | .host = tunnel_host, |
| | 1254 | .port = tunnel_port, |
| | 1255 | .path = "", |
| | 1256 | .query = null, |
| | 1257 | .fragment = null, |
| | 1258 | }; |
| | 1259 | |
| | 1260 | // we can use a small buffer here because a CONNECT response should be very small |
| | 1261 | var buffer: [8096]u8 = undefined; |
| | 1262 | |
| | 1263 | var req = client.open(.CONNECT, uri, proxy.headers, .{ |
| | 1264 | .handle_redirects = false, |
| | 1265 | .connection = conn, |
| | 1266 | .header_strategy = .{ .static = &buffer }, |
| | 1267 | }) catch |err| { |
| | 1268 | std.log.debug("err {}", .{err}); |
| | 1269 | break :tunnel err; |
| 1029 | }; | 1270 | }; |
| | 1271 | defer req.deinit(); |
| | 1272 | |
| | 1273 | req.send(.{ .raw_uri = true }) catch |err| break :tunnel err; |
| | 1274 | req.wait() catch |err| break :tunnel err; |
| | 1275 | |
| | 1276 | if (req.response.status.class() == .server_error) { |
| | 1277 | maybe_valid = true; |
| | 1278 | break :tunnel error.ServerError; |
| | 1279 | } |
| | 1280 | |
| | 1281 | if (req.response.status != .ok) break :tunnel error.ConnectionRefused; |
| 1030 | | 1282 | |
| 1031 | const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol); | 1283 | // 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. |
| 1032 | conn.data.proxied = true; | 1284 | req.connection = null; |
| | 1285 | |
| | 1286 | client.allocator.free(conn.host); |
| | 1287 | conn.host = try client.allocator.dupe(u8, tunnel_host); |
| | 1288 | errdefer client.allocator.free(conn.host); |
| | 1289 | |
| | 1290 | conn.port = tunnel_port; |
| | 1291 | conn.closing = false; |
| 1033 | | 1292 | |
| 1034 | return conn; | 1293 | return conn; |
| 1035 | } else { | 1294 | }) catch { |
| 1036 | return client.connectUnproxied(host, port, protocol); | 1295 | // something went wrong with the tunnel |
| | 1296 | proxy.supports_connect = maybe_valid; |
| | 1297 | return error.TunnelNotSupported; |
| | 1298 | }; |
| | 1299 | } |
| | 1300 | |
| | 1301 | // Prevents a dependency loop in request() |
| | 1302 | const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused }; |
| | 1303 | pub const ConnectError = ConnectErrorPartial || RequestError; |
| | 1304 | |
| | 1305 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| | 1306 | /// |
| | 1307 | /// If a proxy is configured for the client, then the proxy will be used to connect to the host. |
| | 1308 | /// |
| | 1309 | /// This function is threadsafe. |
| | 1310 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection { |
| | 1311 | // pointer required so that `supports_connect` can be updated if a CONNECT fails |
| | 1312 | const potential_proxy: ?*Proxy = switch (protocol) { |
| | 1313 | .plain => if (client.http_proxy) |*proxy_info| proxy_info else null, |
| | 1314 | .tls => if (client.https_proxy) |*proxy_info| proxy_info else null, |
| | 1315 | }; |
| | 1316 | |
| | 1317 | if (potential_proxy) |proxy| { |
| | 1318 | // don't attempt to proxy the proxy thru itself. |
| | 1319 | if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) { |
| | 1320 | return client.connectTcp(host, port, protocol); |
| | 1321 | } |
| | 1322 | |
| | 1323 | if (proxy.supports_connect) tunnel: { |
| | 1324 | return connectTunnel(client, proxy, host, port) catch |err| switch (err) { |
| | 1325 | error.TunnelNotSupported => break :tunnel, |
| | 1326 | else => |e| return e, |
| | 1327 | }; |
| | 1328 | } |
| | 1329 | |
| | 1330 | // fall back to using the proxy as a normal http proxy |
| | 1331 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); |
| | 1332 | errdefer { |
| | 1333 | conn.closing = true; |
| | 1334 | client.connection_pool.release(conn); |
| | 1335 | } |
| | 1336 | |
| | 1337 | conn.proxied = true; |
| | 1338 | return conn; |
| 1037 | } | 1339 | } |
| | 1340 | |
| | 1341 | return client.connectTcp(host, port, protocol); |
| 1038 | } | 1342 | } |
| 1039 | | 1343 | |
| 1040 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || Connection.WriteError || error{ | 1344 | pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || std.fmt.ParseIntError || Connection.WriteError || error{ |
| 1041 | UnsupportedUrlScheme, | 1345 | UnsupportedUrlScheme, |
| 1042 | UriMissingHost, | 1346 | UriMissingHost, |
| 1043 | | 1347 | |
| ... | @@ -1048,12 +1352,20 @@ pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request | ... | @@ -1048,12 +1352,20 @@ pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request |
| 1048 | pub const RequestOptions = struct { | 1352 | pub const RequestOptions = struct { |
| 1049 | version: http.Version = .@"HTTP/1.1", | 1353 | version: http.Version = .@"HTTP/1.1", |
| 1050 | | 1354 | |
| | 1355 | /// Automatically ignore 100 Continue responses. This assumes you don't care, and will have sent the body before you |
| | 1356 | /// wait for the response. |
| | 1357 | /// |
| | 1358 | /// If this is not the case AND you know the server will send a 100 Continue, set this to false and wait for a |
| | 1359 | /// response before sending the body. If you wait AND the server does not send a 100 Continue before you finish the |
| | 1360 | /// request, then the request *will* deadlock. |
| | 1361 | handle_continue: bool = true, |
| | 1362 | |
| 1051 | handle_redirects: bool = true, | 1363 | handle_redirects: bool = true, |
| 1052 | max_redirects: u32 = 3, | 1364 | max_redirects: u32 = 3, |
| 1053 | header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 }, | 1365 | header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 }, |
| 1054 | | 1366 | |
| 1055 | /// Must be an already acquired connection. | 1367 | /// Must be an already acquired connection. |
| 1056 | connection: ?*ConnectionPool.Node = null, | 1368 | connection: ?*Connection = null, |
| 1057 | | 1369 | |
| 1058 | pub const StorageStrategy = union(enum) { | 1370 | pub const StorageStrategy = union(enum) { |
| 1059 | /// In this case, the client's Allocator will be used to store the | 1371 | /// In this case, the client's Allocator will be used to store the |
| ... | @@ -1076,14 +1388,14 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ | ... | @@ -1076,14 +1388,14 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| 1076 | .{ "wss", .tls }, | 1388 | .{ "wss", .tls }, |
| 1077 | }); | 1389 | }); |
| 1078 | | 1390 | |
| 1079 | /// Form and send a http request to a server. | 1391 | /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. |
| 1080 | /// | 1392 | /// |
| 1081 | /// `uri` must remain alive during the entire request. | 1393 | /// `uri` must remain alive during the entire request. |
| 1082 | /// `headers` is cloned and may be freed after this function returns. | 1394 | /// `headers` is cloned and may be freed after this function returns. |
| 1083 | /// | 1395 | /// |
| 1084 | /// The caller is responsible for calling `deinit()` on the `Request`. | 1396 | /// The caller is responsible for calling `deinit()` on the `Request`. |
| 1085 | /// This function is threadsafe. | 1397 | /// This function is threadsafe. |
| 1086 | pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request { | 1398 | pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request { |
| 1087 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; | 1399 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 1088 | | 1400 | |
| 1089 | const port: u16 = uri.port orelse switch (protocol) { | 1401 | const port: u16 = uri.port orelse switch (protocol) { |
| ... | @@ -1094,6 +1406,8 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea | ... | @@ -1094,6 +1406,8 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea |
| 1094 | const host = uri.host orelse return error.UriMissingHost; | 1406 | const host = uri.host orelse return error.UriMissingHost; |
| 1095 | | 1407 | |
| 1096 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { | 1408 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { |
| | 1409 | if (disable_tls) unreachable; |
| | 1410 | |
| 1097 | client.ca_bundle_mutex.lock(); | 1411 | client.ca_bundle_mutex.lock(); |
| 1098 | defer client.ca_bundle_mutex.unlock(); | 1412 | defer client.ca_bundle_mutex.unlock(); |
| 1099 | | 1413 | |
| ... | @@ -1114,6 +1428,7 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea | ... | @@ -1114,6 +1428,7 @@ pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Hea |
| 1114 | .version = options.version, | 1428 | .version = options.version, |
| 1115 | .redirects_left = options.max_redirects, | 1429 | .redirects_left = options.max_redirects, |
| 1116 | .handle_redirects = options.handle_redirects, | 1430 | .handle_redirects = options.handle_redirects, |
| | 1431 | .handle_continue = options.handle_continue, |
| 1117 | .response = .{ | 1432 | .response = .{ |
| 1118 | .status = undefined, | 1433 | .status = undefined, |
| 1119 | .reason = undefined, | 1434 | .reason = undefined, |
| ... | @@ -1178,6 +1493,9 @@ pub const FetchResult = struct { | ... | @@ -1178,6 +1493,9 @@ pub const FetchResult = struct { |
| 1178 | } | 1493 | } |
| 1179 | }; | 1494 | }; |
| 1180 | | 1495 | |
| | 1496 | /// Perform a one-shot HTTP request with the provided options. |
| | 1497 | /// |
| | 1498 | /// This function is threadsafe. |
| 1181 | pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult { | 1499 | pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult { |
| 1182 | const has_transfer_encoding = options.headers.contains("transfer-encoding"); | 1500 | const has_transfer_encoding = options.headers.contains("transfer-encoding"); |
| 1183 | const has_content_length = options.headers.contains("content-length"); | 1501 | const has_content_length = options.headers.contains("content-length"); |
| ... | @@ -1189,7 +1507,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc | ... | @@ -1189,7 +1507,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc |
| 1189 | .uri => |u| u, | 1507 | .uri => |u| u, |
| 1190 | }; | 1508 | }; |
| 1191 | | 1509 | |
| 1192 | var req = try request(client, options.method, uri, options.headers, .{ | 1510 | var req = try open(client, options.method, uri, options.headers, .{ |
| 1193 | .header_strategy = options.header_strategy, | 1511 | .header_strategy = options.header_strategy, |
| 1194 | .handle_redirects = options.payload == .none, | 1512 | .handle_redirects = options.payload == .none, |
| 1195 | }); | 1513 | }); |
| ... | @@ -1206,7 +1524,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc | ... | @@ -1206,7 +1524,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc |
| 1206 | .none => {}, | 1524 | .none => {}, |
| 1207 | } | 1525 | } |
| 1208 | | 1526 | |
| 1209 | try req.start(.{ .raw_uri = options.raw_uri }); | 1527 | try req.send(.{ .raw_uri = options.raw_uri }); |
| 1210 | | 1528 | |
| 1211 | switch (options.payload) { | 1529 | switch (options.payload) { |
| 1212 | .string => |str| try req.writeAll(str), | 1530 | .string => |str| try req.writeAll(str), |