tiny.http.controlled
Defined in tiny.http.
Provides a fixed-storage HTTP client for callers that choose storage bounds before running a request.
API (5)
Actions
Public operations.
Owner: Builds the owner type for the supplied compile-time storage limits, rejecting invalid combinations at compilation.
Types and contracts
Public types and contracts.
LimitsRequestRouteTrust: Provides the caller's choice between an unencrypted plain connection and a connection verified against a caller-owned certificate authority bundle.
Source
Source: lib/http/src/client/controlled.zig
zig
//! Provides a fixed-storage HTTP client for callers that choose storage bounds//! before running a request. One request is prepared and run once. All request,//! response, and connection storage resides in a single fixed-size struct kept//! at the address where successful initialization placed it until//! deinitialization. This is the *owner*. Caller-owned trust storage, the//! worker stack, and kernel socket storage remain outside the owner.//!//! A single worker runs the request from start to finish, and calls block that//! worker throughout execution.//!//! A separate handle carries a deadline and a cancellation flag that another//! thread may set while a request runs. This handle is the *control*. The//! running worker observes cancellation requests at control checks. Reads and//! writes check the control. Blocking socket waits request poll intervals of at//! most 10 milliseconds, shortened to the rounded-up remaining deadline//! interval, and check cancellation and the deadline before and after polling.//! The handle also carries a byte budget that bounds transfers. Each read and//! write restricts transfer length to the remaining budget and subtracts the//! bytes actually moved. Once the remaining budget reaches zero, the next read//! or write returns `error.TransferCapacityExceeded`.const std = @import("std");const sys = @import("sys");const client = @import("root.zig");const alloc_phase = @import("alloc_phase");pub const Limits = struct { host_bytes: u16 = 253, target_bytes: u16 = 1024, request_bytes: u32 = 8192, request_headers: u8 = 32, response_headers: u8 = 32, response_head_bytes: u16 = 4096, /// The bytes reserved for the response body. A chunked body arrives with /// the size prefixes and terminators of a chunked transfer encoding, known /// as chunk framing. Chunk framing sits in this region until the body is /// decoded over it in place, so the region holds the encoded form before it /// holds the decoded one. response_body_bytes: u32 = 4096, trust_bytes: u32 = 1024 * 1024,};/// Provides the caller's choice between an unencrypted plain connection and a/// connection verified against a caller-owned certificate authority bundle./// When verified transport is selected, the TLS handshake checks the peer/// certificate chain against this bundle.////// The bundle, its bytes, and its index stay as they are from `init` until/// `run` returns, and the allocator and `std.Io` handle stay borrowed for the/// same span. Initialization records a SHA-256 digest over the bundle bytes and/// index. Before network work begins, `run` recomputes this digest and returns/// `error.TrustMismatch` if the values differ. The bundle byte length and index/// capacity must each fit within `trust_bytes`, or fingerprinting returns/// `error.Capacity`.////// The choice and the URL scheme have to agree. A plain choice with an HTTPS/// URL, or a verified choice with an HTTP URL or an empty bundle, returns/// `error.TrustMismatch`.////// `init` returns `error.UnsupportedPlatform` on platforms with/// operating-system certificate-chain building or without supported control/// I/O.pub const Trust = union(enum) { plain, verified: struct { bundle: *sys.tls.Bundle, allocator: std.mem.Allocator, io: std.Io },};pub const Route = union(enum) { address: sys.net.IpAddress, dns: struct { nameserver: sys.net.IpAddress, family: sys.net.Control.DnsFamily },};pub const Request = struct { method: []const u8, url: []const u8, headers: []const client.Header, body: []const u8, route: Route, trust: *const Trust,};/// Builds the owner type for the supplied compile-time storage limits,/// rejecting invalid combinations at compilation.////// Initialization parses the URL, validates the request, and writes the request/// bytes into the owner without opening a connection. The socket is opened/// inside `run`, which executes on the caller's worker. The owner records its/// memory address upon successful initialization and verifies it during `run`/// and `deinit`. The caller must keep the owner pinned at that address until/// `deinit`, and leave it unchanged between `init` and `run`.////// During `run`, the worker changes owner state and buffers, retaining/// exclusive access to the owner and socket. Cancellation touches only the/// separate control handle. If `run` was started, it must return before/// `deinit`. An initialized request may also be deinitialized without ever/// running.pub fn Owner(comptime limits: Limits) type { if (limits.host_bytes == 0 or limits.host_bytes > 253 or limits.target_bytes == 0 or limits.target_bytes > 16384 or limits.request_bytes == 0 or limits.request_bytes > 1024 * 1024 or limits.response_head_bytes == 0 or limits.response_body_bytes > 1024 * 1024 or limits.response_headers == 0 or limits.trust_bytes > 16 * 1024 * 1024) @compileError("invalid controlled HTTP limits"); return struct { origin: ?*const @This() = null, state: enum { ready, running, complete } = .ready, trust: Trust, route: Route, prepared: client.transport.Prepared, method: [16]u8 = undefined, method_length: u8, host: [limits.host_bytes]u8 = undefined, target: [limits.target_bytes]u8 = undefined, request: [limits.request_bytes]u8 = undefined, request_length: u32 = 0, response_headers: [limits.response_headers]client.Header = undefined, response_head: [limits.response_head_bytes]u8 = undefined, response_body: [limits.response_body_bytes]u8 = undefined, connection: client.transport.TlsConn = undefined, lock: sys.tls.BundleLock = .init, trust_digest: [32]u8 = @splat(0), failure: ?anyerror = null, const Self = @This(); pub const Limits = @TypeOf(limits); pub const selected_limits = limits; pub const storage_bytes = @sizeOf(Self); pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "http.controlled.request", .kind = .startup_static, .limit_source = .caller, .storage = .{ .covered = &.{.{ .id = "owned", .lifetime = .steady, .detail = "fixed request, response, TLS and operation metadata" }}, .excluded = &.{"caller immutable trust bundle and allocator, caller worker stack and kernel socket storage"}, }, .capacity = .{ .inputs = &.{}, .type_selectors = &.{alloc_phase.capacity.bindType(Self, "owned")}, .nodes = &.{ .{ .constant = 1 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } } }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 1 }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "request refuses before network effects; response and wire bounds refuse incomplete output" }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "caller frozen trust bundle and worker stack" }, .foreign = .{ .status = .excluded, .detail = "kernel sockets and pinned standard-library cryptography" }, }, .work = .{ .equation = "request and trust bounds plus quadratic encoded-body scanning; fixed TLS buffers and Control wire budget; waits check the absolute deadline every 10 ms" }, .obligations = &.{ .{ .key = "http_controlled_capacity", .role = .capacity_model }, .{ .key = "http_controlled_overload", .role = .overload }, .{ .key = "http_controlled_work", .role = .work_bound }, }, }, .bindings = .{ .owner = Self, .seal = .{ .family = alloc_phase.capacity.selector(Self.init), .premise = .{ .class = .checked_semantic_fact, .authority = .checker } }, .teardown = .{ .family = alloc_phase.capacity.selector(Self.deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker } }, }, }; comptime { if (alloc_phase.capacity.validateDeclaration(Self, claim, .provisioned_exact)) |bad| { @compileError("invalid controlled request claim: " ++ @tagName(bad)); } } pub fn init(self: *Self, request: Request) !void { if (comptime sys.tls.Certificate.Chain != void or !sys.net.Control.supported()) return error.UnsupportedPlatform; try validate(request, limits); self.* = .{ .trust = request.trust.*, .route = request.route, .prepared = undefined, .method_length = @intCast(request.method.len) }; errdefer self.scrub(); @memcpy(self.method[0..request.method.len], request.method); self.prepared = try client.ClientOperation.prepare(.{ .host = &self.host, .target = &self.target, .plain_read = &.{}, .plain_write = &.{} }, request.url); if (std.mem.startsWith(u8, self.prepared.host, "[")) { if (!std.mem.endsWith(u8, self.prepared.host, "]")) return error.InvalidRequest; self.prepared.host = self.prepared.host[1 .. self.prepared.host.len - 1]; _ = sys.net.Address.parseIp(self.prepared.host, self.prepared.port) catch return error.InvalidRequest; } else { for (self.prepared.host) |byte| { if (!std.ascii.isAlphanumeric(byte) and byte != '-' and byte != '.') return error.InvalidRequest; } } const uri = std.Uri.parse(request.url) catch unreachable; var target = std.Io.Writer.fixed(&self.target); if (uri.path.isEmpty()) try target.writeByte('/') else uri.path.formatPath(&target) catch return error.ClientTargetCapacityExceeded; if (uri.query) |query| { target.writeByte('?') catch return error.ClientTargetCapacityExceeded; query.formatQuery(&target) catch return error.ClientTargetCapacityExceeded; } self.prepared.target = target.buffered(); if (!safeComponent(self.prepared.host) or !safeComponent(self.prepared.target)) return error.InvalidRequest; switch (request.trust.*) { .plain => if (self.prepared.is_tls) { return error.TrustMismatch; }, .verified => |trust| { if (!self.prepared.is_tls or trust.bundle.bytes.items.len == 0 or trust.bundle.map.count() == 0) return error.TrustMismatch; self.trust_digest = try self.trustFingerprint(); }, } if (request.route == .address and sys.net.ipAddressPort(request.route.address) != self.prepared.port) return error.RouteMismatch; var authority_buffer: [limits.host_bytes + 8]u8 = undefined; const host = self.prepared.host; const is_ip6 = std.mem.indexOfScalar(u8, host, ':') != null; const default_port: u16 = if (self.prepared.is_tls) 443 else 80; var authority = std.Io.Writer.fixed(&authority_buffer); if (is_ip6) try authority.writeByte('['); try authority.writeAll(host); if (is_ip6) try authority.writeByte(']'); if (self.prepared.port != default_port) try authority.print(":{d}", .{self.prepared.port}); var writer = std.Io.Writer.fixed(&self.request); client.ClientOperation.writeRequest(&writer, request.method, authority.buffered(), self.prepared.target, request.headers, request.body) catch return error.Capacity; self.request_length = @intCast(writer.end); self.origin = self; std.debug.assert(self.request_length <= self.request.len); } /// Runs the prepared request and returns a complete response. The /// operation performs no retries and no connection reuse. If the server /// responds with a redirect status, the function returns /// `error.RedirectRefused`. If the server responds with a protocol /// upgrade, it returns `error.UpgradeRefused`, leaving subsequent /// actions to the caller. /// /// `run` first checks the recorded owner address, returning /// `error.WrongOwner` on a mismatch, and then requires the ready state, /// returning `error.RequestConsumed` otherwise. Once these checks pass, /// the attempt consumes the operation whether network work succeeds or /// fails. A subsequent `run` on that initialized owner returns /// `error.RequestConsumed`. /// /// After address and state checks pass, request and method bytes are /// wiped when `run` returns, on success or failure. If request /// execution fails, the response head and body are wiped as well. Early /// returns from address or state check failures do not scrub buffers. A /// successful response borrows header entries, head bytes, and body /// bytes directly from the owner, so these regions remain valid until /// `deinit` and must be left unchanged by the caller while the response /// is in use. pub fn run(self: *Self, control: *sys.net.Control) !client.ClientResponse { if (self.origin != self) return error.WrongOwner; if (self.state != .ready) return error.RequestConsumed; self.state = .running; defer { self.state = .complete; std.crypto.secureZero(u8, &self.request); std.crypto.secureZero(u8, &self.method); } return self.perform(control) catch |err| { self.failure = control.failure orelse err; std.crypto.secureZero(u8, &self.response_head); std.crypto.secureZero(u8, &self.response_body); return self.failure.?; }; } fn perform(self: *Self, control: *sys.net.Control) !client.ClientResponse { try control.check(); if (self.trust == .verified) try self.checkTrust(); const address = switch (self.route) { .address => |value| value, .dns => |dns| try control.resolve(self.prepared.host, self.prepared.port, dns.nameserver, dns.family), }; const socket = try control.open(address); self.connection.initAt(socket); defer self.connection.deinit(false); self.connection.socket_reader.control = control; self.connection.socket_writer.control = control; try control.connectTo(socket, address); if (self.trust == .verified) try self.handshake(control); const writer = if (self.prepared.is_tls) &self.connection.tls_client.?.writer else &self.connection.socket_writer.writer; try writer.writeAll(self.request[0..self.request_length]); try writer.flush(); try self.connection.socket_writer.writer.flush(); const reader = if (self.prepared.is_tls) &self.connection.tls_client.?.reader else &self.connection.socket_reader.reader; var response = try client.response.read(.{ .headers = &self.response_headers, .head = &self.response_head, .body = &self.response_body }, reader, self.method[0..self.method_length]); while (response.status < 200) { try control.check(); if (response.status == 101) return error.UpgradeRefused; response = try client.response.read(.{ .headers = &self.response_headers, .head = &self.response_head, .body = &self.response_body }, reader, self.method[0..self.method_length]); } try control.check(); if (response.status >= 300 and response.status < 400) return error.RedirectRefused; return response; } fn trustFingerprint(self: *Self) ![32]u8 { const bundle = self.trust.verified.bundle; if (bundle.bytes.items.len > limits.trust_bytes or bundle.map.capacity() > limits.trust_bytes) return error.Capacity; var hash = std.crypto.hash.sha2.Sha256.init(.{}); hash.update("tiny.http.trust/v1"); var lengths: [16]u8 = undefined; std.mem.writeInt(u64, lengths[0..8], bundle.bytes.items.len, .little); std.mem.writeInt(u64, lengths[8..16], bundle.map.count(), .little); hash.update(&lengths); hash.update(bundle.bytes.items); var iterator = bundle.map.iterator(); while (iterator.next()) |entry| { if (entry.key_ptr.start > entry.key_ptr.end or entry.key_ptr.end > bundle.bytes.items.len or entry.value_ptr.* >= bundle.bytes.items.len) return error.TrustMismatch; var encoded: [12]u8 = undefined; std.mem.writeInt(u32, encoded[0..4], entry.key_ptr.start, .little); std.mem.writeInt(u32, encoded[4..8], entry.key_ptr.end, .little); std.mem.writeInt(u32, encoded[8..12], entry.value_ptr.*, .little); hash.update(&encoded); } return hash.finalResult(); } fn checkTrust(self: *Self) !void { const digest = try self.trustFingerprint(); if (!std.mem.eql(u8, &digest, &self.trust_digest)) return error.TrustMismatch; } fn handshake(self: *Self, control: *sys.net.Control) !void { const trust = self.trust.verified; var entropy: [sys.tls.Client.Options.entropy_len]u8 = undefined; defer std.crypto.secureZero(u8, &entropy); try control.entropy(&entropy); try control.check(); self.connection.tls_client = try sys.tls.Client.init(&self.connection.socket_reader.reader, &self.connection.socket_writer.writer, .{ .host = .{ .explicit = self.prepared.host }, .ca = .{ .bundle = .{ .bundle = trust.bundle, .lock = &self.lock, .gpa = trust.allocator, .io = trust.io } }, .read_buffer = &self.connection.tls_read_buf, .write_buffer = &self.connection.tls_write_buf, .entropy = &entropy, .realtime_now = sys.tls.realtimeNow(), .allow_truncation_attacks = false, }); try control.check(); } /// Wipes the request, method, head, body, and connection bytes, and /// releases the pinned owner. The function asserts that the owner /// remains at its original address and that no `run` call is active. A /// caller that requested cancellation must join the worker before /// `deinit`. This function also accepts an initialized request that was /// never run. pub fn deinit(self: *Self) void { std.debug.assert(self.origin == self); std.debug.assert(self.state != .running); self.scrub(); self.origin = null; } fn scrub(self: *Self) void { std.crypto.secureZero(u8, &self.request); std.crypto.secureZero(u8, &self.method); std.crypto.secureZero(u8, &self.response_head); std.crypto.secureZero(u8, &self.response_body); std.crypto.secureZero(u8, std.mem.asBytes(&self.connection)); } };}fn safeComponent(value: []const u8) bool { for (value) |byte| if (byte <= 32 or byte == 127) return false; return value.len != 0;}fn validate(request: Request, limits: Limits) !void { if (request.url.len > @as(usize, limits.host_bytes) + limits.target_bytes + 128 or request.headers.len > limits.request_headers or request.body.len > limits.request_bytes) return error.Capacity; if (request.method.len == 0 or request.method.len > 16) return error.InvalidRequest; for (request.method) |byte| if (byte < 'A' or byte > 'Z') return error.InvalidRequest; if (std.mem.eql(u8, request.method, "CONNECT")) return error.InvalidRequest; const uri = std.Uri.parse(request.url) catch return error.InvalidRequest; if (uri.user != null or uri.password != null or uri.fragment != null) return error.InvalidRequest; var count = request.body.len; for (request.headers) |header| { if (header.name.len > limits.request_bytes - count) return error.Capacity; count += header.name.len; if (header.value.len > limits.request_bytes - count) return error.Capacity; count += header.value.len; if (header.name.len == 0) return error.InvalidRequest; for (header.name) |byte| if (!std.ascii.isAlphanumeric(byte) and byte != '-') return error.InvalidRequest; for (header.value) |byte| if (byte < 32 or byte == 127) return error.InvalidRequest; inline for (.{ "host", "content-length", "transfer-encoding", "connection", "upgrade" }) |name| { if (std.ascii.eqlIgnoreCase(header.name, name)) return error.InvalidRequest; } }}Source: lib/http/src/root.zig:77
zig
pub const controlled = client.controlled;Audit
| Definitions | 6 |
|---|---|
| Public names | 6 |
| Members | 18 |
| Version | 26.7.0 |
| Revision | daab053ee433 |