| author | |
| committer | |
| log | 0eebc258809beac9779af48216b01c5c20cbbfea |
| tree | a912eef3d99d3e2371484fd60f6b32a7e11d509b |
| parent | 77fdd76c16196441dce0f38ccea2eae01436c4be |
| parent | a23c8662b41cf6954d8294ea316fb28a88481a7e |
| signature |
std.http: curated error sets and custom Headers10 files changed, 1145 insertions(+), 576 deletions(-)
lib/std/Uri.zig+76| ... | ... | @@ -27,6 +27,18 @@ pub fn escapeQuery(allocator: std.mem.Allocator, input: []const u8) error{OutOfM |
| 27 | 27 | return escapeStringWithFn(allocator, input, isQueryChar); |
| 28 | 28 | } |
| 29 | 29 | |
| 30 | pub fn writeEscapedString(writer: anytype, input: []const u8) !void { | |
| 31 | return writeEscapedStringWithFn(writer, input, isUnreserved); | |
| 32 | } | |
| 33 | ||
| 34 | pub fn writeEscapedPath(writer: anytype, input: []const u8) !void { | |
| 35 | return writeEscapedStringWithFn(writer, input, isPathChar); | |
| 36 | } | |
| 37 | ||
| 38 | pub fn writeEscapedQuery(writer: anytype, input: []const u8) !void { | |
| 39 | return writeEscapedStringWithFn(writer, input, isQueryChar); | |
| 40 | } | |
| 41 | ||
| 30 | 42 | pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) std.mem.Allocator.Error![]const u8 { |
| 31 | 43 | var outsize: usize = 0; |
| 32 | 44 | for (input) |c| { |
| ... | ... | @@ -52,6 +64,16 @@ pub fn escapeStringWithFn(allocator: std.mem.Allocator, input: []const u8, compt |
| 52 | 64 | return output; |
| 53 | 65 | } |
| 54 | 66 | |
| 67 | pub fn writeEscapedStringWithFn(writer: anytype, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) @TypeOf(writer).Error!void { | |
| 68 | for (input) |c| { | |
| 69 | if (keepUnescaped(c)) { | |
| 70 | try writer.writeByte(c); | |
| 71 | } else { | |
| 72 | try writer.print("%{X:0>2}", .{c}); | |
| 73 | } | |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 55 | 77 | /// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies |
| 56 | 78 | /// them to the output. |
| 57 | 79 | pub fn unescapeString(allocator: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]const u8 { |
| ... | ... | @@ -184,6 +206,60 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri { |
| 184 | 206 | return uri; |
| 185 | 207 | } |
| 186 | 208 | |
| 209 | pub fn format( | |
| 210 | uri: Uri, | |
| 211 | comptime fmt: []const u8, | |
| 212 | options: std.fmt.FormatOptions, | |
| 213 | writer: anytype, | |
| 214 | ) @TypeOf(writer).Error!void { | |
| 215 | _ = options; | |
| 216 | ||
| 217 | const needs_absolute = comptime std.mem.indexOf(u8, fmt, "+") != null; | |
| 218 | const needs_path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0; | |
| 219 | ||
| 220 | if (needs_absolute) { | |
| 221 | try writer.writeAll(uri.scheme); | |
| 222 | try writer.writeAll(":"); | |
| 223 | if (uri.host) |host| { | |
| 224 | try writer.writeAll("//"); | |
| 225 | ||
| 226 | if (uri.user) |user| { | |
| 227 | try writer.writeAll(user); | |
| 228 | if (uri.password) |password| { | |
| 229 | try writer.writeAll(":"); | |
| 230 | try writer.writeAll(password); | |
| 231 | } | |
| 232 | try writer.writeAll("@"); | |
| 233 | } | |
| 234 | ||
| 235 | try writer.writeAll(host); | |
| 236 | ||
| 237 | if (uri.port) |port| { | |
| 238 | try writer.writeAll(":"); | |
| 239 | try std.fmt.formatInt(port, 10, .lower, .{}, writer); | |
| 240 | } | |
| 241 | } | |
| 242 | } | |
| 243 | ||
| 244 | if (needs_path) { | |
| 245 | if (uri.path.len == 0) { | |
| 246 | try writer.writeAll("/"); | |
| 247 | } else { | |
| 248 | try Uri.writeEscapedPath(writer, uri.path); | |
| 249 | } | |
| 250 | ||
| 251 | if (uri.query) |q| { | |
| 252 | try writer.writeAll("?"); | |
| 253 | try Uri.writeEscapedQuery(writer, q); | |
| 254 | } | |
| 255 | ||
| 256 | if (uri.fragment) |f| { | |
| 257 | try writer.writeAll("#"); | |
| 258 | try Uri.writeEscapedQuery(writer, f); | |
| 259 | } | |
| 260 | } | |
| 261 | } | |
| 262 | ||
| 187 | 263 | /// Parses the URI or returns an error. |
| 188 | 264 | /// The return value will contain unescaped strings pointing into the |
| 189 | 265 | /// original `text`. Each component that is provided, will be non-`null`. |
lib/std/crypto/Certificate.zig+21-11| ... | ... | @@ -371,7 +371,9 @@ test "Parsed.checkHostName" { |
| 371 | 371 | try expectEqual(false, Parsed.checkHostName("lang.org", "zig*.org")); |
| 372 | 372 | } |
| 373 | 373 | |
| 374 | pub fn parse(cert: Certificate) !Parsed { | |
| 374 | pub const ParseError = der.Element.ParseElementError || ParseVersionError || ParseTimeError || ParseEnumError || ParseBitStringError; | |
| 375 | ||
| 376 | pub fn parse(cert: Certificate) ParseError!Parsed { | |
| 375 | 377 | const cert_bytes = cert.buffer; |
| 376 | 378 | const certificate = try der.Element.parse(cert_bytes, cert.index); |
| 377 | 379 | const tbs_certificate = try der.Element.parse(cert_bytes, certificate.slice.start); |
| ... | ... | @@ -514,14 +516,18 @@ pub fn contents(cert: Certificate, elem: der.Element) []const u8 { |
| 514 | 516 | return cert.buffer[elem.slice.start..elem.slice.end]; |
| 515 | 517 | } |
| 516 | 518 | |
| 519 | pub const ParseBitStringError = error{ CertificateFieldHasWrongDataType, CertificateHasInvalidBitString }; | |
| 520 | ||
| 517 | 521 | pub fn parseBitString(cert: Certificate, elem: der.Element) !der.Element.Slice { |
| 518 | 522 | if (elem.identifier.tag != .bitstring) return error.CertificateFieldHasWrongDataType; |
| 519 | 523 | if (cert.buffer[elem.slice.start] != 0) return error.CertificateHasInvalidBitString; |
| 520 | 524 | return .{ .start = elem.slice.start + 1, .end = elem.slice.end }; |
| 521 | 525 | } |
| 522 | 526 | |
| 527 | pub const ParseTimeError = error{ CertificateTimeInvalid, CertificateFieldHasWrongDataType }; | |
| 528 | ||
| 523 | 529 | /// Returns number of seconds since epoch. |
| 524 | pub fn parseTime(cert: Certificate, elem: der.Element) !u64 { | |
| 530 | pub fn parseTime(cert: Certificate, elem: der.Element) ParseTimeError!u64 { | |
| 525 | 531 | const bytes = cert.contents(elem); |
| 526 | 532 | switch (elem.identifier.tag) { |
| 527 | 533 | .utc_time => { |
| ... | ... | @@ -647,34 +653,38 @@ test parseYear4 { |
| 647 | 653 | try expectError(error.CertificateTimeInvalid, parseYear4("crap")); |
| 648 | 654 | } |
| 649 | 655 | |
| 650 | pub fn parseAlgorithm(bytes: []const u8, element: der.Element) !Algorithm { | |
| 656 | pub fn parseAlgorithm(bytes: []const u8, element: der.Element) ParseEnumError!Algorithm { | |
| 651 | 657 | return parseEnum(Algorithm, bytes, element); |
| 652 | 658 | } |
| 653 | 659 | |
| 654 | pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) !AlgorithmCategory { | |
| 660 | pub fn parseAlgorithmCategory(bytes: []const u8, element: der.Element) ParseEnumError!AlgorithmCategory { | |
| 655 | 661 | return parseEnum(AlgorithmCategory, bytes, element); |
| 656 | 662 | } |
| 657 | 663 | |
| 658 | pub fn parseAttribute(bytes: []const u8, element: der.Element) !Attribute { | |
| 664 | pub fn parseAttribute(bytes: []const u8, element: der.Element) ParseEnumError!Attribute { | |
| 659 | 665 | return parseEnum(Attribute, bytes, element); |
| 660 | 666 | } |
| 661 | 667 | |
| 662 | pub fn parseNamedCurve(bytes: []const u8, element: der.Element) !NamedCurve { | |
| 668 | pub fn parseNamedCurve(bytes: []const u8, element: der.Element) ParseEnumError!NamedCurve { | |
| 663 | 669 | return parseEnum(NamedCurve, bytes, element); |
| 664 | 670 | } |
| 665 | 671 | |
| 666 | pub fn parseExtensionId(bytes: []const u8, element: der.Element) !ExtensionId { | |
| 672 | pub fn parseExtensionId(bytes: []const u8, element: der.Element) ParseEnumError!ExtensionId { | |
| 667 | 673 | return parseEnum(ExtensionId, bytes, element); |
| 668 | 674 | } |
| 669 | 675 | |
| 670 | fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) !E { | |
| 676 | pub const ParseEnumError = error{ CertificateFieldHasWrongDataType, CertificateHasUnrecognizedObjectId }; | |
| 677 | ||
| 678 | fn parseEnum(comptime E: type, bytes: []const u8, element: der.Element) ParseEnumError!E { | |
| 671 | 679 | if (element.identifier.tag != .object_identifier) |
| 672 | 680 | return error.CertificateFieldHasWrongDataType; |
| 673 | 681 | const oid_bytes = bytes[element.slice.start..element.slice.end]; |
| 674 | 682 | return E.map.get(oid_bytes) orelse return error.CertificateHasUnrecognizedObjectId; |
| 675 | 683 | } |
| 676 | 684 | |
| 677 | pub fn parseVersion(bytes: []const u8, version_elem: der.Element) !Version { | |
| 685 | pub const ParseVersionError = error{ UnsupportedCertificateVersion, CertificateFieldHasInvalidLength }; | |
| 686 | ||
| 687 | pub fn parseVersion(bytes: []const u8, version_elem: der.Element) ParseVersionError!Version { | |
| 678 | 688 | if (@bitCast(u8, version_elem.identifier) != 0xa0) |
| 679 | 689 | return .v1; |
| 680 | 690 | |
| ... | ... | @@ -861,9 +871,9 @@ pub const der = struct { |
| 861 | 871 | pub const empty: Slice = .{ .start = 0, .end = 0 }; |
| 862 | 872 | }; |
| 863 | 873 | |
| 864 | pub const ParseError = error{CertificateFieldHasInvalidLength}; | |
| 874 | pub const ParseElementError = error{CertificateFieldHasInvalidLength}; | |
| 865 | 875 | |
| 866 | pub fn parse(bytes: []const u8, index: u32) ParseError!Element { | |
| 876 | pub fn parse(bytes: []const u8, index: u32) ParseElementError!Element { | |
| 867 | 877 | var i = index; |
| 868 | 878 | const identifier = @bitCast(Identifier, bytes[i]); |
| 869 | 879 | i += 1; |
lib/std/crypto/Certificate/Bundle.zig+27-10| ... | ... | @@ -50,11 +50,13 @@ pub fn deinit(cb: *Bundle, gpa: Allocator) void { |
| 50 | 50 | cb.* = undefined; |
| 51 | 51 | } |
| 52 | 52 | |
| 53 | pub const RescanError = RescanLinuxError || RescanMacError || RescanWindowsError; | |
| 54 | ||
| 53 | 55 | /// Clears the set of certificates and then scans the host operating system |
| 54 | 56 | /// file system standard locations for certificates. |
| 55 | 57 | /// For operating systems that do not have standard CA installations to be |
| 56 | 58 | /// found, this function clears the set of certificates. |
| 57 | pub fn rescan(cb: *Bundle, gpa: Allocator) !void { | |
| 59 | pub fn rescan(cb: *Bundle, gpa: Allocator) RescanError!void { | |
| 58 | 60 | switch (builtin.os.tag) { |
| 59 | 61 | .linux => return rescanLinux(cb, gpa), |
| 60 | 62 | .macos => return rescanMac(cb, gpa), |
| ... | ... | @@ -64,8 +66,11 @@ pub fn rescan(cb: *Bundle, gpa: Allocator) !void { |
| 64 | 66 | } |
| 65 | 67 | |
| 66 | 68 | pub const rescanMac = @import("Bundle/macos.zig").rescanMac; |
| 69 | pub const RescanMacError = @import("Bundle/macos.zig").RescanMacError; | |
| 70 | ||
| 71 | pub const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError; | |
| 67 | 72 | |
| 68 | pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void { | |
| 73 | pub fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { | |
| 69 | 74 | // Possible certificate files; stop after finding one. |
| 70 | 75 | const cert_file_paths = [_][]const u8{ |
| 71 | 76 | "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. |
| ... | ... | @@ -107,7 +112,9 @@ pub fn rescanLinux(cb: *Bundle, gpa: Allocator) !void { |
| 107 | 112 | cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); |
| 108 | 113 | } |
| 109 | 114 | |
| 110 | pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void { | |
| 115 | pub const RescanWindowsError = Allocator.Error || ParseCertError || std.os.UnexpectedError || error{FileNotFound}; | |
| 116 | ||
| 117 | pub fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { | |
| 111 | 118 | cb.bytes.clearRetainingCapacity(); |
| 112 | 119 | cb.map.clearRetainingCapacity(); |
| 113 | 120 | |
| ... | ... | @@ -132,12 +139,14 @@ pub fn rescanWindows(cb: *Bundle, gpa: Allocator) !void { |
| 132 | 139 | cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); |
| 133 | 140 | } |
| 134 | 141 | |
| 142 | pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError; | |
| 143 | ||
| 135 | 144 | pub fn addCertsFromDirPath( |
| 136 | 145 | cb: *Bundle, |
| 137 | 146 | gpa: Allocator, |
| 138 | 147 | dir: fs.Dir, |
| 139 | 148 | sub_dir_path: []const u8, |
| 140 | ) !void { | |
| 149 | ) AddCertsFromDirPathError!void { | |
| 141 | 150 | var iterable_dir = try dir.openIterableDir(sub_dir_path, .{}); |
| 142 | 151 | defer iterable_dir.close(); |
| 143 | 152 | return addCertsFromDir(cb, gpa, iterable_dir); |
| ... | ... | @@ -147,14 +156,16 @@ pub fn addCertsFromDirPathAbsolute( |
| 147 | 156 | cb: *Bundle, |
| 148 | 157 | gpa: Allocator, |
| 149 | 158 | abs_dir_path: []const u8, |
| 150 | ) !void { | |
| 159 | ) AddCertsFromDirPathError!void { | |
| 151 | 160 | assert(fs.path.isAbsolute(abs_dir_path)); |
| 152 | 161 | var iterable_dir = try fs.openIterableDirAbsolute(abs_dir_path, .{}); |
| 153 | 162 | defer iterable_dir.close(); |
| 154 | 163 | return addCertsFromDir(cb, gpa, iterable_dir); |
| 155 | 164 | } |
| 156 | 165 | |
| 157 | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) !void { | |
| 166 | pub const AddCertsFromDirError = AddCertsFromFilePathError; | |
| 167 | ||
| 168 | pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir) AddCertsFromDirError!void { | |
| 158 | 169 | var it = iterable_dir.iterate(); |
| 159 | 170 | while (try it.next()) |entry| { |
| 160 | 171 | switch (entry.kind) { |
| ... | ... | @@ -166,11 +177,13 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.IterableDir |
| 166 | 177 | } |
| 167 | 178 | } |
| 168 | 179 | |
| 180 | pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError; | |
| 181 | ||
| 169 | 182 | pub fn addCertsFromFilePathAbsolute( |
| 170 | 183 | cb: *Bundle, |
| 171 | 184 | gpa: Allocator, |
| 172 | 185 | abs_file_path: []const u8, |
| 173 | ) !void { | |
| 186 | ) AddCertsFromFilePathError!void { | |
| 174 | 187 | assert(fs.path.isAbsolute(abs_file_path)); |
| 175 | 188 | var file = try fs.openFileAbsolute(abs_file_path, .{}); |
| 176 | 189 | defer file.close(); |
| ... | ... | @@ -182,13 +195,15 @@ pub fn addCertsFromFilePath( |
| 182 | 195 | gpa: Allocator, |
| 183 | 196 | dir: fs.Dir, |
| 184 | 197 | sub_file_path: []const u8, |
| 185 | ) !void { | |
| 198 | ) AddCertsFromFilePathError!void { | |
| 186 | 199 | var file = try dir.openFile(sub_file_path, .{}); |
| 187 | 200 | defer file.close(); |
| 188 | 201 | return addCertsFromFile(cb, gpa, file); |
| 189 | 202 | } |
| 190 | 203 | |
| 191 | pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void { | |
| 204 | pub const AddCertsFromFileError = Allocator.Error || fs.File.GetSeekPosError || fs.File.ReadError || ParseCertError || std.base64.Error || error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; | |
| 205 | ||
| 206 | pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void { | |
| 192 | 207 | const size = try file.getEndPos(); |
| 193 | 208 | |
| 194 | 209 | // We borrow `bytes` as a temporary buffer for the base64-encoded data. |
| ... | ... | @@ -222,7 +237,9 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) !void { |
| 222 | 237 | } |
| 223 | 238 | } |
| 224 | 239 | |
| 225 | pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) !void { | |
| 240 | pub const ParseCertError = Allocator.Error || Certificate.ParseError; | |
| 241 | ||
| 242 | pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) ParseCertError!void { | |
| 226 | 243 | // Even though we could only partially parse the certificate to find |
| 227 | 244 | // the subject name, we pre-parse all of them to make sure and only |
| 228 | 245 | // include in the bundle ones that we know will parse. This way we can |
lib/std/crypto/Certificate/Bundle/macos.zig+3-1| ... | ... | @@ -5,7 +5,9 @@ const mem = std.mem; |
| 5 | 5 | const Allocator = std.mem.Allocator; |
| 6 | 6 | const Bundle = @import("../Bundle.zig"); |
| 7 | 7 | |
| 8 | pub fn rescanMac(cb: *Bundle, gpa: Allocator) !void { | |
| 8 | pub const RescanMacError = Allocator.Error || fs.File.OpenError || fs.File.ReadError || fs.File.SeekError || Bundle.ParseCertError || error{EndOfStream}; | |
| 9 | ||
| 10 | pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { | |
| 9 | 11 | cb.bytes.clearRetainingCapacity(); |
| 10 | 12 | cb.map.clearRetainingCapacity(); |
| 11 | 13 |
lib/std/http.zig+4-5| ... | ... | @@ -1,6 +1,10 @@ |
| 1 | 1 | pub const Client = @import("http/Client.zig"); |
| 2 | 2 | pub const Server = @import("http/Server.zig"); |
| 3 | 3 | pub const protocol = @import("http/protocol.zig"); |
| 4 | const headers = @import("http/Headers.zig"); | |
| 5 | ||
| 6 | pub const Headers = headers.Headers; | |
| 7 | pub const Field = headers.Field; | |
| 4 | 8 | |
| 5 | 9 | pub const Version = enum { |
| 6 | 10 | @"HTTP/1.0", |
| ... | ... | @@ -265,11 +269,6 @@ pub const Connection = enum { |
| 265 | 269 | close, |
| 266 | 270 | }; |
| 267 | 271 | |
| 268 | pub const CustomHeader = struct { | |
| 269 | name: []const u8, | |
| 270 | value: []const u8, | |
| 271 | }; | |
| 272 | ||
| 273 | 272 | const std = @import("std.zig"); |
| 274 | 273 | |
| 275 | 274 | test { |
lib/std/http/Client.zig+368-352| ... | ... | @@ -25,48 +25,7 @@ next_https_rescan_certs: bool = true, |
| 25 | 25 | /// The pool of connections that can be reused (and currently in use). |
| 26 | 26 | connection_pool: ConnectionPool = .{}, |
| 27 | 27 | |
| 28 | /// The last error that occurred on this client. This is not threadsafe, do not expect it to be completely accurate. | |
| 29 | last_error: ?ExtraError = null, | |
| 30 | ||
| 31 | pub 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 | }; | |
| 28 | proxy: ?HttpProxy = null, | |
| 70 | 29 | |
| 71 | 30 | /// A set of linked lists of connections that can be reused. |
| 72 | 31 | pub const ConnectionPool = struct { |
| ... | ... | @@ -82,6 +41,7 @@ pub const ConnectionPool = struct { |
| 82 | 41 | host: []u8, |
| 83 | 42 | port: u16, |
| 84 | 43 | |
| 44 | proxied: bool = false, | |
| 85 | 45 | closing: bool = false, |
| 86 | 46 | |
| 87 | 47 | pub fn deinit(self: *StoredConnection, client: *Client) void { |
| ... | ... | @@ -158,7 +118,12 @@ pub const ConnectionPool = struct { |
| 158 | 118 | return client.allocator.destroy(popped); |
| 159 | 119 | } |
| 160 | 120 | |
| 161 | pool.free.append(node); | |
| 121 | if (node.data.proxied) { | |
| 122 | pool.free.prepend(node); // proxied connections go to the end of the queue, always try direct connections first | |
| 123 | } else { | |
| 124 | pool.free.append(node); | |
| 125 | } | |
| 126 | ||
| 162 | 127 | pool.free_len += 1; |
| 163 | 128 | } |
| 164 | 129 | |
| ... | ... | @@ -202,30 +167,38 @@ pub const Connection = struct { |
| 202 | 167 | |
| 203 | 168 | pub const Protocol = enum { plain, tls }; |
| 204 | 169 | |
| 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 | }; | |
| 210 | 181 | } |
| 211 | 182 | |
| 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 | }; | |
| 217 | 194 | } |
| 218 | 195 | |
| 219 | pub const ReadError = net.Stream.ReadError || error{ | |
| 220 | TlsConnectionTruncated, | |
| 221 | TlsRecordOverflow, | |
| 222 | TlsDecodeError, | |
| 196 | pub const ReadError = error{ | |
| 197 | TlsFailure, | |
| 223 | 198 | TlsAlert, |
| 224 | TlsBadRecordMac, | |
| 225 | Overflow, | |
| 226 | TlsBadLength, | |
| 227 | TlsIllegalParameter, | |
| 228 | TlsUnexpectedMessage, | |
| 199 | ConnectionTimedOut, | |
| 200 | ConnectionResetByPeer, | |
| 201 | UnexpectedReadFailure, | |
| 229 | 202 | }; |
| 230 | 203 | |
| 231 | 204 | pub const Reader = std.io.Reader(*Connection, ReadError, read); |
| ... | ... | @@ -235,20 +208,30 @@ pub const Connection = struct { |
| 235 | 208 | } |
| 236 | 209 | |
| 237 | 210 | 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 | }; | |
| 242 | 218 | } |
| 243 | 219 | |
| 244 | 220 | 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 | }; | |
| 249 | 228 | } |
| 250 | 229 | |
| 251 | pub const WriteError = net.Stream.WriteError || error{}; | |
| 230 | pub const WriteError = error{ | |
| 231 | ConnectionResetByPeer, | |
| 232 | UnexpectedWriteFailure, | |
| 233 | }; | |
| 234 | ||
| 252 | 235 | pub const Writer = std.io.Writer(*Connection, WriteError, write); |
| 253 | 236 | |
| 254 | 237 | pub fn writer(conn: *Connection) Writer { |
| ... | ... | @@ -371,140 +354,125 @@ pub const Compression = union(enum) { |
| 371 | 354 | |
| 372 | 355 | /// A HTTP response originating from a server. |
| 373 | 356 | pub 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 | }; | |
| 421 | 367 | |
| 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"); | |
| 449 | 370 | |
| 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; | |
| 452 | 374 | |
| 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 | } | |
| 454 | 394 | |
| 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; | |
| 460 | 420 | } |
| 421 | } | |
| 461 | 422 | |
| 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; | |
| 465 | 425 | |
| 466 | const trimmed = mem.trim(u8, header_value, " "); | |
| 426 | const trimmed = mem.trim(u8, second, " "); | |
| 467 | 427 | |
| 468 | 428 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 469 | headers.transfer_compression = ce; | |
| 429 | res.transfer_compression = ce; | |
| 470 | 430 | } else { |
| 471 | 431 | return error.HttpTransferEncodingUnsupported; |
| 472 | 432 | } |
| 473 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 474 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 475 | headers.connection = .keep_alive; | |
| 476 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 477 | headers.connection = .close; | |
| 478 | } else { | |
| 479 | return error.HttpConnectionHeaderUnsupported; | |
| 480 | } | |
| 481 | } else if (std.ascii.eqlIgnoreCase(header_name, "upgrade")) { | |
| 482 | headers.upgrade = header_value; | |
| 483 | 433 | } |
| 484 | } | |
| 485 | 434 | |
| 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; | |
| 488 | 438 | |
| 489 | inline fn int64(array: *const [8]u8) u64 { | |
| 490 | return @bitCast(u64, array.*); | |
| 491 | } | |
| 439 | const trimmed = mem.trim(u8, header_value, " "); | |
| 492 | 440 | |
| 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 | } | |
| 497 | 447 | } |
| 448 | } | |
| 498 | 449 | |
| 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 | } | |
| 506 | 453 | |
| 507 | headers: Headers = undefined, | |
| 454 | fn parseInt3(nnn: @Vector(3, u8)) u10 { | |
| 455 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 456 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 457 | return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); | |
| 458 | } | |
| 459 | ||
| 460 | test parseInt3 { | |
| 461 | const expectEqual = testing.expectEqual; | |
| 462 | try expectEqual(@as(u10, 0), parseInt3("000".*)); | |
| 463 | try expectEqual(@as(u10, 418), parseInt3("418".*)); | |
| 464 | try expectEqual(@as(u10, 999), parseInt3("999".*)); | |
| 465 | } | |
| 466 | ||
| 467 | version: http.Version, | |
| 468 | status: http.Status, | |
| 469 | reason: []const u8, | |
| 470 | ||
| 471 | content_length: ?u64 = null, | |
| 472 | transfer_encoding: ?http.TransferEncoding = null, | |
| 473 | transfer_compression: ?http.ContentEncoding = null, | |
| 474 | ||
| 475 | headers: http.Headers, | |
| 508 | 476 | parser: proto.HeadersParser, |
| 509 | 477 | compression: Compression = .none, |
| 510 | 478 | skip: bool = false, |
| ... | ... | @@ -514,22 +482,14 @@ pub const Response = struct { |
| 514 | 482 | /// |
| 515 | 483 | /// Order of operations: request[ -> write -> finish] -> do -> read |
| 516 | 484 | pub const Request = struct { |
| 517 | pub const Headers = struct { | |
| 518 | version: http.Version = .@"HTTP/1.1", | |
| 519 | method: http.Method = .GET, | |
| 520 | user_agent: []const u8 = "zig (std.http)", | |
| 521 | connection: http.Connection = .keep_alive, | |
| 522 | transfer_encoding: RequestTransfer = .none, | |
| 523 | ||
| 524 | custom: []const http.CustomHeader = &[_]http.CustomHeader{}, | |
| 525 | }; | |
| 526 | ||
| 527 | 485 | uri: Uri, |
| 528 | 486 | client: *Client, |
| 529 | 487 | 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, | |
| 533 | 493 | |
| 534 | 494 | redirects_left: u32, |
| 535 | 495 | handle_redirects: bool, |
| ... | ... | @@ -549,80 +509,104 @@ pub const Request = struct { |
| 549 | 509 | } |
| 550 | 510 | |
| 551 | 511 | if (req.response.parser.header_bytes_owned) { |
| 512 | req.response.headers.deinit(); | |
| 552 | 513 | req.response.parser.header_bytes.deinit(req.client.allocator); |
| 553 | 514 | } |
| 554 | 515 | |
| 555 | 516 | if (!req.response.parser.done) { |
| 556 | 517 | // If the response wasn't fully read, then we need to close the connection. |
| 557 | 518 | req.connection.data.closing = true; |
| 558 | req.client.connection_pool.release(req.client, req.connection); | |
| 559 | 519 | } |
| 560 | 520 | |
| 521 | req.client.connection_pool.release(req.client, req.connection); | |
| 522 | ||
| 561 | 523 | req.arena.deinit(); |
| 562 | 524 | req.* = undefined; |
| 563 | 525 | } |
| 564 | 526 | |
| 565 | pub fn start(req: *Request, uri: Uri, headers: Headers) !void { | |
| 527 | pub const StartError = BufferedConnection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; | |
| 528 | ||
| 529 | /// Send the request to the server. | |
| 530 | pub fn start(req: *Request) StartError!void { | |
| 566 | 531 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 567 | 532 | const w = buffered.writer(); |
| 568 | 533 | |
| 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(' '); | |
| 574 | 536 | |
| 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 | } | |
| 577 | 547 | |
| 578 | try w.writeAll(@tagName(headers.method)); | |
| 579 | 548 | 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"); | |
| 584 | 556 | } |
| 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"); | |
| 588 | 562 | } |
| 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"); | |
| 592 | 566 | } |
| 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"); | |
| 603 | 570 | } |
| 604 | try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); | |
| 605 | try w.writeAll("\r\nTE: gzip, deflate"); // TODO: add trailers when someone finds a nice way to integrate them without completely invalidating all pointers to headers. | |
| 606 | 571 | |
| 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"); | |
| 611 | 574 | } |
| 612 | 575 | |
| 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 | } | |
| 618 | 600 | } |
| 619 | 601 | |
| 620 | try w.writeAll("\r\n\r\n"); | |
| 602 | try w.print("{}", .{req.headers}); | |
| 603 | ||
| 604 | try w.writeAll("\r\n"); | |
| 621 | 605 | |
| 622 | 606 | try buffered.flush(); |
| 623 | 607 | } |
| 624 | 608 | |
| 625 | pub const TransferReadError = proto.HeadersParser.ReadError || error{ReadFailed}; | |
| 609 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; | |
| 626 | 610 | |
| 627 | 611 | pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); |
| 628 | 612 | |
| ... | ... | @@ -635,10 +619,7 @@ pub const Request = struct { |
| 635 | 619 | |
| 636 | 620 | var index: usize = 0; |
| 637 | 621 | 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); | |
| 642 | 623 | if (amt == 0 and req.response.parser.done) break; |
| 643 | 624 | index += amt; |
| 644 | 625 | } |
| ... | ... | @@ -646,7 +627,7 @@ pub const Request = struct { |
| 646 | 627 | return index; |
| 647 | 628 | } |
| 648 | 629 | |
| 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 }; | |
| 650 | 631 | |
| 651 | 632 | /// Waits for a response from the server and parses any headers that are sent. |
| 652 | 633 | /// This function will block until the final response is received. |
| ... | ... | @@ -656,10 +637,7 @@ pub const Request = struct { |
| 656 | 637 | pub fn do(req: *Request) DoError!void { |
| 657 | 638 | while (true) { // handle redirects |
| 658 | 639 | 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(); | |
| 663 | 641 | |
| 664 | 642 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek()); |
| 665 | 643 | req.connection.data.buffered.clear(@intCast(u16, nchecked)); |
| ... | ... | @@ -667,27 +645,39 @@ pub const Request = struct { |
| 667 | 645 | if (req.response.parser.state.isContent()) break; |
| 668 | 646 | } |
| 669 | 647 | |
| 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 | } | |
| 671 | 655 | |
| 672 | if (req.response.headers.status == .switching_protocols) { | |
| 656 | if (req.method == .CONNECT and req.response.status == .ok) { | |
| 673 | 657 | req.connection.data.closing = false; |
| 658 | req.connection.data.proxied = true; | |
| 674 | 659 | req.response.parser.done = true; |
| 675 | 660 | } |
| 676 | 661 | |
| 677 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | |
| 662 | const req_connection = req.headers.getFirstValue("connection"); | |
| 663 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 664 | ||
| 665 | const res_connection = req.response.headers.getFirstValue("connection"); | |
| 666 | const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); | |
| 667 | if (req_keepalive and res_keepalive) { | |
| 678 | 668 | req.connection.data.closing = false; |
| 679 | 669 | } else { |
| 680 | 670 | req.connection.data.closing = true; |
| 681 | 671 | } |
| 682 | 672 | |
| 683 | if (req.response.headers.transfer_encoding) |te| { | |
| 673 | if (req.response.transfer_encoding) |te| { | |
| 684 | 674 | switch (te) { |
| 685 | 675 | .chunked => { |
| 686 | 676 | req.response.parser.next_chunk_length = 0; |
| 687 | 677 | req.response.parser.state = .chunk_head_size; |
| 688 | 678 | }, |
| 689 | 679 | } |
| 690 | } else if (req.response.headers.content_length) |cl| { | |
| 680 | } else if (req.response.content_length) |cl| { | |
| 691 | 681 | req.response.parser.next_chunk_length = cl; |
| 692 | 682 | |
| 693 | 683 | if (cl == 0) req.response.parser.done = true; |
| ... | ... | @@ -695,7 +685,7 @@ pub const Request = struct { |
| 695 | 685 | req.response.parser.done = true; |
| 696 | 686 | } |
| 697 | 687 | |
| 698 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) { | |
| 688 | if (req.response.status.class() == .redirect and req.handle_redirects) { | |
| 699 | 689 | req.response.skip = true; |
| 700 | 690 | |
| 701 | 691 | const empty = @as([*]u8, undefined)[0..0]; |
| ... | ... | @@ -703,7 +693,7 @@ pub const Request = struct { |
| 703 | 693 | |
| 704 | 694 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 705 | 695 | |
| 706 | const location = req.response.headers.location orelse | |
| 696 | const location = req.response.headers.getFirstValue("location") orelse | |
| 707 | 697 | return error.HttpRedirectMissingLocation; |
| 708 | 698 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); |
| 709 | 699 | |
| ... | ... | @@ -714,7 +704,8 @@ pub const Request = struct { |
| 714 | 704 | req.arena.deinit(); |
| 715 | 705 | req.arena = new_arena; |
| 716 | 706 | |
| 717 | const new_req = try req.client.request(resolved_url, req.headers, .{ | |
| 707 | const new_req = try req.client.request(req.method, resolved_url, req.headers, .{ | |
| 708 | .version = req.version, | |
| 718 | 709 | .max_redirects = req.redirects_left - 1, |
| 719 | 710 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ |
| 720 | 711 | .dynamic = req.response.parser.max_header_bytes, |
| ... | ... | @@ -727,19 +718,13 @@ pub const Request = struct { |
| 727 | 718 | } else { |
| 728 | 719 | req.response.skip = false; |
| 729 | 720 | if (!req.response.parser.done) { |
| 730 | if (req.response.headers.transfer_compression) |tc| switch (tc) { | |
| 721 | if (req.response.transfer_compression) |tc| switch (tc) { | |
| 731 | 722 | .compress => return error.CompressionNotSupported, |
| 732 | 723 | .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, | |
| 737 | 725 | }, |
| 738 | 726 | .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, | |
| 743 | 728 | }, |
| 744 | 729 | .zstd => req.response.compression = .{ |
| 745 | 730 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), |
| ... | ... | @@ -752,7 +737,7 @@ pub const Request = struct { |
| 752 | 737 | } |
| 753 | 738 | } |
| 754 | 739 | |
| 755 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError; | |
| 740 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers }; | |
| 756 | 741 | |
| 757 | 742 | pub const Reader = std.io.Reader(*Request, ReadError, read); |
| 758 | 743 | |
| ... | ... | @@ -762,57 +747,47 @@ pub const Request = struct { |
| 762 | 747 | |
| 763 | 748 | /// Reads data from the response body. Must be called after `do`. |
| 764 | 749 | 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 | }; | |
| 791 | 756 | |
| 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)); | |
| 795 | 765 | } |
| 796 | 766 | |
| 797 | return out_index; | |
| 767 | if (has_trail) { | |
| 768 | req.response.headers = http.Headers{ .allocator = req.client.allocator, .owned = false }; | |
| 769 | ||
| 770 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. | |
| 771 | // This will *only* fail for a malformed trailer. | |
| 772 | req.response.parse(req.response.parser.header_bytes.items) catch return error.InvalidTrailers; | |
| 773 | } | |
| 798 | 774 | } |
| 775 | ||
| 776 | return out_index; | |
| 799 | 777 | } |
| 800 | 778 | |
| 801 | 779 | /// Reads data from the response body. Must be called after `do`. |
| 802 | 780 | pub fn readAll(req: *Request, buffer: []u8) !usize { |
| 803 | 781 | var index: usize = 0; |
| 804 | 782 | 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..]); | |
| 809 | 784 | if (amt == 0) break; |
| 810 | 785 | index += amt; |
| 811 | 786 | } |
| 812 | 787 | return index; |
| 813 | 788 | } |
| 814 | 789 | |
| 815 | pub const WriteError = error{ WriteFailed, NotWriteable, MessageTooLong }; | |
| 790 | pub const WriteError = BufferedConnection.WriteError || error{ NotWriteable, MessageTooLong }; | |
| 816 | 791 | |
| 817 | 792 | pub const Writer = std.io.Writer(*Request, WriteError, write); |
| 818 | 793 | |
| ... | ... | @@ -824,28 +799,16 @@ pub const Request = struct { |
| 824 | 799 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { |
| 825 | 800 | switch (req.headers.transfer_encoding) { |
| 826 | 801 | .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"); | |
| 839 | 805 | |
| 840 | 806 | return bytes.len; |
| 841 | 807 | }, |
| 842 | 808 | .content_length => |*len| { |
| 843 | 809 | if (len.* < bytes.len) return error.MessageTooLong; |
| 844 | 810 | |
| 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); | |
| 849 | 812 | len.* -= amt; |
| 850 | 813 | return amt; |
| 851 | 814 | }, |
| ... | ... | @@ -853,19 +816,39 @@ pub const Request = struct { |
| 853 | 816 | } |
| 854 | 817 | } |
| 855 | 818 | |
| 819 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | |
| 820 | var index: usize = 0; | |
| 821 | while (index < bytes.len) { | |
| 822 | index += try write(req, bytes[index..]); | |
| 823 | } | |
| 824 | } | |
| 825 | ||
| 826 | pub const FinishError = WriteError || error{MessageNotCompleted}; | |
| 827 | ||
| 856 | 828 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 857 | pub fn finish(req: *Request) !void { | |
| 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"), | |
| 863 | 832 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 864 | 833 | .none => {}, |
| 865 | 834 | } |
| 866 | 835 | } |
| 867 | 836 | }; |
| 868 | 837 | |
| 838 | pub const HttpProxy = struct { | |
| 839 | pub const ProxyAuthentication = union(enum) { | |
| 840 | basic: []const u8, | |
| 841 | custom: []const u8, | |
| 842 | }; | |
| 843 | ||
| 844 | protocol: Connection.Protocol, | |
| 845 | host: []const u8, | |
| 846 | port: ?u16 = null, | |
| 847 | ||
| 848 | /// The value for the Proxy-Authorization header. | |
| 849 | auth: ?ProxyAuthentication = null, | |
| 850 | }; | |
| 851 | ||
| 869 | 852 | /// Release all associated resources with the client. |
| 870 | 853 | /// TODO: currently leaks all request allocated data |
| 871 | 854 | pub fn deinit(client: *Client) void { |
| ... | ... | @@ -875,11 +858,11 @@ pub fn deinit(client: *Client) void { |
| 875 | 858 | client.* = undefined; |
| 876 | 859 | } |
| 877 | 860 | |
| 878 | pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed }; | |
| 861 | pub const ConnectUnproxiedError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; | |
| 879 | 862 | |
| 880 | 863 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. |
| 881 | 864 | /// This function is threadsafe. |
| 882 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { | |
| 865 | pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectUnproxiedError!*ConnectionPool.Node { | |
| 883 | 866 | if (client.connection_pool.findConnection(.{ |
| 884 | 867 | .host = host, |
| 885 | 868 | .port = port, |
| ... | ... | @@ -891,9 +874,16 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 891 | 874 | errdefer client.allocator.destroy(conn); |
| 892 | 875 | conn.* = .{ .data = undefined }; |
| 893 | 876 | |
| 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, | |
| 897 | 887 | }; |
| 898 | 888 | errdefer stream.close(); |
| 899 | 889 | |
| ... | ... | @@ -914,10 +904,7 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 914 | 904 | conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client); |
| 915 | 905 | errdefer client.allocator.destroy(conn.data.buffered.conn.tls_client); |
| 916 | 906 | |
| 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; | |
| 921 | 908 | // This is appropriate for HTTPS because the HTTP headers contain |
| 922 | 909 | // the content length which is used to detect truncation attacks. |
| 923 | 910 | conn.data.buffered.conn.tls_client.allow_truncation_attacks = true; |
| ... | ... | @@ -929,19 +916,51 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 929 | 916 | return conn; |
| 930 | 917 | } |
| 931 | 918 | |
| 932 | pub const RequestError = ConnectError || error{ | |
| 919 | // Prevents a dependency loop in request() | |
| 920 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; | |
| 921 | pub const ConnectError = ConnectErrorPartial || RequestError; | |
| 922 | ||
| 923 | pub 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 | ||
| 946 | pub const RequestError = ConnectUnproxiedError || ConnectErrorPartial || Request.StartError || std.fmt.ParseIntError || BufferedConnection.WriteError || error{ | |
| 933 | 947 | UnsupportedUrlScheme, |
| 934 | 948 | UriMissingHost, |
| 935 | 949 | |
| 936 | CertificateAuthorityBundleFailed, | |
| 937 | WriteFailed, | |
| 950 | CertificateBundleLoadFailure, | |
| 951 | UnsupportedTransferEncoding, | |
| 938 | 952 | }; |
| 939 | 953 | |
| 940 | 954 | pub const Options = struct { |
| 955 | version: http.Version = .@"HTTP/1.1", | |
| 956 | ||
| 941 | 957 | handle_redirects: bool = true, |
| 942 | 958 | max_redirects: u32 = 3, |
| 943 | 959 | header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, |
| 944 | 960 | |
| 961 | /// Must be an already acquired connection. | |
| 962 | connection: ?*ConnectionPool.Node = null, | |
| 963 | ||
| 945 | 964 | pub const HeaderStrategy = union(enum) { |
| 946 | 965 | /// In this case, the client's Allocator will be used to store the |
| 947 | 966 | /// entire HTTP header. This value is the maximum total size of |
| ... | ... | @@ -965,7 +984,7 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| 965 | 984 | |
| 966 | 985 | /// Form and send a http request to a server. |
| 967 | 986 | /// This function is threadsafe. |
| 968 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { | |
| 987 | pub fn request(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: Options) RequestError!Request { | |
| 969 | 988 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 970 | 989 | |
| 971 | 990 | const port: u16 = uri.port orelse switch (protocol) { |
| ... | ... | @@ -980,22 +999,27 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 980 | 999 | defer client.ca_bundle_mutex.unlock(); |
| 981 | 1000 | |
| 982 | 1001 | 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; | |
| 987 | 1003 | @atomicStore(bool, &client.next_https_rescan_certs, false, .Release); |
| 988 | 1004 | } |
| 989 | 1005 | } |
| 990 | 1006 | |
| 1007 | const conn = options.connection orelse try client.connect(host, port, protocol); | |
| 1008 | ||
| 991 | 1009 | var req: Request = .{ |
| 992 | 1010 | .uri = uri, |
| 993 | 1011 | .client = client, |
| 994 | .connection = try client.connect(host, port, protocol), | |
| 1012 | .connection = conn, | |
| 995 | 1013 | .headers = headers, |
| 1014 | .method = method, | |
| 1015 | .version = options.version, | |
| 996 | 1016 | .redirects_left = options.max_redirects, |
| 997 | 1017 | .handle_redirects = options.handle_redirects, |
| 998 | 1018 | .response = .{ |
| 1019 | .status = undefined, | |
| 1020 | .reason = undefined, | |
| 1021 | .version = undefined, | |
| 1022 | .headers = undefined, | |
| 999 | 1023 | .parser = switch (options.header_strategy) { |
| 1000 | 1024 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 1001 | 1025 | .static => |buf| proto.HeadersParser.initStatic(buf), |
| ... | ... | @@ -1007,14 +1031,6 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 1007 | 1031 | |
| 1008 | 1032 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 1009 | 1033 | |
| 1010 | req.start(uri, headers) catch |err| { | |
| 1011 | if (err == error.OutOfMemory) return error.OutOfMemory; | |
| 1012 | const err_casted = @errSetCast(BufferedConnection.WriteError, err); | |
| 1013 | ||
| 1014 | client.last_error = .{ .write = err_casted }; | |
| 1015 | return error.WriteFailed; | |
| 1016 | }; | |
| 1017 | ||
| 1018 | 1034 | return req; |
| 1019 | 1035 | } |
| 1020 | 1036 |
lib/std/http/Headers.zig created+386| ... | ... | @@ -0,0 +1,386 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | ||
| 3 | const Allocator = std.mem.Allocator; | |
| 4 | ||
| 5 | const testing = std.testing; | |
| 6 | const ascii = std.ascii; | |
| 7 | const assert = std.debug.assert; | |
| 8 | ||
| 9 | pub const HeaderList = std.ArrayListUnmanaged(Field); | |
| 10 | pub const HeaderIndexList = std.ArrayListUnmanaged(usize); | |
| 11 | pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage); | |
| 12 | ||
| 13 | pub 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 | ||
| 35 | pub 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 | ||
| 57 | pub 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 | ||
| 260 | test "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 | ||
| 271 | test "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 | ||
| 285 | test "Headers consistency" { | |
| 286 | var h = Headers{ .allocator = std.testing.allocator }; | |
| 287 | defer h.deinit(); | |
| 288 | ||
| 289 | try h.append("foo", "bar"); | |
| 290 | try h.append("hello", "world"); | |
| 291 | _ = h.delete("Foo"); | |
| 292 | ||
| 293 | try h.append("foo", "bar"); | |
| 294 | try h.append("bar", "world"); | |
| 295 | try h.append("foo", "baz"); | |
| 296 | try h.append("baz", "hello"); | |
| 297 | ||
| 298 | try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello")); | |
| 299 | try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo")); | |
| 300 | try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar")); | |
| 301 | try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz")); | |
| 302 | try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog")); | |
| 303 | ||
| 304 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?); | |
| 305 | try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?); | |
| 306 | try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?); | |
| 307 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?); | |
| 308 | try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog")); | |
| 309 | ||
| 310 | try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value); | |
| 311 | try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value); | |
| 312 | try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value); | |
| 313 | try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value); | |
| 314 | ||
| 315 | const hello_entries = (try h.getEntries(testing.allocator, "hello")).?; | |
| 316 | defer testing.allocator.free(hello_entries); | |
| 317 | try testing.expectEqualDeep(@as([]const Field, &[_]Field{ | |
| 318 | .{ .name = "hello", .value = "world" }, | |
| 319 | }), hello_entries); | |
| 320 | ||
| 321 | const foo_entries = (try h.getEntries(testing.allocator, "foo")).?; | |
| 322 | defer testing.allocator.free(foo_entries); | |
| 323 | try testing.expectEqualDeep(@as([]const Field, &[_]Field{ | |
| 324 | .{ .name = "foo", .value = "bar" }, | |
| 325 | .{ .name = "foo", .value = "baz" }, | |
| 326 | }), foo_entries); | |
| 327 | ||
| 328 | const bar_entries = (try h.getEntries(testing.allocator, "bar")).?; | |
| 329 | defer testing.allocator.free(bar_entries); | |
| 330 | try testing.expectEqualDeep(@as([]const Field, &[_]Field{ | |
| 331 | .{ .name = "bar", .value = "world" }, | |
| 332 | }), bar_entries); | |
| 333 | ||
| 334 | const baz_entries = (try h.getEntries(testing.allocator, "baz")).?; | |
| 335 | defer testing.allocator.free(baz_entries); | |
| 336 | try testing.expectEqualDeep(@as([]const Field, &[_]Field{ | |
| 337 | .{ .name = "baz", .value = "hello" }, | |
| 338 | }), baz_entries); | |
| 339 | ||
| 340 | const pog_entries = (try h.getEntries(testing.allocator, "pog")); | |
| 341 | try testing.expectEqual(@as(?[]const Field, null), pog_entries); | |
| 342 | ||
| 343 | try testing.expectEqualStrings("world", h.getFirstValue("hello").?); | |
| 344 | try testing.expectEqualStrings("bar", h.getFirstValue("foo").?); | |
| 345 | try testing.expectEqualStrings("world", h.getFirstValue("bar").?); | |
| 346 | try testing.expectEqualStrings("hello", h.getFirstValue("baz").?); | |
| 347 | try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog")); | |
| 348 | ||
| 349 | const hello_values = (try h.getValues(testing.allocator, "hello")).?; | |
| 350 | defer testing.allocator.free(hello_values); | |
| 351 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values); | |
| 352 | ||
| 353 | const foo_values = (try h.getValues(testing.allocator, "foo")).?; | |
| 354 | defer testing.allocator.free(foo_values); | |
| 355 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values); | |
| 356 | ||
| 357 | const bar_values = (try h.getValues(testing.allocator, "bar")).?; | |
| 358 | defer testing.allocator.free(bar_values); | |
| 359 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values); | |
| 360 | ||
| 361 | const baz_values = (try h.getValues(testing.allocator, "baz")).?; | |
| 362 | defer testing.allocator.free(baz_values); | |
| 363 | try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values); | |
| 364 | ||
| 365 | const pog_values = (try h.getValues(testing.allocator, "pog")); | |
| 366 | try testing.expectEqual(@as(?[]const []const u8, null), pog_values); | |
| 367 | ||
| 368 | h.sort(); | |
| 369 | ||
| 370 | try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?); | |
| 371 | try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?); | |
| 372 | try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?); | |
| 373 | try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?); | |
| 374 | ||
| 375 | const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h}); | |
| 376 | defer testing.allocator.free(formatted_values); | |
| 377 | ||
| 378 | try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values); | |
| 379 | ||
| 380 | var buf: [128]u8 = undefined; | |
| 381 | var fbs = std.io.fixedBufferStream(&buf); | |
| 382 | const writer = fbs.writer(); | |
| 383 | ||
| 384 | try h.formatCommaSeparated("foo", writer); | |
| 385 | try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten()); | |
| 386 | } |
lib/std/http/Server.zig+253-195| ... | ... | @@ -23,21 +23,33 @@ pub const Connection = struct { |
| 23 | 23 | |
| 24 | 24 | pub const Protocol = enum { plain }; |
| 25 | 25 | |
| 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), | |
| 29 | 29 | // .tls => return conn.tls_client.read(conn.stream, buffer), |
| 30 | } | |
| 30 | } catch |err| switch (err) { | |
| 31 | error.ConnectionTimedOut => return error.ConnectionTimedOut, | |
| 32 | error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, | |
| 33 | else => return error.UnexpectedReadFailure, | |
| 34 | }; | |
| 31 | 35 | } |
| 32 | 36 | |
| 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), | |
| 36 | 40 | // .tls => return conn.tls_client.readAtLeast(conn.stream, buffer, len), |
| 37 | } | |
| 41 | } catch |err| switch (err) { | |
| 42 | error.ConnectionTimedOut => return error.ConnectionTimedOut, | |
| 43 | error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, | |
| 44 | else => return error.UnexpectedReadFailure, | |
| 45 | }; | |
| 38 | 46 | } |
| 39 | 47 | |
| 40 | pub const ReadError = net.Stream.ReadError; | |
| 48 | pub const ReadError = error{ | |
| 49 | ConnectionTimedOut, | |
| 50 | ConnectionResetByPeer, | |
| 51 | UnexpectedReadFailure, | |
| 52 | }; | |
| 41 | 53 | |
| 42 | 54 | pub const Reader = std.io.Reader(*Connection, ReadError, read); |
| 43 | 55 | |
| ... | ... | @@ -45,21 +57,31 @@ pub const Connection = struct { |
| 45 | 57 | return Reader{ .context = conn }; |
| 46 | 58 | } |
| 47 | 59 | |
| 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), | |
| 51 | 63 | // .tls => return conn.tls_client.writeAll(conn.stream, buffer), |
| 52 | } | |
| 64 | } catch |err| switch (err) { | |
| 65 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | |
| 66 | else => return error.UnexpectedWriteFailure, | |
| 67 | }; | |
| 53 | 68 | } |
| 54 | 69 | |
| 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), | |
| 58 | 73 | // .tls => return conn.tls_client.write(conn.stream, buffer), |
| 59 | } | |
| 74 | } catch |err| switch (err) { | |
| 75 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | |
| 76 | else => return error.UnexpectedWriteFailure, | |
| 77 | }; | |
| 60 | 78 | } |
| 61 | 79 | |
| 62 | pub const WriteError = net.Stream.WriteError || error{}; | |
| 80 | pub const WriteError = error{ | |
| 81 | ConnectionResetByPeer, | |
| 82 | UnexpectedWriteFailure, | |
| 83 | }; | |
| 84 | ||
| 63 | 85 | pub const Writer = std.io.Writer(*Connection, WriteError, write); |
| 64 | 86 | |
| 65 | 87 | pub fn writer(conn: *Connection) Writer { |
| ... | ... | @@ -155,136 +177,142 @@ pub const BufferedConnection = struct { |
| 155 | 177 | } |
| 156 | 178 | }; |
| 157 | 179 | |
| 158 | /// A HTTP request originating from a client. | |
| 159 | pub 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. | |
| 181 | pub const ResponseTransfer = union(enum) { | |
| 182 | content_length: u64, | |
| 183 | chunked: void, | |
| 184 | none: void, | |
| 185 | }; | |
| 180 | 186 | |
| 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. | |
| 188 | pub 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, .{}); | |
| 183 | 192 | |
| 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 | }; | |
| 187 | 198 | |
| 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. | |
| 200 | pub 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 | }; | |
| 191 | 212 | |
| 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"); | |
| 194 | 215 | |
| 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; | |
| 202 | 219 | |
| 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; | |
| 204 | 223 | |
| 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; | |
| 210 | 226 | |
| 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 | }; | |
| 217 | 234 | |
| 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]; | |
| 242 | 236 | |
| 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; | |
| 245 | 240 | |
| 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 | } | |
| 247 | 247 | |
| 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; | |
| 253 | 273 | } |
| 274 | } | |
| 254 | 275 | |
| 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; | |
| 258 | 278 | |
| 259 | const trimmed = mem.trim(u8, header_value, " "); | |
| 279 | const trimmed = mem.trim(u8, second, " "); | |
| 260 | 280 | |
| 261 | 281 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { |
| 262 | headers.transfer_compression = ce; | |
| 282 | req.transfer_compression = ce; | |
| 263 | 283 | } else { |
| 264 | 284 | return error.HttpTransferEncodingUnsupported; |
| 265 | 285 | } |
| 266 | } else if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 267 | if (std.ascii.eqlIgnoreCase(header_value, "keep-alive")) { | |
| 268 | headers.connection = .keep_alive; | |
| 269 | } else if (std.ascii.eqlIgnoreCase(header_value, "close")) { | |
| 270 | headers.connection = .close; | |
| 271 | } else { | |
| 272 | return error.HttpConnectionHeaderUnsupported; | |
| 273 | } | |
| 274 | } else if (std.ascii.eqlIgnoreCase(header_name, "host")) { | |
| 275 | headers.host = header_value; | |
| 276 | 286 | } |
| 277 | } | |
| 278 | 287 | |
| 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, " "); | |
| 281 | 293 | |
| 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 | } | |
| 284 | 300 | } |
| 285 | }; | |
| 301 | } | |
| 286 | 302 | |
| 287 | headers: Headers = undefined, | |
| 303 | inline fn int64(array: *const [8]u8) u64 { | |
| 304 | return @bitCast(u64, array.*); | |
| 305 | } | |
| 306 | ||
| 307 | method: http.Method, | |
| 308 | target: []const u8, | |
| 309 | version: http.Version, | |
| 310 | ||
| 311 | content_length: ?u64 = null, | |
| 312 | transfer_encoding: ?http.TransferEncoding = null, | |
| 313 | transfer_compression: ?http.ContentEncoding = null, | |
| 314 | ||
| 315 | headers: http.Headers = undefined, | |
| 288 | 316 | parser: proto.HeadersParser, |
| 289 | 317 | compression: Compression = .none, |
| 290 | 318 | }; |
| ... | ... | @@ -295,23 +323,17 @@ pub const Request = struct { |
| 295 | 323 | /// Order of operations: accept -> wait -> do [ -> write -> finish][ -> reset /] |
| 296 | 324 | /// \ -> read / |
| 297 | 325 | pub 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, | |
| 302 | 329 | |
| 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, | |
| 309 | 331 | |
| 310 | 332 | server: *Server, |
| 311 | 333 | address: net.Address, |
| 312 | 334 | connection: BufferedConnection, |
| 313 | 335 | |
| 314 | headers: Headers = .{}, | |
| 336 | headers: http.Headers, | |
| 315 | 337 | request: Request, |
| 316 | 338 | |
| 317 | 339 | /// Reset this response to its initial state. This must be called before handling a second request on the same connection. |
| ... | ... | @@ -341,46 +363,61 @@ pub const Response = struct { |
| 341 | 363 | } |
| 342 | 364 | } |
| 343 | 365 | |
| 366 | pub const DoError = BufferedConnection.WriteError || error{ UnsupportedTransferEncoding, InvalidContentLength }; | |
| 367 | ||
| 344 | 368 | /// Send the response headers. |
| 345 | 369 | pub fn do(res: *Response) !void { |
| 346 | 370 | var buffered = std.io.bufferedWriter(res.connection.writer()); |
| 347 | 371 | const w = buffered.writer(); |
| 348 | 372 | |
| 349 | try w.writeAll(@tagName(res.headers.version)); | |
| 373 | try w.writeAll(@tagName(res.version)); | |
| 350 | 374 | try w.writeByte(' '); |
| 351 | try w.print("{d}", .{@enumToInt(res.headers.status)}); | |
| 375 | try w.print("{d}", .{@enumToInt(res.status)}); | |
| 352 | 376 | try w.writeByte(' '); |
| 353 | if (res.headers.reason) |reason| { | |
| 377 | if (res.reason) |reason| { | |
| 354 | 378 | try w.writeAll(reason); |
| 355 | } else if (res.headers.status.phrase()) |phrase| { | |
| 379 | } else if (res.status.phrase()) |phrase| { | |
| 356 | 380 | try w.writeAll(phrase); |
| 357 | 381 | } |
| 382 | try w.writeAll("\r\n"); | |
| 358 | 383 | |
| 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"); | |
| 362 | 386 | } |
| 363 | 387 | |
| 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"); | |
| 368 | 390 | } |
| 369 | 391 | |
| 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"); | |
| 375 | 394 | |
| 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 | } | |
| 381 | 416 | } |
| 382 | 417 | |
| 383 | try w.writeAll("\r\n\r\n"); | |
| 418 | try w.print("{}", .{res.headers}); | |
| 419 | ||
| 420 | try w.writeAll("\r\n"); | |
| 384 | 421 | |
| 385 | 422 | try buffered.flush(); |
| 386 | 423 | } |
| ... | ... | @@ -393,23 +430,23 @@ pub const Response = struct { |
| 393 | 430 | return .{ .context = res }; |
| 394 | 431 | } |
| 395 | 432 | |
| 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; | |
| 398 | 435 | |
| 399 | 436 | var index: usize = 0; |
| 400 | 437 | while (index == 0) { |
| 401 | 438 | const amt = try res.request.parser.read(&res.connection, buf[index..], false); |
| 402 | if (amt == 0 and res.request.parser.isComplete()) break; | |
| 439 | if (amt == 0 and res.request.parser.done) break; | |
| 403 | 440 | index += amt; |
| 404 | 441 | } |
| 405 | 442 | |
| 406 | 443 | return index; |
| 407 | 444 | } |
| 408 | 445 | |
| 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 }; | |
| 410 | 447 | |
| 411 | 448 | /// Wait for the client to send a complete request head. |
| 412 | pub fn wait(res: *Response) !void { | |
| 449 | pub fn wait(res: *Response) WaitError!void { | |
| 413 | 450 | while (true) { |
| 414 | 451 | try res.connection.fill(); |
| 415 | 452 | |
| ... | ... | @@ -419,22 +456,28 @@ pub const Response = struct { |
| 419 | 456 | if (res.request.parser.state.isContent()) break; |
| 420 | 457 | } |
| 421 | 458 | |
| 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.?); | |
| 423 | 464 | |
| 424 | if (res.headers.connection == .keep_alive and res.request.headers.connection == .keep_alive) { | |
| 465 | const req_connection = res.request.headers.getFirstValue("connection"); | |
| 466 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | |
| 467 | if (res_keepalive and req_keepalive) { | |
| 425 | 468 | res.connection.conn.closing = false; |
| 426 | 469 | } else { |
| 427 | 470 | res.connection.conn.closing = true; |
| 428 | 471 | } |
| 429 | 472 | |
| 430 | if (res.request.headers.transfer_encoding) |te| { | |
| 473 | if (res.request.transfer_encoding) |te| { | |
| 431 | 474 | switch (te) { |
| 432 | 475 | .chunked => { |
| 433 | 476 | res.request.parser.next_chunk_length = 0; |
| 434 | 477 | res.request.parser.state = .chunk_head_size; |
| 435 | 478 | }, |
| 436 | 479 | } |
| 437 | } else if (res.request.headers.content_length) |cl| { | |
| 480 | } else if (res.request.content_length) |cl| { | |
| 438 | 481 | res.request.parser.next_chunk_length = cl; |
| 439 | 482 | |
| 440 | 483 | if (cl == 0) res.request.parser.done = true; |
| ... | ... | @@ -443,13 +486,13 @@ pub const Response = struct { |
| 443 | 486 | } |
| 444 | 487 | |
| 445 | 488 | if (!res.request.parser.done) { |
| 446 | if (res.request.headers.transfer_compression) |tc| switch (tc) { | |
| 489 | if (res.request.transfer_compression) |tc| switch (tc) { | |
| 447 | 490 | .compress => return error.CompressionNotSupported, |
| 448 | 491 | .deflate => res.request.compression = .{ |
| 449 | .deflate = try std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()), | |
| 492 | .deflate = std.compress.zlib.zlibStream(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 450 | 493 | }, |
| 451 | 494 | .gzip => res.request.compression = .{ |
| 452 | .gzip = try std.compress.gzip.decompress(res.server.allocator, res.transferReader()), | |
| 495 | .gzip = std.compress.gzip.decompress(res.server.allocator, res.transferReader()) catch return error.CompressionInitializationFailed, | |
| 453 | 496 | }, |
| 454 | 497 | .zstd => res.request.compression = .{ |
| 455 | 498 | .zstd = std.compress.zstd.decompressStream(res.server.allocator, res.transferReader()), |
| ... | ... | @@ -458,7 +501,7 @@ pub const Response = struct { |
| 458 | 501 | } |
| 459 | 502 | } |
| 460 | 503 | |
| 461 | pub const ReadError = Compression.DeflateDecompressor.Error || Compression.GzipDecompressor.Error || Compression.ZstdDecompressor.Error || WaitForCompleteHeadError; | |
| 504 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{DecompressionFailure}; | |
| 462 | 505 | |
| 463 | 506 | pub const Reader = std.io.Reader(*Response, ReadError, read); |
| 464 | 507 | |
| ... | ... | @@ -467,12 +510,33 @@ pub const Response = struct { |
| 467 | 510 | } |
| 468 | 511 | |
| 469 | 512 | 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, | |
| 474 | 517 | else => try res.transferRead(buffer), |
| 475 | 518 | }; |
| 519 | ||
| 520 | if (out_index == 0) { | |
| 521 | const has_trail = !res.request.parser.state.isContent(); | |
| 522 | ||
| 523 | while (!res.request.parser.state.isContent()) { // read trailing headers | |
| 524 | try res.connection.fill(); | |
| 525 | ||
| 526 | const nchecked = try res.request.parser.checkCompleteHead(res.server.allocator, res.connection.peek()); | |
| 527 | res.connection.clear(@intCast(u16, nchecked)); | |
| 528 | } | |
| 529 | ||
| 530 | if (has_trail) { | |
| 531 | res.request.headers = http.Headers{ .allocator = res.server.allocator, .owned = false }; | |
| 532 | ||
| 533 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. | |
| 534 | // This will *only* fail for a malformed trailer. | |
| 535 | res.request.parse(res.request.parser.header_bytes.items) catch return error.InvalidTrailers; | |
| 536 | } | |
| 537 | } | |
| 538 | ||
| 539 | return out_index; | |
| 476 | 540 | } |
| 477 | 541 | |
| 478 | 542 | pub fn readAll(res: *Response, buffer: []u8) !usize { |
| ... | ... | @@ -495,7 +559,7 @@ pub const Response = struct { |
| 495 | 559 | |
| 496 | 560 | /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. |
| 497 | 561 | pub fn write(res: *Response, bytes: []const u8) WriteError!usize { |
| 498 | switch (res.headers.transfer_encoding) { | |
| 562 | switch (res.transfer_encoding) { | |
| 499 | 563 | .chunked => { |
| 500 | 564 | try res.connection.writer().print("{x}\r\n", .{bytes.len}); |
| 501 | 565 | try res.connection.writeAll(bytes); |
| ... | ... | @@ -514,9 +578,18 @@ pub const Response = struct { |
| 514 | 578 | } |
| 515 | 579 | } |
| 516 | 580 | |
| 581 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | |
| 582 | var index: usize = 0; | |
| 583 | while (index < bytes.len) { | |
| 584 | index += try write(req, bytes[index..]); | |
| 585 | } | |
| 586 | } | |
| 587 | ||
| 588 | pub const FinishError = WriteError || error{MessageNotCompleted}; | |
| 589 | ||
| 517 | 590 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 518 | pub fn finish(res: *Response) !void { | |
| 519 | switch (res.headers.transfer_encoding) { | |
| 591 | pub fn finish(res: *Response) FinishError!void { | |
| 592 | switch (res.transfer_encoding) { | |
| 520 | 593 | .chunked => try res.connection.writeAll("0\r\n\r\n"), |
| 521 | 594 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 522 | 595 | .none => {}, |
| ... | ... | @@ -524,25 +597,6 @@ pub const Response = struct { |
| 524 | 597 | } |
| 525 | 598 | }; |
| 526 | 599 | |
| 527 | /// The mode of transport for responses. | |
| 528 | pub const RequestTransfer = union(enum) { | |
| 529 | content_length: u64, | |
| 530 | chunked: void, | |
| 531 | none: void, | |
| 532 | }; | |
| 533 | ||
| 534 | /// The decompressor for request messages. | |
| 535 | pub 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 | ||
| 546 | 600 | pub fn init(allocator: Allocator, options: net.StreamServer.Options) Server { |
| 547 | 601 | return .{ |
| 548 | 602 | .allocator = allocator, |
| ... | ... | @@ -588,7 +642,11 @@ pub fn accept(server: *Server, options: HeaderStrategy) AcceptError!*Response { |
| 588 | 642 | .stream = in.stream, |
| 589 | 643 | .protocol = .plain, |
| 590 | 644 | } }, |
| 645 | .headers = .{ .allocator = server.allocator }, | |
| 591 | 646 | .request = .{ |
| 647 | .version = undefined, | |
| 648 | .method = undefined, | |
| 649 | .target = undefined, | |
| 592 | 650 | .parser = switch (options) { |
| 593 | 651 | .dynamic => |max| proto.HeadersParser.initDynamic(max), |
| 594 | 652 | .static => |buf| proto.HeadersParser.initStatic(buf), |
lib/std/http/protocol.zig+1-1| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | const std = @import("std"); | |
| 1 | const std = @import("../std.zig"); | |
| 2 | 2 | const testing = std.testing; |
| 3 | 3 | const mem = std.mem; |
| 4 | 4 |
src/Package.zig+6-1| ... | ... | @@ -479,9 +479,14 @@ fn fetchAndUnpack( |
| 479 | 479 | }; |
| 480 | 480 | defer tmp_directory.closeAndFree(gpa); |
| 481 | 481 | |
| 482 | var req = try http_client.request(uri, .{}, .{}); | |
| 482 | var h = std.http.Headers{ .allocator = gpa }; | |
| 483 | defer h.deinit(); | |
| 484 | ||
| 485 | var req = try http_client.request(.GET, uri, h, .{}); | |
| 483 | 486 | defer req.deinit(); |
| 484 | 487 | |
| 488 | try req.start(); | |
| 489 | ||
| 485 | 490 | try req.do(); |
| 486 | 491 | |
| 487 | 492 | if (mem.endsWith(u8, uri.path, ".tar.gz")) { |