authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-04 18:28:37-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-04 18:37:53-07:00
log8248fdbbdb88cc861c3d02a26ec4a214df3f9a1e
tree1064a9a537b57cf83fe9a496594e831eff188585
parent079f62881ee8291e1b290848c1081c822ea8389f

std.http.Client: support HTTP redirects

* std.http.Status.Class: add a "nonstandard" enum tag. Instead of having `class` return an optional value, it can potentially return nonstandard. * extract out std.http.Client.Connection from std.http.Client.Request - this code abstracts over plain/TLS only - this is the type that will potentially be stored in a client's LRU connection map * introduce two-staged HTTP header parsing - API users can rely on a heap-allocated buffer with a maximum limit, which defaults to 16 KB, or they can provide a static buffer that is borrowed by the Request instance. - The entire HTTP header is buffered because there are strings in there and they must be accessed later, such as with the case of HTTP redirects. - When buffering the HTTP header, the parser only looks for the \r\n\r\n pattern. Further validation is done later. - After the full HTTP header is buffered, it is parsed into components such as Content-Length and Location. * HTTP redirects are handled, with a maximum redirect count option that defaults to 3. - Connection: close is always used for now; implementing keep-alive connections and an LRU connection pool in std.http.Client is a task for another day. see #2007

3 files changed, 470 insertions(+), 263 deletions(-)

