authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-12-12 21:18:56-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-02 16:57:15-07:00
logba44513c2fe363b55b2c534be98179286b832b7e
treebda249eafac429caabb4ce217065e1688aafd4c8
parentcd0d514643404103a83881fc4d7c46674ed9f991

std.http reorg; introduce std.crypto.Tls

TLS is capable of sending a Client Hello

7 files changed, 712 insertions(+), 251 deletions(-)

lib/std/crypto.zig+2
...@@ -176,6 +176,8 @@ const std = @import("std.zig");...@@ -176,6 +176,8 @@ const std = @import("std.zig");
176176
177pub const errors = @import("crypto/errors.zig");177pub const errors = @import("crypto/errors.zig");
178178
179pub const Tls = @import("crypto/Tls.zig");
180
179test {181test {
180 _ = aead.aegis.Aegis128L;182 _ = aead.aegis.Aegis128L;
181 _ = aead.aegis.Aegis256;183 _ = aead.aegis.Aegis256;
lib/std/crypto/Tls.zig created+342
...@@ -0,0 +1,342 @@
1const std = @import("../std.zig");
2const Tls = @This();
3const net = std.net;
4const mem = std.mem;
5const crypto = std.crypto;
6const assert = std.debug.assert;
7
8state: State = .start,
9x25519_priv_key: [32]u8 = undefined,
10x25519_pub_key: [32]u8 = undefined,
11
12const State = enum {
13 /// In this state, all fields are undefined except state.
14 start,
15 sent_hello,
16};
17
18const ContentType = enum(u8) {
19 invalid = 0,
20 change_cipher_spec = 20,
21 alert = 21,
22 handshake = 22,
23 application_data = 23,
24 _,
25};
26
27const HandshakeType = enum(u8) {
28 client_hello = 1,
29 server_hello = 2,
30 new_session_ticket = 4,
31 end_of_early_data = 5,
32 encrypted_extensions = 8,
33 certificate = 11,
34 certificate_request = 13,
35 certificate_verify = 15,
36 finished = 20,
37 key_update = 24,
38 message_hash = 254,
39};
40
41const ExtensionType = enum(u16) {
42 /// RFC 6066
43 server_name = 0,
44 /// RFC 6066
45 max_fragment_length = 1,
46 /// RFC 6066
47 status_request = 5,
48 /// RFC 8422, 7919
49 supported_groups = 10,
50 /// RFC 8446
51 signature_algorithms = 13,
52 /// RFC 5764
53 use_srtp = 14,
54 /// RFC 6520
55 heartbeat = 15,
56 /// RFC 7301
57 application_layer_protocol_negotiation = 16,
58 /// RFC 6962
59 signed_certificate_timestamp = 18,
60 /// RFC 7250
61 client_certificate_type = 19,
62 /// RFC 7250
63 server_certificate_type = 20,
64 /// RFC 7685
65 padding = 21,
66 /// RFC 8446
67 pre_shared_key = 41,
68 /// RFC 8446
69 early_data = 42,
70 /// RFC 8446
71 supported_versions = 43,
72 /// RFC 8446
73 cookie = 44,
74 /// RFC 8446
75 psk_key_exchange_modes = 45,
76 /// RFC 8446
77 certificate_authorities = 47,
78 /// RFC 8446
79 oid_filters = 48,
80 /// RFC 8446
81 post_handshake_auth = 49,
82 /// RFC 8446
83 signature_algorithms_cert = 50,
84 /// RFC 8446
85 key_share = 51,
86};
87
88const AlertLevel = enum(u8) {
89 warning = 1,
90 fatal = 2,
91 _,
92};
93
94const AlertDescription = enum(u8) {
95 close_notify = 0,
96 unexpected_message = 10,
97 bad_record_mac = 20,
98 record_overflow = 22,
99 handshake_failure = 40,
100 bad_certificate = 42,
101 unsupported_certificate = 43,
102 certificate_revoked = 44,
103 certificate_expired = 45,
104 certificate_unknown = 46,
105 illegal_parameter = 47,
106 unknown_ca = 48,
107 access_denied = 49,
108 decode_error = 50,
109 decrypt_error = 51,
110 protocol_version = 70,
111 insufficient_security = 71,
112 internal_error = 80,
113 inappropriate_fallback = 86,
114 user_canceled = 90,
115 missing_extension = 109,
116 unsupported_extension = 110,
117 unrecognized_name = 112,
118 bad_certificate_status_response = 113,
119 unknown_psk_identity = 115,
120 certificate_required = 116,
121 no_application_protocol = 120,
122 _,
123};
124
125const SignatureScheme = enum(u16) {
126 // RSASSA-PKCS1-v1_5 algorithms
127 rsa_pkcs1_sha256 = 0x0401,
128 rsa_pkcs1_sha384 = 0x0501,
129 rsa_pkcs1_sha512 = 0x0601,
130
131 // ECDSA algorithms
132 ecdsa_secp256r1_sha256 = 0x0403,
133 ecdsa_secp384r1_sha384 = 0x0503,
134 ecdsa_secp521r1_sha512 = 0x0603,
135
136 // RSASSA-PSS algorithms with public key OID rsaEncryption
137 rsa_pss_rsae_sha256 = 0x0804,
138 rsa_pss_rsae_sha384 = 0x0805,
139 rsa_pss_rsae_sha512 = 0x0806,
140
141 // EdDSA algorithms
142 ed25519 = 0x0807,
143 ed448 = 0x0808,
144
145 // RSASSA-PSS algorithms with public key OID RSASSA-PSS
146 rsa_pss_pss_sha256 = 0x0809,
147 rsa_pss_pss_sha384 = 0x080a,
148 rsa_pss_pss_sha512 = 0x080b,
149
150 // Legacy algorithms
151 rsa_pkcs1_sha1 = 0x0201,
152 ecdsa_sha1 = 0x0203,
153
154 _,
155};
156
157const NamedGroup = enum(u16) {
158 // Elliptic Curve Groups (ECDHE)
159 secp256r1 = 0x0017,
160 secp384r1 = 0x0018,
161 secp521r1 = 0x0019,
162 x25519 = 0x001D,
163 x448 = 0x001E,
164
165 // Finite Field Groups (DHE)
166 ffdhe2048 = 0x0100,
167 ffdhe3072 = 0x0101,
168 ffdhe4096 = 0x0102,
169 ffdhe6144 = 0x0103,
170 ffdhe8192 = 0x0104,
171
172 _,
173};
174
175// Plaintext:
176// * type: ContentType
177// * legacy_record_version: u16 = 0x0303,
178// * length: u16,
179// - The length (in bytes) of the following TLSPlaintext.fragment. The
180// length MUST NOT exceed 2^14 bytes.
181// * fragment: opaque
182// - the data being transmitted
183
184// Handshake:
185// * type: HandshakeType
186// * length: u24
187// * data: opaque
188
189const CipherSuite = enum(u16) {
190 TLS_AES_128_GCM_SHA256 = 0x1301,
191 TLS_AES_256_GCM_SHA384 = 0x1302,
192 TLS_CHACHA20_POLY1305_SHA256 = 0x1303,
193 TLS_AES_128_CCM_SHA256 = 0x1304,
194 TLS_AES_128_CCM_8_SHA256 = 0x1305,
195};
196
197const cipher_suites = blk: {
198 const fields = @typeInfo(CipherSuite).Enum.fields;
199 var result: [(fields.len + 1) * 2]u8 = undefined;
200 mem.writeIntBig(u16, result[0..2], result.len - 2);
201 for (fields) |field, i| {
202 const int = @enumToInt(@field(CipherSuite, field.name));
203 result[(i + 1) * 2] = @truncate(u8, int >> 8);
204 result[(i + 1) * 2 + 1] = @truncate(u8, int);
205 }
206 break :blk result;
207};
208
209pub fn init(tls: *Tls, stream: net.Stream, host: []const u8) !void {
210 assert(tls.state == .start);
211 crypto.random.bytes(&tls.x25519_priv_key);
212 tls.x25519_pub_key = try crypto.dh.X25519.recoverPublicKey(tls.x25519_priv_key);
213
214 // random (u32)
215 var rand_buf: [32]u8 = undefined;
216 crypto.random.bytes(&rand_buf);
217
218 const extensions_header = [_]u8{
219 // Extensions byte length
220 undefined, undefined,
221
222 // Extension: supported_versions (only TLS 1.3)
223 0, 43, // ExtensionType.supported_versions
224 0x00, 0x05, // byte length of this extension payload
225 0x04, // byte length of supported versions
226 0x03, 0x04, // TLS 1.3
227 0x03, 0x03, // TLS 1.2
228
229 // Extension: signature_algorithms
230 0, 13, // ExtensionType.signature_algorithms
231 0x00, 0x22, // byte length of this extension payload
232 0x00, 0x20, // byte length of signature algorithms list
233 0x04, 0x01, // rsa_pkcs1_sha256
234 0x05, 0x01, // rsa_pkcs1_sha384
235 0x06, 0x01, // rsa_pkcs1_sha512
236 0x04, 0x03, // ecdsa_secp256r1_sha256
237 0x05, 0x03, // ecdsa_secp384r1_sha384
238 0x06, 0x03, // ecdsa_secp521r1_sha512
239 0x08, 0x04, // rsa_pss_rsae_sha256
240 0x08, 0x05, // rsa_pss_rsae_sha384
241 0x08, 0x06, // rsa_pss_rsae_sha512
242 0x08, 0x07, // ed25519
243 0x08, 0x08, // ed448
244 0x08, 0x09, // rsa_pss_pss_sha256
245 0x08, 0x0a, // rsa_pss_pss_sha384
246 0x08, 0x0b, // rsa_pss_pss_sha512
247 0x02, 0x01, // rsa_pkcs1_sha1
248 0x02, 0x03, // ecdsa_sha1
249
250 // Extension: supported_groups
251 0, 10, // ExtensionType.supported_groups
252 0x00, 0x0c, // byte length of this extension payload
253 0x00, 0x0a, // byte length of supported groups list
254 0x00, 0x17, // secp256r1
255 0x00, 0x18, // secp384r1
256 0x00, 0x19, // secp521r1
257 0x00, 0x1D, // x25519
258 0x00, 0x1E, // x448
259
260 // Extension: key_share
261 0, 51, // ExtensionType.key_share
262 0x00, 38, // byte length of this extension payload
263 0x00, 36, // byte length of client_shares
264 0x00, 0x1D, // NamedGroup.x25519
265 0x00, 32, // byte length of key_exchange
266 } ++ tls.x25519_pub_key ++ [_]u8{
267
268 // Extension: server_name
269 0, 0, // ExtensionType.server_name
270 undefined, undefined, // byte length of this extension payload
271 undefined, undefined, // server_name_list byte count
272 0x00, // name_type
273 undefined, undefined, // host name len
274 };
275
276 var hello_header = [_]u8{
277 // Plaintext header
278 @enumToInt(ContentType.handshake),
279 0x03, 0x01, // legacy_record_version
280 undefined, undefined, // Plaintext fragment length (u16)
281
282 // Handshake header
283 @enumToInt(HandshakeType.client_hello),
284 undefined, undefined, undefined, // handshake length (u24)
285
286 // ClientHello
287 0x03, 0x03, // legacy_version
288 } ++ rand_buf ++ [1]u8{0} ++ cipher_suites ++ [_]u8{
289 0x01, 0x00, // legacy_compression_methods
290 } ++ extensions_header;
291
292 mem.writeIntBig(u16, hello_header[3..][0..2], @intCast(u16, hello_header.len - 5 + host.len));
293 mem.writeIntBig(u24, hello_header[6..][0..3], @intCast(u24, hello_header.len - 9 + host.len));
294 mem.writeIntBig(
295 u16,
296 hello_header[hello_header.len - extensions_header.len ..][0..2],
297 @intCast(u16, extensions_header.len - 2 + host.len),
298 );
299 mem.writeIntBig(u16, hello_header[hello_header.len - 7 ..][0..2], @intCast(u16, 5 + host.len));
300 mem.writeIntBig(u16, hello_header[hello_header.len - 5 ..][0..2], @intCast(u16, 3 + host.len));
301 mem.writeIntBig(u16, hello_header[hello_header.len - 2 ..][0..2], @intCast(u16, 0 + host.len));
302
303 var iovecs = [_]std.os.iovec_const{
304 .{
305 .iov_base = &hello_header,
306 .iov_len = hello_header.len,
307 },
308 .{
309 .iov_base = host.ptr,
310 .iov_len = host.len,
311 },
312 };
313 try stream.writevAll(&iovecs);
314
315 {
316 var buf: [1000]u8 = undefined;
317 const amt = try stream.read(&buf);
318 const resp = buf[0..amt];
319 const ct = @intToEnum(ContentType, resp[0]);
320 if (ct == .alert) {
321 //const prot_ver = @bitCast(u16, resp[1..][0..2].*);
322 const len = std.mem.readIntBig(u16, resp[3..][0..2]);
323 const alert = resp[5..][0..len];
324 const level = @intToEnum(AlertLevel, alert[0]);
325 const desc = @intToEnum(AlertDescription, alert[1]);
326 std.debug.print("alert: {s} {s}\n", .{ @tagName(level), @tagName(desc) });
327 std.process.exit(1);
328 } else {
329 std.debug.print("content_type: {s}\n", .{@tagName(ct)});
330 std.debug.print("got {d} bytes: {s}\n", .{ amt, std.fmt.fmtSliceHexLower(resp) });
331 }
332 }
333
334 tls.state = .sent_hello;
335}
336
337pub fn writeAll(tls: *Tls, stream: net.Stream, buffer: []const u8) !void {
338 _ = tls;
339 _ = stream;
340 _ = buffer;
341 @panic("hold on a minute, we didn't finish implementing the handshake yet");
342}
lib/std/http.zig+247-4
...@@ -1,8 +1,251 @@...@@ -1,8 +1,251 @@
1const std = @import("std.zig");1pub const Client = @import("http/Client.zig");
2
3/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
4/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definiton
5/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
6pub const Method = enum {
7 GET,
8 HEAD,
9 POST,
10 PUT,
11 DELETE,
12 CONNECT,
13 OPTIONS,
14 TRACE,
15 PATCH,
16
17 /// Returns true if a request of this method is allowed to have a body
18 /// Actual behavior from servers may vary and should still be checked
19 pub fn requestHasBody(self: Method) bool {
20 return switch (self) {
21 .POST, .PUT, .PATCH => true,
22 .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false,
23 };
24 }
25
26 /// Returns true if a response to this method is allowed to have a body
27 /// Actual behavior from clients may vary and should still be checked
28 pub fn responseHasBody(self: Method) bool {
29 return switch (self) {
30 .GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true,
31 .HEAD, .PUT, .TRACE => false,
32 };
33 }
34
35 /// An HTTP method is safe if it doesn't alter the state of the server.
36 /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
37 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
38 pub fn safe(self: Method) bool {
39 return switch (self) {
40 .GET, .HEAD, .OPTIONS, .TRACE => true,
41 .POST, .PUT, .DELETE, .CONNECT, .PATCH => false,
42 };
43 }
44
45 /// 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.
46 /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
47 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
48 pub fn idempotent(self: Method) bool {
49 return switch (self) {
50 .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true,
51 .CONNECT, .POST, .PATCH => false,
52 };
53 }
54
55 /// 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.
56 /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable
57 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
58 pub fn cacheable(self: Method) bool {
59 return switch (self) {
60 .GET, .HEAD => true,
61 .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false,
62 };
63 }
64};
65
66/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
67pub const Status = enum(u10) {
68 @"continue" = 100, // RFC7231, Section 6.2.1
69 switching_protocols = 101, // RFC7231, Section 6.2.2
70 processing = 102, // RFC2518
71 early_hints = 103, // RFC8297
72
73 ok = 200, // RFC7231, Section 6.3.1
74 created = 201, // RFC7231, Section 6.3.2
75 accepted = 202, // RFC7231, Section 6.3.3
76 non_authoritative_info = 203, // RFC7231, Section 6.3.4
77 no_content = 204, // RFC7231, Section 6.3.5
78 reset_content = 205, // RFC7231, Section 6.3.6
79 partial_content = 206, // RFC7233, Section 4.1
80 multi_status = 207, // RFC4918
81 already_reported = 208, // RFC5842
82 im_used = 226, // RFC3229
83
84 multiple_choice = 300, // RFC7231, Section 6.4.1
85 moved_permanently = 301, // RFC7231, Section 6.4.2
86 found = 302, // RFC7231, Section 6.4.3
87 see_other = 303, // RFC7231, Section 6.4.4
88 not_modified = 304, // RFC7232, Section 4.1
89 use_proxy = 305, // RFC7231, Section 6.4.5
90 temporary_redirect = 307, // RFC7231, Section 6.4.7
91 permanent_redirect = 308, // RFC7538
92
93 bad_request = 400, // RFC7231, Section 6.5.1
94 unauthorized = 401, // RFC7235, Section 3.1
95 payment_required = 402, // RFC7231, Section 6.5.2
96 forbidden = 403, // RFC7231, Section 6.5.3
97 not_found = 404, // RFC7231, Section 6.5.4
98 method_not_allowed = 405, // RFC7231, Section 6.5.5
99 not_acceptable = 406, // RFC7231, Section 6.5.6
100 proxy_auth_required = 407, // RFC7235, Section 3.2
101 request_timeout = 408, // RFC7231, Section 6.5.7
102 conflict = 409, // RFC7231, Section 6.5.8
103 gone = 410, // RFC7231, Section 6.5.9
104 length_required = 411, // RFC7231, Section 6.5.10
105 precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
106 payload_too_large = 413, // RFC7231, Section 6.5.11
107 uri_too_long = 414, // RFC7231, Section 6.5.12
108 unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
109 range_not_satisfiable = 416, // RFC7233, Section 4.4
110 expectation_failed = 417, // RFC7231, Section 6.5.14
111 teapot = 418, // RFC 7168, 2.3.3
112 misdirected_request = 421, // RFC7540, Section 9.1.2
113 unprocessable_entity = 422, // RFC4918
114 locked = 423, // RFC4918
115 failed_dependency = 424, // RFC4918
116 too_early = 425, // RFC8470
117 upgrade_required = 426, // RFC7231, Section 6.5.15
118 precondition_required = 428, // RFC6585
119 too_many_requests = 429, // RFC6585
120 header_fields_too_large = 431, // RFC6585
121 unavailable_for_legal_reasons = 451, // RFC7725
122
123 internal_server_error = 500, // RFC7231, Section 6.6.1
124 not_implemented = 501, // RFC7231, Section 6.6.2
125 bad_gateway = 502, // RFC7231, Section 6.6.3
126 service_unavailable = 503, // RFC7231, Section 6.6.4
127 gateway_timeout = 504, // RFC7231, Section 6.6.5
128 http_version_not_supported = 505, // RFC7231, Section 6.6.6
129 variant_also_negotiates = 506, // RFC2295
130 insufficient_storage = 507, // RFC4918
131 loop_detected = 508, // RFC5842
132 not_extended = 510, // RFC2774
133 network_authentication_required = 511, // RFC6585
2134
3pub const Method = @import("http/method.zig").Method;135 _,
4pub const Status = @import("http/status.zig").Status;136
137 pub fn phrase(self: Status) ?[]const u8 {
138 return switch (self) {
139 // 1xx statuses
140 .@"continue" => "Continue",
141 .switching_protocols => "Switching Protocols",
142 .processing => "Processing",
143 .early_hints => "Early Hints",
144
145 // 2xx statuses
146 .ok => "OK",
147 .created => "Created",
148 .accepted => "Accepted",
149 .non_authoritative_info => "Non-Authoritative Information",
150 .no_content => "No Content",
151 .reset_content => "Reset Content",
152 .partial_content => "Partial Content",
153 .multi_status => "Multi-Status",
154 .already_reported => "Already Reported",
155 .im_used => "IM Used",
156
157 // 3xx statuses
158 .multiple_choice => "Multiple Choice",
159 .moved_permanently => "Moved Permanently",
160 .found => "Found",
161 .see_other => "See Other",
162 .not_modified => "Not Modified",
163 .use_proxy => "Use Proxy",
164 .temporary_redirect => "Temporary Redirect",
165 .permanent_redirect => "Permanent Redirect",
166
167 // 4xx statuses
168 .bad_request => "Bad Request",
169 .unauthorized => "Unauthorized",
170 .payment_required => "Payment Required",
171 .forbidden => "Forbidden",
172 .not_found => "Not Found",
173 .method_not_allowed => "Method Not Allowed",
174 .not_acceptable => "Not Acceptable",
175 .proxy_auth_required => "Proxy Authentication Required",
176 .request_timeout => "Request Timeout",
177 .conflict => "Conflict",
178 .gone => "Gone",
179 .length_required => "Length Required",
180 .precondition_failed => "Precondition Failed",
181 .payload_too_large => "Payload Too Large",
182 .uri_too_long => "URI Too Long",
183 .unsupported_media_type => "Unsupported Media Type",
184 .range_not_satisfiable => "Range Not Satisfiable",
185 .expectation_failed => "Expectation Failed",
186 .teapot => "I'm a teapot",
187 .misdirected_request => "Misdirected Request",
188 .unprocessable_entity => "Unprocessable Entity",
189 .locked => "Locked",
190 .failed_dependency => "Failed Dependency",
191 .too_early => "Too Early",
192 .upgrade_required => "Upgrade Required",
193 .precondition_required => "Precondition Required",
194 .too_many_requests => "Too Many Requests",
195 .header_fields_too_large => "Request Header Fields Too Large",
196 .unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
197
198 // 5xx statuses
199 .internal_server_error => "Internal Server Error",
200 .not_implemented => "Not Implemented",
201 .bad_gateway => "Bad Gateway",
202 .service_unavailable => "Service Unavailable",
203 .gateway_timeout => "Gateway Timeout",
204 .http_version_not_supported => "HTTP Version Not Supported",
205 .variant_also_negotiates => "Variant Also Negotiates",
206 .insufficient_storage => "Insufficient Storage",
207 .loop_detected => "Loop Detected",
208 .not_extended => "Not Extended",
209 .network_authentication_required => "Network Authentication Required",
210
211 else => return null,
212 };
213 }
214
215 pub const Class = enum {
216 informational,
217 success,
218 redirect,
219 client_error,
220 server_error,
221 };
222
223 pub fn class(self: Status) ?Class {
224 return switch (@enumToInt(self)) {
225 100...199 => .informational,
226 200...299 => .success,
227 300...399 => .redirect,
228 400...499 => .client_error,
229 500...599 => .server_error,
230 else => null,
231 };
232 }
233
234 test {
235 try std.testing.expectEqualStrings("OK", Status.ok.phrase().?);
236 try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?);
237 }
238
239 test {
240 try std.testing.expectEqual(@as(?Status.Class, Status.Class.success), Status.ok.class());
241 try std.testing.expectEqual(@as(?Status.Class, Status.Class.client_error), Status.not_found.class());
242 }
243};
244
245const std = @import("std.zig");
5246
6test {247test {
7 std.testing.refAllDecls(@This());248 _ = Client;
249 _ = Method;
250 _ = Status;
8}251}
lib/std/http/Client.zig created+114
...@@ -0,0 +1,114 @@
1const std = @import("../std.zig");
2const assert = std.debug.assert;
3const http = std.http;
4const net = std.net;
5const Client = @This();
6
7allocator: std.mem.Allocator,
8headers: std.ArrayListUnmanaged(u8) = .{},
9active_requests: usize = 0,
10
11pub const Request = struct {
12 client: *Client,
13 stream: net.Stream,
14 headers: std.ArrayListUnmanaged(u8) = .{},
15 tls: std.crypto.Tls = .{},
16 protocol: Protocol,
17
18 pub const Protocol = enum { http, https };
19
20 pub const Options = struct {
21 family: Family = .any,
22 protocol: Protocol = .https,
23 method: http.Method = .GET,
24 host: []const u8 = "localhost",
25 path: []const u8 = "/",
26 port: u16 = 0,
27
28 pub const Family = enum { any, ip4, ip6 };
29 };
30
31 pub fn deinit(req: *Request) void {
32 req.client.active_requests -= 1;
33 req.headers.deinit(req.client.allocator);
34 req.* = undefined;
35 }
36
37 pub fn addHeader(req: *Request, name: []const u8, value: []const u8) !void {
38 const gpa = req.client.allocator;
39 // Ensure an extra +2 for the \r\n in end()
40 try req.headers.ensureUnusedCapacity(gpa, name.len + value.len + 6);
41 req.headers.appendSliceAssumeCapacity(name);
42 req.headers.appendSliceAssumeCapacity(": ");
43 req.headers.appendSliceAssumeCapacity(value);
44 req.headers.appendSliceAssumeCapacity("\r\n");
45 }
46
47 pub fn end(req: *Request) !void {
48 req.headers.appendSliceAssumeCapacity("\r\n");
49 switch (req.protocol) {
50 .http => {
51 try req.stream.writeAll(req.headers.items);
52 },
53 .https => {
54 try req.tls.writeAll(req.stream, req.headers.items);
55 },
56 }
57 }
58};
59
60pub fn deinit(client: *Client) void {
61 assert(client.active_requests == 0);
62 client.headers.denit(client.allocator);
63 client.* = undefined;
64}
65
66pub fn request(client: *Client, options: Request.Options) !Request {
67 var req: Request = .{
68 .client = client,
69 .stream = try net.tcpConnectToHost(client.allocator, options.host, options.port),
70 .protocol = options.protocol,
71 };
72 errdefer req.deinit();
73
74 switch (options.protocol) {
75 .http => {},
76 .https => {
77 try req.tls.init(req.stream, options.host);
78 },
79 }
80
81 try req.headers.ensureUnusedCapacity(
82 client.allocator,
83 @tagName(options.method).len +
84 1 +
85 options.path.len +
86 " HTTP/2\r\nHost: ".len +
87 options.host.len +
88 "\r\nUpgrade-Insecure-Requests: 1\r\n".len +
89 client.headers.items.len +
90 2, // for the \r\n at the end of headers
91 );
92 req.headers.appendSliceAssumeCapacity(@tagName(options.method));
93 req.headers.appendSliceAssumeCapacity(" ");
94 req.headers.appendSliceAssumeCapacity(options.path);
95 req.headers.appendSliceAssumeCapacity(" HTTP/2\r\nHost: ");
96 req.headers.appendSliceAssumeCapacity(options.host);
97 switch (options.protocol) {
98 .https => req.headers.appendSliceAssumeCapacity("\r\nUpgrade-Insecure-Requests: 1\r\n"),
99 .http => req.headers.appendSliceAssumeCapacity("\r\n"),
100 }
101 req.headers.appendSliceAssumeCapacity(client.headers.items);
102
103 client.active_requests += 1;
104 return req;
105}
106
107pub fn addHeader(client: *Client, name: []const u8, value: []const u8) !void {
108 const gpa = client.allocator;
109 try client.headers.ensureUnusedCapacity(gpa, name.len + value.len + 4);
110 client.headers.appendSliceAssumeCapacity(name);
111 client.headers.appendSliceAssumeCapacity(": ");
112 client.headers.appendSliceAssumeCapacity(value);
113 client.headers.appendSliceAssumeCapacity("\r\n");
114}
lib/std/http/method.zig deleted-65
...@@ -1,65 +0,0 @@
1//! HTTP Methods
2//! https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
3
4// Style guide is violated here so that @tagName can be used effectively
5/// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definiton
6/// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH
7pub const Method = enum {
8 GET,
9 HEAD,
10 POST,
11 PUT,
12 DELETE,
13 CONNECT,
14 OPTIONS,
15 TRACE,
16 PATCH,
17
18 /// Returns true if a request of this method is allowed to have a body
19 /// Actual behavior from servers may vary and should still be checked
20 pub fn requestHasBody(self: Method) bool {
21 return switch (self) {
22 .POST, .PUT, .PATCH => true,
23 .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false,
24 };
25 }
26
27 /// Returns true if a response to this method is allowed to have a body
28 /// Actual behavior from clients may vary and should still be checked
29 pub fn responseHasBody(self: Method) bool {
30 return switch (self) {
31 .GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true,
32 .HEAD, .PUT, .TRACE => false,
33 };
34 }
35
36 /// An HTTP method is safe if it doesn't alter the state of the server.
37 /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
38 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
39 pub fn safe(self: Method) bool {
40 return switch (self) {
41 .GET, .HEAD, .OPTIONS, .TRACE => true,
42 .POST, .PUT, .DELETE, .CONNECT, .PATCH => false,
43 };
44 }
45
46 /// 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.
47 /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent
48 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2
49 pub fn idempotent(self: Method) bool {
50 return switch (self) {
51 .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true,
52 .CONNECT, .POST, .PATCH => false,
53 };
54 }
55
56 /// 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.
57 /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable
58 /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3
59 pub fn cacheable(self: Method) bool {
60 return switch (self) {
61 .GET, .HEAD => true,
62 .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false,
63 };
64 }
65};
lib/std/http/status.zig deleted-182
...@@ -1,182 +0,0 @@
1//! HTTP Status
2//! https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
3
4const std = @import("../std.zig");
5
6pub const Status = enum(u10) {
7 @"continue" = 100, // RFC7231, Section 6.2.1
8 switching_protocols = 101, // RFC7231, Section 6.2.2
9 processing = 102, // RFC2518
10 early_hints = 103, // RFC8297
11
12 ok = 200, // RFC7231, Section 6.3.1
13 created = 201, // RFC7231, Section 6.3.2
14 accepted = 202, // RFC7231, Section 6.3.3
15 non_authoritative_info = 203, // RFC7231, Section 6.3.4
16 no_content = 204, // RFC7231, Section 6.3.5
17 reset_content = 205, // RFC7231, Section 6.3.6
18 partial_content = 206, // RFC7233, Section 4.1
19 multi_status = 207, // RFC4918
20 already_reported = 208, // RFC5842
21 im_used = 226, // RFC3229
22
23 multiple_choice = 300, // RFC7231, Section 6.4.1
24 moved_permanently = 301, // RFC7231, Section 6.4.2
25 found = 302, // RFC7231, Section 6.4.3
26 see_other = 303, // RFC7231, Section 6.4.4
27 not_modified = 304, // RFC7232, Section 4.1
28 use_proxy = 305, // RFC7231, Section 6.4.5
29 temporary_redirect = 307, // RFC7231, Section 6.4.7
30 permanent_redirect = 308, // RFC7538
31
32 bad_request = 400, // RFC7231, Section 6.5.1
33 unauthorized = 401, // RFC7235, Section 3.1
34 payment_required = 402, // RFC7231, Section 6.5.2
35 forbidden = 403, // RFC7231, Section 6.5.3
36 not_found = 404, // RFC7231, Section 6.5.4
37 method_not_allowed = 405, // RFC7231, Section 6.5.5
38 not_acceptable = 406, // RFC7231, Section 6.5.6
39 proxy_auth_required = 407, // RFC7235, Section 3.2
40 request_timeout = 408, // RFC7231, Section 6.5.7
41 conflict = 409, // RFC7231, Section 6.5.8
42 gone = 410, // RFC7231, Section 6.5.9
43 length_required = 411, // RFC7231, Section 6.5.10
44 precondition_failed = 412, // RFC7232, Section 4.2][RFC8144, Section 3.2
45 payload_too_large = 413, // RFC7231, Section 6.5.11
46 uri_too_long = 414, // RFC7231, Section 6.5.12
47 unsupported_media_type = 415, // RFC7231, Section 6.5.13][RFC7694, Section 3
48 range_not_satisfiable = 416, // RFC7233, Section 4.4
49 expectation_failed = 417, // RFC7231, Section 6.5.14
50 teapot = 418, // RFC 7168, 2.3.3
51 misdirected_request = 421, // RFC7540, Section 9.1.2
52 unprocessable_entity = 422, // RFC4918
53 locked = 423, // RFC4918
54 failed_dependency = 424, // RFC4918
55 too_early = 425, // RFC8470
56 upgrade_required = 426, // RFC7231, Section 6.5.15
57 precondition_required = 428, // RFC6585
58 too_many_requests = 429, // RFC6585
59 header_fields_too_large = 431, // RFC6585
60 unavailable_for_legal_reasons = 451, // RFC7725
61
62 internal_server_error = 500, // RFC7231, Section 6.6.1
63 not_implemented = 501, // RFC7231, Section 6.6.2
64 bad_gateway = 502, // RFC7231, Section 6.6.3
65 service_unavailable = 503, // RFC7231, Section 6.6.4
66 gateway_timeout = 504, // RFC7231, Section 6.6.5
67 http_version_not_supported = 505, // RFC7231, Section 6.6.6
68 variant_also_negotiates = 506, // RFC2295
69 insufficient_storage = 507, // RFC4918
70 loop_detected = 508, // RFC5842
71 not_extended = 510, // RFC2774
72 network_authentication_required = 511, // RFC6585
73
74 _,
75
76 pub fn phrase(self: Status) ?[]const u8 {
77 return switch (self) {
78 // 1xx statuses
79 .@"continue" => "Continue",
80 .switching_protocols => "Switching Protocols",
81 .processing => "Processing",
82 .early_hints => "Early Hints",
83
84 // 2xx statuses
85 .ok => "OK",
86 .created => "Created",
87 .accepted => "Accepted",
88 .non_authoritative_info => "Non-Authoritative Information",
89 .no_content => "No Content",
90 .reset_content => "Reset Content",
91 .partial_content => "Partial Content",
92 .multi_status => "Multi-Status",
93 .already_reported => "Already Reported",
94 .im_used => "IM Used",
95
96 // 3xx statuses
97 .multiple_choice => "Multiple Choice",
98 .moved_permanently => "Moved Permanently",
99 .found => "Found",
100 .see_other => "See Other",
101 .not_modified => "Not Modified",
102 .use_proxy => "Use Proxy",
103 .temporary_redirect => "Temporary Redirect",
104 .permanent_redirect => "Permanent Redirect",
105
106 // 4xx statuses
107 .bad_request => "Bad Request",
108 .unauthorized => "Unauthorized",
109 .payment_required => "Payment Required",
110 .forbidden => "Forbidden",
111 .not_found => "Not Found",
112 .method_not_allowed => "Method Not Allowed",
113 .not_acceptable => "Not Acceptable",
114 .proxy_auth_required => "Proxy Authentication Required",
115 .request_timeout => "Request Timeout",
116 .conflict => "Conflict",
117 .gone => "Gone",
118 .length_required => "Length Required",
119 .precondition_failed => "Precondition Failed",
120 .payload_too_large => "Payload Too Large",
121 .uri_too_long => "URI Too Long",
122 .unsupported_media_type => "Unsupported Media Type",
123 .range_not_satisfiable => "Range Not Satisfiable",
124 .expectation_failed => "Expectation Failed",
125 .teapot => "I'm a teapot",
126 .misdirected_request => "Misdirected Request",
127 .unprocessable_entity => "Unprocessable Entity",
128 .locked => "Locked",
129 .failed_dependency => "Failed Dependency",
130 .too_early => "Too Early",
131 .upgrade_required => "Upgrade Required",
132 .precondition_required => "Precondition Required",
133 .too_many_requests => "Too Many Requests",
134 .header_fields_too_large => "Request Header Fields Too Large",
135 .unavailable_for_legal_reasons => "Unavailable For Legal Reasons",
136
137 // 5xx statuses
138 .internal_server_error => "Internal Server Error",
139 .not_implemented => "Not Implemented",
140 .bad_gateway => "Bad Gateway",
141 .service_unavailable => "Service Unavailable",
142 .gateway_timeout => "Gateway Timeout",
143 .http_version_not_supported => "HTTP Version Not Supported",
144 .variant_also_negotiates => "Variant Also Negotiates",
145 .insufficient_storage => "Insufficient Storage",
146 .loop_detected => "Loop Detected",
147 .not_extended => "Not Extended",
148 .network_authentication_required => "Network Authentication Required",
149
150 else => return null,
151 };
152 }
153
154 pub const Class = enum {
155 informational,
156 success,
157 redirect,
158 client_error,
159 server_error,
160 };
161
162 pub fn class(self: Status) ?Class {
163 return switch (@enumToInt(self)) {
164 100...199 => .informational,
165 200...299 => .success,
166 300...399 => .redirect,
167 400...499 => .client_error,
168 500...599 => .server_error,
169 else => null,
170 };
171 }
172};
173
174test {
175 try std.testing.expectEqualStrings("OK", Status.ok.phrase().?);
176 try std.testing.expectEqualStrings("Not Found", Status.not_found.phrase().?);
177}
178
179test {
180 try std.testing.expectEqual(@as(?Status.Class, Status.Class.success), Status.ok.class());
181 try std.testing.expectEqual(@as(?Status.Class, Status.Class.client_error), Status.not_found.class());
182}
lib/std/net.zig+7
...@@ -1687,6 +1687,13 @@ pub const Stream = struct {...@@ -1687,6 +1687,13 @@ pub const Stream = struct {
1687 }1687 }
1688 }1688 }
16891689
1690 pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void {
1691 var index: usize = 0;
1692 while (index < bytes.len) {
1693 index += try self.write(bytes[index..]);
1694 }
1695 }
1696
1690 /// See https://github.com/ziglang/zig/issues/76991697 /// See https://github.com/ziglang/zig/issues/7699
1691 /// See equivalent function: `std.fs.File.writev`.1698 /// See equivalent function: `std.fs.File.writev`.
1692 pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {1699 pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize {