std.http reorg; introduce std.crypto.Tls

TLS is capable of sending a Client Hello
This commit is contained in:
Andrew Kelley 2022-12-12 21:18:56 -07:00
parent cd0d514643
commit ba44513c2f
7 changed files with 712 additions and 251 deletions

View File

@ -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;

342
lib/std/crypto/Tls.zig Normal file
View File

@ -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");
}

View File

@ -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;
}

114
lib/std/http/Client.zig Normal file
View File

@ -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");
}

View File

@ -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,
};
}
};

View File

@ -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());
}

View File

@ -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 {