authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-13 12:46:58-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-08 09:59:35-05:00
log08bdaf3bd650ec7682f1424c3644fc3b762ccf27
tree733f2b571bfbe39c3c78d1548a27c5bc7104476c
parentfde05b10b3c29b914e4d2ef034dcd8a78800ef6e
signaturelock-open Commit is signed but in an unrecognized format.

std.http: add http server

* extract http protocol into protocol.zig, as it is shared between client and server * coalesce Request and Response back into Client.zig, they don't contain any large chunks of code anymore * http.Server is implemented as basic as possible, a simple example below: ```zig fn handler(res: *Server.Response) !void { while (true) { defer res.reset(); try res.waitForCompleteHead(); res.headers.transfer_encoding = .{ .content_length = 14 }; res.headers.connection = res.request.headers.connection; try res.sendResponseHead(); _ = try res.write("Hello, World!\n"); if (res.connection.closing) break; } } pub fn main() !void { var server = Server.init(std.heap.page_allocator, .{ .reuse_address = true }); defer server.deinit(); try server.listen(try net.Address.parseIp("127.0.0.1", 8080)); while (true) { const res = try server.accept(.{ .dynamic = 8192 }); const thread = try std.Thread.spawn(.{}, handler, .{res}); thread.detach(); } } ```

6 files changed, 1626 insertions(+), 740 deletions(-)

