| ... | ... | @@ -13,12 +13,16 @@ const assert = std.debug.assert; |
| 13 | 13 | const Client = @This(); |
| 14 | 14 | const proto = @import("protocol.zig"); |
| 15 | 15 | |
| 16 | | pub const default_connection_pool_size = 32; |
| 17 | | pub const connection_pool_size = std.options.http_connection_pool_size; |
| 16 | pub const disable_tls = std.options.http_disable_tls; |
| 18 | 17 | |
| 18 | /// Allocator used for all allocations made by the client. |
| 19 | /// |
| 20 | /// This allocator must be thread-safe. |
| 19 | 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 | 24 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| 25 | |
| 22 | 26 | /// When this is `true`, the next time this client performs an HTTPS request, |
| 23 | 27 | /// it will first rescan the system for root certificates. |
| 24 | 28 | next_https_rescan_certs: bool = true, |
| ... | ... | @@ -26,7 +30,11 @@ next_https_rescan_certs: bool = true, |
| 26 | 30 | /// The pool of connections that can be reused (and currently in use). |
| 27 | 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 | 39 | /// A set of linked lists of connections that can be reused. |
| 32 | 40 | pub const ConnectionPool = struct { |
| ... | ... | @@ -34,7 +42,7 @@ pub const ConnectionPool = struct { |
| 34 | 42 | pub const Criteria = struct { |
| 35 | 43 | host: []const u8, |
| 36 | 44 | port: u16, |
| 37 | | is_tls: bool, |
| 45 | protocol: Connection.Protocol, |
| 38 | 46 | }; |
| 39 | 47 | |
| 40 | 48 | const Queue = std.DoublyLinkedList(Connection); |
| ... | ... | @@ -46,22 +54,24 @@ pub const ConnectionPool = struct { |
| 46 | 54 | /// Open connections that are not currently in use. |
| 47 | 55 | free: Queue = .{}, |
| 48 | 56 | free_len: usize = 0, |
| 49 | | free_size: usize = connection_pool_size, |
| 57 | free_size: usize = 32, |
| 50 | 58 | |
| 51 | 59 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. |
| 52 | 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 | 62 | pool.mutex.lock(); |
| 55 | 63 | defer pool.mutex.unlock(); |
| 56 | 64 | |
| 57 | 65 | var next = pool.free.last; |
| 58 | 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 | 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 | 73 | pool.acquireUnsafe(node); |
| 64 | | return node; |
| 74 | return &node.data; |
| 65 | 75 | } |
| 66 | 76 | |
| 67 | 77 | return null; |
| ... | ... | @@ -85,23 +95,28 @@ pub const ConnectionPool = struct { |
| 85 | 95 | |
| 86 | 96 | /// Tries to release a connection back to the connection pool. This function is threadsafe. |
| 87 | 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 | 102 | pool.mutex.lock(); |
| 90 | 103 | defer pool.mutex.unlock(); |
| 91 | 104 | |
| 105 | const node = @fieldParentPtr(Node, "data", connection); |
| 106 | |
| 92 | 107 | pool.used.remove(node); |
| 93 | 108 | |
| 94 | | if (node.data.closing) { |
| 95 | | node.data.deinit(client); |
| 96 | | return client.allocator.destroy(node); |
| 109 | if (node.data.closing or pool.free_size == 0) { |
| 110 | node.data.close(allocator); |
| 111 | return allocator.destroy(node); |
| 97 | 112 | } |
| 98 | 113 | |
| 99 | 114 | if (pool.free_len >= pool.free_size) { |
| 100 | 115 | const popped = pool.free.popFirst() orelse unreachable; |
| 101 | 116 | pool.free_len -= 1; |
| 102 | 117 | |
| 103 | | popped.data.deinit(client); |
| 104 | | client.allocator.destroy(popped); |
| 118 | popped.data.close(allocator); |
| 119 | allocator.destroy(popped); |
| 105 | 120 | } |
| 106 | 121 | |
| 107 | 122 | if (node.data.proxied) { |
| ... | ... | @@ -121,23 +136,43 @@ pub const ConnectionPool = struct { |
| 121 | 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 | 160 | pool.mutex.lock(); |
| 126 | 161 | |
| 127 | 162 | var next = pool.free.first; |
| 128 | 163 | while (next) |node| { |
| 129 | | defer client.allocator.destroy(node); |
| 164 | defer allocator.destroy(node); |
| 130 | 165 | next = node.next; |
| 131 | 166 | |
| 132 | | node.data.deinit(client); |
| 167 | node.data.close(allocator); |
| 133 | 168 | } |
| 134 | 169 | |
| 135 | 170 | next = pool.used.first; |
| 136 | 171 | while (next) |node| { |
| 137 | | defer client.allocator.destroy(node); |
| 172 | defer allocator.destroy(node); |
| 138 | 173 | next = node.next; |
| 139 | 174 | |
| 140 | | node.data.deinit(client); |
| 175 | node.data.close(allocator); |
| 141 | 176 | } |
| 142 | 177 | |
| 143 | 178 | pool.* = undefined; |
| ... | ... | @@ -147,11 +182,13 @@ pub const ConnectionPool = struct { |
| 147 | 182 | /// An interface to either a plain or TLS connection. |
| 148 | 183 | pub const Connection = struct { |
| 149 | 184 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; |
| 185 | const BufferSize = std.math.IntFittingRange(0, buffer_size); |
| 186 | |
| 150 | 187 | pub const Protocol = enum { plain, tls }; |
| 151 | 188 | |
| 152 | 189 | stream: net.Stream, |
| 153 | 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 | 193 | protocol: Protocol, |
| 157 | 194 | host: []u8, |
| ... | ... | @@ -160,16 +197,15 @@ pub const Connection = struct { |
| 160 | 197 | proxied: bool = false, |
| 161 | 198 | closing: bool = false, |
| 162 | 199 | |
| 163 | | read_start: u16 = 0, |
| 164 | | read_end: u16 = 0, |
| 200 | read_start: BufferSize = 0, |
| 201 | read_end: BufferSize = 0, |
| 202 | write_end: BufferSize = 0, |
| 165 | 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 { |
| 168 | | return switch (conn.protocol) { |
| 169 | | .plain => conn.stream.readAtLeast(buffer, len), |
| 170 | | .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len), |
| 171 | | } catch |err| { |
| 172 | | // TODO: https://github.com/ziglang/zig/issues/2473 |
| 206 | pub fn readvDirectTls(conn: *Connection, buffers: []std.os.iovec) ReadError!usize { |
| 207 | return conn.tls_client.readv(conn.stream, buffers) catch |err| { |
| 208 | // https://github.com/ziglang/zig/issues/2473 |
| 173 | 209 | if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert; |
| 174 | 210 | |
| 175 | 211 | switch (err) { |
| ... | ... | @@ -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 | 234 | pub fn fill(conn: *Connection) ReadError!void { |
| 185 | 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 | 241 | if (nread == 0) return error.EndOfStream; |
| 189 | 242 | conn.read_start = 0; |
| 190 | | conn.read_end = @as(u16, @intCast(nread)); |
| 243 | conn.read_end = @intCast(nread); |
| 191 | 244 | } |
| 192 | 245 | |
| 193 | 246 | pub fn peek(conn: *Connection) []const u8 { |
| 194 | 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 | 251 | conn.read_start += num; |
| 199 | 252 | } |
| 200 | 253 | |
| 201 | | pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize { |
| 202 | | assert(len <= buffer.len); |
| 203 | | |
| 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)); |
| 254 | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { |
| 255 | const available_read = conn.read_end - conn.read_start; |
| 256 | const available_buffer = buffer.len; |
| 213 | 257 | |
| 214 | | break; |
| 215 | | } else if (available_read > 0) { // fully read buffered data |
| 216 | | @memcpy(buffer[out_index..][0..available_read], conn.read_buf[conn.read_start..conn.read_end]); |
| 217 | | out_index += available_read; |
| 218 | | conn.read_start += available_read; |
| 258 | if (available_read > available_buffer) { // partially read buffered data |
| 259 | @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]); |
| 260 | conn.read_start += @intCast(available_buffer); |
| 219 | 261 | |
| 220 | | if (out_index >= len) break; |
| 221 | | } |
| 262 | return available_buffer; |
| 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; |
| 224 | | const leftover_len = len - out_index; |
| 267 | return available_read; |
| 268 | } |
| 225 | 269 | |
| 226 | | if (leftover_buffer > conn.read_buf.len) { |
| 227 | | // skip the buffer if the output is large enough |
| 228 | | return conn.rawReadAtLeast(buffer[out_index..], leftover_len); |
| 229 | | } |
| 270 | var iovecs = [2]std.os.iovec{ |
| 271 | .{ .iov_base = buffer.ptr, .iov_len = buffer.len }, |
| 272 | .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len }, |
| 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; |
| 235 | | } |
| 236 | | |
| 237 | | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { |
| 238 | | return conn.readAtLeast(buffer, 1); |
| 282 | return nread; |
| 239 | 283 | } |
| 240 | 284 | |
| 241 | 285 | pub const ReadError = error{ |
| ... | ... | @@ -253,26 +297,49 @@ pub const Connection = struct { |
| 253 | 297 | return Reader{ .context = conn }; |
| 254 | 298 | } |
| 255 | 299 | |
| 256 | | pub fn writeAll(conn: *Connection, buffer: []const u8) !void { |
| 257 | | return switch (conn.protocol) { |
| 258 | | .plain => conn.stream.writeAll(buffer), |
| 259 | | .tls => conn.tls_client.writeAll(conn.stream, buffer), |
| 260 | | } catch |err| switch (err) { |
| 300 | pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void { |
| 301 | return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) { |
| 261 | 302 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, |
| 262 | 303 | else => return error.UnexpectedWriteFailure, |
| 263 | 304 | }; |
| 264 | 305 | } |
| 265 | 306 | |
| 266 | | pub fn write(conn: *Connection, buffer: []const u8) !usize { |
| 267 | | return switch (conn.protocol) { |
| 268 | | .plain => conn.stream.write(buffer), |
| 269 | | .tls => conn.tls_client.write(conn.stream, buffer), |
| 270 | | } catch |err| switch (err) { |
| 307 | pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void { |
| 308 | if (conn.protocol == .tls) { |
| 309 | if (disable_tls) unreachable; |
| 310 | |
| 311 | return conn.writeAllDirectTls(buffer); |
| 312 | } |
| 313 | |
| 314 | return conn.stream.writeAll(buffer) catch |err| switch (err) { |
| 271 | 315 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, |
| 272 | 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 | 343 | pub const WriteError = error{ |
| 277 | 344 | ConnectionResetByPeer, |
| 278 | 345 | UnexpectedWriteFailure, |
| ... | ... | @@ -284,19 +351,17 @@ pub const Connection = struct { |
| 284 | 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 | 355 | if (conn.protocol == .tls) { |
| 356 | if (disable_tls) unreachable; |
| 357 | |
| 289 | 358 | // try to cleanly close the TLS connection, for any server that cares. |
| 290 | 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 | 363 | conn.stream.close(); |
| 295 | | } |
| 296 | | |
| 297 | | pub fn deinit(conn: *Connection, client: *const Client) void { |
| 298 | | conn.close(client); |
| 299 | | client.allocator.free(conn.host); |
| 364 | allocator.free(conn.host); |
| 300 | 365 | } |
| 301 | 366 | }; |
| 302 | 367 | |
| ... | ... | @@ -331,7 +396,7 @@ pub const Response = struct { |
| 331 | 396 | }; |
| 332 | 397 | |
| 333 | 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 | 401 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| 337 | 402 | if (first_line.len < 12) |
| ... | ... | @@ -350,6 +415,8 @@ pub const Response = struct { |
| 350 | 415 | res.status = status; |
| 351 | 416 | res.reason = reason; |
| 352 | 417 | |
| 418 | res.headers.clearRetainingCapacity(); |
| 419 | |
| 353 | 420 | while (it.next()) |line| { |
| 354 | 421 | if (line.len == 0) return error.HttpHeadersInvalid; |
| 355 | 422 | switch (line[0]) { |
| ... | ... | @@ -365,46 +432,42 @@ pub const Response = struct { |
| 365 | 432 | |
| 366 | 433 | if (trailing) continue; |
| 367 | 434 | |
| 368 | | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 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")) { |
| 435 | if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { |
| 375 | 436 | // Transfer-Encoding: second, first |
| 376 | 437 | // Transfer-Encoding: deflate, chunked |
| 377 | 438 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); |
| 378 | 439 | |
| 379 | | if (iter.next()) |first| { |
| 380 | | const trimmed = mem.trim(u8, first, " "); |
| 440 | const first = iter.first(); |
| 441 | const trimmed_first = mem.trim(u8, first, " "); |
| 381 | 442 | |
| 382 | | if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| { |
| 383 | | if (res.transfer_encoding != null) return error.HttpHeadersInvalid; |
| 384 | | res.transfer_encoding = te; |
| 385 | | } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 386 | | if (res.transfer_compression != null) return error.HttpHeadersInvalid; |
| 387 | | res.transfer_compression = ce; |
| 388 | | } else { |
| 389 | | return error.HttpTransferEncodingUnsupported; |
| 390 | | } |
| 391 | | } |
| 443 | var next: ?[]const u8 = first; |
| 444 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { |
| 445 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding |
| 446 | res.transfer_encoding = transfer; |
| 392 | 447 | |
| 393 | | if (iter.next()) |second| { |
| 394 | | if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported; |
| 448 | next = iter.next(); |
| 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| { |
| 399 | | res.transfer_compression = ce; |
| 454 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { |
| 455 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported |
| 456 | res.transfer_compression = transfer; |
| 400 | 457 | } else { |
| 401 | 458 | return error.HttpTransferEncodingUnsupported; |
| 402 | 459 | } |
| 403 | 460 | } |
| 404 | 461 | |
| 405 | 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 | 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 | 472 | const trimmed = mem.trim(u8, header_value, " "); |
| 410 | 473 | |
| ... | ... | @@ -440,13 +503,21 @@ pub const Response = struct { |
| 440 | 503 | status: http.Status, |
| 441 | 504 | reason: []const u8, |
| 442 | 505 | |
| 506 | /// If present, the number of bytes in the response body. |
| 443 | 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 | 516 | headers: http.Headers, |
| 448 | 517 | parser: proto.HeadersParser, |
| 449 | 518 | compression: Compression = .none, |
| 519 | |
| 520 | /// Whether the response body should be skipped. Any data read from the response body will be discarded. |
| 450 | 521 | skip: bool = false, |
| 451 | 522 | }; |
| 452 | 523 | |
| ... | ... | @@ -457,15 +528,18 @@ pub const Request = struct { |
| 457 | 528 | uri: Uri, |
| 458 | 529 | client: *Client, |
| 459 | 530 | /// is null when this connection is released |
| 460 | | connection: ?*ConnectionPool.Node, |
| 531 | connection: ?*Connection, |
| 461 | 532 | |
| 462 | 533 | method: http.Method, |
| 463 | 534 | version: http.Version = .@"HTTP/1.1", |
| 464 | 535 | headers: http.Headers, |
| 536 | |
| 537 | /// The transfer encoding of the request body. |
| 465 | 538 | transfer_encoding: RequestTransfer = .none, |
| 466 | 539 | |
| 467 | 540 | redirects_left: u32, |
| 468 | 541 | handle_redirects: bool, |
| 542 | handle_continue: bool, |
| 469 | 543 | |
| 470 | 544 | response: Response, |
| 471 | 545 | |
| ... | ... | @@ -491,9 +565,9 @@ pub const Request = struct { |
| 491 | 565 | if (req.connection) |connection| { |
| 492 | 566 | if (!req.response.parser.done) { |
| 493 | 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 | 573 | req.arena.deinit(); |
| ... | ... | @@ -512,7 +586,7 @@ pub const Request = struct { |
| 512 | 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 | 590 | req.connection = null; |
| 517 | 591 | |
| 518 | 592 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| ... | ... | @@ -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 { |
| 545 | | /// Specifies that the uri should be used as is |
| 618 | pub const SendOptions = struct { |
| 619 | /// Specifies that the uri should be used as is. You guarantee that the uri is already escaped. |
| 546 | 620 | raw_uri: bool = false, |
| 547 | 621 | }; |
| 548 | 622 | |
| 549 | | /// Send the request to the server. |
| 550 | | pub fn start(req: *Request, options: StartOptions) StartError!void { |
| 623 | /// Send the HTTP request headers to the server. |
| 624 | pub fn send(req: *Request, options: SendOptions) SendError!void { |
| 551 | 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()); |
| 554 | | const w = buffered.writer(); |
| 627 | const w = req.connection.?.writer(); |
| 555 | 628 | |
| 556 | 629 | try req.method.write(w); |
| 557 | 630 | try w.writeByte(' '); |
| 558 | 631 | |
| 559 | 632 | if (req.method == .CONNECT) { |
| 560 | | try w.writeAll(req.uri.host.?); |
| 561 | | try w.writeByte(':'); |
| 562 | | try w.print("{}", .{req.uri.port.?}); |
| 633 | try req.uri.writeToStream(.{ .authority = true }, w); |
| 563 | 634 | } else { |
| 564 | | if (req.connection.?.data.proxied) { |
| 565 | | // proxied connections require the full uri |
| 566 | | if (options.raw_uri) { |
| 567 | | try w.print("{+/r}", .{req.uri}); |
| 568 | | } else { |
| 569 | | try w.print("{+/}", .{req.uri}); |
| 570 | | } |
| 571 | | } else { |
| 572 | | if (options.raw_uri) { |
| 573 | | try w.print("{/r}", .{req.uri}); |
| 574 | | } else { |
| 575 | | try w.print("{/}", .{req.uri}); |
| 576 | | } |
| 577 | | } |
| 635 | try req.uri.writeToStream(.{ |
| 636 | .scheme = req.connection.?.proxied, |
| 637 | .authentication = req.connection.?.proxied, |
| 638 | .authority = req.connection.?.proxied, |
| 639 | .path = true, |
| 640 | .query = true, |
| 641 | .raw = options.raw_uri, |
| 642 | }, w); |
| 578 | 643 | } |
| 579 | 644 | try w.writeByte(' '); |
| 580 | 645 | try w.writeAll(@tagName(req.version)); |
| ... | ... | @@ -582,7 +647,7 @@ pub const Request = struct { |
| 582 | 647 | |
| 583 | 648 | if (!req.headers.contains("host")) { |
| 584 | 649 | try w.writeAll("Host: "); |
| 585 | | try w.writeAll(req.uri.host.?); |
| 650 | try req.uri.writeToStream(.{ .authority = true }, w); |
| 586 | 651 | try w.writeAll("\r\n"); |
| 587 | 652 | } |
| 588 | 653 | |
| ... | ... | @@ -614,17 +679,17 @@ pub const Request = struct { |
| 614 | 679 | .none => {}, |
| 615 | 680 | } |
| 616 | 681 | } else { |
| 617 | | if (has_content_length) { |
| 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) { |
| 682 | if (has_transfer_encoding) { |
| 622 | 683 | const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?; |
| 623 | 684 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { |
| 624 | 685 | req.transfer_encoding = .chunked; |
| 625 | 686 | } else { |
| 626 | 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 | 693 | } else { |
| 629 | 694 | req.transfer_encoding = .none; |
| 630 | 695 | } |
| ... | ... | @@ -639,9 +704,27 @@ pub const Request = struct { |
| 639 | 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 | 725 | try w.writeAll("\r\n"); |
| 643 | 726 | |
| 644 | | try buffered.flush(); |
| 727 | try req.connection.?.flush(); |
| 645 | 728 | } |
| 646 | 729 | |
| 647 | 730 | const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; |
| ... | ... | @@ -657,7 +740,7 @@ pub const Request = struct { |
| 657 | 740 | |
| 658 | 741 | var index: usize = 0; |
| 659 | 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 | 744 | if (amt == 0 and req.response.parser.done) break; |
| 662 | 745 | index += amt; |
| 663 | 746 | } |
| ... | ... | @@ -665,20 +748,22 @@ pub const Request = struct { |
| 665 | 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 | 753 | /// Waits for a response from the server and parses any headers that are sent. |
| 671 | 754 | /// This function will block until the final response is received. |
| 672 | 755 | /// |
| 673 | 756 | /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow |
| 674 | 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 | 760 | pub fn wait(req: *Request) WaitError!void { |
| 676 | 761 | while (true) { // handle redirects |
| 677 | 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()); |
| 681 | | req.connection.?.data.drop(@as(u16, @intCast(nchecked))); |
| 765 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); |
| 766 | req.connection.?.drop(@intCast(nchecked)); |
| 682 | 767 | |
| 683 | 768 | if (req.response.parser.state.isContent()) break; |
| 684 | 769 | } |
| ... | ... | @@ -688,12 +773,16 @@ pub const Request = struct { |
| 688 | 773 | if (req.response.status == .@"continue") { |
| 689 | 774 | req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response |
| 690 | 775 | req.response.parser.reset(); |
| 776 | |
| 777 | if (req.handle_continue) |
| 778 | continue; |
| 779 | |
| 691 | 780 | break; |
| 692 | 781 | } |
| 693 | 782 | |
| 694 | 783 | // we're switching protocols, so this connection is no longer doing http |
| 695 | 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 | 786 | req.response.parser.done = true; |
| 698 | 787 | } |
| 699 | 788 | |
| ... | ... | @@ -704,13 +793,14 @@ pub const Request = struct { |
| 704 | 793 | const res_connection = req.response.headers.getFirstValue("connection"); |
| 705 | 794 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); |
| 706 | 795 | if (res_keepalive and (req_keepalive or req_connection == null)) { |
| 707 | | req.connection.?.data.closing = false; |
| 796 | req.connection.?.closing = false; |
| 708 | 797 | } else { |
| 709 | | req.connection.?.data.closing = true; |
| 798 | req.connection.?.closing = true; |
| 710 | 799 | } |
| 711 | 800 | |
| 712 | | if (req.response.transfer_encoding) |te| { |
| 713 | | switch (te) { |
| 801 | if (req.response.transfer_encoding != .none) { |
| 802 | switch (req.response.transfer_encoding) { |
| 803 | .none => unreachable, |
| 714 | 804 | .chunked => { |
| 715 | 805 | req.response.parser.next_chunk_length = 0; |
| 716 | 806 | req.response.parser.state = .chunk_head_size; |
| ... | ... | @@ -774,23 +864,23 @@ pub const Request = struct { |
| 774 | 864 | |
| 775 | 865 | try req.redirect(resolved_url); |
| 776 | 866 | |
| 777 | | try req.start(.{}); |
| 867 | try req.send(.{}); |
| 778 | 868 | } else { |
| 779 | 869 | req.response.skip = false; |
| 780 | 870 | if (!req.response.parser.done) { |
| 781 | | if (req.response.transfer_compression) |tc| switch (tc) { |
| 871 | switch (req.response.transfer_compression) { |
| 782 | 872 | .identity => req.response.compression = .none, |
| 783 | | .compress => return error.CompressionNotSupported, |
| 873 | .compress, .@"x-compress" => return error.CompressionNotSupported, |
| 784 | 874 | .deflate => req.response.compression = .{ |
| 785 | 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 | 878 | .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, |
| 789 | 879 | }, |
| 790 | 880 | .zstd => req.response.compression = .{ |
| 791 | 881 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), |
| 792 | 882 | }, |
| 793 | | }; |
| 883 | } |
| 794 | 884 | } |
| 795 | 885 | |
| 796 | 886 | break; |
| ... | ... | @@ -806,7 +896,7 @@ pub const Request = struct { |
| 806 | 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 | 900 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { |
| 811 | 901 | const out_index = switch (req.response.compression) { |
| 812 | 902 | .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, |
| ... | ... | @@ -819,15 +909,13 @@ pub const Request = struct { |
| 819 | 909 | const has_trail = !req.response.parser.state.isContent(); |
| 820 | 910 | |
| 821 | 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()); |
| 825 | | req.connection.?.data.drop(@as(u16, @intCast(nchecked))); |
| 914 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); |
| 915 | req.connection.?.drop(@intCast(nchecked)); |
| 826 | 916 | } |
| 827 | 917 | |
| 828 | 918 | if (has_trail) { |
| 829 | | req.response.headers.clearRetainingCapacity(); |
| 830 | | |
| 831 | 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 | 920 | // This will *only* fail for a malformed trailer. |
| 833 | 921 | req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers; |
| ... | ... | @@ -837,7 +925,7 @@ pub const Request = struct { |
| 837 | 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 | 929 | pub fn readAll(req: *Request, buffer: []u8) !usize { |
| 842 | 930 | var index: usize = 0; |
| 843 | 931 | while (index < buffer.len) { |
| ... | ... | @@ -856,20 +944,21 @@ pub const Request = struct { |
| 856 | 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 | 949 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { |
| 861 | 950 | switch (req.transfer_encoding) { |
| 862 | 951 | .chunked => { |
| 863 | | try req.connection.?.data.writer().print("{x}\r\n", .{bytes.len}); |
| 864 | | try req.connection.?.data.writeAll(bytes); |
| 865 | | try req.connection.?.data.writeAll("\r\n"); |
| 952 | try req.connection.?.writer().print("{x}\r\n", .{bytes.len}); |
| 953 | try req.connection.?.writer().writeAll(bytes); |
| 954 | try req.connection.?.writer().writeAll("\r\n"); |
| 866 | 955 | |
| 867 | 956 | return bytes.len; |
| 868 | 957 | }, |
| 869 | 958 | .content_length => |*len| { |
| 870 | 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 | 962 | len.* -= amt; |
| 874 | 963 | return amt; |
| 875 | 964 | }, |
| ... | ... | @@ -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 | 971 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { |
| 881 | 972 | var index: usize = 0; |
| 882 | 973 | while (index < bytes.len) { |
| ... | ... | @@ -887,50 +978,169 @@ pub const Request = struct { |
| 887 | 978 | pub const FinishError = WriteError || error{MessageNotCompleted}; |
| 888 | 979 | |
| 889 | 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 | 982 | pub fn finish(req: *Request) FinishError!void { |
| 891 | 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 | 985 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 894 | 986 | .none => {}, |
| 895 | 987 | } |
| 988 | |
| 989 | try req.connection.?.flush(); |
| 896 | 990 | } |
| 897 | 991 | }; |
| 898 | 992 | |
| 899 | | pub const HttpProxy = struct { |
| 900 | | pub const ProxyAuthentication = union(enum) { |
| 901 | | basic: []const u8, |
| 902 | | custom: []const u8, |
| 903 | | }; |
| 993 | pub const Proxy = struct { |
| 994 | allocator: Allocator, |
| 995 | headers: http.Headers, |
| 904 | 996 | |
| 905 | 997 | protocol: Connection.Protocol, |
| 906 | 998 | host: []const u8, |
| 907 | | port: ?u16 = null, |
| 999 | port: u16, |
| 908 | 1000 | |
| 909 | | /// The value for the Proxy-Authorization header. |
| 910 | | auth: ?ProxyAuthentication = null, |
| 1001 | supports_connect: bool = true, |
| 911 | 1002 | }; |
| 912 | 1003 | |
| 913 | 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 | 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 | 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 | 1131 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 925 | 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 | 1134 | if (client.connection_pool.findConnection(.{ |
| 928 | 1135 | .host = host, |
| 929 | 1136 | .port = port, |
| 930 | | .is_tls = protocol == .tls, |
| 1137 | .protocol = protocol, |
| 931 | 1138 | })) |node| |
| 932 | 1139 | return node; |
| 933 | 1140 | |
| 1141 | if (disable_tls and protocol == .tls) |
| 1142 | return error.TlsInitializationFailed; |
| 1143 | |
| 934 | 1144 | const conn = try client.allocator.create(ConnectionPool.Node); |
| 935 | 1145 | errdefer client.allocator.destroy(conn); |
| 936 | 1146 | conn.* = .{ .data = undefined }; |
| ... | ... | @@ -951,40 +1161,41 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: |
| 951 | 1161 | conn.data = .{ |
| 952 | 1162 | .stream = stream, |
| 953 | 1163 | .tls_client = undefined, |
| 954 | | .protocol = protocol, |
| 955 | 1164 | |
| 1165 | .protocol = protocol, |
| 956 | 1166 | .host = try client.allocator.dupe(u8, host), |
| 957 | 1167 | .port = port, |
| 958 | 1168 | }; |
| 959 | 1169 | errdefer client.allocator.free(conn.data.host); |
| 960 | 1170 | |
| 961 | | switch (protocol) { |
| 962 | | .plain => {}, |
| 963 | | .tls => { |
| 964 | | conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client); |
| 965 | | errdefer client.allocator.destroy(conn.data.tls_client); |
| 1171 | if (protocol == .tls) { |
| 1172 | if (disable_tls) unreachable; |
| 966 | 1173 | |
| 967 | | conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed; |
| 968 | | // This is appropriate for HTTPS because the HTTP headers contain |
| 969 | | // the content length which is used to detect truncation attacks. |
| 970 | | conn.data.tls_client.allow_truncation_attacks = true; |
| 971 | | }, |
| 1174 | conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client); |
| 1175 | errdefer client.allocator.destroy(conn.data.tls_client); |
| 1176 | |
| 1177 | conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed; |
| 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 | 1183 | client.connection_pool.addUsed(conn); |
| 975 | 1184 | |
| 976 | | return conn; |
| 1185 | return &conn.data; |
| 977 | 1186 | } |
| 978 | 1187 | |
| 979 | 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 | 1193 | if (!net.has_unix_sockets) return error.Unsupported; |
| 983 | 1194 | |
| 984 | 1195 | if (client.connection_pool.findConnection(.{ |
| 985 | 1196 | .host = path, |
| 986 | 1197 | .port = 0, |
| 987 | | .is_tls = false, |
| 1198 | .protocol = .plain, |
| 988 | 1199 | })) |node| |
| 989 | 1200 | return node; |
| 990 | 1201 | |
| ... | ... | @@ -1007,37 +1218,130 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti |
| 1007 | 1218 | |
| 1008 | 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() |
| 1014 | | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; |
| 1015 | | pub const ConnectError = ConnectErrorPartial || RequestError; |
| 1224 | /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open. |
| 1225 | /// This function is threadsafe. |
| 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 | 1234 | if (client.connection_pool.findConnection(.{ |
| 1019 | | .host = host, |
| 1020 | | .port = port, |
| 1021 | | .is_tls = protocol == .tls, |
| 1235 | .host = tunnel_host, |
| 1236 | .port = tunnel_port, |
| 1237 | .protocol = proxy.protocol, |
| 1022 | 1238 | })) |node| |
| 1023 | 1239 | return node; |
| 1024 | 1240 | |
| 1025 | | if (client.proxy) |proxy| { |
| 1026 | | const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) { |
| 1027 | | .plain => 80, |
| 1028 | | .tls => 443, |
| 1241 | var maybe_valid = false; |
| 1242 | (tunnel: { |
| 1243 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); |
| 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); |
| 1032 | | conn.data.proxied = true; |
| 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. |
| 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 | 1293 | return conn; |
| 1035 | | } else { |
| 1036 | | return client.connectUnproxied(host, port, protocol); |
| 1294 | }) catch { |
| 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 | 1345 | UnsupportedUrlScheme, |
| 1042 | 1346 | UriMissingHost, |
| 1043 | 1347 | |
| ... | ... | @@ -1048,12 +1352,20 @@ pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request |
| 1048 | 1352 | pub const RequestOptions = struct { |
| 1049 | 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 | 1363 | handle_redirects: bool = true, |
| 1052 | 1364 | max_redirects: u32 = 3, |
| 1053 | 1365 | header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 }, |
| 1054 | 1366 | |
| 1055 | 1367 | /// Must be an already acquired connection. |
| 1056 | | connection: ?*ConnectionPool.Node = null, |
| 1368 | connection: ?*Connection = null, |
| 1057 | 1369 | |
| 1058 | 1370 | pub const StorageStrategy = union(enum) { |
| 1059 | 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 | 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 | 1393 | /// `uri` must remain alive during the entire request. |
| 1082 | 1394 | /// `headers` is cloned and may be freed after this function returns. |
| 1083 | 1395 | /// |
| 1084 | 1396 | /// The caller is responsible for calling `deinit()` on the `Request`. |
| 1085 | 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 | 1399 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 1088 | 1400 | |
| 1089 | 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 | 1406 | const host = uri.host orelse return error.UriMissingHost; |
| 1095 | 1407 | |
| 1096 | 1408 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { |
| 1409 | if (disable_tls) unreachable; |
| 1410 | |
| 1097 | 1411 | client.ca_bundle_mutex.lock(); |
| 1098 | 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 | 1428 | .version = options.version, |
| 1115 | 1429 | .redirects_left = options.max_redirects, |
| 1116 | 1430 | .handle_redirects = options.handle_redirects, |
| 1431 | .handle_continue = options.handle_continue, |
| 1117 | 1432 | .response = .{ |
| 1118 | 1433 | .status = undefined, |
| 1119 | 1434 | .reason = undefined, |
| ... | ... | @@ -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 | 1499 | pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult { |
| 1182 | 1500 | const has_transfer_encoding = options.headers.contains("transfer-encoding"); |
| 1183 | 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 | 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 | 1511 | .header_strategy = options.header_strategy, |
| 1194 | 1512 | .handle_redirects = options.payload == .none, |
| 1195 | 1513 | }); |
| ... | ... | @@ -1206,7 +1524,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc |
| 1206 | 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 | 1529 | switch (options.payload) { |
| 1212 | 1530 | .string => |str| try req.writeAll(str), |