struct Client [src]

Alias for std.http.Client

HTTP(S) Client implementation. Connections are opened in a thread-safe manner, but individual Requests are not. TLS support may be disabled via std.options.http_disable_tls.

Fields

allocator: AllocatorUsed for all client allocations. Must be thread-safe.
ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}
ca_bundle_mutex: std.Thread.Mutex = .{}
tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_lenUsed both for the reader and writer buffers.
ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = nullIf non-null, ssl secrets are logged to a stream. Creating such a stream allows other processes with access to that stream to decrypt all traffic over connections created with this Client.
next_https_rescan_certs: bool = trueWhen this is true, the next time this client performs an HTTPS request, it will first rescan the system for root certificates.
connection_pool: ConnectionPool = .{}The pool of connections that can be reused (and currently in use).
read_buffer_size: usize = 8192Each Connection allocates this amount for the reader buffer. If the entire HTTP header cannot fit in this amount of bytes, error.HttpHeadersOversize will be returned from Request.wait.
write_buffer_size: usize = 1024Each Connection allocates this amount for the writer buffer.
http_proxy: ?*Proxy = nullIf populated, all http traffic travels through this third party. This field cannot be modified while the client has active connections. Pointer to externally-owned memory.
https_proxy: ?*Proxy = nullIf populated, all https traffic travels through this third party. This field cannot be modified while the client has active connections. Pointer to externally-owned memory.

Members

Source

