mirror of
https://github.com/ziglang/zig.git
synced 2026-02-16 06:18:32 +00:00
std.http reorg; introduce std.crypto.Tls
TLS is capable of sending a Client Hello
This commit is contained in:
parent
cd0d514643
commit
ba44513c2f
@ -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
342
lib/std/crypto/Tls.zig
Normal 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");
|
||||
}
|
||||
251
lib/std/http.zig
251
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;
|
||||
}
|
||||
|
||||
114
lib/std/http/Client.zig
Normal file
114
lib/std/http/Client.zig
Normal 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");
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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());
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user