Commit ba44513c2f

Andrew Kelley <andrew@ziglang.org>
2022-12-13 05:18:56
std.http reorg; introduce std.crypto.Tls
TLS is capable of sending a Client Hello
1 parent cd0d514
lib/std/crypto/Tls.zig
@@ -0,0 +1,342 @@
+const std = @import("../std.zig");
+const Tls = @This();
+const net = std.net;
+const mem = std.mem;
+const crypto = std.crypto;
+const assert = std.debug.assert;
+
+state: State = .start,
+x25519_priv_key: [32]u8 = undefined,
+x25519_pub_key: [32]u8 = undefined,
+
+const State = enum {
+    /// In this state, all fields are undefined except state.
+    start,
+    sent_hello,
+};
+
+const ContentType = enum(u8) {
+    invalid = 0,
+    change_cipher_spec = 20,
+    alert = 21,
+    handshake = 22,
+    application_data = 23,
+    _,
+};
+
+const HandshakeType = enum(u8) {
+    client_hello = 1,
+    server_hello = 2,
+    new_session_ticket = 4,
+    end_of_early_data = 5,
+    encrypted_extensions = 8,
+    certificate = 11,
+    certificate_request = 13,
+    certificate_verify = 15,
+    finished = 20,
+    key_update = 24,
+    message_hash = 254,
+};
+
+const ExtensionType = enum(u16) {
+    /// RFC 6066
+    server_name = 0,
+    /// RFC 6066
+    max_fragment_length = 1,
+    /// RFC 6066
+    status_request = 5,
+    /// RFC 8422, 7919
+    supported_groups = 10,
+    /// RFC 8446
+    signature_algorithms = 13,
+    /// RFC 5764
+    use_srtp = 14,
+    /// RFC 6520
+    heartbeat = 15,
+    /// RFC 7301
+    application_layer_protocol_negotiation = 16,
+    /// RFC 6962
+    signed_certificate_timestamp = 18,
+    /// RFC 7250
+    client_certificate_type = 19,
+    /// RFC 7250
+    server_certificate_type = 20,
+    /// RFC 7685
+    padding = 21,
+    /// RFC 8446
+    pre_shared_key = 41,
+    /// RFC 8446
+    early_data = 42,
+    /// RFC 8446
+    supported_versions = 43,
+    /// RFC 8446
+    cookie = 44,
+    /// RFC 8446
+    psk_key_exchange_modes = 45,
+    /// RFC 8446
+    certificate_authorities = 47,
+    /// RFC 8446
+    oid_filters = 48,
+    /// RFC 8446
+    post_handshake_auth = 49,
+    /// RFC 8446
+    signature_algorithms_cert = 50,
+    /// RFC 8446
+    key_share = 51,
+};
+
+const AlertLevel = enum(u8) {
+    warning = 1,
+    fatal = 2,
+    _,
+};
+
+const AlertDescription = enum(u8) {
+    close_notify = 0,
+    unexpected_message = 10,
+    bad_record_mac = 20,
+    record_overflow = 22,
+    handshake_failure = 40,
+    bad_certificate = 42,
+    unsupported_certificate = 43,
+    certificate_revoked = 44,
+    certificate_expired = 45,
+    certificate_unknown = 46,
+    illegal_parameter = 47,
+    unknown_ca = 48,
+    access_denied = 49,
+    decode_error = 50,
+    decrypt_error = 51,
+    protocol_version = 70,
+    insufficient_security = 71,
+    internal_error = 80,
+    inappropriate_fallback = 86,
+    user_canceled = 90,
+    missing_extension = 109,
+    unsupported_extension = 110,
+    unrecognized_name = 112,
+    bad_certificate_status_response = 113,
+    unknown_psk_identity = 115,
+    certificate_required = 116,
+    no_application_protocol = 120,
+    _,
+};
+
+const SignatureScheme = enum(u16) {
+    // RSASSA-PKCS1-v1_5 algorithms
+    rsa_pkcs1_sha256 = 0x0401,
+    rsa_pkcs1_sha384 = 0x0501,
+    rsa_pkcs1_sha512 = 0x0601,
+
+    // ECDSA algorithms
+    ecdsa_secp256r1_sha256 = 0x0403,
+    ecdsa_secp384r1_sha384 = 0x0503,
+    ecdsa_secp521r1_sha512 = 0x0603,
+
+    // RSASSA-PSS algorithms with public key OID rsaEncryption
+    rsa_pss_rsae_sha256 = 0x0804,
+    rsa_pss_rsae_sha384 = 0x0805,
+    rsa_pss_rsae_sha512 = 0x0806,
+
+    // EdDSA algorithms
+    ed25519 = 0x0807,
+    ed448 = 0x0808,
+
+    // RSASSA-PSS algorithms with public key OID RSASSA-PSS
+    rsa_pss_pss_sha256 = 0x0809,
+    rsa_pss_pss_sha384 = 0x080a,
+    rsa_pss_pss_sha512 = 0x080b,
+
+    // Legacy algorithms
+    rsa_pkcs1_sha1 = 0x0201,
+    ecdsa_sha1 = 0x0203,
+
+    _,
+};
+
+const NamedGroup = enum(u16) {
+    // Elliptic Curve Groups (ECDHE)
+    secp256r1 = 0x0017,
+    secp384r1 = 0x0018,
+    secp521r1 = 0x0019,
+    x25519 = 0x001D,
+    x448 = 0x001E,
+
+    // Finite Field Groups (DHE)
+    ffdhe2048 = 0x0100,
+    ffdhe3072 = 0x0101,
+    ffdhe4096 = 0x0102,
+    ffdhe6144 = 0x0103,
+    ffdhe8192 = 0x0104,
+
+    _,
+};
+
+// Plaintext:
+// * type: ContentType
+// * legacy_record_version: u16 = 0x0303,
+// * length: u16,
+//   - The length (in bytes) of the following TLSPlaintext.fragment.  The
+//     length MUST NOT exceed 2^14 bytes.
+// * fragment: opaque
+//   - the data being transmitted
+
+// Handshake:
+// * type: HandshakeType
+// * length: u24
+// * data: opaque
+
+const CipherSuite = enum(u16) {
+    TLS_AES_128_GCM_SHA256 = 0x1301,
+    TLS_AES_256_GCM_SHA384 = 0x1302,
+    TLS_CHACHA20_POLY1305_SHA256 = 0x1303,
+    TLS_AES_128_CCM_SHA256 = 0x1304,
+    TLS_AES_128_CCM_8_SHA256 = 0x1305,
+};
+
+const cipher_suites = blk: {
+    const fields = @typeInfo(CipherSuite).Enum.fields;
+    var result: [(fields.len + 1) * 2]u8 = undefined;
+    mem.writeIntBig(u16, result[0..2], result.len - 2);
+    for (fields) |field, i| {
+        const int = @enumToInt(@field(CipherSuite, field.name));
+        result[(i + 1) * 2] = @truncate(u8, int >> 8);
+        result[(i + 1) * 2 + 1] = @truncate(u8, int);
+    }
+    break :blk result;
+};
+
+pub fn init(tls: *Tls, stream: net.Stream, host: []const u8) !void {
+    assert(tls.state == .start);
+    crypto.random.bytes(&tls.x25519_priv_key);
+    tls.x25519_pub_key = try crypto.dh.X25519.recoverPublicKey(tls.x25519_priv_key);
+
+    // random (u32)
+    var rand_buf: [32]u8 = undefined;
+    crypto.random.bytes(&rand_buf);
+
+    const extensions_header = [_]u8{
+        // Extensions byte length
+        undefined, undefined,
+
+        // Extension: supported_versions (only TLS 1.3)
+        0, 43, // ExtensionType.supported_versions
+        0x00, 0x05, // byte length of this extension payload
+        0x04, // byte length of supported versions
+        0x03, 0x04, // TLS 1.3
+        0x03, 0x03, // TLS 1.2
+
+        // Extension: signature_algorithms
+        0, 13, // ExtensionType.signature_algorithms
+        0x00, 0x22, // byte length of this extension payload
+        0x00, 0x20, // byte length of signature algorithms list
+        0x04, 0x01, // rsa_pkcs1_sha256
+        0x05, 0x01, // rsa_pkcs1_sha384
+        0x06, 0x01, // rsa_pkcs1_sha512
+        0x04, 0x03, // ecdsa_secp256r1_sha256
+        0x05, 0x03, // ecdsa_secp384r1_sha384
+        0x06, 0x03, // ecdsa_secp521r1_sha512
+        0x08, 0x04, // rsa_pss_rsae_sha256
+        0x08, 0x05, // rsa_pss_rsae_sha384
+        0x08, 0x06, // rsa_pss_rsae_sha512
+        0x08, 0x07, // ed25519
+        0x08, 0x08, // ed448
+        0x08, 0x09, // rsa_pss_pss_sha256
+        0x08, 0x0a, // rsa_pss_pss_sha384
+        0x08, 0x0b, // rsa_pss_pss_sha512
+        0x02, 0x01, // rsa_pkcs1_sha1
+        0x02, 0x03, // ecdsa_sha1
+
+        // Extension: supported_groups
+        0, 10, // ExtensionType.supported_groups
+        0x00, 0x0c, // byte length of this extension payload
+        0x00, 0x0a, // byte length of supported groups list
+        0x00, 0x17, // secp256r1
+        0x00, 0x18, // secp384r1
+        0x00, 0x19, // secp521r1
+        0x00, 0x1D, // x25519
+        0x00, 0x1E, // x448
+
+        // Extension: key_share
+        0, 51, // ExtensionType.key_share
+        0x00, 38, // byte length of this extension payload
+        0x00, 36, // byte length of client_shares
+        0x00, 0x1D, // NamedGroup.x25519
+        0x00, 32, // byte length of key_exchange
+    } ++ tls.x25519_pub_key ++ [_]u8{
+
+        // Extension: server_name
+        0, 0, // ExtensionType.server_name
+        undefined, undefined, // byte length of this extension payload
+        undefined, undefined, // server_name_list byte count
+        0x00, // name_type
+        undefined, undefined, // host name len
+    };
+
+    var hello_header = [_]u8{
+        // Plaintext header
+        @enumToInt(ContentType.handshake),
+        0x03, 0x01, // legacy_record_version
+        undefined,                              undefined, // Plaintext fragment length (u16)
+
+        // Handshake header
+        @enumToInt(HandshakeType.client_hello),
+        undefined, undefined, undefined, // handshake length (u24)
+
+        // ClientHello
+        0x03, 0x03, // legacy_version
+    } ++ rand_buf ++ [1]u8{0} ++ cipher_suites ++ [_]u8{
+        0x01, 0x00, // legacy_compression_methods
+    } ++ extensions_header;
+
+    mem.writeIntBig(u16, hello_header[3..][0..2], @intCast(u16, hello_header.len - 5 + host.len));
+    mem.writeIntBig(u24, hello_header[6..][0..3], @intCast(u24, hello_header.len - 9 + host.len));
+    mem.writeIntBig(
+        u16,
+        hello_header[hello_header.len - extensions_header.len ..][0..2],
+        @intCast(u16, extensions_header.len - 2 + host.len),
+    );
+    mem.writeIntBig(u16, hello_header[hello_header.len - 7 ..][0..2], @intCast(u16, 5 + host.len));
+    mem.writeIntBig(u16, hello_header[hello_header.len - 5 ..][0..2], @intCast(u16, 3 + host.len));
+    mem.writeIntBig(u16, hello_header[hello_header.len - 2 ..][0..2], @intCast(u16, 0 + host.len));
+
+    var iovecs = [_]std.os.iovec_const{
+        .{
+            .iov_base = &hello_header,
+            .iov_len = hello_header.len,
+        },
+        .{
+            .iov_base = host.ptr,
+            .iov_len = host.len,
+        },
+    };
+    try stream.writevAll(&iovecs);
+
+    {
+        var buf: [1000]u8 = undefined;
+        const amt = try stream.read(&buf);
+        const resp = buf[0..amt];
+        const ct = @intToEnum(ContentType, resp[0]);
+        if (ct == .alert) {
+            //const prot_ver = @bitCast(u16, resp[1..][0..2].*);
+            const len = std.mem.readIntBig(u16, resp[3..][0..2]);
+            const alert = resp[5..][0..len];
+            const level = @intToEnum(AlertLevel, alert[0]);
+            const desc = @intToEnum(AlertDescription, alert[1]);
+            std.debug.print("alert: {s} {s}\n", .{ @tagName(level), @tagName(desc) });
+            std.process.exit(1);
+        } else {
+            std.debug.print("content_type: {s}\n", .{@tagName(ct)});
+            std.debug.print("got {d} bytes: {s}\n", .{ amt, std.fmt.fmtSliceHexLower(resp) });
+        }
+    }
+
+    tls.state = .sent_hello;
+}
+
+pub fn writeAll(tls: *Tls, stream: net.Stream, buffer: []const u8) !void {
+    _ = tls;
+    _ = stream;
+    _ = buffer;
+    @panic("hold on a minute, we didn't finish implementing the handshake yet");
+}
lib/std/http/Client.zig
@@ -0,0 +1,114 @@
+const std = @import("../std.zig");
+const assert = std.debug.assert;
+const http = std.http;
+const net = std.net;
+const Client = @This();
+
+allocator: std.mem.Allocator,
+headers: std.ArrayListUnmanaged(u8) = .{},
+active_requests: usize = 0,
+
+pub const Request = struct {
+    client: *Client,
+    stream: net.Stream,
+    headers: std.ArrayListUnmanaged(u8) = .{},
+    tls: std.crypto.Tls = .{},
+    protocol: Protocol,
+
+    pub const Protocol = enum { http, https };
+
+    pub const Options = struct {
+        family: Family = .any,
+        protocol: Protocol = .https,
+        method: http.Method = .GET,
+        host: []const u8 = "localhost",
+        path: []const u8 = "/",
+        port: u16 = 0,
+
+        pub const Family = enum { any, ip4, ip6 };
+    };
+
+    pub fn deinit(req: *Request) void {
+        req.client.active_requests -= 1;
+        req.headers.deinit(req.client.allocator);
+        req.* = undefined;
+    }
+
+    pub fn addHeader(req: *Request, name: []const u8, value: []const u8) !void {
+        const gpa = req.client.allocator;
+        // Ensure an extra +2 for the \r\n in end()
+        try req.headers.ensureUnusedCapacity(gpa, name.len + value.len + 6);
+        req.headers.appendSliceAssumeCapacity(name);
+        req.headers.appendSliceAssumeCapacity(": ");
+        req.headers.appendSliceAssumeCapacity(value);
+        req.headers.appendSliceAssumeCapacity("\r\n");
+    }
+
+    pub fn end(req: *Request) !void {
+        req.headers.appendSliceAssumeCapacity("\r\n");
+        switch (req.protocol) {
+            .http => {
+                try req.stream.writeAll(req.headers.items);
+            },
+            .https => {
+                try req.tls.writeAll(req.stream, req.headers.items);
+            },
+        }
+    }
+};
+
+pub fn deinit(client: *Client) void {
+    assert(client.active_requests == 0);
+    client.headers.denit(client.allocator);
+    client.* = undefined;
+}
+
+pub fn request(client: *Client, options: Request.Options) !Request {
+    var req: Request = .{
+        .client = client,
+        .stream = try net.tcpConnectToHost(client.allocator, options.host, options.port),
+        .protocol = options.protocol,
+    };
+    errdefer req.deinit();
+
+    switch (options.protocol) {
+        .http => {},
+        .https => {
+            try req.tls.init(req.stream, options.host);
+        },
+    }
+
+    try req.headers.ensureUnusedCapacity(
+        client.allocator,
+        @tagName(options.method).len +
+            1 +
+            options.path.len +
+            " HTTP/2\r\nHost: ".len +
+            options.host.len +
+            "\r\nUpgrade-Insecure-Requests: 1\r\n".len +
+            client.headers.items.len +
+            2, // for the \r\n at the end of headers
+    );
+    req.headers.appendSliceAssumeCapacity(@tagName(options.method));
+    req.headers.appendSliceAssumeCapacity(" ");
+    req.headers.appendSliceAssumeCapacity(options.path);
+    req.headers.appendSliceAssumeCapacity(" HTTP/2\r\nHost: ");
+    req.headers.appendSliceAssumeCapacity(options.host);
+    switch (options.protocol) {
+        .https => req.headers.appendSliceAssumeCapacity("\r\nUpgrade-Insecure-Requests: 1\r\n"),
+        .http => req.headers.appendSliceAssumeCapacity("\r\n"),
+    }
+    req.headers.appendSliceAssumeCapacity(client.headers.items);
+
+    client.active_requests += 1;
+    return req;
+}
+
+pub fn addHeader(client: *Client, name: []const u8, value: []const u8) !void {
+    const gpa = client.allocator;
+    try client.headers.ensureUnusedCapacity(gpa, name.len + value.len + 4);
+    client.headers.appendSliceAssumeCapacity(name);
+    client.headers.appendSliceAssumeCapacity(": ");
+    client.headers.appendSliceAssumeCapacity(value);
+    client.headers.appendSliceAssumeCapacity("\r\n");
+}
lib/std/http/method.zig
@@ -1,65 +0,0 @@
-//! HTTP Methods
-//! https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
-
-// Style guide is violated here so that @tagName can be used effectively
-/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definiton
-/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
-pub const Method = enum {
-    GET,
-    HEAD,
-    POST,
-    PUT,
-    DELETE,
-    CONNECT,
-    OPTIONS,
-    TRACE,
-    PATCH,
-
-    /// Returns true if a request of this method is allowed to have a body
-    /// Actual behavior from servers may vary and should still be checked
-    pub fn requestHasBody(self: Method) bool {
-        return switch (self) {
-            .POST, .PUT, .PATCH => true,
-            .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false,
-        };
-    }
-
-    /// Returns true if a response to this method is allowed to have a body
-    /// Actual behavior from clients may vary and should still be checked
-    pub fn responseHasBody(self: Method) bool {
-        return switch (self) {
-            .GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true,
-            .HEAD, .PUT, .TRACE => false,
-        };
-    }
-
-    /// An HTTP method is safe if it doesn't alter the state of the server.
-    /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
-    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
-    pub fn safe(self: Method) bool {
-        return switch (self) {
-            .GET, .HEAD, .OPTIONS, .TRACE => true,
-            .POST, .PUT, .DELETE, .CONNECT, .PATCH => false,
-        };
-    }
-
-    /// An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state.
-    /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
-    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
-    pub fn idempotent(self: Method) bool {
-        return switch (self) {
-            .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true,
-            .CONNECT, .POST, .PATCH => false,
-        };
-    }
-
-    /// A cacheable response is an HTTP response that can be cached, that is stored to be retrieved and used later, saving a new request to the server.
-    /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable
-    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
-    pub fn cacheable(self: Method) bool {
-        return switch (self) {
-            .GET, .HEAD => true,
-            .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false,
-        };
-    }
-};
lib/std/http/status.zig
@@ -1,182 +0,0 @@
-//! HTTP Status
-//! https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
-
-const std = @import("../std.zig");
-
-pub const Status = enum(u10) {
-    @"continue" = 100, // RFC7231, Section 6.2.1
-    switching_protocols = 101, // RFC7231, Section 6.2.2
-    processing = 102, // RFC2518
-    early_hints = 103, // RFC8297
-
-    ok = 200, // RFC7231, Section 6.3.1
-    created = 201, // RFC7231, Section 6.3.2
-    accepted = 202, // RFC7231, Section 6.3.3
-    non_authoritative_info = 203, // RFC7231, Section 6.3.4
-    no_content = 204, // RFC7231, Section 6.3.5
-    reset_content = 205, // RFC7231, Section 6.3.6
-    partial_content = 206, // RFC7233, Section 4.1
-    multi_status = 207, // RFC4918
-    already_reported = 208, // RFC5842
-    im_used = 226, // RFC3229
-
-    multiple_choice = 300, // RFC7231, Section 6.4.1
-    moved_permanently = 301, // RFC7231, Section 6.4.2
-    found = 302, // RFC7231, Section 6.4.3
-    see_other = 303, // RFC7231, Section 6.4.4
-    not_modified = 304, // RFC7232, Section 4.1
-    use_proxy = 305, // RFC7231, Section 6.4.5
-    temporary_redirect = 307, // RFC7231, Section 6.4.7
-    permanent_redirect = 308, // RFC7538
-
-    bad_request = 400, // RFC7231, Section 6.5.1
-    unauthorized = 401, // RFC7235, Section 3.1
-    payment_required = 402, // RFC7231, Section 6.5.2
-    forbidden = 403, // RFC7231, Section 6.5.3
-    not_found = 404, // RFC7231, Section 6.5.4
-    method_not_allowed = 405, // RFC7231, Section 6.5.5
-    not_acceptable = 406, // RFC7231, Section 6.5.6
-    proxy_auth_required = 407, // RFC7235, Section 3.2
-    request_timeout = 408, // RFC7231, Section 6.5.7
-    conflict = 409, // RFC7231, Section 6.5.8
-    gone = 410, // RFC7231, Section 6.5.9
-    length_required = 411, // RFC7231, Section 6.5.10
-    precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
-    payload_too_large = 413, // RFC7231, Section 6.5.11
-    uri_too_long = 414, // RFC7231, Section 6.5.12
-    unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
-    range_not_satisfiable = 416, // RFC7233, Section 4.4
-    expectation_failed = 417, // RFC7231, Section 6.5.14
-    teapot = 418, // RFC 7168, 2.3.3
-    misdirected_request = 421, // RFC7540, Section 9.1.2
-    unprocessable_entity = 422, // RFC4918
-    locked = 423, // RFC4918
-    failed_dependency = 424, // RFC4918
-    too_early = 425, // RFC8470
-    upgrade_required = 426, // RFC7231, Section 6.5.15
-    precondition_required = 428, // RFC6585
-    too_many_requests = 429, // RFC6585
-    header_fields_too_large = 431, // RFC6585
-    unavailable_for_legal_reasons = 451, // RFC7725
-
-    internal_server_error = 500, // RFC7231, Section 6.6.1
-    not_implemented = 501, // RFC7231, Section 6.6.2
-    bad_gateway = 502, // RFC7231, Section 6.6.3
-    service_unavailable = 503, // RFC7231, Section 6.6.4
-    gateway_timeout = 504, // RFC7231, Section 6.6.5
-    http_version_not_supported = 505, // RFC7231, Section 6.6.6
-    variant_also_negotiates = 506, // RFC2295
-    insufficient_storage = 507, // RFC4918
-    loop_detected = 508, // RFC5842
-    not_extended = 510, // RFC2774
-    network_authentication_required = 511, // RFC6585
-
-    _,
-
-    pub fn phrase(self: Status) ?[]const u8 {
-        return switch (self) {
-            // 1xx statuses
-            .@"continue" => "Continue",
-            .switching_protocols => "Switching Protocols",
-            .processing => "Processing",
-            .early_hints => "Early Hints",
-
-            // 2xx statuses
-            .ok => "OK",
-            .created => "Created",
-            .accepted => "Accepted",
-            .non_authoritative_info => "Non-Authoritative Information",
-            .no_content => "No Content",
-            .reset_content => "Reset Content",
-            .partial_content => "Partial Content",
-            .multi_status => "Multi-Status",
-            .already_reported => "Already Reported",
-            .im_used => "IM Used",
-
-            // 3xx statuses
-            .multiple_choice => "Multiple Choice",
-            .moved_permanently => "Moved Permanently",
-            .found => "Found",
-            .see_other => "See Other",
-            .not_modified => "Not Modified",
-            .use_proxy => "Use Proxy",
-            .temporary_redirect => "Temporary Redirect",
-            .permanent_redirect => "Permanent Redirect",
-
-            // 4xx statuses
-            .bad_request => "Bad Request",
-            .unauthorized => "Unauthorized",
-            .payment_required => "Payment Required",
-            .forbidden => "Forbidden",
-            .not_found => "Not Found",
-            .method_not_allowed => "Method Not Allowed",
-            .not_acceptable => "Not Acceptable",
-            .proxy_auth_required => "Proxy Authentication Required",
-            .request_timeout => "Request Timeout",
-            .conflict => "Conflict",
-            .gone => "Gone",
-            .length_required => "Length Required",
-            .precondition_failed => "Precondition Failed",
-            .payload_too_large => "Payload Too Large",
-            .uri_too_long => "URI Too Long",
-            .unsupported_media_type => "Unsupported Media Type",
-            .range_not_satisfiable => "Range Not Satisfiable",
-            .expectation_failed => "Expectation Failed",
-            .teapot => "I'm a teapot",
-            .misdirected_request => "Misdirected Request",
-            .unprocessable_entity => "Unprocessable Entity",
-            .locked => "Locked",
-            .failed_dependency => "Failed Dependency",
-            .too_early => "Too Early",
-            .upgrade_required => "Upgrade Required",
-            .precondition_required => "Precondition Required",
-            .too_many_requests => "Too Many Requests",
-            .header_fields_too_large => "Request Header Fields Too Large",
-            .unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
-
-            // 5xx statuses
-            .internal_server_error => "Internal Server Error",
-            .not_implemented => "Not Implemented",
-            .bad_gateway => "Bad Gateway",
-            .service_unavailable => "Service Unavailable",
-            .gateway_timeout => "Gateway Timeout",
-            .http_version_not_supported => "HTTP Version Not Supported",
-            .variant_also_negotiates => "Variant Also Negotiates",
-            .insufficient_storage => "Insufficient Storage",
-            .loop_detected => "Loop Detected",
-            .not_extended => "Not Extended",
-            .network_authentication_required => "Network Authentication Required",
-
-            else => return null,
-        };
-    }
-
-    pub const Class = enum {
-        informational,
-        success,
-        redirect,
-        client_error,
-        server_error,
-    };
-
-    pub fn class(self: Status) ?Class {
-        return switch (@enumToInt(self)) {
-            100...199 => .informational,
-            200...299 => .success,
-            300...399 => .redirect,
-            400...499 => .client_error,
-            500...599 => .server_error,
-            else => null,
-        };
-    }
-};
-
-test {
-    try std.testing.expectEqualStrings("OK", Status.ok.phrase().?);
-    try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?);
-}
-
-test {
-    try std.testing.expectEqual(@as(?Status.Class, Status.Class.success), Status.ok.class());
-    try std.testing.expectEqual(@as(?Status.Class, Status.Class.client_error), Status.not_found.class());
-}
lib/std/crypto.zig
@@ -176,6 +176,8 @@ const std = @import("std.zig");
 
 pub const errors = @import("crypto/errors.zig");
 
