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
2727 return escapeStringWithFn(allocator, input, isQueryChar);
2828}
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
3042pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) std.mem.Allocator.Error![]const u8 {
3143 var outsize: usize = 0;
3244 for (input) |c| {
......@@ -52,6 +64,16 @@ pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, compt
5264 return output;
5365}
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
5577/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies
5678/// them to the output.
5779pub 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 {
184206 return uri;
185207}
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
187263/// Parses the URI or returns an error.
188264/// The return value will contain unescaped strings pointing into the
189265/// 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" {
371371 try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org"));
372372}
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 {
375377 const cert_bytes = cert.buffer;
376378 const certificate = try der.Element.parse(cert_bytes, cert.index);
377379 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 {
514516 return cert.buffer[elem.slice.start..elem.slice.end];
515517}
516518
519pub const ParseBitStringError = error{ CertificateFieldHasWrongDataType, CertificateHasInvalidBitString };
520
517521pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice {
518522 if (elem.identifier.tag != .bitstring) return error.CertificateFieldHasWrongDataType;
519523 if (cert.buffer[elem.slice.start] != 0) return error.CertificateHasInvalidBitString;
520524 return .{ .start = elem.slice.start + 1, .end = elem.slice.end };
521525}
522526
527pub const ParseTimeError = error{ CertificateTimeInvalid, CertificateFieldHasWrongDataType };
528
523529/// 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 {
525531 const bytes = cert.contents(elem);
526532 switch (elem.identifier.tag) {
527533 .utc_time => {
......@@ -647,34 +653,38 @@ test parseYear4 {
647653 try expectError(error.CertificateTimeInvalid, parseYear4("crap"));
648654}
649655
650pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm {
656pub fn parseAlgorithm(bytes: []const u8, element: der.Element) ParseEnumError!Algorithm {
651657 return parseEnum(Algorithm, bytes, element);
652658}
653659
654pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) !AlgorithmCategory {
660pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) ParseEnumError!AlgorithmCategory {
655661 return parseEnum(AlgorithmCategory, bytes, element);
656662}
657663
658pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute {
664pub fn parseAttribute(bytes: []const u8, element: der.Element) ParseEnumError!Attribute {
659665 return parseEnum(Attribute, bytes, element);
660666}
661667
662pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve {
668pub fn parseNamedCurve(bytes: []const u8, element: der.Element) ParseEnumError!NamedCurve {
663669 return parseEnum(NamedCurve, bytes, element);
664670}
665671
666pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId {
672pub fn parseExtensionId(bytes: []const u8, element: der.Element) ParseEnumError!ExtensionId {
667673 return parseEnum(ExtensionId, bytes, element);
668674}
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 {
671679 if (element.identifier.tag != .object_identifier)
672680 return error.CertificateFieldHasWrongDataType;
673681 const oid_bytes = bytes[element.slice.start..element.slice.end];
674682 return E.map.get(oid_bytes) orelse return error.CertificateHasUnrecognizedObjectId;
675683}
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 {
678688 if (@bitCast(u8, version_elem.identifier) != 0xa0)
679689 return .v1;
680690
......@@ -861,9 +871,9 @@ pub const der = struct {
861871 pub const empty: Slice = .{ .start = 0, .end = 0 };
862872 };
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 {
867877 var i = index;
868878 const identifier = @bitCast(Identifier, bytes[i]);
869879 i += 1;
lib/std/crypto/Certificate/Bundle.zig+27-10
......@@ -50,11 +50,13 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void {
5050 cb.* = undefined;
5151}
5252
53pub const RescanError = RescanLinuxError || RescanMacError || RescanWindowsError;
54
5355/// Clears the set of certificates and then scans the host operating system
5456/// file system standard locations for certificates.
5557/// For operating systems that do not have standard CA installations to be
5658/// 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 {
5860 switch (builtin.os.tag) {
5961 .linux => return rescanLinux(cb, gpa),
6062 .macos => return rescanMac(cb, gpa),
......@@ -64,8 +66,11 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void {
6466}
6567
6668pub 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 {
6974 // Possible certificate files; stop after finding one.
7075 const cert_file_paths = [_][]const u8{
7176 "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
......@@ -107,7 +112,9 @@ pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void {
107112 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
108113}
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 {
111118 cb.bytes.clearRetainingCapacity();
112119 cb.map.clearRetainingCapacity();
113120
......@@ -132,12 +139,14 @@ pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void {
132139 cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len);
133140}
134141
142pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError;
143
135144pub fn addCertsFromDirPath(
136145 cb: *Bundle,
137146 gpa: Allocator,
138147 dir: fs.Dir,
139148 sub_dir_path: []const u8,
140) !void {
149) AddCertsFromDirPathError!void {
141150 var iterable_dir = try dir.openIterableDir(sub_dir_path, .{});
142151 defer iterable_dir.close();
143152 return addCertsFromDir(cb, gpa, iterable_dir);
......@@ -147,14 +156,16 @@ pub fn addCertsFromDirPathAbsolute(
147156 cb: *Bundle,
148157 gpa: Allocator,
149158 abs_dir_path: []const u8,
150) !void {
159) AddCertsFromDirPathError!void {
151160 assert(fs.path.isAbsolute(abs_dir_path));
152161 var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{});
153162 defer iterable_dir.close();
154163 return addCertsFromDir(cb, gpa, iterable_dir);
155164}
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 {
158169 var it = iterable_dir.iterate();
159170 while (try it.next()) |entry| {
160171 switch (entry.kind) {
......@@ -166,11 +177,13 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir
166177 }
167178}
168179
180pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError;
181
169182pub fn addCertsFromFilePathAbsolute(
170183 cb: *Bundle,
171184 gpa: Allocator,
172185 abs_file_path: []const u8,
173) !void {
186) AddCertsFromFilePathError!void {
174187 assert(fs.path.isAbsolute(abs_file_path));
175188 var file = try fs.openFileAbsolute(abs_file_path, .{});
176189 defer file.close();
......@@ -182,13 +195,15 @@ pub fn addCertsFromFilePath(
182195 gpa: Allocator,
183196 dir: fs.Dir,
184197 sub_file_path: []const u8,
185) !void {
198) AddCertsFromFilePathError!void {
186199 var file = try dir.openFile(sub_file_path, .{});
187200 defer file.close();
188201 return addCertsFromFile(cb, gpa, file);
189202}
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 {
192207 const size = try file.getEndPos();
193208
194209 // 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 {
222237 }
223238}
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 {
226243 // Even though we could only partially parse the certificate to find
227244 // the subject name, we pre-parse all of them to make sure and only
228245 // 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;
55const Allocator = std.mem.Allocator;
66const 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 {
911 cb.bytes.clearRetainingCapacity();
1012 cb.map.clearRetainingCapacity();
1113
lib/std/http.zig+4-5
......@@ -1,6 +1,10 @@
11pub const Client = @import("http/Client.zig");
22pub const Server = @import("http/Server.zig");
33pub const protocol = @import("http/protocol.zig");
4const headers = @import("http/Headers.zig");
5
6pub const Headers = headers.Headers;
7pub const Field = headers.Field;
48
59pub const Version = enum {
610 @"HTTP/1.0",
......@@ -265,11 +269,6 @@ pub const Connection = enum {
265269 close,
266270};
267271
268pub const CustomHeader = struct {
269 name: []const u8,
270 value: []const u8,
271};
272
273272const std = @import("std.zig");
274273
275274test {
lib/std/http/Client.zig+368-352
......@@ -25,48 +25,7 @@ next_https_rescan_certs: bool = true,
2525/// The pool of connections that can be reused (and currently in use).
2626connection_pool: ConnectionPool = .{},
2727
28/// The last error that occurred on this client. This is not threadsafe, do not expect it to be completely accurate.
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};
28proxy: ?HttpProxy = null,
7029
7130/// A set of linked lists of connections that can be reused.
7231pub const ConnectionPool = struct {
......@@ -82,6 +41,7 @@ pub const ConnectionPool = struct {
8241 host: []u8,
8342 port: u16,
8443
44 proxied: bool = false,
8545 closing: bool = false,
8646
8747 pub fn deinit(self: *StoredConnection, client: *Client) void {
......@@ -158,7 +118,12 @@ pub const ConnectionPool = struct {
158118 return client.allocator.destroy(popped);
159119 }
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
162127 pool.free_len += 1;
163128 }
164129
......@@ -202,30 +167,38 @@ pub const Connection = struct {
202167
203168 pub const Protocol = enum { plain, tls };
204169
205 pub fn read(conn: *Connection, buffer: []u8) !usize {
206 switch (conn.protocol) {
207 .plain => return conn.stream.read(buffer),
208 .tls => return conn.tls_client.read(conn.stream, buffer),
209 }
170 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
171 return switch (conn.protocol) {
172 .plain => conn.stream.read(buffer),
173 .tls => conn.tls_client.read(conn.stream, buffer),
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 };
210181 }
211182
212 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {
213 switch (conn.protocol) {
214 .plain => return conn.stream.readAtLeast(buffer, len),
215 .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len),
216 }
183 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
184 return switch (conn.protocol) {
185 .plain => conn.stream.readAtLeast(buffer, len),
186 .tls => conn.tls_client.readAtLeast(conn.stream, buffer, len),
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 };
217194 }
218195
219 pub const ReadError = net.Stream.ReadError || error{
220 TlsConnectionTruncated,
221 TlsRecordOverflow,
222 TlsDecodeError,
196 pub const ReadError = error{
197 TlsFailure,
223198 TlsAlert,
224 TlsBadRecordMac,
225 Overflow,
226 TlsBadLength,
227 TlsIllegalParameter,
228 TlsUnexpectedMessage,
199 ConnectionTimedOut,
200 ConnectionResetByPeer,
201 UnexpectedReadFailure,
229202 };
230203
231204 pub const Reader = std.io.Reader(*Connection, ReadError, read);
......@@ -235,20 +208,30 @@ pub const Connection = struct {
235208 }
236209
237210 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
238 switch (conn.protocol) {
239 .plain => return conn.stream.writeAll(buffer),
240 .tls => return conn.tls_client.writeAll(conn.stream, buffer),
241 }
211 return switch (conn.protocol) {
212 .plain => conn.stream.writeAll(buffer),
213 .tls => conn.tls_client.writeAll(conn.stream, buffer),
214 } catch |err| switch (err) {
215 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
216 else => return error.UnexpectedWriteFailure,
217 };
242218 }
243219
244220 pub fn write(conn: *Connection, buffer: []const u8) !usize {
245 switch (conn.protocol) {
246 .plain => return conn.stream.write(buffer),
247 .tls => return conn.tls_client.write(conn.stream, buffer),
248 }
221 return switch (conn.protocol) {
222 .plain => conn.stream.write(buffer),
223 .tls => conn.tls_client.write(conn.stream, buffer),
224 } catch |err| switch (err) {
225 error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer,
226 else => return error.UnexpectedWriteFailure,
227 };
249228 }
250229
251 pub const WriteError = net.Stream.WriteError || error{};
230 pub const WriteError = error{
231 ConnectionResetByPeer,
232 UnexpectedWriteFailure,
233 };
234
252235 pub const Writer = std.io.Writer(*Connection, WriteError, write);
253236
254237 pub fn writer(conn: *Connection) Writer {
......@@ -371,140 +354,125 @@ pub const Compression = union(enum) {
371354
372355/// A HTTP response originating from a server.
373356pub const Response = struct {
374 pub const Headers = struct {
375 status: http.Status,
376 version: http.Version,
377 location: ?[]const u8 = null,
378 content_length: ?u64 = null,
379 transfer_encoding: ?http.TransferEncoding = null,
380 transfer_compression: ?http.ContentEncoding = null,
381 connection: http.Connection = .close,
382 upgrade: ?[]const u8 = null,
383
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 }
357 pub const ParseError = Allocator.Error || error{
358 ShortHttpStatusLine,
359 BadHttpVersion,
360 HttpHeadersInvalid,
361 HttpHeaderContinuationsUnsupported,
362 HttpTransferEncodingUnsupported,
363 HttpConnectionHeaderUnsupported,
364 InvalidContentLength,
365 CompressionNotSupported,
366 };
421367
422 var line_it = mem.tokenize(u8, line, ": ");
423 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
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 }
368 pub fn parse(res: *Response, bytes: []const u8) ParseError!void {
369 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
449370
450 if (iter.next()) |second| {
451 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
371 const first_line = it.next() orelse return error.HttpHeadersInvalid;
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| {
456 headers.transfer_compression = ce;
457 } else {
458 return error.HttpTransferEncodingUnsupported;
459 }
395 var line_it = mem.tokenize(u8, line, ": ");
396 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
397 const header_value = line_it.rest();
398
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;
460420 }
421 }
461422
462 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
463 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
464 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
423 if (iter.next()) |second| {
424 if (res.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
465425
466 const trimmed = mem.trim(u8, header_value, " ");
426 const trimmed = mem.trim(u8, second, " ");
467427
468428 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
469 headers.transfer_compression = ce;
429 res.transfer_compression = ce;
470430 } else {
471431 return error.HttpTransferEncodingUnsupported;
472432 }
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;
483433 }
484 }
485434
486 return headers;
487 }
435 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
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 {
490 return @bitCast(u64, array.*);
491 }
439 const trimmed = mem.trim(u8, header_value, " ");
492440
493 fn parseInt3(nnn: @Vector(3, u8)) u10 {
494 const zero: @Vector(3, u8) = .{ '0', '0', '0' };
495 const mmm: @Vector(3, u10) = .{ 100, 10, 1 };
496 return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm);
441 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
442 res.transfer_compression = ce;
443 } else {
444 return error.HttpTransferEncodingUnsupported;
445 }
446 }
497447 }
448 }
498449
499 test parseInt3 {
500 const expectEqual = testing.expectEqual;
501 try expectEqual(@as(u10, 0), parseInt3("000".*));
502 try expectEqual(@as(u10, 418), parseInt3("418".*));
503 try expectEqual(@as(u10, 999), parseInt3("999".*));
504 }
505 };
450 inline fn int64(array: *const [8]u8) u64 {
451 return @bitCast(u64, array.*);
452 }
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,
508476 parser: proto.HeadersParser,
509477 compression: Compression = .none,
510478 skip: bool = false,
......@@ -514,22 +482,14 @@ pub const Response = struct {
514482///
515483/// Order of operations: request[ -> write -> finish] -> do -> read
516484pub 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
527485 uri: Uri,
528486 client: *Client,
529487 connection: *ConnectionPool.Node,
530 /// These are stored in Request so that they are available when following
531 /// redirects.
532 headers: Headers,
488
489 method: http.Method,
490 version: http.Version = .@"HTTP/1.1",
491 headers: http.Headers,
492 transfer_encoding: RequestTransfer = .none,
533493
534494 redirects_left: u32,
535495 handle_redirects: bool,
......@@ -549,80 +509,104 @@ pub const Request = struct {
549509 }
550510
551511 if (req.response.parser.header_bytes_owned) {
512 req.response.headers.deinit();
552513 req.response.parser.header_bytes.deinit(req.client.allocator);
553514 }
554515
555516 if (!req.response.parser.done) {
556517 // If the response wasn't fully read, then we need to close the connection.
557518 req.connection.data.closing = true;
558 req.client.connection_pool.release(req.client, req.connection);
559519 }
560520
521 req.client.connection_pool.release(req.client, req.connection);
522
561523 req.arena.deinit();
562524 req.* = undefined;
563525 }
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 {
566531 var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer());
567532 const w = buffered.writer();
568533
569 const escaped_path = try Uri.escapePath(req.client.allocator, uri.path);
570 defer req.client.allocator.free(escaped_path);
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);
534 try w.writeAll(@tagName(req.method));
535 try w.writeByte(' ');
574536
575 const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(req.client.allocator, f) else null;
576 defer if (escaped_fragment) |f| req.client.allocator.free(f);
537 if (req.method == .CONNECT) {
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));
579548 try w.writeByte(' ');
580 if (escaped_path.len == 0) {
581 try w.writeByte('/');
582 } else {
583 try w.writeAll(escaped_path);
549 try w.writeAll(@tagName(req.version));
550 try w.writeAll("\r\n");
551
552 if (!req.headers.contains("host")) {
553 try w.writeAll("Host: ");
554 try w.writeAll(req.uri.host.?);
555 try w.writeAll("\r\n");
584556 }
585 if (escaped_query) |q| {
586 try w.writeByte('?');
587 try w.writeAll(q);
557
558 if (!req.headers.contains("user-agent")) {
559 try w.writeAll("User-Agent: zig/");
560 try w.writeAll(@import("builtin").zig_version_string);
561 try w.writeAll(" (std.http)\r\n");
588562 }
589 if (escaped_fragment) |f| {
590 try w.writeByte('#');
591 try w.writeAll(f);
563
564 if (!req.headers.contains("connection")) {
565 try w.writeAll("Connection: keep-alive\r\n");
592566 }
593 try w.writeByte(' ');
594 try w.writeAll(@tagName(headers.version));
595 try w.writeAll("\r\nHost: ");
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");
567
568 if (!req.headers.contains("accept-encoding")) {
569 try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n");
603570 }
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) {
608 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
609 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
610 .none => {},
572 if (!req.headers.contains("te")) {
573 try w.writeAll("TE: gzip, deflate, trailers\r\n");
611574 }
612575
613 for (headers.custom) |header| {
614 try w.writeAll("\r\n");
615 try w.writeAll(header.name);
616 try w.writeAll(": ");
617 try w.writeAll(header.value);
576 const has_transfer_encoding = req.headers.contains("transfer-encoding");
577 const has_content_length = req.headers.contains("content-length");
578
579 if (!has_transfer_encoding and !has_content_length) {
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 }
618600 }
619601
620 try w.writeAll("\r\n\r\n");
602 try w.print("{}", .{req.headers});
603
604 try w.writeAll("\r\n");
621605
622606 try buffered.flush();
623607 }
624608
625 pub const TransferReadError = proto.HeadersParser.ReadError || error{ReadFailed};
609 pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError;
626610
627611 pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead);
628612
......@@ -635,10 +619,7 @@ pub const Request = struct {
635619
636620 var index: usize = 0;
637621 while (index == 0) {
638 const amt = req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip) catch |err| {
639 req.client.last_error = .{ .read = err };
640 return error.ReadFailed;
641 };
622 const amt = try req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip);
642623 if (amt == 0 and req.response.parser.done) break;
643624 index += amt;
644625 }
......@@ -646,7 +627,7 @@ pub const Request = struct {
646627 return index;
647628 }
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
651632 /// Waits for a response from the server and parses any headers that are sent.
652633 /// This function will block until the final response is received.
......@@ -656,10 +637,7 @@ pub const Request = struct {
656637 pub fn do(req: *Request) DoError!void {
657638 while (true) { // handle redirects
658639 while (true) { // read headers
659 req.connection.data.buffered.fill() catch |err| {
660 req.client.last_error = .{ .read = err };
661 return error.ReadFailed;
662 };
640 try req.connection.data.buffered.fill();
663641
664642 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
665643 req.connection.data.buffered.clear(@intCast(u16, nchecked));
......@@ -667,27 +645,39 @@ pub const Request = struct {
667645 if (req.response.parser.state.isContent()) break;
668646 }
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) {
673657 req.connection.data.closing = false;
658 req.connection.data.proxied = true;
674659 req.response.parser.done = true;
675660 }
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) {
678668 req.connection.data.closing = false;
679669 } else {
680670 req.connection.data.closing = true;
681671 }
682672
683 if (req.response.headers.transfer_encoding) |te| {
673 if (req.response.transfer_encoding) |te| {
684674 switch (te) {
685675 .chunked => {
686676 req.response.parser.next_chunk_length = 0;
687677 req.response.parser.state = .chunk_head_size;
688678 },
689679 }
690 } else if (req.response.headers.content_length) |cl| {
680 } else if (req.response.content_length) |cl| {
691681 req.response.parser.next_chunk_length = cl;
692682
693683 if (cl == 0) req.response.parser.done = true;
......@@ -695,7 +685,7 @@ pub const Request = struct {
695685 req.response.parser.done = true;
696686 }
697687
698 if (req.response.headers.status.class() == .redirect and req.handle_redirects) {
688 if (req.response.status.class() == .redirect and req.handle_redirects) {
699689 req.response.skip = true;
700690
701691 const empty = @as([*]u8, undefined)[0..0];
......@@ -703,7 +693,7 @@ pub const Request = struct {
703693
704694 if (req.redirects_left == 0) return error.TooManyHttpRedirects;
705695
706 const location = req.response.headers.location orelse
696 const location = req.response.headers.getFirstValue("location") orelse
707697 return error.HttpRedirectMissingLocation;
708698 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location);
709699
......@@ -714,7 +704,8 @@ pub const Request = struct {
714704 req.arena.deinit();
715705 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,
718709 .max_redirects = req.redirects_left - 1,
719710 .header_strategy = if (req.response.parser.header_bytes_owned) .{
720711 .dynamic = req.response.parser.max_header_bytes,
......@@ -727,19 +718,13 @@ pub const Request = struct {
727718 } else {
728719 req.response.skip = false;
729720 if (!req.response.parser.done) {
730 if (req.response.headers.transfer_compression) |tc| switch (tc) {
721 if (req.response.transfer_compression) |tc| switch (tc) {
731722 .compress => return error.CompressionNotSupported,
732723 .deflate => req.response.compression = .{
733 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch |err| {
734 req.client.last_error = .{ .zlib_init = err };
735 return error.CompressionInitializationFailed;
736 },
724 .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
737725 },
738726 .gzip => req.response.compression = .{
739 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch |err| {
740 req.client.last_error = .{ .gzip_init = err };
741 return error.CompressionInitializationFailed;
742 },
727 .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed,
743728 },
744729 .zstd => req.response.compression = .{
745730 .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()),
......@@ -752,7 +737,7 @@ pub const Request = struct {
752737 }
753738 }
754739
755 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError;
740 pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers };
756741
757742 pub const Reader = std.io.Reader(*Request, ReadError, read);
758743
......@@ -762,57 +747,47 @@ pub const Request = struct {
762747
763748 /// Reads data from the response body. Must be called after `do`.
764749 pub fn read(req: *Request, buffer: []u8) ReadError!usize {
765 while (true) {
766 const out_index = switch (req.response.compression) {
767 .deflate => |*deflate| deflate.read(buffer) catch |err| {
768 req.client.last_error = .{ .decompress = err };
769 err catch {};
770 return error.ReadFailed;
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 };
750 const out_index = switch (req.response.compression) {
751 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
752 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
753 .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
754 else => try req.transferRead(buffer),
755 };
791756
792 const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek());
793 req.connection.data.buffered.clear(@intCast(u16, nchecked));
794 }
757 if (out_index == 0) {
758 const has_trail = !req.response.parser.state.isContent();
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));
795765 }
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 }
798774 }
775
776 return out_index;
799777 }
800778
801779 /// Reads data from the response body. Must be called after `do`.
802780 pub fn readAll(req: *Request, buffer: []u8) !usize {
803781 var index: usize = 0;
804782 while (index < buffer.len) {
805 const amt = read(req, buffer[index..]) catch |err| {
806 req.client.last_error = .{ .read = err };
807 return error.ReadFailed;
808 };
783 const amt = try read(req, buffer[index..]);
809784 if (amt == 0) break;
810785 index += amt;
811786 }
812787 return index;
813788 }
814789
815 pub const WriteError = error{ WriteFailed, NotWriteable, MessageTooLong };
790 pub const WriteError = BufferedConnection.WriteError || error{ NotWriteable, MessageTooLong };
816791
817792 pub const Writer = std.io.Writer(*Request, WriteError, write);
818793
......@@ -824,28 +799,16 @@ pub const Request = struct {
824799 pub fn write(req: *Request, bytes: []const u8) WriteError!usize {
825800 switch (req.headers.transfer_encoding) {
826801 .chunked => {
827 req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}) catch |err| {
828 req.client.last_error = .{ .write = err };
829 return error.WriteFailed;
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 };
802 try req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len});
803 try req.connection.data.conn.writeAll(bytes);
804 try req.connection.data.conn.writeAll("\r\n");
839805
840806 return bytes.len;
841807 },
842808 .content_length => |*len| {
843809 if (len.* < bytes.len) return error.MessageTooLong;
844810
845 const amt = req.connection.data.conn.write(bytes) catch |err| {
846 req.client.last_error = .{ .write = err };
847 return error.WriteFailed;
848 };
811 const amt = try req.connection.data.conn.write(bytes);
849812 len.* -= amt;
850813 return amt;
851814 },
......@@ -853,19 +816,39 @@ pub const Request = struct {
853816 }
854817 }
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
856828 /// 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 {
858 switch (req.headers.transfer_encoding) {
859 .chunked => req.connection.data.conn.writeAll("0\r\n\r\n") catch |err| {
860 req.client.last_error = .{ .write = err };
861 return error.WriteFailed;
862 },
829 pub fn finish(req: *Request) FinishError!void {
830 switch (req.transfer_encoding) {
831 .chunked => try req.connection.data.conn.writeAll("0\r\n\r\n"),
863832 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
864833 .none => {},
865834 }
866835 }
867836};
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
869852/// Release all associated resources with the client.
870853/// TODO: currently leaks all request allocated data
871854pub fn deinit(client: *Client) void {
......@@ -875,11 +858,11 @@ pub fn deinit(client: *Client) void {
875858 client.* = undefined;
876859}
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
880863/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open.
881864/// 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 {
883866 if (client.connection_pool.findConnection(.{
884867 .host = host,
885868 .port = port,
......@@ -891,9 +874,16 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
891874 errdefer client.allocator.destroy(conn);
892875 conn.* = .{ .data = undefined };
893876
894 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| {
895 client.last_error = .{ .connect = err };
896 return error.ConnectionFailed;
877 const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) {
878 error.ConnectionRefused => return error.ConnectionRefused,
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,
897887 };
898888 errdefer stream.close();
899889
......@@ -914,10 +904,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio
914904 conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client);
915905 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| {
918 client.last_error = .{ .tls = err };
919 return error.TlsInitializationFailed;
920 };
907 conn.data.buffered.conn.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed;
921908 // This is appropriate for HTTPS because the HTTP headers contain
922909 // the content length which is used to detect truncation attacks.
923910 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
929916 return conn;
930917}
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{
933947 UnsupportedUrlScheme,
934948 UriMissingHost,
935949
936 CertificateAuthorityBundleFailed,
937 WriteFailed,
950 CertificateBundleLoadFailure,
951 UnsupportedTransferEncoding,
938952};
939953
940954pub const Options = struct {
955 version: http.Version = .@"HTTP/1.1",
956
941957 handle_redirects: bool = true,
942958 max_redirects: u32 = 3,
943959 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 },
944960
961 /// Must be an already acquired connection.
962 connection: ?*ConnectionPool.Node = null,
963
945964 pub const HeaderStrategy = union(enum) {
946965 /// In this case, the client's Allocator will be used to store the
947966 /// entire HTTP header. This value is the maximum total size of
......@@ -965,7 +984,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
965984
966985/// Form and send a http request to a server.
967986/// 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 {
969988 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
970989
971990 const port: u16 = uri.port orelse switch (protocol) {
......@@ -980,22 +999,27 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
980999 defer client.ca_bundle_mutex.unlock();
9811000
9821001 if (client.next_https_rescan_certs) {
983 client.ca_bundle.rescan(client.allocator) catch |err| {
984 client.last_error = .{ .ca_bundle = err };
985 return error.CertificateAuthorityBundleFailed;
986 };
1002 client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure;
9871003 @atomicStore(bool, &client.next_https_rescan_certs, false, .Release);
9881004 }
9891005 }
9901006
1007 const conn = options.connection orelse try client.connect(host, port, protocol);
1008
9911009 var req: Request = .{
9921010 .uri = uri,
9931011 .client = client,
994 .connection = try client.connect(host, port, protocol),
1012 .connection = conn,
9951013 .headers = headers,
1014 .method = method,
1015 .version = options.version,
9961016 .redirects_left = options.max_redirects,
9971017 .handle_redirects = options.handle_redirects,
9981018 .response = .{
1019 .status = undefined,
1020 .reason = undefined,
1021 .version = undefined,
1022 .headers = undefined,
9991023 .parser = switch (options.header_strategy) {
10001024 .dynamic => |max| proto.HeadersParser.initDynamic(max),
10011025 .static => |buf| proto.HeadersParser.initStatic(buf),
......@@ -1007,14 +1031,6 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt
10071031
10081032 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
10181034 return req;
10191035}
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 {
2323
2424 pub const Protocol = enum { plain };
2525
26 pub fn read(conn: *Connection, buffer: []u8) !usize {
27 switch (conn.protocol) {
28 .plain => return conn.stream.read(buffer),
26 pub fn read(conn: *Connection, buffer: []u8) ReadError!usize {
27 return switch (conn.protocol) {
28 .plain => conn.stream.read(buffer),
2929 // .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 };
3135 }
3236
33 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) !usize {
34 switch (conn.protocol) {
35 .plain => return conn.stream.readAtLeast(buffer, len),
37 pub fn readAtLeast(conn: *Connection, buffer: []u8, len: usize) ReadError!usize {
38 return switch (conn.protocol) {
39 .plain => conn.stream.readAtLeast(buffer, len),
3640 // .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 };
3846 }
3947
40 pub const ReadError = net.Stream.ReadError;
48 pub const ReadError = error{
49 ConnectionTimedOut,
50 ConnectionResetByPeer,
51 UnexpectedReadFailure,
52 };
4153
4254 pub const Reader = std.io.Reader(*Connection, ReadError, read);
4355
......@@ -45,21 +57,31 @@ pub const Connection = struct {
4557 return Reader{ .context = conn };
4658 }
4759
48 pub fn writeAll(conn: *Connection, buffer: []const u8) !void {
49 switch (conn.protocol) {
50 .plain => return conn.stream.writeAll(buffer),
60 pub fn writeAll(conn: *Connection, buffer: []const u8) WriteError!void {
61 return switch (conn.protocol) {
62 .plain => conn.stream.writeAll(buffer),
5163 // .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 };
5368 }
5469
55 pub fn write(conn: *Connection, buffer: []const u8) !usize {
56 switch (conn.protocol) {
57 .plain => return conn.stream.write(buffer),
70 pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize {
71 return switch (conn.protocol) {
72 .plain => conn.stream.write(buffer),
5873 // .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 };
6078 }
6179
62 pub const WriteError = net.Stream.WriteError || error{};
80 pub const WriteError = error{
81 ConnectionResetByPeer,
82 UnexpectedWriteFailure,
83 };
84
6385 pub const Writer = std.io.Writer(*Connection, WriteError, write);
6486
6587 pub fn writer(conn: *Connection) Writer {
......@@ -155,136 +177,142 @@ pub const BufferedConnection = struct {
155177 }
156178};
157179
158/// A HTTP request originating from a client.
159pub const Request = struct {
160 pub const Headers = struct {
161 method: http.Method,
162 target: []const u8,
163 version: http.Version,
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 };
180/// The mode of transport for responses.
181pub const ResponseTransfer = union(enum) {
182 content_length: u64,
183 chunked: void,
184 none: void,
185};
180186
181 pub fn parse(bytes: []const u8) !Headers {
182 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
187/// The decompressor for request messages.
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;
185 if (first_line.len < 10)
186 return error.ShortHttpStatusLine;
193 deflate: DeflateDecompressor,
194 gzip: GzipDecompressor,
195 zstd: ZstdDecompressor,
196 none: void,
197};
187198
188 const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
189 const method_str = first_line[0..method_end];
190 const method = std.meta.stringToEnum(http.Method, method_str) orelse return error.UnknownHttpMethod;
199/// A HTTP request originating from a client.
200pub const Request = struct {
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;
193 if (version_start == method_end) return error.HttpHeadersInvalid;
213 pub fn parse(req: *Request, bytes: []const u8) ParseError!void {
214 var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n");
194215
195 const version_str = first_line[version_start + 1 ..];
196 if (version_str.len != 8) return error.HttpHeadersInvalid;
197 const version: http.Version = switch (int64(version_str[0..8])) {
198 int64("HTTP/1.0") => .@"HTTP/1.0",
199 int64("HTTP/1.1") => .@"HTTP/1.1",
200 else => return error.BadHttpVersion,
201 };
216 const first_line = it.next() orelse return error.HttpHeadersInvalid;
217 if (first_line.len < 10)
218 return error.ShortHttpStatusLine;
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 = .{
206 .method = method,
207 .target = target,
208 .version = version,
209 };
224 const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse return error.HttpHeadersInvalid;
225 if (version_start == method_end) return error.HttpHeadersInvalid;
210226
211 while (it.next()) |line| {
212 if (line.len == 0) return error.HttpHeadersInvalid;
213 switch (line[0]) {
214 ' ', '\t' => return error.HttpHeaderContinuationsUnsupported,
215 else => {},
216 }
227 const version_str = first_line[version_start + 1 ..];
228 if (version_str.len != 8) return error.HttpHeadersInvalid;
229 const version: http.Version = switch (int64(version_str[0..8])) {
230 int64("HTTP/1.0") => .@"HTTP/1.0",
231 int64("HTTP/1.1") => .@"HTTP/1.1",
232 else => return error.BadHttpVersion,
233 };
217234
218 var line_it = mem.tokenize(u8, line, ": ");
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 }
235 const target = first_line[method_end + 1 .. version_start];
242236
243 if (iter.next()) |second| {
244 if (headers.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
237 req.method = method;
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| {
249 headers.transfer_compression = ce;
250 } else {
251 return error.HttpTransferEncodingUnsupported;
252 }
248 var line_it = mem.tokenize(u8, line, ": ");
249 const header_name = line_it.next() orelse return error.HttpHeadersInvalid;
250 const header_value = line_it.rest();
251
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;
253273 }
274 }
254275
255 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
256 } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) {
257 if (headers.transfer_compression != null) return error.HttpHeadersInvalid;
276 if (iter.next()) |second| {
277 if (req.transfer_compression != null) return error.HttpTransferEncodingUnsupported;
258278
259 const trimmed = mem.trim(u8, header_value, " ");
279 const trimmed = mem.trim(u8, second, " ");
260280
261281 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
262 headers.transfer_compression = ce;
282 req.transfer_compression = ce;
263283 } else {
264284 return error.HttpTransferEncodingUnsupported;
265285 }
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;
276286 }
277 }
278287
279 return headers;
280 }
288 if (iter.next()) |_| return error.HttpTransferEncodingUnsupported;
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 {
283 return @bitCast(u64, array.*);
294 if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| {
295 req.transfer_compression = ce;
296 } else {
297 return error.HttpTransferEncodingUnsupported;
298 }
299 }
284300 }
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,
288316 parser: proto.HeadersParser,
289317 compression: Compression = .none,
290318};
......@@ -295,23 +323,17 @@ pub const Request = struct {
295323/// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /]
296324/// \ -> read /
297325pub const Response = struct {
298 pub const Headers = struct {
299 version: http.Version = .@"HTTP/1.1",
300 status: http.Status = .ok,
301 reason: ?[]const u8 = null,
326 version: http.Version = .@"HTTP/1.1",
327 status: http.Status = .ok,
328 reason: ?[]const u8 = null,
302329
303 server: ?[]const u8 = "zig (std.http)",
304 connection: http.Connection = .keep_alive,
305 transfer_encoding: RequestTransfer = .none,
306
307 custom: []const http.CustomHeader = &[_]http.CustomHeader{},
308 };
330 transfer_encoding: ResponseTransfer = .none,
309331
310332 server: *Server,
311333 address: net.Address,
312334 connection: BufferedConnection,
313335
314 headers: Headers = .{},
336 headers: http.Headers,
315337 request: Request,
316338
317339 /// 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 {
341363 }
342364 }
343365
366 pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength };
367
344368 /// Send the response headers.
345369 pub fn do(res: *Response) !void {
346370 var buffered = std.io.bufferedWriter(res.connection.writer());
347371 const w = buffered.writer();
348372
349 try w.writeAll(@tagName(res.headers.version));
373 try w.writeAll(@tagName(res.version));
350374 try w.writeByte(' ');
351 try w.print("{d}", .{@enumToInt(res.headers.status)});
375 try w.print("{d}", .{@enumToInt(res.status)});
352376 try w.writeByte(' ');
353 if (res.headers.reason) |reason| {
377 if (res.reason) |reason| {
354378 try w.writeAll(reason);
355 } else if (res.headers.status.phrase()) |phrase| {
379 } else if (res.status.phrase()) |phrase| {
356380 try w.writeAll(phrase);
357381 }
382 try w.writeAll("\r\n");
358383
359 if (res.headers.server) |server| {
360 try w.writeAll("\r\nServer: ");
361 try w.writeAll(server);
384 if (!res.headers.contains("server")) {
385 try w.writeAll("Server: zig (std.http)\r\n");
362386 }
363387
364 if (res.headers.connection == .close) {
365 try w.writeAll("\r\nConnection: close");
366 } else {
367 try w.writeAll("\r\nConnection: keep-alive");
388 if (!res.headers.contains("connection")) {
389 try w.writeAll("Connection: keep-alive\r\n");
368390 }
369391
370 switch (res.headers.transfer_encoding) {
371 .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"),
372 .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}),
373 .none => {},
374 }
392 const has_transfer_encoding = res.headers.contains("transfer-encoding");
393 const has_content_length = res.headers.contains("content-length");
375394
376 for (res.headers.custom) |header| {
377 try w.writeAll("\r\n");
378 try w.writeAll(header.name);
379 try w.writeAll(": ");
380 try w.writeAll(header.value);
395 if (!has_transfer_encoding and !has_content_length) {
396 switch (res.transfer_encoding) {
397 .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"),
398 .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}),
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 }
381416 }
382417
383 try w.writeAll("\r\n\r\n");
418 try w.print("{}", .{res.headers});
419
420 try w.writeAll("\r\n");
384421
385422 try buffered.flush();
386423 }
......@@ -393,23 +430,23 @@ pub const Response = struct {
393430 return .{ .context = res };
394431 }
395432
396 pub fn transferRead(res: *Response, buf: []u8) TransferReadError!usize {
397 if (res.request.parser.isComplete()) return 0;
433 fn transferRead(res: *Response, buf: []u8) TransferReadError!usize {
434 if (res.request.parser.done) return 0;
398435
399436 var index: usize = 0;
400437 while (index == 0) {
401438 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;
403440 index += amt;
404441 }
405442
406443 return index;
407444 }
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
411448 /// 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 {
413450 while (true) {
414451 try res.connection.fill();
415452
......@@ -419,22 +456,28 @@ pub const Response = struct {
419456 if (res.request.parser.state.isContent()) break;
420457 }
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) {
425468 res.connection.conn.closing = false;
426469 } else {
427470 res.connection.conn.closing = true;
428471 }
429472
430 if (res.request.headers.transfer_encoding) |te| {
473 if (res.request.transfer_encoding) |te| {
431474 switch (te) {
432475 .chunked => {
433476 res.request.parser.next_chunk_length = 0;
434477 res.request.parser.state = .chunk_head_size;
435478 },
436479 }
437 } else if (res.request.headers.content_length) |cl| {
480 } else if (res.request.content_length) |cl| {
438481 res.request.parser.next_chunk_length = cl;
439482
440483 if (cl == 0) res.request.parser.done = true;
......@@ -443,13 +486,13 @@ pub const Response = struct {
443486 }
444487
445488 if (!res.request.parser.done) {
446 if (res.request.headers.transfer_compression) |tc| switch (tc) {
489 if (res.request.transfer_compression) |tc| switch (tc) {
447490 .compress => return error.CompressionNotSupported,
448491 .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,
450493 },
451494 .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,
453496 },
454497 .zstd => res.request.compression = .{
455498 .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()),
......@@ -458,7 +501,7 @@ pub const Response = struct {
458501 }
459502 }
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
463506 pub const Reader = std.io.Reader(*Response, ReadError, read);
464507
......@@ -467,12 +510,33 @@ pub const Response = struct {
467510 }
468511
469512 pub fn read(res: *Response, buffer: []u8) ReadError!usize {
470 return switch (res.request.compression) {
471 .deflate => |*deflate| try deflate.read(buffer),
472 .gzip => |*gzip| try gzip.read(buffer),
473 .zstd => |*zstd| try zstd.read(buffer),
513 const out_index = switch (res.request.compression) {
514 .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure,
515 .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure,
516 .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure,
474517 else => try res.transferRead(buffer),
475518 };
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;
476540 }
477541
478542 pub fn readAll(res: *Response, buffer: []u8) !usize {
......@@ -495,7 +559,7 @@ pub const Response = struct {
495559
496560 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent.
497561 pub fn write(res: *Response, bytes: []const u8) WriteError!usize {
498 switch (res.headers.transfer_encoding) {
562 switch (res.transfer_encoding) {
499563 .chunked => {
500564 try res.connection.writer().print("{x}\r\n", .{bytes.len});
501565 try res.connection.writeAll(bytes);
......@@ -514,9 +578,18 @@ pub const Response = struct {
514578 }
515579 }
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
517590 /// 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 {
519 switch (res.headers.transfer_encoding) {
591 pub fn finish(res: *Response) FinishError!void {
592 switch (res.transfer_encoding) {
520593 .chunked => try res.connection.writeAll("0\r\n\r\n"),
521594 .content_length => |len| if (len != 0) return error.MessageNotCompleted,
522595 .none => {},
......@@ -524,25 +597,6 @@ pub const Response = struct {
524597 }
525598};
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
546600pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server {
547601 return .{
548602 .allocator = allocator,
......@@ -588,7 +642,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response {
588642 .stream = in.stream,
589643 .protocol = .plain,
590644 } },
645 .headers = .{ .allocator = server.allocator },
591646 .request = .{
647 .version = undefined,
648 .method = undefined,
649 .target = undefined,
592650 .parser = switch (options) {
593651 .dynamic => |max| proto.HeadersParser.initDynamic(max),
594652 .static => |buf| proto.HeadersParser.initStatic(buf),
lib/std/http/protocol.zig+1-1
......@@ -1,4 +1,4 @@
1const std = @import("std");
1const std = @import("../std.zig");
22const testing = std.testing;
33const mem = std.mem;
44
src/Package.zig+6-1
......@@ -479,9 +479,14 @@ fn fetchAndUnpack(
479479 };
480480 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, .{});
483486 defer req.deinit();
484487
488 try req.start();
489
485490 try req.do();
486491
487492 if (mem.endsWith(u8, uri.path, ".tar.gz")) {