lib/std/http.zig+3-4
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1pub const Client = @import("http/Client.zig");1pub const Client = @import("http/Client.zig");
2pub const Headers = @import("http/Headers.zig");
32
4pub const Version = enum {3pub const Version = enum {
5 @"HTTP/1.0",4 @"HTTP/1.0",
...@@ -219,6 +218,7 @@ pub const Status = enum(u10) {...@@ -219,6 +218,7 @@ pub const Status = enum(u10) {
219 }218 }
220219
221 pub const Class = enum {220 pub const Class = enum {
221 nonstandard,
222 informational,222 informational,
223 success,223 success,
224 redirect,224 redirect,
...@@ -226,14 +226,14 @@ pub const Status = enum(u10) {...@@ -226,14 +226,14 @@ pub const Status = enum(u10) {
226 server_error,226 server_error,
227 };227 };
228228
229 pub fn class(self: Status) ?Class {229 pub fn class(self: Status) Class {
230 return switch (@enumToInt(self)) {230 return switch (@enumToInt(self)) {
231 100...199 => .informational,231 100...199 => .informational,
232 200...299 => .success,232 200...299 => .success,
233 300...399 => .redirect,233 300...399 => .redirect,
234 400...499 => .client_error,234 400...499 => .client_error,
235 500...599 => .server_error,235 500...599 => .server_error,
236 else => null,236 else => .nonstandard,
237 };237 };
238 }238 }
239239
...@@ -254,5 +254,4 @@ test {...@@ -254,5 +254,4 @@ test {
254 _ = Client;254 _ = Client;
255 _ = Method;255 _ = Method;
256 _ = Status;256 _ = Status;
257 _ = Headers;
258}257}
lib/std/http/Client.zig+467-66
...@@ -1,45 +1,419 @@...@@ -1,45 +1,419 @@
1//! This API is a barely-touched, barely-functional http client, just the1//! This API is a barely-touched, barely-functional http client, just the
2//! absolute minimum thing I needed in order to test `std.crypto.tls`. Bear2//! absolute minimum thing I needed in order to test `std.crypto.tls`. Bear
3//! with me and I promise the API will become useful and streamlined.3//! with me and I promise the API will become useful and streamlined.
4//!
5//! TODO: send connection: keep-alive and LRU cache a configurable number of
6//! open connections to skip DNS and TLS handshake for subsequent requests.
47
5const std = @import("../std.zig");8const std = @import("../std.zig");
9const mem = std.mem;
6const assert = std.debug.assert;10const assert = std.debug.assert;
7const http = std.http;11const http = std.http;
8const net = std.net;12const net = std.net;
9const Client = @This();13const Client = @This();
10const Url = std.Url;14const Url = std.Url;
15const Allocator = std.mem.Allocator;
16const testing = std.testing;
1117
12/// TODO: remove this field (currently required due to tcpConnectToHost)18/// Used for tcpConnectToHost and storing HTTP headers when an externally
13allocator: std.mem.Allocator,19/// managed buffer is not provided.
20allocator: Allocator,
14ca_bundle: std.crypto.Certificate.Bundle = .{},21ca_bundle: std.crypto.Certificate.Bundle = .{},
1522
23pub const Connection = struct {
24 stream: net.Stream,
25 /// undefined unless protocol is tls.
26 tls_client: std.crypto.tls.Client,
27 protocol: Protocol,
28
29 pub const Protocol = enum { plain, tls };
30
31 pub fn read(conn: *Connection, buffer: []u8) !usize {
32 switch (conn.protocol) {
33 .plain => return conn.stream.read(buffer),
34 .tls => return conn.tls_client.read(conn.stream, buffer),
35 }
36 }
37
38 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {
39 switch (conn.protocol) {
40 .plain => return conn.stream.readAtLeast(buffer, len),
41 .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),
42 }
43 }
44
45 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
46 switch (conn.protocol) {
47 .plain => return conn.stream.writeAll(buffer),
48 .tls => return conn.tls_client.writeAll(conn.stream, buffer),
49 }
50 }
51
52 pub fn write(conn: *Connection, buffer: []const u8) !usize {
53 switch (conn.protocol) {
54 .plain => return conn.stream.write(buffer),
55 .tls => return conn.tls_client.write(conn.stream, buffer),
56 }
57 }
58};
59
16/// TODO: emit error.UnexpectedEndOfStream or something like that when the read60/// TODO: emit error.UnexpectedEndOfStream or something like that when the read
17/// data does not match the content length. This is necessary since HTTPS disables61/// data does not match the content length. This is necessary since HTTPS disables
18/// close_notify protection on underlying TLS streams.62/// close_notify protection on underlying TLS streams.
19pub const Request = struct {63pub const Request = struct {
20 client: *Client,64 client: *Client,
21 stream: net.Stream,65 connection: Connection,
22 tls_client: std.crypto.tls.Client,
23 protocol: Protocol,
24 response_headers: http.Headers,
25 redirects_left: u32,66 redirects_left: u32,
67 response: Response,
68 /// These are stored in Request so that they are available when following
69 /// redirects.
70 headers: Headers,
2671
27 pub const Headers = struct {72 pub const Response = struct {
28 method: http.Method = .GET,73 headers: Response.Headers,
29 connection: Connection,74 state: State,
75 header_bytes_owned: bool,
76 /// This could either be a fixed buffer provided by the API user or it
77 /// could be our own array list.
78 header_bytes: std.ArrayListUnmanaged(u8),
79 max_header_bytes: usize,
80
81 pub const Headers = struct {
82 location: ?[]const u8 = null,
83 status: http.Status,
84 version: http.Version,
85 content_length: ?u64 = null,
86
87 pub fn parse(bytes: []const u8) !Response.Headers {
88 var it = mem.split(u8, bytes[0 .. bytes.len - 4], "\r\n");
3089
31 pub const Connection = enum {90 const first_line = it.first();
32 close,91 if (first_line.len < 12)
33 @"keep-alive",92 return error.ShortHttpStatusLine;
93
94 const version: http.Version = switch (int64(first_line[0..8])) {
95 int64("HTTP/1.0") => .@"HTTP/1.0",
96 int64("HTTP/1.1") => .@"HTTP/1.1",
97 else => return error.BadHttpVersion,
98 };
99 if (first_line[8] != ' ') return error.InvalidHttpHeaders;
100 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
101
102 var headers: Response.Headers = .{
103 .version = version,
104 .status = status,
105 };
106
107 while (it.next()) |line| {
108 var line_it = mem.split(u8, line, ": ");
109 const header_name = line_it.first();
110 const header_value = line_it.rest();
111 if (std.ascii.eqlIgnoreCase(header_name, "location")) {
112 headers.location = header_value;
113 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
114 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
115 }
116 }
117
118 return headers;
119 }
120
121 test "parse headers" {
122 const example =
123 "HTTP/1.1 301 Moved Permanently\r\n" ++
124 "Location: https://www.example.com/\r\n" ++
125 "Content-Type: text/html; charset=UTF-8\r\n" ++
126 "Content-Length: 220\r\n\r\n";
127 const parsed = try Response.Headers.parse(example);
128 try testing.expectEqual(http.Version.@"HTTP/1.1", parsed.version);
129 try testing.expectEqual(http.Status.moved_permanently, parsed.status);
130 try testing.expectEqualStrings("https://www.example.com/", parsed.location orelse
131 return error.TestFailed);
132 try testing.expectEqual(@as(?u64, 220), parsed.content_length);
133 }
134 };
135
136 pub const State = enum {
137 invalid,
138 finished,
139 start,
140 seen_r,
141 seen_rn,
142 seen_rnr,
34 };143 };
144
145 pub fn initDynamic(max: usize) Response {
146 return .{
147 .state = .start,
148 .headers = undefined,
149 .header_bytes = .{},
150 .max_header_bytes = max,
151 .header_bytes_owned = true,
152 };
153 }
154
155 pub fn initStatic(buf: []u8) Response {
156 return .{
157 .state = .start,
158 .headers = undefined,
159 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },
160 .max_header_bytes = buf.len,
161 .header_bytes_owned = false,
162 };
163 }
164
165 /// Returns how many bytes are part of HTTP headers. Always less than or
166 /// equal to bytes.len. If the amount returned is less than bytes.len, it
167 /// means the headers ended and the first byte after the double \r\n\r\n is
168 /// located at `bytes[result]`.
169 pub fn findHeadersEnd(r: *Response, bytes: []const u8) usize {
170 var index: usize = 0;
171
172 // TODO: https://github.com/ziglang/zig/issues/8220
173 state: while (true) {
174 switch (r.state) {
175 .invalid => unreachable,
176 .finished => unreachable,
177 .start => while (true) {
178 switch (bytes.len - index) {
179 0 => return index,
180 1 => {
181 if (bytes[index] == '\r')
182 r.state = .seen_r;
183 return index + 1;
184 },
185 2 => {
186 if (int16(bytes[index..][0..2]) == int16("\r\n")) {
187 r.state = .seen_rn;
188 } else if (bytes[index + 1] == '\r') {
189 r.state = .seen_r;
190 }
191 return index + 2;
192 },
193 3 => {
194 if (int16(bytes[index..][0..2]) == int16("\r\n") and
195 bytes[index + 2] == '\r')
196 {
197 r.state = .seen_rnr;
198 } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n")) {
199 r.state = .seen_rn;
200 } else if (bytes[index + 2] == '\r') {
201 r.state = .seen_r;
202 }
203 return index + 3;
204 },
205 4...15 => {
206 if (int32(bytes[index..][0..4]) == int32("\r\n\r\n")) {
207 r.state = .finished;
208 return index + 4;
209 } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n") and
210 bytes[index + 3] == '\r')
211 {
212 r.state = .seen_rnr;
213 index += 4;
214 continue :state;
215 } else if (int16(bytes[index + 2 ..][0..2]) == int16("\r\n")) {
216 r.state = .seen_rn;
217 index += 4;
218 continue :state;
219 } else if (bytes[index + 3] == '\r') {
220 r.state = .seen_r;
221 index += 4;
222 continue :state;
223 }
224 index += 4;
225 continue;
226 },
227 else => {
228 const chunk = bytes[index..][0..16];
229 const v: @Vector(16, u8) = chunk.*;
230 const matches_r = v == @splat(16, @as(u8, '\r'));
231 const iota = std.simd.iota(u8, 16);
232 const default = @splat(16, @as(u8, 16));
233 const sub_index = @reduce(.Min, @select(u8, matches_r, iota, default));
234 switch (sub_index) {
235 0...12 => {
236 index += sub_index + 4;
237 if (int32(chunk[sub_index..][0..4]) == int32("\r\n\r\n")) {
238 r.state = .finished;
239 return index;
240 }
241 continue;
242 },
243 13 => {
244 index += 16;
245 if (int16(chunk[14..][0..2]) == int16("\n\r")) {
246 r.state = .seen_rnr;
247 continue :state;
248 }
249 continue;
250 },
251 14 => {
252 index += 16;
253 if (chunk[15] == '\n') {
254 r.state = .seen_rn;
255 continue :state;
256 }
257 continue;
258 },
259 15 => {
260 r.state = .seen_r;
261 index += 16;
262 continue :state;
263 },
264 16 => {
265 index += 16;
266 continue;
267 },
268 else => unreachable,
269 }
270 },
271 }
272 },
273
274 .seen_r => switch (bytes.len - index) {
275 0 => return index,
276 1 => {
277 switch (bytes[index]) {
278 '\n' => r.state = .seen_rn,
279 '\r' => r.state = .seen_r,
280 else => r.state = .start,
281 }
282 return index + 1;
283 },
284 2 => {
285 if (int16(bytes[index..][0..2]) == int16("\n\r")) {
286 r.state = .seen_rnr;
287 return index + 2;
288 }
289 r.state = .start;
290 return index + 2;
291 },
292 else => {
293 if (int16(bytes[index..][0..2]) == int16("\n\r") and
294 bytes[index + 2] == '\n')
295 {
296 r.state = .finished;
297 return index + 3;
298 }
299 index += 3;
300 r.state = .start;
301 continue :state;
302 },
303 },
304 .seen_rn => switch (bytes.len - index) {
305 0 => return index,
306 1 => {
307 switch (bytes[index]) {
308 '\r' => r.state = .seen_rnr,
309 else => r.state = .start,
310 }
311 return index + 1;
312 },
313 else => {
314 if (int16(bytes[index..][0..2]) == int16("\r\n")) {
315 r.state = .finished;
316 return index + 2;
317 }
318 index += 2;
319 r.state = .start;
320 continue :state;
321 },
322 },
323 .seen_rnr => switch (bytes.len - index) {
324 0 => return index,
325 else => {
326 if (bytes[index] == '\n') {
327 r.state = .finished;
328 return index + 1;
329 }
330 index += 1;
331 r.state = .start;
332 continue :state;
333 },
334 },
335 }
336
337 return index;
338 }
339 }
340
341 fn parseInt3(nnn: @Vector(3, u8)) u10 {
342 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
343 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
344 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
345 }
346
347 test parseInt3 {
348 const expectEqual = std.testing.expectEqual;
349 try expectEqual(@as(u10, 0), parseInt3("000".*));
350 try expectEqual(@as(u10, 418), parseInt3("418".*));
351 try expectEqual(@as(u10, 999), parseInt3("999".*));
352 }
353
354 inline fn int16(array: *const [2]u8) u16 {
355 return @bitCast(u16, array.*);
356 }
357
358 inline fn int32(array: *const [4]u8) u32 {
359 return @bitCast(u32, array.*);
360 }
361
362 inline fn int64(array: *const [8]u8) u64 {
363 return @bitCast(u64, array.*);
364 }
365
366 test "find headers end basic" {
367 var buffer: [1]u8 = undefined;
368 var r = Response.initStatic(&buffer);
369 try testing.expectEqual(@as(usize, 10), r.findHeadersEnd("HTTP/1.1 4"));
370 try testing.expectEqual(@as(usize, 2), r.findHeadersEnd("18"));
371 try testing.expectEqual(@as(usize, 8), r.findHeadersEnd(" lol\r\n\r\nblah blah"));
372 }
373
374 test "find headers end vectorized" {
375 var buffer: [1]u8 = undefined;
376 var r = Response.initStatic(&buffer);
377 const example =
378 "HTTP/1.1 301 Moved Permanently\r\n" ++
379 "Location: https://www.example.com/\r\n" ++
380 "Content-Type: text/html; charset=UTF-8\r\n" ++
381 "Content-Length: 220\r\n" ++
382 "\r\ncontent";
383 try testing.expectEqual(@as(usize, 131), r.findHeadersEnd(example));
384 }
35 };385 };
36386
37 pub const Protocol = enum { http, https };387 pub const Headers = struct {
388 method: http.Method = .GET,
389 };
38390
39 pub const Options = struct {391 pub const Options = struct {
40 max_redirects: u32 = 3,392 max_redirects: u32 = 3,
393 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
394
395 pub const HeaderStrategy = union(enum) {
396 /// In this case, the client's Allocator will be used to store the
397 /// entire HTTP header. This value is the maximum total size of
398 /// HTTP headers allowed, otherwise
399 /// error.HttpHeadersExceededSizeLimit is returned from read().
400 dynamic: usize,
401 /// This is used to store the entire HTTP header. If the HTTP
402 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
403 /// is returned from read(). When this is used, `error.OutOfMemory`
404 /// cannot be returned from `read()`.
405 static: []u8,
406 };
41 };407 };
42408
409 /// May be skipped if header strategy is buffer.
410 pub fn deinit(req: *Request) void {
411 if (req.response.header_bytes_owned) {
412 req.response.header_bytes.deinit(req.client.allocator);
413 }
414 req.* = undefined;
415 }
416
43 pub fn readAll(req: *Request, buffer: []u8) !usize {417 pub fn readAll(req: *Request, buffer: []u8) !usize {
44 return readAtLeast(req, buffer, buffer.len);418 return readAtLeast(req, buffer, buffer.len);
45 }419 }
...@@ -52,7 +426,7 @@ pub const Request = struct {...@@ -52,7 +426,7 @@ pub const Request = struct {
52 assert(len <= buffer.len);426 assert(len <= buffer.len);
53 var index: usize = 0;427 var index: usize = 0;
54 while (index < len) {428 while (index < len) {
55 const headers_finished = req.response_headers.state == .finished;429 const headers_finished = req.response.state == .finished;
56 const amt = try readAdvanced(req, buffer[index..]);430 const amt = try readAdvanced(req, buffer[index..]);
57 if (amt == 0 and headers_finished) break;431 if (amt == 0 and headers_finished) break;
58 index += amt;432 index += amt;
...@@ -63,67 +437,102 @@ pub const Request = struct {...@@ -63,67 +437,102 @@ pub const Request = struct {
63 /// This one can return 0 without meaning EOF.437 /// This one can return 0 without meaning EOF.
64 /// TODO change to readvAdvanced438 /// TODO change to readvAdvanced
65 pub fn readAdvanced(req: *Request, buffer: []u8) !usize {439 pub fn readAdvanced(req: *Request, buffer: []u8) !usize {
66 if (req.response_headers.state == .finished) return readRaw(req, buffer);440 if (req.response.state == .finished) return req.connection.read(buffer);
67441
68 const amt = try readRaw(req, buffer);442 const amt = try req.connection.read(buffer);
69 const data = buffer[0..amt];443 const data = buffer[0..amt];
70 const i = req.response_headers.feed(data);444 const i = req.response.findHeadersEnd(data);
71 if (req.response_headers.state == .invalid) return error.InvalidHttpHeaders;445 if (req.response.state == .invalid) return error.InvalidHttpHeaders;
72 if (i < data.len) {446
73 const rest = data[i..];447 const headers_data = data[0..i];
74 std.mem.copy(u8, buffer, rest);448 if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) {
75 return rest.len;449 return error.HttpHeadersExceededSizeLimit;
76 }450 }
77 return 0;451 try req.response.header_bytes.appendSlice(req.client.allocator, headers_data);
78 }
79452
80 /// Only abstracts over http/https.453 if (req.response.state == .finished) {
81 fn readRaw(req: *Request, buffer: []u8) !usize {454 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);
82 switch (req.protocol) {455 }
83 .http => return req.stream.read(buffer),456
84 .https => return req.tls_client.read(req.stream, buffer),457 if (req.response.headers.status.class() == .redirect) {
458 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
459 const location = req.response.headers.location orelse
460 return error.HttpRedirectMissingLocation;
461 const new_url = try std.Url.parse(location);
462 const new_req = try req.client.request(new_url, req.headers, .{
463 .max_redirects = req.redirects_left - 1,
464 .header_strategy = if (req.response.header_bytes_owned) .{
465 .dynamic = req.response.max_header_bytes,
466 } else .{
467 .static = req.response.header_bytes.unusedCapacitySlice(),
468 },
469 });
470 req.deinit();
471 req.* = new_req;
472 return readAdvanced(req, buffer);
85 }473 }
86 }
87474
88 /// Only abstracts over http/https.475 const body_data = data[i..];
89 fn readAtLeastRaw(req: *Request, buffer: []u8, len: usize) !usize {476 if (body_data.len > 0) {
90 switch (req.protocol) {477 mem.copy(u8, buffer, body_data);
91 .http => return req.stream.readAtLeast(buffer, len),478 return body_data.len;
92 .https => return req.tls_client.readAtLeast(req.stream, buffer, len),
93 }479 }
480 return 0;
481 }
482
483 test {
484 _ = Response;
94 }485 }
95};486};
96487
97pub fn deinit(client: *Client, gpa: std.mem.Allocator) void {488pub fn deinit(client: *Client, gpa: Allocator) void {
98 client.ca_bundle.deinit(gpa);489 client.ca_bundle.deinit(gpa);
99 client.* = undefined;490 client.* = undefined;
100}491}
101492
493pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !Connection {
494 var conn: Connection = .{
495 .stream = try net.tcpConnectToHost(client.allocator, host, port),
496 .tls_client = undefined,
497 .protocol = protocol,
498 };
499
500 switch (protocol) {
501 .plain => {},
502 .tls => {
503 conn.tls_client = try std.crypto.tls.Client.init(conn.stream, client.ca_bundle, host);
504 // This is appropriate for HTTPS because the HTTP headers contain
505 // the content length which is used to detect truncation attacks.
506 conn.tls_client.allow_truncation_attacks = true;
507 },
508 }
509
510 return conn;
511}
512
102pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Request.Options) !Request {513pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Request.Options) !Request {
103 const protocol = std.meta.stringToEnum(Request.Protocol, url.scheme) orelse514 const protocol: Connection.Protocol = if (mem.eql(u8, url.scheme, "http"))
515 .plain
516 else if (mem.eql(u8, url.scheme, "https"))
517 .tls
518 else
104 return error.UnsupportedUrlScheme;519 return error.UnsupportedUrlScheme;
520
105 const port: u16 = url.port orelse switch (protocol) {521 const port: u16 = url.port orelse switch (protocol) {
106 .http => 80,522 .plain => 80,
107 .https => 443,523 .tls => 443,
108 };524 };
109525
110 var req: Request = .{526 var req: Request = .{
111 .client = client,527 .client = client,
112 .stream = try net.tcpConnectToHost(client.allocator, url.host, port),528 .headers = headers,
113 .protocol = protocol,529 .connection = try client.connect(url.host, port, protocol),
114 .tls_client = undefined,
115 .redirects_left = options.max_redirects,530 .redirects_left = options.max_redirects,
116 };531 .response = switch (options.header_strategy) {
117532 .dynamic => |max| Request.Response.initDynamic(max),
118 switch (protocol) {533 .static => |buf| Request.Response.initStatic(buf),
119 .http => {},
120 .https => {
121 req.tls_client = try std.crypto.tls.Client.init(req.stream, client.ca_bundle, url.host);
122 // This is appropriate for HTTPS because the HTTP headers contain
123 // the content length which is used to detect truncation attacks.
124 req.tls_client.allow_truncation_attacks = true;
125 },534 },
126 }535 };
127536
128 {537 {
129 var h = try std.BoundedArray(u8, 1000).init(0);538 var h = try std.BoundedArray(u8, 1000).init(0);
...@@ -132,23 +541,15 @@ pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Req...@@ -132,23 +541,15 @@ pub fn request(client: *Client, url: Url, headers: Request.Headers, options: Req
132 try h.appendSlice(url.path);541 try h.appendSlice(url.path);
133 try h.appendSlice(" HTTP/1.1\r\nHost: ");542 try h.appendSlice(" HTTP/1.1\r\nHost: ");
134 try h.appendSlice(url.host);543 try h.appendSlice(url.host);
135 switch (protocol) {544 try h.appendSlice("\r\nConnection: close\r\n\r\n");
136 .https => try h.appendSlice("\r\nUpgrade-Insecure-Requests: 1\r\n"),
137 .http => try h.appendSlice("\r\n"),
138 }
139 try h.writer().print("Connection: {s}\r\n", .{@tagName(headers.connection)});
140 try h.appendSlice("\r\n");
141545
142 const header_bytes = h.slice();546 const header_bytes = h.slice();
143 switch (req.protocol) {547 try req.connection.writeAll(header_bytes);
144 .http => {
145 try req.stream.writeAll(header_bytes);
146 },
147 .https => {
148 try req.tls_client.writeAll(req.stream, header_bytes);
149 },
150 }
151 }548 }
152549
153 return req;550 return req;
154}551}
552
553test {
554 _ = Request;
555}
lib/std/http/Headers.zig deleted-193
...@@ -1,193 +0,0 @@
1status: http.Status,
2version: http.Version,
3
4pub const Parser = struct {
5 state: State,
6 headers: Headers,
7 buffer: [16]u8,
8 buffer_index: u4,
9
10 pub const init: Parser = .{
11 .state = .start,
12 .headers = .{
13 .status = undefined,
14 .version = undefined,
15 },
16 .buffer = undefined,
17 .buffer_index = 0,
18 };
19
20 pub const State = enum {
21 invalid,
22 finished,
23 start,
24 expect_status,
25 find_start_line_end,
26 line,
27 line_r,
28 };
29
30 /// Returns how many bytes are processed into headers. Always less than or
31 /// equal to bytes.len. If the amount returned is less than bytes.len, it
32 /// means the headers ended and the first byte after the double \r\n\r\n is
33 /// located at `bytes[result]`.
34 pub fn feed(p: *Parser, bytes: []const u8) usize {
35 var index: usize = 0;
36
37 while (bytes.len - index >= 16) {
38 index += p.feed16(bytes[index..][0..16]);
39 switch (p.state) {
40 .invalid, .finished => return index,
41 else => continue,
42 }
43 }
44
45 while (index < bytes.len) {
46 var buffer = [1]u8{0} ** 16;
47 const src = bytes[index..bytes.len];
48 std.mem.copy(u8, &buffer, src);
49 index += p.feed16(&buffer);
50 switch (p.state) {
51 .invalid, .finished => return index,
52 else => continue,
53 }
54 }
55
56 return index;
57 }
58
59 pub fn feed16(p: *Parser, chunk: *const [16]u8) u8 {
60 switch (p.state) {
61 .invalid, .finished => return 0,
62 .start => {
63 p.headers.version = switch (std.mem.readIntNative(u64, chunk[0..8])) {
64 std.mem.readIntNative(u64, "HTTP/1.0") => .@"HTTP/1.0",
65 std.mem.readIntNative(u64, "HTTP/1.1") => .@"HTTP/1.1",
66 else => return invalid(p, 0),
67 };
68 p.state = .expect_status;
69 return 8;
70 },
71 .expect_status => {
72 // example: " 200 OK\r\n"
73 // example; " 301 Moved Permanently\r\n"
74 switch (std.mem.readIntNative(u64, chunk[0..8])) {
75 std.mem.readIntNative(u64, " 200 OK\r") => {
76 if (chunk[8] != '\n') return invalid(p, 8);
77 p.headers.status = .ok;
78 p.state = .line;
79 return 9;
80 },
81 std.mem.readIntNative(u64, " 301 Mov") => {
82 p.headers.status = .moved_permanently;
83 if (!std.mem.eql(u8, chunk[9..], "ed Perma"))
84 return invalid(p, 9);
85 p.state = .find_start_line_end;
86 return 16;
87 },
88 else => {
89 if (chunk[0] != ' ') return invalid(p, 0);
90 const status = std.fmt.parseInt(u10, chunk[1..][0..3], 10) catch
91 return invalid(p, 1);
92 p.headers.status = @intToEnum(http.Status, status);
93 const v: @Vector(12, u8) = chunk[4..16].*;
94 const matches_r = v == @splat(12, @as(u8, '\r'));
95 const iota = std.simd.iota(u8, 12);
96 const default = @splat(12, @as(u8, 12));
97 const index = 4 + @reduce(.Min, @select(u8, matches_r, iota, default));
98 if (index >= 15) {
99 p.state = .find_start_line_end;
100 return index;
101 }
102 if (chunk[index + 1] != '\n')
103 return invalid(p, index + 1);
104 p.state = .line;
105 return index + 2;
106 },
107 }
108 },
109 .find_start_line_end => {
110 const v: @Vector(16, u8) = chunk.*;
111 const matches_r = v == @splat(16, @as(u8, '\r'));
112 const iota = std.simd.iota(u8, 16);
113 const default = @splat(16, @as(u8, 16));
114 const index = @reduce(.Min, @select(u8, matches_r, iota, default));
115 if (index >= 15) {
116 p.state = .find_start_line_end;
117 return index;
118 }
119 if (chunk[index + 1] != '\n')
120 return invalid(p, index + 1);
121 p.state = .line;
122 return index + 2;
123 },
124 .line => {
125 const v: @Vector(16, u8) = chunk.*;
126 const matches_r = v == @splat(16, @as(u8, '\r'));
127 const iota = std.simd.iota(u8, 16);
128 const default = @splat(16, @as(u8, 16));
129 const index = @reduce(.Min, @select(u8, matches_r, iota, default));
130 if (index >= 15) {
131 return index;
132 }
133 if (chunk[index + 1] != '\n')
134 return invalid(p, index + 1);
135 if (index + 4 <= 16 and chunk[index + 2] == '\r') {
136 if (chunk[index + 3] != '\n') return invalid(p, index + 3);
137 p.state = .finished;
138 return index + 4;
139 }
140 p.state = .line_r;
141 return index + 2;
142 },
143 .line_r => {
144 if (chunk[0] == '\r') {
145 if (chunk[1] != '\n') return invalid(p, 1);
146 p.state = .finished;
147 return 2;
148 }
149 p.state = .line;
150 // Here would be nice to use this proposal when it is implemented:
151 // https://github.com/ziglang/zig/issues/8220
152 return 0;
153 },
154 }
155 }
156
157 fn invalid(p: *Parser, i: u8) u8 {
158 p.state = .invalid;
159 return i;
160 }
161};
162
163const std = @import("../std.zig");
164const http = std.http;
165const Headers = @This();
166const testing = std.testing;
167
168test "status line ok" {
169 var p = Parser.init;
170 const line = "HTTP/1.1 200 OK\r\n";
171 try testing.expect(p.feed(line) == line.len);
172 try testing.expectEqual(Parser.State.line, p.state);
173 try testing.expect(p.headers.version == .@"HTTP/1.1");
174 try testing.expect(p.headers.status == .ok);
175}
176
177test "status line non hot path long msg" {
178 var p = Parser.init;
179 const line = "HTTP/1.0 418 I'm a teapot\r\n";
180 try testing.expect(p.feed(line) == line.len);
181 try testing.expectEqual(Parser.State.line, p.state);
182 try testing.expect(p.headers.version == .@"HTTP/1.0");
183 try testing.expect(p.headers.status == .teapot);
184}
185
186test "status line non hot path short msg" {
187 var p = Parser.init;
188 const line = "HTTP/1.1 418 lol\r\n";
189 try testing.expect(p.feed(line) == line.len);
190 try testing.expectEqual(Parser.State.line, p.state);
191 try testing.expect(p.headers.version == .@"HTTP/1.1");
192 try testing.expect(p.headers.status == .teapot);
193}