authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-31 22:36:08-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-06 22:42:42-07:00
log6e671d4c779dc2087b0687f9e5ed5cd7a3341ea9
tree868ce9d1e0e9abc34084198d3f5cfa22c960fa91
parent04fe1bfe3ceabd632183a85101cddeeec11f0745

std.http: rework for new std.Io API


7 files changed, 2423 insertions(+), 2808 deletions(-)

lib/std/http.zig+766-15
...@@ -1,14 +1,14 @@...@@ -1,14 +1,14 @@
1const builtin = @import("builtin");1const builtin = @import("builtin");
2const std = @import("std.zig");2const std = @import("std.zig");
3const assert = std.debug.assert;3const assert = std.debug.assert;
4const Writer = std.Io.Writer;
5const File = std.fs.File;
46
5pub const Client = @import("http/Client.zig");7pub const Client = @import("http/Client.zig");
6pub const Server = @import("http/Server.zig");8pub const Server = @import("http/Server.zig");
7pub const protocol = @import("http/protocol.zig");
8pub const HeadParser = @import("http/HeadParser.zig");9pub const HeadParser = @import("http/HeadParser.zig");
9pub const ChunkParser = @import("http/ChunkParser.zig");10pub const ChunkParser = @import("http/ChunkParser.zig");
10pub const HeaderIterator = @import("http/HeaderIterator.zig");11pub const HeaderIterator = @import("http/HeaderIterator.zig");
11pub const WebSocket = @import("http/WebSocket.zig");
1212
13pub const Version = enum {13pub const Version = enum {
14 @"HTTP/1.0",14 @"HTTP/1.0",
...@@ -42,7 +42,7 @@ pub const Method = enum(u64) {...@@ -42,7 +42,7 @@ pub const Method = enum(u64) {
42 return x;42 return x;
43 }43 }
4444
45 pub fn format(self: Method, w: *std.io.Writer) std.io.Writer.Error!void {45 pub fn format(self: Method, w: *Writer) Writer.Error!void {
46 const bytes: []const u8 = @ptrCast(&@intFromEnum(self));46 const bytes: []const u8 = @ptrCast(&@intFromEnum(self));
47 const str = std.mem.sliceTo(bytes, 0);47 const str = std.mem.sliceTo(bytes, 0);
48 try w.writeAll(str);48 try w.writeAll(str);
...@@ -296,13 +296,24 @@ pub const TransferEncoding = enum {...@@ -296,13 +296,24 @@ pub const TransferEncoding = enum {
296};296};
297297
298pub const ContentEncoding = enum {298pub const ContentEncoding = enum {
299 identity,
300 compress,
301 @"x-compress",
302 deflate,
303 gzip,
304 @"x-gzip",
305 zstd,299 zstd,
300 gzip,
301 deflate,
302 compress,
303 identity,
304
305 pub fn fromString(s: []const u8) ?ContentEncoding {
306 const map = std.StaticStringMap(ContentEncoding).initComptime(.{
307 .{ "zstd", .zstd },
308 .{ "gzip", .gzip },
309 .{ "x-gzip", .gzip },
310 .{ "deflate", .deflate },
311 .{ "compress", .compress },
312 .{ "x-compress", .compress },
313 .{ "identity", .identity },
314 });
315 return map.get(s);
316 }
306};317};
307318
308pub const Connection = enum {319pub const Connection = enum {
...@@ -315,15 +326,755 @@ pub const Header = struct {...@@ -315,15 +326,755 @@ pub const Header = struct {
315 value: []const u8,326 value: []const u8,
316};327};
317328
329pub const Reader = struct {
330 in: *std.Io.Reader,
331 /// This is preallocated memory that might be used by `bodyReader`. That
332 /// function might return a pointer to this field, or a different
333 /// `*std.Io.Reader`. Advisable to not access this field directly.
334 interface: std.Io.Reader,
335 /// Keeps track of whether the stream is ready to accept a new request,
336 /// making invalid API usage cause assertion failures rather than HTTP
337 /// protocol violations.
338 state: State,
339 /// HTTP trailer bytes. These are at the end of a transfer-encoding:
340 /// chunked message. This data is available only after calling one of the
341 /// "end" functions and points to data inside the buffer of `in`, and is
342 /// therefore invalidated on the next call to `receiveHead`, or any other
343 /// read from `in`.
344 trailers: []const u8 = &.{},
345 body_err: ?BodyError = null,
346 /// Stolen from `in`.
347 head_buffer: []u8 = &.{},
348
349 pub const max_chunk_header_len = 22;
350
351 pub const RemainingChunkLen = enum(u64) {
352 head = 0,
353 n = 1,
354 rn = 2,
355 _,
356
357 pub fn init(integer: u64) RemainingChunkLen {
358 return @enumFromInt(integer);
359 }
360
361 pub fn int(rcl: RemainingChunkLen) u64 {
362 return @intFromEnum(rcl);
363 }
364 };
365
366 pub const State = union(enum) {
367 /// The stream is available to be used for the first time, or reused.
368 ready,
369 received_head,
370 /// The stream goes until the connection is closed.
371 body_none,
372 body_remaining_content_length: u64,
373 body_remaining_chunk_len: RemainingChunkLen,
374 /// The stream would be eligible for another HTTP request, however the
375 /// client and server did not negotiate a persistent connection.
376 closing,
377 };
378
379 pub const BodyError = error{
380 HttpChunkInvalid,
381 HttpChunkTruncated,
382 HttpHeadersOversize,
383 };
384
385 pub const HeadError = error{
386 /// Too many bytes of HTTP headers.
387 ///
388 /// The HTTP specification suggests to respond with a 431 status code
389 /// before closing the connection.
390 HttpHeadersOversize,
391 /// Partial HTTP request was received but the connection was closed
392 /// before fully receiving the headers.
393 HttpRequestTruncated,
394 /// The client sent 0 bytes of headers before closing the stream. This
395 /// happens when a keep-alive connection is finally closed.
396 HttpConnectionClosing,
397 /// Transitive error occurred reading from `in`.
398 ReadFailed,
399 };
400
401 pub fn restituteHeadBuffer(reader: *Reader) void {
402 reader.in.restitute(reader.head_buffer.len);
403 reader.head_buffer.len = 0;
404 }
405
406 /// Buffers the entire head into `head_buffer`, invalidating the previous
407 /// `head_buffer`, if any.
408 pub fn receiveHead(reader: *Reader) HeadError!void {
409 reader.trailers = &.{};
410 const in = reader.in;
411 in.restitute(reader.head_buffer.len);
412 reader.head_buffer.len = 0;
413 in.rebase();
414 var hp: HeadParser = .{};
415 var head_end: usize = 0;
416 while (true) {
417 if (head_end >= in.buffer.len) return error.HttpHeadersOversize;
418 in.fillMore() catch |err| switch (err) {
419 error.EndOfStream => switch (head_end) {
420 0 => return error.HttpConnectionClosing,
421 else => return error.HttpRequestTruncated,
422 },
423 error.ReadFailed => return error.ReadFailed,
424 };
425 head_end += hp.feed(in.buffered()[head_end..]);
426 if (hp.state == .finished) {
427 reader.head_buffer = in.steal(head_end);
428 reader.state = .received_head;
429 return;
430 }
431 }
432 }
433
434 /// If compressed body has been negotiated this will return compressed bytes.
435 ///
436 /// Asserts only called once and after `receiveHead`.
437 ///
438 /// See also:
439 /// * `interfaceDecompressing`
440 pub fn bodyReader(
441 reader: *Reader,
442 buffer: []u8,
443 transfer_encoding: TransferEncoding,
444 content_length: ?u64,
445 ) *std.Io.Reader {
446 assert(reader.state == .received_head);
447 switch (transfer_encoding) {
448 .chunked => {
449 reader.state = .{ .body_remaining_chunk_len = .head };
450 reader.interface = .{
451 .buffer = buffer,
452 .seek = 0,
453 .end = 0,
454 .vtable = &.{
455 .stream = chunkedStream,
456 .discard = chunkedDiscard,
457 },
458 };
459 return &reader.interface;
460 },
461 .none => {
462 if (content_length) |len| {
463 reader.state = .{ .body_remaining_content_length = len };
464 reader.interface = .{
465 .buffer = buffer,
466 .seek = 0,
467 .end = 0,
468 .vtable = &.{
469 .stream = contentLengthStream,
470 .discard = contentLengthDiscard,
471 },
472 };
473 return &reader.interface;
474 } else {
475 reader.state = .body_none;
476 return reader.in;
477 }
478 },
479 }
480 }
481
482 /// If compressed body has been negotiated this will return decompressed bytes.
483 ///
484 /// Asserts only called once and after `receiveHead`.
485 ///
486 /// See also:
487 /// * `interface`
488 pub fn bodyReaderDecompressing(
489 reader: *Reader,
490 transfer_encoding: TransferEncoding,
491 content_length: ?u64,
492 content_encoding: ContentEncoding,
493 decompressor: *Decompressor,
494 decompression_buffer: []u8,
495 ) *std.Io.Reader {
496 if (transfer_encoding == .none and content_length == null) {
497 assert(reader.state == .received_head);
498 reader.state = .body_none;
499 switch (content_encoding) {
500 .identity => {
501 return reader.in;
502 },
503 .deflate => {
504 decompressor.* = .{ .flate = .init(reader.in, .raw, decompression_buffer) };
505 return &decompressor.flate.reader;
506 },
507 .gzip => {
508 decompressor.* = .{ .flate = .init(reader.in, .gzip, decompression_buffer) };
509 return &decompressor.flate.reader;
510 },
511 .zstd => {
512 decompressor.* = .{ .zstd = .init(reader.in, decompression_buffer, .{ .verify_checksum = false }) };
513 return &decompressor.zstd.reader;
514 },
515 .compress => unreachable,
516 }
517 }
518 const transfer_reader = bodyReader(reader, &.{}, transfer_encoding, content_length);
519 return decompressor.init(transfer_reader, decompression_buffer, content_encoding);
520 }
521
522 fn contentLengthStream(
523 io_r: *std.Io.Reader,
524 w: *Writer,
525 limit: std.Io.Limit,
526 ) std.Io.Reader.StreamError!usize {
527 const reader: *Reader = @fieldParentPtr("interface", io_r);
528 const remaining_content_length = &reader.state.body_remaining_content_length;
529 const remaining = remaining_content_length.*;
530 if (remaining == 0) {
531 reader.state = .ready;
532 return error.EndOfStream;
533 }
534 const n = try reader.in.stream(w, limit.min(.limited(remaining)));
535 remaining_content_length.* = remaining - n;
536 return n;
537 }
538
539 fn contentLengthDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize {
540 const reader: *Reader = @fieldParentPtr("interface", io_r);
541 const remaining_content_length = &reader.state.body_remaining_content_length;
542 const remaining = remaining_content_length.*;
543 if (remaining == 0) {
544 reader.state = .ready;
545 return error.EndOfStream;
546 }
547 const n = try reader.in.discard(limit.min(.limited(remaining)));
548 remaining_content_length.* = remaining - n;
549 return n;
550 }
551
552 fn chunkedStream(io_r: *std.Io.Reader, w: *Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
553 const reader: *Reader = @fieldParentPtr("interface", io_r);
554 const chunk_len_ptr = switch (reader.state) {
555 .ready => return error.EndOfStream,
556 .body_remaining_chunk_len => |*x| x,
557 else => unreachable,
558 };
559 return chunkedReadEndless(reader, w, limit, chunk_len_ptr) catch |err| switch (err) {
560 error.ReadFailed => return error.ReadFailed,
561 error.WriteFailed => return error.WriteFailed,
562 error.EndOfStream => {
563 reader.body_err = error.HttpChunkTruncated;
564 return error.ReadFailed;
565 },
566 else => |e| {
567 reader.body_err = e;
568 return error.ReadFailed;
569 },
570 };
571 }
572
573 fn chunkedReadEndless(
574 reader: *Reader,
575 w: *Writer,
576 limit: std.Io.Limit,
577 chunk_len_ptr: *RemainingChunkLen,
578 ) (BodyError || std.Io.Reader.StreamError)!usize {
579 const in = reader.in;
580 len: switch (chunk_len_ptr.*) {
581 .head => {
582 var cp: ChunkParser = .init;
583 while (true) {
584 const i = cp.feed(in.buffered());
585 switch (cp.state) {
586 .invalid => return error.HttpChunkInvalid,
587 .data => {
588 in.toss(i);
589 break;
590 },
591 else => {
592 in.toss(i);
593 try in.fillMore();
594 continue;
595 },
596 }
597 }
598 if (cp.chunk_len == 0) return parseTrailers(reader, 0);
599 const n = try in.stream(w, limit.min(.limited(cp.chunk_len)));
600 chunk_len_ptr.* = .init(cp.chunk_len + 2 - n);
601 return n;
602 },
603 .n => {
604 if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid;
605 in.toss(1);
606 continue :len .head;
607 },
608 .rn => {
609 const rn = try in.peekArray(2);
610 if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid;
611 in.toss(2);
612 continue :len .head;
613 },
614 else => |remaining_chunk_len| {
615 const n = try in.stream(w, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2)));
616 chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n);
617 return n;
618 },
619 }
620 }
621
622 fn chunkedDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize {
623 const reader: *Reader = @fieldParentPtr("interface", io_r);
624 const chunk_len_ptr = switch (reader.state) {
625 .ready => return error.EndOfStream,
626 .body_remaining_chunk_len => |*x| x,
627 else => unreachable,
628 };
629 return chunkedDiscardEndless(reader, limit, chunk_len_ptr) catch |err| switch (err) {
630 error.ReadFailed => return error.ReadFailed,
631 error.EndOfStream => {
632 reader.body_err = error.HttpChunkTruncated;
633 return error.ReadFailed;
634 },
635 else => |e| {
636 reader.body_err = e;
637 return error.ReadFailed;
638 },
639 };
640 }
641
642 fn chunkedDiscardEndless(
643 reader: *Reader,
644 limit: std.Io.Limit,
645 chunk_len_ptr: *RemainingChunkLen,
646 ) (BodyError || std.Io.Reader.Error)!usize {
647 const in = reader.in;
648 len: switch (chunk_len_ptr.*) {
649 .head => {
650 var cp: ChunkParser = .init;
651 while (true) {
652 const i = cp.feed(in.buffered());
653 switch (cp.state) {
654 .invalid => return error.HttpChunkInvalid,
655 .data => {
656 in.toss(i);
657 break;
658 },
659 else => {
660 in.toss(i);
661 try in.fillMore();
662 continue;
663 },
664 }
665 }
666 if (cp.chunk_len == 0) return parseTrailers(reader, 0);
667 const n = try in.discard(limit.min(.limited(cp.chunk_len)));
668 chunk_len_ptr.* = .init(cp.chunk_len + 2 - n);
669 return n;
670 },
671 .n => {
672 if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid;
673 in.toss(1);
674 continue :len .head;
675 },
676 .rn => {
677 const rn = try in.peekArray(2);
678 if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid;
679 in.toss(2);
680 continue :len .head;
681 },
682 else => |remaining_chunk_len| {
683 const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2)));
684 chunk_len_ptr.* = .init(remaining_chunk_len.int() - n);
685 return n;
686 },
687 }
688 }
689
690 /// Called when next bytes in the stream are trailers, or "\r\n" to indicate
691 /// end of chunked body.
692 fn parseTrailers(reader: *Reader, amt_read: usize) (BodyError || std.Io.Reader.Error)!usize {
693 const in = reader.in;
694 const rn = try in.peekArray(2);
695 if (rn[0] == '\r' and rn[1] == '\n') {
696 in.toss(2);
697 reader.state = .ready;
698 assert(reader.trailers.len == 0);
699 return amt_read;
700 }
701 var hp: HeadParser = .{ .state = .seen_rn };
702 var trailers_len: usize = 2;
703 while (true) {
704 if (in.buffer.len - trailers_len == 0) return error.HttpHeadersOversize;
705 const remaining = in.buffered()[trailers_len..];
706 if (remaining.len == 0) {
707 try in.fillMore();
708 continue;
709 }
710 trailers_len += hp.feed(remaining);
711 if (hp.state == .finished) {
712 reader.state = .ready;
713 reader.trailers = in.buffered()[0..trailers_len];
714 in.toss(trailers_len);
715 return amt_read;
716 }
717 }
718 }
719};
720
721pub const Decompressor = union(enum) {
722 flate: std.compress.flate.Decompress,
723 zstd: std.compress.zstd.Decompress,
724 none: *std.Io.Reader,
725
726 pub fn init(
727 decompressor: *Decompressor,
728 transfer_reader: *std.Io.Reader,
729 buffer: []u8,
730 content_encoding: ContentEncoding,
731 ) *std.Io.Reader {
732 switch (content_encoding) {
733 .identity => {
734 decompressor.* = .{ .none = transfer_reader };
735 return transfer_reader;
736 },
737 .deflate => {
738 decompressor.* = .{ .flate = .init(transfer_reader, .raw, buffer) };
739 return &decompressor.flate.reader;
740 },
741 .gzip => {
742 decompressor.* = .{ .flate = .init(transfer_reader, .gzip, buffer) };
743 return &decompressor.flate.reader;
744 },
745 .zstd => {
746 decompressor.* = .{ .zstd = .init(transfer_reader, buffer, .{ .verify_checksum = false }) };
747 return &decompressor.zstd.reader;
748 },
749 .compress => unreachable,
750 }
751 }
752};
753
754/// Request or response body.
755pub const BodyWriter = struct {
756 /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the
757 /// state of this other than via methods of `BodyWriter`.
758 http_protocol_output: *Writer,
759 state: State,
760 writer: Writer,
761
762 pub const Error = Writer.Error;
763
764 /// How many zeroes to reserve for hex-encoded chunk length.
765 const chunk_len_digits = 8;
766 const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1;
767 const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n";
768
769 comptime {
770 assert(max_chunk_len == std.math.maxInt(u32));
771 }
772
773 pub const State = union(enum) {
774 /// End of connection signals the end of the stream.
775 none,
776 /// As a debugging utility, counts down to zero as bytes are written.
777 content_length: u64,
778 /// Each chunk is wrapped in a header and trailer.
779 chunked: Chunked,
780 /// Cleanly finished stream; connection can be reused.
781 end,
782
783 pub const Chunked = union(enum) {
784 /// Index to the start of the hex-encoded chunk length in the chunk
785 /// header within the buffer of `BodyWriter.http_protocol_output`.
786 /// Buffered chunk data starts here plus length of `chunk_header_template`.
787 offset: usize,
788 /// We are in the middle of a chunk and this is how many bytes are
789 /// left until the next header. This includes +2 for "\r"\n", and
790 /// is zero for the beginning of the stream.
791 chunk_len: usize,
792
793 pub const init: Chunked = .{ .chunk_len = 0 };
794 };
795 };
796
797 pub fn isEliding(w: *const BodyWriter) bool {
798 return w.writer.vtable.drain == Writer.discardingDrain;
799 }
800
801 /// Sends all buffered data across `BodyWriter.http_protocol_output`.
802 pub fn flush(w: *BodyWriter) Error!void {
803 const out = w.http_protocol_output;
804 switch (w.state) {
805 .end, .none, .content_length => return out.flush(),
806 .chunked => |*chunked| switch (chunked.*) {
807 .offset => |offset| {
808 const chunk_len = out.end - offset - chunk_header_template.len;
809 if (chunk_len > 0) {
810 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
811 chunked.* = .{ .chunk_len = 2 };
812 } else {
813 out.end = offset;
814 chunked.* = .{ .chunk_len = 0 };
815 }
816 try out.flush();
817 },
818 .chunk_len => return out.flush(),
819 },
820 }
821 }
822
823 /// When using content-length, asserts that the amount of data sent matches
824 /// the value sent in the header, then flushes.
825 ///
826 /// When using transfer-encoding: chunked, writes the end-of-stream message
827 /// with empty trailers, then flushes the stream to the system. Asserts any
828 /// started chunk has been completely finished.
829 ///
830 /// Respects the value of `isEliding` to omit all data after the headers.
831 ///
832 /// See also:
833 /// * `endUnflushed`
834 /// * `endChunked`
835 pub fn end(w: *BodyWriter) Error!void {
836 try endUnflushed(w);
837 try w.http_protocol_output.flush();
838 }
839
840 /// When using content-length, asserts that the amount of data sent matches
841 /// the value sent in the header.
842 ///
843 /// Otherwise, transfer-encoding: chunked is being used, and it writes the
844 /// end-of-stream message with empty trailers.
845 ///
846 /// Respects the value of `isEliding` to omit all data after the headers.
847 ///
848 /// See also:
849 /// * `end`
850 /// * `endChunked`
851 pub fn endUnflushed(w: *BodyWriter) Error!void {
852 switch (w.state) {
853 .end => unreachable,
854 .content_length => |len| {
855 assert(len == 0); // Trips when end() called before all bytes written.
856 w.state = .end;
857 },
858 .none => {},
859 .chunked => return endChunkedUnflushed(w, .{}),
860 }
861 }
862
863 pub const EndChunkedOptions = struct {
864 trailers: []const Header = &.{},
865 };
866
867 /// Writes the end-of-stream message and any optional trailers, flushing
868 /// the underlying stream.
869 ///
870 /// Asserts that the BodyWriter is using transfer-encoding: chunked.
871 ///
872 /// Respects the value of `isEliding` to omit all data after the headers.
873 ///
874 /// See also:
875 /// * `endChunkedUnflushed`
876 /// * `end`
877 pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) Error!void {
878 try endChunkedUnflushed(w, options);
879 try w.http_protocol_output.flush();
880 }
881
882 /// Writes the end-of-stream message and any optional trailers.
883 ///
884 /// Does not flush.
885 ///
886 /// Asserts that the BodyWriter is using transfer-encoding: chunked.
887 ///
888 /// Respects the value of `isEliding` to omit all data after the headers.
889 ///
890 /// See also:
891 /// * `endChunked`
892 /// * `endUnflushed`
893 /// * `end`
894 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {
895 const chunked = &w.state.chunked;
896 if (w.isEliding()) {
897 w.state = .end;
898 return;
899 }
900 const bw = w.http_protocol_output;
901 switch (chunked.*) {
902 .offset => |offset| {
903 const chunk_len = bw.end - offset - chunk_header_template.len;
904 writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len);
905 try bw.writeAll("\r\n");
906 },
907 .chunk_len => |chunk_len| switch (chunk_len) {
908 0 => {},
909 1 => try bw.writeByte('\n'),
910 2 => try bw.writeAll("\r\n"),
911 else => unreachable, // An earlier write call indicated more data would follow.
912 },
913 }
914 try bw.writeAll("0\r\n");
915 for (options.trailers) |trailer| {
916 try bw.writeAll(trailer.name);
917 try bw.writeAll(": ");
918 try bw.writeAll(trailer.value);
919 try bw.writeAll("\r\n");
920 }
921 try bw.writeAll("\r\n");
922 w.state = .end;
923 }
924
925 pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize {
926 const bw: *BodyWriter = @fieldParentPtr("writer", w);
927 assert(!bw.isEliding());
928 const out = bw.http_protocol_output;
929 const n = try out.writeSplatHeader(w.buffered(), data, splat);
930 bw.state.content_length -= n;
931 return w.consume(n);
932 }
933
934 pub fn noneDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize {
935 const bw: *BodyWriter = @fieldParentPtr("writer", w);
936 assert(!bw.isEliding());
937 const out = bw.http_protocol_output;
938 const n = try out.writeSplatHeader(w.buffered(), data, splat);
939 return w.consume(n);
940 }
941
942 /// Returns `null` if size cannot be computed without making any syscalls.
943 pub fn noneSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize {
944 const bw: *BodyWriter = @fieldParentPtr("writer", w);
945 assert(!bw.isEliding());
946 const out = bw.http_protocol_output;
947 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
948 return w.consume(n);
949 }
950
951 pub fn contentLengthSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize {
952 const bw: *BodyWriter = @fieldParentPtr("writer", w);
953 assert(!bw.isEliding());
954 const out = bw.http_protocol_output;
955 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
956 bw.state.content_length -= n;
957 return w.consume(n);
958 }
959
960 pub fn chunkedSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize {
961 const bw: *BodyWriter = @fieldParentPtr("writer", w);
962 assert(!bw.isEliding());
963 const data_len = Writer.countSendFileLowerBound(w.end, file_reader, limit) orelse {
964 // If the file size is unknown, we cannot lower to a `sendFile` since we would
965 // have to flush the chunk header before knowing the chunk length.
966 return error.Unimplemented;
967 };
968 const out = bw.http_protocol_output;
969 const chunked = &bw.state.chunked;
970 state: switch (chunked.*) {
971 .offset => |off| {
972 // TODO: is it better perf to read small files into the buffer?
973 const buffered_len = out.end - off - chunk_header_template.len;
974 const chunk_len = data_len + buffered_len;
975 writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len);
976 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
977 chunked.* = .{ .chunk_len = data_len + 2 - n };
978 return w.consume(n);
979 },
980 .chunk_len => |chunk_len| l: switch (chunk_len) {
981 0 => {
982 const off = out.end;
983 const header_buf = try out.writableArray(chunk_header_template.len);
984 @memcpy(header_buf, chunk_header_template);
985 chunked.* = .{ .offset = off };
986 continue :state .{ .offset = off };
987 },
988 1 => {
989 try out.writeByte('\n');
990 chunked.chunk_len = 0;
991 continue :l 0;
992 },
993 2 => {
994 try out.writeByte('\r');
995 chunked.chunk_len = 1;
996 continue :l 1;
997 },
998 else => {
999 const new_limit = limit.min(.limited(chunk_len - 2));
1000 const n = try out.sendFileHeader(w.buffered(), file_reader, new_limit);
1001 chunked.chunk_len = chunk_len - n;
1002 return w.consume(n);
1003 },
1004 },
1005 }
1006 }
1007
1008 pub fn chunkedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize {
1009 const bw: *BodyWriter = @fieldParentPtr("writer", w);
1010 assert(!bw.isEliding());
1011 const out = bw.http_protocol_output;
1012 const data_len = w.end + Writer.countSplat(data, splat);
1013 const chunked = &bw.state.chunked;
1014 state: switch (chunked.*) {
1015 .offset => |offset| {
1016 if (out.unusedCapacityLen() >= data_len) {
1017 return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable);
1018 }
1019 const buffered_len = out.end - offset - chunk_header_template.len;
1020 const chunk_len = data_len + buffered_len;
1021 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
1022 const n = try out.writeSplatHeader(w.buffered(), data, splat);
1023 chunked.* = .{ .chunk_len = data_len + 2 - n };
1024 return w.consume(n);
1025 },
1026 .chunk_len => |chunk_len| l: switch (chunk_len) {
1027 0 => {
1028 const offset = out.end;
1029 const header_buf = try out.writableArray(chunk_header_template.len);
1030 @memcpy(header_buf, chunk_header_template);
1031 chunked.* = .{ .offset = offset };
1032 continue :state .{ .offset = offset };
1033 },
1034 1 => {
1035 try out.writeByte('\n');
1036 chunked.chunk_len = 0;
1037 continue :l 0;
1038 },
1039 2 => {
1040 try out.writeByte('\r');
1041 chunked.chunk_len = 1;
1042 continue :l 1;
1043 },
1044 else => {
1045 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2));
1046 chunked.chunk_len = chunk_len - n;
1047 return w.consume(n);
1048 },
1049 },
1050 }
1051 }
1052
1053 /// Writes an integer as base 16 to `buf`, right-aligned, assuming the
1054 /// buffer has already been filled with zeroes.
1055 fn writeHex(buf: []u8, x: usize) void {
1056 assert(std.mem.allEqual(u8, buf, '0'));
1057 const base = 16;
1058 var index: usize = buf.len;
1059 var a = x;
1060 while (a > 0) {
1061 const digit = a % base;
1062 index -= 1;
1063 buf[index] = std.fmt.digitToChar(@intCast(digit), .lower);
1064 a /= base;
1065 }
1066 }
1067};
1068
318test {1069test {
1070 _ = Server;
1071 _ = Status;
1072 _ = Method;
1073 _ = ChunkParser;
1074 _ = HeadParser;
1075
319 if (builtin.os.tag != .wasi) {1076 if (builtin.os.tag != .wasi) {
320 _ = Client;1077 _ = Client;
321 _ = Method;
322 _ = Server;
323 _ = Status;
324 _ = HeadParser;
325 _ = ChunkParser;
326 _ = WebSocket;
327 _ = @import("http/test.zig");1078 _ = @import("http/test.zig");
328 }1079 }
329}1080}
lib/std/http/ChunkParser.zig+3-3
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1//! Parser for transfer-encoding: chunked.1//! Parser for transfer-encoding: chunked.
22
3const ChunkParser = @This();
4const std = @import("std");
5
3state: State,6state: State,
4chunk_len: u64,7chunk_len: u64,
58
...@@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize {...@@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize {
97 return bytes.len;100 return bytes.len;
98}101}
99102
100const ChunkParser = @This();
101const std = @import("std");
102
103test feed {103test feed {
104 const testing = std.testing;104 const testing = std.testing;
105105
lib/std/http/Client.zig+997-1011
...@@ -13,9 +13,10 @@ const net = std.net;...@@ -13,9 +13,10 @@ const net = std.net;
13const Uri = std.Uri;13const Uri = std.Uri;
14const Allocator = mem.Allocator;14const Allocator = mem.Allocator;
15const assert = std.debug.assert;15const assert = std.debug.assert;
16const Writer = std.io.Writer;
17const Reader = std.io.Reader;
1618
17const Client = @This();19const Client = @This();
18const proto = @import("protocol.zig");
1920
20pub const disable_tls = std.options.http_disable_tls;21pub const disable_tls = std.options.http_disable_tls;
2122
...@@ -24,6 +25,12 @@ allocator: Allocator,...@@ -24,6 +25,12 @@ allocator: Allocator,
2425
25ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},26ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{},
26ca_bundle_mutex: std.Thread.Mutex = .{},27ca_bundle_mutex: std.Thread.Mutex = .{},
28/// Used both for the reader and writer buffers.
29tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len,
30/// If non-null, ssl secrets are logged to a stream. Creating such a stream
31/// allows other processes with access to that stream to decrypt all
32/// traffic over connections created with this `Client`.
33ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null,
2734
28/// When this is `true`, the next time this client performs an HTTPS request,35/// When this is `true`, the next time this client performs an HTTPS request,
29/// it will first rescan the system for root certificates.36/// it will first rescan the system for root certificates.
...@@ -31,6 +38,13 @@ next_https_rescan_certs: bool = true,...@@ -31,6 +38,13 @@ next_https_rescan_certs: bool = true,
3138
32/// The pool of connections that can be reused (and currently in use).39/// The pool of connections that can be reused (and currently in use).
33connection_pool: ConnectionPool = .{},40connection_pool: ConnectionPool = .{},
41/// Each `Connection` allocates this amount for the reader buffer.
42///
43/// If the entire HTTP header cannot fit in this amount of bytes,
44/// `error.HttpHeadersOversize` will be returned from `Request.wait`.
45read_buffer_size: usize = 4096,
46/// Each `Connection` allocates this amount for the writer buffer.
47write_buffer_size: usize = 1024,
3448
35/// If populated, all http traffic travels through this third party.49/// If populated, all http traffic travels through this third party.
36/// This field cannot be modified while the client has active connections.50/// This field cannot be modified while the client has active connections.
...@@ -41,7 +55,7 @@ http_proxy: ?*Proxy = null,...@@ -41,7 +55,7 @@ http_proxy: ?*Proxy = null,
41/// Pointer to externally-owned memory.55/// Pointer to externally-owned memory.
42https_proxy: ?*Proxy = null,56https_proxy: ?*Proxy = null,
4357
44/// A set of linked lists of connections that can be reused.58/// A Least-Recently-Used cache of open connections to be reused.
45pub const ConnectionPool = struct {59pub const ConnectionPool = struct {
46 mutex: std.Thread.Mutex = .{},60 mutex: std.Thread.Mutex = .{},
47 /// Open connections that are currently in use.61 /// Open connections that are currently in use.
...@@ -55,11 +69,13 @@ pub const ConnectionPool = struct {...@@ -55,11 +69,13 @@ pub const ConnectionPool = struct {
55 pub const Criteria = struct {69 pub const Criteria = struct {
56 host: []const u8,70 host: []const u8,
57 port: u16,71 port: u16,
58 protocol: Connection.Protocol,72 protocol: Protocol,
59 };73 };
6074
61 /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe.75 /// Finds and acquires a connection from the connection pool matching the criteria.
62 /// If no connection is found, null is returned.76 /// If no connection is found, null is returned.
77 ///
78 /// Threadsafe.
63 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection {79 pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection {
64 pool.mutex.lock();80 pool.mutex.lock();
65 defer pool.mutex.unlock();81 defer pool.mutex.unlock();
...@@ -71,7 +87,7 @@ pub const ConnectionPool = struct {...@@ -71,7 +87,7 @@ pub const ConnectionPool = struct {
71 if (connection.port != criteria.port) continue;87 if (connection.port != criteria.port) continue;
7288
73 // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)89 // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
74 if (!std.ascii.eqlIgnoreCase(connection.host, criteria.host)) continue;90 if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue;
7591
76 pool.acquireUnsafe(connection);92 pool.acquireUnsafe(connection);
77 return connection;93 return connection;
...@@ -96,28 +112,25 @@ pub const ConnectionPool = struct {...@@ -96,28 +112,25 @@ pub const ConnectionPool = struct {
96 return pool.acquireUnsafe(connection);112 return pool.acquireUnsafe(connection);
97 }113 }
98114
99 /// Tries to release a connection back to the connection pool. This function is threadsafe.115 /// Tries to release a connection back to the connection pool.
100 /// If the connection is marked as closing, it will be closed instead.116 /// If the connection is marked as closing, it will be closed instead.
101 ///117 ///
102 /// The allocator must be the owner of all nodes in this pool.118 /// `allocator` must be the same one used to create `connection`.
103 /// The allocator must be the owner of all resources associated with the connection.119 ///
104 pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void {120 /// Threadsafe.
121 pub fn release(pool: *ConnectionPool, connection: *Connection) void {
105 pool.mutex.lock();122 pool.mutex.lock();
106 defer pool.mutex.unlock();123 defer pool.mutex.unlock();
107124
108 pool.used.remove(&connection.pool_node);125 pool.used.remove(&connection.pool_node);
109126
110 if (connection.closing or pool.free_size == 0) {127 if (connection.closing or pool.free_size == 0) return connection.destroy();
111 connection.close(allocator);
112 return allocator.destroy(connection);
113 }
114128
115 if (pool.free_len >= pool.free_size) {129 if (pool.free_len >= pool.free_size) {
116 const popped: *Connection = @fieldParentPtr("pool_node", pool.free.popFirst().?);130 const popped: *Connection = @fieldParentPtr("pool_node", pool.free.popFirst().?);
117 pool.free_len -= 1;131 pool.free_len -= 1;
118132
119 popped.close(allocator);133 popped.destroy();
120 allocator.destroy(popped);
121 }134 }
122135
123 if (connection.proxied) {136 if (connection.proxied) {
...@@ -138,9 +151,11 @@ pub const ConnectionPool = struct {...@@ -138,9 +151,11 @@ pub const ConnectionPool = struct {
138 pool.used.append(&connection.pool_node);151 pool.used.append(&connection.pool_node);
139 }152 }
140153
141 /// Resizes the connection pool. This function is threadsafe.154 /// Resizes the connection pool.
142 ///155 ///
143 /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size.156 /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size.
157 ///
158 /// Threadsafe.
144 pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void {159 pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void {
145 pool.mutex.lock();160 pool.mutex.lock();
146 defer pool.mutex.unlock();161 defer pool.mutex.unlock();
...@@ -158,538 +173,586 @@ pub const ConnectionPool = struct {...@@ -158,538 +173,586 @@ pub const ConnectionPool = struct {
158 pool.free_size = new_size;173 pool.free_size = new_size;
159 }174 }
160175
161 /// Frees the connection pool and closes all connections within. This function is threadsafe.176 /// Frees the connection pool and closes all connections within.
162 ///177 ///
163 /// All future operations on the connection pool will deadlock.178 /// All future operations on the connection pool will deadlock.
164 pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void {179 ///
180 /// Threadsafe.
181 pub fn deinit(pool: *ConnectionPool) void {
165 pool.mutex.lock();182 pool.mutex.lock();
166183
167 var next = pool.free.first;184 var next = pool.free.first;
168 while (next) |node| {185 while (next) |node| {
169 const connection: *Connection = @fieldParentPtr("pool_node", node);186 const connection: *Connection = @fieldParentPtr("pool_node", node);
170 next = node.next;187 next = node.next;
171 connection.close(allocator);188 connection.destroy();
172 allocator.destroy(connection);
173 }189 }
174190
175 next = pool.used.first;191 next = pool.used.first;
176 while (next) |node| {192 while (next) |node| {
177 const connection: *Connection = @fieldParentPtr("pool_node", node);193 const connection: *Connection = @fieldParentPtr("pool_node", node);
178 next = node.next;194 next = node.next;
179 connection.close(allocator);195 connection.destroy();
180 allocator.destroy(node);
181 }196 }
182197
183 pool.* = undefined;198 pool.* = undefined;
184 }199 }
185};200};
186201
187/// An interface to either a plain or TLS connection.202pub const Protocol = enum {
188pub const Connection = struct {203 plain,
189 stream: net.Stream,204 tls,
190 /// undefined unless protocol is tls.
191 tls_client: if (!disable_tls) *std.crypto.tls.Client else void,
192
193 /// Entry in `ConnectionPool.used` or `ConnectionPool.free`.
194 pool_node: std.DoublyLinkedList.Node,
195
196 /// The protocol that this connection is using.
197 protocol: Protocol,
198
199 /// The host that this connection is connected to.
200 host: []u8,
201
202 /// The port that this connection is connected to.
203 port: u16,
204
205 /// Whether this connection is proxied and is not directly connected.
206 proxied: bool = false,
207
208 /// Whether this connection is closing when we're done with it.
209 closing: bool = false,
210
211 read_start: BufferSize = 0,
212 read_end: BufferSize = 0,
213 write_end: BufferSize = 0,
214 read_buf: [buffer_size]u8 = undefined,
215 write_buf: [buffer_size]u8 = undefined,
216
217 pub const buffer_size = std.crypto.tls.max_ciphertext_record_len;
218 const BufferSize = std.math.IntFittingRange(0, buffer_size);
219
220 pub const Protocol = enum { plain, tls };
221
222 pub fn readvDirectTls(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize {
223 return conn.tls_client.readv(conn.stream, buffers) catch |err| {
224 // https://github.com/ziglang/zig/issues/2473
225 if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert;
226
227 switch (err) {
228 error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure,
229 error.ConnectionTimedOut => return error.ConnectionTimedOut,
230 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
231 else => return error.UnexpectedReadFailure,
232 }
233 };
234 }
235
236 pub fn readvDirect(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize {
237 if (conn.protocol == .tls) {
238 if (disable_tls) unreachable;
239205
240 return conn.readvDirectTls(buffers);206 fn port(protocol: Protocol) u16 {
241 }207 return switch (protocol) {
242208 .plain => 80,
243 return conn.stream.readv(buffers) catch |err| switch (err) {209 .tls => 443,
244 error.ConnectionTimedOut => return error.ConnectionTimedOut,
245 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
246 else => return error.UnexpectedReadFailure,
247 };210 };
248 }211 }
249212
250 /// Refills the read buffer with data from the connection.213 pub fn fromScheme(scheme: []const u8) ?Protocol {
251 pub fn fill(conn: *Connection) ReadError!void {214 const protocol_map = std.StaticStringMap(Protocol).initComptime(.{
252 if (conn.read_end != conn.read_start) return;215 .{ "http", .plain },
253216 .{ "ws", .plain },
254 var iovecs = [1]std.posix.iovec{217 .{ "https", .tls },
255 .{ .base = &conn.read_buf, .len = conn.read_buf.len },218 .{ "wss", .tls },
256 };219 });
257 const nread = try conn.readvDirect(&iovecs);220 return protocol_map.get(scheme);
258 if (nread == 0) return error.EndOfStream;
259 conn.read_start = 0;
260 conn.read_end = @intCast(nread);
261 }221 }
262222
263 /// Returns the current slice of buffered data.223 pub fn fromUri(uri: Uri) ?Protocol {
264 pub fn peek(conn: *Connection) []const u8 {224 return fromScheme(uri.scheme);
265 return conn.read_buf[conn.read_start..conn.read_end];
266 }225 }
226};
267227
268 /// Discards the given number of bytes from the read buffer.228pub const Connection = struct {
269 pub fn drop(conn: *Connection, num: BufferSize) void {229 client: *Client,
270 conn.read_start += num;230 stream_writer: net.Stream.Writer,
271 }231 stream_reader: net.Stream.Reader,
232 /// Entry in `ConnectionPool.used` or `ConnectionPool.free`.
233 pool_node: std.DoublyLinkedList.Node,
234 port: u16,
235 host_len: u8,
236 proxied: bool,
237 closing: bool,
238 protocol: Protocol,
272239
273 /// Reads data from the connection into the given buffer.240 const Plain = struct {
274 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {241 connection: Connection,
275 const available_read = conn.read_end - conn.read_start;242
276 const available_buffer = buffer.len;243 fn create(
244 client: *Client,
245 remote_host: []const u8,
246 port: u16,
247 stream: net.Stream,
248 ) error{OutOfMemory}!*Plain {
249 const gpa = client.allocator;
250 const alloc_len = allocLen(client, remote_host.len);
251 const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len);
252 errdefer gpa.free(base);
253 const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len];
254 const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size];
255 const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size];
256 assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len);
257 @memcpy(host_buffer, remote_host);
258 const plain: *Plain = @ptrCast(base);
259 plain.* = .{
260 .connection = .{
261 .client = client,
262 .stream_writer = stream.writer(socket_write_buffer),
263 .stream_reader = stream.reader(socket_read_buffer),
264 .pool_node = .{},
265 .port = port,
266 .host_len = @intCast(remote_host.len),
267 .proxied = false,
268 .closing = false,
269 .protocol = .plain,
270 },
271 };
272 return plain;
273 }
277274
278 if (available_read > available_buffer) { // partially read buffered data275 fn destroy(plain: *Plain) void {
279 @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]);276 const c = &plain.connection;
280 conn.read_start += @intCast(available_buffer);277 const gpa = c.client.allocator;
278 const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain);
279 gpa.free(base[0..allocLen(c.client, c.host_len)]);
280 }
281281
282 return available_buffer;282 fn allocLen(client: *Client, host_len: usize) usize {
283 } else if (available_read > 0) { // fully read buffered data283 return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size;
284 @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]);284 }
285 conn.read_start += available_read;
286285
287 return available_read;286 fn host(plain: *Plain) []u8 {
287 const base: [*]u8 = @ptrCast(plain);
288 return base[@sizeOf(Plain)..][0..plain.connection.host_len];
288 }289 }
290 };
289291
290 var iovecs = [2]std.posix.iovec{292 const Tls = struct {
291 .{ .base = buffer.ptr, .len = buffer.len },293 client: std.crypto.tls.Client,
292 .{ .base = &conn.read_buf, .len = conn.read_buf.len },294 connection: Connection,
293 };295
294 const nread = try conn.readvDirect(&iovecs);296 fn create(
297 client: *Client,
298 remote_host: []const u8,
299 port: u16,
300 stream: net.Stream,
301 ) error{ OutOfMemory, TlsInitializationFailed }!*Tls {
302 const gpa = client.allocator;
303 const alloc_len = allocLen(client, remote_host.len);
304 const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len);
305 errdefer gpa.free(base);
306 const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];
307 const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.tls_buffer_size];
308 const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size];
309 const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];
310 assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len);
311 @memcpy(host_buffer, remote_host);
312 const tls: *Tls = @ptrCast(base);
313 tls.* = .{
314 .connection = .{
315 .client = client,
316 .stream_writer = stream.writer(socket_write_buffer),
317 .stream_reader = stream.reader(&.{}),
318 .pool_node = .{},
319 .port = port,
320 .host_len = @intCast(remote_host.len),
321 .proxied = false,
322 .closing = false,
323 .protocol = .tls,
324 },
325 // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true
326 .client = std.crypto.tls.Client.init(
327 tls.connection.stream_reader.interface(),
328 &tls.connection.stream_writer.interface,
329 .{
330 .host = .{ .explicit = remote_host },
331 .ca = .{ .bundle = client.ca_bundle },
332 .ssl_key_log = client.ssl_key_log,
333 .read_buffer = tls_read_buffer,
334 .write_buffer = tls_write_buffer,
335 // This is appropriate for HTTPS because the HTTP headers contain
336 // the content length which is used to detect truncation attacks.
337 .allow_truncation_attacks = true,
338 },
339 ) catch return error.TlsInitializationFailed,
340 };
341 return tls;
342 }
295343
296 if (nread > buffer.len) {344 fn destroy(tls: *Tls) void {
297 conn.read_start = 0;345 const c = &tls.connection;
298 conn.read_end = @intCast(nread - buffer.len);346 const gpa = c.client.allocator;
299 return buffer.len;347 const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls);
348 gpa.free(base[0..allocLen(c.client, c.host_len)]);
300 }349 }
301350
302 return nread;351 fn allocLen(client: *Client, host_len: usize) usize {
303 }352 return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size + client.write_buffer_size;
353 }
304354
305 pub const ReadError = error{355 fn host(tls: *Tls) []u8 {
306 TlsFailure,356 const base: [*]u8 = @ptrCast(tls);
307 TlsAlert,357 return base[@sizeOf(Tls)..][0..tls.connection.host_len];
308 ConnectionTimedOut,358 }
309 ConnectionResetByPeer,
310 UnexpectedReadFailure,
311 EndOfStream,
312 };359 };
313360
314 pub const Reader = std.io.GenericReader(*Connection, ReadError, read);361 fn getStream(c: *Connection) net.Stream {
315362 return c.stream_reader.getStream();
316 pub fn reader(conn: *Connection) Reader {
317 return Reader{ .context = conn };
318 }363 }
319364
320 pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void {365 fn host(c: *Connection) []u8 {
321 return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) {366 return switch (c.protocol) {
322 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,367 .tls => {
323 else => return error.UnexpectedWriteFailure,368 if (disable_tls) unreachable;
324 };369 const tls: *Tls = @fieldParentPtr("connection", c);
325 }370 return tls.host();
326371 },
327 pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void {372 .plain => {
328 if (conn.protocol == .tls) {373 const plain: *Plain = @fieldParentPtr("connection", c);
329 if (disable_tls) unreachable;374 return plain.host();
330375 },
331 return conn.writeAllDirectTls(buffer);
332 }
333
334 return conn.stream.writeAll(buffer) catch |err| switch (err) {
335 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
336 else => return error.UnexpectedWriteFailure,
337 };376 };
338 }377 }
339378
340 /// Writes the given buffer to the connection.379 /// If this is called without calling `flush` or `end`, data will be
341 pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {380 /// dropped unsent.
342 if (conn.write_buf.len - conn.write_end < buffer.len) {381 pub fn destroy(c: *Connection) void {
343 try conn.flush();382 c.getStream().close();
344383 switch (c.protocol) {
345 if (buffer.len > conn.write_buf.len) {384 .tls => {
346 try conn.writeAllDirect(buffer);385 if (disable_tls) unreachable;
347 return buffer.len;386 const tls: *Tls = @fieldParentPtr("connection", c);
348 }387 tls.destroy();
388 },
389 .plain => {
390 const plain: *Plain = @fieldParentPtr("connection", c);
391 plain.destroy();
392 },
349 }393 }
350
351 @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer);
352 conn.write_end += @intCast(buffer.len);
353
354 return buffer.len;
355 }394 }
356395
357 /// Returns a buffer to be filled with exactly len bytes to write to the connection.396 /// HTTP protocol from client to server.
358 pub fn allocWriteBuffer(conn: *Connection, len: BufferSize) WriteError![]u8 {397 /// This either goes directly to `stream_writer`, or to a TLS client.
359 if (conn.write_buf.len - conn.write_end < len) try conn.flush();398 pub fn writer(c: *Connection) *Writer {
360 defer conn.write_end += len;399 return switch (c.protocol) {
361 return conn.write_buf[conn.write_end..][0..len];400 .tls => {
401 if (disable_tls) unreachable;
402 const tls: *Tls = @fieldParentPtr("connection", c);
403 return &tls.client.writer;
404 },
405 .plain => &c.stream_writer.interface,
406 };
362 }407 }
363408
364 /// Flushes the write buffer to the connection.409 /// HTTP protocol from server to client.
365 pub fn flush(conn: *Connection) WriteError!void {410 /// This either comes directly from `stream_reader`, or from a TLS client.
366 if (conn.write_end == 0) return;411 pub fn reader(c: *Connection) *Reader {
367412 return switch (c.protocol) {
368 try conn.writeAllDirect(conn.write_buf[0..conn.write_end]);413 .tls => {
369 conn.write_end = 0;414 if (disable_tls) unreachable;
415 const tls: *Tls = @fieldParentPtr("connection", c);
416 return &tls.client.reader;
417 },
418 .plain => c.stream_reader.interface(),
419 };
370 }420 }
371421
372 pub const WriteError = error{422 pub fn flush(c: *Connection) Writer.Error!void {
373 ConnectionResetByPeer,423 if (c.protocol == .tls) {
374 UnexpectedWriteFailure,424 if (disable_tls) unreachable;
375 };425 const tls: *Tls = @fieldParentPtr("connection", c);
376426 try tls.client.writer.flush();
377 pub const Writer = std.io.GenericWriter(*Connection, WriteError, write);427 }
378428 try c.stream_writer.interface.flush();
379 pub fn writer(conn: *Connection) Writer {
380 return Writer{ .context = conn };
381 }429 }
382430
383 /// Closes the connection.431 /// If the connection is a TLS connection, sends the close_notify alert.
384 pub fn close(conn: *Connection, allocator: Allocator) void {432 ///
385 if (conn.protocol == .tls) {433 /// Flushes all buffers.
434 pub fn end(c: *Connection) Writer.Error!void {
435 if (c.protocol == .tls) {
386 if (disable_tls) unreachable;436 if (disable_tls) unreachable;
387437 const tls: *Tls = @fieldParentPtr("connection", c);
388 // try to cleanly close the TLS connection, for any server that cares.438 try tls.client.end();
389 _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};439 try tls.client.writer.flush();
390 if (conn.tls_client.ssl_key_log) |key_log| key_log.file.close();
391 allocator.destroy(conn.tls_client);
392 }440 }
393441 try c.stream_writer.interface.flush();
394 conn.stream.close();
395 allocator.free(conn.host);
396 }442 }
397};443};
398444
399/// The mode of transport for requests.
400pub const RequestTransfer = union(enum) {
401 content_length: u64,
402 chunked: void,
403 none: void,
404};
405
406/// The decompressor for response messages.
407pub const Compression = union(enum) {
408 //deflate: std.compress.flate.Decompress,
409 //gzip: std.compress.flate.Decompress,
410 // https://github.com/ziglang/zig/issues/18937
411 //zstd: ZstdDecompressor,
412 none: void,
413};
414
415/// A HTTP response originating from a server.
416pub const Response = struct {445pub const Response = struct {
417 version: http.Version,446 request: *Request,
418 status: http.Status,447 /// Pointers in this struct are invalidated with the next call to
419 reason: []const u8,448 /// `receiveHead`.
420449 head: Head,
421 /// Points into the user-provided `server_header_buffer`.450
422 location: ?[]const u8 = null,451 pub const Head = struct {
423 /// Points into the user-provided `server_header_buffer`.452 bytes: []const u8,
424 content_type: ?[]const u8 = null,453 version: http.Version,
425 /// Points into the user-provided `server_header_buffer`.454 status: http.Status,
426 content_disposition: ?[]const u8 = null,455 reason: []const u8,
427456 location: ?[]const u8 = null,
428 keep_alive: bool,457 content_type: ?[]const u8 = null,
429458 content_disposition: ?[]const u8 = null,
430 /// If present, the number of bytes in the response body.459
431 content_length: ?u64 = null,460 keep_alive: bool,
461
462 /// If present, the number of bytes in the response body.
463 content_length: ?u64 = null,
464
465 transfer_encoding: http.TransferEncoding = .none,
466 content_encoding: http.ContentEncoding = .identity,
467
468 pub const ParseError = error{
469 HttpConnectionHeaderUnsupported,
470 HttpContentEncodingUnsupported,
471 HttpHeaderContinuationsUnsupported,
472 HttpHeadersInvalid,
473 HttpTransferEncodingUnsupported,
474 InvalidContentLength,
475 };
432476
433 /// If present, the transfer encoding of the response body, otherwise none.477 pub fn parse(bytes: []const u8) ParseError!Head {
434 transfer_encoding: http.TransferEncoding = .none,478 var res: Head = .{
479 .bytes = bytes,
480 .status = undefined,
481 .reason = undefined,
482 .version = undefined,
483 .keep_alive = false,
484 };
485 var it = mem.splitSequence(u8, bytes, "\r\n");
435486
436 /// If present, the compression of the response body, otherwise identity (no compression).487 const first_line = it.next().?;
437 transfer_compression: http.ContentEncoding = .identity,488 if (first_line.len < 12) {
489 return error.HttpHeadersInvalid;
490 }
438491
439 parser: proto.HeadersParser,492 const version: http.Version = switch (int64(first_line[0..8])) {
440 compression: Compression = .none,493 int64("HTTP/1.0") => .@"HTTP/1.0",
494 int64("HTTP/1.1") => .@"HTTP/1.1",
495 else => return error.HttpHeadersInvalid,
496 };
497 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
498 const status: http.Status = @enumFromInt(parseInt3(first_line[9..12]));
499 const reason = mem.trimLeft(u8, first_line[12..], " ");
500
501 res.version = version;
502 res.status = status;
503 res.reason = reason;
504 res.keep_alive = switch (version) {
505 .@"HTTP/1.0" => false,
506 .@"HTTP/1.1" => true,
507 };
441508
442 /// Whether the response body should be skipped. Any data read from the509 while (it.next()) |line| {
443 /// response body will be discarded.510 if (line.len == 0) return res;
444 skip: bool = false,511 switch (line[0]) {
512 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
513 else => {},
514 }
445515
446 pub const ParseError = error{516 var line_it = mem.splitScalar(u8, line, ':');
447 HttpHeadersInvalid,517 const header_name = line_it.next().?;
448 HttpHeaderContinuationsUnsupported,518 const header_value = mem.trim(u8, line_it.rest(), " \t");
449 HttpTransferEncodingUnsupported,519 if (header_name.len == 0) return error.HttpHeadersInvalid;
450 HttpConnectionHeaderUnsupported,520
451 InvalidContentLength,521 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
452 CompressionUnsupported,522 res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
453 };523 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
524 res.content_type = header_value;
525 } else if (std.ascii.eqlIgnoreCase(header_name, "location")) {
526 res.location = header_value;
527 } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) {
528 res.content_disposition = header_value;
529 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
530 // Transfer-Encoding: second, first
531 // Transfer-Encoding: deflate, chunked
532 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
533
534 const first = iter.first();
535 const trimmed_first = mem.trim(u8, first, " ");
536
537 var next: ?[]const u8 = first;
538 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
539 if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
540 res.transfer_encoding = transfer;
541
542 next = iter.next();
543 }
454544
455 pub fn parse(res: *Response, bytes: []const u8) ParseError!void {545 if (next) |second| {
456 var it = mem.splitSequence(u8, bytes, "\r\n");546 const trimmed_second = mem.trim(u8, second, " ");
457547
458 const first_line = it.next().?;548 if (http.ContentEncoding.fromString(trimmed_second)) |transfer| {
459 if (first_line.len < 12) {549 if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported
460 return error.HttpHeadersInvalid;550 res.content_encoding = transfer;
461 }551 } else {
552 return error.HttpTransferEncodingUnsupported;
553 }
554 }
462555
463 const version: http.Version = switch (int64(first_line[0..8])) {556 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
464 int64("HTTP/1.0") => .@"HTTP/1.0",557 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
465 int64("HTTP/1.1") => .@"HTTP/1.1",558 const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
466 else => return error.HttpHeadersInvalid,
467 };
468 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
469 const status: http.Status = @enumFromInt(parseInt3(first_line[9..12]));
470 const reason = mem.trimStart(u8, first_line[12..], " ");
471
472 res.version = version;
473 res.status = status;
474 res.reason = reason;
475 res.keep_alive = switch (version) {
476 .@"HTTP/1.0" => false,
477 .@"HTTP/1.1" => true,
478 };
479559
480 while (it.next()) |line| {560 if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid;
481 if (line.len == 0) return;
482 switch (line[0]) {
483 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
484 else => {},
485 }
486561
487 var line_it = mem.splitScalar(u8, line, ':');562 res.content_length = content_length;
488 const header_name = line_it.next().?;563 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
489 const header_value = mem.trim(u8, line_it.rest(), " \t");564 if (res.content_encoding != .identity) return error.HttpHeadersInvalid;
490 if (header_name.len == 0) return error.HttpHeadersInvalid;
491
492 if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
493 res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close");
494 } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) {
495 res.content_type = header_value;
496 } else if (std.ascii.eqlIgnoreCase(header_name, "location")) {
497 res.location = header_value;
498 } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) {
499 res.content_disposition = header_value;
500 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
501 // Transfer-Encoding: second, first
502 // Transfer-Encoding: deflate, chunked
503 var iter = mem.splitBackwardsScalar(u8, header_value, ',');
504
505 const first = iter.first();
506 const trimmed_first = mem.trim(u8, first, " ");
507
508 var next: ?[]const u8 = first;
509 if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| {
510 if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding
511 res.transfer_encoding = transfer;
512
513 next = iter.next();
514 }
515565
516 if (next) |second| {566 const trimmed = mem.trim(u8, header_value, " ");
517 const trimmed_second = mem.trim(u8, second, " ");
518567
519 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {568 if (http.ContentEncoding.fromString(trimmed)) |ce| {
520 if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported569 res.content_encoding = ce;
521 res.transfer_compression = transfer;
522 } else {570 } else {
523 return error.HttpTransferEncodingUnsupported;571 return error.HttpContentEncodingUnsupported;
524 }572 }
525 }573 }
526
527 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
528 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
529 const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
530
531 if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid;
532
533 res.content_length = content_length;
534 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
535 if (res.transfer_compression != .identity) return error.HttpHeadersInvalid;
536
537 const trimmed = mem.trim(u8, header_value, " ");
538
539 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
540 res.transfer_compression = ce;
541 } else {
542 return error.HttpTransferEncodingUnsupported;
543 }
544 }574 }
575 return error.HttpHeadersInvalid; // missing empty line
545 }576 }
546 return error.HttpHeadersInvalid; // missing empty line
547 }
548
549 test parse {
550 const response_bytes = "HTTP/1.1 200 OK\r\n" ++
551 "LOcation:url\r\n" ++
552 "content-tYpe: text/plain\r\n" ++
553 "content-disposition:attachment; filename=example.txt \r\n" ++
554 "content-Length:10\r\n" ++
555 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
556 "connectioN:\t keep-alive \r\n\r\n";
557
558 var header_buffer: [1024]u8 = undefined;
559 var res = Response{
560 .status = undefined,
561 .reason = undefined,
562 .version = undefined,
563 .keep_alive = false,
564 .parser = .init(&header_buffer),
565 };
566577
567 @memcpy(header_buffer[0..response_bytes.len], response_bytes);578 test parse {
568 res.parser.header_bytes_len = response_bytes.len;579 const response_bytes = "HTTP/1.1 200 OK\r\n" ++
580 "LOcation:url\r\n" ++
581 "content-tYpe: text/plain\r\n" ++
582 "content-disposition:attachment; filename=example.txt \r\n" ++
583 "content-Length:10\r\n" ++
584 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
585 "connectioN:\t keep-alive \r\n\r\n";
586
587 const head = try Head.parse(response_bytes);
588
589 try testing.expectEqual(.@"HTTP/1.1", head.version);
590 try testing.expectEqualStrings("OK", head.reason);
591 try testing.expectEqual(.ok, head.status);
592
593 try testing.expectEqualStrings("url", head.location.?);
594 try testing.expectEqualStrings("text/plain", head.content_type.?);
595 try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?);
596
597 try testing.expectEqual(true, head.keep_alive);
598 try testing.expectEqual(10, head.content_length.?);
599 try testing.expectEqual(.chunked, head.transfer_encoding);
600 try testing.expectEqual(.deflate, head.content_encoding);
601 }
569602
570 try res.parse(response_bytes);603 pub fn iterateHeaders(h: Head) http.HeaderIterator {
604 return .init(h.bytes);
605 }
571606
572 try testing.expectEqual(.@"HTTP/1.1", res.version);607 test iterateHeaders {
573 try testing.expectEqualStrings("OK", res.reason);608 const response_bytes = "HTTP/1.1 200 OK\r\n" ++
574 try testing.expectEqual(.ok, res.status);609 "LOcation:url\r\n" ++
610 "content-tYpe: text/plain\r\n" ++
611 "content-disposition:attachment; filename=example.txt \r\n" ++
612 "content-Length:10\r\n" ++
613 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
614 "connectioN:\t keep-alive \r\n\r\n";
615
616 const head = try Head.parse(response_bytes);
617 var it = head.iterateHeaders();
618 {
619 const header = it.next().?;
620 try testing.expectEqualStrings("LOcation", header.name);
621 try testing.expectEqualStrings("url", header.value);
622 try testing.expect(!it.is_trailer);
623 }
624 {
625 const header = it.next().?;
626 try testing.expectEqualStrings("content-tYpe", header.name);
627 try testing.expectEqualStrings("text/plain", header.value);
628 try testing.expect(!it.is_trailer);
629 }
630 {
631 const header = it.next().?;
632 try testing.expectEqualStrings("content-disposition", header.name);
633 try testing.expectEqualStrings("attachment; filename=example.txt", header.value);
634 try testing.expect(!it.is_trailer);
635 }
636 {
637 const header = it.next().?;
638 try testing.expectEqualStrings("content-Length", header.name);
639 try testing.expectEqualStrings("10", header.value);
640 try testing.expect(!it.is_trailer);
641 }
642 {
643 const header = it.next().?;
644 try testing.expectEqualStrings("TRansfer-encoding", header.name);
645 try testing.expectEqualStrings("deflate, chunked", header.value);
646 try testing.expect(!it.is_trailer);
647 }
648 {
649 const header = it.next().?;
650 try testing.expectEqualStrings("connectioN", header.name);
651 try testing.expectEqualStrings("keep-alive", header.value);
652 try testing.expect(!it.is_trailer);
653 }
654 try testing.expectEqual(null, it.next());
655 }
575656
576 try testing.expectEqualStrings("url", res.location.?);657 inline fn int64(array: *const [8]u8) u64 {
577 try testing.expectEqualStrings("text/plain", res.content_type.?);658 return @bitCast(array.*);
578 try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?);659 }
579660
580 try testing.expectEqual(true, res.keep_alive);661 fn parseInt3(text: *const [3]u8) u10 {
581 try testing.expectEqual(10, res.content_length.?);662 const nnn: @Vector(3, u8) = text.*;
582 try testing.expectEqual(.chunked, res.transfer_encoding);663 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
583 try testing.expectEqual(.deflate, res.transfer_compression);664 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
584 }665 return @reduce(.Add, (nnn -% zero) *% mmm);
666 }
585667
586 inline fn int64(array: *const [8]u8) u64 {668 test parseInt3 {
587 return @bitCast(array.*);669 const expectEqual = testing.expectEqual;
588 }670 try expectEqual(@as(u10, 0), parseInt3("000"));
671 try expectEqual(@as(u10, 418), parseInt3("418"));
672 try expectEqual(@as(u10, 999), parseInt3("999"));
673 }
674 };
589675
590 fn parseInt3(text: *const [3]u8) u10 {676 /// If compressed body has been negotiated this will return compressed bytes.
591 const nnn: @Vector(3, u8) = text.*;677 ///
592 const zero: @Vector(3, u8) = .{ '0', '0', '0' };678 /// If the returned `Reader` returns `error.ReadFailed` the error is
593 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };679 /// available via `bodyErr`.
594 return @reduce(.Add, (nnn -% zero) *% mmm);680 ///
681 /// Asserts that this function is only called once.
682 ///
683 /// See also:
684 /// * `readerDecompressing`
685 pub fn reader(response: *Response, buffer: []u8) *Reader {
686 const req = response.request;
687 if (!req.method.responseHasBody()) return .ending;
688 const head = &response.head;
689 return req.reader.bodyReader(buffer, head.transfer_encoding, head.content_length);
595 }690 }
596691
597 test parseInt3 {692 /// If compressed body has been negotiated this will return decompressed bytes.
598 const expectEqual = testing.expectEqual;693 ///
599 try expectEqual(@as(u10, 0), parseInt3("000"));694 /// If the returned `Reader` returns `error.ReadFailed` the error is
600 try expectEqual(@as(u10, 418), parseInt3("418"));695 /// available via `bodyErr`.
601 try expectEqual(@as(u10, 999), parseInt3("999"));696 ///
697 /// Asserts that this function is only called once.
698 ///
699 /// See also:
700 /// * `reader`
701 pub fn readerDecompressing(
702 response: *Response,
703 decompressor: *http.Decompressor,
704 decompression_buffer: []u8,
705 ) *Reader {
706 const head = &response.head;
707 return response.request.reader.bodyReaderDecompressing(
708 head.transfer_encoding,
709 head.content_length,
710 head.content_encoding,
711 decompressor,
712 decompression_buffer,
713 );
602 }714 }
603715
604 pub fn iterateHeaders(r: Response) http.HeaderIterator {716 /// After receiving `error.ReadFailed` from the `Reader` returned by
605 return .init(r.parser.get());717 /// `reader` or `readerDecompressing`, this function accesses the
718 /// more specific error code.
719 pub fn bodyErr(response: *const Response) ?http.Reader.BodyError {
720 return response.request.reader.body_err;
606 }721 }
607722
608 test iterateHeaders {723 pub fn iterateTrailers(response: *const Response) http.HeaderIterator {
609 const response_bytes = "HTTP/1.1 200 OK\r\n" ++724 const r = &response.request.reader;
610 "LOcation:url\r\n" ++725 assert(r.state == .ready);
611 "content-tYpe: text/plain\r\n" ++726 return .{
612 "content-disposition:attachment; filename=example.txt \r\n" ++727 .bytes = r.trailers,
613 "content-Length:10\r\n" ++728 .index = 0,
614 "TRansfer-encoding:\tdeflate, chunked \r\n" ++729 .is_trailer = true,
615 "connectioN:\t keep-alive \r\n\r\n";
616
617 var header_buffer: [1024]u8 = undefined;
618 var res = Response{
619 .status = undefined,
620 .reason = undefined,
621 .version = undefined,
622 .keep_alive = false,
623 .parser = .init(&header_buffer),
624 };730 };
625
626 @memcpy(header_buffer[0..response_bytes.len], response_bytes);
627 res.parser.header_bytes_len = response_bytes.len;
628
629 var it = res.iterateHeaders();
630 {
631 const header = it.next().?;
632 try testing.expectEqualStrings("LOcation", header.name);
633 try testing.expectEqualStrings("url", header.value);
634 try testing.expect(!it.is_trailer);
635 }
636 {
637 const header = it.next().?;
638 try testing.expectEqualStrings("content-tYpe", header.name);
639 try testing.expectEqualStrings("text/plain", header.value);
640 try testing.expect(!it.is_trailer);
641 }
642 {
643 const header = it.next().?;
644 try testing.expectEqualStrings("content-disposition", header.name);
645 try testing.expectEqualStrings("attachment; filename=example.txt", header.value);
646 try testing.expect(!it.is_trailer);
647 }
648 {
649 const header = it.next().?;
650 try testing.expectEqualStrings("content-Length", header.name);
651 try testing.expectEqualStrings("10", header.value);
652 try testing.expect(!it.is_trailer);
653 }
654 {
655 const header = it.next().?;
656 try testing.expectEqualStrings("TRansfer-encoding", header.name);
657 try testing.expectEqualStrings("deflate, chunked", header.value);
658 try testing.expect(!it.is_trailer);
659 }
660 {
661 const header = it.next().?;
662 try testing.expectEqualStrings("connectioN", header.name);
663 try testing.expectEqualStrings("keep-alive", header.value);
664 try testing.expect(!it.is_trailer);
665 }
666 try testing.expectEqual(null, it.next());
667 }731 }
668};732};
669733
670/// A HTTP request that has been sent.
671///
672/// Order of operations: open -> send[ -> write -> finish] -> wait -> read
673pub const Request = struct {734pub const Request = struct {
735 /// This field is provided so that clients can observe redirected URIs.
736 ///
737 /// Its backing memory is externally provided by API users when creating a
738 /// request, and then again provided externally via `redirect_buffer` to
739 /// `receiveHead`.
674 uri: Uri,740 uri: Uri,
675 client: *Client,741 client: *Client,
676 /// This is null when the connection is released.742 /// This is null when the connection is released.
677 connection: ?*Connection,743 connection: ?*Connection,
744 reader: http.Reader,
678 keep_alive: bool,745 keep_alive: bool,
679746
680 method: http.Method,747 method: http.Method,
681 version: http.Version = .@"HTTP/1.1",748 version: http.Version = .@"HTTP/1.1",
682 transfer_encoding: RequestTransfer,749 transfer_encoding: TransferEncoding,
683 redirect_behavior: RedirectBehavior,750 redirect_behavior: RedirectBehavior,
751 accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding,
684752
685 /// Whether the request should handle a 100-continue response before sending the request body.753 /// Whether the request should handle a 100-continue response before sending the request body.
686 handle_continue: bool,754 handle_continue: bool,
687755
688 /// The response associated with this request.
689 ///
690 /// This field is undefined until `wait` is called.
691 response: Response,
692
693 /// Standard headers that have default, but overridable, behavior.756 /// Standard headers that have default, but overridable, behavior.
694 headers: Headers,757 headers: Headers,
695758
...@@ -703,6 +766,20 @@ pub const Request = struct {...@@ -703,6 +766,20 @@ pub const Request = struct {
703 /// Externally-owned; must outlive the Request.766 /// Externally-owned; must outlive the Request.
704 privileged_headers: []const http.Header,767 privileged_headers: []const http.Header,
705768
769 pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: {
770 var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false);
771 result[@intFromEnum(http.ContentEncoding.gzip)] = true;
772 result[@intFromEnum(http.ContentEncoding.deflate)] = true;
773 result[@intFromEnum(http.ContentEncoding.identity)] = true;
774 break :b result;
775 };
776
777 pub const TransferEncoding = union(enum) {
778 content_length: u64,
779 chunked: void,
780 none: void,
781 };
782
706 pub const Headers = struct {783 pub const Headers = struct {
707 host: Value = .default,784 host: Value = .default,
708 authorization: Value = .default,785 authorization: Value = .default,
...@@ -742,98 +819,102 @@ pub const Request = struct {...@@ -742,98 +819,102 @@ pub const Request = struct {
742 }819 }
743 };820 };
744821
745 /// Frees all resources associated with the request.822 /// Returns the request's `Connection` back to the pool of the `Client`.
746 pub fn deinit(req: *Request) void {823 pub fn deinit(r: *Request) void {
747 if (req.connection) |connection| {824 r.reader.restituteHeadBuffer();
748 if (!req.response.parser.done) {825 if (r.connection) |connection| {
749 // If the response wasn't fully read, then we need to close the connection.826 connection.closing = connection.closing or switch (r.reader.state) {
750 connection.closing = true;827 .ready => false,
751 }828 .received_head => r.method.requestHasBody(),
752 req.client.connection_pool.release(req.client.allocator, connection);829 else => true,
830 };
831 r.client.connection_pool.release(connection);
753 }832 }
754 req.* = undefined;833 r.* = undefined;
755 }834 }
756835
757 // This function must deallocate all resources associated with the request,836 /// Sends and flushes a complete request as only HTTP head, no body.
758 // or keep those which will be used.837 pub fn sendBodiless(r: *Request) Writer.Error!void {
759 // This needs to be kept in sync with deinit and request.838 try sendBodilessUnflushed(r);
760 fn redirect(req: *Request, uri: Uri) !void {839 try r.connection.?.flush();
761 assert(req.response.parser.done);
762
763 req.client.connection_pool.release(req.client.allocator, req.connection.?);
764 req.connection = null;
765
766 var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer);
767 defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..];
768 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
769
770 const new_host = valid_uri.host.?.raw;
771 const prev_host = req.uri.host.?.raw;
772 const keep_privileged_headers =
773 std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and
774 std.ascii.endsWithIgnoreCase(new_host, prev_host) and
775 (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.');
776 if (!keep_privileged_headers) {
777 // When redirecting to a different domain, strip privileged headers.
778 req.privileged_headers = &.{};
779 }
780
781 if (switch (req.response.status) {
782 .see_other => true,
783 .moved_permanently, .found => req.method == .POST,
784 else => false,
785 }) {
786 // A redirect to a GET must change the method and remove the body.
787 req.method = .GET;
788 req.transfer_encoding = .none;
789 req.headers.content_type = .omit;
790 }
791
792 if (req.transfer_encoding != .none) {
793 // The request body has already been sent. The request is
794 // still in a valid state, but the redirect must be handled
795 // manually.
796 return error.RedirectRequiresResend;
797 }
798
799 req.uri = valid_uri;
800 req.connection = try req.client.connect(new_host, uriPort(valid_uri, protocol), protocol);
801 req.redirect_behavior.subtractOne();
802 req.response.parser.reset();
803
804 req.response = .{
805 .version = undefined,
806 .status = undefined,
807 .reason = undefined,
808 .keep_alive = undefined,
809 .parser = req.response.parser,
810 };
811 }840 }
812841
813 pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };842 /// Sends but does not flush a complete request as only HTTP head, no body.
843 pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void {
844 assert(r.transfer_encoding == .none);
845 assert(!r.method.requestHasBody());
846 try sendHead(r);
847 }
814848
815 /// Send the HTTP request headers to the server.849 /// Transfers the HTTP head over the connection and flushes.
816 pub fn send(req: *Request) SendError!void {850 ///
817 if (!req.method.requestHasBody() and req.transfer_encoding != .none)851 /// See also:
818 return error.UnsupportedTransferEncoding;852 /// * `sendBodyUnflushed`
853 pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter {
854 const result = try sendBodyUnflushed(r, buffer);
855 try r.connection.?.flush();
856 return result;
857 }
819858
820 const connection = req.connection.?;859 /// Transfers the HTTP head over the connection, which is not flushed until
821 var connection_writer_adapter = connection.writer().adaptToNewApi();860 /// `BodyWriter.flush` or `BodyWriter.end` is called.
822 const w = &connection_writer_adapter.new_interface;861 ///
823 sendAdapted(req, connection, w) catch |err| switch (err) {862 /// See also:
824 error.WriteFailed => return connection_writer_adapter.err.?,863 /// * `sendBody`
825 else => |e| return e,864 pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter {
865 assert(r.method.requestHasBody());
866 try sendHead(r);
867 const http_protocol_output = r.connection.?.writer();
868 return switch (r.transfer_encoding) {
869 .chunked => .{
870 .http_protocol_output = http_protocol_output,
871 .state = .{ .chunked = .init },
872 .writer = .{
873 .buffer = buffer,
874 .vtable = &.{
875 .drain = http.BodyWriter.chunkedDrain,
876 .sendFile = http.BodyWriter.chunkedSendFile,
877 },
878 },
879 },
880 .content_length => |len| .{
881 .http_protocol_output = http_protocol_output,
882 .state = .{ .content_length = len },
883 .writer = .{
884 .buffer = buffer,
885 .vtable = &.{
886 .drain = http.BodyWriter.contentLengthDrain,
887 .sendFile = http.BodyWriter.contentLengthSendFile,
888 },
889 },
890 },
891 .none => .{
892 .http_protocol_output = http_protocol_output,
893 .state = .none,
894 .writer = .{
895 .buffer = buffer,
896 .vtable = &.{
897 .drain = http.BodyWriter.noneDrain,
898 .sendFile = http.BodyWriter.noneSendFile,
899 },
900 },
901 },
826 };902 };
827 }903 }
828904
829 fn sendAdapted(req: *Request, connection: *Connection, w: *std.io.Writer) !void {905 /// Sends HTTP headers without flushing.
830 try req.method.format(w);906 fn sendHead(r: *Request) Writer.Error!void {
907 const uri = r.uri;
908 const connection = r.connection.?;
909 const w = connection.writer();
910
911 try r.method.write(w);
831 try w.writeByte(' ');912 try w.writeByte(' ');
832913
833 if (req.method == .CONNECT) {914 if (r.method == .CONNECT) {
834 try req.uri.writeToStream(w, .{ .authority = true });915 try uri.writeToStream(.{ .authority = true }, w);
835 } else {916 } else {
836 try req.uri.writeToStream(w, .{917 try uri.writeToStream(.{
837 .scheme = connection.proxied,918 .scheme = connection.proxied,
838 .authentication = connection.proxied,919 .authentication = connection.proxied,
839 .authority = connection.proxied,920 .authority = connection.proxied,
...@@ -842,58 +923,64 @@ pub const Request = struct {...@@ -842,58 +923,64 @@ pub const Request = struct {
842 });923 });
843 }924 }
844 try w.writeByte(' ');925 try w.writeByte(' ');
845 try w.writeAll(@tagName(req.version));926 try w.writeAll(@tagName(r.version));
846 try w.writeAll("\r\n");927 try w.writeAll("\r\n");
847928
848 if (try emitOverridableHeader("host: ", req.headers.host, w)) {929 if (try emitOverridableHeader("host: ", r.headers.host, w)) {
849 try w.writeAll("host: ");930 try w.writeAll("host: ");
850 try req.uri.writeToStream(w, .{ .authority = true });931 try uri.writeToStream(.{ .authority = true }, w);
851 try w.writeAll("\r\n");932 try w.writeAll("\r\n");
852 }933 }
853934
854 if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) {935 if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) {
855 if (req.uri.user != null or req.uri.password != null) {936 if (uri.user != null or uri.password != null) {
856 try w.writeAll("authorization: ");937 try w.writeAll("authorization: ");
857 const authorization = try connection.allocWriteBuffer(938 try basic_authorization.write(uri, w);
858 @intCast(basic_authorization.valueLengthFromUri(req.uri)),
859 );
860 assert(basic_authorization.value(req.uri, authorization).len == authorization.len);
861 try w.writeAll("\r\n");939 try w.writeAll("\r\n");
862 }940 }
863 }941 }
864942
865 if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) {943 if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) {
866 try w.writeAll("user-agent: zig/");944 try w.writeAll("user-agent: zig/");
867 try w.writeAll(builtin.zig_version_string);945 try w.writeAll(builtin.zig_version_string);
868 try w.writeAll(" (std.http)\r\n");946 try w.writeAll(" (std.http)\r\n");
869 }947 }
870948
871 if (try emitOverridableHeader("connection: ", req.headers.connection, w)) {949 if (try emitOverridableHeader("connection: ", r.headers.connection, w)) {
872 if (req.keep_alive) {950 if (r.keep_alive) {
873 try w.writeAll("connection: keep-alive\r\n");951 try w.writeAll("connection: keep-alive\r\n");
874 } else {952 } else {
875 try w.writeAll("connection: close\r\n");953 try w.writeAll("connection: close\r\n");
876 }954 }
877 }955 }
878956
879 if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) {957 if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) {
880 // https://github.com/ziglang/zig/issues/18937958 try w.writeAll("accept-encoding: ");
881 //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n");959 for (r.accept_encoding, 0..) |enabled, i| {
882 try w.writeAll("accept-encoding: gzip, deflate\r\n");960 if (!enabled) continue;
961 const tag: http.ContentEncoding = @enumFromInt(i);
962 if (tag == .identity) continue;
963 const tag_name = @tagName(tag);
964 try w.ensureUnusedCapacity(tag_name.len + 2);
965 try w.writeAll(tag_name);
966 try w.writeAll(", ");
967 }
968 w.undo(2);
969 try w.writeAll("\r\n");
883 }970 }
884971
885 switch (req.transfer_encoding) {972 switch (r.transfer_encoding) {
886 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),973 .chunked => try w.writeAll("transfer-encoding: chunked\r\n"),
887 .content_length => |len| try w.print("content-length: {d}\r\n", .{len}),974 .content_length => |len| try w.print("content-length: {d}\r\n", .{len}),
888 .none => {},975 .none => {},
889 }976 }
890977
891 if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) {978 if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) {
892 // The default is to omit content-type if not provided because979 // The default is to omit content-type if not provided because
893 // "application/octet-stream" is redundant.980 // "application/octet-stream" is redundant.
894 }981 }
895982
896 for (req.extra_headers) |header| {983 for (r.extra_headers) |header| {
897 assert(header.name.len != 0);984 assert(header.name.len != 0);
898985
899 try w.writeAll(header.name);986 try w.writeAll(header.name);
...@@ -904,8 +991,8 @@ pub const Request = struct {...@@ -904,8 +991,8 @@ pub const Request = struct {
904991
905 if (connection.proxied) proxy: {992 if (connection.proxied) proxy: {
906 const proxy = switch (connection.protocol) {993 const proxy = switch (connection.protocol) {
907 .plain => req.client.http_proxy,994 .plain => r.client.http_proxy,
908 .tls => req.client.https_proxy,995 .tls => r.client.https_proxy,
909 } orelse break :proxy;996 } orelse break :proxy;
910997
911 const authorization = proxy.authorization orelse break :proxy;998 const authorization = proxy.authorization orelse break :proxy;
...@@ -915,282 +1002,198 @@ pub const Request = struct {...@@ -915,282 +1002,198 @@ pub const Request = struct {
915 }1002 }
9161003
917 try w.writeAll("\r\n");1004 try w.writeAll("\r\n");
918
919 try connection.flush();
920 }1005 }
9211006
922 /// Returns true if the default behavior is required, otherwise handles1007 pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{
923 /// writing (or not writing) the header.1008 /// Server sent headers that did not conform to the HTTP protocol.
924 fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool {1009 ///
925 switch (v) {1010 /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be
926 .default => return true,1011 /// passed directly to `Request.Head.parse`.
927 .omit => return false,1012 HttpHeadersInvalid,
928 .override => |x| {1013 TooManyHttpRedirects,
929 try w.writeAll(prefix);1014 /// This can be avoided by calling `receiveHead` before sending the
930 try w.writeAll(x);1015 /// request body.
931 try w.writeAll("\r\n");1016 RedirectRequiresResend,
932 return false;1017 HttpRedirectLocationMissing,
933 },1018 HttpRedirectLocationOversize,
934 }1019 HttpRedirectLocationInvalid,
935 }1020 HttpContentEncodingUnsupported,
9361021 HttpChunkInvalid,
937 const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError;1022 HttpChunkTruncated,
9381023 HttpHeadersOversize,
939 const TransferReader = std.io.GenericReader(*Request, TransferReadError, transferRead);1024 UnsupportedUriScheme,
940
941 fn transferReader(req: *Request) TransferReader {
942 return .{ .context = req };
943 }
944
945 fn transferRead(req: *Request, buf: []u8) TransferReadError!usize {
946 if (req.response.parser.done) return 0;
947
948 var index: usize = 0;
949 while (index == 0) {
950 const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip);
951 if (amt == 0 and req.response.parser.done) break;
952 index += amt;
953 }
954
955 return index;
956 }
9571025
958 pub const WaitError = RequestError || SendError || TransferReadError ||1026 /// Sending the request failed. Error code can be found on the
959 proto.HeadersParser.CheckCompleteHeadError || Response.ParseError ||1027 /// `Connection` object.
960 error{1028 WriteFailed,
961 TooManyHttpRedirects,1029 };
962 RedirectRequiresResend,
963 HttpRedirectLocationMissing,
964 HttpRedirectLocationInvalid,
965 CompressionInitializationFailed,
966 CompressionUnsupported,
967 };
9681030
969 /// Waits for a response from the server and parses any headers that are sent.
970 /// This function will block until the final response is received.
971 ///
972 /// If handling redirects and the request has no payload, then this1031 /// If handling redirects and the request has no payload, then this
973 /// function will automatically follow redirects. If a request payload is1032 /// function will automatically follow redirects.
974 /// present, then this function will error with
975 /// error.RedirectRequiresResend.
976 ///1033 ///
977 /// Must be called after `send` and, if any data was written to the request1034 /// If a request payload is present, then this function will error with
978 /// body, then also after `finish`.1035 /// `error.RedirectRequiresResend`.
979 pub fn wait(req: *Request) WaitError!void {1036 ///
1037 /// This function takes an auxiliary buffer to store the arbitrarily large
1038 /// URI which may need to be merged with the previous URI, and that data
1039 /// needs to survive across different connections, which is where the input
1040 /// buffer lives.
1041 ///
1042 /// `redirect_buffer` must outlive accesses to `Request.uri`. If this
1043 /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize`
1044 /// is returned instead. This buffer may be empty if no redirects are to be
1045 /// handled.
1046 pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response {
1047 var aux_buf = redirect_buffer;
980 while (true) {1048 while (true) {
981 // This while loop is for handling redirects, which means the request's1049 try r.reader.receiveHead();
982 // connection may be different than the previous iteration. However, it1050 const response: Response = .{
983 // is still guaranteed to be non-null with each iteration of this loop.1051 .request = r,
984 const connection = req.connection.?;1052 .head = Response.Head.parse(r.reader.head_buffer) catch return error.HttpHeadersInvalid,
9851053 };
986 while (true) { // read headers1054 const head = &response.head;
987 try connection.fill();
988
989 const nchecked = try req.response.parser.checkCompleteHead(connection.peek());
990 connection.drop(@intCast(nchecked));
9911055
992 if (req.response.parser.state.isContent()) break;1056 if (head.status == .@"continue") {
1057 if (r.handle_continue) continue;
1058 return response; // we're not handling the 100-continue
993 }1059 }
9941060
995 try req.response.parse(req.response.parser.get());1061 // This while loop is for handling redirects, which means the request's
9961062 // connection may be different than the previous iteration. However, it
997 if (req.response.status == .@"continue") {1063 // is still guaranteed to be non-null with each iteration of this loop.
998 // We're done parsing the continue response; reset to prepare1064 const connection = r.connection.?;
999 // for the real response.
1000 req.response.parser.done = true;
1001 req.response.parser.reset();
1002
1003 if (req.handle_continue)
1004 continue;
1005
1006 return; // we're not handling the 100-continue
1007 }
10081065
1009 // we're switching protocols, so this connection is no longer doing http1066 if (r.method == .CONNECT and head.status.class() == .success) {
1010 if (req.method == .CONNECT and req.response.status.class() == .success) {1067 // This connection is no longer doing HTTP.
1011 connection.closing = false;1068 connection.closing = false;
1012 req.response.parser.done = true;1069 return response;
1013 return; // the connection is not HTTP past this point
1014 }1070 }
10151071
1016 connection.closing = !req.response.keep_alive or !req.keep_alive;1072 connection.closing = !head.keep_alive or !r.keep_alive;
10171073
1018 // Any response to a HEAD request and any response with a 1xx1074 // Any response to a HEAD request and any response with a 1xx
1019 // (Informational), 204 (No Content), or 304 (Not Modified) status1075 // (Informational), 204 (No Content), or 304 (Not Modified) status
1020 // code is always terminated by the first empty line after the1076 // code is always terminated by the first empty line after the
1021 // header fields, regardless of the header fields present in the1077 // header fields, regardless of the header fields present in the
1022 // message.1078 // message.
1023 if (req.method == .HEAD or req.response.status.class() == .informational or1079 if (r.method == .HEAD or head.status.class() == .informational or
1024 req.response.status == .no_content or req.response.status == .not_modified)1080 head.status == .no_content or head.status == .not_modified)
1025 {1081 {
1026 req.response.parser.done = true;1082 return response;
1027 return; // The response is empty; no further setup or redirection is necessary.
1028 }
1029
1030 switch (req.response.transfer_encoding) {
1031 .none => {
1032 if (req.response.content_length) |cl| {
1033 req.response.parser.next_chunk_length = cl;
1034
1035 if (cl == 0) req.response.parser.done = true;
1036 } else {
1037 // read until the connection is closed
1038 req.response.parser.next_chunk_length = std.math.maxInt(u64);
1039 }
1040 },
1041 .chunked => {
1042 req.response.parser.next_chunk_length = 0;
1043 req.response.parser.state = .chunk_head_size;
1044 },
1045 }1083 }
10461084
1047 if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) {1085 if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) {
1048 // skip the body of the redirect response, this will at least1086 if (r.redirect_behavior == .not_allowed) {
1049 // leave the connection in a known good state.1087 // Connection can still be reused by skipping the body.
1050 req.response.skip = true;1088 const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length);
1051 assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary1089 _ = reader.discardRemaining() catch |err| switch (err) {
10521090 error.ReadFailed => connection.closing = true,
1053 if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects;1091 };
10541092 return error.TooManyHttpRedirects;
1055 const location = req.response.location orelse
1056 return error.HttpRedirectLocationMissing;
1057
1058 // This mutates the beginning of header_bytes_buffer and uses that
1059 // for the backing memory of the returned Uri.
1060 try req.redirect(req.uri.resolve_inplace(
1061 location,
1062 &req.response.parser.header_bytes_buffer,
1063 ) catch |err| switch (err) {
1064 error.UnexpectedCharacter,
1065 error.InvalidFormat,
1066 error.InvalidPort,
1067 => return error.HttpRedirectLocationInvalid,
1068 error.NoSpaceLeft => return error.HttpHeadersOversize,
1069 });
1070 try req.send();
1071 } else {
1072 req.response.skip = false;
1073 if (!req.response.parser.done) {
1074 switch (req.response.transfer_compression) {
1075 .identity => req.response.compression = .none,
1076 .compress, .@"x-compress" => return error.CompressionUnsupported,
1077 // I'm about to upstream my http.Client rewrite
1078 .deflate => return error.CompressionUnsupported,
1079 // I'm about to upstream my http.Client rewrite
1080 .gzip, .@"x-gzip" => return error.CompressionUnsupported,
1081 // https://github.com/ziglang/zig/issues/18937
1082 //.zstd => req.response.compression = .{
1083 // .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
1084 //},
1085 .zstd => return error.CompressionUnsupported,
1086 }
1087 }1093 }
10881094 try r.redirect(head, &aux_buf);
1089 break;1095 try r.sendBodiless();
1096 continue;
1090 }1097 }
1091 }
1092 }
1093
1094 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError ||
1095 error{ DecompressionFailure, InvalidTrailers };
10961098
1097 pub const Reader = std.io.GenericReader(*Request, ReadError, read);1099 if (!r.accept_encoding[@intFromEnum(head.content_encoding)])
1100 return error.HttpContentEncodingUnsupported;
10981101
1099 pub fn reader(req: *Request) Reader {1102 return response;
1100 return .{ .context = req };
1101 }
1102
1103 /// Reads data from the response body. Must be called after `wait`.
1104 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
1105 const out_index = switch (req.response.compression) {
1106 // I'm about to upstream my http client rewrite
1107 //.deflate => |*deflate| deflate.readSlice(buffer) catch return error.DecompressionFailure,
1108 //.gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
1109 // https://github.com/ziglang/zig/issues/18937
1110 //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
1111 else => try req.transferRead(buffer),
1112 };
1113 if (out_index > 0) return out_index;
1114
1115 while (!req.response.parser.state.isContent()) { // read trailing headers
1116 try req.connection.?.fill();
1117
1118 const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek());
1119 req.connection.?.drop(@intCast(nchecked));
1120 }1103 }
1121
1122 return 0;
1123 }1104 }
11241105
1125 /// Reads data from the response body. Must be called after `wait`.1106 /// This function takes an auxiliary buffer to store the arbitrarily large
1126 pub fn readAll(req: *Request, buffer: []u8) !usize {1107 /// URI which may need to be merged with the previous URI, and that data
1127 var index: usize = 0;1108 /// needs to survive across different connections, which is where the input
1128 while (index < buffer.len) {1109 /// buffer lives.
1129 const amt = try read(req, buffer[index..]);1110 ///
1130 if (amt == 0) break;1111 /// `aux_buf` must outlive accesses to `Request.uri`.
1131 index += amt;1112 fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void {
1113 const new_location = head.location orelse return error.HttpRedirectLocationMissing;
1114 if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize;
1115 const location = aux_buf.*[0..new_location.len];
1116 @memcpy(location, new_location);
1117 {
1118 // Skip the body of the redirect response to leave the connection in
1119 // the correct state. This causes `new_location` to be invalidated.
1120 const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length);
1121 _ = reader.discardRemaining() catch |err| switch (err) {
1122 error.ReadFailed => return r.reader.body_err.?,
1123 };
1124 r.reader.restituteHeadBuffer();
1132 }1125 }
1133 return index;1126 const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) {
1134 }1127 error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid,
11351128 error.InvalidFormat => return error.HttpRedirectLocationInvalid,
1136 pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong };1129 error.InvalidPort => return error.HttpRedirectLocationInvalid,
11371130 error.NoSpaceLeft => return error.HttpRedirectLocationOversize,
1138 pub const Writer = std.io.GenericWriter(*Request, WriteError, write);1131 };
11391132
1140 pub fn writer(req: *Request) Writer {1133 const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme;
1141 return .{ .context = req };1134 const old_connection = r.connection.?;
1142 }1135 const old_host = old_connection.host();
1136 var new_host_name_buffer: [Uri.host_name_max]u8 = undefined;
1137 const new_host = try new_uri.getHost(&new_host_name_buffer);
1138 const keep_privileged_headers =
1139 std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and
1140 sameParentDomain(old_host, new_host);
11431141
1144 /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent.1142 r.client.connection_pool.release(old_connection);
1145 /// Must be called after `send` and before `finish`.1143 r.connection = null;
1146 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
1147 switch (req.transfer_encoding) {
1148 .chunked => {
1149 if (bytes.len > 0) {
1150 try req.connection.?.writer().print("{x}\r\n", .{bytes.len});
1151 try req.connection.?.writer().writeAll(bytes);
1152 try req.connection.?.writer().writeAll("\r\n");
1153 }
11541144
1155 return bytes.len;1145 if (!keep_privileged_headers) {
1156 },1146 // When redirecting to a different domain, strip privileged headers.
1157 .content_length => |*len| {1147 r.privileged_headers = &.{};
1158 if (len.* < bytes.len) return error.MessageTooLong;1148 }
11591149
1160 const amt = try req.connection.?.write(bytes);1150 if (switch (head.status) {
1161 len.* -= amt;1151 .see_other => true,
1162 return amt;1152 .moved_permanently, .found => r.method == .POST,
1163 },1153 else => false,
1164 .none => return error.NotWriteable,1154 }) {
1155 // A redirect to a GET must change the method and remove the body.
1156 r.method = .GET;
1157 r.transfer_encoding = .none;
1158 r.headers.content_type = .omit;
1165 }1159 }
1166 }
11671160
1168 /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent.1161 if (r.transfer_encoding != .none) {
1169 /// Must be called after `send` and before `finish`.1162 // The request body has already been sent. The request is
1170 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {1163 // still in a valid state, but the redirect must be handled
1171 var index: usize = 0;1164 // manually.
1172 while (index < bytes.len) {1165 return error.RedirectRequiresResend;
1173 index += try write(req, bytes[index..]);
1174 }1166 }
1175 }
11761167
1177 pub const FinishError = WriteError || error{MessageNotCompleted};1168 const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol);
1169 r.uri = new_uri;
1170 r.connection = new_connection;
1171 r.reader = .{
1172 .in = new_connection.reader(),
1173 .state = .ready,
1174 // Populated when `http.Reader.bodyReader` is called.
1175 .interface = undefined,
1176 };
1177 r.redirect_behavior.subtractOne();
1178 }
11781179
1179 /// Finish the body of a request. This notifies the server that you have no more data to send.1180 /// Returns true if the default behavior is required, otherwise handles
1180 /// Must be called after `send`.1181 /// writing (or not writing) the header.
1181 pub fn finish(req: *Request) FinishError!void {1182 fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool {
1182 switch (req.transfer_encoding) {1183 switch (v) {
1183 .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"),1184 .default => return true,
1184 .content_length => |len| if (len != 0) return error.MessageNotCompleted,1185 .omit => return false,
1185 .none => {},1186 .override => |x| {
1187 var vecs: [3][]const u8 = .{ prefix, x, "\r\n" };
1188 try bw.writeVecAll(&vecs);
1189 return false;
1190 },
1186 }1191 }
1187
1188 try req.connection.?.flush();
1189 }1192 }
1190};1193};
11911194
1192pub const Proxy = struct {1195pub const Proxy = struct {
1193 protocol: Connection.Protocol,1196 protocol: Protocol,
1194 host: []const u8,1197 host: []const u8,
1195 authorization: ?[]const u8,1198 authorization: ?[]const u8,
1196 port: u16,1199 port: u16,
...@@ -1204,10 +1207,8 @@ pub const Proxy = struct {...@@ -1204,10 +1207,8 @@ pub const Proxy = struct {
1204pub fn deinit(client: *Client) void {1207pub fn deinit(client: *Client) void {
1205 assert(client.connection_pool.used.first == null); // There are still active requests.1208 assert(client.connection_pool.used.first == null); // There are still active requests.
12061209
1207 client.connection_pool.deinit(client.allocator);1210 client.connection_pool.deinit();
12081211 if (!disable_tls) client.ca_bundle.deinit(client.allocator);
1209 if (!disable_tls)
1210 client.ca_bundle.deinit(client.allocator);
12111212
1212 client.* = undefined;1213 client.* = undefined;
1213}1214}
...@@ -1249,24 +1250,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?...@@ -1249,24 +1250,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?
1249 } else return null;1250 } else return null;
12501251
1251 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);1252 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
1252 const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) {1253 const protocol = Protocol.fromUri(uri) orelse return null;
1253 error.UnsupportedUriScheme => return null,1254 const raw_host = try uri.getHostAlloc(arena);
1254 error.UriMissingHost => return error.HttpProxyMissingHost,
1255 error.OutOfMemory => |e| return e,
1256 };
12571255
1258 const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: {1256 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
1259 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri));1257 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
1260 assert(basic_authorization.value(valid_uri, authorization).len == authorization.len);1258 assert(basic_authorization.value(uri, authorization).len == authorization.len);
1261 break :a authorization;1259 break :a authorization;
1262 } else null;1260 } else null;
12631261
1264 const proxy = try arena.create(Proxy);1262 const proxy = try arena.create(Proxy);
1265 proxy.* = .{1263 proxy.* = .{
1266 .protocol = protocol,1264 .protocol = protocol,
1267 .host = valid_uri.host.?.raw,1265 .host = raw_host,
1268 .authorization = authorization,1266 .authorization = authorization,
1269 .port = uriPort(valid_uri, protocol),1267 .port = uriPort(uri, protocol),
1270 .supports_connect = true,1268 .supports_connect = true,
1271 };1269 };
1272 return proxy;1270 return proxy;
...@@ -1277,10 +1275,8 @@ pub const basic_authorization = struct {...@@ -1277,10 +1275,8 @@ pub const basic_authorization = struct {
1277 pub const max_password_len = 255;1275 pub const max_password_len = 255;
1278 pub const max_value_len = valueLength(max_user_len, max_password_len);1276 pub const max_value_len = valueLength(max_user_len, max_password_len);
12791277
1280 const prefix = "Basic ";
1281
1282 pub fn valueLength(user_len: usize, password_len: usize) usize {1278 pub fn valueLength(user_len: usize, password_len: usize) usize {
1283 return prefix.len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len);1279 return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len);
1284 }1280 }
12851281
1286 pub fn valueLengthFromUri(uri: Uri) usize {1282 pub fn valueLengthFromUri(uri: Uri) usize {
...@@ -1300,37 +1296,69 @@ pub const basic_authorization = struct {...@@ -1300,37 +1296,69 @@ pub const basic_authorization = struct {
1300 }1296 }
13011297
1302 pub fn value(uri: Uri, out: []u8) []u8 {1298 pub fn value(uri: Uri, out: []u8) []u8 {
1303 const user: Uri.Component = uri.user orelse .empty;1299 var bw: Writer = .fixed(out);
1304 const password: Uri.Component = uri.password orelse .empty;1300 write(uri, &bw) catch unreachable;
1301 return bw.getWritten();
1302 }
13051303
1304 pub fn write(uri: Uri, out: *Writer) Writer.Error!void {
1306 var buf: [max_user_len + ":".len + max_password_len]u8 = undefined;1305 var buf: [max_user_len + ":".len + max_password_len]u8 = undefined;
1307 var w: std.io.Writer = .fixed(&buf);1306 var w: Writer = .fixed(&buf);
1308 user.formatUser(&w) catch unreachable; // fixed1307 w.print("{fuser}:{fpassword}", .{
1309 password.formatPassword(&w) catch unreachable; // fixed1308 uri.user orelse Uri.Component.empty,
13101309 uri.password orelse Uri.Component.empty,
1311 @memcpy(out[0..prefix.len], prefix);1310 }) catch unreachable;
1312 const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], w.buffered());1311 try out.print("Basic {b64}", .{w.buffered()});
1313 return out[0 .. prefix.len + base64.len];
1314 }1312 }
1315};1313};
13161314
1317pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };1315pub const ConnectTcpError = Allocator.Error || error{
1316 ConnectionRefused,
1317 NetworkUnreachable,
1318 ConnectionTimedOut,
1319 ConnectionResetByPeer,
1320 TemporaryNameServerFailure,
1321 NameServerFailure,
1322 UnknownHostName,
1323 HostLacksNetworkAddresses,
1324 UnexpectedConnectFailure,
1325 TlsInitializationFailed,
1326};
13181327
1319/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.1328/// Reuses a `Connection` if one matching `host` and `port` is already open.
1320///1329///
1321/// This function is threadsafe.1330/// Threadsafe.
1322pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection {1331pub fn connectTcp(
1323 if (client.connection_pool.findConnection(.{1332 client: *Client,
1324 .host = host,1333 host: []const u8,
1325 .port = port,1334 port: u16,
1326 .protocol = protocol,1335 protocol: Protocol,
1327 })) |node| return node;1336) ConnectTcpError!*Connection {
1337 return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol });
1338}
1339
1340pub const ConnectTcpOptions = struct {
1341 host: []const u8,
1342 port: u16,
1343 protocol: Protocol,
13281344
1329 if (disable_tls and protocol == .tls)1345 proxied_host: ?[]const u8 = null,
1330 return error.TlsInitializationFailed;1346 proxied_port: ?u16 = null,
1347};
13311348
1332 const conn = try client.allocator.create(Connection);1349pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection {
1333 errdefer client.allocator.destroy(conn);1350 const host = options.host;
1351 const port = options.port;
1352 const protocol = options.protocol;
1353
1354 const proxied_host = options.proxied_host orelse host;
1355 const proxied_port = options.proxied_port orelse port;
1356
1357 if (client.connection_pool.findConnection(.{
1358 .host = proxied_host,
1359 .port = proxied_port,
1360 .protocol = protocol,
1361 })) |conn| return conn;
13341362
1335 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {1363 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {
1336 error.ConnectionRefused => return error.ConnectionRefused,1364 error.ConnectionRefused => return error.ConnectionRefused,
...@@ -1345,53 +1373,19 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec...@@ -1345,53 +1373,19 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
1345 };1373 };
1346 errdefer stream.close();1374 errdefer stream.close();
13471375
1348 conn.* = .{1376 switch (protocol) {
1349 .stream = stream,1377 .tls => {
1350 .tls_client = undefined,1378 if (disable_tls) return error.TlsInitializationFailed;
13511379 const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream);
1352 .protocol = protocol,1380 client.connection_pool.addUsed(&tc.connection);
1353 .host = try client.allocator.dupe(u8, host),1381 return &tc.connection;
1354 .port = port,1382 },
13551383 .plain => {
1356 .pool_node = .{},1384 const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream);
1357 };1385 client.connection_pool.addUsed(&pc.connection);
1358 errdefer client.allocator.free(conn.host);1386 return &pc.connection;
13591387 },
1360 if (protocol == .tls) {
1361 if (disable_tls) unreachable;
1362
1363 conn.tls_client = try client.allocator.create(std.crypto.tls.Client);
1364 errdefer client.allocator.destroy(conn.tls_client);
1365
1366 const ssl_key_log_file: ?std.fs.File = if (std.options.http_enable_ssl_key_log_file) ssl_key_log_file: {
1367 const ssl_key_log_path = std.process.getEnvVarOwned(client.allocator, "SSLKEYLOGFILE") catch |err| switch (err) {
1368 error.EnvironmentVariableNotFound, error.InvalidWtf8 => break :ssl_key_log_file null,
1369 error.OutOfMemory => return error.OutOfMemory,
1370 };
1371 defer client.allocator.free(ssl_key_log_path);
1372 break :ssl_key_log_file std.fs.cwd().createFile(ssl_key_log_path, .{
1373 .truncate = false,
1374 .mode = switch (builtin.os.tag) {
1375 .windows, .wasi => 0,
1376 else => 0o600,
1377 },
1378 }) catch null;
1379 } else null;
1380 errdefer if (ssl_key_log_file) |key_log_file| key_log_file.close();
1381
1382 conn.tls_client.* = std.crypto.tls.Client.init(stream, .{
1383 .host = .{ .explicit = host },
1384 .ca = .{ .bundle = client.ca_bundle },
1385 .ssl_key_log_file = ssl_key_log_file,
1386 }) catch return error.TlsInitializationFailed;
1387 // This is appropriate for HTTPS because the HTTP headers contain
1388 // the content length which is used to detect truncation attacks.
1389 conn.tls_client.allow_truncation_attacks = true;
1390 }1388 }
1391
1392 client.connection_pool.addUsed(conn);
1393
1394 return conn;
1395}1389}
13961390
1397pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError;1391pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError;
...@@ -1429,69 +1423,67 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti...@@ -1429,69 +1423,67 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti
1429 return &conn.data;1423 return &conn.data;
1430}1424}
14311425
1432/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP1426/// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP
1433/// CONNECT. This will reuse a connection if one is already open.1427/// CONNECT. This will reuse a connection if one is already open.
1434///1428///
1435/// This function is threadsafe.1429/// This function is threadsafe.
1436pub fn connectTunnel(1430pub fn connectProxied(
1437 client: *Client,1431 client: *Client,
1438 proxy: *Proxy,1432 proxy: *Proxy,
1439 tunnel_host: []const u8,1433 proxied_host: []const u8,
1440 tunnel_port: u16,1434 proxied_port: u16,
1441) !*Connection {1435) !*Connection {
1442 if (!proxy.supports_connect) return error.TunnelNotSupported;1436 if (!proxy.supports_connect) return error.TunnelNotSupported;
14431437
1444 if (client.connection_pool.findConnection(.{1438 if (client.connection_pool.findConnection(.{
1445 .host = tunnel_host,1439 .host = proxied_host,
1446 .port = tunnel_port,1440 .port = proxied_port,
1447 .protocol = proxy.protocol,1441 .protocol = proxy.protocol,
1448 })) |node|1442 })) |node| return node;
1449 return node;
14501443
1451 var maybe_valid = false;1444 var maybe_valid = false;
1452 (tunnel: {1445 (tunnel: {
1453 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);1446 const connection = try client.connectTcpOptions(.{
1447 .host = proxy.host,
1448 .port = proxy.port,
1449 .protocol = proxy.protocol,
1450 .proxied_host = proxied_host,
1451 .proxied_port = proxied_port,
1452 });
1454 errdefer {1453 errdefer {
1455 conn.closing = true;1454 connection.closing = true;
1456 client.connection_pool.release(client.allocator, conn);1455 client.connection_pool.release(connection);
1457 }1456 }
14581457
1459 var buffer: [8096]u8 = undefined;1458 var req = client.request(.CONNECT, .{
1460 var req = client.open(.CONNECT, .{
1461 .scheme = "http",1459 .scheme = "http",
1462 .host = .{ .raw = tunnel_host },1460 .host = .{ .raw = proxied_host },
1463 .port = tunnel_port,1461 .port = proxied_port,
1464 }, .{1462 }, .{
1465 .redirect_behavior = .unhandled,1463 .redirect_behavior = .unhandled,
1466 .connection = conn,1464 .connection = connection,
1467 .server_header_buffer = &buffer,
1468 }) catch |err| {1465 }) catch |err| {
1469 std.log.debug("err {}", .{err});
1470 break :tunnel err;1466 break :tunnel err;
1471 };1467 };
1472 defer req.deinit();1468 defer req.deinit();
14731469
1474 req.send() catch |err| break :tunnel err;1470 req.sendBodiless() catch |err| break :tunnel err;
1475 req.wait() catch |err| break :tunnel err;1471 const response = req.receiveHead(&.{}) catch |err| break :tunnel err;
14761472
1477 if (req.response.status.class() == .server_error) {1473 if (response.head.status.class() == .server_error) {
1478 maybe_valid = true;1474 maybe_valid = true;
1479 break :tunnel error.ServerError;1475 break :tunnel error.ServerError;
1480 }1476 }
14811477
1482 if (req.response.status != .ok) break :tunnel error.ConnectionRefused;1478 if (response.head.status != .ok) break :tunnel error.ConnectionRefused;
14831479
1484 // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized.1480 // this connection is now a tunnel, so we can't use it for anything
1481 // else, it will only be released when the client is de-initialized.
1485 req.connection = null;1482 req.connection = null;
14861483
1487 client.allocator.free(conn.host);1484 connection.closing = false;
1488 conn.host = try client.allocator.dupe(u8, tunnel_host);
1489 errdefer client.allocator.free(conn.host);
14901485
1491 conn.port = tunnel_port;1486 return connection;
1492 conn.closing = false;
1493
1494 return conn;
1495 }) catch {1487 }) catch {
1496 // something went wrong with the tunnel1488 // something went wrong with the tunnel
1497 proxy.supports_connect = maybe_valid;1489 proxy.supports_connect = maybe_valid;
...@@ -1499,12 +1491,11 @@ pub fn connectTunnel(...@@ -1499,12 +1491,11 @@ pub fn connectTunnel(
1499 };1491 };
1500}1492}
15011493
1502// Prevents a dependency loop in open()1494pub const ConnectError = ConnectTcpError || RequestError;
1503const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUriScheme, ConnectionRefused };
1504pub const ConnectError = ConnectErrorPartial || RequestError;
15051495
1506/// Connect to `host:port` using the specified protocol. This will reuse a1496/// Connect to `host:port` using the specified protocol. This will reuse a
1507/// connection if one is already open.1497/// connection if one is already open.
1498///
1508/// If a proxy is configured for the client, then the proxy will be used to1499/// If a proxy is configured for the client, then the proxy will be used to
1509/// connect to the host.1500/// connect to the host.
1510///1501///
...@@ -1513,7 +1504,7 @@ pub fn connect(...@@ -1513,7 +1504,7 @@ pub fn connect(
1513 client: *Client,1504 client: *Client,
1514 host: []const u8,1505 host: []const u8,
1515 port: u16,1506 port: u16,
1516 protocol: Connection.Protocol,1507 protocol: Protocol,
1517) ConnectError!*Connection {1508) ConnectError!*Connection {
1518 const proxy = switch (protocol) {1509 const proxy = switch (protocol) {
1519 .plain => client.http_proxy,1510 .plain => client.http_proxy,
...@@ -1528,32 +1519,24 @@ pub fn connect(...@@ -1528,32 +1519,24 @@ pub fn connect(
1528 }1519 }
15291520
1530 if (proxy.supports_connect) tunnel: {1521 if (proxy.supports_connect) tunnel: {
1531 return connectTunnel(client, proxy, host, port) catch |err| switch (err) {1522 return connectProxied(client, proxy, host, port) catch |err| switch (err) {
1532 error.TunnelNotSupported => break :tunnel,1523 error.TunnelNotSupported => break :tunnel,
1533 else => |e| return e,1524 else => |e| return e,
1534 };1525 };
1535 }1526 }
15361527
1537 // fall back to using the proxy as a normal http proxy1528 // fall back to using the proxy as a normal http proxy
1538 const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);1529 const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol);
1539 errdefer {1530 connection.proxied = true;
1540 conn.closing = true;1531 return connection;
1541 client.connection_pool.release(conn);
1542 }
1543
1544 conn.proxied = true;
1545 return conn;
1546}1532}
15471533
1548pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||1534pub const RequestError = ConnectTcpError || error{
1549 std.fmt.ParseIntError || Connection.WriteError ||1535 UnsupportedUriScheme,
1550 error{1536 UriMissingHost,
1551 UnsupportedUriScheme,1537 UriHostTooLong,
1552 UriMissingHost,1538 CertificateBundleLoadFailure,
15531539};
1554 CertificateBundleLoadFailure,
1555 UnsupportedTransferEncoding,
1556 };
15571540
1558pub const RequestOptions = struct {1541pub const RequestOptions = struct {
1559 version: http.Version = .@"HTTP/1.1",1542 version: http.Version = .@"HTTP/1.1",
...@@ -1578,11 +1561,6 @@ pub const RequestOptions = struct {...@@ -1578,11 +1561,6 @@ pub const RequestOptions = struct {
1578 /// payload or the server has acknowledged the payload).1561 /// payload or the server has acknowledged the payload).
1579 redirect_behavior: Request.RedirectBehavior = @enumFromInt(3),1562 redirect_behavior: Request.RedirectBehavior = @enumFromInt(3),
15801563
1581 /// Externally-owned memory used to store the server's entire HTTP header.
1582 /// `error.HttpHeadersOversize` is returned from read() when a
1583 /// client sends too many bytes of HTTP headers.
1584 server_header_buffer: []u8,
1585
1586 /// Must be an already acquired connection.1564 /// Must be an already acquired connection.
1587 connection: ?*Connection = null,1565 connection: ?*Connection = null,
15881566
...@@ -1598,38 +1576,17 @@ pub const RequestOptions = struct {...@@ -1598,38 +1576,17 @@ pub const RequestOptions = struct {
1598 privileged_headers: []const http.Header = &.{},1576 privileged_headers: []const http.Header = &.{},
1599};1577};
16001578
1601fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } {1579fn uriPort(uri: Uri, protocol: Protocol) u16 {
1602 const protocol_map = std.StaticStringMap(Connection.Protocol).initComptime(.{1580 return uri.port orelse protocol.port();
1603 .{ "http", .plain },
1604 .{ "ws", .plain },
1605 .{ "https", .tls },
1606 .{ "wss", .tls },
1607 });
1608 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme;
1609 var valid_uri = uri;
1610 // The host is always going to be needed as a raw string for hostname resolution anyway.
1611 valid_uri.host = .{
1612 .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena),
1613 };
1614 return .{ protocol, valid_uri };
1615}
1616
1617fn uriPort(uri: Uri, protocol: Connection.Protocol) u16 {
1618 return uri.port orelse switch (protocol) {
1619 .plain => 80,
1620 .tls => 443,
1621 };
1622}1581}
16231582
1624/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.1583/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
1625///1584///
1626/// `uri` must remain alive during the entire request.
1627///
1628/// The caller is responsible for calling `deinit()` on the `Request`.1585/// The caller is responsible for calling `deinit()` on the `Request`.
1629/// This function is threadsafe.1586/// This function is threadsafe.
1630///1587///
1631/// Asserts that "\r\n" does not occur in any header name or value.1588/// Asserts that "\r\n" does not occur in any header name or value.
1632pub fn open(1589pub fn request(
1633 client: *Client,1590 client: *Client,
1634 method: http.Method,1591 method: http.Method,
1635 uri: Uri,1592 uri: Uri,
...@@ -1649,59 +1606,58 @@ pub fn open(...@@ -1649,59 +1606,58 @@ pub fn open(
1649 }1606 }
1650 }1607 }
16511608
1652 var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer);1609 const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme;
1653 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
16541610
1655 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {1611 if (protocol == .tls) {
1656 if (disable_tls) unreachable;1612 if (disable_tls) unreachable;
16571613 if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {
1658 client.ca_bundle_mutex.lock();1614 client.ca_bundle_mutex.lock();
1659 defer client.ca_bundle_mutex.unlock();1615 defer client.ca_bundle_mutex.unlock();
16601616
1661 if (client.next_https_rescan_certs) {1617 if (client.next_https_rescan_certs) {
1662 client.ca_bundle.rescan(client.allocator) catch1618 client.ca_bundle.rescan(client.allocator) catch
1663 return error.CertificateBundleLoadFailure;1619 return error.CertificateBundleLoadFailure;
1664 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);1620 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);
1621 }
1665 }1622 }
1666 }1623 }
16671624
1668 const conn = options.connection orelse1625 const connection = options.connection orelse c: {
1669 try client.connect(valid_uri.host.?.raw, uriPort(valid_uri, protocol), protocol);1626 var host_name_buffer: [Uri.host_name_max]u8 = undefined;
1627 const host_name = try uri.getHost(&host_name_buffer);
1628 break :c try client.connect(host_name, uriPort(uri, protocol), protocol);
1629 };
16701630
1671 var req: Request = .{1631 return .{
1672 .uri = valid_uri,1632 .uri = uri,
1673 .client = client,1633 .client = client,
1674 .connection = conn,1634 .connection = connection,
1635 .reader = .{
1636 .in = connection.reader(),
1637 .state = .ready,
1638 // Populated when `http.Reader.bodyReader` is called.
1639 .interface = undefined,
1640 },
1675 .keep_alive = options.keep_alive,1641 .keep_alive = options.keep_alive,
1676 .method = method,1642 .method = method,
1677 .version = options.version,1643 .version = options.version,
1678 .transfer_encoding = .none,1644 .transfer_encoding = .none,
1679 .redirect_behavior = options.redirect_behavior,1645 .redirect_behavior = options.redirect_behavior,
1680 .handle_continue = options.handle_continue,1646 .handle_continue = options.handle_continue,
1681 .response = .{
1682 .version = undefined,
1683 .status = undefined,
1684 .reason = undefined,
1685 .keep_alive = undefined,
1686 .parser = .init(server_header.buffer[server_header.end_index..]),
1687 },
1688 .headers = options.headers,1647 .headers = options.headers,
1689 .extra_headers = options.extra_headers,1648 .extra_headers = options.extra_headers,
1690 .privileged_headers = options.privileged_headers,1649 .privileged_headers = options.privileged_headers,
1691 };1650 };
1692 errdefer req.deinit();
1693
1694 return req;
1695}1651}
16961652
1697pub const FetchOptions = struct {1653pub const FetchOptions = struct {
1698 server_header_buffer: ?[]u8 = null,1654 /// `null` means it will be heap-allocated.
1655 redirect_buffer: ?[]u8 = null,
1656 /// `null` means it will be heap-allocated.
1657 decompress_buffer: ?[]u8 = null,
1699 redirect_behavior: ?Request.RedirectBehavior = null,1658 redirect_behavior: ?Request.RedirectBehavior = null,
17001659 /// If the server sends a body, it will be stored here.
1701 /// If the server sends a body, it will be appended to this ArrayList.1660 response_storage: ?ResponseStorage = null,
1702 /// `max_append_size` provides an upper limit for how much they can grow.
1703 response_storage: ResponseStorage = .ignore,
1704 max_append_size: ?usize = null,
17051661
1706 location: Location,1662 location: Location,
1707 method: ?http.Method = null,1663 method: ?http.Method = null,
...@@ -1725,11 +1681,11 @@ pub const FetchOptions = struct {...@@ -1725,11 +1681,11 @@ pub const FetchOptions = struct {
1725 uri: Uri,1681 uri: Uri,
1726 };1682 };
17271683
1728 pub const ResponseStorage = union(enum) {1684 pub const ResponseStorage = struct {
1729 ignore,1685 list: *std.ArrayListUnmanaged(u8),
1730 /// Only the existing capacity will be used.1686 /// If null then only the existing capacity will be used.
1731 static: *std.ArrayListUnmanaged(u8),1687 allocator: ?Allocator = null,
1732 dynamic: *std.ArrayList(u8),1688 append_limit: std.io.Limit = .unlimited,
1733 };1689 };
1734};1690};
17351691
...@@ -1737,23 +1693,28 @@ pub const FetchResult = struct {...@@ -1737,23 +1693,28 @@ pub const FetchResult = struct {
1737 status: http.Status,1693 status: http.Status,
1738};1694};
17391695
1696pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{
1697 StreamTooLong,
1698 /// TODO provide optional diagnostics when this occurs or break into more error codes
1699 WriteFailed,
1700};
1701
1740/// Perform a one-shot HTTP request with the provided options.1702/// Perform a one-shot HTTP request with the provided options.
1741///1703///
1742/// This function is threadsafe.1704/// This function is threadsafe.
1743pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {1705pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult {
1744 const uri = switch (options.location) {1706 const uri = switch (options.location) {
1745 .url => |u| try Uri.parse(u),1707 .url => |u| try Uri.parse(u),
1746 .uri => |u| u,1708 .uri => |u| u,
1747 };1709 };
1748 var server_header_buffer: [16 * 1024]u8 = undefined;
1749
1750 const method: http.Method = options.method orelse1710 const method: http.Method = options.method orelse
1751 if (options.payload != null) .POST else .GET;1711 if (options.payload != null) .POST else .GET;
17521712
1753 var req = try open(client, method, uri, .{1713 const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse
1754 .server_header_buffer = options.server_header_buffer orelse &server_header_buffer,1714 if (options.payload == null) @enumFromInt(3) else .unhandled;
1755 .redirect_behavior = options.redirect_behavior orelse1715
1756 if (options.payload == null) @enumFromInt(3) else .unhandled,1716 var req = try request(client, method, uri, .{
1717 .redirect_behavior = redirect_behavior,
1757 .headers = options.headers,1718 .headers = options.headers,
1758 .extra_headers = options.extra_headers,1719 .extra_headers = options.extra_headers,
1759 .privileged_headers = options.privileged_headers,1720 .privileged_headers = options.privileged_headers,
...@@ -1761,44 +1722,69 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {...@@ -1761,44 +1722,69 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {
1761 });1722 });
1762 defer req.deinit();1723 defer req.deinit();
17631724
1764 if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len };1725 if (options.payload) |payload| {
1726 req.transfer_encoding = .{ .content_length = payload.len };
1727 var body = try req.sendBody(&.{});
1728 try body.writer.writeAll(payload);
1729 try body.end();
1730 } else {
1731 try req.sendBodiless();
1732 }
17651733
1766 try req.send();1734 const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse
1735 try client.allocator.alloc(u8, 8 * 1024);
1736 defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer);
17671737
1768 if (options.payload) |payload| try req.writeAll(payload);1738 var response = try req.receiveHead(redirect_buffer);
17691739
1770 try req.finish();1740 const storage = options.response_storage orelse {
1771 try req.wait();1741 const reader = response.reader(&.{});
1742 _ = reader.discardRemaining() catch |err| switch (err) {
1743 error.ReadFailed => return response.bodyErr().?,
1744 };
1745 return .{ .status = response.head.status };
1746 };
17721747
1773 switch (options.response_storage) {1748 const decompress_buffer: []u8 = switch (response.head.content_encoding) {
1774 .ignore => {1749 .identity => &.{},
1775 // Take advantage of request internals to discard the response body1750 .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len),
1776 // and make the connection available for another request.1751 else => options.decompress_buffer orelse try client.allocator.alloc(u8, 8 * 1024),
1777 req.response.skip = true;1752 };
1778 assert(try req.transferRead(&.{}) == 0); // No buffer is necessary when skipping.1753 defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer);
1779 },1754
1780 .dynamic => |list| {1755 var decompressor: http.Decompressor = undefined;
1781 const max_append_size = options.max_append_size orelse 2 * 1024 * 1024;1756 const reader = response.readerDecompressing(&decompressor, decompress_buffer);
1782 try req.reader().readAllArrayList(list, max_append_size);1757 const list = storage.list;
1783 },1758
1784 .static => |list| {1759 if (storage.allocator) |allocator| {
1785 const buf = b: {1760 reader.appendRemaining(allocator, null, list, storage.append_limit) catch |err| switch (err) {
1786 const buf = list.unusedCapacitySlice();1761 error.ReadFailed => return response.bodyErr().?,
1787 if (options.max_append_size) |len| {1762 else => |e| return e,
1788 if (len < buf.len) break :b buf[0..len];1763 };
1789 }1764 } else {
1790 break :b buf;1765 const buf = storage.append_limit.slice(list.unusedCapacitySlice());
1791 };1766 list.items.len += reader.readSliceShort(buf) catch |err| switch (err) {
1792 list.items.len += try req.reader().readAll(buf);1767 error.ReadFailed => return response.bodyErr().?,
1793 },1768 };
1794 }1769 }
17951770
1796 return .{1771 return .{ .status = response.head.status };
1797 .status = req.response.status,1772}
1798 };1773
1774pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool {
1775 if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false;
1776 if (child_host.len == parent_host.len) return true;
1777 if (parent_host.len > child_host.len) return false;
1778 return child_host[child_host.len - parent_host.len - 1] == '.';
1779}
1780
1781test sameParentDomain {
1782 try testing.expect(!sameParentDomain("foo.com", "bar.com"));
1783 try testing.expect(sameParentDomain("foo.com", "foo.com"));
1784 try testing.expect(sameParentDomain("foo.com", "bar.foo.com"));
1785 try testing.expect(!sameParentDomain("bar.foo.com", "foo.com"));
1799}1786}
18001787
1801test {1788test {
1802 _ = Response;1789 _ = Response;
1803 _ = &initDefaultProxies;
1804}1790}
lib/std/http/Server.zig+385-747
...@@ -1,139 +1,70 @@...@@ -1,139 +1,70 @@
1//! Blocking HTTP server implementation.1//! Handles a single connection lifecycle.
2//! Handles a single connection's lifecycle.2
33const std = @import("../std.zig");
4connection: net.Server.Connection,4const http = std.http;
5/// Keeps track of whether the Server is ready to accept a new request on the5const mem = std.mem;
6/// same connection, and makes invalid API usage cause assertion failures6const Uri = std.Uri;
7/// rather than HTTP protocol violations.7const assert = std.debug.assert;
8state: State,8const testing = std.testing;
9/// User-provided buffer that must outlive this Server.9const Writer = std.io.Writer;
10/// Used to store the client's entire HTTP header.10
11read_buffer: []u8,11const Server = @This();
12/// Amount of available data inside read_buffer.12
13read_buffer_len: usize,13/// Data from the HTTP server to the HTTP client.
14/// Index into `read_buffer` of the first byte of the next HTTP request.14out: *Writer,
15next_request_start: usize,15reader: http.Reader,
16
17pub const State = enum {
18 /// The connection is available to be used for the first time, or reused.
19 ready,
20 /// An error occurred in `receiveHead`.
21 receiving_head,
22 /// A Request object has been obtained and from there a Response can be
23 /// opened.
24 received_head,
25 /// The client is uploading something to this Server.
26 receiving_body,
27 /// The connection is eligible for another HTTP request, however the client
28 /// and server did not negotiate a persistent connection.
29 closing,
30};
3116
32/// Initialize an HTTP server that can respond to multiple requests on the same17/// Initialize an HTTP server that can respond to multiple requests on the same
33/// connection.18/// connection.
19///
20/// The buffer of `in` must be large enough to store the client's entire HTTP
21/// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`.
22///
34/// The returned `Server` is ready for `receiveHead` to be called.23/// The returned `Server` is ready for `receiveHead` to be called.
35pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server {24pub fn init(in: *std.io.Reader, out: *Writer) Server {
36 return .{25 return .{
37 .connection = connection,26 .reader = .{
38 .state = .ready,27 .in = in,
39 .read_buffer = read_buffer,28 .state = .ready,
40 .read_buffer_len = 0,29 // Populated when `http.Reader.bodyReader` is called.
41 .next_request_start = 0,30 .interface = undefined,
31 },
32 .out = out,
42 };33 };
43}34}
4435
45pub const ReceiveHeadError = error{36pub fn deinit(s: *Server) void {
46 /// Client sent too many bytes of HTTP headers.37 s.reader.restituteHeadBuffer();
47 /// The HTTP specification suggests to respond with a 431 status code38}
48 /// before closing the connection.39
49 HttpHeadersOversize,40pub const ReceiveHeadError = http.Reader.HeadError || error{
50 /// Client sent headers that did not conform to the HTTP protocol.41 /// Client sent headers that did not conform to the HTTP protocol.
42 ///
43 /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be
44 /// passed directly to `Request.Head.parse`.
51 HttpHeadersInvalid,45 HttpHeadersInvalid,
52 /// A low level I/O error occurred trying to read the headers.
53 HttpHeadersUnreadable,
54 /// Partial HTTP request was received but the connection was closed before
55 /// fully receiving the headers.
56 HttpRequestTruncated,
57 /// The client sent 0 bytes of headers before closing the stream.
58 /// In other words, a keep-alive connection was finally closed.
59 HttpConnectionClosing,
60};46};
6147
62/// The header bytes reference the read buffer that Server was initialized with
63/// and remain alive until the next call to receiveHead.
64pub fn receiveHead(s: *Server) ReceiveHeadError!Request {48pub fn receiveHead(s: *Server) ReceiveHeadError!Request {
65 assert(s.state == .ready);49 try s.reader.receiveHead();
66 s.state = .received_head;
67 errdefer s.state = .receiving_head;
68
69 // In case of a reused connection, move the next request's bytes to the
70 // beginning of the buffer.
71 if (s.next_request_start > 0) {
72 if (s.read_buffer_len > s.next_request_start) {
73 rebase(s, 0);
74 } else {
75 s.read_buffer_len = 0;
76 }
77 }
78
79 var hp: http.HeadParser = .{};
80
81 if (s.read_buffer_len > 0) {
82 const bytes = s.read_buffer[0..s.read_buffer_len];
83 const end = hp.feed(bytes);
84 if (hp.state == .finished)
85 return finishReceivingHead(s, end);
86 }
87
88 while (true) {
89 const buf = s.read_buffer[s.read_buffer_len..];
90 if (buf.len == 0)
91 return error.HttpHeadersOversize;
92 const read_n = s.connection.stream.read(buf) catch
93 return error.HttpHeadersUnreadable;
94 if (read_n == 0) {
95 if (s.read_buffer_len > 0) {
96 return error.HttpRequestTruncated;
97 } else {
98 return error.HttpConnectionClosing;
99 }
100 }
101 s.read_buffer_len += read_n;
102 const bytes = buf[0..read_n];
103 const end = hp.feed(bytes);
104 if (hp.state == .finished)
105 return finishReceivingHead(s, s.read_buffer_len - bytes.len + end);
106 }
107}
108
109fn finishReceivingHead(s: *Server, head_end: usize) ReceiveHeadError!Request {
110 return .{50 return .{
111 .server = s,51 .server = s,
112 .head_end = head_end,52 // No need to track the returned error here since users can repeat the
113 .head = Request.Head.parse(s.read_buffer[0..head_end]) catch53 // parse with the header buffer to get detailed diagnostics.
114 return error.HttpHeadersInvalid,54 .head = Request.Head.parse(s.reader.head_buffer) catch return error.HttpHeadersInvalid,
115 .reader_state = undefined,
116 };55 };
117}56}
11857
119pub const Request = struct {58pub const Request = struct {
120 server: *Server,59 server: *Server,
121 /// Index into Server's read_buffer.60 /// Pointers in this struct are invalidated with the next call to
122 head_end: usize,61 /// `receiveHead`.
123 head: Head,62 head: Head,
124 reader_state: union {63 respond_err: ?RespondError = null,
125 remaining_content_length: u64,64
126 chunk_parser: http.ChunkParser,65 pub const RespondError = error{
127 },66 /// The request contained an `expect` header with an unrecognized value.
12867 HttpExpectationFailed,
129 pub const Compression = union(enum) {
130 pub const DeflateDecompressor = std.compress.zlib.Decompressor(std.io.AnyReader);
131 pub const GzipDecompressor = std.compress.gzip.Decompressor(std.io.AnyReader);
132
133 deflate: std.compress.flate.Decompress,
134 gzip: std.compress.flate.Decompress,
135 zstd: std.compress.zstd.Decompress,
136 none: void,
137 };68 };
13869
139 pub const Head = struct {70 pub const Head = struct {
...@@ -146,7 +77,6 @@ pub const Request = struct {...@@ -146,7 +77,6 @@ pub const Request = struct {
146 transfer_encoding: http.TransferEncoding,77 transfer_encoding: http.TransferEncoding,
147 transfer_compression: http.ContentEncoding,78 transfer_compression: http.ContentEncoding,
148 keep_alive: bool,79 keep_alive: bool,
149 compression: Compression,
15080
151 pub const ParseError = error{81 pub const ParseError = error{
152 UnknownHttpMethod,82 UnknownHttpMethod,
...@@ -200,7 +130,6 @@ pub const Request = struct {...@@ -200,7 +130,6 @@ pub const Request = struct {
200 .@"HTTP/1.0" => false,130 .@"HTTP/1.0" => false,
201 .@"HTTP/1.1" => true,131 .@"HTTP/1.1" => true,
202 },132 },
203 .compression = .none,
204 };133 };
205134
206 while (it.next()) |line| {135 while (it.next()) |line| {
...@@ -230,7 +159,7 @@ pub const Request = struct {...@@ -230,7 +159,7 @@ pub const Request = struct {
230159
231 const trimmed = mem.trim(u8, header_value, " ");160 const trimmed = mem.trim(u8, header_value, " ");
232161
233 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {162 if (http.ContentEncoding.fromString(trimmed)) |ce| {
234 head.transfer_compression = ce;163 head.transfer_compression = ce;
235 } else {164 } else {
236 return error.HttpTransferEncodingUnsupported;165 return error.HttpTransferEncodingUnsupported;
...@@ -255,7 +184,7 @@ pub const Request = struct {...@@ -255,7 +184,7 @@ pub const Request = struct {
255 if (next) |second| {184 if (next) |second| {
256 const trimmed_second = mem.trim(u8, second, " ");185 const trimmed_second = mem.trim(u8, second, " ");
257186
258 if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| {187 if (http.ContentEncoding.fromString(trimmed_second)) |transfer| {
259 if (head.transfer_compression != .identity)188 if (head.transfer_compression != .identity)
260 return error.HttpHeadersInvalid; // double compression is not supported189 return error.HttpHeadersInvalid; // double compression is not supported
261 head.transfer_compression = transfer;190 head.transfer_compression = transfer;
...@@ -299,7 +228,8 @@ pub const Request = struct {...@@ -299,7 +228,8 @@ pub const Request = struct {
299 };228 };
300229
301 pub fn iterateHeaders(r: *Request) http.HeaderIterator {230 pub fn iterateHeaders(r: *Request) http.HeaderIterator {
302 return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]);231 assert(r.server.reader.state == .received_head);
232 return http.HeaderIterator.init(r.server.reader.head_buffer);
303 }233 }
304234
305 test iterateHeaders {235 test iterateHeaders {
...@@ -310,22 +240,19 @@ pub const Request = struct {...@@ -310,22 +240,19 @@ pub const Request = struct {
310 "TRansfer-encoding:\tdeflate, chunked \r\n" ++240 "TRansfer-encoding:\tdeflate, chunked \r\n" ++
311 "connectioN:\t keep-alive \r\n\r\n";241 "connectioN:\t keep-alive \r\n\r\n";
312242
313 var read_buffer: [500]u8 = undefined;
314 @memcpy(read_buffer[0..request_bytes.len], request_bytes);
315
316 var server: Server = .{243 var server: Server = .{
317 .connection = undefined,244 .reader = .{
318 .state = .ready,245 .in = undefined,
319 .read_buffer = &read_buffer,246 .state = .received_head,
320 .read_buffer_len = request_bytes.len,247 .head_buffer = @constCast(request_bytes),
321 .next_request_start = 0,248 .interface = undefined,
249 },
250 .out = undefined,
322 };251 };
323252
324 var request: Request = .{253 var request: Request = .{
325 .server = &server,254 .server = &server,
326 .head_end = request_bytes.len,
327 .head = undefined,255 .head = undefined,
328 .reader_state = undefined,
329 };256 };
330257
331 var it = request.iterateHeaders();258 var it = request.iterateHeaders();
...@@ -384,16 +311,22 @@ pub const Request = struct {...@@ -384,16 +311,22 @@ pub const Request = struct {
384 /// no error is surfaced.311 /// no error is surfaced.
385 ///312 ///
386 /// Asserts status is not `continue`.313 /// Asserts status is not `continue`.
387 /// Asserts there are at most 25 extra_headers.
388 /// Asserts that "\r\n" does not occur in any header name or value.314 /// Asserts that "\r\n" does not occur in any header name or value.
389 pub fn respond(315 pub fn respond(
390 request: *Request,316 request: *Request,
391 content: []const u8,317 content: []const u8,
392 options: RespondOptions,318 options: RespondOptions,
393 ) Response.WriteError!void {319 ) ExpectContinueError!void {
394 const max_extra_headers = 25;320 try respondUnflushed(request, content, options);
321 try request.server.out.flush();
322 }
323
324 pub fn respondUnflushed(
325 request: *Request,
326 content: []const u8,
327 options: RespondOptions,
328 ) ExpectContinueError!void {
395 assert(options.status != .@"continue");329 assert(options.status != .@"continue");
396 assert(options.extra_headers.len <= max_extra_headers);
397 if (std.debug.runtime_safety) {330 if (std.debug.runtime_safety) {
398 for (options.extra_headers) |header| {331 for (options.extra_headers) |header| {
399 assert(header.name.len != 0);332 assert(header.name.len != 0);
...@@ -402,6 +335,7 @@ pub const Request = struct {...@@ -402,6 +335,7 @@ pub const Request = struct {
402 assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);335 assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
403 }336 }
404 }337 }
338 try writeExpectContinue(request);
405339
406 const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none;340 const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none;
407 const server_keep_alive = !transfer_encoding_none and options.keep_alive;341 const server_keep_alive = !transfer_encoding_none and options.keep_alive;
...@@ -409,130 +343,42 @@ pub const Request = struct {...@@ -409,130 +343,42 @@ pub const Request = struct {
409343
410 const phrase = options.reason orelse options.status.phrase() orelse "";344 const phrase = options.reason orelse options.status.phrase() orelse "";
411345
412 var first_buffer: [500]u8 = undefined;346 const out = request.server.out;
413 var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer);347 try out.print("{s} {d} {s}\r\n", .{
414 if (request.head.expect != null) {
415 // reader() and hence discardBody() above sets expect to null if it
416 // is handled. So the fact that it is not null here means unhandled.
417 h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
418 if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n");
419 h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
420 try request.server.connection.stream.writeAll(h.items);
421 return;
422 }
423 h.fixedWriter().print("{s} {d} {s}\r\n", .{
424 @tagName(options.version), @intFromEnum(options.status), phrase,348 @tagName(options.version), @intFromEnum(options.status), phrase,
425 }) catch unreachable;349 });
426350
427 switch (options.version) {351 switch (options.version) {
428 .@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"),352 .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"),
429 .@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"),353 .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"),
430 }354 }
431355
432 if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {356 if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
433 .none => {},357 .none => {},
434 .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"),358 .chunked => try out.writeAll("transfer-encoding: chunked\r\n"),
435 } else {359 } else {
436 h.fixedWriter().print("content-length: {d}\r\n", .{content.len}) catch unreachable;360 try out.print("content-length: {d}\r\n", .{content.len});
437 }361 }
438362
439 var chunk_header_buffer: [18]u8 = undefined;
440 var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined;
441 var iovecs_len: usize = 0;
442
443 iovecs[iovecs_len] = .{
444 .base = h.items.ptr,
445 .len = h.items.len,
446 };
447 iovecs_len += 1;
448
449 for (options.extra_headers) |header| {363 for (options.extra_headers) |header| {
450 iovecs[iovecs_len] = .{364 var vecs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" };
451 .base = header.name.ptr,365 try out.writeVecAll(&vecs);
452 .len = header.name.len,
453 };
454 iovecs_len += 1;
455
456 iovecs[iovecs_len] = .{
457 .base = ": ",
458 .len = 2,
459 };
460 iovecs_len += 1;
461
462 if (header.value.len != 0) {
463 iovecs[iovecs_len] = .{
464 .base = header.value.ptr,
465 .len = header.value.len,
466 };
467 iovecs_len += 1;
468 }
469
470 iovecs[iovecs_len] = .{
471 .base = "\r\n",
472 .len = 2,
473 };
474 iovecs_len += 1;
475 }366 }
476367
477 iovecs[iovecs_len] = .{368 try out.writeAll("\r\n");
478 .base = "\r\n",
479 .len = 2,
480 };
481 iovecs_len += 1;
482369
483 if (request.head.method != .HEAD) {370 if (request.head.method != .HEAD) {
484 const is_chunked = (options.transfer_encoding orelse .none) == .chunked;371 const is_chunked = (options.transfer_encoding orelse .none) == .chunked;
485 if (is_chunked) {372 if (is_chunked) {
486 if (content.len > 0) {373 if (content.len > 0) try out.print("{x}\r\n{s}\r\n", .{ content.len, content });
487 const chunk_header = std.fmt.bufPrint(374 try out.writeAll("0\r\n\r\n");
488 &chunk_header_buffer,
489 "{x}\r\n",
490 .{content.len},
491 ) catch unreachable;
492
493 iovecs[iovecs_len] = .{
494 .base = chunk_header.ptr,
495 .len = chunk_header.len,
496 };
497 iovecs_len += 1;
498
499 iovecs[iovecs_len] = .{
500 .base = content.ptr,
501 .len = content.len,
502 };
503 iovecs_len += 1;
504
505 iovecs[iovecs_len] = .{
506 .base = "\r\n",
507 .len = 2,
508 };
509 iovecs_len += 1;
510 }
511
512 iovecs[iovecs_len] = .{
513 .base = "0\r\n\r\n",
514 .len = 5,
515 };
516 iovecs_len += 1;
517 } else if (content.len > 0) {375 } else if (content.len > 0) {
518 iovecs[iovecs_len] = .{376 try out.writeAll(content);
519 .base = content.ptr,
520 .len = content.len,
521 };
522 iovecs_len += 1;
523 }377 }
524 }378 }
525
526 try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]);
527 }379 }
528380
529 pub const RespondStreamingOptions = struct {381 pub const RespondStreamingOptions = struct {
530 /// An externally managed slice of memory used to batch bytes before
531 /// sending. `respondStreaming` asserts this is large enough to store
532 /// the full HTTP response head.
533 ///
534 /// Must outlive the returned Response.
535 send_buffer: []u8,
536 /// If provided, the response will use the content-length header;382 /// If provided, the response will use the content-length header;
537 /// otherwise it will use transfer-encoding: chunked.383 /// otherwise it will use transfer-encoding: chunked.
538 content_length: ?u64 = null,384 content_length: ?u64 = null,
...@@ -540,254 +386,221 @@ pub const Request = struct {...@@ -540,254 +386,221 @@ pub const Request = struct {
540 respond_options: RespondOptions = .{},386 respond_options: RespondOptions = .{},
541 };387 };
542388
543 /// The header is buffered but not sent until Response.flush is called.389 /// The header is not guaranteed to be sent until `BodyWriter.flush` or
390 /// `BodyWriter.end` is called.
544 ///391 ///
545 /// If the request contains a body and the connection is to be reused,392 /// If the request contains a body and the connection is to be reused,
546 /// discards the request body, leaving the Server in the `ready` state. If393 /// discards the request body, leaving the Server in the `ready` state. If
547 /// this discarding fails, the connection is marked as not to be reused and394 /// this discarding fails, the connection is marked as not to be reused and
548 /// no error is surfaced.395 /// no error is surfaced.
549 ///396 ///
550 /// HEAD requests are handled transparently by setting a flag on the397 /// HEAD requests are handled transparently by setting the
551 /// returned Response to omit the body. However it may be worth noticing398 /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing
399 /// the response stream to omit the body. However, it may be worth noticing
552 /// that flag and skipping any expensive work that would otherwise need to400 /// that flag and skipping any expensive work that would otherwise need to
553 /// be done to satisfy the request.401 /// be done to satisfy the request.
554 ///402 ///
555 /// Asserts `send_buffer` is large enough to store the entire response header.
556 /// Asserts status is not `continue`.403 /// Asserts status is not `continue`.
557 pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response {404 pub fn respondStreaming(
405 request: *Request,
406 buffer: []u8,
407 options: RespondStreamingOptions,
408 ) ExpectContinueError!http.BodyWriter {
409 try writeExpectContinue(request);
558 const o = options.respond_options;410 const o = options.respond_options;
559 assert(o.status != .@"continue");411 assert(o.status != .@"continue");
560 const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none;412 const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none;
561 const server_keep_alive = !transfer_encoding_none and o.keep_alive;413 const server_keep_alive = !transfer_encoding_none and o.keep_alive;
562 const keep_alive = request.discardBody(server_keep_alive);414 const keep_alive = request.discardBody(server_keep_alive);
563 const phrase = o.reason orelse o.status.phrase() orelse "";415 const phrase = o.reason orelse o.status.phrase() orelse "";
416 const out = request.server.out;
564417
565 var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer);418 try out.print("{s} {d} {s}\r\n", .{
566419 @tagName(o.version), @intFromEnum(o.status), phrase,
567 const elide_body = if (request.head.expect != null) eb: {420 });
568 // reader() and hence discardBody() above sets expect to null if it
569 // is handled. So the fact that it is not null here means unhandled.
570 h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n");
571 if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n");
572 h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n");
573 break :eb true;
574 } else eb: {
575 h.fixedWriter().print("{s} {d} {s}\r\n", .{
576 @tagName(o.version), @intFromEnum(o.status), phrase,
577 }) catch unreachable;
578
579 switch (o.version) {
580 .@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"),
581 .@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"),
582 }
583421
584 if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {422 switch (o.version) {
585 .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"),423 .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"),
586 .none => {},424 .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"),
587 } else if (options.content_length) |len| {425 }
588 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;
589 } else {
590 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
591 }
592426
593 for (o.extra_headers) |header| {427 if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
594 assert(header.name.len != 0);428 .chunked => try out.writeAll("transfer-encoding: chunked\r\n"),
595 h.appendSliceAssumeCapacity(header.name);429 .none => {},
596 h.appendSliceAssumeCapacity(": ");430 } else if (options.content_length) |len| {
597 h.appendSliceAssumeCapacity(header.value);431 try out.print("content-length: {d}\r\n", .{len});
598 h.appendSliceAssumeCapacity("\r\n");432 } else {
599 }433 try out.writeAll("transfer-encoding: chunked\r\n");
434 }
600435
601 h.appendSliceAssumeCapacity("\r\n");436 for (o.extra_headers) |header| {
602 break :eb request.head.method == .HEAD;437 assert(header.name.len != 0);
603 };438 try out.writeAll(header.name);
439 try out.writeAll(": ");
440 try out.writeAll(header.value);
441 try out.writeAll("\r\n");
442 }
604443
605 return .{444 try out.writeAll("\r\n");
606 .stream = request.server.connection.stream,445 const elide_body = request.head.method == .HEAD;
607 .send_buffer = options.send_buffer,446 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {
608 .send_buffer_start = 0,447 .chunked => .{ .chunked = .init },
609 .send_buffer_end = h.items.len,448 .none => .none,
610 .transfer_encoding = if (o.transfer_encoding) |te| switch (te) {449 } else if (options.content_length) |len| .{
611 .chunked => .chunked,450 .content_length = len,
612 .none => .none,451 } else .{ .chunked = .init };
613 } else if (options.content_length) |len| .{452
614 .content_length = len,453 return if (elide_body) .{
615 } else .chunked,454 .http_protocol_output = request.server.out,
616 .elide_body = elide_body,455 .state = state,
617 .chunk_len = 0,456 .writer = .discarding(buffer),
457 } else .{
458 .http_protocol_output = request.server.out,
459 .state = state,
460 .writer = .{
461 .buffer = buffer,
462 .vtable = switch (state) {
463 .none => &.{
464 .drain = http.BodyWriter.noneDrain,
465 .sendFile = http.BodyWriter.noneSendFile,
466 },
467 .content_length => &.{
468 .drain = http.BodyWriter.contentLengthDrain,
469 .sendFile = http.BodyWriter.contentLengthSendFile,
470 },
471 .chunked => &.{
472 .drain = http.BodyWriter.chunkedDrain,
473 .sendFile = http.BodyWriter.chunkedSendFile,
474 },
475 .end => unreachable,
476 },
477 },
618 };478 };
619 }479 }
620480
621 pub const ReadError = net.Stream.ReadError || error{481 pub const UpgradeRequest = union(enum) {
622 HttpChunkInvalid,482 websocket: ?[]const u8,
623 HttpHeadersOversize,483 other: []const u8,
484 none,
624 };485 };
625486
626 fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize {487 pub fn upgradeRequested(request: *const Request) UpgradeRequest {
627 const request: *Request = @constCast(@alignCast(@ptrCast(context)));488 switch (request.head.version) {
628 const s = request.server;489 .@"HTTP/1.0" => return null,
629490 .@"HTTP/1.1" => if (request.head.method != .GET) return null,
630 const remaining_content_length = &request.reader_state.remaining_content_length;
631 if (remaining_content_length.* == 0) {
632 s.state = .ready;
633 return 0;
634 }491 }
635 assert(s.state == .receiving_body);
636 const available = try fill(s, request.head_end);
637 const len = @min(remaining_content_length.*, available.len, buffer.len);
638 @memcpy(buffer[0..len], available[0..len]);
639 remaining_content_length.* -= len;
640 s.next_request_start += len;
641 if (remaining_content_length.* == 0)
642 s.state = .ready;
643 return len;
644 }
645492
646 fn fill(s: *Server, head_end: usize) ReadError![]u8 {493 var sec_websocket_key: ?[]const u8 = null;
647 const available = s.read_buffer[s.next_request_start..s.read_buffer_len];494 var upgrade_name: ?[]const u8 = null;
648 if (available.len > 0) return available;495 var it = request.iterateHeaders();
649 s.next_request_start = head_end;496 while (it.next()) |header| {
650 s.read_buffer_len = head_end + try s.connection.stream.read(s.read_buffer[head_end..]);497 if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) {
651 return s.read_buffer[head_end..s.read_buffer_len];498 sec_websocket_key = header.value;
652 }499 } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) {
653500 upgrade_name = header.value;
654 fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize {
655 const request: *Request = @constCast(@alignCast(@ptrCast(context)));
656 const s = request.server;
657
658 const cp = &request.reader_state.chunk_parser;
659 const head_end = request.head_end;
660
661 // Protect against returning 0 before the end of stream.
662 var out_end: usize = 0;
663 while (out_end == 0) {
664 switch (cp.state) {
665 .invalid => return 0,
666 .data => {
667 assert(s.state == .receiving_body);
668 const available = try fill(s, head_end);
669 const len = @min(cp.chunk_len, available.len, buffer.len);
670 @memcpy(buffer[0..len], available[0..len]);
671 cp.chunk_len -= len;
672 if (cp.chunk_len == 0)
673 cp.state = .data_suffix;
674 out_end += len;
675 s.next_request_start += len;
676 continue;
677 },
678 else => {
679 assert(s.state == .receiving_body);
680 const available = try fill(s, head_end);
681 const n = cp.feed(available);
682 switch (cp.state) {
683 .invalid => return error.HttpChunkInvalid,
684 .data => {
685 if (cp.chunk_len == 0) {
686 // The next bytes in the stream are trailers,
687 // or \r\n to indicate end of chunked body.
688 //
689 // This function must append the trailers at
690 // head_end so that headers and trailers are
691 // together.
692 //
693 // Since returning 0 would indicate end of
694 // stream, this function must read all the
695 // trailers before returning.
696 if (s.next_request_start > head_end) rebase(s, head_end);
697 var hp: http.HeadParser = .{};
698 {
699 const bytes = s.read_buffer[head_end..s.read_buffer_len];
700 const end = hp.feed(bytes);
701 if (hp.state == .finished) {
702 cp.state = .invalid;
703 s.state = .ready;
704 s.next_request_start = s.read_buffer_len - bytes.len + end;
705 return out_end;
706 }
707 }
708 while (true) {
709 const buf = s.read_buffer[s.read_buffer_len..];
710 if (buf.len == 0)
711 return error.HttpHeadersOversize;
712 const read_n = try s.connection.stream.read(buf);
713 s.read_buffer_len += read_n;
714 const bytes = buf[0..read_n];
715 const end = hp.feed(bytes);
716 if (hp.state == .finished) {
717 cp.state = .invalid;
718 s.state = .ready;
719 s.next_request_start = s.read_buffer_len - bytes.len + end;
720 return out_end;
721 }
722 }
723 }
724 const data = available[n..];
725 const len = @min(cp.chunk_len, data.len, buffer.len);
726 @memcpy(buffer[0..len], data[0..len]);
727 cp.chunk_len -= len;
728 if (cp.chunk_len == 0)
729 cp.state = .data_suffix;
730 out_end += len;
731 s.next_request_start += n + len;
732 continue;
733 },
734 else => continue,
735 }
736 },
737 }501 }
738 }502 }
739 return out_end;503
504 const name = upgrade_name orelse return .none;
505 if (std.ascii.eqlIgnoreCase(name, "websocket")) return .{ .websocket = sec_websocket_key };
506 return .{ .other = name };
740 }507 }
741508
742 pub const ReaderError = Response.WriteError || error{509 pub const WebSocketOptions = struct {
743 /// The client sent an expect HTTP header value other than510 /// The value from `UpgradeRequest.websocket` (sec-websocket-key header value).
744 /// "100-continue".511 key: []const u8,
745 HttpExpectationFailed,512 reason: ?[]const u8 = null,
513 extra_headers: []const http.Header = &.{},
746 };514 };
747515
516 /// The header is not guaranteed to be sent until `WebSocket.flush` is
517 /// called on the returned struct.
518 pub fn respondWebSocket(request: *Request, options: WebSocketOptions) Writer.Error!WebSocket {
519 if (request.head.expect != null) return error.HttpExpectationFailed;
520
521 const out = request.server.out;
522 const version: http.Version = .@"HTTP/1.1";
523 const status: http.Status = .switching_protocols;
524 const phrase = options.reason orelse status.phrase() orelse "";
525
526 assert(request.head.version == version);
527 assert(request.head.method == .GET);
528
529 var sha1 = std.crypto.hash.Sha1.init(.{});
530 sha1.update(options.key);
531 sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
532 var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
533 sha1.final(&digest);
534 try out.print("{s} {d} {s}\r\n", .{ @tagName(version), @intFromEnum(status), phrase });
535 try out.writeAll("connection: upgrade\r\nupgrade: websocket\r\nsec-websocket-accept: ");
536 const base64_digest = try out.writableArray(28);
537 assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len);
538 out.advance(base64_digest.len);
539 try out.writeAll("\r\n");
540
541 for (options.extra_headers) |header| {
542 assert(header.name.len != 0);
543 try out.writeAll(header.name);
544 try out.writeAll(": ");
545 try out.writeAll(header.value);
546 try out.writeAll("\r\n");
547 }
548
549 try out.writeAll("\r\n");
550
551 return .{
552 .input = request.server.reader.in,
553 .output = request.server.out,
554 .key = options.key,
555 };
556 }
557
748 /// In the case that the request contains "expect: 100-continue", this558 /// In the case that the request contains "expect: 100-continue", this
749 /// function writes the continuation header, which means it can fail with a559 /// function writes the continuation header, which means it can fail with a
750 /// write error. After sending the continuation header, it sets the560 /// write error. After sending the continuation header, it sets the
751 /// request's expect field to `null`.561 /// request's expect field to `null`.
752 ///562 ///
753 /// Asserts that this function is only called once.563 /// Asserts that this function is only called once.
754 pub fn reader(request: *Request) ReaderError!std.io.AnyReader {564 ///
755 const s = request.server;565 /// See `readerExpectNone` for an infallible alternative that cannot write
756 assert(s.state == .received_head);566 /// to the server output stream.
757 s.state = .receiving_body;567 pub fn readerExpectContinue(request: *Request, buffer: []u8) ExpectContinueError!*std.io.Reader {
758 s.next_request_start = request.head_end;568 const flush = request.head.expect != null;
759569 try writeExpectContinue(request);
760 if (request.head.expect) |expect| {570 if (flush) try request.server.out.flush();
761 if (mem.eql(u8, expect, "100-continue")) {571 return readerExpectNone(request, buffer);
762 try request.server.connection.stream.writeAll("HTTP/1.1 100 Continue\r\n\r\n");572 }
763 request.head.expect = null;
764 } else {
765 return error.HttpExpectationFailed;
766 }
767 }
768573
769 switch (request.head.transfer_encoding) {574 /// Asserts the expect header is `null`. The caller must handle the
770 .chunked => {575 /// expectation manually and then set the value to `null` prior to calling
771 request.reader_state = .{ .chunk_parser = http.ChunkParser.init };576 /// this function.
772 return .{577 ///
773 .readFn = read_chunked,578 /// Asserts that this function is only called once.
774 .context = request,579 pub fn readerExpectNone(request: *Request, buffer: []u8) *std.io.Reader {
775 };580 assert(request.server.reader.state == .received_head);
776 },581 assert(request.head.expect == null);
777 .none => {582 if (!request.head.method.requestHasBody()) return .ending;
778 request.reader_state = .{583 return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length);
779 .remaining_content_length = request.head.content_length orelse 0,584 }
780 };585
781 return .{586 pub const ExpectContinueError = error{
782 .readFn = read_cl,587 /// Failed to write "HTTP/1.1 100 Continue\r\n\r\n" to the stream.
783 .context = request,588 WriteFailed,
784 };589 /// The client sent an expect HTTP header value other than
785 },590 /// "100-continue".
786 }591 HttpExpectationFailed,
592 };
593
594 pub fn writeExpectContinue(request: *Request) ExpectContinueError!void {
595 const expect = request.head.expect orelse return;
596 if (!mem.eql(u8, expect, "100-continue")) return error.HttpExpectationFailed;
597 try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n");
598 request.head.expect = null;
787 }599 }
788600
789 /// Returns whether the connection should remain persistent.601 /// Returns whether the connection should remain persistent.
790 /// If it would fail, it instead sets the Server state to `receiving_body`602 ///
603 /// If it would fail, it instead sets the Server state to receiving body
791 /// and returns false.604 /// and returns false.
792 fn discardBody(request: *Request, keep_alive: bool) bool {605 fn discardBody(request: *Request, keep_alive: bool) bool {
793 // Prepare to receive another request on the same connection.606 // Prepare to receive another request on the same connection.
...@@ -798,350 +611,175 @@ pub const Request = struct {...@@ -798,350 +611,175 @@ pub const Request = struct {
798 // or the request body.611 // or the request body.
799 // If the connection won't be kept alive, then none of this matters612 // If the connection won't be kept alive, then none of this matters
800 // because the connection will be severed after the response is sent.613 // because the connection will be severed after the response is sent.
801 const s = request.server;614 const r = &request.server.reader;
802 if (keep_alive and request.head.keep_alive) switch (s.state) {615 if (keep_alive and request.head.keep_alive) switch (r.state) {
803 .received_head => {616 .received_head => {
804 const r = request.reader() catch return false;617 if (request.head.method.requestHasBody()) {
805 _ = r.discard() catch return false;618 assert(request.head.transfer_encoding != .none or request.head.content_length != null);
806 assert(s.state == .ready);619 const reader_interface = request.readerExpectContinue(&.{}) catch return false;
620 _ = reader_interface.discardRemaining() catch return false;
621 assert(r.state == .ready);
622 } else {
623 r.state = .ready;
624 }
807 return true;625 return true;
808 },626 },
809 .receiving_body, .ready => return true,627 .body_remaining_content_length, .body_remaining_chunk_len, .body_none, .ready => return true,
810 else => unreachable,628 else => unreachable,
811 };629 };
812630
813 // Avoid clobbering the state in case a reading stream already exists.631 // Avoid clobbering the state in case a reading stream already exists.
814 switch (s.state) {632 switch (r.state) {
815 .received_head => s.state = .closing,633 .received_head => r.state = .closing,
816 else => {},634 else => {},
817 }635 }
818 return false;636 return false;
819 }637 }
820};638};
821639
822pub const Response = struct {640/// See https://tools.ietf.org/html/rfc6455
823 stream: net.Stream,641pub const WebSocket = struct {
824 send_buffer: []u8,642 key: []const u8,
825 /// Index of the first byte in `send_buffer`.643 input: *std.io.Reader,
826 /// This is 0 unless a short write happens in `write`.644 output: *Writer,
827 send_buffer_start: usize,645
828 /// Index of the last byte + 1 in `send_buffer`.646 pub const Header0 = packed struct(u8) {
829 send_buffer_end: usize,647 opcode: Opcode,
830 /// `null` means transfer-encoding: chunked.648 rsv3: u1 = 0,
831 /// As a debugging utility, counts down to zero as bytes are written.649 rsv2: u1 = 0,
832 transfer_encoding: TransferEncoding,650 rsv1: u1 = 0,
833 elide_body: bool,651 fin: bool,
834 /// Indicates how much of the end of the `send_buffer` corresponds to a
835 /// chunk. This amount of data will be wrapped by an HTTP chunk header.
836 chunk_len: usize,
837
838 pub const TransferEncoding = union(enum) {
839 /// End of connection signals the end of the stream.
840 none,
841 /// As a debugging utility, counts down to zero as bytes are written.
842 content_length: u64,
843 /// Each chunk is wrapped in a header and trailer.
844 chunked,
845 };652 };
846653
847 pub const WriteError = net.Stream.WriteError;654 pub const Header1 = packed struct(u8) {
848655 payload_len: enum(u7) {
849 /// When using content-length, asserts that the amount of data sent matches656 len16 = 126,
850 /// the value sent in the header, then calls `flush`.657 len64 = 127,
851 /// Otherwise, transfer-encoding: chunked is being used, and it writes the658 _,
852 /// end-of-stream message, then flushes the stream to the system.659 },
853 /// Respects the value of `elide_body` to omit all data after the headers.660 mask: bool,
854 pub fn end(r: *Response) WriteError!void {
855 switch (r.transfer_encoding) {
856 .content_length => |len| {
857 assert(len == 0); // Trips when end() called before all bytes written.
858 try flush_cl(r);
859 },
860 .none => {
861 try flush_cl(r);
862 },
863 .chunked => {
864 try flush_chunked(r, &.{});
865 },
866 }
867 r.* = undefined;
868 }
869
870 pub const EndChunkedOptions = struct {
871 trailers: []const http.Header = &.{},
872 };661 };
873662
874 /// Asserts that the Response is using transfer-encoding: chunked.663 pub const Opcode = enum(u4) {
875 /// Writes the end-of-stream message and any optional trailers, then664 continuation = 0,
876 /// flushes the stream to the system.665 text = 1,
877 /// Respects the value of `elide_body` to omit all data after the headers.666 binary = 2,
878 /// Asserts there are at most 25 trailers.667 connection_close = 8,
879 pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void {668 ping = 9,
880 assert(r.transfer_encoding == .chunked);669 /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional
881 try flush_chunked(r, options.trailers);670 /// heartbeat. A response to an unsolicited Pong frame is not expected."
882 r.* = undefined;671 pong = 10,
883 }672 _,
884673 };
885 /// If using content-length, asserts that writing these bytes to the client
886 /// would not exceed the content-length value sent in the HTTP header.
887 /// May return 0, which does not indicate end of stream. The caller decides
888 /// when the end of stream occurs by calling `end`.
889 pub fn write(r: *Response, bytes: []const u8) WriteError!usize {
890 switch (r.transfer_encoding) {
891 .content_length, .none => return write_cl(r, bytes),
892 .chunked => return write_chunked(r, bytes),
893 }
894 }
895
896 fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {
897 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
898674
899 var trash: u64 = std.math.maxInt(u64);675 pub const ReadSmallTextMessageError = error{
900 const len = switch (r.transfer_encoding) {676 ConnectionClose,
901 .content_length => |*len| len,677 UnexpectedOpCode,
902 else => &trash,678 MessageTooBig,
903 };679 MissingMaskBit,
680 };
904681
905 if (r.elide_body) {682 pub const SmallMessage = struct {
906 len.* -= bytes.len;683 /// Can be text, binary, or ping.
907 return bytes.len;684 opcode: Opcode,
908 }685 data: []u8,
686 };
909687
910 if (bytes.len + r.send_buffer_end > r.send_buffer.len) {688 /// Reads the next message from the WebSocket stream, failing if the
911 const send_buffer_len = r.send_buffer_end - r.send_buffer_start;689 /// message does not fit into the input buffer. The returned memory points
912 var iovecs: [2]std.posix.iovec_const = .{690 /// into the input buffer and is invalidated on the next read.
913 .{691 pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage {
914 .base = r.send_buffer.ptr + r.send_buffer_start,692 const in = ws.input;
915 .len = send_buffer_len,693 while (true) {
916 },694 const h0 = in.takeStruct(Header0);
917 .{695 const h1 = in.takeStruct(Header1);
918 .base = bytes.ptr,696
919 .len = bytes.len,697 switch (h0.opcode) {
920 },698 .text, .binary, .pong, .ping => {},
921 };699 .connection_close => return error.ConnectionClose,
922 const n = try r.stream.writev(&iovecs);700 .continuation => return error.UnexpectedOpCode,
923701 _ => return error.UnexpectedOpCode,
924 if (n >= send_buffer_len) {
925 // It was enough to reset the buffer.
926 r.send_buffer_start = 0;
927 r.send_buffer_end = 0;
928 const bytes_n = n - send_buffer_len;
929 len.* -= bytes_n;
930 return bytes_n;
931 }702 }
932703
933 // It didn't even make it through the existing buffer, let704 if (!h0.fin) return error.MessageTooBig;
934 // alone the new bytes provided.705 if (!h1.mask) return error.MissingMaskBit;
935 r.send_buffer_start += n;
936 return 0;
937 }
938
939 // All bytes can be stored in the remaining space of the buffer.
940 @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
941 r.send_buffer_end += bytes.len;
942 len.* -= bytes.len;
943 return bytes.len;
944 }
945706
946 fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {707 const len: usize = switch (h1.payload_len) {
947 const r: *Response = @constCast(@alignCast(@ptrCast(context)));708 .len16 => try in.takeInt(u16, .big),
948 assert(r.transfer_encoding == .chunked);709 .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageTooBig,
949710 else => @intFromEnum(h1.payload_len),
950 if (r.elide_body)711 };
951 return bytes.len;712 if (len > in.buffer.len) return error.MessageTooBig;
952713 const mask: u32 = @bitCast((try in.takeArray(4)).*);
953 if (bytes.len + r.send_buffer_end > r.send_buffer.len) {714 const payload = try in.take(len);
954 const send_buffer_len = r.send_buffer_end - r.send_buffer_start;715
955 const chunk_len = r.chunk_len + bytes.len;716 // Skip pongs.
956 var header_buf: [18]u8 = undefined;717 if (h0.opcode == .pong) continue;
957 const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{chunk_len}) catch unreachable;718
958719 // The last item may contain a partial word of unused data.
959 var iovecs: [5]std.posix.iovec_const = .{720 const floored_len = (payload.len / 4) * 4;
960 .{721 const u32_payload: []align(1) u32 = @ptrCast(payload[0..floored_len]);
961 .base = r.send_buffer.ptr + r.send_buffer_start,722 for (u32_payload) |*elem| elem.* ^= mask;
962 .len = send_buffer_len - r.chunk_len,723 const mask_bytes: []const u8 = @ptrCast(&mask);
963 },724 for (payload[floored_len..], mask_bytes[0 .. payload.len - floored_len]) |*leftover, m|
964 .{725 leftover.* ^= m;
965 .base = chunk_header.ptr,726
966 .len = chunk_header.len,727 return .{
967 },728 .opcode = h0.opcode,
968 .{729 .data = payload,
969 .base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,
970 .len = r.chunk_len,
971 },
972 .{
973 .base = bytes.ptr,
974 .len = bytes.len,
975 },
976 .{
977 .base = "\r\n",
978 .len = 2,
979 },
980 };730 };
981 // TODO make this writev instead of writevAll, which involves
982 // complicating the logic of this function.
983 try r.stream.writevAll(&iovecs);
984 r.send_buffer_start = 0;
985 r.send_buffer_end = 0;
986 r.chunk_len = 0;
987 return bytes.len;
988 }731 }
989
990 // All bytes can be stored in the remaining space of the buffer.
991 @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes);
992 r.send_buffer_end += bytes.len;
993 r.chunk_len += bytes.len;
994 return bytes.len;
995 }732 }
996733
997 /// If using content-length, asserts that writing these bytes to the client734 pub fn writeMessage(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void {
998 /// would not exceed the content-length value sent in the HTTP header.735 try writeMessageVecUnflushed(ws, &.{data}, op);
999 pub fn writeAll(r: *Response, bytes: []const u8) WriteError!void {736 try ws.output.flush();
1000 var index: usize = 0;
1001 while (index < bytes.len) {
1002 index += try write(r, bytes[index..]);
1003 }
1004 }737 }
1005738
1006 /// Sends all buffered data to the client.739 pub fn writeMessageUnflushed(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void {
1007 /// This is redundant after calling `end`.740 try writeMessageVecUnflushed(ws, &.{data}, op);
1008 /// Respects the value of `elide_body` to omit all data after the headers.
1009 pub fn flush(r: *Response) WriteError!void {
1010 switch (r.transfer_encoding) {
1011 .none, .content_length => return flush_cl(r),
1012 .chunked => return flush_chunked(r, null),
1013 }
1014 }741 }
1015742
1016 fn flush_cl(r: *Response) WriteError!void {743 pub fn writeMessageVec(ws: *WebSocket, data: []const []const u8, op: Opcode) Writer.Error!void {
1017 try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);744 try writeMessageVecUnflushed(ws, data, op);
1018 r.send_buffer_start = 0;745 try ws.output.flush();
1019 r.send_buffer_end = 0;
1020 }746 }
1021747
1022 fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {748 pub fn writeMessageVecUnflushed(ws: *WebSocket, data: []const []const u8, op: Opcode) Writer.Error!void {
1023 const max_trailers = 25;749 const total_len = l: {
1024 if (end_trailers) |trailers| assert(trailers.len <= max_trailers);750 var total_len: u64 = 0;
1025 assert(r.transfer_encoding == .chunked);751 for (data) |iovec| total_len += iovec.len;
1026752 break :l total_len;
1027 const http_headers = r.send_buffer[r.send_buffer_start .. r.send_buffer_end - r.chunk_len];
1028
1029 if (r.elide_body) {
1030 try r.stream.writeAll(http_headers);
1031 r.send_buffer_start = 0;
1032 r.send_buffer_end = 0;
1033 r.chunk_len = 0;
1034 return;
1035 }
1036
1037 var header_buf: [18]u8 = undefined;
1038 const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{r.chunk_len}) catch unreachable;
1039
1040 var iovecs: [max_trailers * 4 + 5]std.posix.iovec_const = undefined;
1041 var iovecs_len: usize = 0;
1042
1043 iovecs[iovecs_len] = .{
1044 .base = http_headers.ptr,
1045 .len = http_headers.len,
1046 };753 };
1047 iovecs_len += 1;754 const out = ws.output;
1048755 try out.writeStruct(@as(Header0, .{
1049 if (r.chunk_len > 0) {756 .opcode = op,
1050 iovecs[iovecs_len] = .{757 .fin = true,
1051 .base = chunk_header.ptr,758 }));
1052 .len = chunk_header.len,759 switch (total_len) {
1053 };760 0...125 => try out.writeStruct(@as(Header1, .{
1054 iovecs_len += 1;761 .payload_len = @enumFromInt(total_len),
1055762 .mask = false,
1056 iovecs[iovecs_len] = .{763 })),
1057 .base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len,764 126...0xffff => {
1058 .len = r.chunk_len,765 try out.writeStruct(@as(Header1, .{
1059 };766 .payload_len = .len16,
1060 iovecs_len += 1;767 .mask = false,
1061768 }));
1062 iovecs[iovecs_len] = .{769 try out.writeInt(u16, @intCast(total_len), .big);
1063 .base = "\r\n",770 },
1064 .len = 2,771 else => {
1065 };772 try out.writeStruct(@as(Header1, .{
1066 iovecs_len += 1;773 .payload_len = .len64,
1067 }774 .mask = false,
1068775 }));
1069 if (end_trailers) |trailers| {776 try out.writeInt(u64, total_len, .big);
1070 iovecs[iovecs_len] = .{777 },
1071 .base = "0\r\n",
1072 .len = 3,
1073 };
1074 iovecs_len += 1;
1075
1076 for (trailers) |trailer| {
1077 iovecs[iovecs_len] = .{
1078 .base = trailer.name.ptr,
1079 .len = trailer.name.len,
1080 };
1081 iovecs_len += 1;
1082
1083 iovecs[iovecs_len] = .{
1084 .base = ": ",
1085 .len = 2,
1086 };
1087 iovecs_len += 1;
1088
1089 if (trailer.value.len != 0) {
1090 iovecs[iovecs_len] = .{
1091 .base = trailer.value.ptr,
1092 .len = trailer.value.len,
1093 };
1094 iovecs_len += 1;
1095 }
1096
1097 iovecs[iovecs_len] = .{
1098 .base = "\r\n",
1099 .len = 2,
1100 };
1101 iovecs_len += 1;
1102 }
1103
1104 iovecs[iovecs_len] = .{
1105 .base = "\r\n",
1106 .len = 2,
1107 };
1108 iovecs_len += 1;
1109 }778 }
1110779 try out.writeVecAll(data);
1111 try r.stream.writevAll(iovecs[0..iovecs_len]);
1112 r.send_buffer_start = 0;
1113 r.send_buffer_end = 0;
1114 r.chunk_len = 0;
1115 }780 }
1116781
1117 pub fn writer(r: *Response) std.io.AnyWriter {782 pub fn flush(ws: *WebSocket) Writer.Error!void {
1118 return .{783 try ws.output.flush();
1119 .writeFn = switch (r.transfer_encoding) {
1120 .none, .content_length => write_cl,
1121 .chunked => write_chunked,
1122 },
1123 .context = r,
1124 };
1125 }784 }
1126};785};
1127
1128fn rebase(s: *Server, index: usize) void {
1129 const leftover = s.read_buffer[s.next_request_start..s.read_buffer_len];
1130 const dest = s.read_buffer[index..][0..leftover.len];
1131 if (leftover.len <= s.next_request_start - index) {
1132 @memcpy(dest, leftover);
1133 } else {
1134 mem.copyBackwards(u8, dest, leftover);
1135 }
1136 s.read_buffer_len = index + leftover.len;
1137}
1138
1139const std = @import("../std.zig");
1140const http = std.http;
1141const mem = std.mem;
1142const net = std.net;
1143const Uri = std.Uri;
1144const assert = std.debug.assert;
1145const testing = std.testing;
1146
1147const Server = @This();
lib/std/http/WebSocket.zig deleted-246
...@@ -1,246 +0,0 @@
1//! See https://tools.ietf.org/html/rfc6455
2
3const builtin = @import("builtin");
4const std = @import("std");
5const WebSocket = @This();
6const assert = std.debug.assert;
7const native_endian = builtin.cpu.arch.endian();
8
9key: []const u8,
10request: *std.http.Server.Request,
11recv_fifo: std.fifo.LinearFifo(u8, .Slice),
12reader: std.io.AnyReader,
13response: std.http.Server.Response,
14/// Number of bytes that have been peeked but not discarded yet.
15outstanding_len: usize,
16
17pub const InitError = error{WebSocketUpgradeMissingKey} ||
18 std.http.Server.Request.ReaderError;
19
20pub fn init(
21 request: *std.http.Server.Request,
22 send_buffer: []u8,
23 recv_buffer: []align(4) u8,
24) InitError!?WebSocket {
25 switch (request.head.version) {
26 .@"HTTP/1.0" => return null,
27 .@"HTTP/1.1" => if (request.head.method != .GET) return null,
28 }
29
30 var sec_websocket_key: ?[]const u8 = null;
31 var upgrade_websocket: bool = false;
32 var it = request.iterateHeaders();
33 while (it.next()) |header| {
34 if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) {
35 sec_websocket_key = header.value;
36 } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) {
37 if (!std.ascii.eqlIgnoreCase(header.value, "websocket"))
38 return null;
39 upgrade_websocket = true;
40 }
41 }
42 if (!upgrade_websocket)
43 return null;
44
45 const key = sec_websocket_key orelse return error.WebSocketUpgradeMissingKey;
46
47 var sha1 = std.crypto.hash.Sha1.init(.{});
48 sha1.update(key);
49 sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
50 var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
51 sha1.final(&digest);
52 var base64_digest: [28]u8 = undefined;
53 assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len);
54
55 request.head.content_length = std.math.maxInt(u64);
56
57 return .{
58 .key = key,
59 .recv_fifo = std.fifo.LinearFifo(u8, .Slice).init(recv_buffer),
60 .reader = try request.reader(),
61 .response = request.respondStreaming(.{
62 .send_buffer = send_buffer,
63 .respond_options = .{
64 .status = .switching_protocols,
65 .extra_headers = &.{
66 .{ .name = "upgrade", .value = "websocket" },
67 .{ .name = "connection", .value = "upgrade" },
68 .{ .name = "sec-websocket-accept", .value = &base64_digest },
69 },
70 .transfer_encoding = .none,
71 },
72 }),
73 .request = request,
74 .outstanding_len = 0,
75 };
76}
77
78pub const Header0 = packed struct(u8) {
79 opcode: Opcode,
80 rsv3: u1 = 0,
81 rsv2: u1 = 0,
82 rsv1: u1 = 0,
83 fin: bool,
84};
85
86pub const Header1 = packed struct(u8) {
87 payload_len: enum(u7) {
88 len16 = 126,
89 len64 = 127,
90 _,
91 },
92 mask: bool,
93};
94
95pub const Opcode = enum(u4) {
96 continuation = 0,
97 text = 1,
98 binary = 2,
99 connection_close = 8,
100 ping = 9,
101 /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional
102 /// heartbeat. A response to an unsolicited Pong frame is not expected."
103 pong = 10,
104 _,
105};
106
107pub const ReadSmallTextMessageError = error{
108 ConnectionClose,
109 UnexpectedOpCode,
110 MessageTooBig,
111 MissingMaskBit,
112} || RecvError;
113
114pub const SmallMessage = struct {
115 /// Can be text, binary, or ping.
116 opcode: Opcode,
117 data: []u8,
118};
119
120/// Reads the next message from the WebSocket stream, failing if the message does not fit
121/// into `recv_buffer`.
122pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage {
123 while (true) {
124 const header_bytes = (try recv(ws, 2))[0..2];
125 const h0: Header0 = @bitCast(header_bytes[0]);
126 const h1: Header1 = @bitCast(header_bytes[1]);
127
128 switch (h0.opcode) {
129 .text, .binary, .pong, .ping => {},
130 .connection_close => return error.ConnectionClose,
131 .continuation => return error.UnexpectedOpCode,
132 _ => return error.UnexpectedOpCode,
133 }
134
135 if (!h0.fin) return error.MessageTooBig;
136 if (!h1.mask) return error.MissingMaskBit;
137
138 const len: usize = switch (h1.payload_len) {
139 .len16 => try recvReadInt(ws, u16),
140 .len64 => std.math.cast(usize, try recvReadInt(ws, u64)) orelse return error.MessageTooBig,
141 else => @intFromEnum(h1.payload_len),
142 };
143 if (len > ws.recv_fifo.buf.len) return error.MessageTooBig;
144
145 const mask: u32 = @bitCast((try recv(ws, 4))[0..4].*);
146 const payload = try recv(ws, len);
147
148 // Skip pongs.
149 if (h0.opcode == .pong) continue;
150
151 // The last item may contain a partial word of unused data.
152 const floored_len = (payload.len / 4) * 4;
153 const u32_payload: []align(1) u32 = @alignCast(std.mem.bytesAsSlice(u32, payload[0..floored_len]));
154 for (u32_payload) |*elem| elem.* ^= mask;
155 const mask_bytes = std.mem.asBytes(&mask)[0 .. payload.len - floored_len];
156 for (payload[floored_len..], mask_bytes) |*leftover, m| leftover.* ^= m;
157
158 return .{
159 .opcode = h0.opcode,
160 .data = payload,
161 };
162 }
163}
164
165const RecvError = std.http.Server.Request.ReadError || error{EndOfStream};
166
167fn recv(ws: *WebSocket, len: usize) RecvError![]u8 {
168 ws.recv_fifo.discard(ws.outstanding_len);
169 assert(len <= ws.recv_fifo.buf.len);
170 if (len > ws.recv_fifo.count) {
171 const small_buf = ws.recv_fifo.writableSlice(0);
172 const needed = len - ws.recv_fifo.count;
173 const buf = if (small_buf.len >= needed) small_buf else b: {
174 ws.recv_fifo.realign();
175 break :b ws.recv_fifo.writableSlice(0);
176 };
177 const n = try @as(RecvError!usize, @errorCast(ws.reader.readAtLeast(buf, needed)));
178 if (n < needed) return error.EndOfStream;
179 ws.recv_fifo.update(n);
180 }
181 ws.outstanding_len = len;
182 // TODO: improve the std lib API so this cast isn't necessary.
183 return @constCast(ws.recv_fifo.readableSliceOfLen(len));
184}
185
186fn recvReadInt(ws: *WebSocket, comptime I: type) !I {
187 const unswapped: I = @bitCast((try recv(ws, @sizeOf(I)))[0..@sizeOf(I)].*);
188 return switch (native_endian) {
189 .little => @byteSwap(unswapped),
190 .big => unswapped,
191 };
192}
193
194pub const WriteError = std.http.Server.Response.WriteError;
195
196pub fn writeMessage(ws: *WebSocket, message: []const u8, opcode: Opcode) WriteError!void {
197 const iovecs: [1]std.posix.iovec_const = .{
198 .{ .base = message.ptr, .len = message.len },
199 };
200 return writeMessagev(ws, &iovecs, opcode);
201}
202
203pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opcode: Opcode) WriteError!void {
204 const total_len = l: {
205 var total_len: u64 = 0;
206 for (message) |iovec| total_len += iovec.len;
207 break :l total_len;
208 };
209
210 var header_buf: [2 + 8]u8 = undefined;
211 header_buf[0] = @bitCast(@as(Header0, .{
212 .opcode = opcode,
213 .fin = true,
214 }));
215 const header = switch (total_len) {
216 0...125 => blk: {
217 header_buf[1] = @bitCast(@as(Header1, .{
218 .payload_len = @enumFromInt(total_len),
219 .mask = false,
220 }));
221 break :blk header_buf[0..2];
222 },
223 126...0xffff => blk: {
224 header_buf[1] = @bitCast(@as(Header1, .{
225 .payload_len = .len16,
226 .mask = false,
227 }));
228 std.mem.writeInt(u16, header_buf[2..4], @intCast(total_len), .big);
229 break :blk header_buf[0..4];
230 },
231 else => blk: {
232 header_buf[1] = @bitCast(@as(Header1, .{
233 .payload_len = .len64,
234 .mask = false,
235 }));
236 std.mem.writeInt(u64, header_buf[2..10], total_len, .big);
237 break :blk header_buf[0..10];
238 },
239 };
240
241 const response = &ws.response;
242 try response.writeAll(header);
243 for (message) |iovec|
244 try response.writeAll(iovec.base[0..iovec.len]);
245 try response.flush();
246}
lib/std/http/protocol.zig deleted-464
...@@ -1,464 +0,0 @@
1const std = @import("../std.zig");
2const builtin = @import("builtin");
3const testing = std.testing;
4const mem = std.mem;
5
6const assert = std.debug.assert;
7
8pub const State = enum {
9 invalid,
10
11 // Begin header and trailer parsing states.
12
13 start,
14 seen_n,
15 seen_r,
16 seen_rn,
17 seen_rnr,
18 finished,
19
20 // Begin transfer-encoding: chunked parsing states.
21
22 chunk_head_size,
23 chunk_head_ext,
24 chunk_head_r,
25 chunk_data,
26 chunk_data_suffix,
27 chunk_data_suffix_r,
28
29 /// Returns true if the parser is in a content state (ie. not waiting for more headers).
30 pub fn isContent(self: State) bool {
31 return switch (self) {
32 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false,
33 .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true,
34 };
35 }
36};
37
38pub const HeadersParser = struct {
39 state: State = .start,
40 /// A fixed buffer of len `max_header_bytes`.
41 /// Pointers into this buffer are not stable until after a message is complete.
42 header_bytes_buffer: []u8,
43 header_bytes_len: u32,
44 next_chunk_length: u64,
45 /// `false`: headers. `true`: trailers.
46 done: bool,
47
48 /// Initializes the parser with a provided buffer `buf`.
49 pub fn init(buf: []u8) HeadersParser {
50 return .{
51 .header_bytes_buffer = buf,
52 .header_bytes_len = 0,
53 .done = false,
54 .next_chunk_length = 0,
55 };
56 }
57
58 /// Reinitialize the parser.
59 /// Asserts the parser is in the "done" state.
60 pub fn reset(hp: *HeadersParser) void {
61 assert(hp.done);
62 hp.* = .{
63 .state = .start,
64 .header_bytes_buffer = hp.header_bytes_buffer,
65 .header_bytes_len = 0,
66 .done = false,
67 .next_chunk_length = 0,
68 };
69 }
70
71 pub fn get(hp: HeadersParser) []u8 {
72 return hp.header_bytes_buffer[0..hp.header_bytes_len];
73 }
74
75 pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 {
76 var hp: std.http.HeadParser = .{
77 .state = switch (r.state) {
78 .start => .start,
79 .seen_n => .seen_n,
80 .seen_r => .seen_r,
81 .seen_rn => .seen_rn,
82 .seen_rnr => .seen_rnr,
83 .finished => .finished,
84 else => unreachable,
85 },
86 };
87 const result = hp.feed(bytes);
88 r.state = switch (hp.state) {
89 .start => .start,
90 .seen_n => .seen_n,
91 .seen_r => .seen_r,
92 .seen_rn => .seen_rn,
93 .seen_rnr => .seen_rnr,
94 .finished => .finished,
95 };
96 return @intCast(result);
97 }
98
99 pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 {
100 var cp: std.http.ChunkParser = .{
101 .state = switch (r.state) {
102 .chunk_head_size => .head_size,
103 .chunk_head_ext => .head_ext,
104 .chunk_head_r => .head_r,
105 .chunk_data => .data,
106 .chunk_data_suffix => .data_suffix,
107 .chunk_data_suffix_r => .data_suffix_r,
108 .invalid => .invalid,
109 else => unreachable,
110 },
111 .chunk_len = r.next_chunk_length,
112 };
113 const result = cp.feed(bytes);
114 r.state = switch (cp.state) {
115 .head_size => .chunk_head_size,
116 .head_ext => .chunk_head_ext,
117 .head_r => .chunk_head_r,
118 .data => .chunk_data,
119 .data_suffix => .chunk_data_suffix,
120 .data_suffix_r => .chunk_data_suffix_r,
121 .invalid => .invalid,
122 };
123 r.next_chunk_length = cp.chunk_len;
124 return @intCast(result);
125 }
126
127 /// Returns whether or not the parser has finished parsing a complete
128 /// message. A message is only complete after the entire body has been read
129 /// and any trailing headers have been parsed.
130 pub fn isComplete(r: *HeadersParser) bool {
131 return r.done and r.state == .finished;
132 }
133
134 pub const CheckCompleteHeadError = error{HttpHeadersOversize};
135
136 /// Pushes `in` into the parser. Returns the number of bytes consumed by
137 /// the header. Any header bytes are appended to `header_bytes_buffer`.
138 pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 {
139 if (hp.state.isContent()) return 0;
140
141 const i = hp.findHeadersEnd(in);
142 const data = in[0..i];
143 if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len)
144 return error.HttpHeadersOversize;
145
146 @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data);
147 hp.header_bytes_len += @intCast(data.len);
148
149 return i;
150 }
151
152 pub const ReadError = error{
153 HttpChunkInvalid,
154 };
155
156 /// Reads the body of the message into `buffer`. Returns the number of
157 /// bytes placed in the buffer.
158 ///
159 /// If `skip` is true, the buffer will be unused and the body will be skipped.
160 ///
161 /// See `std.http.Client.Connection for an example of `conn`.
162 pub fn read(r: *HeadersParser, conn: anytype, buffer: []u8, skip: bool) !usize {
163 assert(r.state.isContent());
164 if (r.done) return 0;
165
166 var out_index: usize = 0;
167 while (true) {
168 switch (r.state) {
169 .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable,
170 .finished => {
171 const data_avail = r.next_chunk_length;
172
173 if (skip) {
174 conn.fill() catch |err| switch (err) {
175 error.EndOfStream => {
176 r.done = true;
177 return 0;
178 },
179 else => |e| return e,
180 };
181
182 const nread = @min(conn.peek().len, data_avail);
183 conn.drop(@intCast(nread));
184 r.next_chunk_length -= nread;
185
186 if (r.next_chunk_length == 0 or nread == 0) r.done = true;
187
188 return out_index;
189 } else if (out_index < buffer.len) {
190 const out_avail = buffer.len - out_index;
191
192 const can_read = @as(usize, @intCast(@min(data_avail, out_avail)));
193 const nread = try conn.read(buffer[0..can_read]);
194 r.next_chunk_length -= nread;
195
196 if (r.next_chunk_length == 0 or nread == 0) r.done = true;
197
198 return nread;
199 } else {
200 return out_index;
201 }
202 },
203 .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => {
204 conn.fill() catch |err| switch (err) {
205 error.EndOfStream => {
206 r.done = true;
207 return 0;
208 },
209 else => |e| return e,
210 };
211
212 const i = r.findChunkedLen(conn.peek());
213 conn.drop(@intCast(i));
214
215 switch (r.state) {
216 .invalid => return error.HttpChunkInvalid,
217 .chunk_data => if (r.next_chunk_length == 0) {
218 if (std.mem.eql(u8, conn.peek(), "\r\n")) {
219 r.state = .finished;
220 conn.drop(2);
221 } else {
222 // The trailer section is formatted identically
223 // to the header section.
224 r.state = .seen_rn;
225 }
226 r.done = true;
227
228 return out_index;
229 },
230 else => return out_index,
231 }
232
233 continue;
234 },
235 .chunk_data => {
236 const data_avail = r.next_chunk_length;
237 const out_avail = buffer.len - out_index;
238
239 if (skip) {
240 conn.fill() catch |err| switch (err) {
241 error.EndOfStream => {
242 r.done = true;
243 return 0;
244 },
245 else => |e| return e,
246 };
247
248 const nread = @min(conn.peek().len, data_avail);
249 conn.drop(@intCast(nread));
250 r.next_chunk_length -= nread;
251 } else if (out_avail > 0) {
252 const can_read: usize = @intCast(@min(data_avail, out_avail));
253 const nread = try conn.read(buffer[out_index..][0..can_read]);
254 r.next_chunk_length -= nread;
255 out_index += nread;
256 }
257
258 if (r.next_chunk_length == 0) {
259 r.state = .chunk_data_suffix;
260 continue;
261 }
262
263 return out_index;
264 },
265 }
266 }
267 }
268};
269
270inline fn int16(array: *const [2]u8) u16 {
271 return @as(u16, @bitCast(array.*));
272}
273
274inline fn int24(array: *const [3]u8) u24 {
275 return @as(u24, @bitCast(array.*));
276}
277
278inline fn int32(array: *const [4]u8) u32 {
279 return @as(u32, @bitCast(array.*));
280}
281
282inline fn intShift(comptime T: type, x: anytype) T {
283 switch (@import("builtin").cpu.arch.endian()) {
284 .little => return @as(T, @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T)))),
285 .big => return @as(T, @truncate(x)),
286 }
287}
288
289/// A buffered (and peekable) Connection.
290const MockBufferedConnection = struct {
291 pub const buffer_size = 0x2000;
292
293 conn: std.io.FixedBufferStream([]const u8),
294 buf: [buffer_size]u8 = undefined,
295 start: u16 = 0,
296 end: u16 = 0,
297
298 pub fn fill(conn: *MockBufferedConnection) ReadError!void {
299 if (conn.end != conn.start) return;
300
301 const nread = try conn.conn.read(conn.buf[0..]);
302 if (nread == 0) return error.EndOfStream;
303 conn.start = 0;
304 conn.end = @as(u16, @truncate(nread));
305 }
306
307 pub fn peek(conn: *MockBufferedConnection) []const u8 {
308 return conn.buf[conn.start..conn.end];
309 }
310
311 pub fn drop(conn: *MockBufferedConnection, num: u16) void {
312 conn.start += num;
313 }
314
315 pub fn readAtLeast(conn: *MockBufferedConnection, buffer: []u8, len: usize) ReadError!usize {
316 var out_index: u16 = 0;
317 while (out_index < len) {
318 const available = conn.end - conn.start;
319 const left = buffer.len - out_index;
320
321 if (available > 0) {
322 const can_read = @as(u16, @truncate(@min(available, left)));
323
324 @memcpy(buffer[out_index..][0..can_read], conn.buf[conn.start..][0..can_read]);
325 out_index += can_read;
326 conn.start += can_read;
327
328 continue;
329 }
330
331 if (left > conn.buf.len) {
332 // skip the buffer if the output is large enough
333 return conn.conn.read(buffer[out_index..]);
334 }
335
336 try conn.fill();
337 }
338
339 return out_index;
340 }
341
342 pub fn read(conn: *MockBufferedConnection, buffer: []u8) ReadError!usize {
343 return conn.readAtLeast(buffer, 1);
344 }
345
346 pub const ReadError = std.io.FixedBufferStream([]const u8).ReadError || error{EndOfStream};
347 pub const Reader = std.io.GenericReader(*MockBufferedConnection, ReadError, read);
348
349 pub fn reader(conn: *MockBufferedConnection) Reader {
350 return Reader{ .context = conn };
351 }
352
353 pub fn writeAll(conn: *MockBufferedConnection, buffer: []const u8) WriteError!void {
354 return conn.conn.writeAll(buffer);
355 }
356
357 pub fn write(conn: *MockBufferedConnection, buffer: []const u8) WriteError!usize {
358 return conn.conn.write(buffer);
359 }
360
361 pub const WriteError = std.io.FixedBufferStream([]const u8).WriteError;
362 pub const Writer = std.io.GenericWriter(*MockBufferedConnection, WriteError, write);
363
364 pub fn writer(conn: *MockBufferedConnection) Writer {
365 return Writer{ .context = conn };
366 }
367};
368
369test "HeadersParser.read length" {
370 // mock BufferedConnection for read
371 var headers_buf: [256]u8 = undefined;
372
373 var r = HeadersParser.init(&headers_buf);
374 const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello";
375
376 var conn: MockBufferedConnection = .{
377 .conn = std.io.fixedBufferStream(data),
378 };
379
380 while (true) { // read headers
381 try conn.fill();
382
383 const nchecked = try r.checkCompleteHead(conn.peek());
384 conn.drop(@intCast(nchecked));
385
386 if (r.state.isContent()) break;
387 }
388
389 var buf: [8]u8 = undefined;
390
391 r.next_chunk_length = 5;
392 const len = try r.read(&conn, &buf, false);
393 try std.testing.expectEqual(@as(usize, 5), len);
394 try std.testing.expectEqualStrings("Hello", buf[0..len]);
395
396 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get());
397}
398
399test "HeadersParser.read chunked" {
400 // mock BufferedConnection for read
401
402 var headers_buf: [256]u8 = undefined;
403 var r = HeadersParser.init(&headers_buf);
404 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";
405
406 var conn: MockBufferedConnection = .{
407 .conn = std.io.fixedBufferStream(data),
408 };
409
410 while (true) { // read headers
411 try conn.fill();
412
413 const nchecked = try r.checkCompleteHead(conn.peek());
414 conn.drop(@intCast(nchecked));
415
416 if (r.state.isContent()) break;
417 }
418 var buf: [8]u8 = undefined;
419
420 r.state = .chunk_head_size;
421 const len = try r.read(&conn, &buf, false);
422 try std.testing.expectEqual(@as(usize, 5), len);
423 try std.testing.expectEqualStrings("Hello", buf[0..len]);
424
425 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get());
426}
427
428test "HeadersParser.read chunked trailer" {
429 // mock BufferedConnection for read
430
431 var headers_buf: [256]u8 = undefined;
432 var r = HeadersParser.init(&headers_buf);
433 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";
434
435 var conn: MockBufferedConnection = .{
436 .conn = std.io.fixedBufferStream(data),
437 };
438
439 while (true) { // read headers
440 try conn.fill();
441
442 const nchecked = try r.checkCompleteHead(conn.peek());
443 conn.drop(@intCast(nchecked));
444
445 if (r.state.isContent()) break;
446 }
447 var buf: [8]u8 = undefined;
448
449 r.state = .chunk_head_size;
450 const len = try r.read(&conn, &buf, false);
451 try std.testing.expectEqual(@as(usize, 5), len);
452 try std.testing.expectEqualStrings("Hello", buf[0..len]);
453
454 while (true) { // read headers
455 try conn.fill();
456
457 const nchecked = try r.checkCompleteHead(conn.peek());
458 conn.drop(@intCast(nchecked));
459
460 if (r.state.isContent()) break;
461 }
462
463 try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get());
464}
lib/std/http/test.zig+272-322
...@@ -10,32 +10,33 @@ const expectError = std.testing.expectError;...@@ -10,32 +10,33 @@ const expectError = std.testing.expectError;
1010
11test "trailers" {11test "trailers" {
12 const test_server = try createTestServer(struct {12 const test_server = try createTestServer(struct {
13 fn run(net_server: *std.net.Server) anyerror!void {13 fn run(test_server: *TestServer) anyerror!void {
14 var header_buffer: [1024]u8 = undefined;14 const net_server = &test_server.net_server;
15 var recv_buffer: [1024]u8 = undefined;
16 var send_buffer: [1024]u8 = undefined;
15 var remaining: usize = 1;17 var remaining: usize = 1;
16 while (remaining != 0) : (remaining -= 1) {18 while (remaining != 0) : (remaining -= 1) {
17 const conn = try net_server.accept();19 const connection = try net_server.accept();
18 defer conn.stream.close();20 defer connection.stream.close();
1921
20 var server = http.Server.init(conn, &header_buffer);22 var connection_br = connection.stream.reader(&recv_buffer);
23 var connection_bw = connection.stream.writer(&send_buffer);
24 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
2125
22 try expectEqual(.ready, server.state);26 try expectEqual(.ready, server.reader.state);
23 var request = try server.receiveHead();27 var request = try server.receiveHead();
24 try serve(&request);28 try serve(&request);
25 try expectEqual(.ready, server.state);29 try expectEqual(.ready, server.reader.state);
26 }30 }
27 }31 }
2832
29 fn serve(request: *http.Server.Request) !void {33 fn serve(request: *http.Server.Request) !void {
30 try expectEqualStrings(request.head.target, "/trailer");34 try expectEqualStrings(request.head.target, "/trailer");
3135
32 var send_buffer: [1024]u8 = undefined;36 var response = try request.respondStreaming(&.{}, .{});
33 var response = request.respondStreaming(.{37 try response.writer.writeAll("Hello, ");
34 .send_buffer = &send_buffer,
35 });
36 try response.writeAll("Hello, ");
37 try response.flush();38 try response.flush();
38 try response.writeAll("World!\n");39 try response.writer.writeAll("World!\n");
39 try response.flush();40 try response.flush();
40 try response.endChunked(.{41 try response.endChunked(.{
41 .trailers = &.{42 .trailers = &.{
...@@ -58,34 +59,33 @@ test "trailers" {...@@ -58,34 +59,33 @@ test "trailers" {
58 const uri = try std.Uri.parse(location);59 const uri = try std.Uri.parse(location);
5960
60 {61 {
61 var server_header_buffer: [1024]u8 = undefined;62 var req = try client.request(.GET, uri, .{});
62 var req = try client.open(.GET, uri, .{
63 .server_header_buffer = &server_header_buffer,
64 });
65 defer req.deinit();63 defer req.deinit();
6664
67 try req.send();65 try req.sendBodiless();
68 try req.wait();66 var response = try req.receiveHead(&.{});
6967
70 const body = try req.reader().readAllAlloc(gpa, 8192);68 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
71 defer gpa.free(body);69 defer gpa.free(body);
7270
73 try expectEqualStrings("Hello, World!\n", body);71 try expectEqualStrings("Hello, World!\n", body);
7472
75 var it = req.response.iterateHeaders();
76 {73 {
74 var it = response.head.iterateHeaders();
77 const header = it.next().?;75 const header = it.next().?;
78 try expect(!it.is_trailer);76 try expect(!it.is_trailer);
79 try expectEqualStrings("transfer-encoding", header.name);77 try expectEqualStrings("transfer-encoding", header.name);
80 try expectEqualStrings("chunked", header.value);78 try expectEqualStrings("chunked", header.value);
79 try expectEqual(null, it.next());
81 }80 }
82 {81 {
82 var it = response.iterateTrailers();
83 const header = it.next().?;83 const header = it.next().?;
84 try expect(it.is_trailer);84 try expect(it.is_trailer);
85 try expectEqualStrings("X-Checksum", header.name);85 try expectEqualStrings("X-Checksum", header.name);
86 try expectEqualStrings("aaaa", header.value);86 try expectEqualStrings("aaaa", header.value);
87 try expectEqual(null, it.next());
87 }88 }
88 try expectEqual(null, it.next());
89 }89 }
9090
91 // connection has been kept alive91 // connection has been kept alive
...@@ -94,19 +94,24 @@ test "trailers" {...@@ -94,19 +94,24 @@ test "trailers" {
9494
95test "HTTP server handles a chunked transfer coding request" {95test "HTTP server handles a chunked transfer coding request" {
96 const test_server = try createTestServer(struct {96 const test_server = try createTestServer(struct {
97 fn run(net_server: *std.net.Server) !void {97 fn run(test_server: *TestServer) anyerror!void {
98 var header_buffer: [8192]u8 = undefined;98 const net_server = &test_server.net_server;
99 const conn = try net_server.accept();99 var recv_buffer: [8192]u8 = undefined;
100 defer conn.stream.close();100 var send_buffer: [500]u8 = undefined;
101101 const connection = try net_server.accept();
102 var server = http.Server.init(conn, &header_buffer);102 defer connection.stream.close();
103
104 var connection_br = connection.stream.reader(&recv_buffer);
105 var connection_bw = connection.stream.writer(&send_buffer);
106 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
103 var request = try server.receiveHead();107 var request = try server.receiveHead();
104108
105 try expect(request.head.transfer_encoding == .chunked);109 try expect(request.head.transfer_encoding == .chunked);
106110
107 var buf: [128]u8 = undefined;111 var buf: [128]u8 = undefined;
108 const n = try (try request.reader()).readAll(&buf);112 var br = try request.readerExpectContinue(&.{});
109 try expect(mem.eql(u8, buf[0..n], "ABCD"));113 const n = try br.readSliceShort(&buf);
114 try expectEqualStrings("ABCD", buf[0..n]);
110115
111 try request.respond("message from server!\n", .{116 try request.respond("message from server!\n", .{
112 .extra_headers = &.{117 .extra_headers = &.{
...@@ -154,16 +159,20 @@ test "HTTP server handles a chunked transfer coding request" {...@@ -154,16 +159,20 @@ test "HTTP server handles a chunked transfer coding request" {
154159
155test "echo content server" {160test "echo content server" {
156 const test_server = try createTestServer(struct {161 const test_server = try createTestServer(struct {
157 fn run(net_server: *std.net.Server) anyerror!void {162 fn run(test_server: *TestServer) anyerror!void {
158 var read_buffer: [1024]u8 = undefined;163 const net_server = &test_server.net_server;
164 var recv_buffer: [1024]u8 = undefined;
165 var send_buffer: [100]u8 = undefined;
159166
160 accept: while (true) {167 accept: while (!test_server.shutting_down) {
161 const conn = try net_server.accept();168 const connection = try net_server.accept();
162 defer conn.stream.close();169 defer connection.stream.close();
163170
164 var http_server = http.Server.init(conn, &read_buffer);171 var connection_br = connection.stream.reader(&recv_buffer);
172 var connection_bw = connection.stream.writer(&send_buffer);
173 var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface);
165174
166 while (http_server.state == .ready) {175 while (http_server.reader.state == .ready) {
167 var request = http_server.receiveHead() catch |err| switch (err) {176 var request = http_server.receiveHead() catch |err| switch (err) {
168 error.HttpConnectionClosing => continue :accept,177 error.HttpConnectionClosing => continue :accept,
169 else => |e| return e,178 else => |e| return e,
...@@ -173,7 +182,7 @@ test "echo content server" {...@@ -173,7 +182,7 @@ test "echo content server" {
173 }182 }
174 if (request.head.expect) |expect_header_value| {183 if (request.head.expect) |expect_header_value| {
175 if (mem.eql(u8, expect_header_value, "garbage")) {184 if (mem.eql(u8, expect_header_value, "garbage")) {
176 try expectError(error.HttpExpectationFailed, request.reader());185 try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{}));
177 try request.respond("", .{ .keep_alive = false });186 try request.respond("", .{ .keep_alive = false });
178 continue;187 continue;
179 }188 }
...@@ -195,16 +204,14 @@ test "echo content server" {...@@ -195,16 +204,14 @@ test "echo content server" {
195 // request.head.target,204 // request.head.target,
196 //});205 //});
197206
198 const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192);207 const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .limited(8192));
199 defer std.testing.allocator.free(body);208 defer std.testing.allocator.free(body);
200209
201 try expect(mem.startsWith(u8, request.head.target, "/echo-content"));210 try expect(mem.startsWith(u8, request.head.target, "/echo-content"));
202 try expectEqualStrings("Hello, World!\n", body);211 try expectEqualStrings("Hello, World!\n", body);
203 try expectEqualStrings("text/plain", request.head.content_type.?);212 try expectEqualStrings("text/plain", request.head.content_type.?);
204213
205 var send_buffer: [100]u8 = undefined;214 var response = try request.respondStreaming(&.{}, .{
206 var response = request.respondStreaming(.{
207 .send_buffer = &send_buffer,
208 .content_length = switch (request.head.transfer_encoding) {215 .content_length = switch (request.head.transfer_encoding) {
209 .chunked => null,216 .chunked => null,
210 .none => len: {217 .none => len: {
...@@ -213,9 +220,8 @@ test "echo content server" {...@@ -213,9 +220,8 @@ test "echo content server" {
213 },220 },
214 },221 },
215 });222 });
216
217 try response.flush(); // Test an early flush to send the HTTP headers before the body.223 try response.flush(); // Test an early flush to send the HTTP headers before the body.
218 const w = response.writer();224 const w = &response.writer;
219 try w.writeAll("Hello, ");225 try w.writeAll("Hello, ");
220 try w.writeAll("World!\n");226 try w.writeAll("World!\n");
221 try response.end();227 try response.end();
...@@ -241,35 +247,36 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" {...@@ -241,35 +247,36 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" {
241 // In this case, the response is expected to stream until the connection is247 // In this case, the response is expected to stream until the connection is
242 // closed, indicating the end of the body.248 // closed, indicating the end of the body.
243 const test_server = try createTestServer(struct {249 const test_server = try createTestServer(struct {
244 fn run(net_server: *std.net.Server) anyerror!void {250 fn run(test_server: *TestServer) anyerror!void {
245 var header_buffer: [1000]u8 = undefined;251 const net_server = &test_server.net_server;
252 var recv_buffer: [1000]u8 = undefined;
253 var send_buffer: [500]u8 = undefined;
246 var remaining: usize = 1;254 var remaining: usize = 1;
247 while (remaining != 0) : (remaining -= 1) {255 while (remaining != 0) : (remaining -= 1) {
248 const conn = try net_server.accept();256 const connection = try net_server.accept();
249 defer conn.stream.close();257 defer connection.stream.close();
250258
251 var server = http.Server.init(conn, &header_buffer);259 var connection_br = connection.stream.reader(&recv_buffer);
260 var connection_bw = connection.stream.writer(&send_buffer);
261 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
252262
253 try expectEqual(.ready, server.state);263 try expectEqual(.ready, server.reader.state);
254 var request = try server.receiveHead();264 var request = try server.receiveHead();
255 try expectEqualStrings(request.head.target, "/foo");265 try expectEqualStrings(request.head.target, "/foo");
256 var send_buffer: [500]u8 = undefined;266 var buf: [30]u8 = undefined;
257 var response = request.respondStreaming(.{267 var response = try request.respondStreaming(&buf, .{
258 .send_buffer = &send_buffer,
259 .respond_options = .{268 .respond_options = .{
260 .transfer_encoding = .none,269 .transfer_encoding = .none,
261 },270 },
262 });271 });
263 var total: usize = 0;272 const w = &response.writer;
264 for (0..500) |i| {273 for (0..500) |i| {
265 var buf: [30]u8 = undefined;274 try w.print("{d}, ah ha ha!\n", .{i});
266 const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
267 try response.writeAll(line);
268 total += line.len;
269 }275 }
270 try expectEqual(7390, total);276 try expectEqual(7390, w.count);
277 try w.flush();
271 try response.end();278 try response.end();
272 try expectEqual(.closing, server.state);279 try expectEqual(.closing, server.reader.state);
273 }280 }
274 }281 }
275 });282 });
...@@ -308,15 +315,20 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" {...@@ -308,15 +315,20 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" {
308315
309test "receiving arbitrary http headers from the client" {316test "receiving arbitrary http headers from the client" {
310 const test_server = try createTestServer(struct {317 const test_server = try createTestServer(struct {
311 fn run(net_server: *std.net.Server) anyerror!void {318 fn run(test_server: *TestServer) anyerror!void {
312 var read_buffer: [666]u8 = undefined;319 const net_server = &test_server.net_server;
320 var recv_buffer: [666]u8 = undefined;
321 var send_buffer: [777]u8 = undefined;
313 var remaining: usize = 1;322 var remaining: usize = 1;
314 while (remaining != 0) : (remaining -= 1) {323 while (remaining != 0) : (remaining -= 1) {
315 const conn = try net_server.accept();324 const connection = try net_server.accept();
316 defer conn.stream.close();325 defer connection.stream.close();
326
327 var connection_br = connection.stream.reader(&recv_buffer);
328 var connection_bw = connection.stream.writer(&send_buffer);
329 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
317330
318 var server = http.Server.init(conn, &read_buffer);331 try expectEqual(.ready, server.reader.state);
319 try expectEqual(.ready, server.state);
320 var request = try server.receiveHead();332 var request = try server.receiveHead();
321 try expectEqualStrings("/bar", request.head.target);333 try expectEqualStrings("/bar", request.head.target);
322 var it = request.iterateHeaders();334 var it = request.iterateHeaders();
...@@ -368,19 +380,21 @@ test "general client/server API coverage" {...@@ -368,19 +380,21 @@ test "general client/server API coverage" {
368 return error.SkipZigTest;380 return error.SkipZigTest;
369 }381 }
370382
371 const global = struct {
372 var handle_new_requests = true;
373 };
374 const test_server = try createTestServer(struct {383 const test_server = try createTestServer(struct {
375 fn run(net_server: *std.net.Server) anyerror!void {384 fn run(test_server: *TestServer) anyerror!void {
376 var client_header_buffer: [1024]u8 = undefined;385 const net_server = &test_server.net_server;
377 outer: while (global.handle_new_requests) {386 var recv_buffer: [1024]u8 = undefined;
387 var send_buffer: [100]u8 = undefined;
388
389 outer: while (!test_server.shutting_down) {
378 var connection = try net_server.accept();390 var connection = try net_server.accept();
379 defer connection.stream.close();391 defer connection.stream.close();
380392
381 var http_server = http.Server.init(connection, &client_header_buffer);393 var connection_br = connection.stream.reader(&recv_buffer);
394 var connection_bw = connection.stream.writer(&send_buffer);
395 var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface);
382396
383 while (http_server.state == .ready) {397 while (http_server.reader.state == .ready) {
384 var request = http_server.receiveHead() catch |err| switch (err) {398 var request = http_server.receiveHead() catch |err| switch (err) {
385 error.HttpConnectionClosing => continue :outer,399 error.HttpConnectionClosing => continue :outer,
386 else => |e| return e,400 else => |e| return e,
...@@ -399,14 +413,11 @@ test "general client/server API coverage" {...@@ -399,14 +413,11 @@ test "general client/server API coverage" {
399 });413 });
400414
401 const gpa = std.testing.allocator;415 const gpa = std.testing.allocator;
402 const body = try (try request.reader()).readAllAlloc(gpa, 8192);416 const body = try (try request.readerExpectContinue(&.{})).allocRemaining(gpa, .limited(8192));
403 defer gpa.free(body);417 defer gpa.free(body);
404418
405 var send_buffer: [100]u8 = undefined;
406
407 if (mem.startsWith(u8, request.head.target, "/get")) {419 if (mem.startsWith(u8, request.head.target, "/get")) {
408 var response = request.respondStreaming(.{420 var response = try request.respondStreaming(&.{}, .{
409 .send_buffer = &send_buffer,
410 .content_length = if (mem.indexOf(u8, request.head.target, "?chunked") == null)421 .content_length = if (mem.indexOf(u8, request.head.target, "?chunked") == null)
411 14422 14
412 else423 else
...@@ -417,20 +428,19 @@ test "general client/server API coverage" {...@@ -417,20 +428,19 @@ test "general client/server API coverage" {
417 },428 },
418 },429 },
419 });430 });
420 const w = response.writer();431 const w = &response.writer;
421 try w.writeAll("Hello, ");432 try w.writeAll("Hello, ");
422 try w.writeAll("World!\n");433 try w.writeAll("World!\n");
423 try response.end();434 try response.end();
424 // Writing again would cause an assertion failure.435 // Writing again would cause an assertion failure.
425 } else if (mem.startsWith(u8, request.head.target, "/large")) {436 } else if (mem.startsWith(u8, request.head.target, "/large")) {
426 var response = request.respondStreaming(.{437 var response = try request.respondStreaming(&.{}, .{
427 .send_buffer = &send_buffer,
428 .content_length = 14 * 1024 + 14 * 10,438 .content_length = 14 * 1024 + 14 * 10,
429 });439 });
430440
431 try response.flush(); // Test an early flush to send the HTTP headers before the body.441 try response.flush(); // Test an early flush to send the HTTP headers before the body.
432442
433 const w = response.writer();443 const w = &response.writer;
434444
435 var i: u32 = 0;445 var i: u32 = 0;
436 while (i < 5) : (i += 1) {446 while (i < 5) : (i += 1) {
...@@ -446,8 +456,7 @@ test "general client/server API coverage" {...@@ -446,8 +456,7 @@ test "general client/server API coverage" {
446456
447 try response.end();457 try response.end();
448 } else if (mem.eql(u8, request.head.target, "/redirect/1")) {458 } else if (mem.eql(u8, request.head.target, "/redirect/1")) {
449 var response = request.respondStreaming(.{459 var response = try request.respondStreaming(&.{}, .{
450 .send_buffer = &send_buffer,
451 .respond_options = .{460 .respond_options = .{
452 .status = .found,461 .status = .found,
453 .extra_headers = &.{462 .extra_headers = &.{
...@@ -456,7 +465,7 @@ test "general client/server API coverage" {...@@ -456,7 +465,7 @@ test "general client/server API coverage" {
456 },465 },
457 });466 });
458467
459 const w = response.writer();468 const w = &response.writer;
460 try w.writeAll("Hello, ");469 try w.writeAll("Hello, ");
461 try w.writeAll("Redirected!\n");470 try w.writeAll("Redirected!\n");
462 try response.end();471 try response.end();
...@@ -524,17 +533,13 @@ test "general client/server API coverage" {...@@ -524,17 +533,13 @@ test "general client/server API coverage" {
524 return s.listen_address.in.getPort();533 return s.listen_address.in.getPort();
525 }534 }
526 });535 });
527 defer {536 defer test_server.destroy();
528 global.handle_new_requests = false;
529 test_server.destroy();
530 }
531537
532 const log = std.log.scoped(.client);538 const log = std.log.scoped(.client);
533539
534 const gpa = std.testing.allocator;540 const gpa = std.testing.allocator;
535 var client: http.Client = .{ .allocator = gpa };541 var client: http.Client = .{ .allocator = gpa };
536 errdefer client.deinit();542 defer client.deinit();
537 // defer client.deinit(); handled below
538543
539 const port = test_server.port();544 const port = test_server.port();
540545
...@@ -544,20 +549,18 @@ test "general client/server API coverage" {...@@ -544,20 +549,18 @@ test "general client/server API coverage" {
544 const uri = try std.Uri.parse(location);549 const uri = try std.Uri.parse(location);
545550
546 log.info("{s}", .{location});551 log.info("{s}", .{location});
547 var server_header_buffer: [1024]u8 = undefined;552 var redirect_buffer: [1024]u8 = undefined;
548 var req = try client.open(.GET, uri, .{553 var req = try client.request(.GET, uri, .{});
549 .server_header_buffer = &server_header_buffer,
550 });
551 defer req.deinit();554 defer req.deinit();
552555
553 try req.send();556 try req.sendBodiless();
554 try req.wait();557 var response = try req.receiveHead(&redirect_buffer);
555558
556 const body = try req.reader().readAllAlloc(gpa, 8192);559 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
557 defer gpa.free(body);560 defer gpa.free(body);
558561
559 try expectEqualStrings("Hello, World!\n", body);562 try expectEqualStrings("Hello, World!\n", body);
560 try expectEqualStrings("text/plain", req.response.content_type.?);563 try expectEqualStrings("text/plain", response.head.content_type.?);
561 }564 }
562565
563 // connection has been kept alive566 // connection has been kept alive
...@@ -569,16 +572,14 @@ test "general client/server API coverage" {...@@ -569,16 +572,14 @@ test "general client/server API coverage" {
569 const uri = try std.Uri.parse(location);572 const uri = try std.Uri.parse(location);
570573
571 log.info("{s}", .{location});574 log.info("{s}", .{location});
572 var server_header_buffer: [1024]u8 = undefined;575 var redirect_buffer: [1024]u8 = undefined;
573 var req = try client.open(.GET, uri, .{576 var req = try client.request(.GET, uri, .{});
574 .server_header_buffer = &server_header_buffer,
575 });
576 defer req.deinit();577 defer req.deinit();
577578
578 try req.send();579 try req.sendBodiless();
579 try req.wait();580 var response = try req.receiveHead(&redirect_buffer);
580581
581 const body = try req.reader().readAllAlloc(gpa, 8192 * 1024);582 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192 * 1024));
582 defer gpa.free(body);583 defer gpa.free(body);
583584
584 try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);585 try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len);
...@@ -593,21 +594,19 @@ test "general client/server API coverage" {...@@ -593,21 +594,19 @@ test "general client/server API coverage" {
593 const uri = try std.Uri.parse(location);594 const uri = try std.Uri.parse(location);
594595
595 log.info("{s}", .{location});596 log.info("{s}", .{location});
596 var server_header_buffer: [1024]u8 = undefined;597 var redirect_buffer: [1024]u8 = undefined;
597 var req = try client.open(.HEAD, uri, .{598 var req = try client.request(.HEAD, uri, .{});
598 .server_header_buffer = &server_header_buffer,
599 });
600 defer req.deinit();599 defer req.deinit();
601600
602 try req.send();601 try req.sendBodiless();
603 try req.wait();602 var response = try req.receiveHead(&redirect_buffer);
604603
605 const body = try req.reader().readAllAlloc(gpa, 8192);604 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
606 defer gpa.free(body);605 defer gpa.free(body);
607606
608 try expectEqualStrings("", body);607 try expectEqualStrings("", body);
609 try expectEqualStrings("text/plain", req.response.content_type.?);608 try expectEqualStrings("text/plain", response.head.content_type.?);
610 try expectEqual(14, req.response.content_length.?);609 try expectEqual(14, response.head.content_length.?);
611 }610 }
612611
613 // connection has been kept alive612 // connection has been kept alive
...@@ -619,20 +618,18 @@ test "general client/server API coverage" {...@@ -619,20 +618,18 @@ test "general client/server API coverage" {
619 const uri = try std.Uri.parse(location);618 const uri = try std.Uri.parse(location);
620619
621 log.info("{s}", .{location});620 log.info("{s}", .{location});
622 var server_header_buffer: [1024]u8 = undefined;621 var redirect_buffer: [1024]u8 = undefined;
623 var req = try client.open(.GET, uri, .{622 var req = try client.request(.GET, uri, .{});
624 .server_header_buffer = &server_header_buffer,
625 });
626 defer req.deinit();623 defer req.deinit();
627624
628 try req.send();625 try req.sendBodiless();
629 try req.wait();626 var response = try req.receiveHead(&redirect_buffer);
630627
631 const body = try req.reader().readAllAlloc(gpa, 8192);628 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
632 defer gpa.free(body);629 defer gpa.free(body);
633630
634 try expectEqualStrings("Hello, World!\n", body);631 try expectEqualStrings("Hello, World!\n", body);
635 try expectEqualStrings("text/plain", req.response.content_type.?);632 try expectEqualStrings("text/plain", response.head.content_type.?);
636 }633 }
637634
638 // connection has been kept alive635 // connection has been kept alive
...@@ -644,21 +641,19 @@ test "general client/server API coverage" {...@@ -644,21 +641,19 @@ test "general client/server API coverage" {
644 const uri = try std.Uri.parse(location);641 const uri = try std.Uri.parse(location);
645642
646 log.info("{s}", .{location});643 log.info("{s}", .{location});
647 var server_header_buffer: [1024]u8 = undefined;644 var redirect_buffer: [1024]u8 = undefined;
648 var req = try client.open(.HEAD, uri, .{645 var req = try client.request(.HEAD, uri, .{});
649 .server_header_buffer = &server_header_buffer,
650 });
651 defer req.deinit();646 defer req.deinit();
652647
653 try req.send();648 try req.sendBodiless();
654 try req.wait();649 var response = try req.receiveHead(&redirect_buffer);
655650
656 const body = try req.reader().readAllAlloc(gpa, 8192);651 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
657 defer gpa.free(body);652 defer gpa.free(body);
658653
659 try expectEqualStrings("", body);654 try expectEqualStrings("", body);
660 try expectEqualStrings("text/plain", req.response.content_type.?);655 try expectEqualStrings("text/plain", response.head.content_type.?);
661 try expect(req.response.transfer_encoding == .chunked);656 try expect(response.head.transfer_encoding == .chunked);
662 }657 }
663658
664 // connection has been kept alive659 // connection has been kept alive
...@@ -670,21 +665,20 @@ test "general client/server API coverage" {...@@ -670,21 +665,20 @@ test "general client/server API coverage" {
670 const uri = try std.Uri.parse(location);665 const uri = try std.Uri.parse(location);
671666
672 log.info("{s}", .{location});667 log.info("{s}", .{location});
673 var server_header_buffer: [1024]u8 = undefined;668 var redirect_buffer: [1024]u8 = undefined;
674 var req = try client.open(.GET, uri, .{669 var req = try client.request(.GET, uri, .{
675 .server_header_buffer = &server_header_buffer,
676 .keep_alive = false,670 .keep_alive = false,
677 });671 });
678 defer req.deinit();672 defer req.deinit();
679673
680 try req.send();674 try req.sendBodiless();
681 try req.wait();675 var response = try req.receiveHead(&redirect_buffer);
682676
683 const body = try req.reader().readAllAlloc(gpa, 8192);677 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
684 defer gpa.free(body);678 defer gpa.free(body);
685679
686 try expectEqualStrings("Hello, World!\n", body);680 try expectEqualStrings("Hello, World!\n", body);
687 try expectEqualStrings("text/plain", req.response.content_type.?);681 try expectEqualStrings("text/plain", response.head.content_type.?);
688 }682 }
689683
690 // connection has been closed684 // connection has been closed
...@@ -696,26 +690,25 @@ test "general client/server API coverage" {...@@ -696,26 +690,25 @@ test "general client/server API coverage" {
696 const uri = try std.Uri.parse(location);690 const uri = try std.Uri.parse(location);
697691
698 log.info("{s}", .{location});692 log.info("{s}", .{location});
699 var server_header_buffer: [1024]u8 = undefined;693 var redirect_buffer: [1024]u8 = undefined;
700 var req = try client.open(.GET, uri, .{694 var req = try client.request(.GET, uri, .{
701 .server_header_buffer = &server_header_buffer,
702 .extra_headers = &.{695 .extra_headers = &.{
703 .{ .name = "empty", .value = "" },696 .{ .name = "empty", .value = "" },
704 },697 },
705 });698 });
706 defer req.deinit();699 defer req.deinit();
707700
708 try req.send();701 try req.sendBodiless();
709 try req.wait();702 var response = try req.receiveHead(&redirect_buffer);
710703
711 try std.testing.expectEqual(.ok, req.response.status);704 try std.testing.expectEqual(.ok, response.head.status);
712705
713 const body = try req.reader().readAllAlloc(gpa, 8192);706 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
714 defer gpa.free(body);707 defer gpa.free(body);
715708
716 try expectEqualStrings("", body);709 try expectEqualStrings("", body);
717710
718 var it = req.response.iterateHeaders();711 var it = response.head.iterateHeaders();
719 {712 {
720 const header = it.next().?;713 const header = it.next().?;
721 try expect(!it.is_trailer);714 try expect(!it.is_trailer);
...@@ -740,16 +733,14 @@ test "general client/server API coverage" {...@@ -740,16 +733,14 @@ test "general client/server API coverage" {
740 const uri = try std.Uri.parse(location);733 const uri = try std.Uri.parse(location);
741734
742 log.info("{s}", .{location});735 log.info("{s}", .{location});
743 var server_header_buffer: [1024]u8 = undefined;736 var redirect_buffer: [1024]u8 = undefined;
744 var req = try client.open(.GET, uri, .{737 var req = try client.request(.GET, uri, .{});
745 .server_header_buffer = &server_header_buffer,
746 });
747 defer req.deinit();738 defer req.deinit();
748739
749 try req.send();740 try req.sendBodiless();
750 try req.wait();741 var response = try req.receiveHead(&redirect_buffer);
751742
752 const body = try req.reader().readAllAlloc(gpa, 8192);743 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
753 defer gpa.free(body);744 defer gpa.free(body);
754745
755 try expectEqualStrings("Hello, World!\n", body);746 try expectEqualStrings("Hello, World!\n", body);
...@@ -764,16 +755,14 @@ test "general client/server API coverage" {...@@ -764,16 +755,14 @@ test "general client/server API coverage" {
764 const uri = try std.Uri.parse(location);755 const uri = try std.Uri.parse(location);
765756
766 log.info("{s}", .{location});757 log.info("{s}", .{location});
767 var server_header_buffer: [1024]u8 = undefined;758 var redirect_buffer: [1024]u8 = undefined;
768 var req = try client.open(.GET, uri, .{759 var req = try client.request(.GET, uri, .{});
769 .server_header_buffer = &server_header_buffer,
770 });
771 defer req.deinit();760 defer req.deinit();
772761
773 try req.send();762 try req.sendBodiless();
774 try req.wait();763 var response = try req.receiveHead(&redirect_buffer);
775764
776 const body = try req.reader().readAllAlloc(gpa, 8192);765 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
777 defer gpa.free(body);766 defer gpa.free(body);
778767
779 try expectEqualStrings("Hello, World!\n", body);768 try expectEqualStrings("Hello, World!\n", body);
...@@ -788,16 +777,14 @@ test "general client/server API coverage" {...@@ -788,16 +777,14 @@ test "general client/server API coverage" {
788 const uri = try std.Uri.parse(location);777 const uri = try std.Uri.parse(location);
789778
790 log.info("{s}", .{location});779 log.info("{s}", .{location});
791 var server_header_buffer: [1024]u8 = undefined;780 var redirect_buffer: [1024]u8 = undefined;
792 var req = try client.open(.GET, uri, .{781 var req = try client.request(.GET, uri, .{});
793 .server_header_buffer = &server_header_buffer,
794 });
795 defer req.deinit();782 defer req.deinit();
796783
797 try req.send();784 try req.sendBodiless();
798 try req.wait();785 var response = try req.receiveHead(&redirect_buffer);
799786
800 const body = try req.reader().readAllAlloc(gpa, 8192);787 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
801 defer gpa.free(body);788 defer gpa.free(body);
802789
803 try expectEqualStrings("Hello, World!\n", body);790 try expectEqualStrings("Hello, World!\n", body);
...@@ -812,17 +799,17 @@ test "general client/server API coverage" {...@@ -812,17 +799,17 @@ test "general client/server API coverage" {
812 const uri = try std.Uri.parse(location);799 const uri = try std.Uri.parse(location);
813800
814 log.info("{s}", .{location});801 log.info("{s}", .{location});
815 var server_header_buffer: [1024]u8 = undefined;802 var redirect_buffer: [1024]u8 = undefined;
816 var req = try client.open(.GET, uri, .{803 var req = try client.request(.GET, uri, .{});
817 .server_header_buffer = &server_header_buffer,
818 });
819 defer req.deinit();804 defer req.deinit();
820805
821 try req.send();806 try req.sendBodiless();
822 req.wait() catch |err| switch (err) {807 if (req.receiveHead(&redirect_buffer)) |_| {
808 return error.TestFailed;
809 } else |err| switch (err) {
823 error.TooManyHttpRedirects => {},810 error.TooManyHttpRedirects => {},
824 else => return err,811 else => return err,
825 };812 }
826 }813 }
827814
828 { // redirect to encoded url815 { // redirect to encoded url
...@@ -831,16 +818,14 @@ test "general client/server API coverage" {...@@ -831,16 +818,14 @@ test "general client/server API coverage" {
831 const uri = try std.Uri.parse(location);818 const uri = try std.Uri.parse(location);
832819
833 log.info("{s}", .{location});820 log.info("{s}", .{location});
834 var server_header_buffer: [1024]u8 = undefined;821 var redirect_buffer: [1024]u8 = undefined;
835 var req = try client.open(.GET, uri, .{822 var req = try client.request(.GET, uri, .{});
836 .server_header_buffer = &server_header_buffer,
837 });
838 defer req.deinit();823 defer req.deinit();
839824
840 try req.send();825 try req.sendBodiless();
841 try req.wait();826 var response = try req.receiveHead(&redirect_buffer);
842827
843 const body = try req.reader().readAllAlloc(gpa, 8192);828 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
844 defer gpa.free(body);829 defer gpa.free(body);
845830
846 try expectEqualStrings("Encoded redirect successful!\n", body);831 try expectEqualStrings("Encoded redirect successful!\n", body);
...@@ -855,14 +840,12 @@ test "general client/server API coverage" {...@@ -855,14 +840,12 @@ test "general client/server API coverage" {
855 const uri = try std.Uri.parse(location);840 const uri = try std.Uri.parse(location);
856841
857 log.info("{s}", .{location});842 log.info("{s}", .{location});
858 var server_header_buffer: [1024]u8 = undefined;843 var redirect_buffer: [1024]u8 = undefined;
859 var req = try client.open(.GET, uri, .{844 var req = try client.request(.GET, uri, .{});
860 .server_header_buffer = &server_header_buffer,
861 });
862 defer req.deinit();845 defer req.deinit();
863846
864 try req.send();847 try req.sendBodiless();
865 const result = req.wait();848 const result = req.receiveHead(&redirect_buffer);
866849
867 // a proxy without an upstream is likely to return a 5xx status.850 // a proxy without an upstream is likely to return a 5xx status.
868 if (client.http_proxy == null) {851 if (client.http_proxy == null) {
...@@ -872,77 +855,40 @@ test "general client/server API coverage" {...@@ -872,77 +855,40 @@ test "general client/server API coverage" {
872855
873 // connection has been kept alive856 // connection has been kept alive
874 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);857 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
875
876 { // issue 16282 *** This test leaves the client in an invalid state, it must be last ***
877 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port});
878 defer gpa.free(location);
879 const uri = try std.Uri.parse(location);
880
881 const total_connections = client.connection_pool.free_size + 64;
882 var requests = try gpa.alloc(http.Client.Request, total_connections);
883 defer gpa.free(requests);
884
885 var header_bufs = std.ArrayList([]u8).init(gpa);
886 defer header_bufs.deinit();
887 defer for (header_bufs.items) |item| gpa.free(item);
888
889 for (0..total_connections) |i| {
890 const headers_buf = try gpa.alloc(u8, 1024);
891 try header_bufs.append(headers_buf);
892 var req = try client.open(.GET, uri, .{
893 .server_header_buffer = headers_buf,
894 });
895 req.response.parser.done = true;
896 req.connection.?.closing = false;
897 requests[i] = req;
898 }
899
900 for (0..total_connections) |i| {
901 requests[i].deinit();
902 }
903
904 // free connections should be full now
905 try expect(client.connection_pool.free_len == client.connection_pool.free_size);
906 }
907
908 client.deinit();
909
910 {
911 global.handle_new_requests = false;
912
913 const conn = try std.net.tcpConnectToAddress(test_server.net_server.listen_address);
914 conn.close();
915 }
916}858}
917859
918test "Server streams both reading and writing" {860test "Server streams both reading and writing" {
919 const test_server = try createTestServer(struct {861 const test_server = try createTestServer(struct {
920 fn run(net_server: *std.net.Server) anyerror!void {862 fn run(test_server: *TestServer) anyerror!void {
921 var header_buffer: [1024]u8 = undefined;863 const net_server = &test_server.net_server;
922 const conn = try net_server.accept();864 var recv_buffer: [1024]u8 = undefined;
923 defer conn.stream.close();865 var send_buffer: [777]u8 = undefined;
924866
925 var server = http.Server.init(conn, &header_buffer);867 const connection = try net_server.accept();
926 var request = try server.receiveHead();868 defer connection.stream.close();
927 const reader = try request.reader();
928869
929 var send_buffer: [777]u8 = undefined;870 var connection_br = connection.stream.reader(&recv_buffer);
930 var response = request.respondStreaming(.{871 var connection_bw = connection.stream.writer(&send_buffer);
931 .send_buffer = &send_buffer,872 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
873 var request = try server.receiveHead();
874 var read_buffer: [100]u8 = undefined;
875 var br = try request.readerExpectContinue(&read_buffer);
876 var response = try request.respondStreaming(&.{}, .{
932 .respond_options = .{877 .respond_options = .{
933 .transfer_encoding = .none, // Causes keep_alive=false878 .transfer_encoding = .none, // Causes keep_alive=false
934 },879 },
935 });880 });
936 const writer = response.writer();881 const w = &response.writer;
937882
938 while (true) {883 while (true) {
939 try response.flush();884 try response.flush();
940 var buf: [100]u8 = undefined;885 const buf = br.peekGreedy(1) catch |err| switch (err) {
941 const n = try reader.read(&buf);886 error.EndOfStream => break,
942 if (n == 0) break;887 error.ReadFailed => return error.ReadFailed,
943 const sub_buf = buf[0..n];888 };
944 for (sub_buf) |*b| b.* = std.ascii.toUpper(b.*);889 br.toss(buf.len);
945 try writer.writeAll(sub_buf);890 for (buf) |*b| b.* = std.ascii.toUpper(b.*);
891 try w.writeAll(buf);
946 }892 }
947 try response.end();893 try response.end();
948 }894 }
...@@ -952,27 +898,24 @@ test "Server streams both reading and writing" {...@@ -952,27 +898,24 @@ test "Server streams both reading and writing" {
952 var client: http.Client = .{ .allocator = std.testing.allocator };898 var client: http.Client = .{ .allocator = std.testing.allocator };
953 defer client.deinit();899 defer client.deinit();
954900
955 var server_header_buffer: [555]u8 = undefined;901 var redirect_buffer: [555]u8 = undefined;
956 var req = try client.open(.POST, .{902 var req = try client.request(.POST, .{
957 .scheme = "http",903 .scheme = "http",
958 .host = .{ .raw = "127.0.0.1" },904 .host = .{ .raw = "127.0.0.1" },
959 .port = test_server.port(),905 .port = test_server.port(),
960 .path = .{ .percent_encoded = "/" },906 .path = .{ .percent_encoded = "/" },
961 }, .{907 }, .{});
962 .server_header_buffer = &server_header_buffer,
963 });
964 defer req.deinit();908 defer req.deinit();
965909
966 req.transfer_encoding = .chunked;910 req.transfer_encoding = .chunked;
967 try req.send();911 var body_writer = try req.sendBody(&.{});
968 try req.wait();912 var response = try req.receiveHead(&redirect_buffer);
969913
970 try req.writeAll("one ");914 try body_writer.writer.writeAll("one ");
971 try req.writeAll("fish");915 try body_writer.writer.writeAll("fish");
916 try body_writer.end();
972917
973 try req.finish();918 const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .limited(8192));
974
975 const body = try req.reader().readAllAlloc(std.testing.allocator, 8192);
976 defer std.testing.allocator.free(body);919 defer std.testing.allocator.free(body);
977920
978 try expectEqualStrings("ONE FISH", body);921 try expectEqualStrings("ONE FISH", body);
...@@ -987,9 +930,8 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -987,9 +930,8 @@ fn echoTests(client: *http.Client, port: u16) !void {
987 defer gpa.free(location);930 defer gpa.free(location);
988 const uri = try std.Uri.parse(location);931 const uri = try std.Uri.parse(location);
989932
990 var server_header_buffer: [1024]u8 = undefined;933 var redirect_buffer: [1024]u8 = undefined;
991 var req = try client.open(.POST, uri, .{934 var req = try client.request(.POST, uri, .{
992 .server_header_buffer = &server_header_buffer,
993 .extra_headers = &.{935 .extra_headers = &.{
994 .{ .name = "content-type", .value = "text/plain" },936 .{ .name = "content-type", .value = "text/plain" },
995 },937 },
...@@ -998,14 +940,14 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -998,14 +940,14 @@ fn echoTests(client: *http.Client, port: u16) !void {
998940
999 req.transfer_encoding = .{ .content_length = 14 };941 req.transfer_encoding = .{ .content_length = 14 };
1000942
1001 try req.send();943 var body_writer = try req.sendBody(&.{});
1002 try req.writeAll("Hello, ");944 try body_writer.writer.writeAll("Hello, ");
1003 try req.writeAll("World!\n");945 try body_writer.writer.writeAll("World!\n");
1004 try req.finish();946 try body_writer.end();
1005947
1006 try req.wait();948 var response = try req.receiveHead(&redirect_buffer);
1007949
1008 const body = try req.reader().readAllAlloc(gpa, 8192);950 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
1009 defer gpa.free(body);951 defer gpa.free(body);
1010952
1011 try expectEqualStrings("Hello, World!\n", body);953 try expectEqualStrings("Hello, World!\n", body);
...@@ -1021,9 +963,8 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1021,9 +963,8 @@ fn echoTests(client: *http.Client, port: u16) !void {
1021 .{port},963 .{port},
1022 ));964 ));
1023965
1024 var server_header_buffer: [1024]u8 = undefined;966 var redirect_buffer: [1024]u8 = undefined;
1025 var req = try client.open(.POST, uri, .{967 var req = try client.request(.POST, uri, .{
1026 .server_header_buffer = &server_header_buffer,
1027 .extra_headers = &.{968 .extra_headers = &.{
1028 .{ .name = "content-type", .value = "text/plain" },969 .{ .name = "content-type", .value = "text/plain" },
1029 },970 },
...@@ -1032,14 +973,14 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1032,14 +973,14 @@ fn echoTests(client: *http.Client, port: u16) !void {
1032973
1033 req.transfer_encoding = .chunked;974 req.transfer_encoding = .chunked;
1034975
1035 try req.send();976 var body_writer = try req.sendBody(&.{});
1036 try req.writeAll("Hello, ");977 try body_writer.writer.writeAll("Hello, ");
1037 try req.writeAll("World!\n");978 try body_writer.writer.writeAll("World!\n");
1038 try req.finish();979 try body_writer.end();
1039980
1040 try req.wait();981 var response = try req.receiveHead(&redirect_buffer);
1041982
1042 const body = try req.reader().readAllAlloc(gpa, 8192);983 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
1043 defer gpa.free(body);984 defer gpa.free(body);
1044985
1045 try expectEqualStrings("Hello, World!\n", body);986 try expectEqualStrings("Hello, World!\n", body);
...@@ -1053,8 +994,8 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1053,8 +994,8 @@ fn echoTests(client: *http.Client, port: u16) !void {
1053 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});994 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port});
1054 defer gpa.free(location);995 defer gpa.free(location);
1055996
1056 var body = std.ArrayList(u8).init(gpa);997 var body: std.ArrayListUnmanaged(u8) = .empty;
1057 defer body.deinit();998 defer body.deinit(gpa);
1058999
1059 const res = try client.fetch(.{1000 const res = try client.fetch(.{
1060 .location = .{ .url = location },1001 .location = .{ .url = location },
...@@ -1063,7 +1004,7 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1063,7 +1004,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
1063 .extra_headers = &.{1004 .extra_headers = &.{
1064 .{ .name = "content-type", .value = "text/plain" },1005 .{ .name = "content-type", .value = "text/plain" },
1065 },1006 },
1066 .response_storage = .{ .dynamic = &body },1007 .response_storage = .{ .allocator = gpa, .list = &body },
1067 });1008 });
1068 try expectEqual(.ok, res.status);1009 try expectEqual(.ok, res.status);
1069 try expectEqualStrings("Hello, World!\n", body.items);1010 try expectEqualStrings("Hello, World!\n", body.items);
...@@ -1074,9 +1015,8 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1074,9 +1015,8 @@ fn echoTests(client: *http.Client, port: u16) !void {
1074 defer gpa.free(location);1015 defer gpa.free(location);
1075 const uri = try std.Uri.parse(location);1016 const uri = try std.Uri.parse(location);
10761017
1077 var server_header_buffer: [1024]u8 = undefined;1018 var redirect_buffer: [1024]u8 = undefined;
1078 var req = try client.open(.POST, uri, .{1019 var req = try client.request(.POST, uri, .{
1079 .server_header_buffer = &server_header_buffer,
1080 .extra_headers = &.{1020 .extra_headers = &.{
1081 .{ .name = "expect", .value = "100-continue" },1021 .{ .name = "expect", .value = "100-continue" },
1082 .{ .name = "content-type", .value = "text/plain" },1022 .{ .name = "content-type", .value = "text/plain" },
...@@ -1086,15 +1026,15 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1086,15 +1026,15 @@ fn echoTests(client: *http.Client, port: u16) !void {
10861026
1087 req.transfer_encoding = .chunked;1027 req.transfer_encoding = .chunked;
10881028
1089 try req.send();1029 var body_writer = try req.sendBody(&.{});
1090 try req.writeAll("Hello, ");1030 try body_writer.writer.writeAll("Hello, ");
1091 try req.writeAll("World!\n");1031 try body_writer.writer.writeAll("World!\n");
1092 try req.finish();1032 try body_writer.end();
10931033
1094 try req.wait();1034 var response = try req.receiveHead(&redirect_buffer);
1095 try expectEqual(.ok, req.response.status);1035 try expectEqual(.ok, response.head.status);
10961036
1097 const body = try req.reader().readAllAlloc(gpa, 8192);1037 const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192));
1098 defer gpa.free(body);1038 defer gpa.free(body);
10991039
1100 try expectEqualStrings("Hello, World!\n", body);1040 try expectEqualStrings("Hello, World!\n", body);
...@@ -1105,9 +1045,8 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1105,9 +1045,8 @@ fn echoTests(client: *http.Client, port: u16) !void {
1105 defer gpa.free(location);1045 defer gpa.free(location);
1106 const uri = try std.Uri.parse(location);1046 const uri = try std.Uri.parse(location);
11071047
1108 var server_header_buffer: [1024]u8 = undefined;1048 var redirect_buffer: [1024]u8 = undefined;
1109 var req = try client.open(.POST, uri, .{1049 var req = try client.request(.POST, uri, .{
1110 .server_header_buffer = &server_header_buffer,
1111 .extra_headers = &.{1050 .extra_headers = &.{
1112 .{ .name = "content-type", .value = "text/plain" },1051 .{ .name = "content-type", .value = "text/plain" },
1113 .{ .name = "expect", .value = "garbage" },1052 .{ .name = "expect", .value = "garbage" },
...@@ -1117,23 +1056,24 @@ fn echoTests(client: *http.Client, port: u16) !void {...@@ -1117,23 +1056,24 @@ fn echoTests(client: *http.Client, port: u16) !void {
11171056
1118 req.transfer_encoding = .chunked;1057 req.transfer_encoding = .chunked;
11191058
1120 try req.send();1059 var body_writer = try req.sendBody(&.{});
1121 try req.wait();1060 try body_writer.flush();
1122 try expectEqual(.expectation_failed, req.response.status);1061 var response = try req.receiveHead(&redirect_buffer);
1062 try expectEqual(.expectation_failed, response.head.status);
1063 _ = try response.reader(&.{}).discardRemaining();
1123 }1064 }
1124
1125 _ = try client.fetch(.{
1126 .location = .{
1127 .url = try std.fmt.bufPrint(&location_buffer, "http://127.0.0.1:{d}/end", .{port}),
1128 },
1129 });
1130}1065}
11311066
1132const TestServer = struct {1067const TestServer = struct {
1068 shutting_down: bool,
1133 server_thread: std.Thread,1069 server_thread: std.Thread,
1134 net_server: std.net.Server,1070 net_server: std.net.Server,
11351071
1136 fn destroy(self: *@This()) void {1072 fn destroy(self: *@This()) void {
1073 self.shutting_down = true;
1074 const conn = std.net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure");
1075 conn.close();
1076
1137 self.server_thread.join();1077 self.server_thread.join();
1138 self.net_server.deinit();1078 self.net_server.deinit();
1139 std.testing.allocator.destroy(self);1079 std.testing.allocator.destroy(self);
...@@ -1153,20 +1093,27 @@ fn createTestServer(S: type) !*TestServer {...@@ -1153,20 +1093,27 @@ fn createTestServer(S: type) !*TestServer {
11531093
1154 const address = try std.net.Address.parseIp("127.0.0.1", 0);1094 const address = try std.net.Address.parseIp("127.0.0.1", 0);
1155 const test_server = try std.testing.allocator.create(TestServer);1095 const test_server = try std.testing.allocator.create(TestServer);
1156 test_server.net_server = try address.listen(.{ .reuse_address = true });1096 test_server.* = .{
1157 test_server.server_thread = try std.Thread.spawn(.{}, S.run, .{&test_server.net_server});1097 .net_server = try address.listen(.{ .reuse_address = true }),
1098 .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}),
1099 .shutting_down = false,
1100 };
1158 return test_server;1101 return test_server;
1159}1102}
11601103
1161test "redirect to different connection" {1104test "redirect to different connection" {
1162 const test_server_new = try createTestServer(struct {1105 const test_server_new = try createTestServer(struct {
1163 fn run(net_server: *std.net.Server) anyerror!void {1106 fn run(test_server: *TestServer) anyerror!void {
1164 var header_buffer: [888]u8 = undefined;1107 const net_server = &test_server.net_server;
1108 var recv_buffer: [888]u8 = undefined;
1109 var send_buffer: [777]u8 = undefined;
11651110
1166 const conn = try net_server.accept();1111 const connection = try net_server.accept();
1167 defer conn.stream.close();1112 defer connection.stream.close();
11681113
1169 var server = http.Server.init(conn, &header_buffer);1114 var connection_br = connection.stream.reader(&recv_buffer);
1115 var connection_bw = connection.stream.writer(&send_buffer);
1116 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
1170 var request = try server.receiveHead();1117 var request = try server.receiveHead();
1171 try expectEqualStrings(request.head.target, "/ok");1118 try expectEqualStrings(request.head.target, "/ok");
1172 try request.respond("good job, you pass", .{});1119 try request.respond("good job, you pass", .{});
...@@ -1180,18 +1127,22 @@ test "redirect to different connection" {...@@ -1180,18 +1127,22 @@ test "redirect to different connection" {
1180 global.other_port = test_server_new.port();1127 global.other_port = test_server_new.port();
11811128
1182 const test_server_orig = try createTestServer(struct {1129 const test_server_orig = try createTestServer(struct {
1183 fn run(net_server: *std.net.Server) anyerror!void {1130 fn run(test_server: *TestServer) anyerror!void {
1184 var header_buffer: [999]u8 = undefined;1131 const net_server = &test_server.net_server;
1132 var recv_buffer: [999]u8 = undefined;
1185 var send_buffer: [100]u8 = undefined;1133 var send_buffer: [100]u8 = undefined;
11861134
1187 const conn = try net_server.accept();1135 const connection = try net_server.accept();
1188 defer conn.stream.close();1136 defer connection.stream.close();
11891137
1190 const new_loc = try std.fmt.bufPrint(&send_buffer, "http://127.0.0.1:{d}/ok", .{1138 var loc_buf: [50]u8 = undefined;
1139 const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{
1191 global.other_port.?,1140 global.other_port.?,
1192 });1141 });
11931142
1194 var server = http.Server.init(conn, &header_buffer);1143 var connection_br = connection.stream.reader(&recv_buffer);
1144 var connection_bw = connection.stream.writer(&send_buffer);
1145 var server = http.Server.init(connection_br.interface(), &connection_bw.interface);
1195 var request = try server.receiveHead();1146 var request = try server.receiveHead();
1196 try expectEqualStrings(request.head.target, "/help");1147 try expectEqualStrings(request.head.target, "/help");
1197 try request.respond("", .{1148 try request.respond("", .{
...@@ -1216,16 +1167,15 @@ test "redirect to different connection" {...@@ -1216,16 +1167,15 @@ test "redirect to different connection" {
1216 const uri = try std.Uri.parse(location);1167 const uri = try std.Uri.parse(location);
12171168
1218 {1169 {
1219 var server_header_buffer: [666]u8 = undefined;1170 var redirect_buffer: [666]u8 = undefined;
1220 var req = try client.open(.GET, uri, .{1171 var req = try client.request(.GET, uri, .{});
1221 .server_header_buffer = &server_header_buffer,
1222 });
1223 defer req.deinit();1172 defer req.deinit();
12241173
1225 try req.send();1174 try req.sendBodiless();
1226 try req.wait();1175 var response = try req.receiveHead(&redirect_buffer);
1176 var reader = response.reader(&.{});
12271177
1228 const body = try req.reader().readAllAlloc(gpa, 8192);1178 const body = try reader.allocRemaining(gpa, .limited(8192));
1229 defer gpa.free(body);1179 defer gpa.free(body);
12301180
1231 try expectEqualStrings("good job, you pass", body);1181 try expectEqualStrings("good job, you pass", body);