authorgravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-06 20:13:15-06:00
committergravatar for truemedian@gmail.comNameless <truemedian@gmail.com> 2023-03-09 14:54:26-06:00
logfd2f906d1ede2b65ba21eec59137b2d4b676eedc
tree11273d17254e2be90d947f8810e9e5d3e523eb6e
parent8d86194b6e31788263d2cbdd03e2a8cde4134c37
signaturelock-open Commit is signed but in an unrecognized format.

std.http: handle compressed payloads


2 files changed, 496 insertions(+), 267 deletions(-)

lib/std/http.zig+10
......@@ -253,6 +253,16 @@ pub const TransferEncoding = enum {
253253 gzip,
254254};
255255
256pub const Connection = enum {
257 keep_alive,
258 close,
259};
260
261pub const CustomHeader = struct {
262 name: []const u8,
263 value: []const u8,
264};
265
256266const std = @import("std.zig");
257267
258268test {
lib/std/http/Client.zig+486-267
......@@ -21,27 +21,51 @@ ca_bundle: std.crypto.Certificate.Bundle = .{},
2121/// it will first rescan the system for root certificates.
2222next_https_rescan_certs: bool = true,
2323
24connection_pool: std.TailQueue(Connection) = .{},
24connection_mutex: std.Thread.Mutex = .{},
25connection_pool: ConnectionPool = .{},
26connection_used: ConnectionPool = .{},
2527
2628const ConnectionPool = std.TailQueue(Connection);
2729const ConnectionNode = ConnectionPool.Node;
2830
31/// Acquires an existing connection from the connection pool. This function is threadsafe.
32pub fn acquire(client: *Client, node: *ConnectionNode) void {
33 client.connection_mutex.lock();
34 defer client.connection_mutex.unlock();
35
36 client.connection_pool.remove(node);
37 client.connection_used.append(node);
38}
39
40/// Tries to release a connection back to the connection pool. This function is threadsafe.
41/// If the connection is marked as closing, it will be closed instead.
2942pub fn release(client: *Client, node: *ConnectionNode) void {
30 if (node.data.unusable) return node.data.close(client);
43 if (node.data.closing) {
44 node.data.close(client);
45
46 return client.allocator.destroy(node);
47 }
48
49 client.connection_mutex.lock();
50 defer client.connection_mutex.unlock();
3151
52 client.connection_used.remove(node);
3253 client.connection_pool.append(node);
3354}
3455
56const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.ReaderRaw);
57const GzipDecompressor = std.compress.gzip.Decompress(Request.ReaderRaw);
58
3559pub const Connection = struct {
3660 stream: net.Stream,
3761 /// undefined unless protocol is tls.
38 tls_client: std.crypto.tls.Client, // TODO: allocate this, it's currently 16 KB.
62 tls_client: *std.crypto.tls.Client, // TODO: allocate this, it's currently 16 KB.
3963 protocol: Protocol,
4064 host: []u8,
4165 port: u16,
4266
4367 // This connection has been part of a non keepalive request and cannot be added to the pool.
44 unusable: bool = false,
68 closing: bool = false,
4569
4670 pub const Protocol = enum { plain, tls };
4771
......@@ -59,6 +83,24 @@ pub const Connection = struct {
5983 }
6084 }
6185
86 pub const ReadError = std.net.Stream.ReadError || error{
87 TlsConnectionTruncated,
88 TlsRecordOverflow,
89 TlsDecodeError,
90 TlsAlert,
91 TlsBadRecordMac,
92 Overflow,
93 TlsBadLength,
94 TlsIllegalParameter,
95 TlsUnexpectedMessage,
96 };
97
98 pub const Reader = std.io.Reader(*Connection, ReadError, read);
99
100 pub fn reader(conn: *Connection) Reader {
101 return Reader{ .context = conn };
102 }
103
62104 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
63105 switch (conn.protocol) {
64106 .plain => return conn.stream.writeAll(buffer),
......@@ -73,10 +115,18 @@ pub const Connection = struct {
73115 }
74116 }
75117
118 pub const WriteError = std.net.Stream.WriteError || error{};
119 pub const Writer = std.io.Writer(*Connection, WriteError, write);
120
121 pub fn writer(conn: *Connection) Writer {
122 return Writer{ .context = conn };
123 }
124
76125 pub fn close(conn: *Connection, client: *const Client) void {
77126 if (conn.protocol == .tls) {
78127 // try to cleanly close the TLS connection, for any server that cares.
79128 _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {};
129 client.allocator.destroy(conn.tls_client);
80130 }
81131
82132 conn.stream.close();
......@@ -85,10 +135,10 @@ pub const Connection = struct {
85135 }
86136};
87137
88/// TODO: emit error.UnexpectedEndOfStream or something like that when the read
89/// data does not match the content length. This is necessary since HTTPS disables
90/// close_notify protection on underlying TLS streams.
91138pub const Request = struct {
139 const read_buffer_size = 8192;
140 const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size);
141
92142 client: *Client,
93143 connection: *ConnectionNode,
94144 redirects_left: u32,
......@@ -97,6 +147,11 @@ pub const Request = struct {
97147 /// redirects.
98148 headers: Headers,
99149
150 /// Read buffer for the connection. This is used to pull in large amounts of data from the connection even if the user asks for a small amount. This can probably be removed with careful planning.
151 read_buffer: [read_buffer_size]u8 = undefined,
152 read_buffer_start: ReadBufferIndex = 0,
153 read_buffer_len: ReadBufferIndex = 0,
154
100155 pub const Response = struct {
101156 headers: Response.Headers,
102157 state: State,
......@@ -106,15 +161,24 @@ pub const Request = struct {
106161 header_bytes: std.ArrayListUnmanaged(u8),
107162 max_header_bytes: usize,
108163 next_chunk_length: u64,
109 done: bool,
164 done: bool = false,
165
166 compression: union(enum) {
167 deflate: DeflateDecompressor,
168 gzip: GzipDecompressor,
169 none: void,
170 } = .none,
110171
111172 pub const Headers = struct {
112173 status: http.Status,
113174 version: http.Version,
114175 location: ?[]const u8 = null,
115176 content_length: ?u64 = null,
116 transfer_encoding: ?http.TransferEncoding = null,
117 connection_close: bool = true,
177 transfer_encoding: ?http.TransferEncoding = null, // This should only ever be chunked, compression is handled separately.
178 transfer_compression: ?http.TransferEncoding = null,
179 connection: http.Connection = .close,
180
181 number_of_headers: usize = 0,
118182
119183 pub fn parse(bytes: []const u8) !Response.Headers {
120184 var it = mem.split(u8, bytes[0 .. bytes.len - 4], "\r\n");
......@@ -137,6 +201,8 @@ pub const Request = struct {
137201 };
138202
139203 while (it.next()) |line| {
204 headers.number_of_headers += 1;
205
140206 if (line.len == 0) return error.HttpHeadersInvalid;
141207 switch (line[0]) {
142208 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
......@@ -152,14 +218,65 @@ pub const Request = struct {
152218 if (headers.content_length != null) return error.HttpHeadersInvalid;
153219 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
154220 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
155 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
156 headers.transfer_encoding = std.meta.stringToEnum(http.TransferEncoding, header_value) orelse
221 if (headers.transfer_encoding != null or headers.transfer_compression != null) return error.HttpHeadersInvalid;
222
223 // Transfer-Encoding: second, first
224 // Transfer-Encoding: deflate, chunked
225 var iter = std.mem.splitBackwards(u8, header_value, ",");
226
227 if (iter.next()) |first| {
228 const kind = std.meta.stringToEnum(
229 http.TransferEncoding,
230 std.mem.trim(u8, first, " "),
231 ) orelse
232 return error.HttpTransferEncodingUnsupported;
233
234 switch (kind) {
235 .chunked => headers.transfer_encoding = .chunked,
236 .compress => headers.transfer_compression = .compress,
237 .deflate => headers.transfer_compression = .deflate,
238 .gzip => headers.transfer_compression = .gzip,
239 }
240 }
241
242 if (iter.next()) |second| {
243 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
244
245 const kind = std.meta.stringToEnum(
246 http.TransferEncoding,
247 std.mem.trim(u8, second, " "),
248 ) orelse
249 return error.HttpTransferEncodingUnsupported;
250
251 switch (kind) {
252 .chunked => return error.HttpHeadersInvalid, // chunked must come last
253 .compress => return error.HttpTransferEncodingUnsupported, // compress not supported
254 .deflate => headers.transfer_compression = .deflate,
255 .gzip => headers.transfer_compression = .gzip,
256 }
257 }
258
259 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
260 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
261 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
262
263 const kind = std.meta.stringToEnum(
264 http.TransferEncoding,
265 std.mem.trim(u8, header_value, " "),
266 ) orelse
157267 return error.HttpTransferEncodingUnsupported;
268
269 switch (kind) {
270 .chunked => return error.HttpHeadersInvalid, // not transfer encoding
271 .compress => return error.HttpTransferEncodingUnsupported, // compress not supported
272 .deflate => headers.transfer_compression = .deflate,
273 .gzip => headers.transfer_compression = .gzip,
274 }
158275 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
159276 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
160 headers.connection_close = false;
277 headers.connection = .keep_alive;
161278 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
162 headers.connection_close = true;
279 headers.connection = .close;
163280 } else {
164281 return error.HttpConnectionHeaderUnsupported;
165282 }
......@@ -238,7 +355,6 @@ pub const Request = struct {
238355 .max_header_bytes = max,
239356 .header_bytes_owned = true,
240357 .next_chunk_length = undefined,
241 .done = false,
242358 };
243359 }
244360
......@@ -250,7 +366,6 @@ pub const Request = struct {
250366 .max_header_bytes = buf.len,
251367 .header_bytes_owned = false,
252368 .next_chunk_length = undefined,
253 .done = false,
254369 };
255370 }
256371
......@@ -537,10 +652,19 @@ pub const Request = struct {
537652 }
538653 };
539654
655 pub const RequestTransfer = union(enum) {
656 content_length: u64,
657 chunked: void,
658 none: void,
659 };
660
540661 pub const Headers = struct {
541662 version: http.Version = .@"HTTP/1.1",
542663 method: http.Method = .GET,
543 connection_close: bool = false,
664 connection: http.Connection = .keep_alive,
665 transfer_encoding: RequestTransfer = .none,
666
667 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
544668 };
545669
546670 pub const Options = struct {
......@@ -561,167 +685,131 @@ pub const Request = struct {
561685 };
562686 };
563687
564 /// May be skipped if header strategy is buffer.
688 /// Frees all resources associated with the request.
565689 pub fn deinit(req: *Request) void {
690 switch (req.response.compression) {
691 .none => {},
692 .deflate => |*deflate| deflate.deinit(),
693 .gzip => |*gzip| gzip.deinit(),
694 }
695
566696 if (req.response.header_bytes_owned) {
567697 req.response.header_bytes.deinit(req.client.allocator);
568698 }
699
700 if (!req.response.done) {
701 // If the response wasn't fully read, then we need to close the connection.
702 req.connection.data.closing = true;
703 req.client.release(req.connection);
704 }
705
569706 req.* = undefined;
570707 }
571708
572 pub const Reader = std.io.Reader(*Request, ReadError, read);
709 const ReadRawError = Connection.ReadError || std.Uri.ParseError || RequestError || error{
710 UnexpectedEndOfStream,
711 TooManyHttpRedirects,
712 HttpRedirectMissingLocation,
713 HttpHeadersInvalid,
714 };
573715
574 pub fn reader(req: *Request) Reader {
575 return .{ .context = req };
716 const ReaderRaw = std.io.Reader(*Request, ReadRawError, readRaw);
717
718 /// Read from the underlying stream, without decompressing or parsing the headers. Must be called
719 /// after waitForCompleteHead() has returned successfully.
720 pub fn readRaw(req: *Request, buffer: []u8) ReadRawError!usize {
721 assert(req.response.state.isContent());
722
723 var index: usize = 0;
724 while (index == 0) {
725 const amt = try req.readRawAdvanced(buffer[index..]);
726 const zero_means_end = req.response.done and req.response.headers.status.class() != .redirect;
727
728 if (amt == 0 and zero_means_end) break;
729 index += amt;
730 }
731
732 return index;
576733 }
577734
578 pub fn readAll(req: *Request, buffer: []u8) !usize {
579 return readAtLeast(req, buffer, buffer.len);
735 fn checkForCompleteHead(req: *Request, buffer: []u8) !usize {
736 switch (req.response.state) {
737 .invalid => unreachable,
738 .start, .seen_r, .seen_rn, .seen_rnr => {},
739 else => return 0, // No more headers to read.
740 }
741
742 const i = req.response.findHeadersEnd(buffer[0..]);
743 if (req.response.state == .invalid) return error.HttpHeadersInvalid;
744
745 const headers_data = buffer[0..i];
746 if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) {
747 return error.HttpHeadersExceededSizeLimit;
748 }
749 try req.response.header_bytes.appendSlice(req.client.allocator, headers_data);
750
751 if (req.response.state == .finished) {
752 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);
753
754 if (req.response.headers.connection == .keep_alive) {
755 req.connection.data.closing = false;
756 } else {
757 req.connection.data.closing = true;
758 }
759
760 if (req.response.headers.transfer_encoding) |transfer_encoding| {
761 switch (transfer_encoding) {
762 .chunked => {
763 req.response.next_chunk_length = 0;
764 req.response.state = .chunk_size;
765 },
766 .compress => unreachable,
767 .deflate => unreachable,
768 .gzip => unreachable,
769 }
770 } else if (req.response.headers.content_length) |content_length| {
771 req.response.next_chunk_length = content_length;
772 } else {
773 req.response.done = true;
774 }
775
776 return i;
777 }
778
779 return 0;
580780 }
581781
582 pub const ReadError = net.Stream.ReadError || error{
583 // From HTTP protocol
584 HttpHeadersInvalid,
782 pub const WaitForCompleteHeadError = ReadRawError || error {
783 UnexpectedEndOfStream,
784
585785 HttpHeadersExceededSizeLimit,
586 HttpRedirectMissingLocation,
587 HttpTransferEncodingUnsupported,
588 HttpConnectionHeaderUnsupported,
589 HttpContentLengthUnknown,
590 TooManyHttpRedirects,
591786 ShortHttpStatusLine,
592787 BadHttpVersion,
593788 HttpHeaderContinuationsUnsupported,
594 UnsupportedUrlScheme,
595 UriMissingHost,
596 UnknownHostName,
597
598 // Network problems
599 NetworkUnreachable,
600 HostLacksNetworkAddresses,
601 TemporaryNameServerFailure,
602 NameServerFailure,
603 ProtocolFamilyNotAvailable,
604 ProtocolNotSupported,
605
606 // System resource problems
607 ProcessFdQuotaExceeded,
608 SystemFdQuotaExceeded,
609 OutOfMemory,
610
611 // TLS problems
612 InsufficientEntropy,
613 TlsConnectionTruncated,
614 TlsRecordOverflow,
615 TlsDecodeError,
616 TlsAlert,
617 TlsBadRecordMac,
618 TlsBadLength,
619 TlsIllegalParameter,
620 TlsUnexpectedMessage,
621 TlsDecryptFailure,
622 CertificateFieldHasInvalidLength,
623 CertificateHostMismatch,
624 CertificatePublicKeyInvalid,
625 CertificateExpired,
626 CertificateFieldHasWrongDataType,
627 CertificateIssuerMismatch,
628 CertificateNotYetValid,
629 CertificateSignatureAlgorithmMismatch,
630 CertificateSignatureAlgorithmUnsupported,
631 CertificateSignatureInvalid,
632 CertificateSignatureInvalidLength,
633 CertificateSignatureNamedCurveUnsupported,
634 CertificateSignatureUnsupportedBitCount,
635 TlsCertificateNotVerified,
636 TlsBadSignatureScheme,
637 TlsBadRsaSignatureBitCount,
638 TlsDecryptError,
639 UnsupportedCertificateVersion,
640 CertificateTimeInvalid,
641 CertificateHasUnrecognizedObjectId,
642 CertificateHasInvalidBitString,
643 CertificateAuthorityBundleTooBig,
644
645 // TODO: convert to higher level errors
646 InvalidFormat,
647 InvalidPort,
648 UnexpectedCharacter,
649 Overflow,
650 InvalidCharacter,
651 AddressFamilyNotSupported,
652 AddressInUse,
653 AddressNotAvailable,
654 ConnectionPending,
655 ConnectionRefused,
656 FileNotFound,
657 PermissionDenied,
658 ServiceUnavailable,
659 SocketTypeNotSupported,
660 FileTooBig,
661 LockViolation,
662 NoSpaceLeft,
663 NotOpenForWriting,
664 InvalidEncoding,
665 IdentityElement,
666 NonCanonical,
667 SignatureVerificationFailed,
668 MessageTooLong,
669 NegativeIntoUnsigned,
670 TargetTooSmall,
671 BufferTooSmall,
672 InvalidSignature,
673 NotSquare,
674 DiskQuota,
675 InvalidEnd,
676 Incomplete,
677 InvalidIpv4Mapping,
678 InvalidIPAddressFormat,
679 BadPathName,
680 DeviceBusy,
681 FileBusy,
682 FileLocksNotSupported,
683 InvalidHandle,
684 InvalidUtf8,
685 NameTooLong,
686 NoDevice,
687 PathAlreadyExists,
688 PipeBusy,
689 SharingViolation,
690 SymLinkLoop,
691 FileSystem,
692 InterfaceNotFound,
693 AlreadyBound,
694 FileDescriptorNotASocket,
695 NetworkSubsystemFailed,
696 NotDir,
697 ReadOnlyFileSystem,
698 Unseekable,
699 MissingEndCertificateMarker,
700 InvalidPadding,
701 EndOfStream,
702 InvalidArgument,
789 HttpTransferEncodingUnsupported,
790 HttpConnectionHeaderUnsupported,
703791 };
704792
705 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
706 return readAtLeast(req, buffer, 1);
707 }
708
709 pub fn readAtLeast(req: *Request, buffer: []u8, len: usize) !usize {
710 assert(len <= buffer.len);
711 var index: usize = 0;
712 while (index < len) {
713 const amt = try readAdvanced(req, buffer[index..]);
714 const zero_means_end = req.response.done and req.response.headers.status.class() != .redirect;
793 /// Reads a complete response head. Any leftover data is stored in the request. This function is idempotent.
794 pub fn waitForCompleteHead(req: *Request) WaitForCompleteHeadError!void {
795 if (req.response.state.isContent()) return;
715796
716 if (amt == 0 and zero_means_end) break;
717 index += amt;
797 while (true) {
798 const nread = try req.connection.data.read(req.read_buffer[0..]);
799 const amt = try checkForCompleteHead(req, req.read_buffer[0..nread]);
800
801 if (amt != 0) {
802 req.read_buffer_start = @intCast(ReadBufferIndex, amt);
803 req.read_buffer_len = @intCast(ReadBufferIndex, nread);
804 return;
805 } else if (nread == 0) {
806 return error.UnexpectedEndOfStream;
807 }
718808 }
719 return index;
720809 }
721810
722811 /// This one can return 0 without meaning EOF.
723 /// TODO change to readvAdvanced
724 pub fn readAdvanced(req: *Request, buffer: []u8) !usize {
812 fn readRawAdvanced(req: *Request, buffer: []u8) !usize {
725813 if (req.response.done) {
726814 if (req.response.headers.status.class() == .redirect) {
727815 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
......@@ -744,82 +832,56 @@ pub const Request = struct {
744832 }
745833 }
746834
747 var in = buffer[0..try req.connection.data.read(buffer)];
835 // var in: []const u8 = undefined;
836 if (req.read_buffer_start == req.read_buffer_len) {
837 const nread = try req.connection.data.read(req.read_buffer[0..]);
838 if (nread == 0) return error.UnexpectedEndOfStream;
839
840 req.read_buffer_start = 0;
841 req.read_buffer_len = @intCast(ReadBufferIndex, nread);
842 }
843
748844 var out_index: usize = 0;
749845 while (true) {
750846 switch (req.response.state) {
751 .invalid => unreachable,
752 .start, .seen_r, .seen_rn, .seen_rnr => {
753 const i = req.response.findHeadersEnd(in);
754 if (req.response.state == .invalid) return error.HttpHeadersInvalid;
755
756 const headers_data = in[0..i];
757 if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) {
758 return error.HttpHeadersExceededSizeLimit;
759 }
760 try req.response.header_bytes.appendSlice(req.client.allocator, headers_data);
761
762 if (req.response.state == .finished) {
763 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items);
764
765 if (req.response.headers.connection_close == true) {
766 req.connection.data.unusable = true;
767 } else {
768 req.connection.data.unusable = false;
769 }
770
771 if (req.response.headers.transfer_encoding) |transfer_encoding| {
772 switch (transfer_encoding) {
773 .chunked => {
774 req.response.next_chunk_length = 0;
775 req.response.state = .chunk_size;
776 },
777 .compress => return error.HttpTransferEncodingUnsupported,
778 .deflate => return error.HttpTransferEncodingUnsupported,
779 .gzip => return error.HttpTransferEncodingUnsupported,
780 }
781 } else if (req.response.headers.content_length) |content_length| {
782 req.response.next_chunk_length = content_length;
783 } else {
784 return error.HttpContentLengthUnknown;
847 .invalid, .start, .seen_r, .seen_rn, .seen_rnr => unreachable,
848 .finished => {
849 // TODO https://github.com/ziglang/zig/issues/14039
850 const buf_avail = req.read_buffer_len - req.read_buffer_start;
851 const data_avail = req.response.next_chunk_length;
852 const out_avail = buffer.len;
853
854 if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) {
855 const can_read = @intCast(usize, @min(buf_avail, data_avail));
856 req.response.next_chunk_length -= can_read;
857
858 if (req.response.next_chunk_length == 0) {
859 req.client.release(req.connection);
860 req.connection = undefined;
861 req.response.done = true;
862 continue;
785863 }
786864
787 in = in[i..];
788 continue;
865 return 0; // skip over as much data as possible
789866 }
790867
791 assert(out_index == 0);
792 return 0;
793 },
794 .finished => {
795 const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len));
796 req.response.next_chunk_length -= sub_amt;
868 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail));
869 req.response.next_chunk_length -= can_read;
870
871 mem.copy(u8, buffer[0..], req.read_buffer[req.read_buffer_start..][0..can_read]);
872 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);
797873
798874 if (req.response.next_chunk_length == 0) {
799875 req.client.release(req.connection);
800876 req.connection = undefined;
801
802877 req.response.done = true;
803 assert(in.len == sub_amt); // TODO: figure out how to not read more than necessary.
804
805 if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) return 0;
806
807 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
808 return out_index + sub_amt;
809878 }
810879
811 if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) return 0;
812
813 if (in.ptr == buffer.ptr) {
814 return sub_amt;
815 } else {
816 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
817 return out_index + sub_amt;
818 }
880 return can_read;
819881 },
820 .chunk_size_prefix_r => switch (in.len) {
882 .chunk_size_prefix_r => switch (req.read_buffer_len - req.read_buffer_start) {
821883 0 => return out_index,
822 1 => switch (in[0]) {
884 1 => switch (req.read_buffer[req.read_buffer_start]) {
823885 '\r' => {
824886 req.response.state = .chunk_size_prefix_n;
825887 return out_index;
......@@ -829,9 +891,9 @@ pub const Request = struct {
829891 return error.HttpHeadersInvalid;
830892 },
831893 },
832 else => switch (int16(in[0..2])) {
894 else => switch (int16(req.read_buffer[req.read_buffer_start..][0..2])) {
833895 int16("\r\n") => {
834 in = in[2..];
896 req.read_buffer_start += 2;
835897 req.response.state = .chunk_size;
836898 continue;
837899 },
......@@ -841,11 +903,11 @@ pub const Request = struct {
841903 },
842904 },
843905 },
844 .chunk_size_prefix_n => switch (in.len) {
906 .chunk_size_prefix_n => switch (req.read_buffer_len - req.read_buffer_start) {
845907 0 => return out_index,
846 else => switch (in[0]) {
908 else => switch (req.read_buffer[req.read_buffer_start]) {
847909 '\n' => {
848 in = in[1..];
910 req.read_buffer_start += 1;
849911 req.response.state = .chunk_size;
850912 continue;
851913 },
......@@ -856,7 +918,7 @@ pub const Request = struct {
856918 },
857919 },
858920 .chunk_size, .chunk_r => {
859 const i = req.response.findChunkedLen(in);
921 const i = req.response.findChunkedLen(req.read_buffer[req.read_buffer_start..req.read_buffer_len]);
860922 switch (req.response.state) {
861923 .invalid => return error.HttpHeadersInvalid,
862924 .chunk_data => {
......@@ -867,7 +929,8 @@ pub const Request = struct {
867929
868930 return out_index;
869931 }
870 in = in[i..];
932
933 req.read_buffer_start += @intCast(ReadBufferIndex, i);
871934 continue;
872935 },
873936 .chunk_size => return out_index,
......@@ -876,34 +939,129 @@ pub const Request = struct {
876939 },
877940 .chunk_data => {
878941 // TODO https://github.com/ziglang/zig/issues/14039
879 const sub_amt = @intCast(usize, @min(req.response.next_chunk_length, in.len));
880 req.response.next_chunk_length -= sub_amt;
942 const buf_avail = req.read_buffer_len - req.read_buffer_start;
943 const data_avail = req.response.next_chunk_length;
944 const out_avail = buffer.len - out_index;
945
946 if (req.response.state.isContent() and req.response.headers.status.class() == .redirect) {
947 const can_read = @intCast(usize, @min(buf_avail, data_avail));
948 req.response.next_chunk_length -= can_read;
949
950 if (req.response.next_chunk_length == 0) {
951 req.client.release(req.connection);
952 req.connection = undefined;
953 req.response.done = true;
954 continue;
955 }
956
957 return 0; // skip over as much data as possible
958 }
959
960 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail));
961 req.response.next_chunk_length -= can_read;
962
963 mem.copy(u8, buffer[out_index..], req.read_buffer[req.read_buffer_start..][0..can_read]);
964 req.read_buffer_start += @intCast(ReadBufferIndex, can_read);
965 out_index += can_read;
881966
882967 if (req.response.next_chunk_length == 0) {
883968 req.response.state = .chunk_size_prefix_r;
884 in = in[sub_amt..];
885
886 if (req.response.headers.status.class() == .redirect) continue;
887969
888 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
889 out_index += sub_amt;
890970 continue;
891971 }
892972
893 if (req.response.headers.status.class() == .redirect) return 0;
894
895 if (in.ptr == buffer.ptr) {
896 return sub_amt;
897 } else {
898 mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
899 out_index += sub_amt;
900 return out_index;
901 }
973 return out_index;
902974 },
903975 }
904976 }
905977 }
906978
979 pub const ReadError = DeflateDecompressor.Error || GzipDecompressor.Error || WaitForCompleteHeadError || error{
980 BadHeader,
981 InvalidCompression,
982 StreamTooLong,
983 InvalidWindowSize,
984 };
985
986 pub const Reader = std.io.Reader(*Request, ReadError, read);
987
988 pub fn reader(req: *Request) Reader {
989 return .{ .context = req };
990 }
991
992 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
993 if (!req.response.state.isContent()) try req.waitForCompleteHead();
994
995 if (req.response.compression == .none and req.response.state.isContent()) {
996 if (req.response.headers.transfer_compression) |compression| {
997 switch (compression) {
998 .compress => unreachable,
999 .deflate => req.response.compression = .{
1000 .deflate = try std.compress.zlib.zlibStream(req.client.allocator, ReaderRaw{ .context = req }),
1001 },
1002 .gzip => req.response.compression = .{
1003 .gzip = try std.compress.gzip.decompress(req.client.allocator, ReaderRaw{ .context = req }),
1004 },
1005 .chunked => unreachable,
1006 }
1007 }
1008 }
1009
1010 return switch (req.response.compression) {
1011 .deflate => |*deflate| try deflate.read(buffer),
1012 .gzip => |*gzip| try gzip.read(buffer),
1013 else => try req.readRaw(buffer),
1014 };
1015 }
1016
1017 pub fn readAll(req: *Request, buffer: []u8) !usize {
1018 var index: usize = 0;
1019 while (index < buffer.len) {
1020 const amt = try read(req, buffer[index..]);
1021 if (amt == 0) break;
1022 index += amt;
1023 }
1024 return index;
1025 }
1026
1027 pub const WriteError = Connection.WriteError || error{MessageTooLong};
1028
1029 pub const Writer = std.io.Writer(*Request, WriteError, write);
1030
1031 pub fn writer(req: *Request) Writer {
1032 return .{ .context = req };
1033 }
1034
1035 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
1036 pub fn write(req: *Request, bytes: []const u8) !usize {
1037 switch (req.headers.transfer_encoding) {
1038 .chunked => {
1039 try req.connection.data.writer().print("{x}\r\n", .{bytes.len});
1040 try req.connection.data.writeAll(bytes);
1041 try req.connection.data.writeAll("\r\n");
1042
1043 return bytes.len;
1044 },
1045 .content_length => |*len| {
1046 if (len.* < bytes.len) return error.MessageTooLong;
1047
1048 const amt = try req.connection.data.write(bytes);
1049 len.* -= amt;
1050 return amt;
1051 },
1052 .none => return error.NotWriteable,
1053 }
1054 }
1055
1056 /// Finish the body of a request. This notifies the server that you have no more data to send.
1057 pub fn finish(req: *Request) !void {
1058 switch (req.headers.transfer_encoding) {
1059 .chunked => try req.connection.data.writeAll("0\r\n"),
1060 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
1061 .none => {},
1062 }
1063 }
1064
9071065 inline fn int16(array: *const [2]u8) u16 {
9081066 return @bitCast(u16, array.*);
9091067 }
......@@ -917,6 +1075,10 @@ pub const Request = struct {
9171075 }
9181076
9191077 test {
1078 const builtin = @import("builtin");
1079
1080 if (builtin.os.tag == .wasi) return error.SkipZigTest;
1081
9201082 _ = Response;
9211083 }
9221084};
......@@ -931,23 +1093,39 @@ pub fn deinit(client: *Client) void {
9311093 client.allocator.destroy(node);
9321094 }
9331095
1096 next = client.connection_used.first;
1097 while (next) |node| {
1098 next = node.next;
1099
1100 node.data.close(client);
1101
1102 client.allocator.destroy(node);
1103 }
1104
9341105 client.ca_bundle.deinit(client.allocator);
9351106 client.* = undefined;
9361107}
9371108
938pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) !*ConnectionNode {
939 var potential = client.connection_pool.last;
940 while (potential) |node| {
941 const same_host = mem.eql(u8, node.data.host, host);
942 const same_port = node.data.port == port;
943 const same_protocol = node.data.protocol == protocol;
1109pub const ConnectError = std.mem.Allocator.Error || std.net.TcpConnectToHostError || std.crypto.tls.Client.InitError(std.net.Stream);
9441110
945 if (same_host and same_port and same_protocol) {
946 client.connection_pool.remove(node);
947 return node;
948 }
1111pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionNode {
1112 { // Search through the connection pool for a potential connection.
1113 client.connection_mutex.lock();
1114 defer client.connection_mutex.unlock();
9491115
950 potential = node.prev;
1116 var potential = client.connection_pool.last;
1117 while (potential) |node| {
1118 const same_host = mem.eql(u8, node.data.host, host);
1119 const same_port = node.data.port == port;
1120 const same_protocol = node.data.protocol == protocol;
1121
1122 if (same_host and same_port and same_protocol) {
1123 client.acquire(node);
1124 return node;
1125 }
1126
1127 potential = node.prev;
1128 }
9511129 }
9521130
9531131 const conn = try client.allocator.create(ConnectionNode);
......@@ -964,17 +1142,35 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
9641142 switch (protocol) {
9651143 .plain => {},
9661144 .tls => {
967 conn.data.tls_client = try std.crypto.tls.Client.init(conn.data.stream, client.ca_bundle, host);
1145 conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client);
1146 conn.data.tls_client.* = try std.crypto.tls.Client.init(conn.data.stream, client.ca_bundle, host);
9681147 // This is appropriate for HTTPS because the HTTP headers contain
9691148 // the content length which is used to detect truncation attacks.
9701149 conn.data.tls_client.allow_truncation_attacks = true;
9711150 },
9721151 }
9731152
1153 {
1154 client.connection_mutex.lock();
1155 defer client.connection_mutex.unlock();
1156
1157 client.connection_used.append(conn);
1158 }
1159
9741160 return conn;
9751161}
9761162
977pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) !Request {
1163pub const RequestError = ConnectError || Connection.WriteError || error{
1164 UnsupportedUrlScheme,
1165 UriMissingHost,
1166
1167 CertificateAuthorityBundleTooBig,
1168 InvalidPadding,
1169 MissingEndCertificateMarker,
1170 Unseekable,
1171};
1172
1173pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Request.Options) RequestError!Request {
9781174 const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http"))
9791175 .plain
9801176 else if (mem.eql(u8, uri.scheme, "https"))
......@@ -990,8 +1186,13 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req
9901186 const host = uri.host orelse return error.UriMissingHost;
9911187
9921188 if (client.next_https_rescan_certs and protocol == .tls) {
993 try client.ca_bundle.rescan(client.allocator);
994 client.next_https_rescan_certs = false;
1189 client.connection_mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex.
1190 defer client.connection_mutex.unlock();
1191
1192 if (client.next_https_rescan_certs) {
1193 try client.ca_bundle.rescan(client.allocator);
1194 client.next_https_rescan_certs = false;
1195 }
9951196 }
9961197
9971198 var req: Request = .{
......@@ -1006,23 +1207,39 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Req
10061207 };
10071208
10081209 {
1009 var h = try std.BoundedArray(u8, 1000).init(0);
1010 try h.appendSlice(@tagName(headers.method));
1011 try h.appendSlice(" ");
1012 try h.appendSlice(uri.path);
1013 try h.appendSlice(" ");
1014 try h.appendSlice(@tagName(headers.version));
1015 try h.appendSlice("\r\nHost: ");
1016 try h.appendSlice(host);
1017 if (headers.connection_close) {
1018 try h.appendSlice("\r\nConnection: close");
1210 var buffered = std.io.bufferedWriter(req.connection.data.writer());
1211 const writer = buffered.writer();
1212
1213 try writer.writeAll(@tagName(headers.method));
1214 try writer.writeByte(' ');
1215 try writer.writeAll(uri.path);
1216 try writer.writeByte(' ');
1217 try writer.writeAll(@tagName(headers.version));
1218 try writer.writeAll("\r\nHost: ");
1219 try writer.writeAll(host);
1220 if (headers.connection == .close) {
1221 try writer.writeAll("\r\nConnection: close");
10191222 } else {
1020 try h.appendSlice("\r\nConnection: keep-alive");
1223 try writer.writeAll("\r\nConnection: keep-alive");
10211224 }
1022 try h.appendSlice("\r\n\r\n");
1225 try writer.writeAll("\r\nAccept-Encoding: gzip, deflate");
10231226
1024 const header_bytes = h.slice();
1025 try req.connection.data.writeAll(header_bytes);
1227 switch (headers.transfer_encoding) {
1228 .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"),
1229 .content_length => |content_length| try writer.print("\r\nContent-Length: {d}", .{content_length}),
1230 .none => {},
1231 }
1232
1233 for (headers.custom) |header| {
1234 try writer.writeAll("\r\n");
1235 try writer.writeAll(header.name);
1236 try writer.writeAll(": ");
1237 try writer.writeAll(header.value);
1238 }
1239
1240 try writer.writeAll("\r\n\r\n");
1241
1242 try buffered.flush();
10261243 }
10271244
10281245 return req;
......@@ -1036,5 +1253,7 @@ test {
10361253 return error.SkipZigTest;
10371254 }
10381255
1256 if (builtin.os.tag == .wasi) return error.SkipZigTest;
1257
10391258 _ = Request;
10401259}