//! HTTP(S) Client implementation. //! //! Connections are opened in a thread-safe manner, but individual Requests are not. //! //! TLS support may be disabled via `std.options.http_disable_tls`. const std = @import("../std.zig"); const builtin = @import("builtin"); const testing = std.testing; const http = std.http; const mem = std.mem; const net = std.net; const Uri = std.Uri; const Allocator = mem.Allocator; const assert = std.debug.assert; const Writer = std.Io.Writer; const Reader = std.Io.Reader; const Client = @This(); pub const disable_tls = std.options.http_disable_tls; /// Used for all client allocations. Must be thread-safe. allocator: Allocator, ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, ca_bundle_mutex: std.Thread.Mutex = .{}, /// Used both for the reader and writer buffers. tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len, /// If non-null, ssl secrets are logged to a stream. Creating such a stream /// allows other processes with access to that stream to decrypt all /// traffic over connections created with this `Client`. ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null, /// When this is `true`, the next time this client performs an HTTPS request, /// it will first rescan the system for root certificates. next_https_rescan_certs: bool = true, /// The pool of connections that can be reused (and currently in use). connection_pool: ConnectionPool = .{}, /// Each `Connection` allocates this amount for the reader buffer. /// /// If the entire HTTP header cannot fit in this amount of bytes, /// `error.HttpHeadersOversize` will be returned from `Request.wait`. read_buffer_size: usize = 8192, /// Each `Connection` allocates this amount for the writer buffer. write_buffer_size: usize = 1024, /// If populated, all http traffic travels through this third party. /// This field cannot be modified while the client has active connections. /// Pointer to externally-owned memory. http_proxy: ?*Proxy = null, /// If populated, all https traffic travels through this third party. /// This field cannot be modified while the client has active connections. /// Pointer to externally-owned memory. https_proxy: ?*Proxy = null, /// A Least-Recently-Used cache of open connections to be reused. pub const ConnectionPool = struct { mutex: std.Thread.Mutex = .{}, /// Open connections that are currently in use. used: std.DoublyLinkedList = .{}, /// Open connections that are not currently in use. free: std.DoublyLinkedList = .{}, free_len: usize = 0, free_size: usize = 32, /// The criteria for a connection to be considered a match. pub const Criteria = struct { host: []const u8, port: u16, protocol: Protocol, }; /// Finds and acquires a connection from the connection pool matching the criteria. /// If no connection is found, null is returned. /// /// Threadsafe. pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection { pool.mutex.lock(); defer pool.mutex.unlock(); var next = pool.free.last; while (next) |node| : (next = node.prev) { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); if (connection.protocol != criteria.protocol) continue; if (connection.port != criteria.port) continue; // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue; pool.acquireUnsafe(connection); return connection; } return null; } /// Acquires an existing connection from the connection pool. This function is not threadsafe. pub fn acquireUnsafe(pool: *ConnectionPool, connection: *Connection) void { pool.free.remove(&connection.pool_node); pool.free_len -= 1; pool.used.append(&connection.pool_node); } /// Acquires an existing connection from the connection pool. This function is threadsafe. pub fn acquire(pool: *ConnectionPool, connection: *Connection) void { pool.mutex.lock(); defer pool.mutex.unlock(); return pool.acquireUnsafe(connection); } /// Tries to release a connection back to the connection pool. /// If the connection is marked as closing, it will be closed instead. /// /// Threadsafe. pub fn release(pool: *ConnectionPool, connection: *Connection) void { pool.mutex.lock(); defer pool.mutex.unlock(); pool.used.remove(&connection.pool_node); if (connection.closing or pool.free_size == 0) return connection.destroy(); if (pool.free_len >= pool.free_size) { const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?)); pool.free_len -= 1; popped.destroy(); } if (connection.proxied) { // proxied connections go to the end of the queue, always try direct connections first pool.free.prepend(&connection.pool_node); } else { pool.free.append(&connection.pool_node); } pool.free_len += 1; } /// Adds a newly created node to the pool of used connections. This function is threadsafe. pub fn addUsed(pool: *ConnectionPool, connection: *Connection) void { pool.mutex.lock(); defer pool.mutex.unlock(); pool.used.append(&connection.pool_node); } /// Resizes the connection pool. /// /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. /// /// Threadsafe. pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { pool.mutex.lock(); defer pool.mutex.unlock(); const next = pool.free.first; _ = next; while (pool.free_len > new_size) { const popped = pool.free.popFirst() orelse unreachable; pool.free_len -= 1; popped.data.close(allocator); allocator.destroy(popped); } pool.free_size = new_size; } /// Frees the connection pool and closes all connections within. /// /// All future operations on the connection pool will deadlock. /// /// Threadsafe. pub fn deinit(pool: *ConnectionPool) void { pool.mutex.lock(); var next = pool.free.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; connection.destroy(); } next = pool.used.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; connection.destroy(); } pool.* = undefined; } }; pub const Protocol = enum { plain, tls, fn port(protocol: Protocol) u16 { return switch (protocol) { .plain => 80, .tls => 443, }; } pub fn fromScheme(scheme: []const u8) ?Protocol { const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ .{ "http", .plain }, .{ "ws", .plain }, .{ "https", .tls }, .{ "wss", .tls }, }); return protocol_map.get(scheme); } pub fn fromUri(uri: Uri) ?Protocol { return fromScheme(uri.scheme); } }; pub const Connection = struct { client: *Client, stream_writer: net.Stream.Writer, stream_reader: net.Stream.Reader, /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. pool_node: std.DoublyLinkedList.Node, port: u16, host_len: u8, proxied: bool, closing: bool, protocol: Protocol, const Plain = struct { connection: Connection, fn create( client: *Client, remote_host: []const u8, port: u16, stream: net.Stream, ) error{OutOfMemory}!*Plain { const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.len); const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); errdefer gpa.free(base); const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); @memcpy(host_buffer, remote_host); const plain: *Plain = @ptrCast(base); plain.* = .{ .connection = .{ .client = client, .stream_writer = stream.writer(socket_write_buffer), .stream_reader = stream.reader(socket_read_buffer), .pool_node = .{}, .port = port, .host_len = @intCast(remote_host.len), .proxied = false, .closing = false, .protocol = .plain, }, }; return plain; } fn destroy(plain: *Plain) void { const c = &plain.connection; const gpa = c.client.allocator; const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain); gpa.free(base[0..allocLen(c.client, c.host_len)]); } fn allocLen(client: *Client, host_len: usize) usize { return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; } fn host(plain: *Plain) []u8 { const base: [*]u8 = @ptrCast(plain); return base[@sizeOf(Plain)..][0..plain.connection.host_len]; } }; const Tls = struct { client: std.crypto.tls.Client, connection: Connection, fn create( client: *Client, remote_host: []const u8, port: u16, stream: net.Stream, ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.len); const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); errdefer gpa.free(base); const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; // The TLS client wants enough buffer for the max encrypted frame // size, and the HTTP body reader wants enough buffer for the // entire HTTP header. This means we need a combined upper bound. const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size; const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..tls_read_buffer_len]; const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size]; const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size]; assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len); @memcpy(host_buffer, remote_host); const tls: *Tls = @ptrCast(base); tls.* = .{ .connection = .{ .client = client, .stream_writer = stream.writer(tls_write_buffer), .stream_reader = stream.reader(socket_read_buffer), .pool_node = .{}, .port = port, .host_len = @intCast(remote_host.len), .proxied = false, .closing = false, .protocol = .tls, }, // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true .client = std.crypto.tls.Client.init( tls.connection.stream_reader.interface(), &tls.connection.stream_writer.interface, .{ .host = .{ .explicit = remote_host }, .ca = .{ .bundle = client.ca_bundle }, .ssl_key_log = client.ssl_key_log, .read_buffer = tls_read_buffer, .write_buffer = socket_write_buffer, // This is appropriate for HTTPS because the HTTP headers contain // the content length which is used to detect truncation attacks. .allow_truncation_attacks = true, }, ) catch return error.TlsInitializationFailed, }; return tls; } fn destroy(tls: *Tls) void { const c = &tls.connection; const gpa = c.client.allocator; const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls); gpa.free(base[0..allocLen(c.client, c.host_len)]); } fn allocLen(client: *Client, host_len: usize) usize { const tls_read_buffer_len = client.tls_buffer_size + client.read_buffer_size; return @sizeOf(Tls) + host_len + tls_read_buffer_len + client.tls_buffer_size + client.write_buffer_size + client.tls_buffer_size; } fn host(tls: *Tls) []u8 { const base: [*]u8 = @ptrCast(tls); return base[@sizeOf(Tls)..][0..tls.connection.host_len]; } }; pub const ReadError = std.crypto.tls.Client.ReadError || std.net.Stream.ReadError; pub fn getReadError(c: *const Connection) ?ReadError { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c)); return tls.client.read_err orelse c.stream_reader.getError(); }, .plain => { return c.stream_reader.getError(); }, }; } fn getStream(c: *Connection) net.Stream { return c.stream_reader.getStream(); } pub fn host(c: *Connection) []u8 { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); return tls.host(); }, .plain => { const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); return plain.host(); }, }; } /// If this is called without calling `flush` or `end`, data will be /// dropped unsent. pub fn destroy(c: *Connection) void { c.getStream().close(); switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); tls.destroy(); }, .plain => { const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); plain.destroy(); }, } } /// HTTP protocol from client to server. /// This either goes directly to `stream_writer`, or to a TLS client. pub fn writer(c: *Connection) *Writer { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); return &tls.client.writer; }, .plain => &c.stream_writer.interface, }; } /// HTTP protocol from server to client. /// This either comes directly from `stream_reader`, or from a TLS client. pub fn reader(c: *Connection) *Reader { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); return &tls.client.reader; }, .plain => c.stream_reader.interface(), }; } pub fn flush(c: *Connection) Writer.Error!void { if (c.protocol == .tls) { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); try tls.client.writer.flush(); } try c.stream_writer.interface.flush(); } /// If the connection is a TLS connection, sends the close_notify alert. /// /// Flushes all buffers. pub fn end(c: *Connection) Writer.Error!void { if (c.protocol == .tls) { if (disable_tls) unreachable; const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); try tls.client.end(); } try c.stream_writer.interface.flush(); } }; pub const Response = struct { request: *Request, /// Pointers in this struct are invalidated when the response body stream /// is initialized. head: Head, pub const Head = struct { bytes: []const u8, version: http.Version, status: http.Status, reason: []const u8, location: ?[]const u8 = null, content_type: ?[]const u8 = null, content_disposition: ?[]const u8 = null, keep_alive: bool, /// If present, the number of bytes in the response body. content_length: ?u64 = null, transfer_encoding: http.TransferEncoding = .none, content_encoding: http.ContentEncoding = .identity, pub const ParseError = error{ HttpConnectionHeaderUnsupported, HttpContentEncodingUnsupported, HttpHeaderContinuationsUnsupported, HttpHeadersInvalid, HttpTransferEncodingUnsupported, InvalidContentLength, }; pub fn parse(bytes: []const u8) ParseError!Head { var res: Head = .{ .bytes = bytes, .status = undefined, .reason = undefined, .version = undefined, .keep_alive = false, }; var it = mem.splitSequence(u8, bytes, "\r\n"); const first_line = it.first(); if (first_line.len < 12) return error.HttpHeadersInvalid; const version: http.Version = switch (int64(first_line[0..8])) { int64("HTTP/1.0") => .@"HTTP/1.0", int64("HTTP/1.1") => .@"HTTP/1.1", else => return error.HttpHeadersInvalid, }; if (first_line[8] != ' ') return error.HttpHeadersInvalid; const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); const reason = mem.trimLeft(u8, first_line[12..], " "); res.version = version; res.status = status; res.reason = reason; res.keep_alive = switch (version) { .@"HTTP/1.0" => false, .@"HTTP/1.1" => true, }; while (it.next()) |line| { if (line.len == 0) return res; switch (line[0]) { ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, else => {}, } var line_it = mem.splitScalar(u8, line, ':'); const header_name = line_it.next().?; const header_value = mem.trim(u8, line_it.rest(), " \t"); if (header_name.len == 0) return error.HttpHeadersInvalid; if (std.ascii.eqlIgnoreCase(header_name, "connection")) { res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { res.content_type = header_value; } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { res.location = header_value; } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { res.content_disposition = header_value; } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { // Transfer-Encoding: second, first // Transfer-Encoding: deflate, chunked var iter = mem.splitBackwardsScalar(u8, header_value, ','); const first = iter.first(); const trimmed_first = mem.trim(u8, first, " "); var next: ?[]const u8 = first; if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding res.transfer_encoding = transfer; next = iter.next(); } if (next) |second| { const trimmed_second = mem.trim(u8, second, " "); if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported res.content_encoding = transfer; } else { return error.HttpTransferEncodingUnsupported; } } if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; res.content_length = content_length; } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { if (res.content_encoding != .identity) return error.HttpHeadersInvalid; const trimmed = mem.trim(u8, header_value, " "); if (http.ContentEncoding.fromString(trimmed)) |ce| { res.content_encoding = ce; } else { return error.HttpContentEncodingUnsupported; } } } return error.HttpHeadersInvalid; // missing empty line } test parse { const response_bytes = "HTTP/1.1 200 OK\r\n" ++ "LOcation:url\r\n" ++ "content-tYpe: text/plain\r\n" ++ "content-disposition:attachment; filename=example.txt \r\n" ++ "content-Length:10\r\n" ++ "TRansfer-encoding:\tdeflate, chunked \r\n" ++ "connectioN:\t keep-alive \r\n\r\n"; const head = try Head.parse(response_bytes); try testing.expectEqual(.@"HTTP/1.1", head.version); try testing.expectEqualStrings("OK", head.reason); try testing.expectEqual(.ok, head.status); try testing.expectEqualStrings("url", head.location.?); try testing.expectEqualStrings("text/plain", head.content_type.?); try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); try testing.expectEqual(true, head.keep_alive); try testing.expectEqual(10, head.content_length.?); try testing.expectEqual(.chunked, head.transfer_encoding); try testing.expectEqual(.deflate, head.content_encoding); } pub fn iterateHeaders(h: Head) http.HeaderIterator { return .init(h.bytes); } test iterateHeaders { const response_bytes = "HTTP/1.1 200 OK\r\n" ++ "LOcation:url\r\n" ++ "content-tYpe: text/plain\r\n" ++ "content-disposition:attachment; filename=example.txt \r\n" ++ "content-Length:10\r\n" ++ "TRansfer-encoding:\tdeflate, chunked \r\n" ++ "connectioN:\t keep-alive \r\n\r\n"; const head = try Head.parse(response_bytes); var it = head.iterateHeaders(); { const header = it.next().?; try testing.expectEqualStrings("LOcation", header.name); try testing.expectEqualStrings("url", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("content-tYpe", header.name); try testing.expectEqualStrings("text/plain", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("content-disposition", header.name); try testing.expectEqualStrings("attachment; filename=example.txt", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("content-Length", header.name); try testing.expectEqualStrings("10", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("TRansfer-encoding", header.name); try testing.expectEqualStrings("deflate, chunked", header.value); try testing.expect(!it.is_trailer); } { const header = it.next().?; try testing.expectEqualStrings("connectioN", header.name); try testing.expectEqualStrings("keep-alive", header.value); try testing.expect(!it.is_trailer); } try testing.expectEqual(null, it.next()); } inline fn int64(array: *const [8]u8) u64 { return @bitCast(array.*); } fn parseInt3(text: *const [3]u8) u10 { const nnn: @Vector(3, u8) = text.*; const zero: @Vector(3, u8) = .{ '0', '0', '0' }; const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; return @reduce(.Add, (nnn -% zero) *% mmm); } test parseInt3 { const expectEqual = testing.expectEqual; try expectEqual(@as(u10, 0), parseInt3("000")); try expectEqual(@as(u10, 418), parseInt3("418")); try expectEqual(@as(u10, 999), parseInt3("999")); } /// Help the programmer avoid bugs by calling this when the string /// memory of `Head` becomes invalidated. fn invalidateStrings(h: *Head) void { h.bytes = undefined; h.reason = undefined; if (h.location) |*s| s.* = undefined; if (h.content_type) |*s| s.* = undefined; if (h.content_disposition) |*s| s.* = undefined; } }; /// If compressed body has been negotiated this will return compressed bytes. /// /// If the returned `Reader` returns `error.ReadFailed` the error is /// available via `bodyErr`. /// /// Asserts that this function is only called once. /// /// See also: /// * `readerDecompressing` pub fn reader(response: *Response, transfer_buffer: []u8) *Reader { response.head.invalidateStrings(); const req = response.request; if (!req.method.responseHasBody()) return .ending; const head = &response.head; return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length); } /// If compressed body has been negotiated this will return decompressed bytes. /// /// If the returned `Reader` returns `error.ReadFailed` the error is /// available via `bodyErr`. /// /// Asserts that this function is only called once. /// /// See also: /// * `reader` pub fn readerDecompressing( response: *Response, transfer_buffer: []u8, decompress: *http.Decompress, decompress_buffer: []u8, ) *Reader { response.head.invalidateStrings(); const head = &response.head; return response.request.reader.bodyReaderDecompressing( transfer_buffer, head.transfer_encoding, head.content_length, head.content_encoding, decompress, decompress_buffer, ); } /// After receiving `error.ReadFailed` from the `Reader` returned by /// `reader` or `readerDecompressing`, this function accesses the /// more specific error code. pub fn bodyErr(response: *const Response) ?http.Reader.BodyError { return response.request.reader.body_err; } pub fn iterateTrailers(response: *const Response) http.HeaderIterator { const r = &response.request.reader; assert(r.state == .ready); return .{ .bytes = r.trailers, .index = 0, .is_trailer = true, }; } }; pub const Request = struct { /// This field is provided so that clients can observe redirected URIs. /// /// Its backing memory is externally provided by API users when creating a /// request, and then again provided externally via `redirect_buffer` to /// `receiveHead`. uri: Uri, client: *Client, /// This is null when the connection is released. connection: ?*Connection, reader: http.Reader, keep_alive: bool, method: http.Method, version: http.Version = .@"HTTP/1.1", transfer_encoding: TransferEncoding, redirect_behavior: RedirectBehavior, accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding, /// Whether the request should handle a 100-continue response before sending the request body. handle_continue: bool, /// Standard headers that have default, but overridable, behavior. headers: Headers, /// Populated in `receiveHead`; used in `deinit` to determine whether to /// discard the body to reuse the connection. response_content_length: ?u64 = null, /// Populated in `receiveHead`; used in `deinit` to determine whether to /// discard the body to reuse the connection. response_transfer_encoding: http.TransferEncoding = .none, /// These headers are kept including when following a redirect to a /// different domain. /// Externally-owned; must outlive the Request. extra_headers: []const http.Header, /// These headers are stripped when following a redirect to a different /// domain. /// Externally-owned; must outlive the Request. privileged_headers: []const http.Header, pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: { var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false); result[@intFromEnum(http.ContentEncoding.gzip)] = true; result[@intFromEnum(http.ContentEncoding.deflate)] = true; result[@intFromEnum(http.ContentEncoding.identity)] = true; break :b result; }; pub const TransferEncoding = union(enum) { content_length: u64, chunked: void, none: void, }; pub const Headers = struct { host: Value = .default, authorization: Value = .default, user_agent: Value = .default, connection: Value = .default, accept_encoding: Value = .default, content_type: Value = .default, pub const Value = union(enum) { default, omit, override: []const u8, }; }; /// Any value other than `not_allowed` or `unhandled` means that integer represents /// how many remaining redirects are allowed. pub const RedirectBehavior = enum(u16) { /// The next redirect will cause an error. not_allowed = 0, /// Redirects are passed to the client to analyze the redirect response /// directly. unhandled = std.math.maxInt(u16), _, pub fn init(n: u16) RedirectBehavior { assert(n != std.math.maxInt(u16)); return @enumFromInt(n); } pub fn subtractOne(rb: *RedirectBehavior) void { switch (rb.*) { .not_allowed => unreachable, .unhandled => unreachable, _ => rb.* = @enumFromInt(@intFromEnum(rb.*) - 1), } } pub fn remaining(rb: RedirectBehavior) u16 { assert(rb != .unhandled); return @intFromEnum(rb); } }; /// Returns the request's `Connection` back to the pool of the `Client`. pub fn deinit(r: *Request) void { if (r.connection) |connection| { connection.closing = connection.closing or switch (r.reader.state) { .ready => false, .received_head => c: { if (r.method.requestHasBody()) break :c true; if (!r.method.responseHasBody()) break :c false; const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => break :c true, }; break :c r.reader.state != .ready; }, else => true, }; r.client.connection_pool.release(connection); } r.* = undefined; } /// Sends and flushes a complete request as only HTTP head, no body. pub fn sendBodiless(r: *Request) Writer.Error!void { try sendBodilessUnflushed(r); try r.connection.?.flush(); } /// Sends but does not flush a complete request as only HTTP head, no body. pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { assert(r.transfer_encoding == .none); assert(!r.method.requestHasBody()); try sendHead(r); } /// Transfers the HTTP head over the connection and flushes. /// /// See also: /// * `sendBodyUnflushed` pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { const result = try sendBodyUnflushed(r, buffer); try r.connection.?.flush(); return result; } /// Transfers the HTTP head and body over the connection and flushes. pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void { r.transfer_encoding = .{ .content_length = body.len }; var bw = try sendBodyUnflushed(r, body); bw.writer.end = body.len; try bw.end(); try r.connection.?.flush(); } /// Transfers the HTTP head over the connection, which is not flushed until /// `BodyWriter.flush` or `BodyWriter.end` is called. /// /// See also: /// * `sendBody` pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { assert(r.method.requestHasBody()); try sendHead(r); const http_protocol_output = r.connection.?.writer(); return switch (r.transfer_encoding) { .chunked => .{ .http_protocol_output = http_protocol_output, .state = .init_chunked, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.chunkedDrain, .sendFile = http.BodyWriter.chunkedSendFile, }, }, }, .content_length => |len| .{ .http_protocol_output = http_protocol_output, .state = .{ .content_length = len }, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.contentLengthDrain, .sendFile = http.BodyWriter.contentLengthSendFile, }, }, }, .none => .{ .http_protocol_output = http_protocol_output, .state = .none, .writer = .{ .buffer = buffer, .vtable = &.{ .drain = http.BodyWriter.noneDrain, .sendFile = http.BodyWriter.noneSendFile, }, }, }, }; } /// Sends HTTP headers without flushing. fn sendHead(r: *Request) Writer.Error!void { const uri = r.uri; const connection = r.connection.?; const w = connection.writer(); try w.writeAll(@tagName(r.method)); try w.writeByte(' '); if (r.method == .CONNECT) { try uri.writeToStream(w, .{ .authority = true }); } else { try uri.writeToStream(w, .{ .scheme = connection.proxied, .authentication = connection.proxied, .authority = connection.proxied, .path = true, .query = true, }); } try w.writeByte(' '); try w.writeAll(@tagName(r.version)); try w.writeAll("\r\n"); if (try emitOverridableHeader("host: ", r.headers.host, w)) { try w.writeAll("host: "); try uri.writeToStream(w, .{ .authority = true }); try w.writeAll("\r\n"); } if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { if (uri.user != null or uri.password != null) { try w.writeAll("authorization: "); try basic_authorization.write(uri, w); try w.writeAll("\r\n"); } } if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { try w.writeAll("user-agent: zig/"); try w.writeAll(builtin.zig_version_string); try w.writeAll(" (std.http)\r\n"); } if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { if (r.keep_alive) { try w.writeAll("connection: keep-alive\r\n"); } else { try w.writeAll("connection: close\r\n"); } } if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { try w.writeAll("accept-encoding: "); for (r.accept_encoding, 0..) |enabled, i| { if (!enabled) continue; const tag: http.ContentEncoding = @enumFromInt(i); if (tag == .identity) continue; const tag_name = @tagName(tag); try w.ensureUnusedCapacity(tag_name.len + 2); try w.writeAll(tag_name); try w.writeAll(", "); } w.undo(2); try w.writeAll("\r\n"); } switch (r.transfer_encoding) { .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), .none => {}, } if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { // The default is to omit content-type if not provided because // "application/octet-stream" is redundant. } for (r.extra_headers) |header| { assert(header.name.len != 0); try w.writeAll(header.name); try w.writeAll(": "); try w.writeAll(header.value); try w.writeAll("\r\n"); } if (connection.proxied) proxy: { const proxy = switch (connection.protocol) { .plain => r.client.http_proxy, .tls => r.client.https_proxy, } orelse break :proxy; const authorization = proxy.authorization orelse break :proxy; try w.writeAll("proxy-authorization: "); try w.writeAll(authorization); try w.writeAll("\r\n"); } try w.writeAll("\r\n"); } pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{ /// Server sent headers that did not conform to the HTTP protocol. /// /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be /// passed directly to `Request.Head.parse`. HttpHeadersInvalid, TooManyHttpRedirects, /// This can be avoided by calling `receiveHead` before sending the /// request body. RedirectRequiresResend, HttpRedirectLocationMissing, HttpRedirectLocationOversize, HttpRedirectLocationInvalid, HttpContentEncodingUnsupported, HttpChunkInvalid, HttpChunkTruncated, HttpHeadersOversize, UnsupportedUriScheme, /// Sending the request failed. Error code can be found on the /// `Connection` object. WriteFailed, }; /// If handling redirects and the request has no payload, then this /// function will automatically follow redirects. /// /// If a request payload is present, then this function will error with /// `error.RedirectRequiresResend`. /// /// This function takes an auxiliary buffer to store the arbitrarily large /// URI which may need to be merged with the previous URI, and that data /// needs to survive across different connections, which is where the input /// buffer lives. /// /// `redirect_buffer` must outlive accesses to `Request.uri`. If this /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` /// is returned instead. This buffer may be empty if no redirects are to be /// handled. /// /// If this fails with `error.ReadFailed` then the `Connection.getReadError` /// method of `r.connection` can be used to get more detailed information. pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { var aux_buf = redirect_buffer; while (true) { const head_buffer = try r.reader.receiveHead(); const response: Response = .{ .request = r, .head = Response.Head.parse(head_buffer) catch return error.HttpHeadersInvalid, }; const head = &response.head; if (head.status == .@"continue") { if (r.handle_continue) continue; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; // we're not handling the 100-continue } // This while loop is for handling redirects, which means the request's // connection may be different than the previous iteration. However, it // is still guaranteed to be non-null with each iteration of this loop. const connection = r.connection.?; if (r.method == .CONNECT and head.status.class() == .success) { // This connection is no longer doing HTTP. connection.closing = false; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } connection.closing = !head.keep_alive or !r.keep_alive; // Any response to a HEAD request and any response with a 1xx // (Informational), 204 (No Content), or 304 (Not Modified) status // code is always terminated by the first empty line after the // header fields, regardless of the header fields present in the // message. if (r.method == .HEAD or head.status.class() == .informational or head.status == .no_content or head.status == .not_modified) { r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { if (r.redirect_behavior == .not_allowed) { // Connection can still be reused by skipping the body. const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => connection.closing = true, }; return error.TooManyHttpRedirects; } try r.redirect(head, &aux_buf); try r.sendBodiless(); continue; } if (!r.accept_encoding[@intFromEnum(head.content_encoding)]) return error.HttpContentEncodingUnsupported; r.response_transfer_encoding = head.transfer_encoding; r.response_content_length = head.content_length; return response; } } /// This function takes an auxiliary buffer to store the arbitrarily large /// URI which may need to be merged with the previous URI, and that data /// needs to survive across different connections, which is where the input /// buffer lives. /// /// `aux_buf` must outlive accesses to `Request.uri`. fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { const new_location = head.location orelse return error.HttpRedirectLocationMissing; if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; const location = aux_buf.*[0..new_location.len]; @memcpy(location, new_location); { // Skip the body of the redirect response to leave the connection in // the correct state. This causes `new_location` to be invalidated. const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => return r.reader.body_err.?, }; } const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, error.InvalidFormat => return error.HttpRedirectLocationInvalid, error.InvalidPort => return error.HttpRedirectLocationInvalid, error.NoSpaceLeft => return error.HttpRedirectLocationOversize, }; const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; const old_connection = r.connection.?; const old_host = old_connection.host(); var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; const new_host = try new_uri.getHost(&new_host_name_buffer); const keep_privileged_headers = std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and sameParentDomain(old_host, new_host); r.client.connection_pool.release(old_connection); r.connection = null; if (!keep_privileged_headers) { // When redirecting to a different domain, strip privileged headers. r.privileged_headers = &.{}; } if (switch (head.status) { .see_other => true, .moved_permanently, .found => r.method == .POST, else => false, }) { // A redirect to a GET must change the method and remove the body. r.method = .GET; r.transfer_encoding = .none; r.headers.content_type = .omit; } if (r.transfer_encoding != .none) { // The request body has already been sent. The request is // still in a valid state, but the redirect must be handled // manually. return error.RedirectRequiresResend; } const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); r.uri = new_uri; r.connection = new_connection; r.reader = .{ .in = new_connection.reader(), .state = .ready, // Populated when `http.Reader.bodyReader` is called. .interface = undefined, .max_head_len = r.client.read_buffer_size, }; r.redirect_behavior.subtractOne(); } /// Returns true if the default behavior is required, otherwise handles /// writing (or not writing) the header. fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool { switch (v) { .default => return true, .omit => return false, .override => |x| { var vecs: [3][]const u8 = .{ prefix, x, "\r\n" }; try bw.writeVecAll(&vecs); return false; }, } } }; pub const Proxy = struct { protocol: Protocol, host: []const u8, authorization: ?[]const u8, port: u16, supports_connect: bool, }; /// Release all associated resources with the client. /// /// All pending requests must be de-initialized and all active connections released /// before calling this function. pub fn deinit(client: *Client) void { assert(client.connection_pool.used.first == null); // There are still active requests. client.connection_pool.deinit(); if (!disable_tls) client.ca_bundle.deinit(client.allocator); client.* = undefined; } /// Populates `http_proxy` and `https_proxy` via standard proxy environment variables. /// Asserts the client has no active connections. /// Uses `arena` for a few small allocations that must outlive the client, or /// at least until those fields are set to different values. pub fn initDefaultProxies(client: *Client, arena: Allocator) !void { // Prevent any new connections from being created. client.connection_pool.mutex.lock(); defer client.connection_pool.mutex.unlock(); assert(client.connection_pool.used.first == null); // There are active requests. if (client.http_proxy == null) { client.http_proxy = try createProxyFromEnvVar(arena, &.{ "http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY", }); } if (client.https_proxy == null) { client.https_proxy = try createProxyFromEnvVar(arena, &.{ "https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY", }); } } fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?*Proxy { const content = for (env_var_names) |name| { const content = std.process.getEnvVarOwned(arena, name) catch |err| switch (err) { error.EnvironmentVariableNotFound => continue, else => |e| return e, }; if (content.len == 0) continue; break content; } else return null; const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); const protocol = Protocol.fromUri(uri) orelse return null; const raw_host = try uri.getHostAlloc(arena); const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); assert(basic_authorization.value(uri, authorization).len == authorization.len); break :a authorization; } else null; const proxy = try arena.create(Proxy); proxy.* = .{ .protocol = protocol, .host = raw_host, .authorization = authorization, .port = uriPort(uri, protocol), .supports_connect = true, }; return proxy; } pub const basic_authorization = struct { pub const max_user_len = 255; pub const max_password_len = 255; pub const max_value_len = valueLength(max_user_len, max_password_len); pub fn valueLength(user_len: usize, password_len: usize) usize { return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); } pub fn valueLengthFromUri(uri: Uri) usize { const user: Uri.Component = uri.user orelse .empty; const password: Uri.Component = uri.password orelse .empty; var dw: Writer.Discarding = .init(&.{}); user.formatUser(&dw.writer) catch unreachable; // discarding const user_len = dw.count + dw.writer.end; dw.count = 0; dw.writer.end = 0; password.formatPassword(&dw.writer) catch unreachable; // discarding const password_len = dw.count + dw.writer.end; return valueLength(@intCast(user_len), @intCast(password_len)); } pub fn value(uri: Uri, out: []u8) []u8 { var bw: Writer = .fixed(out); write(uri, &bw) catch unreachable; return bw.buffered(); } pub fn write(uri: Uri, out: *Writer) Writer.Error!void { var buf: [max_user_len + 1 + max_password_len]u8 = undefined; var w: Writer = .fixed(&buf); const user: Uri.Component = uri.user orelse .empty; const password: Uri.Component = uri.password orelse .empty; user.formatUser(&w) catch unreachable; w.writeByte(':') catch unreachable; password.formatPassword(&w) catch unreachable; try out.print("Basic {b64}", .{w.buffered()}); } }; pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed, }; /// Reuses a `Connection` if one matching `host` and `port` is already open. /// /// Threadsafe. pub fn connectTcp( client: *Client, host: []const u8, port: u16, protocol: Protocol, ) ConnectTcpError!*Connection { return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol }); } pub const ConnectTcpOptions = struct { host: []const u8, port: u16, protocol: Protocol, proxied_host: ?[]const u8 = null, proxied_port: ?u16 = null, }; pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { const host = options.host; const port = options.port; const protocol = options.protocol; const proxied_host = options.proxied_host orelse host; const proxied_port = options.proxied_port orelse port; if (client.connection_pool.findConnection(.{ .host = proxied_host, .port = proxied_port, .protocol = protocol, })) |conn| return conn; const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { error.ConnectionRefused => return error.ConnectionRefused, error.NetworkUnreachable => return error.NetworkUnreachable, error.ConnectionTimedOut => return error.ConnectionTimedOut, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure, error.NameServerFailure => return error.NameServerFailure, error.UnknownHostName => return error.UnknownHostName, error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, else => return error.UnexpectedConnectFailure, }; errdefer stream.close(); switch (protocol) { .tls => { if (disable_tls) return error.TlsInitializationFailed; const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); client.connection_pool.addUsed(&tc.connection); return &tc.connection; }, .plain => { const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream); client.connection_pool.addUsed(&pc.connection); return &pc.connection; }, } } pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError; /// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. /// /// This function is threadsafe. pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { if (client.connection_pool.findConnection(.{ .host = path, .port = 0, .protocol = .plain, })) |node| return node; const conn = try client.allocator.create(ConnectionPool.Node); errdefer client.allocator.destroy(conn); conn.* = .{ .data = undefined }; const stream = try std.net.connectUnixSocket(path); errdefer stream.close(); conn.data = .{ .stream = stream, .tls_client = undefined, .protocol = .plain, .host = try client.allocator.dupe(u8, path), .port = 0, }; errdefer client.allocator.free(conn.data.host); client.connection_pool.addUsed(conn); return &conn.data; } /// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP /// CONNECT. This will reuse a connection if one is already open. /// /// This function is threadsafe. pub fn connectProxied( client: *Client, proxy: *Proxy, proxied_host: []const u8, proxied_port: u16, ) !*Connection { if (!proxy.supports_connect) return error.TunnelNotSupported; if (client.connection_pool.findConnection(.{ .host = proxied_host, .port = proxied_port, .protocol = proxy.protocol, })) |node| return node; var maybe_valid = false; (tunnel: { const connection = try client.connectTcpOptions(.{ .host = proxy.host, .port = proxy.port, .protocol = proxy.protocol, .proxied_host = proxied_host, .proxied_port = proxied_port, }); errdefer { connection.closing = true; client.connection_pool.release(connection); } var req = client.request(.CONNECT, .{ .scheme = "http", .host = .{ .raw = proxied_host }, .port = proxied_port, }, .{ .redirect_behavior = .unhandled, .connection = connection, }) catch |err| { break :tunnel err; }; defer req.deinit(); req.sendBodiless() catch |err| break :tunnel err; const response = req.receiveHead(&.{}) catch |err| break :tunnel err; if (response.head.status.class() == .server_error) { maybe_valid = true; break :tunnel error.ServerError; } if (response.head.status != .ok) break :tunnel error.ConnectionRefused; // this connection is now a tunnel, so we can't use it for anything // else, it will only be released when the client is de-initialized. req.connection = null; connection.closing = false; return connection; }) catch { // something went wrong with the tunnel proxy.supports_connect = maybe_valid; return error.TunnelNotSupported; }; } pub const ConnectError = ConnectTcpError || RequestError; /// Connect to `host:port` using the specified protocol. This will reuse a /// connection if one is already open. /// /// If a proxy is configured for the client, then the proxy will be used to /// connect to the host. /// /// This function is threadsafe. pub fn connect( client: *Client, host: []const u8, port: u16, protocol: Protocol, ) ConnectError!*Connection { const proxy = switch (protocol) { .plain => client.http_proxy, .tls => client.https_proxy, } orelse return client.connectTcp(host, port, protocol); // Prevent proxying through itself. if (std.ascii.eqlIgnoreCase(proxy.host, host) and proxy.port == port and proxy.protocol == protocol) { return client.connectTcp(host, port, protocol); } if (proxy.supports_connect) tunnel: { return connectProxied(client, proxy, host, port) catch |err| switch (err) { error.TunnelNotSupported => break :tunnel, else => |e| return e, }; } // fall back to using the proxy as a normal http proxy const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); connection.proxied = true; return connection; } pub const RequestError = ConnectTcpError || error{ UnsupportedUriScheme, UriMissingHost, UriHostTooLong, CertificateBundleLoadFailure, }; pub const RequestOptions = struct { version: http.Version = .@"HTTP/1.1", /// Automatically ignore 100 Continue responses. This assumes you don't /// care, and will have sent the body before you wait for the response. /// /// If this is not the case AND you know the server will send a 100 /// Continue, set this to false and wait for a response before sending the /// body. If you wait AND the server does not send a 100 Continue before /// you finish the request, then the request *will* deadlock. handle_continue: bool = true, /// If false, close the connection after the one request. If true, /// participate in the client connection pool. keep_alive: bool = true, /// This field specifies whether to automatically follow redirects, and if /// so, how many redirects to follow before returning an error. /// /// This will only follow redirects for repeatable requests (ie. with no /// payload or the server has acknowledged the payload). redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), /// Must be an already acquired connection. connection: ?*Connection = null, /// Standard headers that have default, but overridable, behavior. headers: Request.Headers = .{}, /// These headers are kept including when following a redirect to a /// different domain. /// Externally-owned; must outlive the Request. extra_headers: []const http.Header = &.{}, /// These headers are stripped when following a redirect to a different /// domain. /// Externally-owned; must outlive the Request. privileged_headers: []const http.Header = &.{}, }; fn uriPort(uri: Uri, protocol: Protocol) u16 { return uri.port orelse protocol.port(); } /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. /// /// The caller is responsible for calling `deinit()` on the `Request`. /// This function is threadsafe. /// /// Asserts that "\r\n" does not occur in any header name or value. pub fn request( client: *Client, method: http.Method, uri: Uri, options: RequestOptions, ) RequestError!Request { if (std.debug.runtime_safety) { for (options.extra_headers) |header| { assert(header.name.len != 0); assert(std.mem.indexOfScalar(u8, header.name, ':') == null); assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null); assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); } for (options.privileged_headers) |header| { assert(header.name.len != 0); assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null); assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); } } const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; if (protocol == .tls) { if (disable_tls) unreachable; if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { client.ca_bundle_mutex.lock(); defer client.ca_bundle_mutex.unlock(); if (client.next_https_rescan_certs) { client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure; @atomicStore(bool, &client.next_https_rescan_certs, false, .release); } } } const connection = options.connection orelse c: { var host_name_buffer: [Uri.host_name_max]u8 = undefined; const host_name = try uri.getHost(&host_name_buffer); break :c try client.connect(host_name, uriPort(uri, protocol), protocol); }; return .{ .uri = uri, .client = client, .connection = connection, .reader = .{ .in = connection.reader(), .state = .ready, // Populated when `http.Reader.bodyReader` is called. .interface = undefined, .max_head_len = client.read_buffer_size, }, .keep_alive = options.keep_alive, .method = method, .version = options.version, .transfer_encoding = .none, .redirect_behavior = options.redirect_behavior, .handle_continue = options.handle_continue, .headers = options.headers, .extra_headers = options.extra_headers, .privileged_headers = options.privileged_headers, }; } pub const FetchOptions = struct { /// `null` means it will be heap-allocated. redirect_buffer: ?[]u8 = null, /// `null` means it will be heap-allocated. decompress_buffer: ?[]u8 = null, redirect_behavior: ?Request.RedirectBehavior = null, /// If the server sends a body, it will be written here. response_writer: ?*Writer = null, location: Location, method: ?http.Method = null, payload: ?[]const u8 = null, raw_uri: bool = false, keep_alive: bool = true, /// Standard headers that have default, but overridable, behavior. headers: Request.Headers = .{}, /// These headers are kept including when following a redirect to a /// different domain. /// Externally-owned; must outlive the Request. extra_headers: []const http.Header = &.{}, /// These headers are stripped when following a redirect to a different /// domain. /// Externally-owned; must outlive the Request. privileged_headers: []const http.Header = &.{}, pub const Location = union(enum) { url: []const u8, uri: Uri, }; }; pub const FetchResult = struct { status: http.Status, }; pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{ StreamTooLong, /// TODO provide optional diagnostics when this occurs or break into more error codes WriteFailed, UnsupportedCompressionMethod, }; /// Perform a one-shot HTTP request with the provided options. /// /// This function is threadsafe. pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { const uri = switch (options.location) { .url => |u| try Uri.parse(u), .uri => |u| u, }; const method: http.Method = options.method orelse if (options.payload != null) .POST else .GET; const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse if (options.payload == null) @enumFromInt(3) else .unhandled; var req = try request(client, method, uri, .{ .redirect_behavior = redirect_behavior, .headers = options.headers, .extra_headers = options.extra_headers, .privileged_headers = options.privileged_headers, .keep_alive = options.keep_alive, }); defer req.deinit(); if (options.payload) |payload| { req.transfer_encoding = .{ .content_length = payload.len }; var body = try req.sendBodyUnflushed(&.{}); try body.writer.writeAll(payload); try body.end(); try req.connection.?.flush(); } else { try req.sendBodiless(); } const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse try client.allocator.alloc(u8, 8 * 1024); defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer); var response = try req.receiveHead(redirect_buffer); const response_writer = options.response_writer orelse { const reader = response.reader(&.{}); _ = reader.discardRemaining() catch |err| switch (err) { error.ReadFailed => return response.bodyErr().?, }; return .{ .status = response.head.status }; }; const decompress_buffer: []u8 = switch (response.head.content_encoding) { .identity => &.{}, .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len), .deflate, .gzip => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.flate.max_window_len), .compress => return error.UnsupportedCompressionMethod, }; defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer); var transfer_buffer: [64]u8 = undefined; var decompress: http.Decompress = undefined; const reader = response.readerDecompressing(&transfer_buffer, &decompress, decompress_buffer); _ = reader.streamRemaining(response_writer) catch |err| switch (err) { error.ReadFailed => return response.bodyErr().?, else => |e| return e, }; return .{ .status = response.head.status }; } pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; if (child_host.len == parent_host.len) return true; if (parent_host.len > child_host.len) return false; return child_host[child_host.len - parent_host.len - 1] == '.'; } test sameParentDomain { try testing.expect(!sameParentDomain("foo.com", "bar.com")); try testing.expect(sameParentDomain("foo.com", "foo.com")); try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); } test { _ = Response; }