authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-14 12:37:32-05:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-04-17 19:15:55-05:00
log134294230a08d531afd0a6d823ae3046b1699b0f
tree771bbcec0a45f293bba1ae3dd8b8fea4ddeb0df9
parent96533b1289f210b415e12d4cf5bbac466279c2e5
signature Commit is signed but in an unrecognized format.

std.http: add Headers


5 files changed, 733 insertions(+), 318 deletions(-)

lib/std/http.zig+4-5
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1pub const Client = @import("http/Client.zig");1pub const Client = @import("http/Client.zig");
2pub const Server = @import("http/Server.zig");2pub const Server = @import("http/Server.zig");
3pub const protocol = @import("http/protocol.zig");3pub const protocol = @import("http/protocol.zig");
4const headers = @import("http/Headers.zig");
5
6pub const Headers = headers.Headers;
7pub const Header = headers.HeaderEntry;
48
5pub const Version = enum {9pub const Version = enum {
6 @"HTTP/1.0",10 @"HTTP/1.0",
...@@ -265,11 +269,6 @@ pub const Connection = enum {...@@ -265,11 +269,6 @@ pub const Connection = enum {
265 close,269 close,
266};270};
267271
268pub const Header = struct {
269 name: []const u8,
270 value: []const u8,
271};
272
273const std = @import("std.zig");272const std = @import("std.zig");
274273
275test {274test {
lib/std/http/Client.zig+190-166
...@@ -348,140 +348,125 @@ pub const Compression = union(enum) {...@@ -348,140 +348,125 @@ pub const Compression = union(enum) {
348348
349/// A HTTP response originating from a server.349/// A HTTP response originating from a server.
350pub const Response = struct {350pub const Response = struct {
351 pub const Headers = struct {351 pub const ParseError = Allocator.Error || error{
352 status: http.Status,352 ShortHttpStatusLine,
353 version: http.Version,353 BadHttpVersion,
354 location: ?[]const u8 = null,354 HttpHeadersInvalid,
355 content_length: ?u64 = null,355 HttpHeaderContinuationsUnsupported,
356 transfer_encoding: ?http.TransferEncoding = null,356 HttpTransferEncodingUnsupported,
357 transfer_compression: ?http.ContentEncoding = null,357 HttpConnectionHeaderUnsupported,
358 connection: http.Connection = .close,358 InvalidContentLength,
359 upgrade: ?[]const u8 = null,359 CompressionNotSupported,
360360 };
361 pub const ParseError = error{
362 ShortHttpStatusLine,
363 BadHttpVersion,
364 HttpHeadersInvalid,
365 HttpHeaderContinuationsUnsupported,
366 HttpTransferEncodingUnsupported,
367 HttpConnectionHeaderUnsupported,
368 InvalidContentLength,
369 CompressionNotSupported,
370 };
371
372 pub fn parse(bytes: []const u8) ParseError!Headers {
373 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
374
375 const first_line = it.next() orelse return error.HttpHeadersInvalid;
376 if (first_line.len < 12)
377 return error.ShortHttpStatusLine;
378
379 const version: http.Version = switch (int64(first_line[0..8])) {
380 int64("HTTP/1.0") => .@"HTTP/1.0",
381 int64("HTTP/1.1") => .@"HTTP/1.1",
382 else => return error.BadHttpVersion,
383 };
384 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
385 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
386
387 var headers: Headers = .{
388 .version = version,
389 .status = status,
390 };
391
392 while (it.next()) |line| {
393 if (line.len == 0) return error.HttpHeadersInvalid;
394 switch (line[0]) {
395 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
396 else => {},
397 }
398361
399 var line_it = mem.tokenize(u8, line, ": ");362 pub fn parse(res: *Response, bytes: []const u8) ParseError!void {
400 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;363 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
401 const header_value = line_it.rest();
402 if (std.ascii.eqlIgnoreCase(header_name, "location")) {
403 if (headers.location != null) return error.HttpHeadersInvalid;
404 headers.location = header_value;
405 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
406 if (headers.content_length != null) return error.HttpHeadersInvalid;
407 headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
408 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
409 // Transfer-Encoding: second, first
410 // Transfer-Encoding: deflate, chunked
411 var iter = mem.splitBackwards(u8, header_value, ",");
412
413 if (iter.next()) |first| {
414 const trimmed = mem.trim(u8, first, " ");
415
416 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
417 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
418 headers.transfer_encoding = te;
419 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
420 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
421 headers.transfer_compression = ce;
422 } else {
423 return error.HttpTransferEncodingUnsupported;
424 }
425 }
426364
427 if (iter.next()) |second| {365 const first_line = it.next() orelse return error.HttpHeadersInvalid;
428 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;366 if (first_line.len < 12)
367 return error.ShortHttpStatusLine;
429368
430 const trimmed = mem.trim(u8, second, " ");369 const version: http.Version = switch (int64(first_line[0..8])) {
370 int64("HTTP/1.0") => .@"HTTP/1.0",
371 int64("HTTP/1.1") => .@"HTTP/1.1",
372 else => return error.BadHttpVersion,
373 };
374 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
375 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
376 const reason = mem.trimLeft(u8, first_line[12..], " ");
377
378 res.version = version;
379 res.status = status;
380 res.reason = reason;
381
382 while (it.next()) |line| {
383 if (line.len == 0) return error.HttpHeadersInvalid;
384 switch (line[0]) {
385 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
386 else => {},
387 }
431388
432 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {389 var line_it = mem.tokenize(u8, line, ": ");
433 headers.transfer_compression = ce;390 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
434 } else {391 const header_value = line_it.rest();
435 return error.HttpTransferEncodingUnsupported;392
436 }393 try res.headers.append(header_name, header_value);
394
395 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
396 if (res.content_length != null) return error.HttpHeadersInvalid;
397 res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
398 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
399 // Transfer-Encoding: second, first
400 // Transfer-Encoding: deflate, chunked
401 var iter = mem.splitBackwards(u8, header_value, ",");
402
403 if (iter.next()) |first| {
404 const trimmed = mem.trim(u8, first, " ");
405
406 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
407 if (res.transfer_encoding != null) return error.HttpHeadersInvalid;
408 res.transfer_encoding = te;
409 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
410 if (res.transfer_compression != null) return error.HttpHeadersInvalid;
411 res.transfer_compression = ce;
412 } else {
413 return error.HttpTransferEncodingUnsupported;
437 }414 }
415 }
438416
439 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;417 if (iter.next()) |second| {
440 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {418 if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
441 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
442419
443 const trimmed = mem.trim(u8, header_value, " ");420 const trimmed = mem.trim(u8, second, " ");
444421
445 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {422 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
446 headers.transfer_compression = ce;423 res.transfer_compression = ce;
447 } else {424 } else {
448 return error.HttpTransferEncodingUnsupported;425 return error.HttpTransferEncodingUnsupported;
449 }426 }
450 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
451 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
452 headers.connection = .keep_alive;
453 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
454 headers.connection = .close;
455 } else {
456 return error.HttpConnectionHeaderUnsupported;
457 }
458 } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
459 headers.upgrade = header_value;
460 }427 }
461 }
462428
463 return headers;429 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
464 }430 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
431 if (res.transfer_compression != null) return error.HttpHeadersInvalid;
465432
466 inline fn int64(array: *const [8]u8) u64 {433 const trimmed = mem.trim(u8, header_value, " ");
467 return @bitCast(u64, array.*);
468 }
469434
470 fn parseInt3(nnn: @Vector(3, u8)) u10 {435 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
471 const zero: @Vector(3, u8) = .{ '0', '0', '0' };436 res.transfer_compression = ce;
472 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };437 } else {
473 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);438 return error.HttpTransferEncodingUnsupported;
439 }
440 }
474 }441 }
442 }
475443
476 test parseInt3 {444 inline fn int64(array: *const [8]u8) u64 {
477 const expectEqual = testing.expectEqual;445 return @bitCast(u64, array.*);
478 try expectEqual(@as(u10, 0), parseInt3("000".*));446 }
479 try expectEqual(@as(u10, 418), parseInt3("418".*));
480 try expectEqual(@as(u10, 999), parseInt3("999".*));
481 }
482 };
483447
484 headers: Headers = undefined,448 fn parseInt3(nnn: @Vector(3, u8)) u10 {
449 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
450 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
451 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
452 }
453
454 test parseInt3 {
455 const expectEqual = testing.expectEqual;
456 try expectEqual(@as(u10, 0), parseInt3("000".*));
457 try expectEqual(@as(u10, 418), parseInt3("418".*));
458 try expectEqual(@as(u10, 999), parseInt3("999".*));
459 }
460
461 version: http.Version,
462 status: http.Status,
463 reason: []const u8,
464
465 content_length: ?u64 = null,
466 transfer_encoding: ?http.TransferEncoding = null,
467 transfer_compression: ?http.ContentEncoding = null,
468
469 headers: http.Headers,
485 parser: proto.HeadersParser,470 parser: proto.HeadersParser,
486 compression: Compression = .none,471 compression: Compression = .none,
487 skip: bool = false,472 skip: bool = false,
...@@ -491,22 +476,14 @@ pub const Response = struct {...@@ -491,22 +476,14 @@ pub const Response = struct {
491///476///
492/// Order of operations: request[ -> write -> finish] -> do -> read477/// Order of operations: request[ -> write -> finish] -> do -> read
493pub const Request = struct {478pub const Request = struct {
494 pub const Headers = struct {
495 version: http.Version = .@"HTTP/1.1",
496 method: http.Method = .GET,
497 user_agent: []const u8 = "zig (std.http)",
498 connection: http.Connection = .keep_alive,
499 transfer_encoding: RequestTransfer = .none,
500
501 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
502 };
503
504 uri: Uri,479 uri: Uri,
505 client: *Client,480 client: *Client,
506 connection: *ConnectionPool.Node,481 connection: *ConnectionPool.Node,
507 /// These are stored in Request so that they are available when following482
508 /// redirects.483 method: http.Method,
509 headers: Headers,484 version: http.Version = .@"HTTP/1.1",
485 headers: http.Headers,
486 transfer_encoding: RequestTransfer = .none,
510487
511 redirects_left: u32,488 redirects_left: u32,
512 handle_redirects: bool,489 handle_redirects: bool,
...@@ -526,6 +503,7 @@ pub const Request = struct {...@@ -526,6 +503,7 @@ pub const Request = struct {
526 }503 }
527504
528 if (req.response.parser.header_bytes_owned) {505 if (req.response.parser.header_bytes_owned) {
506 req.response.headers.deinit();
529 req.response.parser.header_bytes.deinit(req.client.allocator);507 req.response.parser.header_bytes.deinit(req.client.allocator);
530 }508 }
531509
...@@ -540,14 +518,14 @@ pub const Request = struct {...@@ -540,14 +518,14 @@ pub const Request = struct {
540 req.* = undefined;518 req.* = undefined;
541 }519 }
542520
543 pub fn start(req: *Request, uri: Uri, headers: Headers) !void {521 pub fn start(req: *Request, uri: Uri) !void {
544 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());522 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
545 const w = buffered.writer();523 const w = buffered.writer();
546524
547 try w.writeAll(@tagName(headers.method));525 try w.writeAll(@tagName(req.method));
548 try w.writeByte(' ');526 try w.writeByte(' ');
549527
550 if (req.headers.method == .CONNECT) {528 if (req.method == .CONNECT) {
551 try w.writeAll(uri.host.?);529 try w.writeAll(uri.host.?);
552 try w.writeByte(':');530 try w.writeByte(':');
553 try w.print("{}", .{uri.port.?});531 try w.print("{}", .{uri.port.?});
...@@ -559,33 +537,62 @@ pub const Request = struct {...@@ -559,33 +537,62 @@ pub const Request = struct {
559 }537 }
560538
561 try w.writeByte(' ');539 try w.writeByte(' ');
562 try w.writeAll(@tagName(headers.version));540 try w.writeAll(@tagName(req.version));
563 try w.writeAll("\r\nHost: ");541 try w.writeAll("\r\n");
564 try w.writeAll(uri.host.?);542
565 try w.writeAll("\r\nUser-Agent: ");543 if (!req.headers.contains("host")) {
566 try w.writeAll(headers.user_agent);544 try w.writeAll("Host: ");
567 if (headers.connection == .close) {545 try w.writeAll(uri.host.?);
568 try w.writeAll("\r\nConnection: close");546 try w.writeAll("\r\n");
569 } else {
570 try w.writeAll("\r\nConnection: keep-alive");
571 }547 }
572 try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
573 try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers.
574548
575 switch (headers.transfer_encoding) {549 if (!req.headers.contains("user-agent")) {
576 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),550 try w.writeAll("User-Agent: zig/");
577 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),551 try w.writeAll(@import("builtin").zig_version_string);
578 .none => {},552 try w.writeAll(" (std.http)\r\n");
579 }553 }
580554
581 for (headers.custom) |header| {555 if (!req.headers.contains("connection")) {
582 try w.writeAll("\r\n");556 try w.writeAll("Connection: keep-alive\r\n");
583 try w.writeAll(header.name);
584 try w.writeAll(": ");
585 try w.writeAll(header.value);
586 }557 }
587558
588 try w.writeAll("\r\n\r\n");559 if (!req.headers.contains("accept-encoding")) {
560 try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");
561 }
562
563 if (!req.headers.contains("te")) {
564 try w.writeAll("TE: gzip, deflate, trailers\r\n");
565 }
566
567 const has_transfer_encoding = req.headers.contains("transfer-encoding");
568 const has_content_length = req.headers.contains("content-length");
569
570 if (!has_transfer_encoding and !has_content_length) {
571 switch (req.transfer_encoding) {
572 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
573 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
574 .none => {},
575 }
576 } else {
577 if (has_content_length) {
578 const content_length = try std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10);
579
580 req.transfer_encoding = .{ .content_length = content_length };
581 } else if (has_transfer_encoding) {
582 const transfer_encoding = req.headers.getFirstValue("content-length").?;
583 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
584 req.transfer_encoding = .chunked;
585 } else {
586 return error.UnsupportedTransferEncoding;
587 }
588 } else {
589 req.transfer_encoding = .none;
590 }
591 }
592
593 try w.print("{}", .{req.headers});
594
595 try w.writeAll("\r\n");
589596
590 try buffered.flush();597 try buffered.flush();
591 }598 }
...@@ -611,7 +618,7 @@ pub const Request = struct {...@@ -611,7 +618,7 @@ pub const Request = struct {
611 return index;618 return index;
612 }619 }
613620
614 pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };621 pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };
615622
616 /// Waits for a response from the server and parses any headers that are sent.623 /// Waits for a response from the server and parses any headers that are sent.
617 /// This function will block until the final response is received.624 /// This function will block until the final response is received.
...@@ -629,33 +636,39 @@ pub const Request = struct {...@@ -629,33 +636,39 @@ pub const Request = struct {
629 if (req.response.parser.state.isContent()) break;636 if (req.response.parser.state.isContent()) break;
630 }637 }
631638
632 req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);639 req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false };
640 try req.response.parse(req.response.parser.header_bytes.items);
633641
634 if (req.response.headers.status == .switching_protocols) {642 if (req.response.status == .switching_protocols) {
635 req.connection.data.closing = false;643 req.connection.data.closing = false;
636 req.response.parser.done = true;644 req.response.parser.done = true;
637 }645 }
638646
639 if (req.headers.method == .CONNECT and req.response.headers.status == .ok) {647 if (req.method == .CONNECT and req.response.status == .ok) {
640 req.connection.data.closing = false;648 req.connection.data.closing = false;
641 req.connection.data.proxied = true;649 req.connection.data.proxied = true;
642 req.response.parser.done = true;650 req.response.parser.done = true;
643 }651 }
644652
645 if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {653 const req_connection = req.headers.getFirstValue("connection");
654 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
655
656 const res_connection = req.response.headers.getFirstValue("connection");
657 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
658 if (req_keepalive and res_keepalive) {
646 req.connection.data.closing = false;659 req.connection.data.closing = false;
647 } else {660 } else {
648 req.connection.data.closing = true;661 req.connection.data.closing = true;
649 }662 }
650663
651 if (req.response.headers.transfer_encoding) |te| {664 if (req.response.transfer_encoding) |te| {
652 switch (te) {665 switch (te) {
653 .chunked => {666 .chunked => {
654 req.response.parser.next_chunk_length = 0;667 req.response.parser.next_chunk_length = 0;
655 req.response.parser.state = .chunk_head_size;668 req.response.parser.state = .chunk_head_size;
656 },669 },
657 }670 }
658 } else if (req.response.headers.content_length) |cl| {671 } else if (req.response.content_length) |cl| {
659 req.response.parser.next_chunk_length = cl;672 req.response.parser.next_chunk_length = cl;
660673
661 if (cl == 0) req.response.parser.done = true;674 if (cl == 0) req.response.parser.done = true;
...@@ -663,7 +676,7 @@ pub const Request = struct {...@@ -663,7 +676,7 @@ pub const Request = struct {
663 req.response.parser.done = true;676 req.response.parser.done = true;
664 }677 }
665678
666 if (req.response.headers.status.class() == .redirect and req.handle_redirects) {679 if (req.response.status.class() == .redirect and req.handle_redirects) {
667 req.response.skip = true;680 req.response.skip = true;
668681
669 const empty = @as([*]u8, undefined)[0..0];682 const empty = @as([*]u8, undefined)[0..0];
...@@ -671,7 +684,7 @@ pub const Request = struct {...@@ -671,7 +684,7 @@ pub const Request = struct {
671684
672 if (req.redirects_left == 0) return error.TooManyHttpRedirects;685 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
673686
674 const location = req.response.headers.location orelse687 const location = req.response.headers.getFirstValue("location") orelse
675 return error.HttpRedirectMissingLocation;688 return error.HttpRedirectMissingLocation;
676 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);689 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
677690
...@@ -683,6 +696,8 @@ pub const Request = struct {...@@ -683,6 +696,8 @@ pub const Request = struct {
683 req.arena = new_arena;696 req.arena = new_arena;
684697
685 const new_req = try req.client.request(resolved_url, req.headers, .{698 const new_req = try req.client.request(resolved_url, req.headers, .{
699 .method = req.method,
700 .version = req.version,
686 .max_redirects = req.redirects_left - 1,701 .max_redirects = req.redirects_left - 1,
687 .header_strategy = if (req.response.parser.header_bytes_owned) .{702 .header_strategy = if (req.response.parser.header_bytes_owned) .{
688 .dynamic = req.response.parser.max_header_bytes,703 .dynamic = req.response.parser.max_header_bytes,
...@@ -695,7 +710,7 @@ pub const Request = struct {...@@ -695,7 +710,7 @@ pub const Request = struct {
695 } else {710 } else {
696 req.response.skip = false;711 req.response.skip = false;
697 if (!req.response.parser.done) {712 if (!req.response.parser.done) {
698 if (req.response.headers.transfer_compression) |tc| switch (tc) {713 if (req.response.transfer_compression) |tc| switch (tc) {
699 .compress => return error.CompressionNotSupported,714 .compress => return error.CompressionNotSupported,
700 .deflate => req.response.compression = .{715 .deflate => req.response.compression = .{
701 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,716 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
...@@ -789,7 +804,7 @@ pub const Request = struct {...@@ -789,7 +804,7 @@ pub const Request = struct {
789804
790 /// Finish the body of a request. This notifies the server that you have no more data to send.805 /// Finish the body of a request. This notifies the server that you have no more data to send.
791 pub fn finish(req: *Request) FinishError!void {806 pub fn finish(req: *Request) FinishError!void {
792 switch (req.headers.transfer_encoding) {807 switch (req.transfer_encoding) {
793 .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| {808 .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| {
794 req.client.last_error = .{ .write = err };809 req.client.last_error = .{ .write = err };
795 return error.WriteFailed;810 return error.WriteFailed;
...@@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -908,14 +923,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
908 }923 }
909}924}
910925
911pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || BufferedConnection.WriteError || error{926pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || std.fmt.ParseIntError || BufferedConnection.WriteError || error{
912 UnsupportedUrlScheme,927 UnsupportedUrlScheme,
913 UriMissingHost,928 UriMissingHost,
914929
915 CertificateBundleLoadFailure,930 CertificateBundleLoadFailure,
931 UnsupportedTransferEncoding,
916};932};
917933
918pub const Options = struct {934pub const Options = struct {
935 method: http.Method = .GET,
936 version: http.Version = .@"HTTP/1.1",
937
919 handle_redirects: bool = true,938 handle_redirects: bool = true,
920 max_redirects: u32 = 3,939 max_redirects: u32 = 3,
921 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },940 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
...@@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -946,7 +965,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
946965
947/// Form and send a http request to a server.966/// Form and send a http request to a server.
948/// This function is threadsafe.967/// This function is threadsafe.
949pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {968pub fn request(client: *Client, uri: Uri, headers: http.Headers, options: Options) RequestError!Request {
950 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;969 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
951970
952 const port: u16 = uri.port orelse switch (protocol) {971 const port: u16 = uri.port orelse switch (protocol) {
...@@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt...@@ -973,9 +992,14 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
973 .client = client,992 .client = client,
974 .connection = conn,993 .connection = conn,
975 .headers = headers,994 .headers = headers,
995 .method = options.method,
996 .version = options.version,
976 .redirects_left = options.max_redirects,997 .redirects_left = options.max_redirects,
977 .handle_redirects = options.handle_redirects,998 .handle_redirects = options.handle_redirects,
978 .response = .{999 .response = .{
1000 .status = undefined,
1001 .version = undefined,
1002 .headers = undefined,
979 .parser = switch (options.header_strategy) {1003 .parser = switch (options.header_strategy) {
980 .dynamic => |max| proto.HeadersParser.initDynamic(max),1004 .dynamic => |max| proto.HeadersParser.initDynamic(max),
981 .static => |buf| proto.HeadersParser.initStatic(buf),1005 .static => |buf| proto.HeadersParser.initStatic(buf),
...@@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt...@@ -987,7 +1011,7 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
9871011
988 req.arena = std.heap.ArenaAllocator.init(client.allocator);1012 req.arena = std.heap.ArenaAllocator.init(client.allocator);
9891013
990 try req.start(uri, headers);1014 try req.start(uri);
9911015
992 return req;1016 return req;
993}1017}
lib/std/http/Headers.zig created+386
...@@ -0,0 +1,386 @@
1const std = @import("../std.zig");
2
3const Allocator = std.mem.Allocator;
4
5const testing = std.testing;
6const ascii = std.ascii;
7const assert = std.debug.assert;
8
9pub const HeaderList = std.ArrayListUnmanaged(HeaderEntry);
10pub const HeaderIndexList = std.ArrayListUnmanaged(usize);
11pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage);
12
13pub const CaseInsensitiveStringContext = struct {
14 pub fn hash(self: @This(), s: []const u8) u64 {
15 _ = self;
16 var buf: [64]u8 = undefined;
17 var i: u8 = 0;
18
19 var h = std.hash.Wyhash.init(0);
20 while (i < s.len) : (i += 64) {
21 const left = @min(64, s.len - i);
22 const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
23 h.update(ret);
24 }
25
26 return h.final();
27 }
28
29 pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
30 _ = self;
31 return ascii.eqlIgnoreCase(a, b);
32 }
33};
34
35pub const HeaderEntry = struct {
36 name: []const u8,
37 value: []const u8,
38
39 pub fn modify(entry: *HeaderEntry, allocator: Allocator, new_value: []const u8) !void {
40 if (entry.value.len <= new_value.len) {
41 std.mem.copy(u8, @constCast(entry.value), new_value);
42 } else {
43 allocator.free(entry.value);
44
45 entry.value = try allocator.dupe(u8, new_value);
46 }
47 }
48
49 fn lessThan(ctx: void, a: HeaderEntry, b: HeaderEntry) bool {
50 _ = ctx;
51 if (a.name.ptr == b.name.ptr) return false;
52
53 return ascii.lessThanIgnoreCase(a.name, b.name);
54 }
55};
56
57pub const Headers = struct {
58 allocator: Allocator,
59 list: HeaderList = .{},
60 index: HeaderIndex = .{},
61
62 /// When this is false, names and values will not be duplicated.
63 /// Use with caution.
64 owned: bool = true,
65
66 pub fn init(allocator: Allocator) Headers {
67 return .{ .allocator = allocator };
68 }
69
70 pub fn deinit(headers: *Headers) void {
71 var it = headers.index.iterator();
72 while (it.next()) |entry| {
73 entry.value_ptr.deinit(headers.allocator);
74
75 if (headers.owned) headers.allocator.free(entry.key_ptr.*);
76 }
77
78 for (headers.list.items) |entry| {
79 if (headers.owned) headers.allocator.free(entry.value);
80 }
81
82 headers.index.deinit(headers.allocator);
83 headers.list.deinit(headers.allocator);
84
85 headers.* = undefined;
86 }
87
88 /// Appends a header to the list. Both name and value are copied.
89 pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
90 const n = headers.list.items.len;
91
92 const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value;
93 errdefer if (headers.owned) headers.allocator.free(value_duped);
94
95 var entry = HeaderEntry{ .name = undefined, .value = value_duped };
96
97 if (headers.index.getEntry(name)) |kv| {
98 entry.name = kv.key_ptr.*;
99 try kv.value_ptr.append(headers.allocator, n);
100 } else {
101 const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name;
102 errdefer if (headers.owned) headers.allocator.free(name_duped);
103
104 entry.name = name_duped;
105
106 var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1);
107 errdefer new_index.deinit(headers.allocator);
108
109 new_index.appendAssumeCapacity(n);
110 try headers.index.put(headers.allocator, name_duped, new_index);
111 }
112
113 try headers.list.append(headers.allocator, entry);
114 }
115
116 pub fn contains(headers: Headers, name: []const u8) bool {
117 return headers.index.contains(name);
118 }
119
120 pub fn delete(headers: *Headers, name: []const u8) bool {
121 if (headers.index.fetchRemove(name)) |kv| {
122 var index = kv.value;
123
124 // iterate backwards
125 var i = index.items.len;
126 while (i > 0) {
127 i -= 1;
128 const data_index = index.items[i];
129 const removed = headers.list.orderedRemove(data_index);
130
131 assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted
132 if (headers.owned) headers.allocator.free(removed.value);
133 }
134
135 if (headers.owned) headers.allocator.free(kv.key);
136 index.deinit(headers.allocator);
137 headers.rebuildIndex();
138
139 return true;
140 } else {
141 return false;
142 }
143 }
144
145 /// Returns the index of the first occurrence of a header with the given name.
146 pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
147 const index = headers.index.get(name) orelse return null;
148
149 return index.items[0];
150 }
151
152 /// Returns a list of indices containing headers with the given name.
153 pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
154 const index = headers.index.get(name) orelse return null;
155
156 return index.items;
157 }
158
159 /// Returns the entry of the first occurrence of a header with the given name.
160 pub fn getFirstEntry(headers: Headers, name: []const u8) ?HeaderEntry {
161 const first_index = headers.firstIndexOf(name) orelse return null;
162
163 return headers.list.items[first_index];
164 }
165
166 /// Returns a slice containing each header with the given name.
167 /// The caller owns the returned slice, but NOT the values in the slice.
168 pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const HeaderEntry {
169 const indices = headers.getIndices(name) orelse return null;
170
171 const buf = try allocator.alloc(HeaderEntry, indices.len);
172 for (indices, 0..) |idx, n| {
173 buf[n] = headers.list.items[idx];
174 }
175
176 return buf;
177 }
178
179 /// Returns the value in the entry of the first occurrence of a header with the given name.
180 pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 {
181 const first_index = headers.firstIndexOf(name) orelse return null;
182
183 return headers.list.items[first_index].value;
184 }
185
186 /// Returns a slice containing the value of each header with the given name.
187 /// The caller owns the returned slice, but NOT the values in the slice.
188 pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 {
189 const indices = headers.getIndices(name) orelse return null;
190
191 const buf = try allocator.alloc([]const u8, indices.len);
192 for (indices, 0..) |idx, n| {
193 buf[n] = headers.list.items[idx].value;
194 }
195
196 return buf;
197 }
198
199 fn rebuildIndex(headers: *Headers) void {
200 // clear out the indexes
201 var it = headers.index.iterator();
202 while (it.next()) |entry| {
203 entry.value_ptr.shrinkRetainingCapacity(0);
204 }
205
206 // fill up indexes again; we know capacity is fine from before
207 for (headers.list.items, 0..) |entry, i| {
208 headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
209 }
210 }
211
212 /// Sorts the headers in lexicographical order.
213 pub fn sort(headers: *Headers) void {
214 std.sort.sort(HeaderEntry, headers.list.items, {}, HeaderEntry.lessThan);
215 headers.rebuildIndex();
216 }
217
218 /// Writes the headers to the given stream.
219 pub fn format(
220 headers: Headers,
221 comptime fmt: []const u8,
222 options: std.fmt.FormatOptions,
223 out_stream: anytype,
224 ) !void {
225 _ = fmt;
226 _ = options;
227
228 for (headers.list.items) |entry| {
229 if (entry.value.len == 0) continue;
230
231 try out_stream.writeAll(entry.name);
232 try out_stream.writeAll(": ");
233 try out_stream.writeAll(entry.value);
234 try out_stream.writeAll("\r\n");
235 }
236 }
237
238 /// Writes all of the headers with the given name to the given stream, separated by commas.
239 ///
240 /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2
241 pub fn formatCommaSeparated(
242 headers: Headers,
243 name: []const u8,
244 out_stream: anytype,
245 ) !void {
246 const indices = headers.getIndices(name) orelse return;
247
248 try out_stream.writeAll(name);
249 try out_stream.writeAll(": ");
250
251 for (indices, 0..) |idx, n| {
252 if (n != 0) try out_stream.writeAll(", ");
253 try out_stream.writeAll(headers.list.items[idx].value);
254 }
255
256 try out_stream.writeAll("\r\n");
257 }
258};
259
260test "Headers.append" {
261 var h = Headers{ .allocator = std.testing.allocator };
262 defer h.deinit();
263
264 try h.append("foo", "bar");
265 try h.append("hello", "world");
266
267 try testing.expect(h.contains("Foo"));
268 try testing.expect(!h.contains("Bar"));
269}
270
271test "Headers.delete" {
272 var h = Headers{ .allocator = std.testing.allocator };
273 defer h.deinit();
274
275 try h.append("foo", "bar");
276 try h.append("hello", "world");
277
278 try testing.expect(h.contains("Foo"));
279
280 _ = h.delete("Foo");
281
282 try testing.expect(!h.contains("foo"));
283}
284
285test "Headers consistency" {
286 var h = Headers{ .allocator = std.testing.allocator };
287 defer h.deinit();
288
289 try h.append("foo", "bar");
290 try h.append("hello", "world");
291 _ = h.delete("Foo");
292
293 try h.append("foo", "bar");
294 try h.append("bar", "world");
295 try h.append("foo", "baz");
296 try h.append("baz", "hello");
297
298 try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello"));
299 try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo"));
300 try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar"));
301 try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz"));
302 try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog"));
303
304 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?);
305 try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?);
306 try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?);
307 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?);
308 try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog"));
309
310 try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value);
311 try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value);
312 try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value);
313 try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value);
314
315 const hello_entries = (try h.getEntries(testing.allocator, "hello")).?;
316 defer testing.allocator.free(hello_entries);
317 try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
318 .{ .name = "hello", .value = "world" },
319 }), hello_entries);
320
321 const foo_entries = (try h.getEntries(testing.allocator, "foo")).?;
322 defer testing.allocator.free(foo_entries);
323 try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
324 .{ .name = "foo", .value = "bar" },
325 .{ .name = "foo", .value = "baz" },
326 }), foo_entries);
327
328 const bar_entries = (try h.getEntries(testing.allocator, "bar")).?;
329 defer testing.allocator.free(bar_entries);
330 try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
331 .{ .name = "bar", .value = "world" },
332 }), bar_entries);
333
334 const baz_entries = (try h.getEntries(testing.allocator, "baz")).?;
335 defer testing.allocator.free(baz_entries);
336 try testing.expectEqualDeep(@as([]const HeaderEntry, &[_]HeaderEntry{
337 .{ .name = "baz", .value = "hello" },
338 }), baz_entries);
339
340 const pog_entries = (try h.getEntries(testing.allocator, "pog"));
341 try testing.expectEqual(@as(?[]const HeaderEntry, null), pog_entries);
342
343 try testing.expectEqualStrings("world", h.getFirstValue("hello").?);
344 try testing.expectEqualStrings("bar", h.getFirstValue("foo").?);
345 try testing.expectEqualStrings("world", h.getFirstValue("bar").?);
346 try testing.expectEqualStrings("hello", h.getFirstValue("baz").?);
347 try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog"));
348
349 const hello_values = (try h.getValues(testing.allocator, "hello")).?;
350 defer testing.allocator.free(hello_values);
351 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values);
352
353 const foo_values = (try h.getValues(testing.allocator, "foo")).?;
354 defer testing.allocator.free(foo_values);
355 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values);
356
357 const bar_values = (try h.getValues(testing.allocator, "bar")).?;
358 defer testing.allocator.free(bar_values);
359 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values);
360
361 const baz_values = (try h.getValues(testing.allocator, "baz")).?;
362 defer testing.allocator.free(baz_values);
363 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values);
364
365 const pog_values = (try h.getValues(testing.allocator, "pog"));
366 try testing.expectEqual(@as(?[]const []const u8, null), pog_values);
367
368 h.sort();
369
370 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?);
371 try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?);
372 try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?);
373 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?);
374
375 const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h});
376 defer testing.allocator.free(formatted_values);
377
378 try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values);
379
380 var buf: [128]u8 = undefined;
381 var fbs = std.io.fixedBufferStream(&buf);
382 const writer = fbs.writer();
383
384 try h.formatCommaSeparated("foo", writer);
385 try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten());
386}
lib/std/http/Server.zig+149-146
...@@ -157,134 +157,120 @@ pub const BufferedConnection = struct {...@@ -157,134 +157,120 @@ pub const BufferedConnection = struct {
157157
158/// A HTTP request originating from a client.158/// A HTTP request originating from a client.
159pub const Request = struct {159pub const Request = struct {
160 pub const Headers = struct {160 pub const ParseError = Allocator.Error || error{
161 method: http.Method,161 ShortHttpStatusLine,
162 target: []const u8,162 BadHttpVersion,
163 version: http.Version,163 UnknownHttpMethod,
164 content_length: ?u64 = null,164 HttpHeadersInvalid,
165 transfer_encoding: ?http.TransferEncoding = null,165 HttpHeaderContinuationsUnsupported,
166 transfer_compression: ?http.ContentEncoding = null,166 HttpTransferEncodingUnsupported,
167 connection: http.Connection = .close,167 HttpConnectionHeaderUnsupported,
168 host: ?[]const u8 = null,168 InvalidCharacter,
169169 };
170 pub const ParseError = error{
171 ShortHttpStatusLine,
172 BadHttpVersion,
173 UnknownHttpMethod,
174 HttpHeadersInvalid,
175 HttpHeaderContinuationsUnsupported,
176 HttpTransferEncodingUnsupported,
177 HttpConnectionHeaderUnsupported,
178 InvalidCharacter,
179 };
180
181 pub fn parse(bytes: []const u8) !Headers {
182 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
183
184 const first_line = it.next() orelse return error.HttpHeadersInvalid;
185 if (first_line.len < 10)
186 return error.ShortHttpStatusLine;
187
188 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
189 const method_str = first_line[0..method_end];
190 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
191170
192 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;171 pub fn parse(req: *Request, bytes: []const u8) !void {
193 if (version_start == method_end) return error.HttpHeadersInvalid;172 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
194173
195 const version_str = first_line[version_start + 1 ..];174 const first_line = it.next() orelse return error.HttpHeadersInvalid;
196 if (version_str.len != 8) return error.HttpHeadersInvalid;175 if (first_line.len < 10)
197 const version: http.Version = switch (int64(version_str[0..8])) {176 return error.ShortHttpStatusLine;
198 int64("HTTP/1.0") => .@"HTTP/1.0",
199 int64("HTTP/1.1") => .@"HTTP/1.1",
200 else => return error.BadHttpVersion,
201 };
202177
203 const target = first_line[method_end + 1 .. version_start];178 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
179 const method_str = first_line[0..method_end];
180 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
204181
205 var headers: Headers = .{182 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
206 .method = method,183 if (version_start == method_end) return error.HttpHeadersInvalid;
207 .target = target,
208 .version = version,
209 };
210184
211 while (it.next()) |line| {185 const version_str = first_line[version_start + 1 ..];
212 if (line.len == 0) return error.HttpHeadersInvalid;186 if (version_str.len != 8) return error.HttpHeadersInvalid;
213 switch (line[0]) {187 const version: http.Version = switch (int64(version_str[0..8])) {
214 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,188 int64("HTTP/1.0") => .@"HTTP/1.0",
215 else => {},189 int64("HTTP/1.1") => .@"HTTP/1.1",
216 }190 else => return error.BadHttpVersion,
191 };
217192
218 var line_it = mem.tokenize(u8, line, ": ");193 const target = first_line[method_end + 1 .. version_start];
219 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
220 const header_value = line_it.rest();
221 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
222 if (headers.content_length != null) return error.HttpHeadersInvalid;
223 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
224 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
225 // Transfer-Encoding: second, first
226 // Transfer-Encoding: deflate, chunked
227 var iter = mem.splitBackwards(u8, header_value, ",");
228
229 if (iter.next()) |first| {
230 const trimmed = mem.trim(u8, first, " ");
231
232 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
233 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
234 headers.transfer_encoding = te;
235 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
236 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
237 headers.transfer_compression = ce;
238 } else {
239 return error.HttpTransferEncodingUnsupported;
240 }
241 }
242194
243 if (iter.next()) |second| {195 req.method = method;
244 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;196 req.target = target;
197 req.version = version;
245198
246 const trimmed = mem.trim(u8, second, " ");199 while (it.next()) |line| {
200 if (line.len == 0) return error.HttpHeadersInvalid;
201 switch (line[0]) {
202 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
203 else => {},
204 }
247205
248 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {206 var line_it = mem.tokenize(u8, line, ": ");
249 headers.transfer_compression = ce;207 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
250 } else {208 const header_value = line_it.rest();
251 return error.HttpTransferEncodingUnsupported;209
252 }210 try req.headers.append(header_name, header_value);
211
212 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
213 if (req.content_length != null) return error.HttpHeadersInvalid;
214 req.content_length = try std.fmt.parseInt(u64, header_value, 10);
215 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
216 // Transfer-Encoding: second, first
217 // Transfer-Encoding: deflate, chunked
218 var iter = mem.splitBackwards(u8, header_value, ",");
219
220 if (iter.next()) |first| {
221 const trimmed = mem.trim(u8, first, " ");
222
223 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
224 if (req.transfer_encoding != null) return error.HttpHeadersInvalid;
225 req.transfer_encoding = te;
226 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
227 if (req.transfer_compression != null) return error.HttpHeadersInvalid;
228 req.transfer_compression = ce;
229 } else {
230 return error.HttpTransferEncodingUnsupported;
253 }231 }
232 }
254233
255 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;234 if (iter.next()) |second| {
256 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {235 if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
257 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
258236
259 const trimmed = mem.trim(u8, header_value, " ");237 const trimmed = mem.trim(u8, second, " ");
260238
261 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {239 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
262 headers.transfer_compression = ce;240 req.transfer_compression = ce;
263 } else {241 } else {
264 return error.HttpTransferEncodingUnsupported;242 return error.HttpTransferEncodingUnsupported;
265 }243 }
266 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
267 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
268 headers.connection = .keep_alive;
269 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
270 headers.connection = .close;
271 } else {
272 return error.HttpConnectionHeaderUnsupported;
273 }
274 } else if (std.ascii.eqlIgnoreCase(header_name, "host")) {
275 headers.host = header_value;
276 }244 }
277 }
278245
279 return headers;246 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
280 }247 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
248 if (req.transfer_compression != null) return error.HttpHeadersInvalid;
281249
282 inline fn int64(array: *const [8]u8) u64 {250 const trimmed = mem.trim(u8, header_value, " ");
283 return @bitCast(u64, array.*);251
252 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
253 req.transfer_compression = ce;
254 } else {
255 return error.HttpTransferEncodingUnsupported;
256 }
257 }
284 }258 }
285 };259 }
260
261 inline fn int64(array: *const [8]u8) u64 {
262 return @bitCast(u64, array.*);
263 }
286264
287 headers: Headers = undefined,265 method: http.Method,
266 target: []const u8,
267 version: http.Version,
268
269 content_length: ?u64 = null,
270 transfer_encoding: ?http.TransferEncoding = null,
271 transfer_compression: ?http.ContentEncoding = null,
272
273 headers: http.Headers = undefined,
288 parser: proto.HeadersParser,274 parser: proto.HeadersParser,
289 compression: Compression = .none,275 compression: Compression = .none,
290};276};
...@@ -295,23 +281,17 @@ pub const Request = struct {...@@ -295,23 +281,17 @@ pub const Request = struct {
295/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]281/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]
296/// \ -> read /282/// \ -> read /
297pub const Response = struct {283pub const Response = struct {
298 pub const Headers = struct {284 version: http.Version = .@"HTTP/1.1",
299 version: http.Version = .@"HTTP/1.1",285 status: http.Status = .ok,
300 status: http.Status = .ok,286 reason: ?[]const u8 = null,
301 reason: ?[]const u8 = null,
302287
303 server: ?[]const u8 = "zig (std.http)",288 transfer_encoding: ResponseTransfer = .none,
304 connection: http.Connection = .keep_alive,
305 transfer_encoding: RequestTransfer = .none,
306
307 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
308 };
309289
310 server: *Server,290 server: *Server,
311 address: net.Address,291 address: net.Address,
312 connection: BufferedConnection,292 connection: BufferedConnection,
313293
314 headers: Headers = .{},294 headers: http.Headers,
315 request: Request,295 request: Request,
316296
317 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.297 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
...@@ -346,41 +326,54 @@ pub const Response = struct {...@@ -346,41 +326,54 @@ pub const Response = struct {
346 var buffered = std.io.bufferedWriter(res.connection.writer());326 var buffered = std.io.bufferedWriter(res.connection.writer());
347 const w = buffered.writer();327 const w = buffered.writer();
348328
349 try w.writeAll(@tagName(res.headers.version));329 try w.writeAll(@tagName(res.version));
350 try w.writeByte(' ');330 try w.writeByte(' ');
351 try w.print("{d}", .{@enumToInt(res.headers.status)});331 try w.print("{d}", .{@enumToInt(res.status)});
352 try w.writeByte(' ');332 try w.writeByte(' ');
353 if (res.headers.reason) |reason| {333 if (res.reason) |reason| {
354 try w.writeAll(reason);334 try w.writeAll(reason);
355 } else if (res.headers.status.phrase()) |phrase| {335 } else if (res.status.phrase()) |phrase| {
356 try w.writeAll(phrase);336 try w.writeAll(phrase);
357 }337 }
338 try w.writeAll("\r\n");
358339
359 if (res.headers.server) |server| {340 if (!res.headers.contains("server")) {
360 try w.writeAll("\r\nServer: ");341 try w.writeAll("Server: zig (std.http)\r\n");
361 try w.writeAll(server);
362 }342 }
363343
364 if (res.headers.connection == .close) {344 if (!res.headers.contains("connection")) {
365 try w.writeAll("\r\nConnection: close");345 try w.writeAll("Connection: keep-alive\r\n");
366 } else {
367 try w.writeAll("\r\nConnection: keep-alive");
368 }346 }
369347
370 switch (res.headers.transfer_encoding) {348 const has_transfer_encoding = res.headers.contains("transfer-encoding");
371 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),349 const has_content_length = res.headers.contains("content-length");
372 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
373 .none => {},
374 }
375350
376 for (res.headers.custom) |header| {351 if (!has_transfer_encoding and !has_content_length) {
377 try w.writeAll("\r\n");352 switch (res.transfer_encoding) {
378 try w.writeAll(header.name);353 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
379 try w.writeAll(": ");354 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
380 try w.writeAll(header.value);355 .none => {},
356 }
357 } else {
358 if (has_content_length) {
359 const content_length = try std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10);
360
361 res.transfer_encoding = .{ .content_length = content_length };
362 } else if (has_transfer_encoding) {
363 const transfer_encoding = res.headers.getFirstValue("content-length").?;
364 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
365 res.transfer_encoding = .chunked;
366 } else {
367 return error.UnsupportedTransferEncoding;
368 }
369 } else {
370 res.transfer_encoding = .none;
371 }
381 }372 }
382373
383 try w.writeAll("\r\n\r\n");374 try w.print("{}", .{res.headers});
375
376 try w.writeAll("\r\n");
384377
385 try buffered.flush();378 try buffered.flush();
386 }379 }
...@@ -419,22 +412,28 @@ pub const Response = struct {...@@ -419,22 +412,28 @@ pub const Response = struct {
419 if (res.request.parser.state.isContent()) break;412 if (res.request.parser.state.isContent()) break;
420 }413 }
421414
422 res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items);415 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
416 try res.request.parse(res.request.parser.header_bytes.items);
423417
424 if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) {418 const res_connection = res.headers.getFirstValue("connection");
419 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
420
421 const req_connection = res.request.headers.getFirstValue("connection");
422 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
423 if (res_keepalive and req_keepalive) {
425 res.connection.conn.closing = false;424 res.connection.conn.closing = false;
426 } else {425 } else {
427 res.connection.conn.closing = true;426 res.connection.conn.closing = true;
428 }427 }
429428
430 if (res.request.headers.transfer_encoding) |te| {429 if (res.request.transfer_encoding) |te| {
431 switch (te) {430 switch (te) {
432 .chunked => {431 .chunked => {
433 res.request.parser.next_chunk_length = 0;432 res.request.parser.next_chunk_length = 0;
434 res.request.parser.state = .chunk_head_size;433 res.request.parser.state = .chunk_head_size;
435 },434 },
436 }435 }
437 } else if (res.request.headers.content_length) |cl| {436 } else if (res.request.content_length) |cl| {
438 res.request.parser.next_chunk_length = cl;437 res.request.parser.next_chunk_length = cl;
439438
440 if (cl == 0) res.request.parser.done = true;439 if (cl == 0) res.request.parser.done = true;
...@@ -443,7 +442,7 @@ pub const Response = struct {...@@ -443,7 +442,7 @@ pub const Response = struct {
443 }442 }
444443
445 if (!res.request.parser.done) {444 if (!res.request.parser.done) {
446 if (res.request.headers.transfer_compression) |tc| switch (tc) {445 if (res.request.transfer_compression) |tc| switch (tc) {
447 .compress => return error.CompressionNotSupported,446 .compress => return error.CompressionNotSupported,
448 .deflate => res.request.compression = .{447 .deflate => res.request.compression = .{
449 .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()),448 .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()),
...@@ -495,7 +494,7 @@ pub const Response = struct {...@@ -495,7 +494,7 @@ pub const Response = struct {
495494
496 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.495 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
497 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {496 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
498 switch (res.headers.transfer_encoding) {497 switch (res.transfer_encoding) {
499 .chunked => {498 .chunked => {
500 try res.connection.writer().print("{x}\r\n", .{bytes.len});499 try res.connection.writer().print("{x}\r\n", .{bytes.len});
501 try res.connection.writeAll(bytes);500 try res.connection.writeAll(bytes);
...@@ -525,7 +524,7 @@ pub const Response = struct {...@@ -525,7 +524,7 @@ pub const Response = struct {
525};524};
526525
527/// The mode of transport for responses.526/// The mode of transport for responses.
528pub const RequestTransfer = union(enum) {527pub const ResponseTransfer = union(enum) {
529 content_length: u64,528 content_length: u64,
530 chunked: void,529 chunked: void,
531 none: void,530 none: void,
...@@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {...@@ -588,7 +587,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
588 .stream = in.stream,587 .stream = in.stream,
589 .protocol = .plain,588 .protocol = .plain,
590 } },589 } },
590 .headers = .{ .allocator = server.allocator },
591 .request = .{591 .request = .{
592 .version = undefined,
593 .method = undefined,
594 .target = undefined,
592 .parser = switch (options) {595 .parser = switch (options) {
593 .dynamic => |max| proto.HeadersParser.initDynamic(max),596 .dynamic => |max| proto.HeadersParser.initDynamic(max),
594 .static => |buf| proto.HeadersParser.initStatic(buf),597 .static => |buf| proto.HeadersParser.initStatic(buf),
src/Package.zig+4-1
...@@ -479,7 +479,10 @@ fn fetchAndUnpack(...@@ -479,7 +479,10 @@ fn fetchAndUnpack(
479 };479 };
480 defer tmp_directory.closeAndFree(gpa);480 defer tmp_directory.closeAndFree(gpa);
481481
482 var req = try http_client.request(uri, .{}, .{});482 var h = std.http.Headers{ .allocator = gpa };
483 defer h.deinit();
484
485 var req = try http_client.request(uri, h, .{ .method = .GET });
483 defer req.deinit();486 defer req.deinit();
484487
485 try req.do();488 try req.do();