authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-04-18 19:56:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-04-18 19:56:24-07:00
log0eebc258809beac9779af48216b01c5c20cbbfea
treea912eef3d99d3e2371484fd60f6b32a7e11d509b
parent77fdd76c16196441dce0f38ccea2eae01436c4be
parenta23c8662b41cf6954d8294ea316fb28a88481a7e
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #15299 from truemedian/std-http

std.http: curated error sets and custom Headers

10 files changed, 1145 insertions(+), 576 deletions(-)

lib/std/Uri.zig+76
...@@ -27,6 +27,18 @@ pub fn escapeQuery(allocator: std.mem.Allocator, input: []const u8) error{OutOfM...@@ -27,6 +27,18 @@ pub fn escapeQuery(allocator: std.mem.Allocator, input: []const u8) error{OutOfM
27 return escapeStringWithFn(allocator, input, isQueryChar);27 return escapeStringWithFn(allocator, input, isQueryChar);
28}28}
2929
30pub fn writeEscapedString(writer: anytype, input: []const u8) !void {
31 return writeEscapedStringWithFn(writer, input, isUnreserved);
32}
33
34pub fn writeEscapedPath(writer: anytype, input: []const u8) !void {
35 return writeEscapedStringWithFn(writer, input, isPathChar);
36}
37
38pub fn writeEscapedQuery(writer: anytype, input: []const u8) !void {
39 return writeEscapedStringWithFn(writer, input, isQueryChar);
40}
41
30pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) std.mem.Allocator.Error![]const u8 {42pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) std.mem.Allocator.Error![]const u8 {
31 var outsize: usize = 0;43 var outsize: usize = 0;
32 for (input) |c| {44 for (input) |c| {
...@@ -52,6 +64,16 @@ pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, compt...@@ -52,6 +64,16 @@ pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, compt
52 return output;64 return output;
53}65}
5466
67pub fn writeEscapedStringWithFn(writer: anytype, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) @TypeOf(writer).Error!void {
68 for (input) |c| {
69 if (keepUnescaped(c)) {
70 try writer.writeByte(c);
71 } else {
72 try writer.print("%{X:0>2}", .{c});
73 }
74 }
75}
76
55/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies77/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies
56/// them to the output.78/// them to the output.
57pub fn unescapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {79pub fn unescapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 {
...@@ -184,6 +206,60 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {...@@ -184,6 +206,60 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
184 return uri;206 return uri;
185}207}
186208
209pub fn format(
210 uri: Uri,
211 comptime fmt: []const u8,
212 options: std.fmt.FormatOptions,
213 writer: anytype,
214) @TypeOf(writer).Error!void {
215 _ = options;
216
217 const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null;
218 const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
219
220 if (needs_absolute) {
221 try writer.writeAll(uri.scheme);
222 try writer.writeAll(":");
223 if (uri.host) |host| {
224 try writer.writeAll("//");
225
226 if (uri.user) |user| {
227 try writer.writeAll(user);
228 if (uri.password) |password| {
229 try writer.writeAll(":");
230 try writer.writeAll(password);
231 }
232 try writer.writeAll("@");
233 }
234
235 try writer.writeAll(host);
236
237 if (uri.port) |port| {
238 try writer.writeAll(":");
239 try std.fmt.formatInt(port, 10, .lower, .{}, writer);
240 }
241 }
242 }
243
244 if (needs_path) {
245 if (uri.path.len == 0) {
246 try writer.writeAll("/");
247 } else {
248 try Uri.writeEscapedPath(writer, uri.path);
249 }
250
251 if (uri.query) |q| {
252 try writer.writeAll("?");
253 try Uri.writeEscapedQuery(writer, q);
254 }
255
256 if (uri.fragment) |f| {
257 try writer.writeAll("#");
258 try Uri.writeEscapedQuery(writer, f);
259 }
260 }
261}
262
187/// Parses the URI or returns an error.263/// Parses the URI or returns an error.
188/// The return value will contain unescaped strings pointing into the264/// The return value will contain unescaped strings pointing into the
189/// original `text`. Each component that is provided, will be non-`null`.265/// original `text`. Each component that is provided, will be non-`null`.
lib/std/crypto/Certificate.zig+21-11
...@@ -371,7 +371,9 @@ test "Parsed.checkHostName" {...@@ -371,7 +371,9 @@ test "Parsed.checkHostName" {
371 try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org"));371 try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org"));
372}372}
373373
374pub fn parse(cert: Certificate) !Parsed {374pub const ParseError = der.Element.ParseElementError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError;
375
376pub fn parse(cert: Certificate) ParseError!Parsed {
375 const cert_bytes = cert.buffer;377 const cert_bytes = cert.buffer;
376 const certificate = try der.Element.parse(cert_bytes, cert.index);378 const certificate = try der.Element.parse(cert_bytes, cert.index);
377 const tbs_certificate = try der.Element.parse(cert_bytes, certificate.slice.start);379 const tbs_certificate = try der.Element.parse(cert_bytes, certificate.slice.start);
...@@ -514,14 +516,18 @@ pub fn contents(cert: Certificate, elem: der.Element) []const u8 {...@@ -514,14 +516,18 @@ pub fn contents(cert: Certificate, elem: der.Element) []const u8 {
514 return cert.buffer[elem.slice.start..elem.slice.end];516 return cert.buffer[elem.slice.start..elem.slice.end];
515}517}
516518
519pub const ParseBitStringError = error{ CertificateFieldHasWrongDataType, CertificateHasInvalidBitString };
520
517pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice {521pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice {
518 if (elem.identifier.tag != .bitstring) return error.CertificateFieldHasWrongDataType;522 if (elem.identifier.tag != .bitstring) return error.CertificateFieldHasWrongDataType;
519 if (cert.buffer[elem.slice.start] != 0) return error.CertificateHasInvalidBitString;523 if (cert.buffer[elem.slice.start] != 0) return error.CertificateHasInvalidBitString;
520 return .{ .start = elem.slice.start + 1, .end = elem.slice.end };524 return .{ .start = elem.slice.start + 1, .end = elem.slice.end };
521}525}
522526
527pub const ParseTimeError = error{ CertificateTimeInvalid, CertificateFieldHasWrongDataType };
528
523/// Returns number of seconds since epoch.529/// Returns number of seconds since epoch.
524pub fn parseTime(cert: Certificate, elem: der.Element) !u64 {530pub fn parseTime(cert: Certificate, elem: der.Element) ParseTimeError!u64 {
525 const bytes = cert.contents(elem);531 const bytes = cert.contents(elem);
526 switch (elem.identifier.tag) {532 switch (elem.identifier.tag) {
527 .utc_time => {533 .utc_time => {
...@@ -647,34 +653,38 @@ test parseYear4 {...@@ -647,34 +653,38 @@ test parseYear4 {
647 try expectError(error.CertificateTimeInvalid, parseYear4("crap"));653 try expectError(error.CertificateTimeInvalid, parseYear4("crap"));
648}654}
649655
650pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm {656pub fn parseAlgorithm(bytes: []const u8, element: der.Element) ParseEnumError!Algorithm {
651 return parseEnum(Algorithm, bytes, element);657 return parseEnum(Algorithm, bytes, element);
652}658}
653659
654pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) !AlgorithmCategory {660pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) ParseEnumError!AlgorithmCategory {
655 return parseEnum(AlgorithmCategory, bytes, element);661 return parseEnum(AlgorithmCategory, bytes, element);
656}662}
657663
658pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute {664pub fn parseAttribute(bytes: []const u8, element: der.Element) ParseEnumError!Attribute {
659 return parseEnum(Attribute, bytes, element);665 return parseEnum(Attribute, bytes, element);
660}666}
661667
662pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve {668pub fn parseNamedCurve(bytes: []const u8, element: der.Element) ParseEnumError!NamedCurve {
663 return parseEnum(NamedCurve, bytes, element);669 return parseEnum(NamedCurve, bytes, element);
664}670}
665671
666pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId {672pub fn parseExtensionId(bytes: []const u8, element: der.Element) ParseEnumError!ExtensionId {
667 return parseEnum(ExtensionId, bytes, element);673 return parseEnum(ExtensionId, bytes, element);
668}674}
669675
670fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E {676pub const ParseEnumError = error{ CertificateFieldHasWrongDataType, CertificateHasUnrecognizedObjectId };
677
678fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) ParseEnumError!E {
671 if (element.identifier.tag != .object_identifier)679 if (element.identifier.tag != .object_identifier)
672 return error.CertificateFieldHasWrongDataType;680 return error.CertificateFieldHasWrongDataType;
673 const oid_bytes = bytes[element.slice.start..element.slice.end];681 const oid_bytes = bytes[element.slice.start..element.slice.end];
674 return E.map.get(oid_bytes) orelse return error.CertificateHasUnrecognizedObjectId;682 return E.map.get(oid_bytes) orelse return error.CertificateHasUnrecognizedObjectId;
675}683}
676684
677pub fn parseVersion(bytes: []const u8, version_elem: der.Element) !Version {685pub const ParseVersionError = error{ UnsupportedCertificateVersion, CertificateFieldHasInvalidLength };
686
687pub fn parseVersion(bytes: []const u8, version_elem: der.Element) ParseVersionError!Version {
678 if (@bitCast(u8, version_elem.identifier) != 0xa0)688 if (@bitCast(u8, version_elem.identifier) != 0xa0)
679 return .v1;689 return .v1;
680690
...@@ -861,9 +871,9 @@ pub const der = struct {...@@ -861,9 +871,9 @@ pub const der = struct {
861 pub const empty: Slice = .{ .start = 0, .end = 0 };871 pub const empty: Slice = .{ .start = 0, .end = 0 };
862 };872 };
863873
864 pub const ParseError = error{CertificateFieldHasInvalidLength};874 pub const ParseElementError = error{CertificateFieldHasInvalidLength};
865875
866 pub fn parse(bytes: []const u8, index: u32) ParseError!Element {876 pub fn parse(bytes: []const u8, index: u32) ParseElementError!Element {
867 var i = index;877 var i = index;
868 const identifier = @bitCast(Identifier, bytes[i]);878 const identifier = @bitCast(Identifier, bytes[i]);
869 i += 1;879 i += 1;
lib/std/crypto/Certificate/Bundle.zig+27-10
...@@ -50,11 +50,13 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {...@@ -50,11 +50,13 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {
50 cb.* = undefined;50 cb.* = undefined;
51}51}
5252
53pub const RescanError = RescanLinuxError || RescanMacError || RescanWindowsError;
54
53/// Clears the set of certificates and then scans the host operating system55/// Clears the set of certificates and then scans the host operating system
54/// file system standard locations for certificates.56/// file system standard locations for certificates.
55/// For operating systems that do not have standard CA installations to be57/// For operating systems that do not have standard CA installations to be
56/// found, this function clears the set of certificates.58/// found, this function clears the set of certificates.
57pub fn rescan(cb: *Bundle, gpa: Allocator) !void {59pub fn rescan(cb: *Bundle, gpa: Allocator) RescanError!void {
58 switch (builtin.os.tag) {60 switch (builtin.os.tag) {
59 .linux => return rescanLinux(cb, gpa),61 .linux => return rescanLinux(cb, gpa),
60 .macos => return rescanMac(cb, gpa),62 .macos => return rescanMac(cb, gpa),
...@@ -64,8 +66,11 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void {...@@ -64,8 +66,11 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void {
64}66}
6567
66pub const rescanMac = @import("Bundle/macos.zig").rescanMac;68pub const rescanMac = @import("Bundle/macos.zig").rescanMac;
69pub const RescanMacError = @import("Bundle/macos.zig").RescanMacError;
70
71pub const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError;
6772
68pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void {73pub fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void {
69 // Possible certificate files; stop after finding one.74 // Possible certificate files; stop after finding one.
70 const cert_file_paths = [_][]const u8{75 const cert_file_paths = [_][]const u8{
71 "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.76 "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
...@@ -107,7 +112,9 @@ pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void {...@@ -107,7 +112,9 @@ pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void {
107 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);112 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
108}113}
109114
110pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void {115pub const RescanWindowsError = Allocator.Error || ParseCertError || std.os.UnexpectedError || error{FileNotFound};
116
117pub fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void {
111 cb.bytes.clearRetainingCapacity();118 cb.bytes.clearRetainingCapacity();
112 cb.map.clearRetainingCapacity();119 cb.map.clearRetainingCapacity();
113120
...@@ -132,12 +139,14 @@ pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void {...@@ -132,12 +139,14 @@ pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void {
132 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);139 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
133}140}
134141
142pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError;
143
135pub fn addCertsFromDirPath(144pub fn addCertsFromDirPath(
136 cb: *Bundle,145 cb: *Bundle,
137 gpa: Allocator,146 gpa: Allocator,
138 dir: fs.Dir,147 dir: fs.Dir,
139 sub_dir_path: []const u8,148 sub_dir_path: []const u8,
140) !void {149) AddCertsFromDirPathError!void {
141 var iterable_dir = try dir.openIterableDir(sub_dir_path, .{});150 var iterable_dir = try dir.openIterableDir(sub_dir_path, .{});
142 defer iterable_dir.close();151 defer iterable_dir.close();
143 return addCertsFromDir(cb, gpa, iterable_dir);152 return addCertsFromDir(cb, gpa, iterable_dir);
...@@ -147,14 +156,16 @@ pub fn addCertsFromDirPathAbsolute(...@@ -147,14 +156,16 @@ pub fn addCertsFromDirPathAbsolute(
147 cb: *Bundle,156 cb: *Bundle,
148 gpa: Allocator,157 gpa: Allocator,
149 abs_dir_path: []const u8,158 abs_dir_path: []const u8,
150) !void {159) AddCertsFromDirPathError!void {
151 assert(fs.path.isAbsolute(abs_dir_path));160 assert(fs.path.isAbsolute(abs_dir_path));
152 var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{});161 var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{});
153 defer iterable_dir.close();162 defer iterable_dir.close();
154 return addCertsFromDir(cb, gpa, iterable_dir);163 return addCertsFromDir(cb, gpa, iterable_dir);
155}164}
156165
157pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) !void {166pub const AddCertsFromDirError = AddCertsFromFilePathError;
167
168pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) AddCertsFromDirError!void {
158 var it = iterable_dir.iterate();169 var it = iterable_dir.iterate();
159 while (try it.next()) |entry| {170 while (try it.next()) |entry| {
160 switch (entry.kind) {171 switch (entry.kind) {
...@@ -166,11 +177,13 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir...@@ -166,11 +177,13 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir
166 }177 }
167}178}
168179
180pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError;
181
169pub fn addCertsFromFilePathAbsolute(182pub fn addCertsFromFilePathAbsolute(
170 cb: *Bundle,183 cb: *Bundle,
171 gpa: Allocator,184 gpa: Allocator,
172 abs_file_path: []const u8,185 abs_file_path: []const u8,
173) !void {186) AddCertsFromFilePathError!void {
174 assert(fs.path.isAbsolute(abs_file_path));187 assert(fs.path.isAbsolute(abs_file_path));
175 var file = try fs.openFileAbsolute(abs_file_path, .{});188 var file = try fs.openFileAbsolute(abs_file_path, .{});
176 defer file.close();189 defer file.close();
...@@ -182,13 +195,15 @@ pub fn addCertsFromFilePath(...@@ -182,13 +195,15 @@ pub fn addCertsFromFilePath(
182 gpa: Allocator,195 gpa: Allocator,
183 dir: fs.Dir,196 dir: fs.Dir,
184 sub_file_path: []const u8,197 sub_file_path: []const u8,
185) !void {198) AddCertsFromFilePathError!void {
186 var file = try dir.openFile(sub_file_path, .{});199 var file = try dir.openFile(sub_file_path, .{});
187 defer file.close();200 defer file.close();
188 return addCertsFromFile(cb, gpa, file);201 return addCertsFromFile(cb, gpa, file);
189}202}
190203
191pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void {204pub const AddCertsFromFileError = Allocator.Error || fs.File.GetSeekPosError || fs.File.ReadError || ParseCertError || std.base64.Error || error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker };
205
206pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void {
192 const size = try file.getEndPos();207 const size = try file.getEndPos();
193208
194 // We borrow `bytes` as a temporary buffer for the base64-encoded data.209 // We borrow `bytes` as a temporary buffer for the base64-encoded data.
...@@ -222,7 +237,9 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void {...@@ -222,7 +237,9 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void {
222 }237 }
223}238}
224239
225pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) !void {240pub const ParseCertError = Allocator.Error || Certificate.ParseError;
241
242pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) ParseCertError!void {
226 // Even though we could only partially parse the certificate to find243 // Even though we could only partially parse the certificate to find
227 // the subject name, we pre-parse all of them to make sure and only244 // the subject name, we pre-parse all of them to make sure and only
228 // include in the bundle ones that we know will parse. This way we can245 // include in the bundle ones that we know will parse. This way we can
lib/std/crypto/Certificate/Bundle/macos.zig+3-1
...@@ -5,7 +5,9 @@ const mem = std.mem;...@@ -5,7 +5,9 @@ const mem = std.mem;
5const Allocator = std.mem.Allocator;5const Allocator = std.mem.Allocator;
6const Bundle = @import("../Bundle.zig");6const Bundle = @import("../Bundle.zig");
77
8pub fn rescanMac(cb: *Bundle, gpa: Allocator) !void {8pub const RescanMacError = Allocator.Error || fs.File.OpenError || fs.File.ReadError || fs.File.SeekError || Bundle.ParseCertError || error{EndOfStream};
9
10pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void {
9 cb.bytes.clearRetainingCapacity();11 cb.bytes.clearRetainingCapacity();
10 cb.map.clearRetainingCapacity();12 cb.map.clearRetainingCapacity();
1113
lib/std/http.zig+4-5
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1pub const Client = @import("http/Client.zig");1pub const Client = @import("http/Client.zig");
2pub const Server = @import("http/Server.zig");2pub const Server = @import("http/Server.zig");
3pub const protocol = @import("http/protocol.zig");3pub const protocol = @import("http/protocol.zig");
4const headers = @import("http/Headers.zig");
5
6pub const Headers = headers.Headers;
7pub const Field = headers.Field;
48
5pub const Version = enum {9pub const Version = enum {
6 @"HTTP/1.0",10 @"HTTP/1.0",
...@@ -265,11 +269,6 @@ pub const Connection = enum {...@@ -265,11 +269,6 @@ pub const Connection = enum {
265 close,269 close,
266};270};
267271
268pub const CustomHeader = struct {
269 name: []const u8,
270 value: []const u8,
271};
272
273const std = @import("std.zig");272const std = @import("std.zig");
274273
275test {274test {
lib/std/http/Client.zig+368-352
...@@ -25,48 +25,7 @@ next_https_rescan_certs: bool = true,...@@ -25,48 +25,7 @@ next_https_rescan_certs: bool = true,
25/// The pool of connections that can be reused (and currently in use).25/// The pool of connections that can be reused (and currently in use).
26connection_pool: ConnectionPool = .{},26connection_pool: ConnectionPool = .{},
2727
28/// The last error that occurred on this client. This is not threadsafe, do not expect it to be completely accurate.28proxy: ?HttpProxy = null,
29last_error: ?ExtraError = null,
30
31pub const ExtraError = union(enum) {
32 fn impliedErrorSet(comptime f: anytype) type {
33 const set = @typeInfo(@typeInfo(@TypeOf(f)).Fn.return_type.?).ErrorUnion.error_set;
34 if (@typeName(set)[0] != '@') @compileError(@typeName(f) ++ " doesn't have an implied error set any more.");
35 return set;
36 }
37
38 // There's apparently a dependency loop with using Client.DeflateDecompressor.
39 const FakeTransferError = proto.HeadersParser.ReadError || error{ReadFailed};
40 const FakeTransferReader = std.io.Reader(void, FakeTransferError, fakeRead);
41 fn fakeRead(ctx: void, buf: []u8) FakeTransferError!usize {
42 _ = .{ buf, ctx };
43 return 0;
44 }
45
46 const FakeDeflateDecompressor = std.compress.zlib.ZlibStream(FakeTransferReader);
47 const FakeGzipDecompressor = std.compress.gzip.Decompress(FakeTransferReader);
48 const FakeZstdDecompressor = std.compress.zstd.DecompressStream(FakeTransferReader, .{});
49
50 pub const TcpConnectError = std.net.TcpConnectToHostError;
51 pub const TlsError = std.crypto.tls.Client.InitError(net.Stream);
52 pub const WriteError = BufferedConnection.WriteError;
53 pub const ReadError = BufferedConnection.ReadError || error{HttpChunkInvalid};
54 pub const CaBundleError = impliedErrorSet(std.crypto.Certificate.Bundle.rescan);
55
56 pub const ZlibInitError = error{ BadHeader, InvalidCompression, InvalidWindowSize, Unsupported, EndOfStream, OutOfMemory } || Request.TransferReadError;
57 pub const GzipInitError = error{ BadHeader, InvalidCompression, OutOfMemory, WrongChecksum, EndOfStream, StreamTooLong } || Request.TransferReadError;
58 // pub const DecompressError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error;
59 pub const DecompressError = FakeDeflateDecompressor.Error || FakeGzipDecompressor.Error || FakeZstdDecompressor.Error;
60
61 zlib_init: ZlibInitError, // error.CompressionInitializationFailed
62 gzip_init: GzipInitError, // error.CompressionInitializationFailed
63 connect: TcpConnectError, // error.ConnectionFailed
64 ca_bundle: CaBundleError, // error.CertificateAuthorityBundleFailed
65 tls: TlsError, // error.TlsInitializationFailed
66 write: WriteError, // error.WriteFailed
67 read: ReadError, // error.ReadFailed
68 decompress: DecompressError, // error.ReadFailed
69};
7029
71/// A set of linked lists of connections that can be reused.30/// A set of linked lists of connections that can be reused.
72pub const ConnectionPool = struct {31pub const ConnectionPool = struct {
...@@ -82,6 +41,7 @@ pub const ConnectionPool = struct {...@@ -82,6 +41,7 @@ pub const ConnectionPool = struct {
82 host: []u8,41 host: []u8,
83 port: u16,42 port: u16,
8443
44 proxied: bool = false,
85 closing: bool = false,45 closing: bool = false,
8646
87 pub fn deinit(self: *StoredConnection, client: *Client) void {47 pub fn deinit(self: *StoredConnection, client: *Client) void {
...@@ -158,7 +118,12 @@ pub const ConnectionPool = struct {...@@ -158,7 +118,12 @@ pub const ConnectionPool = struct {
158 return client.allocator.destroy(popped);118 return client.allocator.destroy(popped);
159 }119 }
160120
161 pool.free.append(node);121 if (node.data.proxied) {
122 pool.free.prepend(node); // proxied connections go to the end of the queue, always try direct connections first
123 } else {
124 pool.free.append(node);
125 }
126
162 pool.free_len += 1;127 pool.free_len += 1;
163 }128 }
164129
...@@ -202,30 +167,38 @@ pub const Connection = struct {...@@ -202,30 +167,38 @@ pub const Connection = struct {
202167
203 pub const Protocol = enum { plain, tls };168 pub const Protocol = enum { plain, tls };
204169
205 pub fn read(conn: *Connection, buffer: []u8) !usize {170 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
206 switch (conn.protocol) {171 return switch (conn.protocol) {
207 .plain => return conn.stream.read(buffer),172 .plain => conn.stream.read(buffer),
208 .tls => return conn.tls_client.read(conn.stream, buffer),173 .tls => conn.tls_client.read(conn.stream, buffer),
209 }174 } catch |err| switch (err) {
175 error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure,
176 error.TlsAlert => return error.TlsAlert,
177 error.ConnectionTimedOut => return error.ConnectionTimedOut,
178 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
179 else => return error.UnexpectedReadFailure,
180 };
210 }181 }
211182
212 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {183 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
213 switch (conn.protocol) {184 return switch (conn.protocol) {
214 .plain => return conn.stream.readAtLeast(buffer, len),185 .plain => conn.stream.readAtLeast(buffer, len),
215 .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),186 .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len),
216 }187 } catch |err| switch (err) {
188 error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure,
189 error.TlsAlert => return error.TlsAlert,
190 error.ConnectionTimedOut => return error.ConnectionTimedOut,
191 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
192 else => return error.UnexpectedReadFailure,
193 };
217 }194 }
218195
219 pub const ReadError = net.Stream.ReadError || error{196 pub const ReadError = error{
220 TlsConnectionTruncated,197 TlsFailure,
221 TlsRecordOverflow,
222 TlsDecodeError,
223 TlsAlert,198 TlsAlert,
224 TlsBadRecordMac,199 ConnectionTimedOut,
225 Overflow,200 ConnectionResetByPeer,
226 TlsBadLength,201 UnexpectedReadFailure,
227 TlsIllegalParameter,
228 TlsUnexpectedMessage,
229 };202 };
230203
231 pub const Reader = std.io.Reader(*Connection, ReadError, read);204 pub const Reader = std.io.Reader(*Connection, ReadError, read);
...@@ -235,20 +208,30 @@ pub const Connection = struct {...@@ -235,20 +208,30 @@ pub const Connection = struct {
235 }208 }
236209
237 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {210 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
238 switch (conn.protocol) {211 return switch (conn.protocol) {
239 .plain => return conn.stream.writeAll(buffer),212 .plain => conn.stream.writeAll(buffer),
240 .tls => return conn.tls_client.writeAll(conn.stream, buffer),213 .tls => conn.tls_client.writeAll(conn.stream, buffer),
241 }214 } catch |err| switch (err) {
215 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
216 else => return error.UnexpectedWriteFailure,
217 };
242 }218 }
243219
244 pub fn write(conn: *Connection, buffer: []const u8) !usize {220 pub fn write(conn: *Connection, buffer: []const u8) !usize {
245 switch (conn.protocol) {221 return switch (conn.protocol) {
246 .plain => return conn.stream.write(buffer),222 .plain => conn.stream.write(buffer),
247 .tls => return conn.tls_client.write(conn.stream, buffer),223 .tls => conn.tls_client.write(conn.stream, buffer),
248 }224 } catch |err| switch (err) {
225 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
226 else => return error.UnexpectedWriteFailure,
227 };
249 }228 }
250229
251 pub const WriteError = net.Stream.WriteError || error{};230 pub const WriteError = error{
231 ConnectionResetByPeer,
232 UnexpectedWriteFailure,
233 };
234
252 pub const Writer = std.io.Writer(*Connection, WriteError, write);235 pub const Writer = std.io.Writer(*Connection, WriteError, write);
253236
254 pub fn writer(conn: *Connection) Writer {237 pub fn writer(conn: *Connection) Writer {
...@@ -371,140 +354,125 @@ pub const Compression = union(enum) {...@@ -371,140 +354,125 @@ pub const Compression = union(enum) {
371354
372/// A HTTP response originating from a server.355/// A HTTP response originating from a server.
373pub const Response = struct {356pub const Response = struct {
374 pub const Headers = struct {357 pub const ParseError = Allocator.Error || error{
375 status: http.Status,358 ShortHttpStatusLine,
376 version: http.Version,359 BadHttpVersion,
377 location: ?[]const u8 = null,360 HttpHeadersInvalid,
378 content_length: ?u64 = null,361 HttpHeaderContinuationsUnsupported,
379 transfer_encoding: ?http.TransferEncoding = null,362 HttpTransferEncodingUnsupported,
380 transfer_compression: ?http.ContentEncoding = null,363 HttpConnectionHeaderUnsupported,
381 connection: http.Connection = .close,364 InvalidContentLength,
382 upgrade: ?[]const u8 = null,365 CompressionNotSupported,
383366 };
384 pub const ParseError = error{
385 ShortHttpStatusLine,
386 BadHttpVersion,
387 HttpHeadersInvalid,
388 HttpHeaderContinuationsUnsupported,
389 HttpTransferEncodingUnsupported,
390 HttpConnectionHeaderUnsupported,
391 InvalidContentLength,
392 CompressionNotSupported,
393 };
394
395 pub fn parse(bytes: []const u8) ParseError!Headers {
396 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
397
398 const first_line = it.next() orelse return error.HttpHeadersInvalid;
399 if (first_line.len < 12)
400 return error.ShortHttpStatusLine;
401
402 const version: http.Version = switch (int64(first_line[0..8])) {
403 int64("HTTP/1.0") => .@"HTTP/1.0",
404 int64("HTTP/1.1") => .@"HTTP/1.1",
405 else => return error.BadHttpVersion,
406 };
407 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
408 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
409
410 var headers: Headers = .{
411 .version = version,
412 .status = status,
413 };
414
415 while (it.next()) |line| {
416 if (line.len == 0) return error.HttpHeadersInvalid;
417 switch (line[0]) {
418 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
419 else => {},
420 }
421367
422 var line_it = mem.tokenize(u8, line, ": ");368 pub fn parse(res: *Response, bytes: []const u8) ParseError!void {
423 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;369 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
424 const header_value = line_it.rest();
425 if (std.ascii.eqlIgnoreCase(header_name, "location")) {
426 if (headers.location != null) return error.HttpHeadersInvalid;
427 headers.location = header_value;
428 } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
429 if (headers.content_length != null) return error.HttpHeadersInvalid;
430 headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
431 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
432 // Transfer-Encoding: second, first
433 // Transfer-Encoding: deflate, chunked
434 var iter = mem.splitBackwards(u8, header_value, ",");
435
436 if (iter.next()) |first| {
437 const trimmed = mem.trim(u8, first, " ");
438
439 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
440 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
441 headers.transfer_encoding = te;
442 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
443 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
444 headers.transfer_compression = ce;
445 } else {
446 return error.HttpTransferEncodingUnsupported;
447 }
448 }
449370
450 if (iter.next()) |second| {371 const first_line = it.next() orelse return error.HttpHeadersInvalid;
451 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;372 if (first_line.len < 12)
373 return error.ShortHttpStatusLine;
452374
453 const trimmed = mem.trim(u8, second, " ");375 const version: http.Version = switch (int64(first_line[0..8])) {
376 int64("HTTP/1.0") => .@"HTTP/1.0",
377 int64("HTTP/1.1") => .@"HTTP/1.1",
378 else => return error.BadHttpVersion,
379 };
380 if (first_line[8] != ' ') return error.HttpHeadersInvalid;
381 const status = @intToEnum(http.Status, parseInt3(first_line[9..12].*));
382 const reason = mem.trimLeft(u8, first_line[12..], " ");
383
384 res.version = version;
385 res.status = status;
386 res.reason = reason;
387
388 while (it.next()) |line| {
389 if (line.len == 0) return error.HttpHeadersInvalid;
390 switch (line[0]) {
391 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
392 else => {},
393 }
454394
455 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {395 var line_it = mem.tokenize(u8, line, ": ");
456 headers.transfer_compression = ce;396 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
457 } else {397 const header_value = line_it.rest();
458 return error.HttpTransferEncodingUnsupported;398
459 }399 try res.headers.append(header_name, header_value);
400
401 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
402 if (res.content_length != null) return error.HttpHeadersInvalid;
403 res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
404 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
405 // Transfer-Encoding: second, first
406 // Transfer-Encoding: deflate, chunked
407 var iter = mem.splitBackwards(u8, header_value, ",");
408
409 if (iter.next()) |first| {
410 const trimmed = mem.trim(u8, first, " ");
411
412 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
413 if (res.transfer_encoding != null) return error.HttpHeadersInvalid;
414 res.transfer_encoding = te;
415 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
416 if (res.transfer_compression != null) return error.HttpHeadersInvalid;
417 res.transfer_compression = ce;
418 } else {
419 return error.HttpTransferEncodingUnsupported;
460 }420 }
421 }
461422
462 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;423 if (iter.next()) |second| {
463 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {424 if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
464 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
465425
466 const trimmed = mem.trim(u8, header_value, " ");426 const trimmed = mem.trim(u8, second, " ");
467427
468 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {428 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
469 headers.transfer_compression = ce;429 res.transfer_compression = ce;
470 } else {430 } else {
471 return error.HttpTransferEncodingUnsupported;431 return error.HttpTransferEncodingUnsupported;
472 }432 }
473 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
474 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
475 headers.connection = .keep_alive;
476 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
477 headers.connection = .close;
478 } else {
479 return error.HttpConnectionHeaderUnsupported;
480 }
481 } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) {
482 headers.upgrade = header_value;
483 }433 }
484 }
485434
486 return headers;435 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
487 }436 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
437 if (res.transfer_compression != null) return error.HttpHeadersInvalid;
488438
489 inline fn int64(array: *const [8]u8) u64 {439 const trimmed = mem.trim(u8, header_value, " ");
490 return @bitCast(u64, array.*);
491 }
492440
493 fn parseInt3(nnn: @Vector(3, u8)) u10 {441 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
494 const zero: @Vector(3, u8) = .{ '0', '0', '0' };442 res.transfer_compression = ce;
495 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };443 } else {
496 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);444 return error.HttpTransferEncodingUnsupported;
445 }
446 }
497 }447 }
448 }
498449
499 test parseInt3 {450 inline fn int64(array: *const [8]u8) u64 {
500 const expectEqual = testing.expectEqual;451 return @bitCast(u64, array.*);
501 try expectEqual(@as(u10, 0), parseInt3("000".*));452 }
502 try expectEqual(@as(u10, 418), parseInt3("418".*));
503 try expectEqual(@as(u10, 999), parseInt3("999".*));
504 }
505 };
506453
507 headers: Headers = undefined,454 fn parseInt3(nnn: @Vector(3, u8)) u10 {
455 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
456 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
457 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
458 }
459
460 test parseInt3 {
461 const expectEqual = testing.expectEqual;
462 try expectEqual(@as(u10, 0), parseInt3("000".*));
463 try expectEqual(@as(u10, 418), parseInt3("418".*));
464 try expectEqual(@as(u10, 999), parseInt3("999".*));
465 }
466
467 version: http.Version,
468 status: http.Status,
469 reason: []const u8,
470
471 content_length: ?u64 = null,
472 transfer_encoding: ?http.TransferEncoding = null,
473 transfer_compression: ?http.ContentEncoding = null,
474
475 headers: http.Headers,
508 parser: proto.HeadersParser,476 parser: proto.HeadersParser,
509 compression: Compression = .none,477 compression: Compression = .none,
510 skip: bool = false,478 skip: bool = false,
...@@ -514,22 +482,14 @@ pub const Response = struct {...@@ -514,22 +482,14 @@ pub const Response = struct {
514///482///
515/// Order of operations: request[ -> write -> finish] -> do -> read483/// Order of operations: request[ -> write -> finish] -> do -> read
516pub const Request = struct {484pub const Request = struct {
517 pub const Headers = struct {
518 version: http.Version = .@"HTTP/1.1",
519 method: http.Method = .GET,
520 user_agent: []const u8 = "zig (std.http)",
521 connection: http.Connection = .keep_alive,
522 transfer_encoding: RequestTransfer = .none,
523
524 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
525 };
526
527 uri: Uri,485 uri: Uri,
528 client: *Client,486 client: *Client,
529 connection: *ConnectionPool.Node,487 connection: *ConnectionPool.Node,
530 /// These are stored in Request so that they are available when following488
531 /// redirects.489 method: http.Method,
532 headers: Headers,490 version: http.Version = .@"HTTP/1.1",
491 headers: http.Headers,
492 transfer_encoding: RequestTransfer = .none,
533493
534 redirects_left: u32,494 redirects_left: u32,
535 handle_redirects: bool,495 handle_redirects: bool,
...@@ -549,80 +509,104 @@ pub const Request = struct {...@@ -549,80 +509,104 @@ pub const Request = struct {
549 }509 }
550510
551 if (req.response.parser.header_bytes_owned) {511 if (req.response.parser.header_bytes_owned) {
512 req.response.headers.deinit();
552 req.response.parser.header_bytes.deinit(req.client.allocator);513 req.response.parser.header_bytes.deinit(req.client.allocator);
553 }514 }
554515
555 if (!req.response.parser.done) {516 if (!req.response.parser.done) {
556 // If the response wasn't fully read, then we need to close the connection.517 // If the response wasn't fully read, then we need to close the connection.
557 req.connection.data.closing = true;518 req.connection.data.closing = true;
558 req.client.connection_pool.release(req.client, req.connection);
559 }519 }
560520
521 req.client.connection_pool.release(req.client, req.connection);
522
561 req.arena.deinit();523 req.arena.deinit();
562 req.* = undefined;524 req.* = undefined;
563 }525 }
564526
565 pub fn start(req: *Request, uri: Uri, headers: Headers) !void {527 pub const StartError = BufferedConnection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };
528
529 /// Send the request to the server.
530 pub fn start(req: *Request) StartError!void {
566 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());531 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
567 const w = buffered.writer();532 const w = buffered.writer();
568533
569 const escaped_path = try Uri.escapePath(req.client.allocator, uri.path);534 try w.writeAll(@tagName(req.method));
570 defer req.client.allocator.free(escaped_path);535 try w.writeByte(' ');
571
572 const escaped_query = if (uri.query) |q| try Uri.escapeQuery(req.client.allocator, q) else null;
573 defer if (escaped_query) |q| req.client.allocator.free(q);
574536
575 const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(req.client.allocator, f) else null;537 if (req.method == .CONNECT) {
576 defer if (escaped_fragment) |f| req.client.allocator.free(f);538 try w.writeAll(req.uri.host.?);
539 try w.writeByte(':');
540 try w.print("{}", .{req.uri.port.?});
541 } else if (req.connection.data.proxied) {
542 // proxied connections require the full uri
543 try w.print("{+/}", .{req.uri});
544 } else {
545 try w.print("{/}", .{req.uri});
546 }
577547
578 try w.writeAll(@tagName(headers.method));
579 try w.writeByte(' ');548 try w.writeByte(' ');
580 if (escaped_path.len == 0) {549 try w.writeAll(@tagName(req.version));
581 try w.writeByte('/');550 try w.writeAll("\r\n");
582 } else {551
583 try w.writeAll(escaped_path);552 if (!req.headers.contains("host")) {
553 try w.writeAll("Host: ");
554 try w.writeAll(req.uri.host.?);
555 try w.writeAll("\r\n");
584 }556 }
585 if (escaped_query) |q| {557
586 try w.writeByte('?');558 if (!req.headers.contains("user-agent")) {
587 try w.writeAll(q);559 try w.writeAll("User-Agent: zig/");
560 try w.writeAll(@import("builtin").zig_version_string);
561 try w.writeAll(" (std.http)\r\n");
588 }562 }
589 if (escaped_fragment) |f| {563
590 try w.writeByte('#');564 if (!req.headers.contains("connection")) {
591 try w.writeAll(f);565 try w.writeAll("Connection: keep-alive\r\n");
592 }566 }
593 try w.writeByte(' ');567
594 try w.writeAll(@tagName(headers.version));568 if (!req.headers.contains("accept-encoding")) {
595 try w.writeAll("\r\nHost: ");569 try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");
596 try w.writeAll(uri.host.?);
597 try w.writeAll("\r\nUser-Agent: ");
598 try w.writeAll(headers.user_agent);
599 if (headers.connection == .close) {
600 try w.writeAll("\r\nConnection: close");
601 } else {
602 try w.writeAll("\r\nConnection: keep-alive");
603 }570 }
604 try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd");
605 try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers.
606571
607 switch (headers.transfer_encoding) {572 if (!req.headers.contains("te")) {
608 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),573 try w.writeAll("TE: gzip, deflate, trailers\r\n");
609 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
610 .none => {},
611 }574 }
612575
613 for (headers.custom) |header| {576 const has_transfer_encoding = req.headers.contains("transfer-encoding");
614 try w.writeAll("\r\n");577 const has_content_length = req.headers.contains("content-length");
615 try w.writeAll(header.name);578
616 try w.writeAll(": ");579 if (!has_transfer_encoding and !has_content_length) {
617 try w.writeAll(header.value);580 switch (req.transfer_encoding) {
581 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
582 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
583 .none => {},
584 }
585 } else {
586 if (has_content_length) {
587 const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
588
589 req.transfer_encoding = .{ .content_length = content_length };
590 } else if (has_transfer_encoding) {
591 const transfer_encoding = req.headers.getFirstValue("content-length").?;
592 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
593 req.transfer_encoding = .chunked;
594 } else {
595 return error.UnsupportedTransferEncoding;
596 }
597 } else {
598 req.transfer_encoding = .none;
599 }
618 }600 }
619601
620 try w.writeAll("\r\n\r\n");602 try w.print("{}", .{req.headers});
603
604 try w.writeAll("\r\n");
621605
622 try buffered.flush();606 try buffered.flush();
623 }607 }
624608
625 pub const TransferReadError = proto.HeadersParser.ReadError || error{ReadFailed};609 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
626610
627 pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead);611 pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead);
628612
...@@ -635,10 +619,7 @@ pub const Request = struct {...@@ -635,10 +619,7 @@ pub const Request = struct {
635619
636 var index: usize = 0;620 var index: usize = 0;
637 while (index == 0) {621 while (index == 0) {
638 const amt = req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip) catch |err| {622 const amt = try req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip);
639 req.client.last_error = .{ .read = err };
640 return error.ReadFailed;
641 };
642 if (amt == 0 and req.response.parser.done) break;623 if (amt == 0 and req.response.parser.done) break;
643 index += amt;624 index += amt;
644 }625 }
...@@ -646,7 +627,7 @@ pub const Request = struct {...@@ -646,7 +627,7 @@ pub const Request = struct {
646 return index;627 return index;
647 }628 }
648629
649 pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed };630 pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported };
650631
651 /// Waits for a response from the server and parses any headers that are sent.632 /// Waits for a response from the server and parses any headers that are sent.
652 /// This function will block until the final response is received.633 /// This function will block until the final response is received.
...@@ -656,10 +637,7 @@ pub const Request = struct {...@@ -656,10 +637,7 @@ pub const Request = struct {
656 pub fn do(req: *Request) DoError!void {637 pub fn do(req: *Request) DoError!void {
657 while (true) { // handle redirects638 while (true) { // handle redirects
658 while (true) { // read headers639 while (true) { // read headers
659 req.connection.data.buffered.fill() catch |err| {640 try req.connection.data.buffered.fill();
660 req.client.last_error = .{ .read = err };
661 return error.ReadFailed;
662 };
663641
664 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());642 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
665 req.connection.data.buffered.clear(@intCast(u16, nchecked));643 req.connection.data.buffered.clear(@intCast(u16, nchecked));
...@@ -667,27 +645,39 @@ pub const Request = struct {...@@ -667,27 +645,39 @@ pub const Request = struct {
667 if (req.response.parser.state.isContent()) break;645 if (req.response.parser.state.isContent()) break;
668 }646 }
669647
670 req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items);648 req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false };
649 try req.response.parse(req.response.parser.header_bytes.items);
650
651 if (req.response.status == .switching_protocols) {
652 req.connection.data.closing = false;
653 req.response.parser.done = true;
654 }
671655
672 if (req.response.headers.status == .switching_protocols) {656 if (req.method == .CONNECT and req.response.status == .ok) {
673 req.connection.data.closing = false;657 req.connection.data.closing = false;
658 req.connection.data.proxied = true;
674 req.response.parser.done = true;659 req.response.parser.done = true;
675 }660 }
676661
677 if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) {662 const req_connection = req.headers.getFirstValue("connection");
663 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
664
665 const res_connection = req.response.headers.getFirstValue("connection");
666 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
667 if (req_keepalive and res_keepalive) {
678 req.connection.data.closing = false;668 req.connection.data.closing = false;
679 } else {669 } else {
680 req.connection.data.closing = true;670 req.connection.data.closing = true;
681 }671 }
682672
683 if (req.response.headers.transfer_encoding) |te| {673 if (req.response.transfer_encoding) |te| {
684 switch (te) {674 switch (te) {
685 .chunked => {675 .chunked => {
686 req.response.parser.next_chunk_length = 0;676 req.response.parser.next_chunk_length = 0;
687 req.response.parser.state = .chunk_head_size;677 req.response.parser.state = .chunk_head_size;
688 },678 },
689 }679 }
690 } else if (req.response.headers.content_length) |cl| {680 } else if (req.response.content_length) |cl| {
691 req.response.parser.next_chunk_length = cl;681 req.response.parser.next_chunk_length = cl;
692682
693 if (cl == 0) req.response.parser.done = true;683 if (cl == 0) req.response.parser.done = true;
...@@ -695,7 +685,7 @@ pub const Request = struct {...@@ -695,7 +685,7 @@ pub const Request = struct {
695 req.response.parser.done = true;685 req.response.parser.done = true;
696 }686 }
697687
698 if (req.response.headers.status.class() == .redirect and req.handle_redirects) {688 if (req.response.status.class() == .redirect and req.handle_redirects) {
699 req.response.skip = true;689 req.response.skip = true;
700690
701 const empty = @as([*]u8, undefined)[0..0];691 const empty = @as([*]u8, undefined)[0..0];
...@@ -703,7 +693,7 @@ pub const Request = struct {...@@ -703,7 +693,7 @@ pub const Request = struct {
703693
704 if (req.redirects_left == 0) return error.TooManyHttpRedirects;694 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
705695
706 const location = req.response.headers.location orelse696 const location = req.response.headers.getFirstValue("location") orelse
707 return error.HttpRedirectMissingLocation;697 return error.HttpRedirectMissingLocation;
708 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);698 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
709699
...@@ -714,7 +704,8 @@ pub const Request = struct {...@@ -714,7 +704,8 @@ pub const Request = struct {
714 req.arena.deinit();704 req.arena.deinit();
715 req.arena = new_arena;705 req.arena = new_arena;
716706
717 const new_req = try req.client.request(resolved_url, req.headers, .{707 const new_req = try req.client.request(req.method, resolved_url, req.headers, .{
708 .version = req.version,
718 .max_redirects = req.redirects_left - 1,709 .max_redirects = req.redirects_left - 1,
719 .header_strategy = if (req.response.parser.header_bytes_owned) .{710 .header_strategy = if (req.response.parser.header_bytes_owned) .{
720 .dynamic = req.response.parser.max_header_bytes,711 .dynamic = req.response.parser.max_header_bytes,
...@@ -727,19 +718,13 @@ pub const Request = struct {...@@ -727,19 +718,13 @@ pub const Request = struct {
727 } else {718 } else {
728 req.response.skip = false;719 req.response.skip = false;
729 if (!req.response.parser.done) {720 if (!req.response.parser.done) {
730 if (req.response.headers.transfer_compression) |tc| switch (tc) {721 if (req.response.transfer_compression) |tc| switch (tc) {
731 .compress => return error.CompressionNotSupported,722 .compress => return error.CompressionNotSupported,
732 .deflate => req.response.compression = .{723 .deflate => req.response.compression = .{
733 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch |err| {724 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
734 req.client.last_error = .{ .zlib_init = err };
735 return error.CompressionInitializationFailed;
736 },
737 },725 },
738 .gzip => req.response.compression = .{726 .gzip => req.response.compression = .{
739 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch |err| {727 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
740 req.client.last_error = .{ .gzip_init = err };
741 return error.CompressionInitializationFailed;
742 },
743 },728 },
744 .zstd => req.response.compression = .{729 .zstd => req.response.compression = .{
745 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),730 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
...@@ -752,7 +737,7 @@ pub const Request = struct {...@@ -752,7 +737,7 @@ pub const Request = struct {
752 }737 }
753 }738 }
754739
755 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError;740 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers };
756741
757 pub const Reader = std.io.Reader(*Request, ReadError, read);742 pub const Reader = std.io.Reader(*Request, ReadError, read);
758743
...@@ -762,57 +747,47 @@ pub const Request = struct {...@@ -762,57 +747,47 @@ pub const Request = struct {
762747
763 /// Reads data from the response body. Must be called after `do`.748 /// Reads data from the response body. Must be called after `do`.
764 pub fn read(req: *Request, buffer: []u8) ReadError!usize {749 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
765 while (true) {750 const out_index = switch (req.response.compression) {
766 const out_index = switch (req.response.compression) {751 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
767 .deflate => |*deflate| deflate.read(buffer) catch |err| {752 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
768 req.client.last_error = .{ .decompress = err };753 .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
769 err catch {};754 else => try req.transferRead(buffer),
770 return error.ReadFailed;755 };
771 },
772 .gzip => |*gzip| gzip.read(buffer) catch |err| {
773 req.client.last_error = .{ .decompress = err };
774 err catch {};
775 return error.ReadFailed;
776 },
777 .zstd => |*zstd| zstd.read(buffer) catch |err| {
778 req.client.last_error = .{ .decompress = err };
779 err catch {};
780 return error.ReadFailed;
781 },
782 else => try req.transferRead(buffer),
783 };
784
785 if (out_index == 0) {
786 while (!req.response.parser.state.isContent()) { // read trailing headers
787 req.connection.data.buffered.fill() catch |err| {
788 req.client.last_error = .{ .read = err };
789 return error.ReadFailed;
790 };
791756
792 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());757 if (out_index == 0) {
793 req.connection.data.buffered.clear(@intCast(u16, nchecked));758 const has_trail = !req.response.parser.state.isContent();
794 }759
760 while (!req.response.parser.state.isContent()) { // read trailing headers
761 try req.connection.data.buffered.fill();
762
763 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
764 req.connection.data.buffered.clear(@intCast(u16, nchecked));
795 }765 }
796766
797 return out_index;767 if (has_trail) {
768 req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false };
769
770 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
771 // This will *only* fail for a malformed trailer.
772 req.response.parse(req.response.parser.header_bytes.items) catch return error.InvalidTrailers;
773 }
798 }774 }
775
776 return out_index;
799 }777 }
800778
801 /// Reads data from the response body. Must be called after `do`.779 /// Reads data from the response body. Must be called after `do`.
802 pub fn readAll(req: *Request, buffer: []u8) !usize {780 pub fn readAll(req: *Request, buffer: []u8) !usize {
803 var index: usize = 0;781 var index: usize = 0;
804 while (index < buffer.len) {782 while (index < buffer.len) {
805 const amt = read(req, buffer[index..]) catch |err| {783 const amt = try read(req, buffer[index..]);
806 req.client.last_error = .{ .read = err };
807 return error.ReadFailed;
808 };
809 if (amt == 0) break;784 if (amt == 0) break;
810 index += amt;785 index += amt;
811 }786 }
812 return index;787 return index;
813 }788 }
814789
815 pub const WriteError = error{ WriteFailed, NotWriteable, MessageTooLong };790 pub const WriteError = BufferedConnection.WriteError || error{ NotWriteable, MessageTooLong };
816791
817 pub const Writer = std.io.Writer(*Request, WriteError, write);792 pub const Writer = std.io.Writer(*Request, WriteError, write);
818793
...@@ -824,28 +799,16 @@ pub const Request = struct {...@@ -824,28 +799,16 @@ pub const Request = struct {
824 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {799 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
825 switch (req.headers.transfer_encoding) {800 switch (req.headers.transfer_encoding) {
826 .chunked => {801 .chunked => {
827 req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}) catch |err| {802 try req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len});
828 req.client.last_error = .{ .write = err };803 try req.connection.data.conn.writeAll(bytes);
829 return error.WriteFailed;804 try req.connection.data.conn.writeAll("\r\n");
830 };
831 req.connection.data.conn.writeAll(bytes) catch |err| {
832 req.client.last_error = .{ .write = err };
833 return error.WriteFailed;
834 };
835 req.connection.data.conn.writeAll("\r\n") catch |err| {
836 req.client.last_error = .{ .write = err };
837 return error.WriteFailed;
838 };
839805
840 return bytes.len;806 return bytes.len;
841 },807 },
842 .content_length => |*len| {808 .content_length => |*len| {
843 if (len.* < bytes.len) return error.MessageTooLong;809 if (len.* < bytes.len) return error.MessageTooLong;
844810
845 const amt = req.connection.data.conn.write(bytes) catch |err| {811 const amt = try req.connection.data.conn.write(bytes);
846 req.client.last_error = .{ .write = err };
847 return error.WriteFailed;
848 };
849 len.* -= amt;812 len.* -= amt;
850 return amt;813 return amt;
851 },814 },
...@@ -853,19 +816,39 @@ pub const Request = struct {...@@ -853,19 +816,39 @@ pub const Request = struct {
853 }816 }
854 }817 }
855818
819 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {
820 var index: usize = 0;
821 while (index < bytes.len) {
822 index += try write(req, bytes[index..]);
823 }
824 }
825
826 pub const FinishError = WriteError || error{MessageNotCompleted};
827
856 /// Finish the body of a request. This notifies the server that you have no more data to send.828 /// Finish the body of a request. This notifies the server that you have no more data to send.
857 pub fn finish(req: *Request) !void {829 pub fn finish(req: *Request) FinishError!void {
858 switch (req.headers.transfer_encoding) {830 switch (req.transfer_encoding) {
859 .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| {831 .chunked => try req.connection.data.conn.writeAll("0\r\n\r\n"),
860 req.client.last_error = .{ .write = err };
861 return error.WriteFailed;
862 },
863 .content_length => |len| if (len != 0) return error.MessageNotCompleted,832 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
864 .none => {},833 .none => {},
865 }834 }
866 }835 }
867};836};
868837
838pub const HttpProxy = struct {
839 pub const ProxyAuthentication = union(enum) {
840 basic: []const u8,
841 custom: []const u8,
842 };
843
844 protocol: Connection.Protocol,
845 host: []const u8,
846 port: ?u16 = null,
847
848 /// The value for the Proxy-Authorization header.
849 auth: ?ProxyAuthentication = null,
850};
851
869/// Release all associated resources with the client.852/// Release all associated resources with the client.
870/// TODO: currently leaks all request allocated data853/// TODO: currently leaks all request allocated data
871pub fn deinit(client: *Client) void {854pub fn deinit(client: *Client) void {
...@@ -875,11 +858,11 @@ pub fn deinit(client: *Client) void {...@@ -875,11 +858,11 @@ pub fn deinit(client: *Client) void {
875 client.* = undefined;858 client.* = undefined;
876}859}
877860
878pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed };861pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed };
879862
880/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.863/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
881/// This function is threadsafe.864/// This function is threadsafe.
882pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {865pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node {
883 if (client.connection_pool.findConnection(.{866 if (client.connection_pool.findConnection(.{
884 .host = host,867 .host = host,
885 .port = port,868 .port = port,
...@@ -891,9 +874,16 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -891,9 +874,16 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
891 errdefer client.allocator.destroy(conn);874 errdefer client.allocator.destroy(conn);
892 conn.* = .{ .data = undefined };875 conn.* = .{ .data = undefined };
893876
894 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| {877 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {
895 client.last_error = .{ .connect = err };878 error.ConnectionRefused => return error.ConnectionRefused,
896 return error.ConnectionFailed;879 error.NetworkUnreachable => return error.NetworkUnreachable,
880 error.ConnectionTimedOut => return error.ConnectionTimedOut,
881 error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
882 error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure,
883 error.NameServerFailure => return error.NameServerFailure,
884 error.UnknownHostName => return error.UnknownHostName,
885 error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses,
886 else => return error.UnexpectedConnectFailure,
897 };887 };
898 errdefer stream.close();888 errdefer stream.close();
899889
...@@ -914,10 +904,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -914,10 +904,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
914 conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client);904 conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client);
915 errdefer client.allocator.destroy(conn.data.buffered.conn.tls_client);905 errdefer client.allocator.destroy(conn.data.buffered.conn.tls_client);
916906
917 conn.data.buffered.conn.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch |err| {907 conn.data.buffered.conn.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;
918 client.last_error = .{ .tls = err };
919 return error.TlsInitializationFailed;
920 };
921 // This is appropriate for HTTPS because the HTTP headers contain908 // This is appropriate for HTTPS because the HTTP headers contain
922 // the content length which is used to detect truncation attacks.909 // the content length which is used to detect truncation attacks.
923 conn.data.buffered.conn.tls_client.allow_truncation_attacks = true;910 conn.data.buffered.conn.tls_client.allow_truncation_attacks = true;
...@@ -929,19 +916,51 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio...@@ -929,19 +916,51 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
929 return conn;916 return conn;
930}917}
931918
932pub const RequestError = ConnectError || error{919// Prevents a dependency loop in request()
920const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused };
921pub const ConnectError = ConnectErrorPartial || RequestError;
922
923pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node {
924 if (client.connection_pool.findConnection(.{
925 .host = host,
926 .port = port,
927 .is_tls = protocol == .tls,
928 })) |node|
929 return node;
930
931 if (client.proxy) |proxy| {
932 const proxy_port: u16 = proxy.port orelse switch (proxy.protocol) {
933 .plain => 80,
934 .tls => 443,
935 };
936
937 const conn = try client.connectUnproxied(proxy.host, proxy_port, proxy.protocol);
938 conn.data.proxied = true;
939
940 return conn;
941 } else {
942 return client.connectUnproxied(host, port, protocol);
943 }
944}
945
946pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || BufferedConnection.WriteError || error{
933 UnsupportedUrlScheme,947 UnsupportedUrlScheme,
934 UriMissingHost,948 UriMissingHost,
935949
936 CertificateAuthorityBundleFailed,950 CertificateBundleLoadFailure,
937 WriteFailed,951 UnsupportedTransferEncoding,
938};952};
939953
940pub const Options = struct {954pub const Options = struct {
955 version: http.Version = .@"HTTP/1.1",
956
941 handle_redirects: bool = true,957 handle_redirects: bool = true,
942 max_redirects: u32 = 3,958 max_redirects: u32 = 3,
943 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },959 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
944960
961 /// Must be an already acquired connection.
962 connection: ?*ConnectionPool.Node = null,
963
945 pub const HeaderStrategy = union(enum) {964 pub const HeaderStrategy = union(enum) {
946 /// In this case, the client's Allocator will be used to store the965 /// In this case, the client's Allocator will be used to store the
947 /// entire HTTP header. This value is the maximum total size of966 /// entire HTTP header. This value is the maximum total size of
...@@ -965,7 +984,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{...@@ -965,7 +984,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
965984
966/// Form and send a http request to a server.985/// Form and send a http request to a server.
967/// This function is threadsafe.986/// This function is threadsafe.
968pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request {987pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: Options) RequestError!Request {
969 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;988 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
970989
971 const port: u16 = uri.port orelse switch (protocol) {990 const port: u16 = uri.port orelse switch (protocol) {
...@@ -980,22 +999,27 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt...@@ -980,22 +999,27 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
980 defer client.ca_bundle_mutex.unlock();999 defer client.ca_bundle_mutex.unlock();
9811000
982 if (client.next_https_rescan_certs) {1001 if (client.next_https_rescan_certs) {
983 client.ca_bundle.rescan(client.allocator) catch |err| {1002 client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure;
984 client.last_error = .{ .ca_bundle = err };
985 return error.CertificateAuthorityBundleFailed;
986 };
987 @atomicStore(bool, &client.next_https_rescan_certs, false, .Release);1003 @atomicStore(bool, &client.next_https_rescan_certs, false, .Release);
988 }1004 }
989 }1005 }
9901006
1007 const conn = options.connection orelse try client.connect(host, port, protocol);
1008
991 var req: Request = .{1009 var req: Request = .{
992 .uri = uri,1010 .uri = uri,
993 .client = client,1011 .client = client,
994 .connection = try client.connect(host, port, protocol),1012 .connection = conn,
995 .headers = headers,1013 .headers = headers,
1014 .method = method,
1015 .version = options.version,
996 .redirects_left = options.max_redirects,1016 .redirects_left = options.max_redirects,
997 .handle_redirects = options.handle_redirects,1017 .handle_redirects = options.handle_redirects,
998 .response = .{1018 .response = .{
1019 .status = undefined,
1020 .reason = undefined,
1021 .version = undefined,
1022 .headers = undefined,
999 .parser = switch (options.header_strategy) {1023 .parser = switch (options.header_strategy) {
1000 .dynamic => |max| proto.HeadersParser.initDynamic(max),1024 .dynamic => |max| proto.HeadersParser.initDynamic(max),
1001 .static => |buf| proto.HeadersParser.initStatic(buf),1025 .static => |buf| proto.HeadersParser.initStatic(buf),
...@@ -1007,14 +1031,6 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt...@@ -1007,14 +1031,6 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
10071031
1008 req.arena = std.heap.ArenaAllocator.init(client.allocator);1032 req.arena = std.heap.ArenaAllocator.init(client.allocator);
10091033
1010 req.start(uri, headers) catch |err| {
1011 if (err == error.OutOfMemory) return error.OutOfMemory;
1012 const err_casted = @errSetCast(BufferedConnection.WriteError, err);
1013
1014 client.last_error = .{ .write = err_casted };
1015 return error.WriteFailed;
1016 };
1017
1018 return req;1034 return req;
1019}1035}
10201036
lib/std/http/Headers.zig created+386
...@@ -0,0 +1,386 @@
1const std = @import("../std.zig");
2
3const Allocator = std.mem.Allocator;
4
5const testing = std.testing;
6const ascii = std.ascii;
7const assert = std.debug.assert;
8
9pub const HeaderList = std.ArrayListUnmanaged(Field);
10pub const HeaderIndexList = std.ArrayListUnmanaged(usize);
11pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage);
12
13pub const CaseInsensitiveStringContext = struct {
14 pub fn hash(self: @This(), s: []const u8) u64 {
15 _ = self;
16 var buf: [64]u8 = undefined;
17 var i: u8 = 0;
18
19 var h = std.hash.Wyhash.init(0);
20 while (i < s.len) : (i += 64) {
21 const left = @min(64, s.len - i);
22 const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
23 h.update(ret);
24 }
25
26 return h.final();
27 }
28
29 pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
30 _ = self;
31 return ascii.eqlIgnoreCase(a, b);
32 }
33};
34
35pub const Field = struct {
36 name: []const u8,
37 value: []const u8,
38
39 pub fn modify(entry: *Field, allocator: Allocator, new_value: []const u8) !void {
40 if (entry.value.len <= new_value.len) {
41 std.mem.copy(u8, @constCast(entry.value), new_value);
42 } else {
43 allocator.free(entry.value);
44
45 entry.value = try allocator.dupe(u8, new_value);
46 }
47 }
48
49 fn lessThan(ctx: void, a: Field, b: Field) bool {
50 _ = ctx;
51 if (a.name.ptr == b.name.ptr) return false;
52
53 return ascii.lessThanIgnoreCase(a.name, b.name);
54 }
55};
56
57pub const Headers = struct {
58 allocator: Allocator,
59 list: HeaderList = .{},
60 index: HeaderIndex = .{},
61
62 /// When this is false, names and values will not be duplicated.
63 /// Use with caution.
64 owned: bool = true,
65
66 pub fn init(allocator: Allocator) Headers {
67 return .{ .allocator = allocator };
68 }
69
70 pub fn deinit(headers: *Headers) void {
71 var it = headers.index.iterator();
72 while (it.next()) |entry| {
73 entry.value_ptr.deinit(headers.allocator);
74
75 if (headers.owned) headers.allocator.free(entry.key_ptr.*);
76 }
77
78 for (headers.list.items) |entry| {
79 if (headers.owned) headers.allocator.free(entry.value);
80 }
81
82 headers.index.deinit(headers.allocator);
83 headers.list.deinit(headers.allocator);
84
85 headers.* = undefined;
86 }
87
88 /// Appends a header to the list. Both name and value are copied.
89 pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
90 const n = headers.list.items.len;
91
92 const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value;
93 errdefer if (headers.owned) headers.allocator.free(value_duped);
94
95 var entry = Field{ .name = undefined, .value = value_duped };
96
97 if (headers.index.getEntry(name)) |kv| {
98 entry.name = kv.key_ptr.*;
99 try kv.value_ptr.append(headers.allocator, n);
100 } else {
101 const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name;
102 errdefer if (headers.owned) headers.allocator.free(name_duped);
103
104 entry.name = name_duped;
105
106 var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1);
107 errdefer new_index.deinit(headers.allocator);
108
109 new_index.appendAssumeCapacity(n);
110 try headers.index.put(headers.allocator, name_duped, new_index);
111 }
112
113 try headers.list.append(headers.allocator, entry);
114 }
115
116 pub fn contains(headers: Headers, name: []const u8) bool {
117 return headers.index.contains(name);
118 }
119
120 pub fn delete(headers: *Headers, name: []const u8) bool {
121 if (headers.index.fetchRemove(name)) |kv| {
122 var index = kv.value;
123
124 // iterate backwards
125 var i = index.items.len;
126 while (i > 0) {
127 i -= 1;
128 const data_index = index.items[i];
129 const removed = headers.list.orderedRemove(data_index);
130
131 assert(ascii.eqlIgnoreCase(removed.name, name)); // ensure the index hasn't been corrupted
132 if (headers.owned) headers.allocator.free(removed.value);
133 }
134
135 if (headers.owned) headers.allocator.free(kv.key);
136 index.deinit(headers.allocator);
137 headers.rebuildIndex();
138
139 return true;
140 } else {
141 return false;
142 }
143 }
144
145 /// Returns the index of the first occurrence of a header with the given name.
146 pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
147 const index = headers.index.get(name) orelse return null;
148
149 return index.items[0];
150 }
151
152 /// Returns a list of indices containing headers with the given name.
153 pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
154 const index = headers.index.get(name) orelse return null;
155
156 return index.items;
157 }
158
159 /// Returns the entry of the first occurrence of a header with the given name.
160 pub fn getFirstEntry(headers: Headers, name: []const u8) ?Field {
161 const first_index = headers.firstIndexOf(name) orelse return null;
162
163 return headers.list.items[first_index];
164 }
165
166 /// Returns a slice containing each header with the given name.
167 /// The caller owns the returned slice, but NOT the values in the slice.
168 pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const Field {
169 const indices = headers.getIndices(name) orelse return null;
170
171 const buf = try allocator.alloc(Field, indices.len);
172 for (indices, 0..) |idx, n| {
173 buf[n] = headers.list.items[idx];
174 }
175
176 return buf;
177 }
178
179 /// Returns the value in the entry of the first occurrence of a header with the given name.
180 pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 {
181 const first_index = headers.firstIndexOf(name) orelse return null;
182
183 return headers.list.items[first_index].value;
184 }
185
186 /// Returns a slice containing the value of each header with the given name.
187 /// The caller owns the returned slice, but NOT the values in the slice.
188 pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 {
189 const indices = headers.getIndices(name) orelse return null;
190
191 const buf = try allocator.alloc([]const u8, indices.len);
192 for (indices, 0..) |idx, n| {
193 buf[n] = headers.list.items[idx].value;
194 }
195
196 return buf;
197 }
198
199 fn rebuildIndex(headers: *Headers) void {
200 // clear out the indexes
201 var it = headers.index.iterator();
202 while (it.next()) |entry| {
203 entry.value_ptr.shrinkRetainingCapacity(0);
204 }
205
206 // fill up indexes again; we know capacity is fine from before
207 for (headers.list.items, 0..) |entry, i| {
208 headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
209 }
210 }
211
212 /// Sorts the headers in lexicographical order.
213 pub fn sort(headers: *Headers) void {
214 std.sort.sort(Field, headers.list.items, {}, Field.lessThan);
215 headers.rebuildIndex();
216 }
217
218 /// Writes the headers to the given stream.
219 pub fn format(
220 headers: Headers,
221 comptime fmt: []const u8,
222 options: std.fmt.FormatOptions,
223 out_stream: anytype,
224 ) !void {
225 _ = fmt;
226 _ = options;
227
228 for (headers.list.items) |entry| {
229 if (entry.value.len == 0) continue;
230
231 try out_stream.writeAll(entry.name);
232 try out_stream.writeAll(": ");
233 try out_stream.writeAll(entry.value);
234 try out_stream.writeAll("\r\n");
235 }
236 }
237
238 /// Writes all of the headers with the given name to the given stream, separated by commas.
239 ///
240 /// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2
241 pub fn formatCommaSeparated(
242 headers: Headers,
243 name: []const u8,
244 out_stream: anytype,
245 ) !void {
246 const indices = headers.getIndices(name) orelse return;
247
248 try out_stream.writeAll(name);
249 try out_stream.writeAll(": ");
250
251 for (indices, 0..) |idx, n| {
252 if (n != 0) try out_stream.writeAll(", ");
253 try out_stream.writeAll(headers.list.items[idx].value);
254 }
255
256 try out_stream.writeAll("\r\n");
257 }
258};
259
260test "Headers.append" {
261 var h = Headers{ .allocator = std.testing.allocator };
262 defer h.deinit();
263
264 try h.append("foo", "bar");
265 try h.append("hello", "world");
266
267 try testing.expect(h.contains("Foo"));
268 try testing.expect(!h.contains("Bar"));
269}
270
271test "Headers.delete" {
272 var h = Headers{ .allocator = std.testing.allocator };
273 defer h.deinit();
274
275 try h.append("foo", "bar");
276 try h.append("hello", "world");
277
278 try testing.expect(h.contains("Foo"));
279
280 _ = h.delete("Foo");
281
282 try testing.expect(!h.contains("foo"));
283}
284
285test "Headers consistency" {
286 var h = Headers{ .allocator = std.testing.allocator };
287 defer h.deinit();
288
289 try h.append("foo", "bar");
290 try h.append("hello", "world");
291 _ = h.delete("Foo");
292
293 try h.append("foo", "bar");
294 try h.append("bar", "world");
295 try h.append("foo", "baz");
296 try h.append("baz", "hello");
297
298 try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello"));
299 try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo"));
300 try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar"));
301 try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz"));
302 try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog"));
303
304 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?);
305 try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?);
306 try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?);
307 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?);
308 try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog"));
309
310 try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value);
311 try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value);
312 try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value);
313 try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value);
314
315 const hello_entries = (try h.getEntries(testing.allocator, "hello")).?;
316 defer testing.allocator.free(hello_entries);
317 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
318 .{ .name = "hello", .value = "world" },
319 }), hello_entries);
320
321 const foo_entries = (try h.getEntries(testing.allocator, "foo")).?;
322 defer testing.allocator.free(foo_entries);
323 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
324 .{ .name = "foo", .value = "bar" },
325 .{ .name = "foo", .value = "baz" },
326 }), foo_entries);
327
328 const bar_entries = (try h.getEntries(testing.allocator, "bar")).?;
329 defer testing.allocator.free(bar_entries);
330 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
331 .{ .name = "bar", .value = "world" },
332 }), bar_entries);
333
334 const baz_entries = (try h.getEntries(testing.allocator, "baz")).?;
335 defer testing.allocator.free(baz_entries);
336 try testing.expectEqualDeep(@as([]const Field, &[_]Field{
337 .{ .name = "baz", .value = "hello" },
338 }), baz_entries);
339
340 const pog_entries = (try h.getEntries(testing.allocator, "pog"));
341 try testing.expectEqual(@as(?[]const Field, null), pog_entries);
342
343 try testing.expectEqualStrings("world", h.getFirstValue("hello").?);
344 try testing.expectEqualStrings("bar", h.getFirstValue("foo").?);
345 try testing.expectEqualStrings("world", h.getFirstValue("bar").?);
346 try testing.expectEqualStrings("hello", h.getFirstValue("baz").?);
347 try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog"));
348
349 const hello_values = (try h.getValues(testing.allocator, "hello")).?;
350 defer testing.allocator.free(hello_values);
351 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values);
352
353 const foo_values = (try h.getValues(testing.allocator, "foo")).?;
354 defer testing.allocator.free(foo_values);
355 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values);
356
357 const bar_values = (try h.getValues(testing.allocator, "bar")).?;
358 defer testing.allocator.free(bar_values);
359 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values);
360
361 const baz_values = (try h.getValues(testing.allocator, "baz")).?;
362 defer testing.allocator.free(baz_values);
363 try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values);
364
365 const pog_values = (try h.getValues(testing.allocator, "pog"));
366 try testing.expectEqual(@as(?[]const []const u8, null), pog_values);
367
368 h.sort();
369
370 try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?);
371 try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?);
372 try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?);
373 try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?);
374
375 const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h});
376 defer testing.allocator.free(formatted_values);
377
378 try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values);
379
380 var buf: [128]u8 = undefined;
381 var fbs = std.io.fixedBufferStream(&buf);
382 const writer = fbs.writer();
383
384 try h.formatCommaSeparated("foo", writer);
385 try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten());
386}
lib/std/http/Server.zig+253-195
...@@ -23,21 +23,33 @@ pub const Connection = struct {...@@ -23,21 +23,33 @@ pub const Connection = struct {
2323
24 pub const Protocol = enum { plain };24 pub const Protocol = enum { plain };
2525
26 pub fn read(conn: *Connection, buffer: []u8) !usize {26 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
27 switch (conn.protocol) {27 return switch (conn.protocol) {
28 .plain => return conn.stream.read(buffer),28 .plain => conn.stream.read(buffer),
29 // .tls => return conn.tls_client.read(conn.stream, buffer),29 // .tls => return conn.tls_client.read(conn.stream, buffer),
30 }30 } catch |err| switch (err) {
31 error.ConnectionTimedOut => return error.ConnectionTimedOut,
32 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
33 else => return error.UnexpectedReadFailure,
34 };
31 }35 }
3236
33 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {37 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
34 switch (conn.protocol) {38 return switch (conn.protocol) {
35 .plain => return conn.stream.readAtLeast(buffer, len),39 .plain => conn.stream.readAtLeast(buffer, len),
36 // .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),40 // .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),
37 }41 } catch |err| switch (err) {
42 error.ConnectionTimedOut => return error.ConnectionTimedOut,
43 error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer,
44 else => return error.UnexpectedReadFailure,
45 };
38 }46 }
3947
40 pub const ReadError = net.Stream.ReadError;48 pub const ReadError = error{
49 ConnectionTimedOut,
50 ConnectionResetByPeer,
51 UnexpectedReadFailure,
52 };
4153
42 pub const Reader = std.io.Reader(*Connection, ReadError, read);54 pub const Reader = std.io.Reader(*Connection, ReadError, read);
4355
...@@ -45,21 +57,31 @@ pub const Connection = struct {...@@ -45,21 +57,31 @@ pub const Connection = struct {
45 return Reader{ .context = conn };57 return Reader{ .context = conn };
46 }58 }
4759
48 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {60 pub fn writeAll(conn: *Connection, buffer: []const u8) WriteError!void {
49 switch (conn.protocol) {61 return switch (conn.protocol) {
50 .plain => return conn.stream.writeAll(buffer),62 .plain => conn.stream.writeAll(buffer),
51 // .tls => return conn.tls_client.writeAll(conn.stream, buffer),63 // .tls => return conn.tls_client.writeAll(conn.stream, buffer),
52 }64 } catch |err| switch (err) {
65 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
66 else => return error.UnexpectedWriteFailure,
67 };
53 }68 }
5469
55 pub fn write(conn: *Connection, buffer: []const u8) !usize {70 pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
56 switch (conn.protocol) {71 return switch (conn.protocol) {
57 .plain => return conn.stream.write(buffer),72 .plain => conn.stream.write(buffer),
58 // .tls => return conn.tls_client.write(conn.stream, buffer),73 // .tls => return conn.tls_client.write(conn.stream, buffer),
59 }74 } catch |err| switch (err) {
75 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
76 else => return error.UnexpectedWriteFailure,
77 };
60 }78 }
6179
62 pub const WriteError = net.Stream.WriteError || error{};80 pub const WriteError = error{
81 ConnectionResetByPeer,
82 UnexpectedWriteFailure,
83 };
84
63 pub const Writer = std.io.Writer(*Connection, WriteError, write);85 pub const Writer = std.io.Writer(*Connection, WriteError, write);
6486
65 pub fn writer(conn: *Connection) Writer {87 pub fn writer(conn: *Connection) Writer {
...@@ -155,136 +177,142 @@ pub const BufferedConnection = struct {...@@ -155,136 +177,142 @@ pub const BufferedConnection = struct {
155 }177 }
156};178};
157179
158/// A HTTP request originating from a client.180/// The mode of transport for responses.
159pub const Request = struct {181pub const ResponseTransfer = union(enum) {
160 pub const Headers = struct {182 content_length: u64,
161 method: http.Method,183 chunked: void,
162 target: []const u8,184 none: void,
163 version: http.Version,185};
164 content_length: ?u64 = null,
165 transfer_encoding: ?http.TransferEncoding = null,
166 transfer_compression: ?http.ContentEncoding = null,
167 connection: http.Connection = .close,
168 host: ?[]const u8 = null,
169
170 pub const ParseError = error{
171 ShortHttpStatusLine,
172 BadHttpVersion,
173 UnknownHttpMethod,
174 HttpHeadersInvalid,
175 HttpHeaderContinuationsUnsupported,
176 HttpTransferEncodingUnsupported,
177 HttpConnectionHeaderUnsupported,
178 InvalidCharacter,
179 };
180186
181 pub fn parse(bytes: []const u8) !Headers {187/// The decompressor for request messages.
182 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");188pub const Compression = union(enum) {
189 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader);
190 pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader);
191 pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
183192
184 const first_line = it.next() orelse return error.HttpHeadersInvalid;193 deflate: DeflateDecompressor,
185 if (first_line.len < 10)194 gzip: GzipDecompressor,
186 return error.ShortHttpStatusLine;195 zstd: ZstdDecompressor,
196 none: void,
197};
187198
188 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;199/// A HTTP request originating from a client.
189 const method_str = first_line[0..method_end];200pub const Request = struct {
190 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;201 pub const ParseError = Allocator.Error || error{
202 ShortHttpStatusLine,
203 BadHttpVersion,
204 UnknownHttpMethod,
205 HttpHeadersInvalid,
206 HttpHeaderContinuationsUnsupported,
207 HttpTransferEncodingUnsupported,
208 HttpConnectionHeaderUnsupported,
209 InvalidContentLength,
210 CompressionNotSupported,
211 };
191212
192 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;213 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {
193 if (version_start == method_end) return error.HttpHeadersInvalid;214 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
194215
195 const version_str = first_line[version_start + 1 ..];216 const first_line = it.next() orelse return error.HttpHeadersInvalid;
196 if (version_str.len != 8) return error.HttpHeadersInvalid;217 if (first_line.len < 10)
197 const version: http.Version = switch (int64(version_str[0..8])) {218 return error.ShortHttpStatusLine;
198 int64("HTTP/1.0") => .@"HTTP/1.0",
199 int64("HTTP/1.1") => .@"HTTP/1.1",
200 else => return error.BadHttpVersion,
201 };
202219
203 const target = first_line[method_end + 1 .. version_start];220 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
221 const method_str = first_line[0..method_end];
222 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
204223
205 var headers: Headers = .{224 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
206 .method = method,225 if (version_start == method_end) return error.HttpHeadersInvalid;
207 .target = target,
208 .version = version,
209 };
210226
211 while (it.next()) |line| {227 const version_str = first_line[version_start + 1 ..];
212 if (line.len == 0) return error.HttpHeadersInvalid;228 if (version_str.len != 8) return error.HttpHeadersInvalid;
213 switch (line[0]) {229 const version: http.Version = switch (int64(version_str[0..8])) {
214 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,230 int64("HTTP/1.0") => .@"HTTP/1.0",
215 else => {},231 int64("HTTP/1.1") => .@"HTTP/1.1",
216 }232 else => return error.BadHttpVersion,
233 };
217234
218 var line_it = mem.tokenize(u8, line, ": ");235 const target = first_line[method_end + 1 .. version_start];
219 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
220 const header_value = line_it.rest();
221 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
222 if (headers.content_length != null) return error.HttpHeadersInvalid;
223 headers.content_length = try std.fmt.parseInt(u64, header_value, 10);
224 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
225 // Transfer-Encoding: second, first
226 // Transfer-Encoding: deflate, chunked
227 var iter = mem.splitBackwards(u8, header_value, ",");
228
229 if (iter.next()) |first| {
230 const trimmed = mem.trim(u8, first, " ");
231
232 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
233 if (headers.transfer_encoding != null) return error.HttpHeadersInvalid;
234 headers.transfer_encoding = te;
235 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
236 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
237 headers.transfer_compression = ce;
238 } else {
239 return error.HttpTransferEncodingUnsupported;
240 }
241 }
242236
243 if (iter.next()) |second| {237 req.method = method;
244 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;238 req.target = target;
239 req.version = version;
245240
246 const trimmed = mem.trim(u8, second, " ");241 while (it.next()) |line| {
242 if (line.len == 0) return error.HttpHeadersInvalid;
243 switch (line[0]) {
244 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
245 else => {},
246 }
247247
248 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {248 var line_it = mem.tokenize(u8, line, ": ");
249 headers.transfer_compression = ce;249 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
250 } else {250 const header_value = line_it.rest();
251 return error.HttpTransferEncodingUnsupported;251
252 }252 try req.headers.append(header_name, header_value);
253
254 if (std.ascii.eqlIgnoreCase(header_name, "content-length")) {
255 if (req.content_length != null) return error.HttpHeadersInvalid;
256 req.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength;
257 } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) {
258 // Transfer-Encoding: second, first
259 // Transfer-Encoding: deflate, chunked
260 var iter = mem.splitBackwards(u8, header_value, ",");
261
262 if (iter.next()) |first| {
263 const trimmed = mem.trim(u8, first, " ");
264
265 if (std.meta.stringToEnum(http.TransferEncoding, trimmed)) |te| {
266 if (req.transfer_encoding != null) return error.HttpHeadersInvalid;
267 req.transfer_encoding = te;
268 } else if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
269 if (req.transfer_compression != null) return error.HttpHeadersInvalid;
270 req.transfer_compression = ce;
271 } else {
272 return error.HttpTransferEncodingUnsupported;
253 }273 }
274 }
254275
255 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;276 if (iter.next()) |second| {
256 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {277 if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
257 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
258278
259 const trimmed = mem.trim(u8, header_value, " ");279 const trimmed = mem.trim(u8, second, " ");
260280
261 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {281 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
262 headers.transfer_compression = ce;282 req.transfer_compression = ce;
263 } else {283 } else {
264 return error.HttpTransferEncodingUnsupported;284 return error.HttpTransferEncodingUnsupported;
265 }285 }
266 } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) {
267 if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) {
268 headers.connection = .keep_alive;
269 } else if (std.ascii.eqlIgnoreCase(header_value, "close")) {
270 headers.connection = .close;
271 } else {
272 return error.HttpConnectionHeaderUnsupported;
273 }
274 } else if (std.ascii.eqlIgnoreCase(header_name, "host")) {
275 headers.host = header_value;
276 }286 }
277 }
278287
279 return headers;288 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
280 }289 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
290 if (req.transfer_compression != null) return error.HttpHeadersInvalid;
291
292 const trimmed = mem.trim(u8, header_value, " ");
281293
282 inline fn int64(array: *const [8]u8) u64 {294 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
283 return @bitCast(u64, array.*);295 req.transfer_compression = ce;
296 } else {
297 return error.HttpTransferEncodingUnsupported;
298 }
299 }
284 }300 }
285 };301 }
286302
287 headers: Headers = undefined,303 inline fn int64(array: *const [8]u8) u64 {
304 return @bitCast(u64, array.*);
305 }
306
307 method: http.Method,
308 target: []const u8,
309 version: http.Version,
310
311 content_length: ?u64 = null,
312 transfer_encoding: ?http.TransferEncoding = null,
313 transfer_compression: ?http.ContentEncoding = null,
314
315 headers: http.Headers = undefined,
288 parser: proto.HeadersParser,316 parser: proto.HeadersParser,
289 compression: Compression = .none,317 compression: Compression = .none,
290};318};
...@@ -295,23 +323,17 @@ pub const Request = struct {...@@ -295,23 +323,17 @@ pub const Request = struct {
295/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]323/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]
296/// \ -> read /324/// \ -> read /
297pub const Response = struct {325pub const Response = struct {
298 pub const Headers = struct {326 version: http.Version = .@"HTTP/1.1",
299 version: http.Version = .@"HTTP/1.1",327 status: http.Status = .ok,
300 status: http.Status = .ok,328 reason: ?[]const u8 = null,
301 reason: ?[]const u8 = null,
302329
303 server: ?[]const u8 = "zig (std.http)",330 transfer_encoding: ResponseTransfer = .none,
304 connection: http.Connection = .keep_alive,
305 transfer_encoding: RequestTransfer = .none,
306
307 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
308 };
309331
310 server: *Server,332 server: *Server,
311 address: net.Address,333 address: net.Address,
312 connection: BufferedConnection,334 connection: BufferedConnection,
313335
314 headers: Headers = .{},336 headers: http.Headers,
315 request: Request,337 request: Request,
316338
317 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.339 /// Reset this response to its initial state. This must be called before handling a second request on the same connection.
...@@ -341,46 +363,61 @@ pub const Response = struct {...@@ -341,46 +363,61 @@ pub const Response = struct {
341 }363 }
342 }364 }
343365
366 pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
367
344 /// Send the response headers.368 /// Send the response headers.
345 pub fn do(res: *Response) !void {369 pub fn do(res: *Response) !void {
346 var buffered = std.io.bufferedWriter(res.connection.writer());370 var buffered = std.io.bufferedWriter(res.connection.writer());
347 const w = buffered.writer();371 const w = buffered.writer();
348372
349 try w.writeAll(@tagName(res.headers.version));373 try w.writeAll(@tagName(res.version));
350 try w.writeByte(' ');374 try w.writeByte(' ');
351 try w.print("{d}", .{@enumToInt(res.headers.status)});375 try w.print("{d}", .{@enumToInt(res.status)});
352 try w.writeByte(' ');376 try w.writeByte(' ');
353 if (res.headers.reason) |reason| {377 if (res.reason) |reason| {
354 try w.writeAll(reason);378 try w.writeAll(reason);
355 } else if (res.headers.status.phrase()) |phrase| {379 } else if (res.status.phrase()) |phrase| {
356 try w.writeAll(phrase);380 try w.writeAll(phrase);
357 }381 }
382 try w.writeAll("\r\n");
358383
359 if (res.headers.server) |server| {384 if (!res.headers.contains("server")) {
360 try w.writeAll("\r\nServer: ");385 try w.writeAll("Server: zig (std.http)\r\n");
361 try w.writeAll(server);
362 }386 }
363387
364 if (res.headers.connection == .close) {388 if (!res.headers.contains("connection")) {
365 try w.writeAll("\r\nConnection: close");389 try w.writeAll("Connection: keep-alive\r\n");
366 } else {
367 try w.writeAll("\r\nConnection: keep-alive");
368 }390 }
369391
370 switch (res.headers.transfer_encoding) {392 const has_transfer_encoding = res.headers.contains("transfer-encoding");
371 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),393 const has_content_length = res.headers.contains("content-length");
372 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
373 .none => {},
374 }
375394
376 for (res.headers.custom) |header| {395 if (!has_transfer_encoding and !has_content_length) {
377 try w.writeAll("\r\n");396 switch (res.transfer_encoding) {
378 try w.writeAll(header.name);397 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
379 try w.writeAll(": ");398 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
380 try w.writeAll(header.value);399 .none => {},
400 }
401 } else {
402 if (has_content_length) {
403 const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength;
404
405 res.transfer_encoding = .{ .content_length = content_length };
406 } else if (has_transfer_encoding) {
407 const transfer_encoding = res.headers.getFirstValue("content-length").?;
408 if (std.mem.eql(u8, transfer_encoding, "chunked")) {
409 res.transfer_encoding = .chunked;
410 } else {
411 return error.UnsupportedTransferEncoding;
412 }
413 } else {
414 res.transfer_encoding = .none;
415 }
381 }416 }
382417
383 try w.writeAll("\r\n\r\n");418 try w.print("{}", .{res.headers});
419
420 try w.writeAll("\r\n");
384421
385 try buffered.flush();422 try buffered.flush();
386 }423 }
...@@ -393,23 +430,23 @@ pub const Response = struct {...@@ -393,23 +430,23 @@ pub const Response = struct {
393 return .{ .context = res };430 return .{ .context = res };
394 }431 }
395432
396 pub fn transferRead(res: *Response, buf: []u8) TransferReadError!usize {433 fn transferRead(res: *Response, buf: []u8) TransferReadError!usize {
397 if (res.request.parser.isComplete()) return 0;434 if (res.request.parser.done) return 0;
398435
399 var index: usize = 0;436 var index: usize = 0;
400 while (index == 0) {437 while (index == 0) {
401 const amt = try res.request.parser.read(&res.connection, buf[index..], false);438 const amt = try res.request.parser.read(&res.connection, buf[index..], false);
402 if (amt == 0 and res.request.parser.isComplete()) break;439 if (amt == 0 and res.request.parser.done) break;
403 index += amt;440 index += amt;
404 }441 }
405442
406 return index;443 return index;
407 }444 }
408445
409 pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.WaitForCompleteHeadError || Request.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported};446 pub const WaitError = BufferedConnection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Request.ParseError || error{ CompressionInitializationFailed, CompressionNotSupported };
410447
411 /// Wait for the client to send a complete request head.448 /// Wait for the client to send a complete request head.
412 pub fn wait(res: *Response) !void {449 pub fn wait(res: *Response) WaitError!void {
413 while (true) {450 while (true) {
414 try res.connection.fill();451 try res.connection.fill();
415452
...@@ -419,22 +456,28 @@ pub const Response = struct {...@@ -419,22 +456,28 @@ pub const Response = struct {
419 if (res.request.parser.state.isContent()) break;456 if (res.request.parser.state.isContent()) break;
420 }457 }
421458
422 res.request.headers = try Request.Headers.parse(res.request.parser.header_bytes.items);459 res.request.headers = .{ .allocator = res.server.allocator, .owned = true };
460 try res.request.parse(res.request.parser.header_bytes.items);
461
462 const res_connection = res.headers.getFirstValue("connection");
463 const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?);
423464
424 if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) {465 const req_connection = res.request.headers.getFirstValue("connection");
466 const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?);
467 if (res_keepalive and req_keepalive) {
425 res.connection.conn.closing = false;468 res.connection.conn.closing = false;
426 } else {469 } else {
427 res.connection.conn.closing = true;470 res.connection.conn.closing = true;
428 }471 }
429472
430 if (res.request.headers.transfer_encoding) |te| {473 if (res.request.transfer_encoding) |te| {
431 switch (te) {474 switch (te) {
432 .chunked => {475 .chunked => {
433 res.request.parser.next_chunk_length = 0;476 res.request.parser.next_chunk_length = 0;
434 res.request.parser.state = .chunk_head_size;477 res.request.parser.state = .chunk_head_size;
435 },478 },
436 }479 }
437 } else if (res.request.headers.content_length) |cl| {480 } else if (res.request.content_length) |cl| {
438 res.request.parser.next_chunk_length = cl;481 res.request.parser.next_chunk_length = cl;
439482
440 if (cl == 0) res.request.parser.done = true;483 if (cl == 0) res.request.parser.done = true;
...@@ -443,13 +486,13 @@ pub const Response = struct {...@@ -443,13 +486,13 @@ pub const Response = struct {
443 }486 }
444487
445 if (!res.request.parser.done) {488 if (!res.request.parser.done) {
446 if (res.request.headers.transfer_compression) |tc| switch (tc) {489 if (res.request.transfer_compression) |tc| switch (tc) {
447 .compress => return error.CompressionNotSupported,490 .compress => return error.CompressionNotSupported,
448 .deflate => res.request.compression = .{491 .deflate => res.request.compression = .{
449 .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()),492 .deflate = std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
450 },493 },
451 .gzip => res.request.compression = .{494 .gzip => res.request.compression = .{
452 .gzip = try std.compress.gzip.decompress(res.server.allocator, res.transferReader()),495 .gzip = std.compress.gzip.decompress(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed,
453 },496 },
454 .zstd => res.request.compression = .{497 .zstd => res.request.compression = .{
455 .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()),498 .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()),
...@@ -458,7 +501,7 @@ pub const Response = struct {...@@ -458,7 +501,7 @@ pub const Response = struct {
458 }501 }
459 }502 }
460503
461 pub const ReadError = Compression.DeflateDecompressor.Error || Compression.GzipDecompressor.Error || Compression.ZstdDecompressor.Error || WaitForCompleteHeadError;504 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{DecompressionFailure};
462505
463 pub const Reader = std.io.Reader(*Response, ReadError, read);506 pub const Reader = std.io.Reader(*Response, ReadError, read);
464507
...@@ -467,12 +510,33 @@ pub const Response = struct {...@@ -467,12 +510,33 @@ pub const Response = struct {
467 }510 }
468511
469 pub fn read(res: *Response, buffer: []u8) ReadError!usize {512 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
470 return switch (res.request.compression) {513 const out_index = switch (res.request.compression) {
471 .deflate => |*deflate| try deflate.read(buffer),514 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
472 .gzip => |*gzip| try gzip.read(buffer),515 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
473 .zstd => |*zstd| try zstd.read(buffer),516 .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
474 else => try res.transferRead(buffer),517 else => try res.transferRead(buffer),
475 };518 };
519
520 if (out_index == 0) {
521 const has_trail = !res.request.parser.state.isContent();
522
523 while (!res.request.parser.state.isContent()) { // read trailing headers
524 try res.connection.fill();
525
526 const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek());
527 res.connection.clear(@intCast(u16, nchecked));
528 }
529
530 if (has_trail) {
531 res.request.headers = http.Headers{ .allocator = res.server.allocator, .owned = false };
532
533 // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error.
534 // This will *only* fail for a malformed trailer.
535 res.request.parse(res.request.parser.header_bytes.items) catch return error.InvalidTrailers;
536 }
537 }
538
539 return out_index;
476 }540 }
477541
478 pub fn readAll(res: *Response, buffer: []u8) !usize {542 pub fn readAll(res: *Response, buffer: []u8) !usize {
...@@ -495,7 +559,7 @@ pub const Response = struct {...@@ -495,7 +559,7 @@ pub const Response = struct {
495559
496 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.560 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
497 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {561 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
498 switch (res.headers.transfer_encoding) {562 switch (res.transfer_encoding) {
499 .chunked => {563 .chunked => {
500 try res.connection.writer().print("{x}\r\n", .{bytes.len});564 try res.connection.writer().print("{x}\r\n", .{bytes.len});
501 try res.connection.writeAll(bytes);565 try res.connection.writeAll(bytes);
...@@ -514,9 +578,18 @@ pub const Response = struct {...@@ -514,9 +578,18 @@ pub const Response = struct {
514 }578 }
515 }579 }
516580
581 pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void {
582 var index: usize = 0;
583 while (index < bytes.len) {
584 index += try write(req, bytes[index..]);
585 }
586 }
587
588 pub const FinishError = WriteError || error{MessageNotCompleted};
589
517 /// Finish the body of a request. This notifies the server that you have no more data to send.590 /// Finish the body of a request. This notifies the server that you have no more data to send.
518 pub fn finish(res: *Response) !void {591 pub fn finish(res: *Response) FinishError!void {
519 switch (res.headers.transfer_encoding) {592 switch (res.transfer_encoding) {
520 .chunked => try res.connection.writeAll("0\r\n\r\n"),593 .chunked => try res.connection.writeAll("0\r\n\r\n"),
521 .content_length => |len| if (len != 0) return error.MessageNotCompleted,594 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
522 .none => {},595 .none => {},
...@@ -524,25 +597,6 @@ pub const Response = struct {...@@ -524,25 +597,6 @@ pub const Response = struct {
524 }597 }
525};598};
526599
527/// The mode of transport for responses.
528pub const RequestTransfer = union(enum) {
529 content_length: u64,
530 chunked: void,
531 none: void,
532};
533
534/// The decompressor for request messages.
535pub const Compression = union(enum) {
536 pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Response.TransferReader);
537 pub const GzipDecompressor = std.compress.gzip.Decompress(Response.TransferReader);
538 pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Response.TransferReader, .{});
539
540 deflate: DeflateDecompressor,
541 gzip: GzipDecompressor,
542 zstd: ZstdDecompressor,
543 none: void,
544};
545
546pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {600pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
547 return .{601 return .{
548 .allocator = allocator,602 .allocator = allocator,
...@@ -588,7 +642,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {...@@ -588,7 +642,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
588 .stream = in.stream,642 .stream = in.stream,
589 .protocol = .plain,643 .protocol = .plain,
590 } },644 } },
645 .headers = .{ .allocator = server.allocator },
591 .request = .{646 .request = .{
647 .version = undefined,
648 .method = undefined,
649 .target = undefined,
592 .parser = switch (options) {650 .parser = switch (options) {
593 .dynamic => |max| proto.HeadersParser.initDynamic(max),651 .dynamic => |max| proto.HeadersParser.initDynamic(max),
594 .static => |buf| proto.HeadersParser.initStatic(buf),652 .static => |buf| proto.HeadersParser.initStatic(buf),
lib/std/http/protocol.zig+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1const std = @import("std");1const std = @import("../std.zig");
2const testing = std.testing;2const testing = std.testing;
3const mem = std.mem;3const mem = std.mem;
44
src/Package.zig+6-1
...@@ -479,9 +479,14 @@ fn fetchAndUnpack(...@@ -479,9 +479,14 @@ fn fetchAndUnpack(
479 };479 };
480 defer tmp_directory.closeAndFree(gpa);480 defer tmp_directory.closeAndFree(gpa);
481481
482 var req = try http_client.request(uri, .{}, .{});482 var h = std.http.Headers{ .allocator = gpa };
483 defer h.deinit();
484
485 var req = try http_client.request(.GET, uri, h, .{});
483 defer req.deinit();486 defer req.deinit();
484487
488 try req.start();
489
485 try req.do();490 try req.do();
486491
487 if (mem.endsWith(u8, uri.path, ".tar.gz")) {492 if (mem.endsWith(u8, uri.path, ".tar.gz")) {