lib/std/http.zig+2
...@@ -1,4 +1,6 @@...@@ -1,4 +1,6 @@
1pub const Client = @import("http/Client.zig");1pub const Client = @import("http/Client.zig");
2pub const Server = @import("http/Server.zig");
3pub const protocol = @import("http/protocol.zig");
24
3pub const Version = enum {5pub const Version = enum {
4 @"HTTP/1.0",6 @"HTTP/1.0",
lib/std/http/Client.zig+414-24
...@@ -1,23 +1,19 @@...@@ -1,23 +1,19 @@
1//! TODO: send connection: keep-alive and LRU cache a configurable number of1//! Connecting and opening requests are threadsafe. Individual requests are not.
2//! open connections to skip DNS and TLS handshake for subsequent requests.
3//!
4//! This API is *not* thread safe.
52
6const std = @import("../std.zig");3const std = @import("../std.zig");
7const mem = std.mem;4const testing = std.testing;
8const assert = std.debug.assert;
9const http = std.http;5const http = std.http;
6const mem = std.mem;
10const net = std.net;7const net = std.net;
11const Client = @This();
12const Uri = std.Uri;8const Uri = std.Uri;
13const Allocator = std.mem.Allocator;9const Allocator = mem.Allocator;
14const testing = std.testing;10const assert = std.debug.assert;
1511
16pub const Request = @import("Client/Request.zig");12const Client = @This();
17pub const Response = @import("Client/Response.zig");13const proto = @import("protocol.zig");
1814
19pub const default_connection_pool_size = 32;15pub const default_connection_pool_size = 32;
20const connection_pool_size = std.options.http_connection_pool_size;16pub const connection_pool_size = std.options.http_connection_pool_size;
2117
22/// Used for tcpConnectToHost and storing HTTP headers when an externally18/// Used for tcpConnectToHost and storing HTTP headers when an externally
23/// managed buffer is not provided.19/// managed buffer is not provided.
...@@ -43,7 +39,7 @@ pub const ConnectionPool = struct {...@@ -43,7 +39,7 @@ pub const ConnectionPool = struct {
43 used: Queue = .{},39 used: Queue = .{},
44 free: Queue = .{},40 free: Queue = .{},
45 free_len: usize = 0,41 free_len: usize = 0,
46 free_size: usize = default_connection_pool_size,42 free_size: usize = connection_pool_size,
4743
48 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.44 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.
49 /// If no connection is found, null is returned.45 /// If no connection is found, null is returned.
...@@ -55,7 +51,7 @@ pub const ConnectionPool = struct {...@@ -55,7 +51,7 @@ pub const ConnectionPool = struct {
55 while (next) |node| : (next = node.prev) {51 while (next) |node| : (next = node.prev) {
56 if ((node.data.protocol == .tls) != criteria.is_tls) continue;52 if ((node.data.protocol == .tls) != criteria.is_tls) continue;
57 if (node.data.port != criteria.port) continue;53 if (node.data.port != criteria.port) continue;
58 if (std.mem.eql(u8, node.data.host, criteria.host)) continue;54 if (mem.eql(u8, node.data.host, criteria.host)) continue;
5955
60 pool.acquireUnsafe(node);56 pool.acquireUnsafe(node);
61 return node;57 return node;
...@@ -137,9 +133,9 @@ pub const ConnectionPool = struct {...@@ -137,9 +133,9 @@ pub const ConnectionPool = struct {
137 }133 }
138};134};
139135
140pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw);136pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader);
141pub const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw);137pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader);
142pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.ReaderRaw, .{});138pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{});
143139
144pub const Connection = struct {140pub const Connection = struct {
145 stream: net.Stream,141 stream: net.Stream,
...@@ -220,6 +216,379 @@ pub const Connection = struct {...@@ -220,6 +216,379 @@ pub const Connection = struct {
220 }216 }
221};217};
222218
219pub const RequestTransfer = union(enum) {
220 content_length: u64,
221 chunked: void,
222 none: void,
223};
224
225pub const Compression = union(enum) {
226 deflate: DeflateDecompressor,
227 gzip: GzipDecompressor,
228 zstd: ZstdDecompressor,
229 none: void,
230};
231
232pub const Response = struct {
233 pub const Headers = struct {
234 status: http.Status,
235 version: http.Version,
236 location: ?[]const u8 = null,
237 content_length: ?u64 = null,
238 transfer_encoding: ?http.TransferEncoding = null,
239 transfer_compression: ?http.ContentEncoding = null,
240 connection: http.Connection = .close,
241 upgrade: ?[]const u8 = null,
242
243 pub const ParseError = error{
244 ShortHttpStatusLine,
245 BadHttpVersion,
246 HttpHeadersInvalid,
247 HttpHeaderContinuationsUnsupported,
248 HttpTransferEncodingUnsupported,
249 HttpConnectionHeaderUnsupported,
250 InvalidCharacter,
251 };
252
253 pub fn parse(bytes: []const u8) !Headers {
254 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
255
256 const first_line = it.next() orelse return error.HttpHeadersInvalid;
257 if (first_line.len < 12)
258 return error.ShortHttpStatusLine;
259
260 const version: http.Version = switch (int64(first_line[0..8])) {
261 int64("HTTP/1.0") => .@"HTTP/1.0",
262 int64("HTTP/1.1") => .@"HTTP/1.1",
263 else => return error.BadHttpVersion,
264 };
265 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
266 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
267
268 var headers: Headers = .{
269 .version = version,
270 .status = status,
271 };
272
273 while (it.next()) |line| {
274 if (line.len == 0) return error.HttpHeadersInvalid;
275 switch (line[0]) {
276 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
277 else => {},
278 }
279
280 var line_it = mem.tokenize(u8, line, ": ");
281 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
282 const header_value = line_it.rest();
283 if (std.ascii.eqlIgnoreCase(header_name, "location")) {
284 if (headers.location != null) return error.HttpHeadersInvalid;
285 headers.location = header_value;
286 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
287 if (headers.content_length != null) return error.HttpHeadersInvalid;
288 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
289 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
290 // Transfer-Encoding: second, first
291 // Transfer-Encoding: deflate, chunked
292 var iter = mem.splitBackwards(u8, header_value, ",");
293
294 if (iter.next()) |first| {
295 const trimmed = mem.trim(u8, first, " ");
296
297 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
298 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
299 headers.transfer_encoding = te;
300 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
301 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
302 headers.transfer_compression = ce;
303 } else {
304 return error.HttpTransferEncodingUnsupported;
305 }
306 }
307
308 if (iter.next()) |second| {
309 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
310
311 const trimmed = mem.trim(u8, second, " ");
312
313 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
314 headers.transfer_compression = ce;
315 } else {
316 return error.HttpTransferEncodingUnsupported;
317 }
318 }
319
320 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
321 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
322 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
323
324 const trimmed = mem.trim(u8, header_value, " ");
325
326 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
327 headers.transfer_compression = ce;
328 } else {
329 return error.HttpTransferEncodingUnsupported;
330 }
331 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
332 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
333 headers.connection = .keep_alive;
334 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
335 headers.connection = .close;
336 } else {
337 return error.HttpConnectionHeaderUnsupported;
338 }
339 } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
340 headers.upgrade = header_value;
341 }
342 }
343
344 return headers;
345 }
346
347 inline fn int64(array: *const [8]u8) u64 {
348 return @bitCast(u64, array.*);
349 }
350
351 fn parseInt3(nnn: @Vector(3, u8)) u10 {
352 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
353 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
354 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
355 }
356
357 test parseInt3 {
358 const expectEqual = testing.expectEqual;
359 try expectEqual(@as(u10, 0), parseInt3("000".*));
360 try expectEqual(@as(u10, 418), parseInt3("418".*));
361 try expectEqual(@as(u10, 999), parseInt3("999".*));
362 }
363 };
364
365 headers: Headers = undefined,
366 parser: proto.HeadersParser,
367 compression: Compression = .none,
368 skip: bool = false,
369};
370
371pub const Request = struct {
372 pub const Headers = struct {
373 version: http.Version = .@"HTTP/1.1",
374 method: http.Method = .GET,
375 user_agent: []const u8 = "zig (std.http)",
376 connection: http.Connection = .keep_alive,
377 transfer_encoding: RequestTransfer = .none,
378
379 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
380 };
381
382 uri: Uri,
383 client: *Client,
384 connection: *ConnectionPool.Node,
385 /// These are stored in Request so that they are available when following
386 /// redirects.
387 headers: Headers,
388
389 redirects_left: u32,
390 handle_redirects: bool,
391
392 response: Response,
393
394 /// Used as a allocator for resolving redirects locations.
395 arena: std.heap.ArenaAllocator,
396
397 /// Frees all resources associated with the request.
398 pub fn deinit(req: *Request) void {
399 switch (req.response.compression) {
400 .none => {},
401 .deflate => |*deflate| deflate.deinit(),
402 .gzip => |*gzip| gzip.deinit(),
403 .zstd => |*zstd| zstd.deinit(),
404 }
405
406 if (req.response.parser.header_bytes_owned) {
407 req.response.parser.header_bytes.deinit(req.client.allocator);
408 }
409
410 if (!req.response.parser.done) {
411 // If the response wasn't fully read, then we need to close the connection.
412 req.connection.data.closing = true;
413 req.client.connection_pool.release(req.client, req.connection);
414 }
415
416 req.arena.deinit();
417 req.* = undefined;
418 }
419
420 pub const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
421
422 pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead);
423
424 pub fn transferReader(req: *Request) TransferReader {
425 return .{ .context = req };
426 }
427
428 pub fn transferRead(req: *Request, buf: []u8) TransferReadError!usize {
429 if (req.response.parser.isComplete()) return 0;
430
431 var index: usize = 0;
432 while (index == 0) {
433 const amt = try req.response.parser.read(req.connection.data.reader(), buf[index..], req.response.skip);
434 if (amt == 0 and req.response.parser.isComplete()) break;
435 index += amt;
436 }
437
438 return index;
439 }
440
441 pub const WaitForCompleteHeadError = Connection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Response.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};
442
443 pub fn waitForCompleteHead(req: *Request) !void {
444 try req.response.parser.waitForCompleteHead(req.connection.data.reader(), req.client.allocator);
445
446 req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);
447
448 if (req.response.headers.status == .switching_protocols) {
449 req.connection.data.closing = false;
450 req.response.parser.done = true;
451 }
452
453 if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {
454 req.connection.data.closing = false;
455 } else {
456 req.connection.data.closing = true;
457 }
458
459 if (req.response.headers.transfer_encoding) |te| {
460 switch (te) {
461 .chunked => {
462 req.response.parser.next_chunk_length = 0;
463 req.response.parser.state = .chunk_head_size;
464 },
465 }
466 } else if (req.response.headers.content_length) |cl| {
467 req.response.parser.next_chunk_length = cl;
468
469 if (cl == 0) req.response.parser.done = true;
470 } else {
471 req.response.parser.done = true;
472 }
473
474 if (!req.response.parser.done) {
475 if (req.response.headers.transfer_compression) |tc| switch (tc) {
476 .compress => return error.CompressionNotSupported,
477 .deflate => req.response.compression = .{
478 .deflate = try std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()),
479 },
480 .gzip => req.response.compression = .{
481 .gzip = try std.compress.gzip.decompress(req.client.allocator, req.transferReader()),
482 },
483 .zstd => req.response.compression = .{
484 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
485 },
486 };
487 }
488
489 if (req.response.headers.status.class() == .redirect and req.handle_redirects) req.response.skip = true;
490 }
491
492 pub const ReadError = RequestError || Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, InvalidFormat, InvalidPort, UnexpectedCharacter };
493
494 pub const Reader = std.io.Reader(*Request, ReadError, read);
495
496 pub fn reader(req: *Request) Reader {
497 return .{ .context = req };
498 }
499
500 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
501 while (true) {
502 if (!req.response.parser.state.isContent()) try req.waitForCompleteHead();
503
504 if (req.handle_redirects and req.response.headers.status.class() == .redirect) {
505 assert(try req.transferRead(buffer) == 0);
506
507 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
508
509 const location = req.response.headers.location orelse
510 return error.HttpRedirectMissingLocation;
511 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
512
513 var new_arena = std.heap.ArenaAllocator.init(req.client.allocator);
514 const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator());
515 errdefer new_arena.deinit();
516
517 req.arena.deinit();
518 req.arena = new_arena;
519
520 const new_req = try req.client.request(resolved_url, req.headers, .{
521 .max_redirects = req.redirects_left - 1,
522 .header_strategy = if (req.response.parser.header_bytes_owned) .{
523 .dynamic = req.response.parser.max_header_bytes,
524 } else .{
525 .static = req.response.parser.header_bytes.items.ptr[0..req.response.parser.max_header_bytes],
526 },
527 });
528 req.deinit();
529 req.* = new_req;
530 } else {
531 break;
532 }
533 }
534
535 return switch (req.response.compression) {
536 .deflate => |*deflate| try deflate.read(buffer),
537 .gzip => |*gzip| try gzip.read(buffer),
538 .zstd => |*zstd| try zstd.read(buffer),
539 else => try req.transferRead(buffer),
540 };
541 }
542
543 pub fn readAll(req: *Request, buffer: []u8) !usize {
544 var index: usize = 0;
545 while (index < buffer.len) {
546 const amt = try read(req, buffer[index..]);
547 if (amt == 0) break;
548 index += amt;
549 }
550 return index;
551 }
552
553 pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong };
554
555 pub const Writer = std.io.Writer(*Request, WriteError, write);
556
557 pub fn writer(req: *Request) Writer {
558 return .{ .context = req };
559 }
560
561 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
562 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
563 switch (req.headers.transfer_encoding) {
564 .chunked => {
565 try req.connection.data.writer().print("{x}\r\n", .{bytes.len});
566 try req.connection.data.writeAll(bytes);
567 try req.connection.data.writeAll("\r\n");
568
569 return bytes.len;
570 },
571 .content_length => |*len| {
572 if (len.* < bytes.len) return error.MessageTooLong;
573
574 const amt = try req.connection.data.write(bytes);
575 len.* -= amt;
576 return amt;
577 },
578 .none => return error.NotWriteable,
579 }
580 }
581
582 /// Finish the body of a request. This notifies the server that you have no more data to send.
583 pub fn finish(req: *Request) !void {
584 switch (req.headers.transfer_encoding) {
585 .chunked => try req.connection.data.writeAll("0\r\n"),
586 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
587 .none => {},
588 }
589 }
590};
591
223pub fn deinit(client: *Client) void {592pub fn deinit(client: *Client) void {
224 client.connection_pool.deinit(client);593 client.connection_pool.deinit(client);
225594
...@@ -227,7 +596,7 @@ pub fn deinit(client: *Client) void {...@@ -227,7 +596,7 @@ pub fn deinit(client: *Client) void {
227 client.* = undefined;596 client.* = undefined;
228}597}
229598
230pub const ConnectError = std.mem.Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream);599pub const ConnectError = Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream);
231600
232pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {601pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
233 if (client.connection_pool.findConnection(.{602 if (client.connection_pool.findConnection(.{
...@@ -276,7 +645,26 @@ pub const RequestError = ConnectError || Connection.WriteError || error{...@@ -276,7 +645,26 @@ pub const RequestError = ConnectError || Connection.WriteError || error{
276 EndOfStream,645 EndOfStream,
277};646};
278647
279pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) RequestError!Request {648pub const Options = struct {
649 handle_redirects: bool = true,
650 max_redirects: u32 = 3,
651 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
652
653 pub const HeaderStrategy = union(enum) {
654 /// In this case, the client's Allocator will be used to store the
655 /// entire HTTP header. This value is the maximum total size of
656 /// HTTP headers allowed, otherwise
657 /// error.HttpHeadersExceededSizeLimit is returned from read().
658 dynamic: usize,
659 /// This is used to store the entire HTTP header. If the HTTP
660 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
661 /// is returned from read(). When this is used, `error.OutOfMemory`
662 /// cannot be returned from `read()`.
663 static: []u8,
664 };
665};
666
667pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {
280 const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http"))668 const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http"))
281 .plain669 .plain
282 else if (mem.eql(u8, uri.scheme, "https"))670 else if (mem.eql(u8, uri.scheme, "https"))
...@@ -304,14 +692,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req...@@ -304,14 +692,15 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req
304 var req: Request = .{692 var req: Request = .{
305 .uri = uri,693 .uri = uri,
306 .client = client,694 .client = client,
307 .headers = headers,
308 .connection = try client.connect(host, port, protocol),695 .connection = try client.connect(host, port, protocol),
696 .headers = headers,
309 .redirects_left = options.max_redirects,697 .redirects_left = options.max_redirects,
310 .handle_redirects = options.handle_redirects,698 .handle_redirects = options.handle_redirects,
311 .compression_init = false,699 .response = .{
312 .response = switch (options.header_strategy) {700 .parser = switch (options.header_strategy) {
313 .dynamic => |max| Response.initDynamic(max),701 .dynamic => |max| proto.HeadersParser.initDynamic(max),
314 .static => |buf| Response.initStatic(buf),702 .static => |buf| proto.HeadersParser.initStatic(buf),
703 },
315 },704 },
316 .arena = undefined,705 .arena = undefined,
317 };706 };
...@@ -358,6 +747,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req...@@ -358,6 +747,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req
358 try writer.writeAll("\r\nConnection: keep-alive");747 try writer.writeAll("\r\nConnection: keep-alive");
359 }748 }
360 try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");749 try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
750 try writer.writeAll("\r\nTE: trailers, gzip, deflate");
361751
362 switch (headers.transfer_encoding) {752 switch (headers.transfer_encoding) {
363 .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"),753 .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"),
lib/std/http/Client/Request.zig deleted-482
...@@ -1,482 +0,0 @@
1const std = @import("std");
2const http = std.http;
3const Uri = std.Uri;
4const mem = std.mem;
5const assert = std.debug.assert;
6
7const Client = @import("../Client.zig");
8const Connection = Client.Connection;
9const ConnectionNode = Client.ConnectionPool.Node;
10const Response = @import("Response.zig");
11
12const Request = @This();
13
14const read_buffer_size = 8192;
15const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size);
16
17uri: Uri,
18client: *Client,
19connection: *ConnectionNode,
20response: Response,
21/// These are stored in Request so that they are available when following
22/// redirects.
23headers: Headers,
24
25redirects_left: u32,
26handle_redirects: bool,
27compression_init: bool,
28
29/// Used as a allocator for resolving redirects locations.
30arena: std.heap.ArenaAllocator,
31
32/// Read buffer for the connection. This is used to pull in large amounts of data from the connection even if the user asks for a small amount. This can probably be removed with careful planning.
33read_buffer: [read_buffer_size]u8 = undefined,
34read_buffer_start: ReadBufferIndex = 0,
35read_buffer_len: ReadBufferIndex = 0,
36
37pub const RequestTransfer = union(enum) {
38 content_length: u64,
39 chunked: void,
40 none: void,
41};
42
43pub const Headers = struct {
44 version: http.Version = .@"HTTP/1.1",
45 method: http.Method = .GET,
46 user_agent: []const u8 = "zig (std.http)",
47 connection: http.Connection = .keep_alive,
48 transfer_encoding: RequestTransfer = .none,
49
50 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
51};
52
53pub const Options = struct {
54 handle_redirects: bool = true,
55 max_redirects: u32 = 3,
56 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
57
58 pub const HeaderStrategy = union(enum) {
59 /// In this case, the client's Allocator will be used to store the
60 /// entire HTTP header. This value is the maximum total size of
61 /// HTTP headers allowed, otherwise
62 /// error.HttpHeadersExceededSizeLimit is returned from read().
63 dynamic: usize,
64 /// This is used to store the entire HTTP header. If the HTTP
65 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
66 /// is returned from read(). When this is used, `error.OutOfMemory`
67 /// cannot be returned from `read()`.
68 static: []u8,
69 };
70};
71
72/// Frees all resources associated with the request.
73pub fn deinit(req: *Request) void {
74 switch (req.response.compression) {
75 .none => {},
76 .deflate => |*deflate| deflate.deinit(),
77 .gzip => |*gzip| gzip.deinit(),
78 .zstd => |*zstd| zstd.deinit(),
79 }
80
81 if (req.response.header_bytes_owned) {
82 req.response.header_bytes.deinit(req.client.allocator);
83 }
84
85 if (!req.response.done) {
86 // If the response wasn't fully read, then we need to close the connection.
87 req.connection.data.closing = true;
88 req.client.connection_pool.release(req.client, req.connection);
89 }
90
91 req.arena.deinit();
92 req.* = undefined;
93}
94
95pub const ReadRawError = Connection.ReadError || Uri.ParseError || Client.RequestError || error{
96 UnexpectedEndOfStream,
97 TooManyHttpRedirects,
98 HttpRedirectMissingLocation,
99 HttpHeadersInvalid,
100};
101
102pub const ReaderRaw = std.io.Reader(*Request, ReadRawError, readRaw);
103
104/// Read from the underlying stream, without decompressing or parsing the headers. Must be called
105/// after waitForCompleteHead() has returned successfully.
106pub fn readRaw(req: *Request, buffer: []u8) ReadRawError!usize {
107 assert(req.response.state.isContent());
108
109 var index: usize = 0;
110 while (index == 0) {
111 const amt = try req.readRawAdvanced(buffer[index..]);
112 if (amt == 0 and req.response.done) break;
113 index += amt;
114 }
115
116 return index;
117}
118
119fn checkForCompleteHead(req: *Request, buffer: []u8) !usize {
120 switch (req.response.state) {
121 .invalid => unreachable,
122 .start, .seen_r, .seen_rn, .seen_rnr => {},
123 else => return 0, // No more headers to read.
124 }
125
126 const i = req.response.findHeadersEnd(buffer[0..]);
127 if (req.response.state == .invalid) return error.HttpHeadersInvalid;
128
129 const headers_data = buffer[0..i];
130 if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) {
131 return error.HttpHeadersExceededSizeLimit;
132 }
133 try req.response.header_bytes.appendSlice(req.client.allocator, headers_data);
134
135 if (req.response.state == .finished) {
136 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);
137
138 if (req.response.headers.upgrade) |_| {
139 req.connection.data.closing = false;
140 req.response.done = true;
141 return i;
142 }
143
144 if (req.response.headers.connection == .keep_alive) {
145 req.connection.data.closing = false;
146 } else {
147 req.connection.data.closing = true;
148 }
149
150 if (req.response.headers.transfer_encoding) |transfer_encoding| {
151 switch (transfer_encoding) {
152 .chunked => {
153 req.response.next_chunk_length = 0;
154 req.response.state = .chunk_size;
155 },
156 }
157 } else if (req.response.headers.content_length) |content_length| {
158 req.response.next_chunk_length = content_length;
159
160 if (content_length == 0) req.response.done = true;
161 } else {
162 req.response.done = true;
163 }
164
165 return i;
166 }
167
168 return 0;
169}
170
171pub const WaitForCompleteHeadError = ReadRawError || error{
172 UnexpectedEndOfStream,
173
174 HttpHeadersExceededSizeLimit,
175 ShortHttpStatusLine,
176 BadHttpVersion,
177 HttpHeaderContinuationsUnsupported,
178 HttpTransferEncodingUnsupported,
179 HttpConnectionHeaderUnsupported,
180};
181
182/// Reads a complete response head. Any leftover data is stored in the request. This function is idempotent.
183pub fn waitForCompleteHead(req: *Request) WaitForCompleteHeadError!void {
184 if (req.response.state.isContent()) return;
185
186 while (true) {
187 const nread = try req.connection.data.read(req.read_buffer[0..]);
188 const amt = try checkForCompleteHead(req, req.read_buffer[0..nread]);
189
190 if (amt != 0) {
191 req.read_buffer_start = @intCast(ReadBufferIndex, amt);
192 req.read_buffer_len = @intCast(ReadBufferIndex, nread);
193 return;
194 } else if (nread == 0) {
195 return error.UnexpectedEndOfStream;
196 }
197 }
198}
199
200/// This one can return 0 without meaning EOF.
201fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
202 assert(req.response.state.isContent());
203 if (req.response.done) return 0;
204
205 // var in: []const u8 = undefined;
206 if (req.read_buffer_start == req.read_buffer_len) {
207 const nread = try req.connection.data.read(req.read_buffer[0..]);
208 if (nread == 0) return error.UnexpectedEndOfStream;
209
210 req.read_buffer_start = 0;
211 req.read_buffer_len = @intCast(ReadBufferIndex, nread);
212 }
213
214 var out_index: usize = 0;
215 while (true) {
216 switch (req.response.state) {
217 .invalid, .start, .seen_r, .seen_rn, .seen_rnr => unreachable,
218 .finished => {
219 // TODO https://github.com/ziglang/zig/issues/14039
220 const buf_avail = req.read_buffer_len - req.read_buffer_start;
221 const data_avail = req.response.next_chunk_length;
222 const out_avail = buffer.len;
223
224 if (req.handle_redirects and req.response.headers.status.class() == .redirect) {
225 const can_read = @intCast(usize, @min(buf_avail, data_avail));
226 req.response.next_chunk_length -= can_read;
227
228 if (req.response.next_chunk_length == 0) {
229 req.client.connection_pool.release(req.client, req.connection);
230 req.connection = undefined;
231 req.response.done = true;
232 }
233
234 return 0; // skip over as much data as possible
235 }
236
237 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail));
238 req.response.next_chunk_length -= can_read;
239
240 mem.copy(u8, buffer[0..], req.read_buffer[req.read_buffer_start..][0..can_read]);
241 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);
242
243 if (req.response.next_chunk_length == 0) {
244 req.client.connection_pool.release(req.client, req.connection);
245 req.connection = undefined;
246 req.response.done = true;
247 }
248
249 return can_read;
250 },
251 .chunk_size_prefix_r => switch (req.read_buffer_len - req.read_buffer_start) {
252 0 => return out_index,
253 1 => switch (req.read_buffer[req.read_buffer_start]) {
254 '\r' => {
255 req.response.state = .chunk_size_prefix_n;
256 return out_index;
257 },
258 else => {
259 req.response.state = .invalid;
260 return error.HttpHeadersInvalid;
261 },
262 },
263 else => switch (int16(req.read_buffer[req.read_buffer_start..][0..2])) {
264 int16("\r\n") => {
265 req.read_buffer_start += 2;
266 req.response.state = .chunk_size;
267 continue;
268 },
269 else => {
270 req.response.state = .invalid;
271 return error.HttpHeadersInvalid;
272 },
273 },
274 },
275 .chunk_size_prefix_n => switch (req.read_buffer_len - req.read_buffer_start) {
276 0 => return out_index,
277 else => switch (req.read_buffer[req.read_buffer_start]) {
278 '\n' => {
279 req.read_buffer_start += 1;
280 req.response.state = .chunk_size;
281 continue;
282 },
283 else => {
284 req.response.state = .invalid;
285 return error.HttpHeadersInvalid;
286 },
287 },
288 },
289 .chunk_size, .chunk_r => {
290 const i = req.response.findChunkedLen(req.read_buffer[req.read_buffer_start..req.read_buffer_len]);
291 switch (req.response.state) {
292 .invalid => return error.HttpHeadersInvalid,
293 .chunk_data => {
294 if (req.response.next_chunk_length == 0) {
295 req.response.done = true;
296 req.client.connection_pool.release(req.client, req.connection);
297 req.connection = undefined;
298
299 return out_index;
300 }
301
302 req.read_buffer_start += @intCast(ReadBufferIndex, i);
303 continue;
304 },
305 .chunk_size => return out_index,
306 else => unreachable,
307 }
308 },
309 .chunk_data => {
310 // TODO https://github.com/ziglang/zig/issues/14039
311 const buf_avail = req.read_buffer_len - req.read_buffer_start;
312 const data_avail = req.response.next_chunk_length;
313 const out_avail = buffer.len - out_index;
314
315 if (req.handle_redirects and req.response.headers.status.class() == .redirect) {
316 const can_read = @intCast(usize, @min(buf_avail, data_avail));
317 req.response.next_chunk_length -= can_read;
318
319 if (req.response.next_chunk_length == 0) {
320 req.client.connection_pool.release(req.client, req.connection);
321 req.connection = undefined;
322 req.response.done = true;
323 continue;
324 }
325
326 return 0; // skip over as much data as possible
327 }
328
329 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail));
330 req.response.next_chunk_length -= can_read;
331
332 mem.copy(u8, buffer[out_index..], req.read_buffer[req.read_buffer_start..][0..can_read]);
333 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);
334 out_index += can_read;
335
336 if (req.response.next_chunk_length == 0) {
337 req.response.state = .chunk_size_prefix_r;
338
339 continue;
340 }
341
342 return out_index;
343 },
344 }
345 }
346}
347
348pub const ReadError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize, CompressionNotSupported };
349
350pub const Reader = std.io.Reader(*Request, ReadError, read);
351
352pub fn reader(req: *Request) Reader {
353 return .{ .context = req };
354}
355
356pub fn read(req: *Request, buffer: []u8) ReadError!usize {
357 while (true) {
358 if (!req.response.state.isContent()) try req.waitForCompleteHead();
359
360 if (req.handle_redirects and req.response.headers.status.class() == .redirect) {
361 assert(try req.readRaw(buffer) == 0);
362
363 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
364
365 const location = req.response.headers.location orelse
366 return error.HttpRedirectMissingLocation;
367 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
368
369 var new_arena = std.heap.ArenaAllocator.init(req.client.allocator);
370 const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator());
371 errdefer new_arena.deinit();
372
373 req.arena.deinit();
374 req.arena = new_arena;
375
376 const new_req = try req.client.request(resolved_url, req.headers, .{
377 .max_redirects = req.redirects_left - 1,
378 .header_strategy = if (req.response.header_bytes_owned) .{
379 .dynamic = req.response.max_header_bytes,
380 } else .{
381 .static = req.response.header_bytes.unusedCapacitySlice(),
382 },
383 });
384 req.deinit();
385 req.* = new_req;
386 } else {
387 break;
388 }
389 }
390
391 if (req.response.compression == .none) {
392 if (req.response.headers.transfer_compression) |compression| {
393 switch (compression) {
394 .compress => return error.CompressionNotSupported,
395 .deflate => req.response.compression = .{
396 .deflate = try std.compress.zlib.zlibStream(req.client.allocator, ReaderRaw{ .context = req }),
397 },
398 .gzip => req.response.compression = .{
399 .gzip = try std.compress.gzip.decompress(req.client.allocator, ReaderRaw{ .context = req }),
400 },
401 .zstd => req.response.compression = .{
402 .zstd = std.compress.zstd.decompressStream(req.client.allocator, ReaderRaw{ .context = req }),
403 },
404 }
405 }
406 }
407
408 return switch (req.response.compression) {
409 .deflate => |*deflate| try deflate.read(buffer),
410 .gzip => |*gzip| try gzip.read(buffer),
411 .zstd => |*zstd| try zstd.read(buffer),
412 else => try req.readRaw(buffer),
413 };
414}
415
416pub fn readAll(req: *Request, buffer: []u8) !usize {
417 var index: usize = 0;
418 while (index < buffer.len) {
419 const amt = try read(req, buffer[index..]);
420 if (amt == 0) break;
421 index += amt;
422 }
423 return index;
424}
425
426pub const WriteError = Connection.WriteError || error{MessageTooLong};
427
428pub const Writer = std.io.Writer(*Request, WriteError, write);
429
430pub fn writer(req: *Request) Writer {
431 return .{ .context = req };
432}
433
434/// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
435pub fn write(req: *Request, bytes: []const u8) !usize {
436 switch (req.headers.transfer_encoding) {
437 .chunked => {
438 try req.connection.data.writer().print("{x}\r\n", .{bytes.len});
439 try req.connection.data.writeAll(bytes);
440 try req.connection.data.writeAll("\r\n");
441
442 return bytes.len;
443 },
444 .content_length => |*len| {
445 if (len.* < bytes.len) return error.MessageTooLong;
446
447 const amt = try req.connection.data.write(bytes);
448 len.* -= amt;
449 return amt;
450 },
451 .none => return error.NotWriteable,
452 }
453}
454
455/// Finish the body of a request. This notifies the server that you have no more data to send.
456pub fn finish(req: *Request) !void {
457 switch (req.headers.transfer_encoding) {
458 .chunked => try req.connection.data.writeAll("0\r\n"),
459 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
460 .none => {},
461 }
462}
463
464inline fn int16(array: *const [2]u8) u16 {
465 return @bitCast(u16, array.*);
466}
467
468inline fn int32(array: *const [4]u8) u32 {
469 return @bitCast(u32, array.*);
470}
471
472inline fn int64(array: *const [8]u8) u64 {
473 return @bitCast(u64, array.*);
474}
475
476test {
477 const builtin = @import("builtin");
478
479 if (builtin.os.tag == .wasi) return error.SkipZigTest;
480
481 _ = Response;
482}
lib/std/http/Client/Response.zig+1-234
...@@ -4,6 +4,7 @@ const mem = std.mem;...@@ -4,6 +4,7 @@ const mem = std.mem;
4const testing = std.testing;4const testing = std.testing;
5const assert = std.debug.assert;5const assert = std.debug.assert;
66
7const protocol = @import("../protocol.zig");
7const Client = @import("../Client.zig");8const Client = @import("../Client.zig");
8const Response = @This();9const Response = @This();
910
...@@ -169,14 +170,6 @@ pub const Headers = struct {...@@ -169,14 +170,6 @@ pub const Headers = struct {
169 }170 }
170};171};
171172
172inline fn int16(array: *const [2]u8) u16 {
173 return @bitCast(u16, array.*);
174}
175
176inline fn int32(array: *const [4]u8) u32 {
177 return @bitCast(u32, array.*);
178}
179
180inline fn int64(array: *const [8]u8) u64 {173inline fn int64(array: *const [8]u8) u64 {
181 return @bitCast(u64, array.*);174 return @bitCast(u64, array.*);
182}175}
...@@ -226,232 +219,6 @@ pub fn initStatic(buf: []u8) Response {...@@ -226,232 +219,6 @@ pub fn initStatic(buf: []u8) Response {
226 };219 };
227}220}
228221
229/// Returns how many bytes are part of HTTP headers. Always less than or
230/// equal to bytes.len. If the amount returned is less than bytes.len, it
231/// means the headers ended and the first byte after the double \r\n\r\n is
232/// located at `bytes[result]`.
233pub fn findHeadersEnd(r: *Response, bytes: []const u8) usize {
234 var index: usize = 0;
235
236 // TODO: https://github.com/ziglang/zig/issues/8220
237 state: while (true) {
238 switch (r.state) {
239 .invalid => unreachable,
240 .finished => unreachable,
241 .start => while (true) {
242 switch (bytes.len - index) {
243 0 => return index,
244 1 => {
245 if (bytes[index] == '\r')
246 r.state = .seen_r;
247 return index + 1;
248 },
249 2 => {
250 if (int16(bytes[index..][0..2]) == int16("\r\n")) {
251 r.state = .seen_rn;
252 } else if (bytes[index + 1] == '\r') {
253 r.state = .seen_r;
254 }
255 return index + 2;
256 },
257 3 => {
258 if (int16(bytes[index..][0..2]) == int16("\r\n") and
259 bytes[index + 2] == '\r')
260 {
261 r.state = .seen_rnr;
262 } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n")) {
263 r.state = .seen_rn;
264 } else if (bytes[index + 2] == '\r') {
265 r.state = .seen_r;
266 }
267 return index + 3;
268 },
269 4...15 => {
270 if (int32(bytes[index..][0..4]) == int32("\r\n\r\n")) {
271 r.state = .finished;
272 return index + 4;
273 } else if (int16(bytes[index + 1 ..][0..2]) == int16("\r\n") and
274 bytes[index + 3] == '\r')
275 {
276 r.state = .seen_rnr;
277 index += 4;
278 continue :state;
279 } else if (int16(bytes[index + 2 ..][0..2]) == int16("\r\n")) {
280 r.state = .seen_rn;
281 index += 4;
282 continue :state;
283 } else if (bytes[index + 3] == '\r') {
284 r.state = .seen_r;
285 index += 4;
286 continue :state;
287 }
288 index += 4;
289 continue;
290 },
291 else => {
292 const chunk = bytes[index..][0..16];
293 const v: @Vector(16, u8) = chunk.*;
294 const matches_r = v == @splat(16, @as(u8, '\r'));
295 const iota = std.simd.iota(u8, 16);
296 const default = @splat(16, @as(u8, 16));
297 const sub_index = @reduce(.Min, @select(u8, matches_r, iota, default));
298 switch (sub_index) {
299 0...12 => {
300 index += sub_index + 4;
301 if (int32(chunk[sub_index..][0..4]) == int32("\r\n\r\n")) {
302 r.state = .finished;
303 return index;
304 }
305 continue;
306 },
307 13 => {
308 index += 16;
309 if (int16(chunk[14..][0..2]) == int16("\n\r")) {
310 r.state = .seen_rnr;
311 continue :state;
312 }
313 continue;
314 },
315 14 => {
316 index += 16;
317 if (chunk[15] == '\n') {
318 r.state = .seen_rn;
319 continue :state;
320 }
321 continue;
322 },
323 15 => {
324 r.state = .seen_r;
325 index += 16;
326 continue :state;
327 },
328 16 => {
329 index += 16;
330 continue;
331 },
332 else => unreachable,
333 }
334 },
335 }
336 },
337
338 .seen_r => switch (bytes.len - index) {
339 0 => return index,
340 1 => {
341 switch (bytes[index]) {
342 '\n' => r.state = .seen_rn,
343 '\r' => r.state = .seen_r,
344 else => r.state = .start,
345 }
346 return index + 1;
347 },
348 2 => {
349 if (int16(bytes[index..][0..2]) == int16("\n\r")) {
350 r.state = .seen_rnr;
351 return index + 2;
352 }
353 r.state = .start;
354 return index + 2;
355 },
356 else => {
357 if (int16(bytes[index..][0..2]) == int16("\n\r") and
358 bytes[index + 2] == '\n')
359 {
360 r.state = .finished;
361 return index + 3;
362 }
363 index += 3;
364 r.state = .start;
365 continue :state;
366 },
367 },
368 .seen_rn => switch (bytes.len - index) {
369 0 => return index,
370 1 => {
371 switch (bytes[index]) {
372 '\r' => r.state = .seen_rnr,
373 else => r.state = .start,
374 }
375 return index + 1;
376 },
377 else => {
378 if (int16(bytes[index..][0..2]) == int16("\r\n")) {
379 r.state = .finished;
380 return index + 2;
381 }
382 index += 2;
383 r.state = .start;
384 continue :state;
385 },
386 },
387 .seen_rnr => switch (bytes.len - index) {
388 0 => return index,
389 else => {
390 if (bytes[index] == '\n') {
391 r.state = .finished;
392 return index + 1;
393 }
394 index += 1;
395 r.state = .start;
396 continue :state;
397 },
398 },
399 .chunk_size_prefix_r => unreachable,
400 .chunk_size_prefix_n => unreachable,
401 .chunk_size => unreachable,
402 .chunk_r => unreachable,
403 .chunk_data => unreachable,
404 }
405
406 return index;
407 }
408}
409
410pub fn findChunkedLen(r: *Response, bytes: []const u8) usize {
411 var i: usize = 0;
412 if (r.state == .chunk_size) {
413 while (i < bytes.len) : (i += 1) {
414 const digit = switch (bytes[i]) {
415 '0'...'9' => |b| b - '0',
416 'A'...'Z' => |b| b - 'A' + 10,
417 'a'...'z' => |b| b - 'a' + 10,
418 '\r' => {
419 r.state = .chunk_r;
420 i += 1;
421 break;
422 },
423 else => {
424 r.state = .invalid;
425 return i;
426 },
427 };
428 const mul = @mulWithOverflow(r.next_chunk_length, 16);
429 if (mul[1] != 0) {
430 r.state = .invalid;
431 return i;
432 }
433 const add = @addWithOverflow(mul[0], digit);
434 if (add[1] != 0) {
435 r.state = .invalid;
436 return i;
437 }
438 r.next_chunk_length = add[0];
439 } else {
440 return i;
441 }
442 }
443 assert(r.state == .chunk_r);
444 if (i == bytes.len) return i;
445
446 if (bytes[i] == '\n') {
447 r.state = .chunk_data;
448 return i + 1;
449 } else {
450 r.state = .invalid;
451 return i;
452 }
453}
454
455fn parseInt3(nnn: @Vector(3, u8)) u10 {222fn parseInt3(nnn: @Vector(3, u8)) u10 {
456 const zero: @Vector(3, u8) = .{ '0', '0', '0' };223 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
457 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };224 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
lib/std/http/Server.zig created+495
...@@ -0,0 +1,495 @@
1const std = @import("../std.zig");
2const testing = std.testing;
3const http = std.http;
4const mem = std.mem;
5const net = std.net;
6const Uri = std.Uri;
7const Allocator = mem.Allocator;
8const assert = std.debug.assert;
9
10const Server = @This();
11const proto = @import("protocol.zig");
12
13allocator: Allocator,
14
15socket: net.StreamServer,
16
17pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader);
18pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader);
19pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
20
21pub const Connection = struct {
22 stream: net.Stream,
23 protocol: Protocol,
24
25 closing: bool = true,
26
27 pub const Protocol = enum { plain };
28
29 pub fn read(conn: *Connection, buffer: []u8) !usize {
30 switch (conn.protocol) {
31 .plain => return conn.stream.read(buffer),
32 // .tls => return conn.tls_client.read(conn.stream, buffer),
33 }
34 }
35
36 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {
37 switch (conn.protocol) {
38 .plain => return conn.stream.readAtLeast(buffer, len),
39 // .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),
40 }
41 }
42
43 pub const ReadError = net.Stream.ReadError;
44
45 pub const Reader = std.io.Reader(*Connection, ReadError, read);
46
47 pub fn reader(conn: *Connection) Reader {
48 return Reader{ .context = conn };
49 }
50
51 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
52 switch (conn.protocol) {
53 .plain => return conn.stream.writeAll(buffer),
54 // .tls => return conn.tls_client.writeAll(conn.stream, buffer),
55 }
56 }
57
58 pub fn write(conn: *Connection, buffer: []const u8) !usize {
59 switch (conn.protocol) {
60 .plain => return conn.stream.write(buffer),
61 // .tls => return conn.tls_client.write(conn.stream, buffer),
62 }
63 }
64
65 pub const WriteError = net.Stream.WriteError || error{};
66 pub const Writer = std.io.Writer(*Connection, WriteError, write);
67
68 pub fn writer(conn: *Connection) Writer {
69 return Writer{ .context = conn };
70 }
71
72 pub fn close(conn: *Connection) void {
73 conn.stream.close();
74 }
75};
76
77pub const Request = struct {
78 pub const Headers = struct {
79 method: http.Method,
80 target: []const u8,
81 version: http.Version,
82 content_length: ?u64 = null,
83 transfer_encoding: ?http.TransferEncoding = null,
84 transfer_compression: ?http.ContentEncoding = null,
85 connection: http.Connection = .close,
86 host: ?[]const u8 = null,
87
88 pub const ParseError = error{
89 ShortHttpStatusLine,
90 BadHttpVersion,
91 UnknownHttpMethod,
92 HttpHeadersInvalid,
93 HttpHeaderContinuationsUnsupported,
94 HttpTransferEncodingUnsupported,
95 HttpConnectionHeaderUnsupported,
96 InvalidCharacter,
97 };
98
99 pub fn parse(bytes: []const u8) !Headers {
100 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
101
102 const first_line = it.next() orelse return error.HttpHeadersInvalid;
103 if (first_line.len < 10)
104 return error.ShortHttpStatusLine;
105
106 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
107 const method_str = first_line[0..method_end];
108 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
109
110 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
111 if (version_start == method_end) return error.HttpHeadersInvalid;
112
113 const version_str = first_line[version_start + 1 ..];
114 if (version_str.len != 8) return error.HttpHeadersInvalid;
115 const version: http.Version = switch (int64(version_str[0..8])) {
116 int64("HTTP/1.0") => .@"HTTP/1.0",
117 int64("HTTP/1.1") => .@"HTTP/1.1",
118 else => return error.BadHttpVersion,
119 };
120
121 const target = first_line[method_end + 1 .. version_start];
122
123 var headers: Headers = .{
124 .method = method,
125 .target = target,
126 .version = version,
127 };
128
129 while (it.next()) |line| {
130 if (line.len == 0) return error.HttpHeadersInvalid;
131 switch (line[0]) {
132 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
133 else => {},
134 }
135
136 var line_it = mem.tokenize(u8, line, ": ");
137 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
138 const header_value = line_it.rest();
139 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
140 if (headers.content_length != null) return error.HttpHeadersInvalid;
141 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
142 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
143 // Transfer-Encoding: second, first
144 // Transfer-Encoding: deflate, chunked
145 var iter = mem.splitBackwards(u8, header_value, ",");
146
147 if (iter.next()) |first| {
148 const trimmed = mem.trim(u8, first, " ");
149
150 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
151 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
152 headers.transfer_encoding = te;
153 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
154 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
155 headers.transfer_compression = ce;
156 } else {
157 return error.HttpTransferEncodingUnsupported;
158 }
159 }
160
161 if (iter.next()) |second| {
162 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
163
164 const trimmed = mem.trim(u8, second, " ");
165
166 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
167 headers.transfer_compression = ce;
168 } else {
169 return error.HttpTransferEncodingUnsupported;
170 }
171 }
172
173 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
174 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
175 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
176
177 const trimmed = mem.trim(u8, header_value, " ");
178
179 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
180 headers.transfer_compression = ce;
181 } else {
182 return error.HttpTransferEncodingUnsupported;
183 }
184 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
185 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
186 headers.connection = .keep_alive;
187 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
188 headers.connection = .close;
189 } else {
190 return error.HttpConnectionHeaderUnsupported;
191 }
192 } else if (std.ascii.eqlIgnoreCase(header_name, "host")) {
193 headers.host = header_value;
194 }
195 }
196
197 return headers;
198 }
199
200 inline fn int64(array: *const [8]u8) u64 {
201 return @bitCast(u64, array.*);
202 }
203 };
204
205 headers: Headers = undefined,
206 parser: proto.HeadersParser,
207 compression: Compression = .none,
208};
209
210pub const Response = struct {
211 pub const Headers = struct {
212 version: http.Version = .@"HTTP/1.1",
213 status: http.Status = .ok,
214 reason: ?[]const u8 = null,
215
216 server: ?[]const u8 = "zig (std.http)",
217 connection: http.Connection = .keep_alive,
218 transfer_encoding: RequestTransfer = .none,
219
220 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
221 };
222
223 server: *Server,
224 address: net.Address,
225 connection: Connection,
226
227 headers: Headers = .{},
228 request: Request,
229
230 pub fn reset(res: *Response) void {
231 switch (res.request.compression) {
232 .none => {},
233 .deflate => |*deflate| deflate.deinit(),
234 .gzip => |*gzip| gzip.deinit(),
235 .zstd => |*zstd| zstd.deinit(),
236 }
237
238 if (!res.request.parser.done) {
239 // If the response wasn't fully read, then we need to close the connection.
240 res.connection.closing = true;
241 }
242
243 if (res.connection.closing) {
244 res.connection.close();
245
246 if (res.request.parser.header_bytes_owned) {
247 res.request.parser.header_bytes.deinit(res.server.allocator);
248 }
249
250 res.* = undefined;
251 } else {
252 res.request.parser.reset();
253 }
254 }
255
256 pub fn sendResponseHead(res: *Response) !void {
257 var buffered = std.io.bufferedWriter(res.connection.writer());
258 const w = buffered.writer();
259
260 try w.writeAll(@tagName(res.headers.version));
261 try w.writeByte(' ');
262 try w.print("{d}", .{@enumToInt(res.headers.status)});
263 try w.writeByte(' ');
264 if (res.headers.reason) |reason| {
265 try w.writeAll(reason);
266 } else if (res.headers.status.phrase()) |phrase| {
267 try w.writeAll(phrase);
268 }
269
270 if (res.headers.server) |server| {
271 try w.writeAll("\r\nServer: ");
272 try w.writeAll(server);
273 }
274
275 if (res.headers.connection == .close) {
276 try w.writeAll("\r\nConnection: close");
277 } else {
278 try w.writeAll("\r\nConnection: keep-alive");
279 }
280
281 switch (res.headers.transfer_encoding) {
282 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
283 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
284 .none => {},
285 }
286
287 for (res.headers.custom) |header| {
288 try w.writeAll("\r\n");
289 try w.writeAll(header.name);
290 try w.writeAll(": ");
291 try w.writeAll(header.value);
292 }
293
294 try w.writeAll("\r\n\r\n");
295
296 try buffered.flush();
297 }
298
299 pub const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;
300
301 pub const TransferReader = std.io.Reader(*Response, TransferReadError, transferRead);
302
303 pub fn transferReader(res: *Response) TransferReader {
304 return .{ .context = res };
305 }
306
307 pub fn transferRead(res: *Response, buf: []u8) TransferReadError!usize {
308 if (res.request.parser.isComplete()) return 0;
309
310 var index: usize = 0;
311 while (index == 0) {
312 const amt = try res.request.parser.read(res.connection.reader(), buf[index..], false);
313 if (amt == 0 and res.request.parser.isComplete()) break;
314 index += amt;
315 }
316
317 return index;
318 }
319
320 pub const WaitForCompleteHeadError = Connection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};
321
322 pub fn waitForCompleteHead(res: *Response) !void {
323 try res.request.parser.waitForCompleteHead(res.connection.reader(), res.server.allocator);
324
325 res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items);
326
327 if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) {
328 res.connection.closing = false;
329 } else {
330 res.connection.closing = true;
331 }
332
333 if (res.request.headers.transfer_encoding) |te| {
334 switch (te) {
335 .chunked => {
336 res.request.parser.next_chunk_length = 0;
337 res.request.parser.state = .chunk_head_size;
338 },
339 }
340 } else if (res.request.headers.content_length) |cl| {
341 res.request.parser.next_chunk_length = cl;
342
343 if (cl == 0) res.request.parser.done = true;
344 } else {
345 res.request.parser.done = true;
346 }
347
348 if (!res.request.parser.done) {
349 if (res.request.headers.transfer_compression) |tc| switch (tc) {
350 .compress => return error.CompressionNotSupported,
351 .deflate => res.request.compression = .{
352 .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()),
353 },
354 .gzip => res.request.compression = .{
355 .gzip = try std.compress.gzip.decompress(res.server.allocator, res.transferReader()),
356 },
357 .zstd => res.request.compression = .{
358 .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()),
359 },
360 };
361 }
362 }
363
364 pub const ReadError = DeflateDecompressor.Error || GzipDecompressor.Error || ZstdDecompressor.Error || WaitForCompleteHeadError;
365
366 pub const Reader = std.io.Reader(*Response, ReadError, read);
367
368 pub fn reader(res: *Response) Reader {
369 return .{ .context = res };
370 }
371
372 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
373 return switch (res.request.compression) {
374 .deflate => |*deflate| try deflate.read(buffer),
375 .gzip => |*gzip| try gzip.read(buffer),
376 .zstd => |*zstd| try zstd.read(buffer),
377 else => try res.transferRead(buffer),
378 };
379 }
380
381 pub fn readAll(res: *Response, buffer: []u8) !usize {
382 var index: usize = 0;
383 while (index < buffer.len) {
384 const amt = try read(res, buffer[index..]);
385 if (amt == 0) break;
386 index += amt;
387 }
388 return index;
389 }
390
391 pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong };
392
393 pub const Writer = std.io.Writer(*Response, WriteError, write);
394
395 pub fn writer(res: *Response) Writer {
396 return .{ .context = res };
397 }
398
399 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
400 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
401 switch (res.headers.transfer_encoding) {
402 .chunked => {
403 try res.connection.writer().print("{x}\r\n", .{bytes.len});
404 try res.connection.writeAll(bytes);
405 try res.connection.writeAll("\r\n");
406
407 return bytes.len;
408 },
409 .content_length => |*len| {
410 if (len.* < bytes.len) return error.MessageTooLong;
411
412 const amt = try res.connection.write(bytes);
413 len.* -= amt;
414 return amt;
415 },
416 .none => return error.NotWriteable,
417 }
418 }
419
420 /// Finish the body of a request. This notifies the server that you have no more data to send.
421 pub fn finish(res: *Response) !void {
422 switch (res.headers.transfer_encoding) {
423 .chunked => try res.connection.writeAll("0\r\n"),
424 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
425 .none => {},
426 }
427 }
428};
429
430pub const RequestTransfer = union(enum) {
431 content_length: u64,
432 chunked: void,
433 none: void,
434};
435
436pub const Compression = union(enum) {
437 deflate: DeflateDecompressor,
438 gzip: GzipDecompressor,
439 zstd: ZstdDecompressor,
440 none: void,
441};
442
443pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
444 return .{
445 .allocator = allocator,
446 .socket = net.StreamServer.init(options),
447 };
448}
449
450pub fn deinit(server: *Server) void {
451 server.socket.deinit();
452}
453
454pub const ListenError = std.os.SocketError || std.os.BindError || std.os.ListenError || std.os.SetSockOptError || std.os.GetSockNameError;
455
456pub fn listen(server: *Server, address: net.Address) !void {
457 try server.socket.listen(address);
458}
459
460pub const AcceptError = net.StreamServer.AcceptError || Allocator.Error;
461
462pub const HeaderStrategy = union(enum) {
463 /// In this case, the client's Allocator will be used to store the
464 /// entire HTTP header. This value is the maximum total size of
465 /// HTTP headers allowed, otherwise
466 /// error.HttpHeadersExceededSizeLimit is returned from read().
467 dynamic: usize,
468 /// This is used to store the entire HTTP header. If the HTTP
469 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit`
470 /// is returned from read(). When this is used, `error.OutOfMemory`
471 /// cannot be returned from `read()`.
472 static: []u8,
473};
474
475pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
476 const in = try server.socket.accept();
477
478 const res = try server.allocator.create(Response);
479 res.* = .{
480 .server = server,
481 .address = in.address,
482 .connection = .{
483 .stream = in.stream,
484 .protocol = .plain,
485 },
486 .request = .{
487 .parser = switch (options) {
488 .dynamic => |max| proto.HeadersParser.initDynamic(max),
489 .static => |buf| proto.HeadersParser.initStatic(buf),
490 },
491 },
492 };
493
494 return res;
495}
lib/std/http/protocol.zig created+714
...@@ -0,0 +1,714 @@
1const std = @import("std");
2const testing = std.testing;
3const mem = std.mem;
4
5const assert = std.debug.assert;
6
7pub const State = enum {
8 /// Begin header parsing states.
9 invalid,
10 start,
11 seen_n,
12 seen_r,
13 seen_rn,
14 seen_rnr,
15 finished,
16 /// Begin transfer-encoding: chunked parsing states.
17 chunk_head_size,
18 chunk_head_ext,
19 chunk_head_r,
20 chunk_data,
21 chunk_data_suffix,
22 chunk_data_suffix_r,
23
24 pub fn isContent(self: State) bool {
25 return switch (self) {
26 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false,
27 .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true,
28 };
29 }
30};
31
32const read_buffer_size = 0x4000;
33const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size);
34
35pub const HeadersParser = struct {
36 state: State = .start,
37 /// Wether or not `header_bytes` is allocated or was provided as a fixed buffer.
38 header_bytes_owned: bool,
39 /// Either a fixed buffer of len `max_header_bytes` or a dynamic buffer that can grow up to `max_header_bytes`.
40 /// Pointers into this buffer are not stable until after a message is complete.
41 header_bytes: std.ArrayListUnmanaged(u8),
42 /// The maximum allowed size of `header_bytes`.
43 max_header_bytes: usize,
44 next_chunk_length: u64 = 0,
45 /// Wether this parser is done parsing a complete message.
46 /// A message is only done when the entire payload has been read
47 done: bool = false,
48
49 read_buffer: [read_buffer_size]u8 = undefined,
50 read_buffer_start: ReadBufferIndex = 0,
51 read_buffer_len: ReadBufferIndex = 0,
52
53 pub fn initDynamic(max: usize) HeadersParser {
54 return .{
55 .header_bytes = .{},
56 .max_header_bytes = max,
57 .header_bytes_owned = true,
58 };
59 }
60
61 pub fn initStatic(buf: []u8) HeadersParser {
62 return .{
63 .header_bytes = .{ .items = buf[0..0], .capacity = buf.len },
64 .max_header_bytes = buf.len,
65 .header_bytes_owned = false,
66 };
67 }
68
69 pub fn reset(r: *HeadersParser) void {
70 r.header_bytes.clearRetainingCapacity();
71
72 r.* = .{
73 .header_bytes = r.header_bytes,
74 .max_header_bytes = r.max_header_bytes,
75 .header_bytes_owned = r.header_bytes_owned,
76 };
77 }
78
79 /// Returns how many bytes are part of HTTP headers. Always less than or
80 /// equal to bytes.len. If the amount returned is less than bytes.len, it
81 /// means the headers ended and the first byte after the double \r\n\r\n is
82 /// located at `bytes[result]`.
83 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
84 const vector_len = 16;
85 const len = @truncate(u32, bytes.len);
86 var index: u32 = 0;
87
88 while (true) {
89 switch (r.state) {
90 .invalid => unreachable,
91 .finished => return index,
92 .start => switch (len - index) {
93 0 => return index,
94 1 => {
95 switch (bytes[index]) {
96 '\r' => r.state = .seen_r,
97 '\n' => r.state = .seen_n,
98 else => {},
99 }
100
101 return index + 1;
102 },
103 2 => {
104 const b16 = int16(bytes[index..][0..2]);
105 const b8 = intShift(u8, b16);
106
107 switch (b8) {
108 '\r' => r.state = .seen_r,
109 '\n' => r.state = .seen_n,
110 else => {},
111 }
112
113 switch (b16) {
114 int16("\r\n") => r.state = .seen_rn,
115 int16("\n\n") => r.state = .finished,
116 else => {},
117 }
118
119 return index + 2;
120 },
121 3 => {
122 const b24 = int24(bytes[index..][0..3]);
123 const b16 = intShift(u16, b24);
124 const b8 = intShift(u8, b24);
125
126 switch (b8) {
127 '\r' => r.state = .seen_r,
128 '\n' => r.state = .seen_n,
129 else => {},
130 }
131
132 switch (b16) {
133 int16("\r\n") => r.state = .seen_rn,
134 int16("\n\n") => r.state = .finished,
135 else => {},
136 }
137
138 switch (b24) {
139 int24("\r\n\r") => r.state = .seen_rnr,
140 else => {},
141 }
142
143 return index + 3;
144 },
145 4...vector_len - 1 => {
146 const b32 = int32(bytes[index..][0..4]);
147 const b24 = intShift(u24, b32);
148 const b16 = intShift(u16, b32);
149 const b8 = intShift(u8, b32);
150
151 switch (b8) {
152 '\r' => r.state = .seen_r,
153 '\n' => r.state = .seen_n,
154 else => {},
155 }
156
157 switch (b16) {
158 int16("\r\n") => r.state = .seen_rn,
159 int16("\n\n") => r.state = .finished,
160 else => {},
161 }
162
163 switch (b24) {
164 int24("\r\n\r") => r.state = .seen_rnr,
165 else => {},
166 }
167
168 switch (b32) {
169 int32("\r\n\r\n") => r.state = .finished,
170 else => {},
171 }
172
173 index += 4;
174 continue;
175 },
176 else => {
177 const Vector = @Vector(vector_len, u8);
178 // const BoolVector = @Vector(vector_len, bool);
179 const BitVector = @Vector(vector_len, u1);
180 const SizeVector = @Vector(vector_len, u8);
181
182 const chunk = bytes[index..][0..vector_len];
183 const v: Vector = chunk.*;
184 const matches_r = @bitCast(BitVector, v == @splat(vector_len, @as(u8, '\r')));
185 const matches_n = @bitCast(BitVector, v == @splat(vector_len, @as(u8, '\n')));
186 const matches_or: SizeVector = matches_r | matches_n;
187
188 const matches = @reduce(.Add, matches_or);
189 switch (matches) {
190 0 => {},
191 1 => switch (chunk[vector_len - 1]) {
192 '\r' => r.state = .seen_r,
193 '\n' => r.state = .seen_n,
194 else => {},
195 },
196 2 => {
197 const b16 = int16(chunk[vector_len - 2 ..][0..2]);
198 const b8 = intShift(u8, b16);
199
200 switch (b8) {
201 '\r' => r.state = .seen_r,
202 '\n' => r.state = .seen_n,
203 else => {},
204 }
205
206 switch (b16) {
207 int16("\r\n") => r.state = .seen_rn,
208 int16("\n\n") => r.state = .finished,
209 else => {},
210 }
211 },
212 3 => {
213 const b24 = int24(chunk[vector_len - 3 ..][0..3]);
214 const b16 = intShift(u16, b24);
215 const b8 = intShift(u8, b24);
216
217 switch (b8) {
218 '\r' => r.state = .seen_r,
219 '\n' => r.state = .seen_n,
220 else => {},
221 }
222
223 switch (b16) {
224 int16("\r\n") => r.state = .seen_rn,
225 int16("\n\n") => r.state = .finished,
226 else => {},
227 }
228
229 switch (b24) {
230 int24("\r\n\r") => r.state = .seen_rnr,
231 else => {},
232 }
233 },
234 4...vector_len - 1 => {
235 for (0..vector_len - 4) |i_usize| {
236 const i = @truncate(u32, i_usize);
237
238 const b32 = int32(chunk[i..][0..4]);
239 const b16 = intShift(u16, b32);
240
241 if (b32 == int32("\r\n\r\n")) {
242 r.state = .finished;
243 return index + i + 4;
244 } else if (b16 == int16("\n\n")) {
245 r.state = .finished;
246 return index + i + 2;
247 }
248 }
249 },
250 else => unreachable,
251 }
252
253 index += vector_len;
254 continue;
255 },
256 },
257 .seen_n => switch (len - index) {
258 0 => return index,
259 else => {
260 switch (bytes[index]) {
261 '\n' => r.state = .finished,
262 else => r.state = .start,
263 }
264
265 index += 1;
266 continue;
267 },
268 },
269 .seen_r => switch (len - index) {
270 0 => return index,
271 1 => {
272 switch (bytes[index]) {
273 '\n' => r.state = .seen_rn,
274 '\r' => r.state = .seen_r,
275 else => r.state = .start,
276 }
277
278 return index + 1;
279 },
280 2 => {
281 const b16 = int16(bytes[index..][0..2]);
282 const b8 = intShift(u8, b16);
283
284 switch (b8) {
285 '\r' => r.state = .seen_r,
286 '\n' => r.state = .seen_rn,
287 else => r.state = .start,
288 }
289
290 switch (b16) {
291 int16("\r\n") => r.state = .seen_rn,
292 int16("\n\n") => r.state = .finished,
293 else => {},
294 }
295
296 return index + 2;
297 },
298 else => {
299 const b24 = int24(bytes[index..][0..3]);
300 const b16 = intShift(u16, b24);
301 const b8 = intShift(u8, b24);
302
303 switch (b8) {
304 '\r' => r.state = .seen_r,
305 '\n' => r.state = .seen_n,
306 else => r.state = .start,
307 }
308
309 switch (b16) {
310 int16("\r\n") => r.state = .seen_rn,
311 int16("\n\n") => r.state = .finished,
312 else => {},
313 }
314
315 switch (b24) {
316 int24("\n\r\n") => r.state = .finished,
317 else => {},
318 }
319
320 index += 3;
321 continue;
322 },
323 },
324 .seen_rn => switch (len - index) {
325 0 => return index,
326 1 => {
327 switch (bytes[index]) {
328 '\r' => r.state = .seen_rnr,
329 '\n' => r.state = .seen_n,
330 else => r.state = .start,
331 }
332
333 return index + 1;
334 },
335 else => {
336 const b16 = int16(bytes[index..][0..2]);
337 const b8 = intShift(u8, b16);
338
339 switch (b8) {
340 '\r' => r.state = .seen_rnr,
341 '\n' => r.state = .seen_n,
342 else => r.state = .start,
343 }
344
345 switch (b16) {
346 int16("\r\n") => r.state = .finished,
347 int16("\n\n") => r.state = .finished,
348 else => {},
349 }
350
351 index += 2;
352 continue;
353 },
354 },
355 .seen_rnr => switch (len - index) {
356 0 => return index,
357 else => {
358 switch (bytes[index]) {
359 '\n' => r.state = .finished,
360 else => r.state = .start,
361 }
362
363 index += 1;
364 continue;
365 },
366 },
367 .chunk_head_size => unreachable,
368 .chunk_head_ext => unreachable,
369 .chunk_head_r => unreachable,
370 .chunk_data => unreachable,
371 .chunk_data_suffix => unreachable,
372 .chunk_data_suffix_r => unreachable,
373 }
374
375 return index;
376 }
377 }
378
379 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
380 const len = @truncate(u32, bytes.len);
381
382 for (bytes[0..], 0..) |c, i| {
383 const index = @intCast(u32, i);
384 switch (r.state) {
385 .chunk_data_suffix => switch (c) {
386 '\r' => r.state = .chunk_data_suffix_r,
387 '\n' => r.state = .chunk_head_size,
388 else => {
389 r.state = .invalid;
390 return index;
391 },
392 },
393 .chunk_data_suffix_r => switch (c) {
394 '\n' => r.state = .chunk_head_size,
395 else => {
396 r.state = .invalid;
397 return index;
398 },
399 },
400 .chunk_head_size => {
401 const digit = switch (c) {
402 '0'...'9' => |b| b - '0',
403 'A'...'Z' => |b| b - 'A' + 10,
404 'a'...'z' => |b| b - 'a' + 10,
405 '\r' => {
406 r.state = .chunk_head_r;
407 continue;
408 },
409 '\n' => {
410 r.state = .chunk_data;
411 return index + 1;
412 },
413 else => {
414 r.state = .chunk_head_ext;
415 continue;
416 },
417 };
418
419 const new_len = r.next_chunk_length *% 16 +% digit;
420 if (new_len <= r.next_chunk_length and r.next_chunk_length != 0) {
421 r.state = .invalid;
422 return index;
423 }
424
425 r.next_chunk_length = new_len;
426 },
427 .chunk_head_ext => switch (c) {
428 '\r' => r.state = .chunk_head_r,
429 '\n' => {
430 r.state = .chunk_data;
431 return index + 1;
432 },
433 else => continue,
434 },
435 .chunk_head_r => switch (c) {
436 '\n' => {
437 r.state = .chunk_data;
438 return index + 1;
439 },
440 else => {
441 r.state = .invalid;
442 return index;
443 },
444 },
445 else => unreachable,
446 }
447 }
448
449 return len;
450 }
451
452 /// Returns whether or not the parser has finished parsing a complete message. A message is only complete after the
453 /// entire body has been read and any trailing headers have been parsed.
454 pub fn isComplete(r: *HeadersParser) bool {
455 return r.done and r.state == .finished;
456 }
457
458 pub const CheckCompleteHeadError = mem.Allocator.Error || error{HttpHeadersExceededSizeLimit};
459
460 /// Pumps `in` bytes into the parser. Returns the number of bytes consumed. This function will return 0 if the parser
461 /// is not in a state to parse more headers.
462 pub fn checkCompleteHead(r: *HeadersParser, allocator: std.mem.Allocator, in: []const u8) CheckCompleteHeadError!u32 {
463 if (r.state.isContent()) return 0;
464
465 const i = r.findHeadersEnd(in);
466 const data = in[0..i];
467 if (r.header_bytes.items.len + data.len > r.max_header_bytes) {
468 return error.HttpHeadersExceededSizeLimit;
469 } else {
470 if (r.header_bytes_owned) try r.header_bytes.ensureUnusedCapacity(allocator, data.len);
471
472 r.header_bytes.appendSliceAssumeCapacity(data);
473 }
474
475 return i;
476 }
477
478 /// Set of errors that `waitForCompleteHead` can throw except any errors inherited by `reader`
479 pub const WaitForCompleteHeadError = CheckCompleteHeadError || error{UnexpectedEndOfStream};
480
481 /// Waits for the complete head to be available. This function will continue trying to read until the head is complete
482 /// or an error occurs.
483 pub fn waitForCompleteHead(r: *HeadersParser, reader: anytype, allocator: std.mem.Allocator) !void {
484 if (r.state.isContent()) return;
485
486 while (true) {
487 if (r.read_buffer_start == r.read_buffer_len) {
488 const nread = try reader.read(r.read_buffer[0..]);
489 if (nread == 0) return error.UnexpectedEndOfStream;
490
491 r.read_buffer_start = 0;
492 r.read_buffer_len = @intCast(ReadBufferIndex, nread);
493 }
494
495 const amt = try r.checkCompleteHead(allocator, r.read_buffer[r.read_buffer_start..r.read_buffer_len]);
496 r.read_buffer_start += @intCast(ReadBufferIndex, amt);
497
498 if (amt != 0) return;
499 }
500 }
501
502 pub const ReadError = error{
503 UnexpectedEndOfStream,
504 HttpHeadersExceededSizeLimit,
505 HttpChunkInvalid,
506 };
507
508 /// Reads the body of the message into `buffer`. If `skip` is true, the buffer will be unused and the body will be
509 /// skipped. Returns the number of bytes placed in the buffer.
510 pub fn read(r: *HeadersParser, reader: anytype, buffer: []u8, skip: bool) !usize {
511 assert(r.state.isContent());
512 if (r.done) return 0;
513
514 if (r.read_buffer_start == r.read_buffer_len) {
515 const nread = try reader.read(r.read_buffer[0..]);
516 if (nread == 0) return error.UnexpectedEndOfStream;
517
518 r.read_buffer_start = 0;
519 r.read_buffer_len = @intCast(ReadBufferIndex, nread);
520 }
521
522 var out_index: usize = 0;
523 while (true) {
524 switch (r.state) {
525 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable,
526 .finished => {
527 const buf_avail = r.read_buffer_len - r.read_buffer_start;
528 const data_avail = r.next_chunk_length;
529 const out_avail = buffer.len;
530
531 // TODO https://github.com/ziglang/zig/issues/14039
532 const read_available = @intCast(usize, @min(buf_avail, data_avail));
533 if (skip) {
534 r.next_chunk_length -= read_available;
535 r.read_buffer_start += @intCast(ReadBufferIndex, read_available);
536 } else {
537 const can_read = @min(read_available, out_avail);
538 r.next_chunk_length -= can_read;
539
540 mem.copy(u8, buffer[out_index..], r.read_buffer[r.read_buffer_start..][0..can_read]);
541 r.read_buffer_start += @intCast(ReadBufferIndex, can_read);
542 out_index += can_read;
543 }
544
545 if (r.next_chunk_length == 0) r.done = true;
546
547 return out_index;
548 },
549 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {
550 const i = r.findChunkedLen(r.read_buffer[r.read_buffer_start..r.read_buffer_len]);
551 r.read_buffer_start += @intCast(ReadBufferIndex, i);
552
553 switch (r.state) {
554 .invalid => return error.HttpChunkInvalid,
555 .chunk_data => if (r.next_chunk_length == 0) {
556 // The trailer section is formatted identically to the header section.
557 r.state = .seen_rn;
558 r.done = true;
559
560 return out_index;
561 },
562 else => return out_index,
563 }
564
565 continue;
566 },
567 .chunk_data => {
568 const buf_avail = r.read_buffer_len - r.read_buffer_start;
569 const data_avail = r.next_chunk_length;
570 const out_avail = buffer.len;
571
572 // TODO https://github.com/ziglang/zig/issues/14039
573 const read_available = @intCast(usize, @min(buf_avail, data_avail));
574 if (skip) {
575 r.next_chunk_length -= read_available;
576 r.read_buffer_start += @intCast(ReadBufferIndex, read_available);
577 } else {
578 const can_read = @min(read_available, out_avail);
579 r.next_chunk_length -= can_read;
580
581 mem.copy(u8, buffer[out_index..], r.read_buffer[r.read_buffer_start..][0..can_read]);
582 r.read_buffer_start += @intCast(ReadBufferIndex, can_read);
583 out_index += can_read;
584 }
585
586 if (r.next_chunk_length == 0) {
587 r.state = .chunk_data_suffix;
588 continue;
589 }
590
591 return out_index;
592 },
593 }
594 }
595 }
596};
597
598inline fn int16(array: *const [2]u8) u16 {
599 return @bitCast(u16, array.*);
600}
601
602inline fn int24(array: *const [3]u8) u24 {
603 return @bitCast(u24, array.*);
604}
605
606inline fn int32(array: *const [4]u8) u32 {
607 return @bitCast(u32, array.*);
608}
609
610inline fn intShift(comptime T: type, x: anytype) T {
611 switch (@import("builtin").cpu.arch.endian()) {
612 .Little => return @truncate(T, x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T))),
613 .Big => return @truncate(T, x),
614 }
615}
616
617test "HeadersParser.findHeadersEnd" {
618 var r: HeadersParser = undefined;
619 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\nHello";
620
621 for (0..36) |i| {
622 r = HeadersParser.initDynamic(0);
623 try std.testing.expectEqual(@intCast(u32, i), r.findHeadersEnd(data[0..i]));
624 try std.testing.expectEqual(@intCast(u32, 35 - i), r.findHeadersEnd(data[i..]));
625 }
626}
627
628test "HeadersParser.findChunkedLen" {
629 var r: HeadersParser = undefined;
630 const data = "Ff\r\nf0f000 ; ext\n0\r\nffffffffffffffffffffffffffffffffffffffff\r\n";
631
632 r = HeadersParser.initDynamic(0);
633 r.state = .chunk_head_size;
634 r.next_chunk_length = 0;
635
636 const first = r.findChunkedLen(data[0..]);
637 try testing.expectEqual(@as(u32, 4), first);
638 try testing.expectEqual(@as(u64, 0xff), r.next_chunk_length);
639 try testing.expectEqual(State.chunk_data, r.state);
640 r.state = .chunk_head_size;
641 r.next_chunk_length = 0;
642
643 const second = r.findChunkedLen(data[first..]);
644 try testing.expectEqual(@as(u32, 13), second);
645 try testing.expectEqual(@as(u64, 0xf0f000), r.next_chunk_length);
646 try testing.expectEqual(State.chunk_data, r.state);
647 r.state = .chunk_head_size;
648 r.next_chunk_length = 0;
649
650 const third = r.findChunkedLen(data[first + second ..]);
651 try testing.expectEqual(@as(u32, 3), third);
652 try testing.expectEqual(@as(u64, 0), r.next_chunk_length);
653 try testing.expectEqual(State.chunk_data, r.state);
654 r.state = .chunk_head_size;
655 r.next_chunk_length = 0;
656
657 const fourth = r.findChunkedLen(data[first + second + third ..]);
658 try testing.expectEqual(@as(u32, 16), fourth);
659 try testing.expectEqual(@as(u64, 0xffffffffffffffff), r.next_chunk_length);
660 try testing.expectEqual(State.invalid, r.state);
661}
662
663test "HeadersParser.read length" {
664 var r = HeadersParser.initDynamic(256);
665 defer r.header_bytes.deinit(std.testing.allocator);
666 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
667 var fbs = std.io.fixedBufferStream(data);
668
669 try r.waitForCompleteHead(fbs.reader(), std.testing.allocator);
670 var buf: [8]u8 = undefined;
671
672 r.next_chunk_length = 5;
673 const len = try r.read(fbs.reader(), &buf, false);
674 try std.testing.expectEqual(@as(usize, 5), len);
675 try std.testing.expectEqualStrings("Hello", buf[0..len]);
676
677 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.header_bytes.items);
678}
679
680test "HeadersParser.read chunked" {
681 var r = HeadersParser.initDynamic(256);
682 defer r.header_bytes.deinit(std.testing.allocator);
683 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n";
684 var fbs = std.io.fixedBufferStream(data);
685
686 try r.waitForCompleteHead(fbs.reader(), std.testing.allocator);
687 var buf: [8]u8 = undefined;
688
689 r.state = .chunk_head_size;
690 const len = try r.read(fbs.reader(), &buf, false);
691 try std.testing.expectEqual(@as(usize, 5), len);
692 try std.testing.expectEqualStrings("Hello", buf[0..len]);
693
694 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.header_bytes.items);
695}
696
697test "HeadersParser.read chunked trailer" {
698 var r = HeadersParser.initDynamic(256);
699 defer r.header_bytes.deinit(std.testing.allocator);
700 const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n";
701 var fbs = std.io.fixedBufferStream(data);
702
703 try r.waitForCompleteHead(fbs.reader(), std.testing.allocator);
704 var buf: [8]u8 = undefined;
705
706 r.state = .chunk_head_size;
707 const len = try r.read(fbs.reader(), &buf, false);
708 try std.testing.expectEqual(@as(usize, 5), len);
709 try std.testing.expectEqualStrings("Hello", buf[0..len]);
710
711 try r.waitForCompleteHead(fbs.reader(), std.testing.allocator);
712
713 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.header_bytes.items);
714}