blob 81bfbdcc (70579B) - Raw
1 //! HTTP(S) Client implementation. 2 //! 3 //! Connections are opened in a thread-safe manner, but individual Requests are not. 4 //! 5 //! TLS support may be disabled via `std.options.http_disable_tls`. 6 7 const std = @import("../std.zig"); 8 const builtin = @import("builtin"); 9 const testing = std.testing; 10 const http = std.http; 11 const mem = std.mem; 12 const Uri = std.Uri; 13 const Allocator = mem.Allocator; 14 const assert = std.debug.assert; 15 const Io = std.Io; 16 const Writer = std.Io.Writer; 17 const Reader = std.Io.Reader; 18 19 const Client = @This(); 20 21 pub const disable_tls = std.options.http_disable_tls; 22 23 /// Used for all client allocations. Must be thread-safe. 24 allocator: Allocator, 25 /// Used for opening TCP connections. 26 io: Io, 27 28 ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, 29 ca_bundle_mutex: std.Thread.Mutex = .{}, 30 /// Used both for the reader and writer buffers. 31 tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len, 32 /// If non-null, ssl secrets are logged to a stream. Creating such a stream 33 /// allows other processes with access to that stream to decrypt all 34 /// traffic over connections created with this `Client`. 35 ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null, 36 37 /// When this is `true`, the next time this client performs an HTTPS request, 38 /// it will first rescan the system for root certificates. 39 next_https_rescan_certs: bool = true, 40 41 /// The pool of connections that can be reused (and currently in use). 42 connection_pool: ConnectionPool = .{}, 43 /// Each `Connection` allocates this amount for the reader buffer. 44 /// 45 /// If the entire HTTP header cannot fit in this amount of bytes, 46 /// `error.HttpHeadersOversize` will be returned from `Request.wait`. 47 read_buffer_size: usize = 8192, 48 /// Each `Connection` allocates this amount for the writer buffer. 49 write_buffer_size: usize = 1024, 50 51 /// If populated, all http traffic travels through this third party. 52 /// This field cannot be modified while the client has active connections. 53 /// Pointer to externally-owned memory. 54 http_proxy: ?*Proxy = null, 55 /// If populated, all https traffic travels through this third party. 56 /// This field cannot be modified while the client has active connections. 57 /// Pointer to externally-owned memory. 58 https_proxy: ?*Proxy = null, 59 60 /// A Least-Recently-Used cache of open connections to be reused. 61 pub const ConnectionPool = struct { 62 mutex: std.Thread.Mutex = .{}, 63 /// Open connections that are currently in use. 64 used: std.DoublyLinkedList = .{}, 65 /// Open connections that are not currently in use. 66 free: std.DoublyLinkedList = .{}, 67 free_len: usize = 0, 68 free_size: usize = 32, 69 70 /// The criteria for a connection to be considered a match. 71 pub const Criteria = struct { 72 host: []const u8, 73 port: u16, 74 protocol: Protocol, 75 }; 76 77 /// Finds and acquires a connection from the connection pool matching the criteria. 78 /// If no connection is found, null is returned. 79 /// 80 /// Threadsafe. 81 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection { 82 pool.mutex.lock(); 83 defer pool.mutex.unlock(); 84 85 var next = pool.free.last; 86 while (next) |node| : (next = node.prev) { 87 const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); 88 if (connection.protocol != criteria.protocol) continue; 89 if (connection.port != criteria.port) continue; 90 91 // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) 92 if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue; 93 94 pool.acquireUnsafe(connection); 95 return connection; 96 } 97 98 return null; 99 } 100 101 /// Acquires an existing connection from the connection pool. This function is not threadsafe. 102 pub fn acquireUnsafe(pool: *ConnectionPool, connection: *Connection) void { 103 pool.free.remove(&connection.pool_node); 104 pool.free_len -= 1; 105 106 pool.used.append(&connection.pool_node); 107 } 108 109 /// Acquires an existing connection from the connection pool. This function is threadsafe. 110 pub fn acquire(pool: *ConnectionPool, connection: *Connection) void { 111 pool.mutex.lock(); 112 defer pool.mutex.unlock(); 113 114 return pool.acquireUnsafe(connection); 115 } 116 117 /// Tries to release a connection back to the connection pool. 118 /// If the connection is marked as closing, it will be closed instead. 119 /// 120 /// Threadsafe. 121 pub fn release(pool: *ConnectionPool, connection: *Connection) void { 122 pool.mutex.lock(); 123 defer pool.mutex.unlock(); 124 125 pool.used.remove(&connection.pool_node); 126 127 if (connection.closing or pool.free_size == 0) return connection.destroy(); 128 129 if (pool.free_len >= pool.free_size) { 130 const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?)); 131 pool.free_len -= 1; 132 133 popped.destroy(); 134 } 135 136 if (connection.proxied) { 137 // proxied connections go to the end of the queue, always try direct connections first 138 pool.free.prepend(&connection.pool_node); 139 } else { 140 pool.free.append(&connection.pool_node); 141 } 142 143 pool.free_len += 1; 144 } 145 146 /// Adds a newly created node to the pool of used connections. This function is threadsafe. 147 pub fn addUsed(pool: *ConnectionPool, connection: *Connection) void { 148 pool.mutex.lock(); 149 defer pool.mutex.unlock(); 150 151 pool.used.append(&connection.pool_node); 152 } 153 154 /// Resizes the connection pool. 155 /// 156 /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. 157 /// 158 /// Threadsafe. 159 pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { 160 pool.mutex.lock(); 161 defer pool.mutex.unlock(); 162 163 const next = pool.free.first; 164 _ = next; 165 while (pool.free_len > new_size) { 166 const popped = pool.free.popFirst() orelse unreachable; 167 pool.free_len -= 1; 168 169 popped.data.close(allocator); 170 allocator.destroy(popped); 171 } 172 173 pool.free_size = new_size; 174 } 175 176 /// Frees the connection pool and closes all connections within. 177 /// 178 /// All future operations on the connection pool will deadlock. 179 /// 180 /// Threadsafe. 181 pub fn deinit(pool: *ConnectionPool) void { 182 pool.mutex.lock(); 183 184 var next = pool.free.first; 185 while (next) |node| { 186 const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); 187 next = node.next; 188 connection.destroy(); 189 } 190 191 next = pool.used.first; 192 while (next) |node| { 193 const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); 194 next = node.next; 195 connection.destroy(); 196 } 197 198 pool.* = undefined; 199 } 200 }; 201 202 pub const Protocol = enum { 203 plain, 204 tls, 205 206 fn port(protocol: Protocol) u16 { 207 return switch (protocol) { 208 .plain => 80, 209 .tls => 443, 210 }; 211 } 212 213 pub fn fromScheme(scheme: []const u8) ?Protocol { 214 const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ 215 .{ "http", .plain }, 216 .{ "ws", .plain }, 217 .{ "https", .tls }, 218 .{ "wss", .tls }, 219 }); 220 return protocol_map.get(scheme); 221 } 222 223 pub fn fromUri(uri: Uri) ?Protocol { 224 return fromScheme(uri.scheme); 225 } 226 }; 227 228 pub const Connection = struct { 229 client: *Client, 230 stream_writer: Io.net.Stream.Writer, 231 stream_reader: Io.net.Stream.Reader, 232 /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. 233 pool_node: std.DoublyLinkedList.Node, 234 port: u16, 235 host_len: u8, 236 proxied: bool, 237 closing: bool, 238 protocol: Protocol, 239 240 const Plain = struct { 241 connection: Connection, 242 243 fn create( 244 client: *Client, 245 remote_host: []const u8, 246 port: u16, 247 stream: Io.net.Stream, 248 ) error{OutOfMemory}!*Plain { 249 const gpa = client.allocator; 250 const alloc_len = allocLen(client, remote_host.len); 251 const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); 252 errdefer gpa.free(base); 253 const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; 254 const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; 255 const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; 256 assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); 257 @memcpy(host_buffer, remote_host); 258 const plain: *Plain = @ptrCast(base); 259 plain.* = .{ 260 .connection = .{ 261 .client = client, 262 .stream_writer = stream.writer(socket_write_buffer), 263 .stream_reader = stream.reader(socket_read_buffer), 264 .pool_node = .{}, 265 .port = port, 266 .host_len = @intCast(remote_host.len), 267 .proxied = false, 268 .closing = false, 269 .protocol = .plain, 270 }, 271 }; 272 return plain; 273 } 274 275 fn destroy(plain: *Plain) void { 276 const c = &plain.connection; 277 const gpa = c.client.allocator; 278 const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain); 279 gpa.free(base[0..allocLen(c.client, c.host_len)]); 280 } 281 282 fn allocLen(client: *Client, host_len: usize) usize { 283 return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; 284 } 285 286 fn host(plain: *Plain) []u8 { 287 const base: [*]u8 = @ptrCast(plain); 288 return base[@sizeOf(Plain)..][0..plain.connection.host_len]; 289 } 290 }; 291 292 const Tls = struct { 293 client: std.crypto.tls.Client, 294 connection: Connection, 295 296 fn create( 297 client: *Client, 298 remote_host: []const u8, 299 port: u16, 300 stream: Io.net.Stream, 301 ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { 302 const gpa = client.allocator; 303 const alloc_len = allocLen(client, remote_host.len); 304 const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); 305 errdefer gpa.free(base); 306 const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; 307 // The TLS client wants enough buffer for the max encrypted frame 308 // size, and the HTTP body reader wants enough buffer for the 309 // entire HTTP header. This means we need a combined upper bound. 310 const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size; 311 const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..tls_read_buffer_len]; 312 const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size]; 313 const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; 314 const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size]; 315 assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len); 316 @memcpy(host_buffer, remote_host); 317 const tls: *Tls = @ptrCast(base); 318 tls.* = .{ 319 .connection = .{ 320 .client = client, 321 .stream_writer = stream.writer(tls_write_buffer), 322 .stream_reader = stream.reader(socket_read_buffer), 323 .pool_node = .{}, 324 .port = port, 325 .host_len = @intCast(remote_host.len), 326 .proxied = false, 327 .closing = false, 328 .protocol = .tls, 329 }, 330 // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true 331 .client = std.crypto.tls.Client.init( 332 tls.connection.stream_reader.interface(), 333 &tls.connection.stream_writer.interface, 334 .{ 335 .host = .{ .explicit = remote_host }, 336 .ca = .{ .bundle = client.ca_bundle }, 337 .ssl_key_log = client.ssl_key_log, 338 .read_buffer = tls_read_buffer, 339 .write_buffer = socket_write_buffer, 340 // This is appropriate for HTTPS because the HTTP headers contain 341 // the content length which is used to detect truncation attacks. 342 .allow_truncation_attacks = true, 343 }, 344 ) catch return error.TlsInitializationFailed, 345 }; 346 return tls; 347 } 348 349 fn destroy(tls: *Tls) void { 350 const c = &tls.connection; 351 const gpa = c.client.allocator; 352 const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls); 353 gpa.free(base[0..allocLen(c.client, c.host_len)]); 354 } 355 356 fn allocLen(client: *Client, host_len: usize) usize { 357 const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size; 358 return @sizeOf(Tls) + host_len + tls_read_buffer_len + client.tls_buffer_size + 359 client.write_buffer_size + client.tls_buffer_size; 360 } 361 362 fn host(tls: *Tls) []u8 { 363 const base: [*]u8 = @ptrCast(tls); 364 return base[@sizeOf(Tls)..][0..tls.connection.host_len]; 365 } 366 }; 367 368 pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.ReadError; 369 370 pub fn getReadError(c: *const Connection) ?ReadError { 371 return switch (c.protocol) { 372 .tls => { 373 if (disable_tls) unreachable; 374 const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c)); 375 return tls.client.read_err orelse c.stream_reader.getError(); 376 }, 377 .plain => { 378 return c.stream_reader.getError(); 379 }, 380 }; 381 } 382 383 fn getStream(c: *Connection) Io.net.Stream { 384 return c.stream_reader.stream; 385 } 386 387 pub fn host(c: *Connection) []u8 { 388 return switch (c.protocol) { 389 .tls => { 390 if (disable_tls) unreachable; 391 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 392 return tls.host(); 393 }, 394 .plain => { 395 const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); 396 return plain.host(); 397 }, 398 }; 399 } 400 401 /// If this is called without calling `flush` or `end`, data will be 402 /// dropped unsent. 403 pub fn destroy(c: *Connection) void { 404 c.getStream().close(); 405 switch (c.protocol) { 406 .tls => { 407 if (disable_tls) unreachable; 408 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 409 tls.destroy(); 410 }, 411 .plain => { 412 const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); 413 plain.destroy(); 414 }, 415 } 416 } 417 418 /// HTTP protocol from client to server. 419 /// This either goes directly to `stream_writer`, or to a TLS client. 420 pub fn writer(c: *Connection) *Writer { 421 return switch (c.protocol) { 422 .tls => { 423 if (disable_tls) unreachable; 424 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 425 return &tls.client.writer; 426 }, 427 .plain => &c.stream_writer.interface, 428 }; 429 } 430 431 /// HTTP protocol from server to client. 432 /// This either comes directly from `stream_reader`, or from a TLS client. 433 pub fn reader(c: *Connection) *Reader { 434 return switch (c.protocol) { 435 .tls => { 436 if (disable_tls) unreachable; 437 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 438 return &tls.client.reader; 439 }, 440 .plain => c.stream_reader.interface(), 441 }; 442 } 443 444 pub fn flush(c: *Connection) Writer.Error!void { 445 if (c.protocol == .tls) { 446 if (disable_tls) unreachable; 447 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 448 try tls.client.writer.flush(); 449 } 450 try c.stream_writer.interface.flush(); 451 } 452 453 /// If the connection is a TLS connection, sends the close_notify alert. 454 /// 455 /// Flushes all buffers. 456 pub fn end(c: *Connection) Writer.Error!void { 457 if (c.protocol == .tls) { 458 if (disable_tls) unreachable; 459 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); 460 try tls.client.end(); 461 } 462 try c.stream_writer.interface.flush(); 463 } 464 }; 465 466 pub const Response = struct { 467 request: *Request, 468 /// Pointers in this struct are invalidated when the response body stream 469 /// is initialized. 470 head: Head, 471 472 pub const Head = struct { 473 bytes: []const u8, 474 version: http.Version, 475 status: http.Status, 476 reason: []const u8, 477 location: ?[]const u8 = null, 478 content_type: ?[]const u8 = null, 479 content_disposition: ?[]const u8 = null, 480 481 keep_alive: bool, 482 483 /// If present, the number of bytes in the response body. 484 content_length: ?u64 = null, 485 486 transfer_encoding: http.TransferEncoding = .none, 487 content_encoding: http.ContentEncoding = .identity, 488 489 pub const ParseError = error{ 490 HttpConnectionHeaderUnsupported, 491 HttpContentEncodingUnsupported, 492 HttpHeaderContinuationsUnsupported, 493 HttpHeadersInvalid, 494 HttpTransferEncodingUnsupported, 495 InvalidContentLength, 496 }; 497 498 pub fn parse(bytes: []const u8) ParseError!Head { 499 var res: Head = .{ 500 .bytes = bytes, 501 .status = undefined, 502 .reason = undefined, 503 .version = undefined, 504 .keep_alive = false, 505 }; 506 var it = mem.splitSequence(u8, bytes, "\r\n"); 507 508 const first_line = it.first(); 509 if (first_line.len < 12) return error.HttpHeadersInvalid; 510 511 const version: http.Version = switch (int64(first_line[0..8])) { 512 int64("HTTP/1.0") => .@"HTTP/1.0", 513 int64("HTTP/1.1") => .@"HTTP/1.1", 514 else => return error.HttpHeadersInvalid, 515 }; 516 if (first_line[8] != ' ') return error.HttpHeadersInvalid; 517 const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); 518 const reason = mem.trimLeft(u8, first_line[12..], " "); 519 520 res.version = version; 521 res.status = status; 522 res.reason = reason; 523 res.keep_alive = switch (version) { 524 .@"HTTP/1.0" => false, 525 .@"HTTP/1.1" => true, 526 }; 527 528 while (it.next()) |line| { 529 if (line.len == 0) return res; 530 switch (line[0]) { 531 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, 532 else => {}, 533 } 534 535 var line_it = mem.splitScalar(u8, line, ':'); 536 const header_name = line_it.next().?; 537 const header_value = mem.trim(u8, line_it.rest(), " \t"); 538 if (header_name.len == 0) return error.HttpHeadersInvalid; 539 540 if (std.ascii.eqlIgnoreCase(header_name, "connection")) { 541 res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); 542 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { 543 res.content_type = header_value; 544 } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { 545 res.location = header_value; 546 } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { 547 res.content_disposition = header_value; 548 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { 549 // Transfer-Encoding: second, first 550 // Transfer-Encoding: deflate, chunked 551 var iter = mem.splitBackwardsScalar(u8, header_value, ','); 552 553 const first = iter.first(); 554 const trimmed_first = mem.trim(u8, first, " "); 555 556 var next: ?[]const u8 = first; 557 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { 558 if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding 559 res.transfer_encoding = transfer; 560 561 next = iter.next(); 562 } 563 564 if (next) |second| { 565 const trimmed_second = mem.trim(u8, second, " "); 566 567 if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { 568 if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported 569 res.content_encoding = transfer; 570 } else { 571 return error.HttpTransferEncodingUnsupported; 572 } 573 } 574 575 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; 576 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { 577 const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; 578 579 if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; 580 581 res.content_length = content_length; 582 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { 583 if (res.content_encoding != .identity) return error.HttpHeadersInvalid; 584 585 const trimmed = mem.trim(u8, header_value, " "); 586 587 if (http.ContentEncoding.fromString(trimmed)) |ce| { 588 res.content_encoding = ce; 589 } else { 590 return error.HttpContentEncodingUnsupported; 591 } 592 } 593 } 594 return error.HttpHeadersInvalid; // missing empty line 595 } 596 597 test parse { 598 const response_bytes = "HTTP/1.1 200 OK\r\n" ++ 599 "LOcation:url\r\n" ++ 600 "content-tYpe: text/plain\r\n" ++ 601 "content-disposition:attachment; filename=example.txt \r\n" ++ 602 "content-Length:10\r\n" ++ 603 "TRansfer-encoding:\tdeflate, chunked \r\n" ++ 604 "connectioN:\t keep-alive \r\n\r\n"; 605 606 const head = try Head.parse(response_bytes); 607 608 try testing.expectEqual(.@"HTTP/1.1", head.version); 609 try testing.expectEqualStrings("OK", head.reason); 610 try testing.expectEqual(.ok, head.status); 611 612 try testing.expectEqualStrings("url", head.location.?); 613 try testing.expectEqualStrings("text/plain", head.content_type.?); 614 try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); 615 616 try testing.expectEqual(true, head.keep_alive); 617 try testing.expectEqual(10, head.content_length.?); 618 try testing.expectEqual(.chunked, head.transfer_encoding); 619 try testing.expectEqual(.deflate, head.content_encoding); 620 } 621 622 pub fn iterateHeaders(h: Head) http.HeaderIterator { 623 return .init(h.bytes); 624 } 625 626 test iterateHeaders { 627 const response_bytes = "HTTP/1.1 200 OK\r\n" ++ 628 "LOcation:url\r\n" ++ 629 "content-tYpe: text/plain\r\n" ++ 630 "content-disposition:attachment; filename=example.txt \r\n" ++ 631 "content-Length:10\r\n" ++ 632 "TRansfer-encoding:\tdeflate, chunked \r\n" ++ 633 "connectioN:\t keep-alive \r\n\r\n"; 634 635 const head = try Head.parse(response_bytes); 636 var it = head.iterateHeaders(); 637 { 638 const header = it.next().?; 639 try testing.expectEqualStrings("LOcation", header.name); 640 try testing.expectEqualStrings("url", header.value); 641 try testing.expect(!it.is_trailer); 642 } 643 { 644 const header = it.next().?; 645 try testing.expectEqualStrings("content-tYpe", header.name); 646 try testing.expectEqualStrings("text/plain", header.value); 647 try testing.expect(!it.is_trailer); 648 } 649 { 650 const header = it.next().?; 651 try testing.expectEqualStrings("content-disposition", header.name); 652 try testing.expectEqualStrings("attachment; filename=example.txt", header.value); 653 try testing.expect(!it.is_trailer); 654 } 655 { 656 const header = it.next().?; 657 try testing.expectEqualStrings("content-Length", header.name); 658 try testing.expectEqualStrings("10", header.value); 659 try testing.expect(!it.is_trailer); 660 } 661 { 662 const header = it.next().?; 663 try testing.expectEqualStrings("TRansfer-encoding", header.name); 664 try testing.expectEqualStrings("deflate, chunked", header.value); 665 try testing.expect(!it.is_trailer); 666 } 667 { 668 const header = it.next().?; 669 try testing.expectEqualStrings("connectioN", header.name); 670 try testing.expectEqualStrings("keep-alive", header.value); 671 try testing.expect(!it.is_trailer); 672 } 673 try testing.expectEqual(null, it.next()); 674 } 675 676 inline fn int64(array: *const [8]u8) u64 { 677 return @bitCast(array.*); 678 } 679 680 fn parseInt3(text: *const [3]u8) u10 { 681 const nnn: @Vector(3, u8) = text.*; 682 const zero: @Vector(3, u8) = .{ '0', '0', '0' }; 683 const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; 684 return @reduce(.Add, (nnn -% zero) *% mmm); 685 } 686 687 test parseInt3 { 688 const expectEqual = testing.expectEqual; 689 try expectEqual(@as(u10, 0), parseInt3("000")); 690 try expectEqual(@as(u10, 418), parseInt3("418")); 691 try expectEqual(@as(u10, 999), parseInt3("999")); 692 } 693 694 /// Help the programmer avoid bugs by calling this when the string 695 /// memory of `Head` becomes invalidated. 696 fn invalidateStrings(h: *Head) void { 697 h.bytes = undefined; 698 h.reason = undefined; 699 if (h.location) |*s| s.* = undefined; 700 if (h.content_type) |*s| s.* = undefined; 701 if (h.content_disposition) |*s| s.* = undefined; 702 } 703 }; 704 705 /// If compressed body has been negotiated this will return compressed bytes. 706 /// 707 /// If the returned `Reader` returns `error.ReadFailed` the error is 708 /// available via `bodyErr`. 709 /// 710 /// Asserts that this function is only called once. 711 /// 712 /// See also: 713 /// * `readerDecompressing` 714 pub fn reader(response: *Response, transfer_buffer: []u8) *Reader { 715 response.head.invalidateStrings(); 716 const req = response.request; 717 if (!req.method.responseHasBody()) return .ending; 718 const head = &response.head; 719 return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length); 720 } 721 722 /// If compressed body has been negotiated this will return decompressed bytes. 723 /// 724 /// If the returned `Reader` returns `error.ReadFailed` the error is 725 /// available via `bodyErr`. 726 /// 727 /// Asserts that this function is only called once. 728 /// 729 /// See also: 730 /// * `reader` 731 pub fn readerDecompressing( 732 response: *Response, 733 transfer_buffer: []u8, 734 decompress: *http.Decompress, 735 decompress_buffer: []u8, 736 ) *Reader { 737 response.head.invalidateStrings(); 738 const head = &response.head; 739 return response.request.reader.bodyReaderDecompressing( 740 transfer_buffer, 741 head.transfer_encoding, 742 head.content_length, 743 head.content_encoding, 744 decompress, 745 decompress_buffer, 746 ); 747 } 748 749 /// After receiving `error.ReadFailed` from the `Reader` returned by 750 /// `reader` or `readerDecompressing`, this function accesses the 751 /// more specific error code. 752 pub fn bodyErr(response: *const Response) ?http.Reader.BodyError { 753 return response.request.reader.body_err; 754 } 755 756 pub fn iterateTrailers(response: *const Response) http.HeaderIterator { 757 const r = &response.request.reader; 758 assert(r.state == .ready); 759 return .{ 760 .bytes = r.trailers, 761 .index = 0, 762 .is_trailer = true, 763 }; 764 } 765 }; 766 767 pub const Request = struct { 768 /// This field is provided so that clients can observe redirected URIs. 769 /// 770 /// Its backing memory is externally provided by API users when creating a 771 /// request, and then again provided externally via `redirect_buffer` to 772 /// `receiveHead`. 773 uri: Uri, 774 client: *Client, 775 /// This is null when the connection is released. 776 connection: ?*Connection, 777 reader: http.Reader, 778 keep_alive: bool, 779 780 method: http.Method, 781 version: http.Version = .@"HTTP/1.1", 782 transfer_encoding: TransferEncoding, 783 redirect_behavior: RedirectBehavior, 784 accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding, 785 786 /// Whether the request should handle a 100-continue response before sending the request body. 787 handle_continue: bool, 788 789 /// Standard headers that have default, but overridable, behavior. 790 headers: Headers, 791 792 /// Populated in `receiveHead`; used in `deinit` to determine whether to 793 /// discard the body to reuse the connection. 794 response_content_length: ?u64 = null, 795 /// Populated in `receiveHead`; used in `deinit` to determine whether to 796 /// discard the body to reuse the connection. 797 response_transfer_encoding: http.TransferEncoding = .none, 798 799 /// These headers are kept including when following a redirect to a 800 /// different domain. 801 /// Externally-owned; must outlive the Request. 802 extra_headers: []const http.Header, 803 804 /// These headers are stripped when following a redirect to a different 805 /// domain. 806 /// Externally-owned; must outlive the Request. 807 privileged_headers: []const http.Header, 808 809 pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: { 810 var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false); 811 result[@intFromEnum(http.ContentEncoding.gzip)] = true; 812 result[@intFromEnum(http.ContentEncoding.deflate)] = true; 813 result[@intFromEnum(http.ContentEncoding.identity)] = true; 814 break :b result; 815 }; 816 817 pub const TransferEncoding = union(enum) { 818 content_length: u64, 819 chunked: void, 820 none: void, 821 }; 822 823 pub const Headers = struct { 824 host: Value = .default, 825 authorization: Value = .default, 826 user_agent: Value = .default, 827 connection: Value = .default, 828 accept_encoding: Value = .default, 829 content_type: Value = .default, 830 831 pub const Value = union(enum) { 832 default, 833 omit, 834 override: []const u8, 835 }; 836 }; 837 838 /// Any value other than `not_allowed` or `unhandled` means that integer represents 839 /// how many remaining redirects are allowed. 840 pub const RedirectBehavior = enum(u16) { 841 /// The next redirect will cause an error. 842 not_allowed = 0, 843 /// Redirects are passed to the client to analyze the redirect response 844 /// directly. 845 unhandled = std.math.maxInt(u16), 846 _, 847 848 pub fn init(n: u16) RedirectBehavior { 849 assert(n != std.math.maxInt(u16)); 850 return @enumFromInt(n); 851 } 852 853 pub fn subtractOne(rb: *RedirectBehavior) void { 854 switch (rb.*) { 855 .not_allowed => unreachable, 856 .unhandled => unreachable, 857 _ => rb.* = @enumFromInt(@intFromEnum(rb.*) - 1), 858 } 859 } 860 861 pub fn remaining(rb: RedirectBehavior) u16 { 862 assert(rb != .unhandled); 863 return @intFromEnum(rb); 864 } 865 }; 866 867 /// Returns the request's `Connection` back to the pool of the `Client`. 868 pub fn deinit(r: *Request) void { 869 if (r.connection) |connection| { 870 connection.closing = connection.closing or switch (r.reader.state) { 871 .ready => false, 872 .received_head => c: { 873 if (r.method.requestHasBody()) break :c true; 874 if (!r.method.responseHasBody()) break :c false; 875 const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); 876 _ = reader.discardRemaining() catch |err| switch (err) { 877 error.ReadFailed => break :c true, 878 }; 879 break :c r.reader.state != .ready; 880 }, 881 else => true, 882 }; 883 r.client.connection_pool.release(connection); 884 } 885 r.* = undefined; 886 } 887 888 /// Sends and flushes a complete request as only HTTP head, no body. 889 pub fn sendBodiless(r: *Request) Writer.Error!void { 890 try sendBodilessUnflushed(r); 891 try r.connection.?.flush(); 892 } 893 894 /// Sends but does not flush a complete request as only HTTP head, no body. 895 pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { 896 assert(r.transfer_encoding == .none); 897 assert(!r.method.requestHasBody()); 898 try sendHead(r); 899 } 900 901 /// Transfers the HTTP head over the connection and flushes. 902 /// 903 /// See also: 904 /// * `sendBodyUnflushed` 905 pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { 906 const result = try sendBodyUnflushed(r, buffer); 907 try r.connection.?.flush(); 908 return result; 909 } 910 911 /// Transfers the HTTP head and body over the connection and flushes. 912 pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void { 913 r.transfer_encoding = .{ .content_length = body.len }; 914 var bw = try sendBodyUnflushed(r, body); 915 bw.writer.end = body.len; 916 try bw.end(); 917 try r.connection.?.flush(); 918 } 919 920 /// Transfers the HTTP head over the connection, which is not flushed until 921 /// `BodyWriter.flush` or `BodyWriter.end` is called. 922 /// 923 /// See also: 924 /// * `sendBody` 925 pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { 926 assert(r.method.requestHasBody()); 927 try sendHead(r); 928 const http_protocol_output = r.connection.?.writer(); 929 return switch (r.transfer_encoding) { 930 .chunked => .{ 931 .http_protocol_output = http_protocol_output, 932 .state = .init_chunked, 933 .writer = .{ 934 .buffer = buffer, 935 .vtable = &.{ 936 .drain = http.BodyWriter.chunkedDrain, 937 .sendFile = http.BodyWriter.chunkedSendFile, 938 }, 939 }, 940 }, 941 .content_length => |len| .{ 942 .http_protocol_output = http_protocol_output, 943 .state = .{ .content_length = len }, 944 .writer = .{ 945 .buffer = buffer, 946 .vtable = &.{ 947 .drain = http.BodyWriter.contentLengthDrain, 948 .sendFile = http.BodyWriter.contentLengthSendFile, 949 }, 950 }, 951 }, 952 .none => .{ 953 .http_protocol_output = http_protocol_output, 954 .state = .none, 955 .writer = .{ 956 .buffer = buffer, 957 .vtable = &.{ 958 .drain = http.BodyWriter.noneDrain, 959 .sendFile = http.BodyWriter.noneSendFile, 960 }, 961 }, 962 }, 963 }; 964 } 965 966 /// Sends HTTP headers without flushing. 967 fn sendHead(r: *Request) Writer.Error!void { 968 const uri = r.uri; 969 const connection = r.connection.?; 970 const w = connection.writer(); 971 972 try w.writeAll(@tagName(r.method)); 973 try w.writeByte(' '); 974 975 if (r.method == .CONNECT) { 976 try uri.writeToStream(w, .{ .authority = true }); 977 } else { 978 try uri.writeToStream(w, .{ 979 .scheme = connection.proxied, 980 .authentication = connection.proxied, 981 .authority = connection.proxied, 982 .path = true, 983 .query = true, 984 }); 985 } 986 try w.writeByte(' '); 987 try w.writeAll(@tagName(r.version)); 988 try w.writeAll("\r\n"); 989 990 if (try emitOverridableHeader("host: ", r.headers.host, w)) { 991 try w.writeAll("host: "); 992 try uri.writeToStream(w, .{ .authority = true }); 993 try w.writeAll("\r\n"); 994 } 995 996 if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { 997 if (uri.user != null or uri.password != null) { 998 try w.writeAll("authorization: "); 999 try basic_authorization.write(uri, w); 1000 try w.writeAll("\r\n"); 1001 } 1002 } 1003 1004 if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { 1005 try w.writeAll("user-agent: zig/"); 1006 try w.writeAll(builtin.zig_version_string); 1007 try w.writeAll(" (std.http)\r\n"); 1008 } 1009 1010 if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { 1011 if (r.keep_alive) { 1012 try w.writeAll("connection: keep-alive\r\n"); 1013 } else { 1014 try w.writeAll("connection: close\r\n"); 1015 } 1016 } 1017 1018 if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { 1019 try w.writeAll("accept-encoding: "); 1020 for (r.accept_encoding, 0..) |enabled, i| { 1021 if (!enabled) continue; 1022 const tag: http.ContentEncoding = @enumFromInt(i); 1023 if (tag == .identity) continue; 1024 const tag_name = @tagName(tag); 1025 try w.ensureUnusedCapacity(tag_name.len + 2); 1026 try w.writeAll(tag_name); 1027 try w.writeAll(", "); 1028 } 1029 w.undo(2); 1030 try w.writeAll("\r\n"); 1031 } 1032 1033 switch (r.transfer_encoding) { 1034 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), 1035 .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), 1036 .none => {}, 1037 } 1038 1039 if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { 1040 // The default is to omit content-type if not provided because 1041 // "application/octet-stream" is redundant. 1042 } 1043 1044 for (r.extra_headers) |header| { 1045 assert(header.name.len != 0); 1046 1047 try w.writeAll(header.name); 1048 try w.writeAll(": "); 1049 try w.writeAll(header.value); 1050 try w.writeAll("\r\n"); 1051 } 1052 1053 if (connection.proxied) proxy: { 1054 const proxy = switch (connection.protocol) { 1055 .plain => r.client.http_proxy, 1056 .tls => r.client.https_proxy, 1057 } orelse break :proxy; 1058 1059 const authorization = proxy.authorization orelse break :proxy; 1060 try w.writeAll("proxy-authorization: "); 1061 try w.writeAll(authorization); 1062 try w.writeAll("\r\n"); 1063 } 1064 1065 try w.writeAll("\r\n"); 1066 } 1067 1068 pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{ 1069 /// Server sent headers that did not conform to the HTTP protocol. 1070 /// 1071 /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be 1072 /// passed directly to `Request.Head.parse`. 1073 HttpHeadersInvalid, 1074 TooManyHttpRedirects, 1075 /// This can be avoided by calling `receiveHead` before sending the 1076 /// request body. 1077 RedirectRequiresResend, 1078 HttpRedirectLocationMissing, 1079 HttpRedirectLocationOversize, 1080 HttpRedirectLocationInvalid, 1081 HttpContentEncodingUnsupported, 1082 HttpChunkInvalid, 1083 HttpChunkTruncated, 1084 HttpHeadersOversize, 1085 UnsupportedUriScheme, 1086 1087 /// Sending the request failed. Error code can be found on the 1088 /// `Connection` object. 1089 WriteFailed, 1090 }; 1091 1092 /// If handling redirects and the request has no payload, then this 1093 /// function will automatically follow redirects. 1094 /// 1095 /// If a request payload is present, then this function will error with 1096 /// `error.RedirectRequiresResend`. 1097 /// 1098 /// This function takes an auxiliary buffer to store the arbitrarily large 1099 /// URI which may need to be merged with the previous URI, and that data 1100 /// needs to survive across different connections, which is where the input 1101 /// buffer lives. 1102 /// 1103 /// `redirect_buffer` must outlive accesses to `Request.uri`. If this 1104 /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` 1105 /// is returned instead. This buffer may be empty if no redirects are to be 1106 /// handled. 1107 /// 1108 /// If this fails with `error.ReadFailed` then the `Connection.getReadError` 1109 /// method of `r.connection` can be used to get more detailed information. 1110 pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { 1111 var aux_buf = redirect_buffer; 1112 while (true) { 1113 const head_buffer = try r.reader.receiveHead(); 1114 const response: Response = .{ 1115 .request = r, 1116 .head = Response.Head.parse(head_buffer) catch return error.HttpHeadersInvalid, 1117 }; 1118 const head = &response.head; 1119 1120 if (head.status == .@"continue") { 1121 if (r.handle_continue) continue; 1122 r.response_transfer_encoding = head.transfer_encoding; 1123 r.response_content_length = head.content_length; 1124 return response; // we're not handling the 100-continue 1125 } 1126 1127 // This while loop is for handling redirects, which means the request's 1128 // connection may be different than the previous iteration. However, it 1129 // is still guaranteed to be non-null with each iteration of this loop. 1130 const connection = r.connection.?; 1131 1132 if (r.method == .CONNECT and head.status.class() == .success) { 1133 // This connection is no longer doing HTTP. 1134 connection.closing = false; 1135 r.response_transfer_encoding = head.transfer_encoding; 1136 r.response_content_length = head.content_length; 1137 return response; 1138 } 1139 1140 connection.closing = !head.keep_alive or !r.keep_alive; 1141 1142 // Any response to a HEAD request and any response with a 1xx 1143 // (Informational), 204 (No Content), or 304 (Not Modified) status 1144 // code is always terminated by the first empty line after the 1145 // header fields, regardless of the header fields present in the 1146 // message. 1147 if (r.method == .HEAD or head.status.class() == .informational or 1148 head.status == .no_content or head.status == .not_modified) 1149 { 1150 r.response_transfer_encoding = head.transfer_encoding; 1151 r.response_content_length = head.content_length; 1152 return response; 1153 } 1154 1155 if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { 1156 if (r.redirect_behavior == .not_allowed) { 1157 // Connection can still be reused by skipping the body. 1158 const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); 1159 _ = reader.discardRemaining() catch |err| switch (err) { 1160 error.ReadFailed => connection.closing = true, 1161 }; 1162 return error.TooManyHttpRedirects; 1163 } 1164 try r.redirect(head, &aux_buf); 1165 try r.sendBodiless(); 1166 continue; 1167 } 1168 1169 if (!r.accept_encoding[@intFromEnum(head.content_encoding)]) 1170 return error.HttpContentEncodingUnsupported; 1171 1172 r.response_transfer_encoding = head.transfer_encoding; 1173 r.response_content_length = head.content_length; 1174 return response; 1175 } 1176 } 1177 1178 /// This function takes an auxiliary buffer to store the arbitrarily large 1179 /// URI which may need to be merged with the previous URI, and that data 1180 /// needs to survive across different connections, which is where the input 1181 /// buffer lives. 1182 /// 1183 /// `aux_buf` must outlive accesses to `Request.uri`. 1184 fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { 1185 const new_location = head.location orelse return error.HttpRedirectLocationMissing; 1186 if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; 1187 const location = aux_buf.*[0..new_location.len]; 1188 @memcpy(location, new_location); 1189 { 1190 // Skip the body of the redirect response to leave the connection in 1191 // the correct state. This causes `new_location` to be invalidated. 1192 const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); 1193 _ = reader.discardRemaining() catch |err| switch (err) { 1194 error.ReadFailed => return r.reader.body_err.?, 1195 }; 1196 } 1197 const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { 1198 error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, 1199 error.InvalidFormat => return error.HttpRedirectLocationInvalid, 1200 error.InvalidPort => return error.HttpRedirectLocationInvalid, 1201 error.NoSpaceLeft => return error.HttpRedirectLocationOversize, 1202 }; 1203 1204 const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; 1205 const old_connection = r.connection.?; 1206 const old_host = old_connection.host(); 1207 var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; 1208 const new_host = try new_uri.getHost(&new_host_name_buffer); 1209 const keep_privileged_headers = 1210 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and 1211 sameParentDomain(old_host, new_host); 1212 1213 r.client.connection_pool.release(old_connection); 1214 r.connection = null; 1215 1216 if (!keep_privileged_headers) { 1217 // When redirecting to a different domain, strip privileged headers. 1218 r.privileged_headers = &.{}; 1219 } 1220 1221 if (switch (head.status) { 1222 .see_other => true, 1223 .moved_permanently, .found => r.method == .POST, 1224 else => false, 1225 }) { 1226 // A redirect to a GET must change the method and remove the body. 1227 r.method = .GET; 1228 r.transfer_encoding = .none; 1229 r.headers.content_type = .omit; 1230 } 1231 1232 if (r.transfer_encoding != .none) { 1233 // The request body has already been sent. The request is 1234 // still in a valid state, but the redirect must be handled 1235 // manually. 1236 return error.RedirectRequiresResend; 1237 } 1238 1239 const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); 1240 r.uri = new_uri; 1241 r.connection = new_connection; 1242 r.reader = .{ 1243 .in = new_connection.reader(), 1244 .state = .ready, 1245 // Populated when `http.Reader.bodyReader` is called. 1246 .interface = undefined, 1247 .max_head_len = r.client.read_buffer_size, 1248 }; 1249 r.redirect_behavior.subtractOne(); 1250 } 1251 1252 /// Returns true if the default behavior is required, otherwise handles 1253 /// writing (or not writing) the header. 1254 fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool { 1255 switch (v) { 1256 .default => return true, 1257 .omit => return false, 1258 .override => |x| { 1259 var vecs: [3][]const u8 = .{ prefix, x, "\r\n" }; 1260 try bw.writeVecAll(&vecs); 1261 return false; 1262 }, 1263 } 1264 } 1265 }; 1266 1267 pub const Proxy = struct { 1268 protocol: Protocol, 1269 host: []const u8, 1270 authorization: ?[]const u8, 1271 port: u16, 1272 supports_connect: bool, 1273 }; 1274 1275 /// Release all associated resources with the client. 1276 /// 1277 /// All pending requests must be de-initialized and all active connections released 1278 /// before calling this function. 1279 pub fn deinit(client: *Client) void { 1280 assert(client.connection_pool.used.first == null); // There are still active requests. 1281 1282 client.connection_pool.deinit(); 1283 if (!disable_tls) client.ca_bundle.deinit(client.allocator); 1284 1285 client.* = undefined; 1286 } 1287 1288 /// Populates `http_proxy` and `https_proxy` via standard proxy environment variables. 1289 /// Asserts the client has no active connections. 1290 /// Uses `arena` for a few small allocations that must outlive the client, or 1291 /// at least until those fields are set to different values. 1292 pub fn initDefaultProxies(client: *Client, arena: Allocator) !void { 1293 // Prevent any new connections from being created. 1294 client.connection_pool.mutex.lock(); 1295 defer client.connection_pool.mutex.unlock(); 1296 1297 assert(client.connection_pool.used.first == null); // There are active requests. 1298 1299 if (client.http_proxy == null) { 1300 client.http_proxy = try createProxyFromEnvVar(arena, &.{ 1301 "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY", 1302 }); 1303 } 1304 1305 if (client.https_proxy == null) { 1306 client.https_proxy = try createProxyFromEnvVar(arena, &.{ 1307 "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY", 1308 }); 1309 } 1310 } 1311 1312 fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?*Proxy { 1313 const content = for (env_var_names) |name| { 1314 const content = std.process.getEnvVarOwned(arena, name) catch |err| switch (err) { 1315 error.EnvironmentVariableNotFound => continue, 1316 else => |e| return e, 1317 }; 1318 1319 if (content.len == 0) continue; 1320 1321 break content; 1322 } else return null; 1323 1324 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); 1325 const protocol = Protocol.fromUri(uri) orelse return null; 1326 const raw_host = try uri.getHostAlloc(arena); 1327 1328 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { 1329 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); 1330 assert(basic_authorization.value(uri, authorization).len == authorization.len); 1331 break :a authorization; 1332 } else null; 1333 1334 const proxy = try arena.create(Proxy); 1335 proxy.* = .{ 1336 .protocol = protocol, 1337 .host = raw_host, 1338 .authorization = authorization, 1339 .port = uriPort(uri, protocol), 1340 .supports_connect = true, 1341 }; 1342 return proxy; 1343 } 1344 1345 pub const basic_authorization = struct { 1346 pub const max_user_len = 255; 1347 pub const max_password_len = 255; 1348 pub const max_value_len = valueLength(max_user_len, max_password_len); 1349 1350 pub fn valueLength(user_len: usize, password_len: usize) usize { 1351 return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); 1352 } 1353 1354 pub fn valueLengthFromUri(uri: Uri) usize { 1355 const user: Uri.Component = uri.user orelse .empty; 1356 const password: Uri.Component = uri.password orelse .empty; 1357 1358 var dw: Writer.Discarding = .init(&.{}); 1359 user.formatUser(&dw.writer) catch unreachable; // discarding 1360 const user_len = dw.count + dw.writer.end; 1361 1362 dw.count = 0; 1363 dw.writer.end = 0; 1364 password.formatPassword(&dw.writer) catch unreachable; // discarding 1365 const password_len = dw.count + dw.writer.end; 1366 1367 return valueLength(@intCast(user_len), @intCast(password_len)); 1368 } 1369 1370 pub fn value(uri: Uri, out: []u8) []u8 { 1371 var bw: Writer = .fixed(out); 1372 write(uri, &bw) catch unreachable; 1373 return bw.buffered(); 1374 } 1375 1376 pub fn write(uri: Uri, out: *Writer) Writer.Error!void { 1377 var buf: [max_user_len + 1 + max_password_len]u8 = undefined; 1378 var w: Writer = .fixed(&buf); 1379 const user: Uri.Component = uri.user orelse .empty; 1380 const password: Uri.Component = uri.password orelse .empty; 1381 user.formatUser(&w) catch unreachable; 1382 w.writeByte(':') catch unreachable; 1383 password.formatPassword(&w) catch unreachable; 1384 try out.print("Basic {b64}", .{w.buffered()}); 1385 } 1386 }; 1387 1388 pub const ConnectTcpError = Allocator.Error || error{ 1389 ConnectionRefused, 1390 NetworkUnreachable, 1391 ConnectionTimedOut, 1392 ConnectionResetByPeer, 1393 TemporaryNameServerFailure, 1394 NameServerFailure, 1395 UnknownHostName, 1396 HostLacksNetworkAddresses, 1397 UnexpectedConnectFailure, 1398 TlsInitializationFailed, 1399 }; 1400 1401 /// Reuses a `Connection` if one matching `host` and `port` is already open. 1402 /// 1403 /// Threadsafe. 1404 pub fn connectTcp( 1405 client: *Client, 1406 host: []const u8, 1407 port: u16, 1408 protocol: Protocol, 1409 ) ConnectTcpError!*Connection { 1410 return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol }); 1411 } 1412 1413 pub const ConnectTcpOptions = struct { 1414 host: Io.net.HostName, 1415 port: u16, 1416 protocol: Protocol, 1417 1418 proxied_host: ?[]const u8 = null, 1419 proxied_port: ?u16 = null, 1420 }; 1421 1422 pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { 1423 const host = options.host_name; 1424 const port = options.port; 1425 const protocol = options.protocol; 1426 1427 const proxied_host = options.proxied_host orelse host; 1428 const proxied_port = options.proxied_port orelse port; 1429 1430 if (client.connection_pool.findConnection(.{ 1431 .host = proxied_host, 1432 .port = proxied_port, 1433 .protocol = protocol, 1434 })) |conn| return conn; 1435 1436 const stream = host.connectTcp(client.io, port) catch |err| switch (err) { 1437 error.ConnectionRefused => return error.ConnectionRefused, 1438 error.NetworkUnreachable => return error.NetworkUnreachable, 1439 error.ConnectionTimedOut => return error.ConnectionTimedOut, 1440 error.ConnectionResetByPeer => return error.ConnectionResetByPeer, 1441 error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure, 1442 error.NameServerFailure => return error.NameServerFailure, 1443 error.UnknownHostName => return error.UnknownHostName, 1444 error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, 1445 error.Canceled => return error.Canceled, 1446 else => return error.UnexpectedConnectFailure, 1447 }; 1448 errdefer stream.close(); 1449 1450 switch (protocol) { 1451 .tls => { 1452 if (disable_tls) return error.TlsInitializationFailed; 1453 const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); 1454 client.connection_pool.addUsed(&tc.connection); 1455 return &tc.connection; 1456 }, 1457 .plain => { 1458 const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream); 1459 client.connection_pool.addUsed(&pc.connection); 1460 return &pc.connection; 1461 }, 1462 } 1463 } 1464 1465 pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError; 1466 1467 /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. 1468 /// 1469 /// This function is threadsafe. 1470 pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { 1471 if (client.connection_pool.findConnection(.{ 1472 .host = path, 1473 .port = 0, 1474 .protocol = .plain, 1475 })) |node| 1476 return node; 1477 1478 const conn = try client.allocator.create(ConnectionPool.Node); 1479 errdefer client.allocator.destroy(conn); 1480 conn.* = .{ .data = undefined }; 1481 1482 const stream = try std.net.connectUnixSocket(path); 1483 errdefer stream.close(); 1484 1485 conn.data = .{ 1486 .stream = stream, 1487 .tls_client = undefined, 1488 .protocol = .plain, 1489 1490 .host = try client.allocator.dupe(u8, path), 1491 .port = 0, 1492 }; 1493 errdefer client.allocator.free(conn.data.host); 1494 1495 client.connection_pool.addUsed(conn); 1496 1497 return &conn.data; 1498 } 1499 1500 /// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP 1501 /// CONNECT. This will reuse a connection if one is already open. 1502 /// 1503 /// This function is threadsafe. 1504 pub fn connectProxied( 1505 client: *Client, 1506 proxy: *Proxy, 1507 proxied_host: []const u8, 1508 proxied_port: u16, 1509 ) !*Connection { 1510 if (!proxy.supports_connect) return error.TunnelNotSupported; 1511 1512 if (client.connection_pool.findConnection(.{ 1513 .host = proxied_host, 1514 .port = proxied_port, 1515 .protocol = proxy.protocol, 1516 })) |node| return node; 1517 1518 var maybe_valid = false; 1519 (tunnel: { 1520 const connection = try client.connectTcpOptions(.{ 1521 .host = proxy.host, 1522 .port = proxy.port, 1523 .protocol = proxy.protocol, 1524 .proxied_host = proxied_host, 1525 .proxied_port = proxied_port, 1526 }); 1527 errdefer { 1528 connection.closing = true; 1529 client.connection_pool.release(connection); 1530 } 1531 1532 var req = client.request(.CONNECT, .{ 1533 .scheme = "http", 1534 .host = .{ .raw = proxied_host }, 1535 .port = proxied_port, 1536 }, .{ 1537 .redirect_behavior = .unhandled, 1538 .connection = connection, 1539 }) catch |err| { 1540 break :tunnel err; 1541 }; 1542 defer req.deinit(); 1543 1544 req.sendBodiless() catch |err| break :tunnel err; 1545 const response = req.receiveHead(&.{}) catch |err| break :tunnel err; 1546 1547 if (response.head.status.class() == .server_error) { 1548 maybe_valid = true; 1549 break :tunnel error.ServerError; 1550 } 1551 1552 if (response.head.status != .ok) break :tunnel error.ConnectionRefused; 1553 1554 // this connection is now a tunnel, so we can't use it for anything 1555 // else, it will only be released when the client is de-initialized. 1556 req.connection = null; 1557 1558 connection.closing = false; 1559 1560 return connection; 1561 }) catch { 1562 // something went wrong with the tunnel 1563 proxy.supports_connect = maybe_valid; 1564 return error.TunnelNotSupported; 1565 }; 1566 } 1567 1568 pub const ConnectError = ConnectTcpError || RequestError; 1569 1570 /// Connect to `host:port` using the specified protocol. This will reuse a 1571 /// connection if one is already open. 1572 /// 1573 /// If a proxy is configured for the client, then the proxy will be used to 1574 /// connect to the host. 1575 /// 1576 /// This function is threadsafe. 1577 pub fn connect( 1578 client: *Client, 1579 host: []const u8, 1580 port: u16, 1581 protocol: Protocol, 1582 ) ConnectError!*Connection { 1583 const proxy = switch (protocol) { 1584 .plain => client.http_proxy, 1585 .tls => client.https_proxy, 1586 } orelse return client.connectTcp(host, port, protocol); 1587 1588 // Prevent proxying through itself. 1589 if (std.ascii.eqlIgnoreCase(proxy.host, host) and 1590 proxy.port == port and proxy.protocol == protocol) 1591 { 1592 return client.connectTcp(host, port, protocol); 1593 } 1594 1595 if (proxy.supports_connect) tunnel: { 1596 return connectProxied(client, proxy, host, port) catch |err| switch (err) { 1597 error.TunnelNotSupported => break :tunnel, 1598 else => |e| return e, 1599 }; 1600 } 1601 1602 // fall back to using the proxy as a normal http proxy 1603 const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); 1604 connection.proxied = true; 1605 return connection; 1606 } 1607 1608 pub const RequestError = ConnectTcpError || error{ 1609 UnsupportedUriScheme, 1610 UriMissingHost, 1611 UriHostTooLong, 1612 CertificateBundleLoadFailure, 1613 }; 1614 1615 pub const RequestOptions = struct { 1616 version: http.Version = .@"HTTP/1.1", 1617 1618 /// Automatically ignore 100 Continue responses. This assumes you don't 1619 /// care, and will have sent the body before you wait for the response. 1620 /// 1621 /// If this is not the case AND you know the server will send a 100 1622 /// Continue, set this to false and wait for a response before sending the 1623 /// body. If you wait AND the server does not send a 100 Continue before 1624 /// you finish the request, then the request *will* deadlock. 1625 handle_continue: bool = true, 1626 1627 /// If false, close the connection after the one request. If true, 1628 /// participate in the client connection pool. 1629 keep_alive: bool = true, 1630 1631 /// This field specifies whether to automatically follow redirects, and if 1632 /// so, how many redirects to follow before returning an error. 1633 /// 1634 /// This will only follow redirects for repeatable requests (ie. with no 1635 /// payload or the server has acknowledged the payload). 1636 redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), 1637 1638 /// Must be an already acquired connection. 1639 connection: ?*Connection = null, 1640 1641 /// Standard headers that have default, but overridable, behavior. 1642 headers: Request.Headers = .{}, 1643 /// These headers are kept including when following a redirect to a 1644 /// different domain. 1645 /// Externally-owned; must outlive the Request. 1646 extra_headers: []const http.Header = &.{}, 1647 /// These headers are stripped when following a redirect to a different 1648 /// domain. 1649 /// Externally-owned; must outlive the Request. 1650 privileged_headers: []const http.Header = &.{}, 1651 }; 1652 1653 fn uriPort(uri: Uri, protocol: Protocol) u16 { 1654 return uri.port orelse protocol.port(); 1655 } 1656 1657 /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. 1658 /// 1659 /// The caller is responsible for calling `deinit()` on the `Request`. 1660 /// This function is threadsafe. 1661 /// 1662 /// Asserts that "\r\n" does not occur in any header name or value. 1663 pub fn request( 1664 client: *Client, 1665 method: http.Method, 1666 uri: Uri, 1667 options: RequestOptions, 1668 ) RequestError!Request { 1669 if (std.debug.runtime_safety) { 1670 for (options.extra_headers) |header| { 1671 assert(header.name.len != 0); 1672 assert(std.mem.indexOfScalar(u8, header.name, ':') == null); 1673 assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null); 1674 assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); 1675 } 1676 for (options.privileged_headers) |header| { 1677 assert(header.name.len != 0); 1678 assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null); 1679 assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); 1680 } 1681 } 1682 1683 const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; 1684 1685 if (protocol == .tls) { 1686 if (disable_tls) unreachable; 1687 if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { 1688 client.ca_bundle_mutex.lock(); 1689 defer client.ca_bundle_mutex.unlock(); 1690 1691 if (client.next_https_rescan_certs) { 1692 client.ca_bundle.rescan(client.allocator) catch 1693 return error.CertificateBundleLoadFailure; 1694 @atomicStore(bool, &client.next_https_rescan_certs, false, .release); 1695 } 1696 } 1697 } 1698 1699 const connection = options.connection orelse c: { 1700 var host_name_buffer: [Uri.host_name_max]u8 = undefined; 1701 const host_name = try uri.getHost(&host_name_buffer); 1702 break :c try client.connect(host_name, uriPort(uri, protocol), protocol); 1703 }; 1704 1705 return .{ 1706 .uri = uri, 1707 .client = client, 1708 .connection = connection, 1709 .reader = .{ 1710 .in = connection.reader(), 1711 .state = .ready, 1712 // Populated when `http.Reader.bodyReader` is called. 1713 .interface = undefined, 1714 .max_head_len = client.read_buffer_size, 1715 }, 1716 .keep_alive = options.keep_alive, 1717 .method = method, 1718 .version = options.version, 1719 .transfer_encoding = .none, 1720 .redirect_behavior = options.redirect_behavior, 1721 .handle_continue = options.handle_continue, 1722 .headers = options.headers, 1723 .extra_headers = options.extra_headers, 1724 .privileged_headers = options.privileged_headers, 1725 }; 1726 } 1727 1728 pub const FetchOptions = struct { 1729 /// `null` means it will be heap-allocated. 1730 redirect_buffer: ?[]u8 = null, 1731 /// `null` means it will be heap-allocated. 1732 decompress_buffer: ?[]u8 = null, 1733 redirect_behavior: ?Request.RedirectBehavior = null, 1734 /// If the server sends a body, it will be written here. 1735 response_writer: ?*Writer = null, 1736 1737 location: Location, 1738 method: ?http.Method = null, 1739 payload: ?[]const u8 = null, 1740 raw_uri: bool = false, 1741 keep_alive: bool = true, 1742 1743 /// Standard headers that have default, but overridable, behavior. 1744 headers: Request.Headers = .{}, 1745 /// These headers are kept including when following a redirect to a 1746 /// different domain. 1747 /// Externally-owned; must outlive the Request. 1748 extra_headers: []const http.Header = &.{}, 1749 /// These headers are stripped when following a redirect to a different 1750 /// domain. 1751 /// Externally-owned; must outlive the Request. 1752 privileged_headers: []const http.Header = &.{}, 1753 1754 pub const Location = union(enum) { 1755 url: []const u8, 1756 uri: Uri, 1757 }; 1758 }; 1759 1760 pub const FetchResult = struct { 1761 status: http.Status, 1762 }; 1763 1764 pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{ 1765 StreamTooLong, 1766 /// TODO provide optional diagnostics when this occurs or break into more error codes 1767 WriteFailed, 1768 UnsupportedCompressionMethod, 1769 }; 1770 1771 /// Perform a one-shot HTTP request with the provided options. 1772 /// 1773 /// This function is threadsafe. 1774 pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { 1775 const uri = switch (options.location) { 1776 .url => |u| try Uri.parse(u), 1777 .uri => |u| u, 1778 }; 1779 const method: http.Method = options.method orelse 1780 if (options.payload != null) .POST else .GET; 1781 1782 const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse 1783 if (options.payload == null) @enumFromInt(3) else .unhandled; 1784 1785 var req = try request(client, method, uri, .{ 1786 .redirect_behavior = redirect_behavior, 1787 .headers = options.headers, 1788 .extra_headers = options.extra_headers, 1789 .privileged_headers = options.privileged_headers, 1790 .keep_alive = options.keep_alive, 1791 }); 1792 defer req.deinit(); 1793 1794 if (options.payload) |payload| { 1795 req.transfer_encoding = .{ .content_length = payload.len }; 1796 var body = try req.sendBodyUnflushed(&.{}); 1797 try body.writer.writeAll(payload); 1798 try body.end(); 1799 try req.connection.?.flush(); 1800 } else { 1801 try req.sendBodiless(); 1802 } 1803 1804 const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse 1805 try client.allocator.alloc(u8, 8 * 1024); 1806 defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer); 1807 1808 var response = try req.receiveHead(redirect_buffer); 1809 1810 const response_writer = options.response_writer orelse { 1811 const reader = response.reader(&.{}); 1812 _ = reader.discardRemaining() catch |err| switch (err) { 1813 error.ReadFailed => return response.bodyErr().?, 1814 }; 1815 return .{ .status = response.head.status }; 1816 }; 1817 1818 const decompress_buffer: []u8 = switch (response.head.content_encoding) { 1819 .identity => &.{}, 1820 .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len), 1821 .deflate, .gzip => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.flate.max_window_len), 1822 .compress => return error.UnsupportedCompressionMethod, 1823 }; 1824 defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer); 1825 1826 var transfer_buffer: [64]u8 = undefined; 1827 var decompress: http.Decompress = undefined; 1828 const reader = response.readerDecompressing(&transfer_buffer, &decompress, decompress_buffer); 1829 1830 _ = reader.streamRemaining(response_writer) catch |err| switch (err) { 1831 error.ReadFailed => return response.bodyErr().?, 1832 else => |e| return e, 1833 }; 1834 1835 return .{ .status = response.head.status }; 1836 } 1837 1838 pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { 1839 if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; 1840 if (child_host.len == parent_host.len) return true; 1841 if (parent_host.len > child_host.len) return false; 1842 return child_host[child_host.len - parent_host.len - 1] == '.'; 1843 } 1844 1845 test sameParentDomain { 1846 try testing.expect(!sameParentDomain("foo.com", "bar.com")); 1847 try testing.expect(sameParentDomain("foo.com", "foo.com")); 1848 try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); 1849 try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); 1850 } 1851 1852 test { 1853 _ = Response; 1854 }