+pub const Tls = @import("crypto/Tls.zig");
+
 test {
     _ = aead.aegis.Aegis128L;
     _ = aead.aegis.Aegis256;
lib/std/http.zig
@@ -1,8 +1,251 @@
-const std = @import("std.zig");
+pub const Client = @import("http/Client.zig");
+
+/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
+/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definiton
+/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
+pub const Method = enum {
+    GET,
+    HEAD,
+    POST,
+    PUT,
+    DELETE,
+    CONNECT,
+    OPTIONS,
+    TRACE,
+    PATCH,
+
+    /// Returns true if a request of this method is allowed to have a body
+    /// Actual behavior from servers may vary and should still be checked
+    pub fn requestHasBody(self: Method) bool {
+        return switch (self) {
+            .POST, .PUT, .PATCH => true,
+            .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false,
+        };
+    }
+
+    /// Returns true if a response to this method is allowed to have a body
+    /// Actual behavior from clients may vary and should still be checked
+    pub fn responseHasBody(self: Method) bool {
+        return switch (self) {
+            .GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true,
+            .HEAD, .PUT, .TRACE => false,
+        };
+    }
+
+    /// An HTTP method is safe if it doesn't alter the state of the server.
+    /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
+    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
+    pub fn safe(self: Method) bool {
+        return switch (self) {
+            .GET, .HEAD, .OPTIONS, .TRACE => true,
+            .POST, .PUT, .DELETE, .CONNECT, .PATCH => false,
+        };
+    }
+
+    /// An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state.
+    /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
+    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
+    pub fn idempotent(self: Method) bool {
+        return switch (self) {
+            .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true,
+            .CONNECT, .POST, .PATCH => false,
+        };
+    }
+
+    /// A cacheable response is an HTTP response that can be cached, that is stored to be retrieved and used later, saving a new request to the server.
+    /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable
+    /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
+    pub fn cacheable(self: Method) bool {
+        return switch (self) {
+            .GET, .HEAD => true,
+            .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false,
+        };
+    }
+};
+
+/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
+pub const Status = enum(u10) {
+    @"continue" = 100, // RFC7231, Section 6.2.1
+    switching_protocols = 101, // RFC7231, Section 6.2.2
+    processing = 102, // RFC2518
+    early_hints = 103, // RFC8297
+
+    ok = 200, // RFC7231, Section 6.3.1
+    created = 201, // RFC7231, Section 6.3.2
+    accepted = 202, // RFC7231, Section 6.3.3
+    non_authoritative_info = 203, // RFC7231, Section 6.3.4
+    no_content = 204, // RFC7231, Section 6.3.5
+    reset_content = 205, // RFC7231, Section 6.3.6
+    partial_content = 206, // RFC7233, Section 4.1
+    multi_status = 207, // RFC4918
+    already_reported = 208, // RFC5842
+    im_used = 226, // RFC3229
+
+    multiple_choice = 300, // RFC7231, Section 6.4.1
+    moved_permanently = 301, // RFC7231, Section 6.4.2
+    found = 302, // RFC7231, Section 6.4.3
+    see_other = 303, // RFC7231, Section 6.4.4
+    not_modified = 304, // RFC7232, Section 4.1
+    use_proxy = 305, // RFC7231, Section 6.4.5
+    temporary_redirect = 307, // RFC7231, Section 6.4.7
+    permanent_redirect = 308, // RFC7538
+
+    bad_request = 400, // RFC7231, Section 6.5.1
+    unauthorized = 401, // RFC7235, Section 3.1
+    payment_required = 402, // RFC7231, Section 6.5.2
+    forbidden = 403, // RFC7231, Section 6.5.3
+    not_found = 404, // RFC7231, Section 6.5.4
+    method_not_allowed = 405, // RFC7231, Section 6.5.5
+    not_acceptable = 406, // RFC7231, Section 6.5.6
+    proxy_auth_required = 407, // RFC7235, Section 3.2
+    request_timeout = 408, // RFC7231, Section 6.5.7
+    conflict = 409, // RFC7231, Section 6.5.8
+    gone = 410, // RFC7231, Section 6.5.9
+    length_required = 411, // RFC7231, Section 6.5.10
+    precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
+    payload_too_large = 413, // RFC7231, Section 6.5.11
+    uri_too_long = 414, // RFC7231, Section 6.5.12
+    unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
+    range_not_satisfiable = 416, // RFC7233, Section 4.4
+    expectation_failed = 417, // RFC7231, Section 6.5.14
+    teapot = 418, // RFC 7168, 2.3.3
+    misdirected_request = 421, // RFC7540, Section 9.1.2
+    unprocessable_entity = 422, // RFC4918
+    locked = 423, // RFC4918
+    failed_dependency = 424, // RFC4918
+    too_early = 425, // RFC8470
+    upgrade_required = 426, // RFC7231, Section 6.5.15
+    precondition_required = 428, // RFC6585
+    too_many_requests = 429, // RFC6585
+    header_fields_too_large = 431, // RFC6585
+    unavailable_for_legal_reasons = 451, // RFC7725
+
+    internal_server_error = 500, // RFC7231, Section 6.6.1
+    not_implemented = 501, // RFC7231, Section 6.6.2
+    bad_gateway = 502, // RFC7231, Section 6.6.3
+    service_unavailable = 503, // RFC7231, Section 6.6.4
+    gateway_timeout = 504, // RFC7231, Section 6.6.5
+    http_version_not_supported = 505, // RFC7231, Section 6.6.6
+    variant_also_negotiates = 506, // RFC2295
+    insufficient_storage = 507, // RFC4918
+    loop_detected = 508, // RFC5842
+    not_extended = 510, // RFC2774
+    network_authentication_required = 511, // RFC6585
 
-pub const Method = @import("http/method.zig").Method;
-pub const Status = @import("http/status.zig").Status;
+    _,
+
+    pub fn phrase(self: Status) ?[]const u8 {
+        return switch (self) {
+            // 1xx statuses
+            .@"continue" => "Continue",
+            .switching_protocols => "Switching Protocols",
+            .processing => "Processing",
+            .early_hints => "Early Hints",
+
+            // 2xx statuses
+            .ok => "OK",
+            .created => "Created",
+            .accepted => "Accepted",
+            .non_authoritative_info => "Non-Authoritative Information",
+            .no_content => "No Content",
+            .reset_content => "Reset Content",
+            .partial_content => "Partial Content",
+            .multi_status => "Multi-Status",
+            .already_reported => "Already Reported",
+            .im_used => "IM Used",
+
+            // 3xx statuses
+            .multiple_choice => "Multiple Choice",
+            .moved_permanently => "Moved Permanently",
+            .found => "Found",
+            .see_other => "See Other",
+            .not_modified => "Not Modified",
+            .use_proxy => "Use Proxy",
+            .temporary_redirect => "Temporary Redirect",
+            .permanent_redirect => "Permanent Redirect",
+
+            // 4xx statuses
+            .bad_request => "Bad Request",
+            .unauthorized => "Unauthorized",
+            .payment_required => "Payment Required",
+            .forbidden => "Forbidden",
+            .not_found => "Not Found",
+            .method_not_allowed => "Method Not Allowed",
+            .not_acceptable => "Not Acceptable",
+            .proxy_auth_required => "Proxy Authentication Required",
+            .request_timeout => "Request Timeout",
+            .conflict => "Conflict",
+            .gone => "Gone",
+            .length_required => "Length Required",
+            .precondition_failed => "Precondition Failed",
+            .payload_too_large => "Payload Too Large",
+            .uri_too_long => "URI Too Long",
+            .unsupported_media_type => "Unsupported Media Type",
+            .range_not_satisfiable => "Range Not Satisfiable",
+            .expectation_failed => "Expectation Failed",
+            .teapot => "I'm a teapot",
+            .misdirected_request => "Misdirected Request",
+            .unprocessable_entity => "Unprocessable Entity",
+            .locked => "Locked",
+            .failed_dependency => "Failed Dependency",
+            .too_early => "Too Early",
+            .upgrade_required => "Upgrade Required",
+            .precondition_required => "Precondition Required",
+            .too_many_requests => "Too Many Requests",
+            .header_fields_too_large => "Request Header Fields Too Large",
+            .unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
+
+            // 5xx statuses
+            .internal_server_error => "Internal Server Error",
+            .not_implemented => "Not Implemented",
+            .bad_gateway => "Bad Gateway",
+            .service_unavailable => "Service Unavailable",
+            .gateway_timeout => "Gateway Timeout",
+            .http_version_not_supported => "HTTP Version Not Supported",
+            .variant_also_negotiates => "Variant Also Negotiates",
+            .insufficient_storage => "Insufficient Storage",
+            .loop_detected => "Loop Detected",
+            .not_extended => "Not Extended",
+            .network_authentication_required => "Network Authentication Required",
+
+            else => return null,
+        };
+    }
+
+    pub const Class = enum {
+        informational,
+        success,
+        redirect,
+        client_error,
+        server_error,
+    };
+
+    pub fn class(self: Status) ?Class {
+        return switch (@enumToInt(self)) {
+            100...199 => .informational,
+            200...299 => .success,
+            300...399 => .redirect,
+            400...499 => .client_error,
+            500...599 => .server_error,
+            else => null,
+        };
+    }
+
+    test {
+        try std.testing.expectEqualStrings("OK", Status.ok.phrase().?);
+        try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?);
+    }
+
+    test {
+        try std.testing.expectEqual(@as(?Status.Class, Status.Class.success), Status.ok.class());
+        try std.testing.expectEqual(@as(?Status.Class, Status.Class.client_error), Status.not_found.class());
+    }
+};
+
+const std = @import("std.zig");
 
 test {
-    std.testing.refAllDecls(@This());
+    _ = Client;
+    _ = Method;
+    _ = Status;
 }
lib/std/net.zig
@@ -1687,6 +1687,13 @@ pub const Stream = struct {
         }
     }
 
+    pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
+        var index: usize = 0;
+        while (index < bytes.len) {
+            index += try self.write(bytes[index..]);
+        }
+    }
+
     /// See https://github.com/ziglang/zig/issues/7699
     /// See equivalent function: `std.fs.File.writev`.
     pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {