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