diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index 8aaf305143..7b4a116d35 100644 --- a/lib/std/crypto.zig +++ b/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; diff --git a/lib/std/crypto/Tls.zig b/lib/std/crypto/Tls.zig new file mode 100644 index 0000000000..ab54b42b70 --- /dev/null +++ b/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"); +} diff --git a/lib/std/http.zig b/lib/std/http.zig index 8da6968403..cf92b462b8 100644 --- a/lib/std/http.zig +++ b/lib/std/http.zig @@ -1,8 +1,251 @@ +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 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"); -pub const Method = @import("http/method.zig").Method; -pub const Status = @import("http/status.zig").Status; - test { - std.testing.refAllDecls(@This()); + _ = Client; + _ = Method; + _ = Status; } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig new file mode 100644 index 0000000000..80904d765c --- /dev/null +++ b/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"); +} diff --git a/lib/std/http/method.zig b/lib/std/http/method.zig deleted file mode 100644 index c118ca9a47..0000000000 --- a/lib/std/http/method.zig +++ /dev/null @@ -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, - }; - } -}; diff --git a/lib/std/http/status.zig b/lib/std/http/status.zig deleted file mode 100644 index 91738e0533..0000000000 --- a/lib/std/http/status.zig +++ /dev/null @@ -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()); -} diff --git a/lib/std/net.zig b/lib/std/net.zig index 4a0582e7f5..ebc4de08b9 100644 --- a/lib/std/net.zig +++ b/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 {