Skip to documentation
SLOP

tiny.http.controlled

Reference 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.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.httpcontrolled
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callersprivate sourcelib.http.src.client.controlledsafeComponentprivate sourcelib.http.src.client.controlledvalidateprivate sourcelib.http.src.client.responsereadcontrolledOwner
Static calls · unresolved targets: 2 · external targets: 39.

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

Definitions6
Public names6
Members18
Version26.7.0
Revisiondaab053ee433