| author | |
| committer | |
| log | 5998a8cebe3973d70c258b2a1440c5c3252d3539 |
| tree | a889e12970e72420c07c9e988536997479d16a7c |
| parent | 2cf15bee0325321e9da496580b55310d4ba1053f |
| parent | 46b34949c3b40d286233c98c97bd2e0c221c1518 |
| signature |
std: rework HTTP and TLS for new I/O API31 files changed, 3743 insertions(+), 5295 deletions(-)
lib/compiler/resinator/cli.zig+7-5| ... | ... | @@ -1141,6 +1141,8 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn |
| 1141 | 1141 | } |
| 1142 | 1142 | output_format = .res; |
| 1143 | 1143 | } |
| 1144 | } else { | |
| 1145 | output_format_source = .output_format_arg; | |
| 1144 | 1146 | } |
| 1145 | 1147 | options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) }; |
| 1146 | 1148 | } else { |
| ... | ... | @@ -1529,21 +1531,21 @@ fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Opti |
| 1529 | 1531 | var diagnostics = Diagnostics.init(std.testing.allocator); |
| 1530 | 1532 | defer diagnostics.deinit(); |
| 1531 | 1533 | |
| 1532 | var output = std.ArrayList(u8).init(std.testing.allocator); | |
| 1534 | var output: std.io.Writer.Allocating = .init(std.testing.allocator); | |
| 1533 | 1535 | defer output.deinit(); |
| 1534 | 1536 | |
| 1535 | 1537 | var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) { |
| 1536 | 1538 | error.ParseError => { |
| 1537 | try diagnostics.renderToWriter(args, output.writer(), .no_color); | |
| 1538 | try std.testing.expectEqualStrings(expected_output, output.items); | |
| 1539 | try diagnostics.renderToWriter(args, &output.writer, .no_color); | |
| 1540 | try std.testing.expectEqualStrings(expected_output, output.getWritten()); | |
| 1539 | 1541 | return null; |
| 1540 | 1542 | }, |
| 1541 | 1543 | else => |e| return e, |
| 1542 | 1544 | }; |
| 1543 | 1545 | errdefer options.deinit(); |
| 1544 | 1546 | |
| 1545 | try diagnostics.renderToWriter(args, output.writer(), .no_color); | |
| 1546 | try std.testing.expectEqualStrings(expected_output, output.items); | |
| 1547 | try diagnostics.renderToWriter(args, &output.writer, .no_color); | |
| 1548 | try std.testing.expectEqualStrings(expected_output, output.getWritten()); | |
| 1547 | 1549 | return options; |
| 1548 | 1550 | } |
| 1549 | 1551 |
lib/compiler/resinator/compile.zig+50-48| ... | ... | @@ -550,7 +550,7 @@ pub const Compiler = struct { |
| 550 | 550 | // so get it here to simplify future usage. |
| 551 | 551 | const filename_token = node.filename.getFirstToken(); |
| 552 | 552 | |
| 553 | const file = self.searchForFile(filename_utf8) catch |err| switch (err) { | |
| 553 | const file_handle = self.searchForFile(filename_utf8) catch |err| switch (err) { | |
| 554 | 554 | error.OutOfMemory => |e| return e, |
| 555 | 555 | else => |e| { |
| 556 | 556 | const filename_string_index = try self.diagnostics.putString(filename_utf8); |
| ... | ... | @@ -564,13 +564,15 @@ pub const Compiler = struct { |
| 564 | 564 | }); |
| 565 | 565 | }, |
| 566 | 566 | }; |
| 567 | defer file.close(); | |
| 567 | defer file_handle.close(); | |
| 568 | var file_buffer: [2048]u8 = undefined; | |
| 569 | var file_reader = file_handle.reader(&file_buffer); | |
| 568 | 570 | |
| 569 | 571 | if (maybe_predefined_type) |predefined_type| { |
| 570 | 572 | switch (predefined_type) { |
| 571 | 573 | .GROUP_ICON, .GROUP_CURSOR => { |
| 572 | 574 | // Check for animated icon first |
| 573 | if (ani.isAnimatedIcon(file.deprecatedReader())) { | |
| 575 | if (ani.isAnimatedIcon(file_reader.interface.adaptToOldInterface())) { | |
| 574 | 576 | // Animated icons are just put into the resource unmodified, |
| 575 | 577 | // and the resource type changes to ANIICON/ANICURSOR |
| 576 | 578 | |
| ... | ... | @@ -582,18 +584,18 @@ pub const Compiler = struct { |
| 582 | 584 | header.type_value.ordinal = @intFromEnum(new_predefined_type); |
| 583 | 585 | header.memory_flags = MemoryFlags.defaults(new_predefined_type); |
| 584 | 586 | header.applyMemoryFlags(node.common_resource_attributes, self.source); |
| 585 | header.data_size = @intCast(try file.getEndPos()); | |
| 587 | header.data_size = @intCast(try file_reader.getSize()); | |
| 586 | 588 | |
| 587 | 589 | try header.write(writer, self.errContext(node.id)); |
| 588 | try file.seekTo(0); | |
| 589 | try writeResourceData(writer, file.deprecatedReader(), header.data_size); | |
| 590 | try file_reader.seekTo(0); | |
| 591 | try writeResourceData(writer, &file_reader.interface, header.data_size); | |
| 590 | 592 | return; |
| 591 | 593 | } |
| 592 | 594 | |
| 593 | 595 | // isAnimatedIcon moved the file cursor so reset to the start |
| 594 | try file.seekTo(0); | |
| 596 | try file_reader.seekTo(0); | |
| 595 | 597 | |
| 596 | const icon_dir = ico.read(self.allocator, file.deprecatedReader(), try file.getEndPos()) catch |err| switch (err) { | |
| 598 | const icon_dir = ico.read(self.allocator, file_reader.interface.adaptToOldInterface(), try file_reader.getSize()) catch |err| switch (err) { | |
| 597 | 599 | error.OutOfMemory => |e| return e, |
| 598 | 600 | else => |e| { |
| 599 | 601 | return self.iconReadError( |
| ... | ... | @@ -671,15 +673,15 @@ pub const Compiler = struct { |
| 671 | 673 | try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little); |
| 672 | 674 | } |
| 673 | 675 | |
| 674 | try file.seekTo(entry.data_offset_from_start_of_file); | |
| 675 | var header_bytes = file.deprecatedReader().readBytesNoEof(16) catch { | |
| 676 | try file_reader.seekTo(entry.data_offset_from_start_of_file); | |
| 677 | var header_bytes = (file_reader.interface.takeArray(16) catch { | |
| 676 | 678 | return self.iconReadError( |
| 677 | 679 | error.UnexpectedEOF, |
| 678 | 680 | filename_utf8, |
| 679 | 681 | filename_token, |
| 680 | 682 | predefined_type, |
| 681 | 683 | ); |
| 682 | }; | |
| 684 | }).*; | |
| 683 | 685 | |
| 684 | 686 | const image_format = ico.ImageFormat.detect(&header_bytes); |
| 685 | 687 | if (!image_format.validate(&header_bytes)) { |
| ... | ... | @@ -802,8 +804,8 @@ pub const Compiler = struct { |
| 802 | 804 | }, |
| 803 | 805 | } |
| 804 | 806 | |
| 805 | try file.seekTo(entry.data_offset_from_start_of_file); | |
| 806 | try writeResourceDataNoPadding(writer, file.deprecatedReader(), entry.data_size_in_bytes); | |
| 807 | try file_reader.seekTo(entry.data_offset_from_start_of_file); | |
| 808 | try writeResourceDataNoPadding(writer, &file_reader.interface, entry.data_size_in_bytes); | |
| 807 | 809 | try writeDataPadding(writer, full_data_size); |
| 808 | 810 | |
| 809 | 811 | if (self.state.icon_id == std.math.maxInt(u16)) { |
| ... | ... | @@ -857,9 +859,9 @@ pub const Compiler = struct { |
| 857 | 859 | }, |
| 858 | 860 | .BITMAP => { |
| 859 | 861 | header.applyMemoryFlags(node.common_resource_attributes, self.source); |
| 860 | const file_size = try file.getEndPos(); | |
| 862 | const file_size = try file_reader.getSize(); | |
| 861 | 863 | |
| 862 | const bitmap_info = bmp.read(file.deprecatedReader(), file_size) catch |err| { | |
| 864 | const bitmap_info = bmp.read(file_reader.interface.adaptToOldInterface(), file_size) catch |err| { | |
| 863 | 865 | const filename_string_index = try self.diagnostics.putString(filename_utf8); |
| 864 | 866 | return self.addErrorDetailsAndFail(.{ |
| 865 | 867 | .err = .bmp_read_error, |
| ... | ... | @@ -921,18 +923,17 @@ pub const Compiler = struct { |
| 921 | 923 | |
| 922 | 924 | header.data_size = bmp_bytes_to_write; |
| 923 | 925 | try header.write(writer, self.errContext(node.id)); |
| 924 | try file.seekTo(bmp.file_header_len); | |
| 925 | const file_reader = file.deprecatedReader(); | |
| 926 | try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size); | |
| 926 | try file_reader.seekTo(bmp.file_header_len); | |
| 927 | try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.dib_header_size); | |
| 927 | 928 | if (bitmap_info.getBitmasksByteLen() > 0) { |
| 928 | try writeResourceDataNoPadding(writer, file_reader, bitmap_info.getBitmasksByteLen()); | |
| 929 | try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.getBitmasksByteLen()); | |
| 929 | 930 | } |
| 930 | 931 | if (bitmap_info.getExpectedPaletteByteLen() > 0) { |
| 931 | try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen())); | |
| 932 | try writeResourceDataNoPadding(writer, &file_reader.interface, @intCast(bitmap_info.getActualPaletteByteLen())); | |
| 932 | 933 | } |
| 933 | try file.seekTo(bitmap_info.pixel_data_offset); | |
| 934 | try file_reader.seekTo(bitmap_info.pixel_data_offset); | |
| 934 | 935 | const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset); |
| 935 | try writeResourceDataNoPadding(writer, file_reader, pixel_bytes); | |
| 936 | try writeResourceDataNoPadding(writer, &file_reader.interface, pixel_bytes); | |
| 936 | 937 | try writeDataPadding(writer, bmp_bytes_to_write); |
| 937 | 938 | return; |
| 938 | 939 | }, |
| ... | ... | @@ -956,7 +957,7 @@ pub const Compiler = struct { |
| 956 | 957 | return; |
| 957 | 958 | } |
| 958 | 959 | header.applyMemoryFlags(node.common_resource_attributes, self.source); |
| 959 | const file_size = try file.getEndPos(); | |
| 960 | const file_size = try file_reader.getSize(); | |
| 960 | 961 | if (file_size > std.math.maxInt(u32)) { |
| 961 | 962 | return self.addErrorDetailsAndFail(.{ |
| 962 | 963 | .err = .resource_data_size_exceeds_max, |
| ... | ... | @@ -968,8 +969,9 @@ pub const Compiler = struct { |
| 968 | 969 | header.data_size = @intCast(file_size); |
| 969 | 970 | try header.write(writer, self.errContext(node.id)); |
| 970 | 971 | |
| 971 | var header_slurping_reader = headerSlurpingReader(148, file.deprecatedReader()); | |
| 972 | try writeResourceData(writer, header_slurping_reader.reader(), header.data_size); | |
| 972 | var header_slurping_reader = headerSlurpingReader(148, file_reader.interface.adaptToOldInterface()); | |
| 973 | var adapter = header_slurping_reader.reader().adaptToNewApi(&.{}); | |
| 974 | try writeResourceData(writer, &adapter.new_interface, header.data_size); | |
| 973 | 975 | |
| 974 | 976 | try self.state.font_dir.add(self.arena, FontDir.Font{ |
| 975 | 977 | .id = header.name_value.ordinal, |
| ... | ... | @@ -992,7 +994,7 @@ pub const Compiler = struct { |
| 992 | 994 | } |
| 993 | 995 | |
| 994 | 996 | // Fallback to just writing out the entire contents of the file |
| 995 | const data_size = try file.getEndPos(); | |
| 997 | const data_size = try file_reader.getSize(); | |
| 996 | 998 | if (data_size > std.math.maxInt(u32)) { |
| 997 | 999 | return self.addErrorDetailsAndFail(.{ |
| 998 | 1000 | .err = .resource_data_size_exceeds_max, |
| ... | ... | @@ -1002,7 +1004,7 @@ pub const Compiler = struct { |
| 1002 | 1004 | // We now know that the data size will fit in a u32 |
| 1003 | 1005 | header.data_size = @intCast(data_size); |
| 1004 | 1006 | try header.write(writer, self.errContext(node.id)); |
| 1005 | try writeResourceData(writer, file.deprecatedReader(), header.data_size); | |
| 1007 | try writeResourceData(writer, &file_reader.interface, header.data_size); | |
| 1006 | 1008 | } |
| 1007 | 1009 | |
| 1008 | 1010 | fn iconReadError( |
| ... | ... | @@ -1250,8 +1252,8 @@ pub const Compiler = struct { |
| 1250 | 1252 | const data_len: u32 = @intCast(data_buffer.items.len); |
| 1251 | 1253 | try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language); |
| 1252 | 1254 | |
| 1253 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 1254 | try writeResourceData(writer, data_fbs.reader(), data_len); | |
| 1255 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 1256 | try writeResourceData(writer, &data_fbs, data_len); | |
| 1255 | 1257 | } |
| 1256 | 1258 | |
| 1257 | 1259 | pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void { |
| ... | ... | @@ -1266,15 +1268,15 @@ pub const Compiler = struct { |
| 1266 | 1268 | try header.write(writer, self.errContext(id_token)); |
| 1267 | 1269 | } |
| 1268 | 1270 | |
| 1269 | pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void { | |
| 1270 | var limited_reader = std.io.limitedReader(data_reader, data_size); | |
| 1271 | ||
| 1272 | const FifoBuffer = std.fifo.LinearFifo(u8, .{ .Static = 4096 }); | |
| 1273 | var fifo = FifoBuffer.init(); | |
| 1274 | try fifo.pump(limited_reader.reader(), writer); | |
| 1271 | pub fn writeResourceDataNoPadding(writer: anytype, data_reader: *std.Io.Reader, data_size: u32) !void { | |
| 1272 | var adapted = writer.adaptToNewApi(); | |
| 1273 | var buffer: [128]u8 = undefined; | |
| 1274 | adapted.new_interface.buffer = &buffer; | |
| 1275 | try data_reader.streamExact(&adapted.new_interface, data_size); | |
| 1276 | try adapted.new_interface.flush(); | |
| 1275 | 1277 | } |
| 1276 | 1278 | |
| 1277 | pub fn writeResourceData(writer: anytype, data_reader: anytype, data_size: u32) !void { | |
| 1279 | pub fn writeResourceData(writer: anytype, data_reader: *std.Io.Reader, data_size: u32) !void { | |
| 1278 | 1280 | try writeResourceDataNoPadding(writer, data_reader, data_size); |
| 1279 | 1281 | try writeDataPadding(writer, data_size); |
| 1280 | 1282 | } |
| ... | ... | @@ -1339,8 +1341,8 @@ pub const Compiler = struct { |
| 1339 | 1341 | |
| 1340 | 1342 | try header.write(writer, self.errContext(node.id)); |
| 1341 | 1343 | |
| 1342 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 1343 | try writeResourceData(writer, data_fbs.reader(), data_size); | |
| 1344 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 1345 | try writeResourceData(writer, &data_fbs, data_size); | |
| 1344 | 1346 | } |
| 1345 | 1347 | |
| 1346 | 1348 | /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to |
| ... | ... | @@ -1732,8 +1734,8 @@ pub const Compiler = struct { |
| 1732 | 1734 | |
| 1733 | 1735 | try header.write(writer, self.errContext(node.id)); |
| 1734 | 1736 | |
| 1735 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 1736 | try writeResourceData(writer, data_fbs.reader(), data_size); | |
| 1737 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 1738 | try writeResourceData(writer, &data_fbs, data_size); | |
| 1737 | 1739 | } |
| 1738 | 1740 | |
| 1739 | 1741 | fn writeDialogHeaderAndStrings( |
| ... | ... | @@ -2046,8 +2048,8 @@ pub const Compiler = struct { |
| 2046 | 2048 | |
| 2047 | 2049 | try header.write(writer, self.errContext(node.id)); |
| 2048 | 2050 | |
| 2049 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 2050 | try writeResourceData(writer, data_fbs.reader(), data_size); | |
| 2051 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 2052 | try writeResourceData(writer, &data_fbs, data_size); | |
| 2051 | 2053 | } |
| 2052 | 2054 | |
| 2053 | 2055 | /// Weight and italic carry over from previous FONT statements within a single resource, |
| ... | ... | @@ -2121,8 +2123,8 @@ pub const Compiler = struct { |
| 2121 | 2123 | |
| 2122 | 2124 | try header.write(writer, self.errContext(node.id)); |
| 2123 | 2125 | |
| 2124 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 2125 | try writeResourceData(writer, data_fbs.reader(), data_size); | |
| 2126 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 2127 | try writeResourceData(writer, &data_fbs, data_size); | |
| 2126 | 2128 | } |
| 2127 | 2129 | |
| 2128 | 2130 | /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to |
| ... | ... | @@ -2386,8 +2388,8 @@ pub const Compiler = struct { |
| 2386 | 2388 | |
| 2387 | 2389 | try header.write(writer, self.errContext(node.id)); |
| 2388 | 2390 | |
| 2389 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 2390 | try writeResourceData(writer, data_fbs.reader(), data_size); | |
| 2391 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 2392 | try writeResourceData(writer, &data_fbs, data_size); | |
| 2391 | 2393 | } |
| 2392 | 2394 | |
| 2393 | 2395 | /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to |
| ... | ... | @@ -3321,8 +3323,8 @@ pub const StringTable = struct { |
| 3321 | 3323 | // we fully control and know are numbers, so they have a fixed size. |
| 3322 | 3324 | try header.writeAssertNoOverflow(writer); |
| 3323 | 3325 | |
| 3324 | var data_fbs = std.io.fixedBufferStream(data_buffer.items); | |
| 3325 | try Compiler.writeResourceData(writer, data_fbs.reader(), data_size); | |
| 3326 | var data_fbs: std.Io.Reader = .fixed(data_buffer.items); | |
| 3327 | try Compiler.writeResourceData(writer, &data_fbs, data_size); | |
| 3326 | 3328 | } |
| 3327 | 3329 | }; |
| 3328 | 3330 |
lib/compiler/resinator/cvtres.zig+24-29| ... | ... | @@ -65,7 +65,7 @@ pub const ParseResOptions = struct { |
| 65 | 65 | }; |
| 66 | 66 | |
| 67 | 67 | /// The returned ParsedResources should be freed by calling its `deinit` function. |
| 68 | pub fn parseRes(allocator: Allocator, reader: anytype, options: ParseResOptions) !ParsedResources { | |
| 68 | pub fn parseRes(allocator: Allocator, reader: *std.Io.Reader, options: ParseResOptions) !ParsedResources { | |
| 69 | 69 | var resources = ParsedResources.init(allocator); |
| 70 | 70 | errdefer resources.deinit(); |
| 71 | 71 | |
| ... | ... | @@ -74,7 +74,7 @@ pub fn parseRes(allocator: Allocator, reader: anytype, options: ParseResOptions) |
| 74 | 74 | return resources; |
| 75 | 75 | } |
| 76 | 76 | |
| 77 | pub fn parseResInto(resources: *ParsedResources, reader: anytype, options: ParseResOptions) !void { | |
| 77 | pub fn parseResInto(resources: *ParsedResources, reader: *std.Io.Reader, options: ParseResOptions) !void { | |
| 78 | 78 | const allocator = resources.allocator; |
| 79 | 79 | var bytes_remaining: u64 = options.max_size; |
| 80 | 80 | { |
| ... | ... | @@ -103,43 +103,38 @@ pub const ResourceAndSize = struct { |
| 103 | 103 | total_size: u64, |
| 104 | 104 | }; |
| 105 | 105 | |
| 106 | pub fn parseResource(allocator: Allocator, reader: anytype, max_size: u64) !ResourceAndSize { | |
| 107 | var header_counting_reader = std.io.countingReader(reader); | |
| 108 | const header_reader = header_counting_reader.reader(); | |
| 109 | const data_size = try header_reader.readInt(u32, .little); | |
| 110 | const header_size = try header_reader.readInt(u32, .little); | |
| 106 | pub fn parseResource(allocator: Allocator, reader: *std.Io.Reader, max_size: u64) !ResourceAndSize { | |
| 107 | const data_size = try reader.takeInt(u32, .little); | |
| 108 | const header_size = try reader.takeInt(u32, .little); | |
| 111 | 109 | const total_size: u64 = @as(u64, header_size) + data_size; |
| 112 | 110 | if (total_size > max_size) return error.ImpossibleSize; |
| 113 | 111 | |
| 114 | var header_bytes_available = header_size -| 8; | |
| 115 | var type_reader = std.io.limitedReader(header_reader, header_bytes_available); | |
| 116 | const type_value = try parseNameOrOrdinal(allocator, type_reader.reader()); | |
| 112 | const remaining_header_bytes = try reader.take(header_size -| 8); | |
| 113 | var remaining_header_reader: std.Io.Reader = .fixed(remaining_header_bytes); | |
| 114 | const type_value = try parseNameOrOrdinal(allocator, &remaining_header_reader); | |
| 117 | 115 | errdefer type_value.deinit(allocator); |
| 118 | 116 | |
| 119 | header_bytes_available -|= @intCast(type_value.byteLen()); | |
| 120 | var name_reader = std.io.limitedReader(header_reader, header_bytes_available); | |
| 121 | const name_value = try parseNameOrOrdinal(allocator, name_reader.reader()); | |
| 117 | const name_value = try parseNameOrOrdinal(allocator, &remaining_header_reader); | |
| 122 | 118 | errdefer name_value.deinit(allocator); |
| 123 | 119 | |
| 124 | const padding_after_name = numPaddingBytesNeeded(@intCast(header_counting_reader.bytes_read)); | |
| 125 | try header_reader.skipBytes(padding_after_name, .{ .buf_size = 3 }); | |
| 120 | const padding_after_name = numPaddingBytesNeeded(@intCast(remaining_header_reader.seek)); | |
| 121 | try remaining_header_reader.discardAll(padding_after_name); | |
| 126 | 122 | |
| 127 | std.debug.assert(header_counting_reader.bytes_read % 4 == 0); | |
| 128 | const data_version = try header_reader.readInt(u32, .little); | |
| 129 | const memory_flags: MemoryFlags = @bitCast(try header_reader.readInt(u16, .little)); | |
| 130 | const language: Language = @bitCast(try header_reader.readInt(u16, .little)); | |
| 131 | const version = try header_reader.readInt(u32, .little); | |
| 132 | const characteristics = try header_reader.readInt(u32, .little); | |
| 123 | std.debug.assert(remaining_header_reader.seek % 4 == 0); | |
| 124 | const data_version = try remaining_header_reader.takeInt(u32, .little); | |
| 125 | const memory_flags: MemoryFlags = @bitCast(try remaining_header_reader.takeInt(u16, .little)); | |
| 126 | const language: Language = @bitCast(try remaining_header_reader.takeInt(u16, .little)); | |
| 127 | const version = try remaining_header_reader.takeInt(u32, .little); | |
| 128 | const characteristics = try remaining_header_reader.takeInt(u32, .little); | |
| 133 | 129 | |
| 134 | const header_bytes_read = header_counting_reader.bytes_read; | |
| 135 | if (header_size != header_bytes_read) return error.HeaderSizeMismatch; | |
| 130 | if (remaining_header_reader.seek != remaining_header_reader.end) return error.HeaderSizeMismatch; | |
| 136 | 131 | |
| 137 | 132 | const data = try allocator.alloc(u8, data_size); |
| 138 | 133 | errdefer allocator.free(data); |
| 139 | try reader.readNoEof(data); | |
| 134 | try reader.readSliceAll(data); | |
| 140 | 135 | |
| 141 | 136 | const padding_after_data = numPaddingBytesNeeded(@intCast(data_size)); |
| 142 | try reader.skipBytes(padding_after_data, .{ .buf_size = 3 }); | |
| 137 | try reader.discardAll(padding_after_data); | |
| 143 | 138 | |
| 144 | 139 | return .{ |
| 145 | 140 | .resource = .{ |
| ... | ... | @@ -156,10 +151,10 @@ pub fn parseResource(allocator: Allocator, reader: anytype, max_size: u64) !Reso |
| 156 | 151 | }; |
| 157 | 152 | } |
| 158 | 153 | |
| 159 | pub fn parseNameOrOrdinal(allocator: Allocator, reader: anytype) !NameOrOrdinal { | |
| 160 | const first_code_unit = try reader.readInt(u16, .little); | |
| 154 | pub fn parseNameOrOrdinal(allocator: Allocator, reader: *std.Io.Reader) !NameOrOrdinal { | |
| 155 | const first_code_unit = try reader.takeInt(u16, .little); | |
| 161 | 156 | if (first_code_unit == 0xFFFF) { |
| 162 | const ordinal_value = try reader.readInt(u16, .little); | |
| 157 | const ordinal_value = try reader.takeInt(u16, .little); | |
| 163 | 158 | return .{ .ordinal = ordinal_value }; |
| 164 | 159 | } |
| 165 | 160 | var name_buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 16); |
| ... | ... | @@ -167,7 +162,7 @@ pub fn parseNameOrOrdinal(allocator: Allocator, reader: anytype) !NameOrOrdinal |
| 167 | 162 | var code_unit = first_code_unit; |
| 168 | 163 | while (code_unit != 0) { |
| 169 | 164 | try name_buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); |
| 170 | code_unit = try reader.readInt(u16, .little); | |
| 165 | code_unit = try reader.takeInt(u16, .little); | |
| 171 | 166 | } |
| 172 | 167 | return .{ .name = try name_buf.toOwnedSliceSentinel(allocator, 0) }; |
| 173 | 168 | } |
lib/compiler/resinator/errors.zig+4-8| ... | ... | @@ -1078,11 +1078,9 @@ const CorrespondingLines = struct { |
| 1078 | 1078 | at_eof: bool = false, |
| 1079 | 1079 | span: SourceMappings.CorrespondingSpan, |
| 1080 | 1080 | file: std.fs.File, |
| 1081 | buffered_reader: BufferedReaderType, | |
| 1081 | buffered_reader: std.fs.File.Reader, | |
| 1082 | 1082 | code_page: SupportedCodePage, |
| 1083 | 1083 | |
| 1084 | const BufferedReaderType = std.io.BufferedReader(512, std.fs.File.DeprecatedReader); | |
| 1085 | ||
| 1086 | 1084 | pub fn init(cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines { |
| 1087 | 1085 | // We don't do line comparison for this error, so don't print the note if the line |
| 1088 | 1086 | // number is different |
| ... | ... | @@ -1101,9 +1099,7 @@ const CorrespondingLines = struct { |
| 1101 | 1099 | .buffered_reader = undefined, |
| 1102 | 1100 | .code_page = err_details.code_page, |
| 1103 | 1101 | }; |
| 1104 | corresponding_lines.buffered_reader = BufferedReaderType{ | |
| 1105 | .unbuffered_reader = corresponding_lines.file.deprecatedReader(), | |
| 1106 | }; | |
| 1102 | corresponding_lines.buffered_reader = corresponding_lines.file.reader(&.{}); | |
| 1107 | 1103 | errdefer corresponding_lines.deinit(); |
| 1108 | 1104 | |
| 1109 | 1105 | var fbs = std.io.fixedBufferStream(&corresponding_lines.line_buf); |
| ... | ... | @@ -1111,7 +1107,7 @@ const CorrespondingLines = struct { |
| 1111 | 1107 | |
| 1112 | 1108 | try corresponding_lines.writeLineFromStreamVerbatim( |
| 1113 | 1109 | writer, |
| 1114 | corresponding_lines.buffered_reader.reader(), | |
| 1110 | corresponding_lines.buffered_reader.interface.adaptToOldInterface(), | |
| 1115 | 1111 | corresponding_span.start_line, |
| 1116 | 1112 | ); |
| 1117 | 1113 | |
| ... | ... | @@ -1154,7 +1150,7 @@ const CorrespondingLines = struct { |
| 1154 | 1150 | |
| 1155 | 1151 | try self.writeLineFromStreamVerbatim( |
| 1156 | 1152 | writer, |
| 1157 | self.buffered_reader.reader(), | |
| 1153 | self.buffered_reader.interface.adaptToOldInterface(), | |
| 1158 | 1154 | self.line_num, |
| 1159 | 1155 | ); |
| 1160 | 1156 |
lib/compiler/resinator/ico.zig+2-1| ... | ... | @@ -14,8 +14,9 @@ pub fn read(allocator: std.mem.Allocator, reader: anytype, max_size: u64) ReadEr |
| 14 | 14 | // Some Reader implementations have an empty ReadError error set which would |
| 15 | 15 | // cause 'unreachable else' if we tried to use an else in the switch, so we |
| 16 | 16 | // need to detect this case and not try to translate to ReadError |
| 17 | const anyerror_reader_errorset = @TypeOf(reader).Error == anyerror; | |
| 17 | 18 | const empty_reader_errorset = @typeInfo(@TypeOf(reader).Error).error_set == null or @typeInfo(@TypeOf(reader).Error).error_set.?.len == 0; |
| 18 | if (empty_reader_errorset) { | |
| 19 | if (empty_reader_errorset and !anyerror_reader_errorset) { | |
| 19 | 20 | return readAnyError(allocator, reader, max_size) catch |err| switch (err) { |
| 20 | 21 | error.EndOfStream => error.UnexpectedEOF, |
| 21 | 22 | else => |e| return e, |
lib/compiler/resinator/main.zig+2-2| ... | ... | @@ -325,8 +325,8 @@ pub fn main() !void { |
| 325 | 325 | std.debug.assert(options.output_format == .coff); |
| 326 | 326 | |
| 327 | 327 | // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs |
| 328 | var fbs = std.io.fixedBufferStream(res_data.bytes); | |
| 329 | break :resources cvtres.parseRes(allocator, fbs.reader(), .{ .max_size = res_data.bytes.len }) catch |err| { | |
| 328 | var res_reader: std.Io.Reader = .fixed(res_data.bytes); | |
| 329 | break :resources cvtres.parseRes(allocator, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { | |
| 330 | 330 | // TODO: Better errors |
| 331 | 331 | try error_handler.emitMessage(allocator, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); |
| 332 | 332 | std.process.exit(1); |
lib/docs/wasm/markdown.zig+6-7| ... | ... | @@ -145,13 +145,12 @@ fn mainImpl() !void { |
| 145 | 145 | var parser = try Parser.init(gpa); |
| 146 | 146 | defer parser.deinit(); |
| 147 | 147 | |
| 148 | var stdin_buf = std.io.bufferedReader(std.fs.File.stdin().deprecatedReader()); | |
| 149 | var line_buf = std.ArrayList(u8).init(gpa); | |
| 150 | defer line_buf.deinit(); | |
| 151 | while (stdin_buf.reader().streamUntilDelimiter(line_buf.writer(), '\n', null)) { | |
| 152 | if (line_buf.getLastOrNull() == '\r') _ = line_buf.pop(); | |
| 153 | try parser.feedLine(line_buf.items); | |
| 154 | line_buf.clearRetainingCapacity(); | |
| 148 | var stdin_buffer: [1024]u8 = undefined; | |
| 149 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); | |
| 150 | ||
| 151 | while (stdin_reader.takeDelimiterExclusive('\n')) |line| { | |
| 152 | const trimmed = std.mem.trimRight(u8, line, '\r'); | |
| 153 | try parser.feedLine(trimmed); | |
| 155 | 154 | } else |err| switch (err) { |
| 156 | 155 | error.EndOfStream => {}, |
| 157 | 156 | else => |e| return e, |
lib/std/Build/Fuzz.zig+16-23| ... | ... | @@ -234,7 +234,7 @@ pub const Previous = struct { |
| 234 | 234 | }; |
| 235 | 235 | pub fn sendUpdate( |
| 236 | 236 | fuzz: *Fuzz, |
| 237 | socket: *std.http.WebSocket, | |
| 237 | socket: *std.http.Server.WebSocket, | |
| 238 | 238 | prev: *Previous, |
| 239 | 239 | ) !void { |
| 240 | 240 | fuzz.coverage_mutex.lock(); |
| ... | ... | @@ -263,36 +263,36 @@ pub fn sendUpdate( |
| 263 | 263 | .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), |
| 264 | 264 | .start_timestamp = coverage_map.start_timestamp, |
| 265 | 265 | }; |
| 266 | const iovecs: [5]std.posix.iovec_const = .{ | |
| 267 | makeIov(@ptrCast(&header)), | |
| 268 | makeIov(@ptrCast(coverage_map.coverage.directories.keys())), | |
| 269 | makeIov(@ptrCast(coverage_map.coverage.files.keys())), | |
| 270 | makeIov(@ptrCast(coverage_map.source_locations)), | |
| 271 | makeIov(coverage_map.coverage.string_bytes.items), | |
| 266 | var iovecs: [5][]const u8 = .{ | |
| 267 | @ptrCast(&header), | |
| 268 | @ptrCast(coverage_map.coverage.directories.keys()), | |
| 269 | @ptrCast(coverage_map.coverage.files.keys()), | |
| 270 | @ptrCast(coverage_map.source_locations), | |
| 271 | coverage_map.coverage.string_bytes.items, | |
| 272 | 272 | }; |
| 273 | try socket.writeMessagev(&iovecs, .binary); | |
| 273 | try socket.writeMessageVec(&iovecs, .binary); | |
| 274 | 274 | } |
| 275 | 275 | |
| 276 | 276 | const header: abi.CoverageUpdateHeader = .{ |
| 277 | 277 | .n_runs = n_runs, |
| 278 | 278 | .unique_runs = unique_runs, |
| 279 | 279 | }; |
| 280 | const iovecs: [2]std.posix.iovec_const = .{ | |
| 281 | makeIov(@ptrCast(&header)), | |
| 282 | makeIov(@ptrCast(seen_pcs)), | |
| 280 | var iovecs: [2][]const u8 = .{ | |
| 281 | @ptrCast(&header), | |
| 282 | @ptrCast(seen_pcs), | |
| 283 | 283 | }; |
| 284 | try socket.writeMessagev(&iovecs, .binary); | |
| 284 | try socket.writeMessageVec(&iovecs, .binary); | |
| 285 | 285 | |
| 286 | 286 | prev.unique_runs = unique_runs; |
| 287 | 287 | } |
| 288 | 288 | |
| 289 | 289 | if (prev.entry_points != coverage_map.entry_points.items.len) { |
| 290 | 290 | const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len)); |
| 291 | const iovecs: [2]std.posix.iovec_const = .{ | |
| 292 | makeIov(@ptrCast(&header)), | |
| 293 | makeIov(@ptrCast(coverage_map.entry_points.items)), | |
| 291 | var iovecs: [2][]const u8 = .{ | |
| 292 | @ptrCast(&header), | |
| 293 | @ptrCast(coverage_map.entry_points.items), | |
| 294 | 294 | }; |
| 295 | try socket.writeMessagev(&iovecs, .binary); | |
| 295 | try socket.writeMessageVec(&iovecs, .binary); | |
| 296 | 296 | |
| 297 | 297 | prev.entry_points = coverage_map.entry_points.items.len; |
| 298 | 298 | } |
| ... | ... | @@ -448,10 +448,3 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte |
| 448 | 448 | } |
| 449 | 449 | try coverage_map.entry_points.append(fuzz.ws.gpa, @intCast(index)); |
| 450 | 450 | } |
| 451 | ||
| 452 | fn makeIov(s: []const u8) std.posix.iovec_const { | |
| 453 | return .{ | |
| 454 | .base = s.ptr, | |
| 455 | .len = s.len, | |
| 456 | }; | |
| 457 | } |
lib/std/Build/WebServer.zig+42-53| ... | ... | @@ -251,48 +251,44 @@ pub fn now(s: *const WebServer) i64 { |
| 251 | 251 | fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { |
| 252 | 252 | defer connection.stream.close(); |
| 253 | 253 | |
| 254 | var read_buf: [0x4000]u8 = undefined; | |
| 255 | var server: std.http.Server = .init(connection, &read_buf); | |
| 254 | var send_buffer: [4096]u8 = undefined; | |
| 255 | var recv_buffer: [4096]u8 = undefined; | |
| 256 | var connection_reader = connection.stream.reader(&recv_buffer); | |
| 257 | var connection_writer = connection.stream.writer(&send_buffer); | |
| 258 | var server: http.Server = .init(connection_reader.interface(), &connection_writer.interface); | |
| 256 | 259 | |
| 257 | 260 | while (true) { |
| 258 | 261 | var request = server.receiveHead() catch |err| switch (err) { |
| 259 | 262 | error.HttpConnectionClosing => return, |
| 260 | else => { | |
| 261 | log.err("failed to receive http request: {s}", .{@errorName(err)}); | |
| 262 | return; | |
| 263 | }, | |
| 263 | else => return log.err("failed to receive http request: {t}", .{err}), | |
| 264 | 264 | }; |
| 265 | var ws_send_buf: [0x4000]u8 = undefined; | |
| 266 | var ws_recv_buf: [0x4000]u8 align(4) = undefined; | |
| 267 | if (std.http.WebSocket.init(&request, &ws_send_buf, &ws_recv_buf) catch |err| { | |
| 268 | log.err("failed to initialize websocket connection: {s}", .{@errorName(err)}); | |
| 269 | return; | |
| 270 | }) |ws_init| { | |
| 271 | var web_socket = ws_init; | |
| 272 | ws.serveWebSocket(&web_socket) catch |err| { | |
| 273 | log.err("failed to serve websocket: {s}", .{@errorName(err)}); | |
| 274 | return; | |
| 275 | }; | |
| 276 | comptime unreachable; | |
| 277 | } else { | |
| 278 | ws.serveRequest(&request) catch |err| switch (err) { | |
| 279 | error.AlreadyReported => return, | |
| 280 | else => { | |
| 281 | log.err("failed to serve '{s}': {s}", .{ request.head.target, @errorName(err) }); | |
| 265 | switch (request.upgradeRequested()) { | |
| 266 | .websocket => |opt_key| { | |
| 267 | const key = opt_key orelse return log.err("missing websocket key", .{}); | |
| 268 | var web_socket = request.respondWebSocket(.{ .key = key }) catch { | |
| 269 | return log.err("failed to respond web socket: {t}", .{connection_writer.err.?}); | |
| 270 | }; | |
| 271 | ws.serveWebSocket(&web_socket) catch |err| { | |
| 272 | log.err("failed to serve websocket: {t}", .{err}); | |
| 282 | 273 | return; |
| 283 | }, | |
| 284 | }; | |
| 274 | }; | |
| 275 | comptime unreachable; | |
| 276 | }, | |
| 277 | .other => |name| return log.err("unknown upgrade request: {s}", .{name}), | |
| 278 | .none => { | |
| 279 | ws.serveRequest(&request) catch |err| switch (err) { | |
| 280 | error.AlreadyReported => return, | |
| 281 | else => { | |
| 282 | log.err("failed to serve '{s}': {t}", .{ request.head.target, err }); | |
| 283 | return; | |
| 284 | }, | |
| 285 | }; | |
| 286 | }, | |
| 285 | 287 | } |
| 286 | 288 | } |
| 287 | 289 | } |
| 288 | 290 | |
| 289 | fn makeIov(s: []const u8) std.posix.iovec_const { | |
| 290 | return .{ | |
| 291 | .base = s.ptr, | |
| 292 | .len = s.len, | |
| 293 | }; | |
| 294 | } | |
| 295 | fn serveWebSocket(ws: *WebServer, sock: *std.http.WebSocket) !noreturn { | |
| 291 | fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn { | |
| 296 | 292 | var prev_build_status = ws.build_status.load(.monotonic); |
| 297 | 293 | |
| 298 | 294 | const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len); |
| ... | ... | @@ -312,11 +308,8 @@ fn serveWebSocket(ws: *WebServer, sock: *std.http.WebSocket) !noreturn { |
| 312 | 308 | .timestamp = ws.now(), |
| 313 | 309 | .steps_len = @intCast(ws.all_steps.len), |
| 314 | 310 | }; |
| 315 | try sock.writeMessagev(&.{ | |
| 316 | makeIov(@ptrCast(&hello_header)), | |
| 317 | makeIov(ws.step_names_trailing), | |
| 318 | makeIov(prev_step_status_bits), | |
| 319 | }, .binary); | |
| 311 | var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits }; | |
| 312 | try sock.writeMessageVec(&bufs, .binary); | |
| 320 | 313 | } |
| 321 | 314 | |
| 322 | 315 | var prev_fuzz: Fuzz.Previous = .init; |
| ... | ... | @@ -380,7 +373,7 @@ fn serveWebSocket(ws: *WebServer, sock: *std.http.WebSocket) !noreturn { |
| 380 | 373 | std.Thread.Futex.timedWait(&ws.update_id, start_update_id, std.time.ns_per_ms * default_update_interval_ms) catch {}; |
| 381 | 374 | } |
| 382 | 375 | } |
| 383 | fn recvWebSocketMessages(ws: *WebServer, sock: *std.http.WebSocket) void { | |
| 376 | fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void { | |
| 384 | 377 | while (true) { |
| 385 | 378 | const msg = sock.readSmallMessage() catch return; |
| 386 | 379 | if (msg.opcode != .binary) continue; |
| ... | ... | @@ -402,7 +395,7 @@ fn recvWebSocketMessages(ws: *WebServer, sock: *std.http.WebSocket) void { |
| 402 | 395 | } |
| 403 | 396 | } |
| 404 | 397 | |
| 405 | fn serveRequest(ws: *WebServer, req: *std.http.Server.Request) !void { | |
| 398 | fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void { | |
| 406 | 399 | // Strip an optional leading '/debug' component from the request. |
| 407 | 400 | const target: []const u8, const debug: bool = target: { |
| 408 | 401 | if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true }; |
| ... | ... | @@ -431,7 +424,7 @@ fn serveRequest(ws: *WebServer, req: *std.http.Server.Request) !void { |
| 431 | 424 | |
| 432 | 425 | fn serveLibFile( |
| 433 | 426 | ws: *WebServer, |
| 434 | request: *std.http.Server.Request, | |
| 427 | request: *http.Server.Request, | |
| 435 | 428 | sub_path: []const u8, |
| 436 | 429 | content_type: []const u8, |
| 437 | 430 | ) !void { |
| ... | ... | @@ -442,7 +435,7 @@ fn serveLibFile( |
| 442 | 435 | } |
| 443 | 436 | fn serveClientWasm( |
| 444 | 437 | ws: *WebServer, |
| 445 | req: *std.http.Server.Request, | |
| 438 | req: *http.Server.Request, | |
| 446 | 439 | optimize_mode: std.builtin.OptimizeMode, |
| 447 | 440 | ) !void { |
| 448 | 441 | var arena_state: std.heap.ArenaAllocator = .init(ws.gpa); |
| ... | ... | @@ -456,12 +449,12 @@ fn serveClientWasm( |
| 456 | 449 | |
| 457 | 450 | pub fn serveFile( |
| 458 | 451 | ws: *WebServer, |
| 459 | request: *std.http.Server.Request, | |
| 452 | request: *http.Server.Request, | |
| 460 | 453 | path: Cache.Path, |
| 461 | 454 | content_type: []const u8, |
| 462 | 455 | ) !void { |
| 463 | 456 | const gpa = ws.gpa; |
| 464 | // The desired API is actually sendfile, which will require enhancing std.http.Server. | |
| 457 | // The desired API is actually sendfile, which will require enhancing http.Server. | |
| 465 | 458 | // We load the file with every request so that the user can make changes to the file |
| 466 | 459 | // and refresh the HTML page without restarting this server. |
| 467 | 460 | const file_contents = path.root_dir.handle.readFileAlloc(gpa, path.sub_path, 10 * 1024 * 1024) catch |err| { |
| ... | ... | @@ -478,14 +471,13 @@ pub fn serveFile( |
| 478 | 471 | } |
| 479 | 472 | pub fn serveTarFile( |
| 480 | 473 | ws: *WebServer, |
| 481 | request: *std.http.Server.Request, | |
| 474 | request: *http.Server.Request, | |
| 482 | 475 | paths: []const Cache.Path, |
| 483 | 476 | ) !void { |
| 484 | 477 | const gpa = ws.gpa; |
| 485 | 478 | |
| 486 | var send_buf: [0x4000]u8 = undefined; | |
| 487 | var response = request.respondStreaming(.{ | |
| 488 | .send_buffer = &send_buf, | |
| 479 | var send_buffer: [0x4000]u8 = undefined; | |
| 480 | var response = try request.respondStreaming(&send_buffer, .{ | |
| 489 | 481 | .respond_options = .{ |
| 490 | 482 | .extra_headers = &.{ |
| 491 | 483 | .{ .name = "Content-Type", .value = "application/x-tar" }, |
| ... | ... | @@ -497,10 +489,7 @@ pub fn serveTarFile( |
| 497 | 489 | var cached_cwd_path: ?[]const u8 = null; |
| 498 | 490 | defer if (cached_cwd_path) |p| gpa.free(p); |
| 499 | 491 | |
| 500 | var response_buf: [1024]u8 = undefined; | |
| 501 | var adapter = response.writer().adaptToNewApi(); | |
| 502 | adapter.new_interface.buffer = &response_buf; | |
| 503 | var archiver: std.tar.Writer = .{ .underlying_writer = &adapter.new_interface }; | |
| 492 | var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; | |
| 504 | 493 | |
| 505 | 494 | for (paths) |path| { |
| 506 | 495 | var file = path.root_dir.handle.openFile(path.sub_path, .{}) catch |err| { |
| ... | ... | @@ -526,7 +515,6 @@ pub fn serveTarFile( |
| 526 | 515 | } |
| 527 | 516 | |
| 528 | 517 | // intentionally not calling `archiver.finishPedantically` |
| 529 | try adapter.new_interface.flush(); | |
| 530 | 518 | try response.end(); |
| 531 | 519 | } |
| 532 | 520 | |
| ... | ... | @@ -804,7 +792,7 @@ pub fn wait(ws: *WebServer) RunnerRequest { |
| 804 | 792 | } |
| 805 | 793 | } |
| 806 | 794 | |
| 807 | const cache_control_header: std.http.Header = .{ | |
| 795 | const cache_control_header: http.Header = .{ | |
| 808 | 796 | .name = "Cache-Control", |
| 809 | 797 | .value = "max-age=0, must-revalidate", |
| 810 | 798 | }; |
| ... | ... | @@ -819,5 +807,6 @@ const Build = std.Build; |
| 819 | 807 | const Cache = Build.Cache; |
| 820 | 808 | const Fuzz = Build.Fuzz; |
| 821 | 809 | const abi = Build.abi; |
| 810 | const http = std.http; | |
| 822 | 811 | |
| 823 | 812 | const WebServer = @This(); |
lib/std/Io.zig-11| ... | ... | @@ -428,19 +428,9 @@ pub const BufferedWriter = @import("Io/buffered_writer.zig").BufferedWriter; |
| 428 | 428 | /// Deprecated in favor of `Writer`. |
| 429 | 429 | pub const bufferedWriter = @import("Io/buffered_writer.zig").bufferedWriter; |
| 430 | 430 | /// Deprecated in favor of `Reader`. |
| 431 | pub const BufferedReader = @import("Io/buffered_reader.zig").BufferedReader; | |
| 432 | /// Deprecated in favor of `Reader`. | |
| 433 | pub const bufferedReader = @import("Io/buffered_reader.zig").bufferedReader; | |
| 434 | /// Deprecated in favor of `Reader`. | |
| 435 | pub const bufferedReaderSize = @import("Io/buffered_reader.zig").bufferedReaderSize; | |
| 436 | /// Deprecated in favor of `Reader`. | |
| 437 | 431 | pub const FixedBufferStream = @import("Io/fixed_buffer_stream.zig").FixedBufferStream; |
| 438 | 432 | /// Deprecated in favor of `Reader`. |
| 439 | 433 | pub const fixedBufferStream = @import("Io/fixed_buffer_stream.zig").fixedBufferStream; |
| 440 | /// Deprecated in favor of `Reader.Limited`. | |
| 441 | pub const LimitedReader = @import("Io/limited_reader.zig").LimitedReader; | |
| 442 | /// Deprecated in favor of `Reader.Limited`. | |
| 443 | pub const limitedReader = @import("Io/limited_reader.zig").limitedReader; | |
| 444 | 434 | /// Deprecated with no replacement; inefficient pattern |
| 445 | 435 | pub const CountingWriter = @import("Io/counting_writer.zig").CountingWriter; |
| 446 | 436 | /// Deprecated with no replacement; inefficient pattern |
| ... | ... | @@ -926,7 +916,6 @@ pub fn PollFiles(comptime StreamEnum: type) type { |
| 926 | 916 | test { |
| 927 | 917 | _ = Reader; |
| 928 | 918 | _ = Writer; |
| 929 | _ = BufferedReader; | |
| 930 | 919 | _ = BufferedWriter; |
| 931 | 920 | _ = CountingWriter; |
| 932 | 921 | _ = CountingReader; |
lib/std/Io/Reader.zig+5-27| ... | ... | @@ -367,8 +367,11 @@ pub fn appendRemainingUnlimited( |
| 367 | 367 | const buffer_contents = r.buffer[r.seek..r.end]; |
| 368 | 368 | try list.ensureUnusedCapacity(gpa, buffer_contents.len + bump); |
| 369 | 369 | list.appendSliceAssumeCapacity(buffer_contents); |
| 370 | r.seek = 0; | |
| 371 | r.end = 0; | |
| 370 | // If statement protects `ending`. | |
| 371 | if (r.end != 0) { | |
| 372 | r.seek = 0; | |
| 373 | r.end = 0; | |
| 374 | } | |
| 372 | 375 | // From here, we leave `buffer` empty, appending directly to `list`. |
| 373 | 376 | var writer: Writer = .{ |
| 374 | 377 | .buffer = undefined, |
| ... | ... | @@ -1306,31 +1309,6 @@ pub fn defaultRebase(r: *Reader, capacity: usize) RebaseError!void { |
| 1306 | 1309 | r.end = data.len; |
| 1307 | 1310 | } |
| 1308 | 1311 | |
| 1309 | /// Advances the stream and decreases the size of the storage buffer by `n`, | |
| 1310 | /// returning the range of bytes no longer accessible by `r`. | |
| 1311 | /// | |
| 1312 | /// This action can be undone by `restitute`. | |
| 1313 | /// | |
| 1314 | /// Asserts there are at least `n` buffered bytes already. | |
| 1315 | /// | |
| 1316 | /// Asserts that `r.seek` is zero, i.e. the buffer is in a rebased state. | |
| 1317 | pub fn steal(r: *Reader, n: usize) []u8 { | |
| 1318 | assert(r.seek == 0); | |
| 1319 | assert(n <= r.end); | |
| 1320 | const stolen = r.buffer[0..n]; | |
| 1321 | r.buffer = r.buffer[n..]; | |
| 1322 | r.end -= n; | |
| 1323 | return stolen; | |
| 1324 | } | |
| 1325 | ||
| 1326 | /// Expands the storage buffer, undoing the effects of `steal` | |
| 1327 | /// Assumes that `n` does not exceed the total number of stolen bytes. | |
| 1328 | pub fn restitute(r: *Reader, n: usize) void { | |
| 1329 | r.buffer = (r.buffer.ptr - n)[0 .. r.buffer.len + n]; | |
| 1330 | r.end += n; | |
| 1331 | r.seek += n; | |
| 1332 | } | |
| 1333 | ||
| 1334 | 1312 | test fixed { |
| 1335 | 1313 | var r: Reader = .fixed("a\x02"); |
| 1336 | 1314 | try testing.expect((try r.takeByte()) == 'a'); |
lib/std/Io/Writer.zig+77-19| ... | ... | @@ -191,29 +191,87 @@ pub fn writeSplatHeader( |
| 191 | 191 | data: []const []const u8, |
| 192 | 192 | splat: usize, |
| 193 | 193 | ) Error!usize { |
| 194 | const new_end = w.end + header.len; | |
| 195 | if (new_end <= w.buffer.len) { | |
| 196 | @memcpy(w.buffer[w.end..][0..header.len], header); | |
| 197 | w.end = new_end; | |
| 198 | return header.len + try writeSplat(w, data, splat); | |
| 199 | } | |
| 200 | var vecs: [8][]const u8 = undefined; // Arbitrarily chosen size. | |
| 201 | var i: usize = 1; | |
| 202 | vecs[0] = header; | |
| 203 | for (data[0 .. data.len - 1]) |buf| { | |
| 204 | if (buf.len == 0) continue; | |
| 205 | vecs[i] = buf; | |
| 206 | i += 1; | |
| 207 | if (vecs.len - i == 0) break; | |
| 194 | return writeSplatHeaderLimit(w, header, data, splat, .unlimited); | |
| 195 | } | |
| 196 | ||
| 197 | /// Equivalent to `writeSplatHeader` but writes at most `limit` bytes. | |
| 198 | pub fn writeSplatHeaderLimit( | |
| 199 | w: *Writer, | |
| 200 | header: []const u8, | |
| 201 | data: []const []const u8, | |
| 202 | splat: usize, | |
| 203 | limit: Limit, | |
| 204 | ) Error!usize { | |
| 205 | var remaining = @intFromEnum(limit); | |
| 206 | { | |
| 207 | const copy_len = @min(header.len, w.buffer.len - w.end, remaining); | |
| 208 | if (header.len - copy_len != 0) return writeSplatHeaderLimitFinish(w, header, data, splat, remaining); | |
| 209 | @memcpy(w.buffer[w.end..][0..copy_len], header[0..copy_len]); | |
| 210 | w.end += copy_len; | |
| 211 | remaining -= copy_len; | |
| 212 | } | |
| 213 | for (data[0 .. data.len - 1], 0..) |buf, i| { | |
| 214 | const copy_len = @min(buf.len, w.buffer.len - w.end, remaining); | |
| 215 | if (buf.len - copy_len != 0) return @intFromEnum(limit) - remaining + | |
| 216 | try writeSplatHeaderLimitFinish(w, &.{}, data[i..], splat, remaining); | |
| 217 | @memcpy(w.buffer[w.end..][0..copy_len], buf[0..copy_len]); | |
| 218 | w.end += copy_len; | |
| 219 | remaining -= copy_len; | |
| 208 | 220 | } |
| 209 | 221 | const pattern = data[data.len - 1]; |
| 210 | const new_splat = s: { | |
| 211 | if (pattern.len == 0 or vecs.len - i == 0) break :s 1; | |
| 222 | const splat_n = pattern.len * splat; | |
| 223 | if (splat_n > @min(w.buffer.len - w.end, remaining)) { | |
| 224 | const buffered_n = @intFromEnum(limit) - remaining; | |
| 225 | const written = try writeSplatHeaderLimitFinish(w, &.{}, data[data.len - 1 ..][0..1], splat, remaining); | |
| 226 | return buffered_n + written; | |
| 227 | } | |
| 228 | ||
| 229 | for (0..splat) |_| { | |
| 230 | @memcpy(w.buffer[w.end..][0..pattern.len], pattern); | |
| 231 | w.end += pattern.len; | |
| 232 | } | |
| 233 | ||
| 234 | remaining -= splat_n; | |
| 235 | return @intFromEnum(limit) - remaining; | |
| 236 | } | |
| 237 | ||
| 238 | fn writeSplatHeaderLimitFinish( | |
| 239 | w: *Writer, | |
| 240 | header: []const u8, | |
| 241 | data: []const []const u8, | |
| 242 | splat: usize, | |
| 243 | limit: usize, | |
| 244 | ) Error!usize { | |
| 245 | var remaining = limit; | |
| 246 | var vecs: [8][]const u8 = undefined; | |
| 247 | var i: usize = 0; | |
| 248 | v: { | |
| 249 | if (header.len != 0) { | |
| 250 | const copy_len = @min(header.len, remaining); | |
| 251 | vecs[i] = header[0..copy_len]; | |
| 252 | i += 1; | |
| 253 | remaining -= copy_len; | |
| 254 | if (remaining == 0) break :v; | |
| 255 | } | |
| 256 | for (data[0 .. data.len - 1]) |buf| if (buf.len != 0) { | |
| 257 | const copy_len = @min(header.len, remaining); | |
| 258 | vecs[i] = buf; | |
| 259 | i += 1; | |
| 260 | remaining -= copy_len; | |
| 261 | if (remaining == 0) break :v; | |
| 262 | if (vecs.len - i == 0) break :v; | |
| 263 | }; | |
| 264 | const pattern = data[data.len - 1]; | |
| 265 | if (splat == 1) { | |
| 266 | vecs[i] = pattern[0..@min(remaining, pattern.len)]; | |
| 267 | i += 1; | |
| 268 | break :v; | |
| 269 | } | |
| 212 | 270 | vecs[i] = pattern; |
| 213 | 271 | i += 1; |
| 214 | break :s splat; | |
| 215 | }; | |
| 216 | return w.vtable.drain(w, vecs[0..i], new_splat); | |
| 272 | return w.vtable.drain(w, (&vecs)[0..i], @min(remaining / pattern.len, splat)); | |
| 273 | } | |
| 274 | return w.vtable.drain(w, (&vecs)[0..i], 1); | |
| 217 | 275 | } |
| 218 | 276 | |
| 219 | 277 | test "writeSplatHeader splatting avoids buffer aliasing temptation" { |
lib/std/Io/buffered_reader.zig deleted-201| ... | ... | @@ -1,201 +0,0 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const io = std.io; | |
| 3 | const mem = std.mem; | |
| 4 | const assert = std.debug.assert; | |
| 5 | const testing = std.testing; | |
| 6 | ||
| 7 | pub fn BufferedReader(comptime buffer_size: usize, comptime ReaderType: type) type { | |
| 8 | return struct { | |
| 9 | unbuffered_reader: ReaderType, | |
| 10 | buf: [buffer_size]u8 = undefined, | |
| 11 | start: usize = 0, | |
| 12 | end: usize = 0, | |
| 13 | ||
| 14 | pub const Error = ReaderType.Error; | |
| 15 | pub const Reader = io.GenericReader(*Self, Error, read); | |
| 16 | ||
| 17 | const Self = @This(); | |
| 18 | ||
| 19 | pub fn read(self: *Self, dest: []u8) Error!usize { | |
| 20 | // First try reading from the already buffered data onto the destination. | |
| 21 | const current = self.buf[self.start..self.end]; | |
| 22 | if (current.len != 0) { | |
| 23 | const to_transfer = @min(current.len, dest.len); | |
| 24 | @memcpy(dest[0..to_transfer], current[0..to_transfer]); | |
| 25 | self.start += to_transfer; | |
| 26 | return to_transfer; | |
| 27 | } | |
| 28 | ||
| 29 | // If dest is large, read from the unbuffered reader directly into the destination. | |
| 30 | if (dest.len >= buffer_size) { | |
| 31 | return self.unbuffered_reader.read(dest); | |
| 32 | } | |
| 33 | ||
| 34 | // If dest is small, read from the unbuffered reader into our own internal buffer, | |
| 35 | // and then transfer to destination. | |
| 36 | self.end = try self.unbuffered_reader.read(&self.buf); | |
| 37 | const to_transfer = @min(self.end, dest.len); | |
| 38 | @memcpy(dest[0..to_transfer], self.buf[0..to_transfer]); | |
| 39 | self.start = to_transfer; | |
| 40 | return to_transfer; | |
| 41 | } | |
| 42 | ||
| 43 | pub fn reader(self: *Self) Reader { | |
| 44 | return .{ .context = self }; | |
| 45 | } | |
| 46 | }; | |
| 47 | } | |
| 48 | ||
| 49 | pub fn bufferedReader(reader: anytype) BufferedReader(4096, @TypeOf(reader)) { | |
| 50 | return .{ .unbuffered_reader = reader }; | |
| 51 | } | |
| 52 | ||
| 53 | pub fn bufferedReaderSize(comptime size: usize, reader: anytype) BufferedReader(size, @TypeOf(reader)) { | |
| 54 | return .{ .unbuffered_reader = reader }; | |
| 55 | } | |
| 56 | ||
| 57 | test "OneByte" { | |
| 58 | const OneByteReadReader = struct { | |
| 59 | str: []const u8, | |
| 60 | curr: usize, | |
| 61 | ||
| 62 | const Error = error{NoError}; | |
| 63 | const Self = @This(); | |
| 64 | const Reader = io.GenericReader(*Self, Error, read); | |
| 65 | ||
| 66 | fn init(str: []const u8) Self { | |
| 67 | return Self{ | |
| 68 | .str = str, | |
| 69 | .curr = 0, | |
| 70 | }; | |
| 71 | } | |
| 72 | ||
| 73 | fn read(self: *Self, dest: []u8) Error!usize { | |
| 74 | if (self.str.len <= self.curr or dest.len == 0) | |
| 75 | return 0; | |
| 76 | ||
| 77 | dest[0] = self.str[self.curr]; | |
| 78 | self.curr += 1; | |
| 79 | return 1; | |
| 80 | } | |
| 81 | ||
| 82 | fn reader(self: *Self) Reader { | |
| 83 | return .{ .context = self }; | |
| 84 | } | |
| 85 | }; | |
| 86 | ||
| 87 | const str = "This is a test"; | |
| 88 | var one_byte_stream = OneByteReadReader.init(str); | |
| 89 | var buf_reader = bufferedReader(one_byte_stream.reader()); | |
| 90 | const stream = buf_reader.reader(); | |
| 91 | ||
| 92 | const res = try stream.readAllAlloc(testing.allocator, str.len + 1); | |
| 93 | defer testing.allocator.free(res); | |
| 94 | try testing.expectEqualSlices(u8, str, res); | |
| 95 | } | |
| 96 | ||
| 97 | fn smallBufferedReader(underlying_stream: anytype) BufferedReader(8, @TypeOf(underlying_stream)) { | |
| 98 | return .{ .unbuffered_reader = underlying_stream }; | |
| 99 | } | |
| 100 | test "Block" { | |
| 101 | const BlockReader = struct { | |
| 102 | block: []const u8, | |
| 103 | reads_allowed: usize, | |
| 104 | curr_read: usize, | |
| 105 | ||
| 106 | const Error = error{NoError}; | |
| 107 | const Self = @This(); | |
| 108 | const Reader = io.GenericReader(*Self, Error, read); | |
| 109 | ||
| 110 | fn init(block: []const u8, reads_allowed: usize) Self { | |
| 111 | return Self{ | |
| 112 | .block = block, | |
| 113 | .reads_allowed = reads_allowed, | |
| 114 | .curr_read = 0, | |
| 115 | }; | |
| 116 | } | |
| 117 | ||
| 118 | fn read(self: *Self, dest: []u8) Error!usize { | |
| 119 | if (self.curr_read >= self.reads_allowed) return 0; | |
| 120 | @memcpy(dest[0..self.block.len], self.block); | |
| 121 | ||
| 122 | self.curr_read += 1; | |
| 123 | return self.block.len; | |
| 124 | } | |
| 125 | ||
| 126 | fn reader(self: *Self) Reader { | |
| 127 | return .{ .context = self }; | |
| 128 | } | |
| 129 | }; | |
| 130 | ||
| 131 | const block = "0123"; | |
| 132 | ||
| 133 | // len out == block | |
| 134 | { | |
| 135 | var test_buf_reader: BufferedReader(4, BlockReader) = .{ | |
| 136 | .unbuffered_reader = BlockReader.init(block, 2), | |
| 137 | }; | |
| 138 | const reader = test_buf_reader.reader(); | |
| 139 | var out_buf: [4]u8 = undefined; | |
| 140 | _ = try reader.readAll(&out_buf); | |
| 141 | try testing.expectEqualSlices(u8, &out_buf, block); | |
| 142 | _ = try reader.readAll(&out_buf); | |
| 143 | try testing.expectEqualSlices(u8, &out_buf, block); | |
| 144 | try testing.expectEqual(try reader.readAll(&out_buf), 0); | |
| 145 | } | |
| 146 | ||
| 147 | // len out < block | |
| 148 | { | |
| 149 | var test_buf_reader: BufferedReader(4, BlockReader) = .{ | |
| 150 | .unbuffered_reader = BlockReader.init(block, 2), | |
| 151 | }; | |
| 152 | const reader = test_buf_reader.reader(); | |
| 153 | var out_buf: [3]u8 = undefined; | |
| 154 | _ = try reader.readAll(&out_buf); | |
| 155 | try testing.expectEqualSlices(u8, &out_buf, "012"); | |
| 156 | _ = try reader.readAll(&out_buf); | |
| 157 | try testing.expectEqualSlices(u8, &out_buf, "301"); | |
| 158 | const n = try reader.readAll(&out_buf); | |
| 159 | try testing.expectEqualSlices(u8, out_buf[0..n], "23"); | |
| 160 | try testing.expectEqual(try reader.readAll(&out_buf), 0); | |
| 161 | } | |
| 162 | ||
| 163 | // len out > block | |
| 164 | { | |
| 165 | var test_buf_reader: BufferedReader(4, BlockReader) = .{ | |
| 166 | .unbuffered_reader = BlockReader.init(block, 2), | |
| 167 | }; | |
| 168 | const reader = test_buf_reader.reader(); | |
| 169 | var out_buf: [5]u8 = undefined; | |
| 170 | _ = try reader.readAll(&out_buf); | |
| 171 | try testing.expectEqualSlices(u8, &out_buf, "01230"); | |
| 172 | const n = try reader.readAll(&out_buf); | |
| 173 | try testing.expectEqualSlices(u8, out_buf[0..n], "123"); | |
| 174 | try testing.expectEqual(try reader.readAll(&out_buf), 0); | |
| 175 | } | |
| 176 | ||
| 177 | // len out == 0 | |
| 178 | { | |
| 179 | var test_buf_reader: BufferedReader(4, BlockReader) = .{ | |
| 180 | .unbuffered_reader = BlockReader.init(block, 2), | |
| 181 | }; | |
| 182 | const reader = test_buf_reader.reader(); | |
| 183 | var out_buf: [0]u8 = undefined; | |
| 184 | _ = try reader.readAll(&out_buf); | |
| 185 | try testing.expectEqualSlices(u8, &out_buf, ""); | |
| 186 | } | |
| 187 | ||
| 188 | // len bufreader buf > block | |
| 189 | { | |
| 190 | var test_buf_reader: BufferedReader(5, BlockReader) = .{ | |
| 191 | .unbuffered_reader = BlockReader.init(block, 2), | |
| 192 | }; | |
| 193 | const reader = test_buf_reader.reader(); | |
| 194 | var out_buf: [4]u8 = undefined; | |
| 195 | _ = try reader.readAll(&out_buf); | |
| 196 | try testing.expectEqualSlices(u8, &out_buf, block); | |
| 197 | _ = try reader.readAll(&out_buf); | |
| 198 | try testing.expectEqualSlices(u8, &out_buf, block); | |
| 199 | try testing.expectEqual(try reader.readAll(&out_buf), 0); | |
| 200 | } | |
| 201 | } |
lib/std/Io/limited_reader.zig deleted-45| ... | ... | @@ -1,45 +0,0 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const io = std.io; | |
| 3 | const assert = std.debug.assert; | |
| 4 | const testing = std.testing; | |
| 5 | ||
| 6 | pub fn LimitedReader(comptime ReaderType: type) type { | |
| 7 | return struct { | |
| 8 | inner_reader: ReaderType, | |
| 9 | bytes_left: u64, | |
| 10 | ||
| 11 | pub const Error = ReaderType.Error; | |
| 12 | pub const Reader = io.GenericReader(*Self, Error, read); | |
| 13 | ||
| 14 | const Self = @This(); | |
| 15 | ||
| 16 | pub fn read(self: *Self, dest: []u8) Error!usize { | |
| 17 | const max_read = @min(self.bytes_left, dest.len); | |
| 18 | const n = try self.inner_reader.read(dest[0..max_read]); | |
| 19 | self.bytes_left -= n; | |
| 20 | return n; | |
| 21 | } | |
| 22 | ||
| 23 | pub fn reader(self: *Self) Reader { | |
| 24 | return .{ .context = self }; | |
| 25 | } | |
| 26 | }; | |
| 27 | } | |
| 28 | ||
| 29 | /// Returns an initialised `LimitedReader`. | |
| 30 | /// `bytes_left` is a `u64` to be able to take 64 bit file offsets | |
| 31 | pub fn limitedReader(inner_reader: anytype, bytes_left: u64) LimitedReader(@TypeOf(inner_reader)) { | |
| 32 | return .{ .inner_reader = inner_reader, .bytes_left = bytes_left }; | |
| 33 | } | |
| 34 | ||
| 35 | test "basic usage" { | |
| 36 | const data = "hello world"; | |
| 37 | var fbs = std.io.fixedBufferStream(data); | |
| 38 | var early_stream = limitedReader(fbs.reader(), 3); | |
| 39 | ||
| 40 | var buf: [5]u8 = undefined; | |
| 41 | try testing.expectEqual(@as(usize, 3), try early_stream.reader().read(&buf)); | |
| 42 | try testing.expectEqualSlices(u8, data[0..3], buf[0..3]); | |
| 43 | try testing.expectEqual(@as(usize, 0), try early_stream.reader().read(&buf)); | |
| 44 | try testing.expectError(error.EndOfStream, early_stream.reader().skipBytes(10, .{})); | |
| 45 | } |
lib/std/Io/test.zig+3-3| ... | ... | @@ -45,9 +45,9 @@ test "write a file, read it, then delete it" { |
| 45 | 45 | const expected_file_size: u64 = "begin".len + data.len + "end".len; |
| 46 | 46 | try expectEqual(expected_file_size, file_size); |
| 47 | 47 | |
| 48 | var buf_stream = io.bufferedReader(file.deprecatedReader()); | |
| 49 | const st = buf_stream.reader(); | |
| 50 | const contents = try st.readAllAlloc(std.testing.allocator, 2 * 1024); | |
| 48 | var file_buffer: [1024]u8 = undefined; | |
| 49 | var file_reader = file.reader(&file_buffer); | |
| 50 | const contents = try file_reader.interface.allocRemaining(std.testing.allocator, .limited(2 * 1024)); | |
| 51 | 51 | defer std.testing.allocator.free(contents); |
| 52 | 52 | |
| 53 | 53 | try expect(mem.eql(u8, contents[0.."begin".len], "begin")); |
lib/std/Uri.zig+92-147| ... | ... | @@ -4,6 +4,8 @@ |
| 4 | 4 | const std = @import("std.zig"); |
| 5 | 5 | const testing = std.testing; |
| 6 | 6 | const Uri = @This(); |
| 7 | const Allocator = std.mem.Allocator; | |
| 8 | const Writer = std.Io.Writer; | |
| 7 | 9 | |
| 8 | 10 | scheme: []const u8, |
| 9 | 11 | user: ?Component = null, |
| ... | ... | @@ -14,6 +16,32 @@ path: Component = Component.empty, |
| 14 | 16 | query: ?Component = null, |
| 15 | 17 | fragment: ?Component = null, |
| 16 | 18 | |
| 19 | pub const host_name_max = 255; | |
| 20 | ||
| 21 | /// Returned value may point into `buffer` or be the original string. | |
| 22 | /// | |
| 23 | /// Suggested buffer length: `host_name_max`. | |
| 24 | /// | |
| 25 | /// See also: | |
| 26 | /// * `getHostAlloc` | |
| 27 | pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 { | |
| 28 | const component = uri.host orelse return error.UriMissingHost; | |
| 29 | return component.toRaw(buffer) catch |err| switch (err) { | |
| 30 | error.NoSpaceLeft => return error.UriHostTooLong, | |
| 31 | }; | |
| 32 | } | |
| 33 | ||
| 34 | /// Returned value may point into `buffer` or be the original string. | |
| 35 | /// | |
| 36 | /// See also: | |
| 37 | /// * `getHost` | |
| 38 | pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 { | |
| 39 | const component = uri.host orelse return error.UriMissingHost; | |
| 40 | const result = try component.toRawMaybeAlloc(arena); | |
| 41 | if (result.len > host_name_max) return error.UriHostTooLong; | |
| 42 | return result; | |
| 43 | } | |
| 44 | ||
| 17 | 45 | pub const Component = union(enum) { |
| 18 | 46 | /// Invalid characters in this component must be percent encoded |
| 19 | 47 | /// before being printed as part of a URI. |
| ... | ... | @@ -30,11 +58,19 @@ pub const Component = union(enum) { |
| 30 | 58 | }; |
| 31 | 59 | } |
| 32 | 60 | |
| 61 | /// Returned value may point into `buffer` or be the original string. | |
| 62 | pub fn toRaw(component: Component, buffer: []u8) error{NoSpaceLeft}![]const u8 { | |
| 63 | return switch (component) { | |
| 64 | .raw => |raw| raw, | |
| 65 | .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| | |
| 66 | try std.fmt.bufPrint(buffer, "{f}", .{std.fmt.alt(component, .formatRaw)}) | |
| 67 | else | |
| 68 | percent_encoded, | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 33 | 72 | /// Allocates the result with `arena` only if needed, so the result should not be freed. |
| 34 | pub fn toRawMaybeAlloc( | |
| 35 | component: Component, | |
| 36 | arena: std.mem.Allocator, | |
| 37 | ) std.mem.Allocator.Error![]const u8 { | |
| 73 | pub fn toRawMaybeAlloc(component: Component, arena: Allocator) Allocator.Error![]const u8 { | |
| 38 | 74 | return switch (component) { |
| 39 | 75 | .raw => |raw| raw, |
| 40 | 76 | .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_| |
| ... | ... | @@ -44,7 +80,7 @@ pub const Component = union(enum) { |
| 44 | 80 | }; |
| 45 | 81 | } |
| 46 | 82 | |
| 47 | pub fn formatRaw(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 83 | pub fn formatRaw(component: Component, w: *Writer) Writer.Error!void { | |
| 48 | 84 | switch (component) { |
| 49 | 85 | .raw => |raw| try w.writeAll(raw), |
| 50 | 86 | .percent_encoded => |percent_encoded| { |
| ... | ... | @@ -67,56 +103,56 @@ pub const Component = union(enum) { |
| 67 | 103 | } |
| 68 | 104 | } |
| 69 | 105 | |
| 70 | pub fn formatEscaped(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 106 | pub fn formatEscaped(component: Component, w: *Writer) Writer.Error!void { | |
| 71 | 107 | switch (component) { |
| 72 | 108 | .raw => |raw| try percentEncode(w, raw, isUnreserved), |
| 73 | 109 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 74 | 110 | } |
| 75 | 111 | } |
| 76 | 112 | |
| 77 | pub fn formatUser(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 113 | pub fn formatUser(component: Component, w: *Writer) Writer.Error!void { | |
| 78 | 114 | switch (component) { |
| 79 | 115 | .raw => |raw| try percentEncode(w, raw, isUserChar), |
| 80 | 116 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 81 | 117 | } |
| 82 | 118 | } |
| 83 | 119 | |
| 84 | pub fn formatPassword(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 120 | pub fn formatPassword(component: Component, w: *Writer) Writer.Error!void { | |
| 85 | 121 | switch (component) { |
| 86 | 122 | .raw => |raw| try percentEncode(w, raw, isPasswordChar), |
| 87 | 123 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 88 | 124 | } |
| 89 | 125 | } |
| 90 | 126 | |
| 91 | pub fn formatHost(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 127 | pub fn formatHost(component: Component, w: *Writer) Writer.Error!void { | |
| 92 | 128 | switch (component) { |
| 93 | 129 | .raw => |raw| try percentEncode(w, raw, isHostChar), |
| 94 | 130 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 95 | 131 | } |
| 96 | 132 | } |
| 97 | 133 | |
| 98 | pub fn formatPath(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 134 | pub fn formatPath(component: Component, w: *Writer) Writer.Error!void { | |
| 99 | 135 | switch (component) { |
| 100 | 136 | .raw => |raw| try percentEncode(w, raw, isPathChar), |
| 101 | 137 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 102 | 138 | } |
| 103 | 139 | } |
| 104 | 140 | |
| 105 | pub fn formatQuery(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 141 | pub fn formatQuery(component: Component, w: *Writer) Writer.Error!void { | |
| 106 | 142 | switch (component) { |
| 107 | 143 | .raw => |raw| try percentEncode(w, raw, isQueryChar), |
| 108 | 144 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 109 | 145 | } |
| 110 | 146 | } |
| 111 | 147 | |
| 112 | pub fn formatFragment(component: Component, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 148 | pub fn formatFragment(component: Component, w: *Writer) Writer.Error!void { | |
| 113 | 149 | switch (component) { |
| 114 | 150 | .raw => |raw| try percentEncode(w, raw, isFragmentChar), |
| 115 | 151 | .percent_encoded => |percent_encoded| try w.writeAll(percent_encoded), |
| 116 | 152 | } |
| 117 | 153 | } |
| 118 | 154 | |
| 119 | pub fn percentEncode(w: *std.io.Writer, raw: []const u8, comptime isValidChar: fn (u8) bool) std.io.Writer.Error!void { | |
| 155 | pub fn percentEncode(w: *Writer, raw: []const u8, comptime isValidChar: fn (u8) bool) Writer.Error!void { | |
| 120 | 156 | var start: usize = 0; |
| 121 | 157 | for (raw, 0..) |char, index| { |
| 122 | 158 | if (isValidChar(char)) continue; |
| ... | ... | @@ -165,17 +201,15 @@ pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; |
| 165 | 201 | /// The return value will contain strings pointing into the original `text`. |
| 166 | 202 | /// Each component that is provided, will be non-`null`. |
| 167 | 203 | pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { |
| 168 | var reader = SliceReader{ .slice = text }; | |
| 169 | ||
| 170 | 204 | var uri: Uri = .{ .scheme = scheme, .path = undefined }; |
| 205 | var i: usize = 0; | |
| 171 | 206 | |
| 172 | if (reader.peekPrefix("//")) a: { // authority part | |
| 173 | std.debug.assert(reader.get().? == '/'); | |
| 174 | std.debug.assert(reader.get().? == '/'); | |
| 175 | ||
| 176 | const authority = reader.readUntil(isAuthoritySeparator); | |
| 207 | if (std.mem.startsWith(u8, text, "//")) a: { | |
| 208 | i = std.mem.indexOfAnyPos(u8, text, 2, &authority_sep) orelse text.len; | |
| 209 | const authority = text[2..i]; | |
| 177 | 210 | if (authority.len == 0) { |
| 178 | if (reader.peekPrefix("/")) break :a else return error.InvalidFormat; | |
| 211 | if (!std.mem.startsWith(u8, text[2..], "/")) return error.InvalidFormat; | |
| 212 | break :a; | |
| 179 | 213 | } |
| 180 | 214 | |
| 181 | 215 | var start_of_host: usize = 0; |
| ... | ... | @@ -225,26 +259,28 @@ pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri { |
| 225 | 259 | uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] }; |
| 226 | 260 | } |
| 227 | 261 | |
| 228 | uri.path = .{ .percent_encoded = reader.readUntil(isPathSeparator) }; | |
| 262 | const path_start = i; | |
| 263 | i = std.mem.indexOfAnyPos(u8, text, path_start, &path_sep) orelse text.len; | |
| 264 | uri.path = .{ .percent_encoded = text[path_start..i] }; | |
| 229 | 265 | |
| 230 | if ((reader.peek() orelse 0) == '?') { // query part | |
| 231 | std.debug.assert(reader.get().? == '?'); | |
| 232 | uri.query = .{ .percent_encoded = reader.readUntil(isQuerySeparator) }; | |
| 266 | if (std.mem.startsWith(u8, text[i..], "?")) { | |
| 267 | const query_start = i + 1; | |
| 268 | i = std.mem.indexOfScalarPos(u8, text, query_start, '#') orelse text.len; | |
| 269 | uri.query = .{ .percent_encoded = text[query_start..i] }; | |
| 233 | 270 | } |
| 234 | 271 | |
| 235 | if ((reader.peek() orelse 0) == '#') { // fragment part | |
| 236 | std.debug.assert(reader.get().? == '#'); | |
| 237 | uri.fragment = .{ .percent_encoded = reader.readUntilEof() }; | |
| 272 | if (std.mem.startsWith(u8, text[i..], "#")) { | |
| 273 | uri.fragment = .{ .percent_encoded = text[i + 1 ..] }; | |
| 238 | 274 | } |
| 239 | 275 | |
| 240 | 276 | return uri; |
| 241 | 277 | } |
| 242 | 278 | |
| 243 | pub fn format(uri: *const Uri, writer: *std.io.Writer) std.io.Writer.Error!void { | |
| 279 | pub fn format(uri: *const Uri, writer: *Writer) Writer.Error!void { | |
| 244 | 280 | return writeToStream(uri, writer, .all); |
| 245 | 281 | } |
| 246 | 282 | |
| 247 | pub fn writeToStream(uri: *const Uri, writer: *std.io.Writer, flags: Format.Flags) std.io.Writer.Error!void { | |
| 283 | pub fn writeToStream(uri: *const Uri, writer: *Writer, flags: Format.Flags) Writer.Error!void { | |
| 248 | 284 | if (flags.scheme) { |
| 249 | 285 | try writer.print("{s}:", .{uri.scheme}); |
| 250 | 286 | if (flags.authority and uri.host != null) { |
| ... | ... | @@ -318,7 +354,7 @@ pub const Format = struct { |
| 318 | 354 | }; |
| 319 | 355 | }; |
| 320 | 356 | |
| 321 | pub fn default(f: Format, writer: *std.io.Writer) std.io.Writer.Error!void { | |
| 357 | pub fn default(f: Format, writer: *Writer) Writer.Error!void { | |
| 322 | 358 | return writeToStream(f.uri, writer, f.flags); |
| 323 | 359 | } |
| 324 | 360 | }; |
| ... | ... | @@ -327,41 +363,34 @@ pub fn fmt(uri: *const Uri, flags: Format.Flags) std.fmt.Formatter(Format, Forma |
| 327 | 363 | return .{ .data = .{ .uri = uri, .flags = flags } }; |
| 328 | 364 | } |
| 329 | 365 | |
| 330 | /// Parses the URI or returns an error. | |
| 331 | /// The return value will contain strings pointing into the | |
| 332 | /// original `text`. Each component that is provided, will be non-`null`. | |
| 366 | /// The return value will contain strings pointing into the original `text`. | |
| 367 | /// Each component that is provided will be non-`null`. | |
| 333 | 368 | pub fn parse(text: []const u8) ParseError!Uri { |
| 334 | var reader: SliceReader = .{ .slice = text }; | |
| 335 | const scheme = reader.readWhile(isSchemeChar); | |
| 336 | ||
| 337 | // after the scheme, a ':' must appear | |
| 338 | if (reader.get()) |c| { | |
| 339 | if (c != ':') | |
| 340 | return error.UnexpectedCharacter; | |
| 341 | } else { | |
| 342 | return error.InvalidFormat; | |
| 343 | } | |
| 344 | ||
| 345 | return parseAfterScheme(scheme, reader.readUntilEof()); | |
| 369 | const end = for (text, 0..) |byte, i| { | |
| 370 | if (!isSchemeChar(byte)) break i; | |
| 371 | } else text.len; | |
| 372 | // After the scheme, a ':' must appear. | |
| 373 | if (end >= text.len) return error.InvalidFormat; | |
| 374 | if (text[end] != ':') return error.UnexpectedCharacter; | |
| 375 | return parseAfterScheme(text[0..end], text[end + 1 ..]); | |
| 346 | 376 | } |
| 347 | 377 | |
| 348 | 378 | pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft}; |
| 349 | 379 | |
| 350 | /// Resolves a URI against a base URI, conforming to RFC 3986, Section 5. | |
| 351 | /// Copies `new` to the beginning of `aux_buf.*`, allowing the slices to overlap, | |
| 352 | /// then parses `new` as a URI, and then resolves the path in place. | |
| 380 | /// Resolves a URI against a base URI, conforming to | |
| 381 | /// [RFC 3986, Section 5](https://www.rfc-editor.org/rfc/rfc3986#section-5) | |
| 382 | /// | |
| 383 | /// Assumes new location is already copied to the beginning of `aux_buf.*`. | |
| 384 | /// Parses that new location as a URI, and then resolves the path in place. | |
| 385 | /// | |
| 353 | 386 | /// If a merge needs to take place, the newly constructed path will be stored |
| 354 | /// in `aux_buf.*` just after the copied `new`, and `aux_buf.*` will be modified | |
| 355 | /// to only contain the remaining unused space. | |
| 356 | pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri { | |
| 357 | std.mem.copyForwards(u8, aux_buf.*, new); | |
| 358 | // At this point, new is an invalid pointer. | |
| 359 | const new_mut = aux_buf.*[0..new.len]; | |
| 360 | aux_buf.* = aux_buf.*[new.len..]; | |
| 361 | ||
| 362 | const new_parsed = parse(new_mut) catch |err| | |
| 363 | (parseAfterScheme("", new_mut) catch return err); | |
| 364 | // As you can see above, `new_mut` is not a const pointer. | |
| 387 | /// in `aux_buf.*` just after the copied location, and `aux_buf.*` will be | |
| 388 | /// modified to only contain the remaining unused space. | |
| 389 | pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceError!Uri { | |
| 390 | const new = aux_buf.*[0..new_len]; | |
| 391 | const new_parsed = parse(new) catch |err| (parseAfterScheme("", new) catch return err); | |
| 392 | aux_buf.* = aux_buf.*[new_len..]; | |
| 393 | // As you can see above, `new` is not a const pointer. | |
| 365 | 394 | const new_path: []u8 = @constCast(new_parsed.path.percent_encoded); |
| 366 | 395 | |
| 367 | 396 | if (new_parsed.scheme.len > 0) return .{ |
| ... | ... | @@ -461,7 +490,7 @@ test remove_dot_segments { |
| 461 | 490 | |
| 462 | 491 | /// 5.2.3. Merge Paths |
| 463 | 492 | fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Component { |
| 464 | var aux: std.io.Writer = .fixed(aux_buf.*); | |
| 493 | var aux: Writer = .fixed(aux_buf.*); | |
| 465 | 494 | if (!base.isEmpty()) { |
| 466 | 495 | base.formatPath(&aux) catch return error.NoSpaceLeft; |
| 467 | 496 | aux.end = std.mem.lastIndexOfScalar(u8, aux.buffered(), '/') orelse return remove_dot_segments(new); |
| ... | ... | @@ -472,59 +501,6 @@ fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Co |
| 472 | 501 | return merged_path; |
| 473 | 502 | } |
| 474 | 503 | |
| 475 | const SliceReader = struct { | |
| 476 | const Self = @This(); | |
| 477 | ||
| 478 | slice: []const u8, | |
| 479 | offset: usize = 0, | |
| 480 | ||
| 481 | fn get(self: *Self) ?u8 { | |
| 482 | if (self.offset >= self.slice.len) | |
| 483 | return null; | |
| 484 | const c = self.slice[self.offset]; | |
| 485 | self.offset += 1; | |
| 486 | return c; | |
| 487 | } | |
| 488 | ||
| 489 | fn peek(self: Self) ?u8 { | |
| 490 | if (self.offset >= self.slice.len) | |
| 491 | return null; | |
| 492 | return self.slice[self.offset]; | |
| 493 | } | |
| 494 | ||
| 495 | fn readWhile(self: *Self, comptime predicate: fn (u8) bool) []const u8 { | |
| 496 | const start = self.offset; | |
| 497 | var end = start; | |
| 498 | while (end < self.slice.len and predicate(self.slice[end])) { | |
| 499 | end += 1; | |
| 500 | } | |
| 501 | self.offset = end; | |
| 502 | return self.slice[start..end]; | |
| 503 | } | |
| 504 | ||
| 505 | fn readUntil(self: *Self, comptime predicate: fn (u8) bool) []const u8 { | |
| 506 | const start = self.offset; | |
| 507 | var end = start; | |
| 508 | while (end < self.slice.len and !predicate(self.slice[end])) { | |
| 509 | end += 1; | |
| 510 | } | |
| 511 | self.offset = end; | |
| 512 | return self.slice[start..end]; | |
| 513 | } | |
| 514 | ||
| 515 | fn readUntilEof(self: *Self) []const u8 { | |
| 516 | const start = self.offset; | |
| 517 | self.offset = self.slice.len; | |
| 518 | return self.slice[start..]; | |
| 519 | } | |
| 520 | ||
| 521 | fn peekPrefix(self: Self, prefix: []const u8) bool { | |
| 522 | if (self.offset + prefix.len > self.slice.len) | |
| 523 | return false; | |
| 524 | return std.mem.eql(u8, self.slice[self.offset..][0..prefix.len], prefix); | |
| 525 | } | |
| 526 | }; | |
| 527 | ||
| 528 | 504 | /// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) |
| 529 | 505 | fn isSchemeChar(c: u8) bool { |
| 530 | 506 | return switch (c) { |
| ... | ... | @@ -533,19 +509,6 @@ fn isSchemeChar(c: u8) bool { |
| 533 | 509 | }; |
| 534 | 510 | } |
| 535 | 511 | |
| 536 | /// reserved = gen-delims / sub-delims | |
| 537 | fn isReserved(c: u8) bool { | |
| 538 | return isGenLimit(c) or isSubLimit(c); | |
| 539 | } | |
| 540 | ||
| 541 | /// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" | |
| 542 | fn isGenLimit(c: u8) bool { | |
| 543 | return switch (c) { | |
| 544 | ':', ',', '?', '#', '[', ']', '@' => true, | |
| 545 | else => false, | |
| 546 | }; | |
| 547 | } | |
| 548 | ||
| 549 | 512 | /// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" |
| 550 | 513 | /// / "*" / "+" / "," / ";" / "=" |
| 551 | 514 | fn isSubLimit(c: u8) bool { |
| ... | ... | @@ -585,26 +548,8 @@ fn isQueryChar(c: u8) bool { |
| 585 | 548 | |
| 586 | 549 | const isFragmentChar = isQueryChar; |
| 587 | 550 | |
| 588 | fn isAuthoritySeparator(c: u8) bool { | |
| 589 | return switch (c) { | |
| 590 | '/', '?', '#' => true, | |
| 591 | else => false, | |
| 592 | }; | |
| 593 | } | |
| 594 | ||
| 595 | fn isPathSeparator(c: u8) bool { | |
| 596 | return switch (c) { | |
| 597 | '?', '#' => true, | |
| 598 | else => false, | |
| 599 | }; | |
| 600 | } | |
| 601 | ||
| 602 | fn isQuerySeparator(c: u8) bool { | |
| 603 | return switch (c) { | |
| 604 | '#' => true, | |
| 605 | else => false, | |
| 606 | }; | |
| 607 | } | |
| 551 | const authority_sep: [3]u8 = .{ '/', '?', '#' }; | |
| 552 | const path_sep: [2]u8 = .{ '?', '#' }; | |
| 608 | 553 | |
| 609 | 554 | test "basic" { |
| 610 | 555 | const parsed = try parse("https://ziglang.org/download"); |
lib/std/crypto/tls.zig+106-99| ... | ... | @@ -49,8 +49,8 @@ pub const hello_retry_request_sequence = [32]u8{ |
| 49 | 49 | }; |
| 50 | 50 | |
| 51 | 51 | pub const close_notify_alert = [_]u8{ |
| 52 | @intFromEnum(AlertLevel.warning), | |
| 53 | @intFromEnum(AlertDescription.close_notify), | |
| 52 | @intFromEnum(Alert.Level.warning), | |
| 53 | @intFromEnum(Alert.Description.close_notify), | |
| 54 | 54 | }; |
| 55 | 55 | |
| 56 | 56 | pub const ProtocolVersion = enum(u16) { |
| ... | ... | @@ -138,103 +138,108 @@ pub const ExtensionType = enum(u16) { |
| 138 | 138 | _, |
| 139 | 139 | }; |
| 140 | 140 | |
| 141 | pub const AlertLevel = enum(u8) { | |
| 142 | warning = 1, | |
| 143 | fatal = 2, | |
| 144 | _, | |
| 145 | }; | |
| 141 | pub const Alert = struct { | |
| 142 | level: Level, | |
| 143 | description: Description, | |
| 146 | 144 | |
| 147 | pub const AlertDescription = enum(u8) { | |
| 148 | pub const Error = error{ | |
| 149 | TlsAlertUnexpectedMessage, | |
| 150 | TlsAlertBadRecordMac, | |
| 151 | TlsAlertRecordOverflow, | |
| 152 | TlsAlertHandshakeFailure, | |
| 153 | TlsAlertBadCertificate, | |
| 154 | TlsAlertUnsupportedCertificate, | |
| 155 | TlsAlertCertificateRevoked, | |
| 156 | TlsAlertCertificateExpired, | |
| 157 | TlsAlertCertificateUnknown, | |
| 158 | TlsAlertIllegalParameter, | |
| 159 | TlsAlertUnknownCa, | |
| 160 | TlsAlertAccessDenied, | |
| 161 | TlsAlertDecodeError, | |
| 162 | TlsAlertDecryptError, | |
| 163 | TlsAlertProtocolVersion, | |
| 164 | TlsAlertInsufficientSecurity, | |
| 165 | TlsAlertInternalError, | |
| 166 | TlsAlertInappropriateFallback, | |
| 167 | TlsAlertMissingExtension, | |
| 168 | TlsAlertUnsupportedExtension, | |
| 169 | TlsAlertUnrecognizedName, | |
| 170 | TlsAlertBadCertificateStatusResponse, | |
| 171 | TlsAlertUnknownPskIdentity, | |
| 172 | TlsAlertCertificateRequired, | |
| 173 | TlsAlertNoApplicationProtocol, | |
| 174 | TlsAlertUnknown, | |
| 145 | pub const Level = enum(u8) { | |
| 146 | warning = 1, | |
| 147 | fatal = 2, | |
| 148 | _, | |
| 175 | 149 | }; |
| 176 | 150 | |
| 177 | close_notify = 0, | |
| 178 | unexpected_message = 10, | |
| 179 | bad_record_mac = 20, | |
| 180 | record_overflow = 22, | |
| 181 | handshake_failure = 40, | |
| 182 | bad_certificate = 42, | |
| 183 | unsupported_certificate = 43, | |
| 184 | certificate_revoked = 44, | |
| 185 | certificate_expired = 45, | |
| 186 | certificate_unknown = 46, | |
| 187 | illegal_parameter = 47, | |
| 188 | unknown_ca = 48, | |
| 189 | access_denied = 49, | |
| 190 | decode_error = 50, | |
| 191 | decrypt_error = 51, | |
| 192 | protocol_version = 70, | |
| 193 | insufficient_security = 71, | |
| 194 | internal_error = 80, | |
| 195 | inappropriate_fallback = 86, | |
| 196 | user_canceled = 90, | |
| 197 | missing_extension = 109, | |
| 198 | unsupported_extension = 110, | |
| 199 | unrecognized_name = 112, | |
| 200 | bad_certificate_status_response = 113, | |
| 201 | unknown_psk_identity = 115, | |
| 202 | certificate_required = 116, | |
| 203 | no_application_protocol = 120, | |
| 204 | _, | |
| 151 | pub const Description = enum(u8) { | |
| 152 | pub const Error = error{ | |
| 153 | TlsAlertUnexpectedMessage, | |
| 154 | TlsAlertBadRecordMac, | |
| 155 | TlsAlertRecordOverflow, | |
| 156 | TlsAlertHandshakeFailure, | |
| 157 | TlsAlertBadCertificate, | |
| 158 | TlsAlertUnsupportedCertificate, | |
| 159 | TlsAlertCertificateRevoked, | |
| 160 | TlsAlertCertificateExpired, | |
| 161 | TlsAlertCertificateUnknown, | |
| 162 | TlsAlertIllegalParameter, | |
| 163 | TlsAlertUnknownCa, | |
| 164 | TlsAlertAccessDenied, | |
| 165 | TlsAlertDecodeError, | |
| 166 | TlsAlertDecryptError, | |
| 167 | TlsAlertProtocolVersion, | |
| 168 | TlsAlertInsufficientSecurity, | |
| 169 | TlsAlertInternalError, | |
| 170 | TlsAlertInappropriateFallback, | |
| 171 | TlsAlertMissingExtension, | |
| 172 | TlsAlertUnsupportedExtension, | |
| 173 | TlsAlertUnrecognizedName, | |
| 174 | TlsAlertBadCertificateStatusResponse, | |
| 175 | TlsAlertUnknownPskIdentity, | |
| 176 | TlsAlertCertificateRequired, | |
| 177 | TlsAlertNoApplicationProtocol, | |
| 178 | TlsAlertUnknown, | |
| 179 | }; | |
| 205 | 180 | |
| 206 | pub fn toError(alert: AlertDescription) Error!void { | |
| 207 | switch (alert) { | |
| 208 | .close_notify => {}, // not an error | |
| 209 | .unexpected_message => return error.TlsAlertUnexpectedMessage, | |
| 210 | .bad_record_mac => return error.TlsAlertBadRecordMac, | |
| 211 | .record_overflow => return error.TlsAlertRecordOverflow, | |
| 212 | .handshake_failure => return error.TlsAlertHandshakeFailure, | |
| 213 | .bad_certificate => return error.TlsAlertBadCertificate, | |
| 214 | .unsupported_certificate => return error.TlsAlertUnsupportedCertificate, | |
| 215 | .certificate_revoked => return error.TlsAlertCertificateRevoked, | |
| 216 | .certificate_expired => return error.TlsAlertCertificateExpired, | |
| 217 | .certificate_unknown => return error.TlsAlertCertificateUnknown, | |
| 218 | .illegal_parameter => return error.TlsAlertIllegalParameter, | |
| 219 | .unknown_ca => return error.TlsAlertUnknownCa, | |
| 220 | .access_denied => return error.TlsAlertAccessDenied, | |
| 221 | .decode_error => return error.TlsAlertDecodeError, | |
| 222 | .decrypt_error => return error.TlsAlertDecryptError, | |
| 223 | .protocol_version => return error.TlsAlertProtocolVersion, | |
| 224 | .insufficient_security => return error.TlsAlertInsufficientSecurity, | |
| 225 | .internal_error => return error.TlsAlertInternalError, | |
| 226 | .inappropriate_fallback => return error.TlsAlertInappropriateFallback, | |
| 227 | .user_canceled => {}, // not an error | |
| 228 | .missing_extension => return error.TlsAlertMissingExtension, | |
| 229 | .unsupported_extension => return error.TlsAlertUnsupportedExtension, | |
| 230 | .unrecognized_name => return error.TlsAlertUnrecognizedName, | |
| 231 | .bad_certificate_status_response => return error.TlsAlertBadCertificateStatusResponse, | |
| 232 | .unknown_psk_identity => return error.TlsAlertUnknownPskIdentity, | |
| 233 | .certificate_required => return error.TlsAlertCertificateRequired, | |
| 234 | .no_application_protocol => return error.TlsAlertNoApplicationProtocol, | |
| 235 | _ => return error.TlsAlertUnknown, | |
| 181 | close_notify = 0, | |
| 182 | unexpected_message = 10, | |
| 183 | bad_record_mac = 20, | |
| 184 | record_overflow = 22, | |
| 185 | handshake_failure = 40, | |
| 186 | bad_certificate = 42, | |
| 187 | unsupported_certificate = 43, | |
| 188 | certificate_revoked = 44, | |
| 189 | certificate_expired = 45, | |
| 190 | certificate_unknown = 46, | |
| 191 | illegal_parameter = 47, | |
| 192 | unknown_ca = 48, | |
| 193 | access_denied = 49, | |
| 194 | decode_error = 50, | |
| 195 | decrypt_error = 51, | |
| 196 | protocol_version = 70, | |
| 197 | insufficient_security = 71, | |
| 198 | internal_error = 80, | |
| 199 | inappropriate_fallback = 86, | |
| 200 | user_canceled = 90, | |
| 201 | missing_extension = 109, | |
| 202 | unsupported_extension = 110, | |
| 203 | unrecognized_name = 112, | |
| 204 | bad_certificate_status_response = 113, | |
| 205 | unknown_psk_identity = 115, | |
| 206 | certificate_required = 116, | |
| 207 | no_application_protocol = 120, | |
| 208 | _, | |
| 209 | ||
| 210 | pub fn toError(description: Description) Error!void { | |
| 211 | switch (description) { | |
| 212 | .close_notify => {}, // not an error | |
| 213 | .unexpected_message => return error.TlsAlertUnexpectedMessage, | |
| 214 | .bad_record_mac => return error.TlsAlertBadRecordMac, | |
| 215 | .record_overflow => return error.TlsAlertRecordOverflow, | |
| 216 | .handshake_failure => return error.TlsAlertHandshakeFailure, | |
| 217 | .bad_certificate => return error.TlsAlertBadCertificate, | |
| 218 | .unsupported_certificate => return error.TlsAlertUnsupportedCertificate, | |
| 219 | .certificate_revoked => return error.TlsAlertCertificateRevoked, | |
| 220 | .certificate_expired => return error.TlsAlertCertificateExpired, | |
| 221 | .certificate_unknown => return error.TlsAlertCertificateUnknown, | |
| 222 | .illegal_parameter => return error.TlsAlertIllegalParameter, | |
| 223 | .unknown_ca => return error.TlsAlertUnknownCa, | |
| 224 | .access_denied => return error.TlsAlertAccessDenied, | |
| 225 | .decode_error => return error.TlsAlertDecodeError, | |
| 226 | .decrypt_error => return error.TlsAlertDecryptError, | |
| 227 | .protocol_version => return error.TlsAlertProtocolVersion, | |
| 228 | .insufficient_security => return error.TlsAlertInsufficientSecurity, | |
| 229 | .internal_error => return error.TlsAlertInternalError, | |
| 230 | .inappropriate_fallback => return error.TlsAlertInappropriateFallback, | |
| 231 | .user_canceled => {}, // not an error | |
| 232 | .missing_extension => return error.TlsAlertMissingExtension, | |
| 233 | .unsupported_extension => return error.TlsAlertUnsupportedExtension, | |
| 234 | .unrecognized_name => return error.TlsAlertUnrecognizedName, | |
| 235 | .bad_certificate_status_response => return error.TlsAlertBadCertificateStatusResponse, | |
| 236 | .unknown_psk_identity => return error.TlsAlertUnknownPskIdentity, | |
| 237 | .certificate_required => return error.TlsAlertCertificateRequired, | |
| 238 | .no_application_protocol => return error.TlsAlertNoApplicationProtocol, | |
| 239 | _ => return error.TlsAlertUnknown, | |
| 240 | } | |
| 236 | 241 | } |
| 237 | } | |
| 242 | }; | |
| 238 | 243 | }; |
| 239 | 244 | |
| 240 | 245 | pub const SignatureScheme = enum(u16) { |
| ... | ... | @@ -650,7 +655,7 @@ pub const Decoder = struct { |
| 650 | 655 | } |
| 651 | 656 | |
| 652 | 657 | /// Use this function to increase `their_end`. |
| 653 | pub fn readAtLeast(d: *Decoder, stream: anytype, their_amt: usize) !void { | |
| 658 | pub fn readAtLeast(d: *Decoder, stream: *std.io.Reader, their_amt: usize) !void { | |
| 654 | 659 | assert(!d.disable_reads); |
| 655 | 660 | const existing_amt = d.cap - d.idx; |
| 656 | 661 | d.their_end = d.idx + their_amt; |
| ... | ... | @@ -658,14 +663,16 @@ pub const Decoder = struct { |
| 658 | 663 | const request_amt = their_amt - existing_amt; |
| 659 | 664 | const dest = d.buf[d.cap..]; |
| 660 | 665 | if (request_amt > dest.len) return error.TlsRecordOverflow; |
| 661 | const actual_amt = try stream.readAtLeast(dest, request_amt); | |
| 662 | if (actual_amt < request_amt) return error.TlsConnectionTruncated; | |
| 663 | d.cap += actual_amt; | |
| 666 | stream.readSlice(dest[0..request_amt]) catch |err| switch (err) { | |
| 667 | error.EndOfStream => return error.TlsConnectionTruncated, | |
| 668 | error.ReadFailed => return error.ReadFailed, | |
| 669 | }; | |
| 670 | d.cap += request_amt; | |
| 664 | 671 | } |
| 665 | 672 | |
| 666 | 673 | /// Same as `readAtLeast` but also increases `our_end` by exactly `our_amt`. |
| 667 | 674 | /// Use when `our_amt` is calculated by us, not by them. |
| 668 | pub fn readAtLeastOurAmt(d: *Decoder, stream: anytype, our_amt: usize) !void { | |
| 675 | pub fn readAtLeastOurAmt(d: *Decoder, stream: *std.io.Reader, our_amt: usize) !void { | |
| 669 | 676 | assert(!d.disable_reads); |
| 670 | 677 | try readAtLeast(d, stream, our_amt); |
| 671 | 678 | d.our_end = d.idx + our_amt; |
lib/std/crypto/tls/Client.zig+417-805| ... | ... | @@ -1,11 +1,15 @@ |
| 1 | const builtin = @import("builtin"); | |
| 2 | const native_endian = builtin.cpu.arch.endian(); | |
| 3 | ||
| 1 | 4 | const std = @import("../../std.zig"); |
| 2 | 5 | const tls = std.crypto.tls; |
| 3 | 6 | const Client = @This(); |
| 4 | const net = std.net; | |
| 5 | 7 | const mem = std.mem; |
| 6 | 8 | const crypto = std.crypto; |
| 7 | 9 | const assert = std.debug.assert; |
| 8 | 10 | const Certificate = std.crypto.Certificate; |
| 11 | const Reader = std.Io.Reader; | |
| 12 | const Writer = std.Io.Writer; | |
| 9 | 13 | |
| 10 | 14 | const max_ciphertext_len = tls.max_ciphertext_len; |
| 11 | 15 | const hmacExpandLabel = tls.hmacExpandLabel; |
| ... | ... | @@ -13,44 +17,60 @@ const hkdfExpandLabel = tls.hkdfExpandLabel; |
| 13 | 17 | const int = tls.int; |
| 14 | 18 | const array = tls.array; |
| 15 | 19 | |
| 20 | /// The encrypted stream from the server to the client. Bytes are pulled from | |
| 21 | /// here via `reader`. | |
| 22 | /// | |
| 23 | /// The buffer is asserted to have capacity at least `min_buffer_len`. | |
| 24 | input: *Reader, | |
| 25 | /// Decrypted stream from the server to the client. | |
| 26 | reader: Reader, | |
| 27 | ||
| 28 | /// The encrypted stream from the client to the server. Bytes are pushed here | |
| 29 | /// via `writer`. | |
| 30 | /// | |
| 31 | /// The buffer is asserted to have capacity at least `min_buffer_len`. | |
| 32 | output: *Writer, | |
| 33 | /// The plaintext stream from the client to the server. | |
| 34 | writer: Writer, | |
| 35 | ||
| 36 | /// Populated when `error.TlsAlert` is returned. | |
| 37 | alert: ?tls.Alert = null, | |
| 38 | read_err: ?ReadError = null, | |
| 16 | 39 | tls_version: tls.ProtocolVersion, |
| 17 | 40 | read_seq: u64, |
| 18 | 41 | write_seq: u64, |
| 19 | /// The starting index of cleartext bytes inside `partially_read_buffer`. | |
| 20 | partial_cleartext_idx: u15, | |
| 21 | /// The ending index of cleartext bytes inside `partially_read_buffer` as well | |
| 22 | /// as the starting index of ciphertext bytes. | |
| 23 | partial_ciphertext_idx: u15, | |
| 24 | /// The ending index of ciphertext bytes inside `partially_read_buffer`. | |
| 25 | partial_ciphertext_end: u15, | |
| 26 | 42 | /// When this is true, the stream may still not be at the end because there |
| 27 | /// may be data in `partially_read_buffer`. | |
| 43 | /// may be data in the input buffer. | |
| 28 | 44 | received_close_notify: bool, |
| 29 | /// By default, reaching the end-of-stream when reading from the server will | |
| 30 | /// cause `error.TlsConnectionTruncated` to be returned, unless a close_notify | |
| 31 | /// message has been received. By setting this flag to `true`, instead, the | |
| 32 | /// end-of-stream will be forwarded to the application layer above TLS. | |
| 33 | /// This makes the application vulnerable to truncation attacks unless the | |
| 34 | /// application layer itself verifies that the amount of data received equals | |
| 35 | /// the amount of data expected, such as HTTP with the Content-Length header. | |
| 36 | 45 | allow_truncation_attacks: bool, |
| 37 | 46 | application_cipher: tls.ApplicationCipher, |
| 38 | /// The size is enough to contain exactly one TLSCiphertext record. | |
| 39 | /// This buffer is segmented into four parts: | |
| 40 | /// 0. unused | |
| 41 | /// 1. cleartext | |
| 42 | /// 2. ciphertext | |
| 43 | /// 3. unused | |
| 44 | /// The fields `partial_cleartext_idx`, `partial_ciphertext_idx`, and | |
| 45 | /// `partial_ciphertext_end` describe the span of the segments. | |
| 46 | partially_read_buffer: [tls.max_ciphertext_record_len]u8, | |
| 47 | /// If non-null, ssl secrets are logged to a file. Creating such a log file allows other | |
| 48 | /// programs with access to that file to decrypt all traffic over this connection. | |
| 49 | ssl_key_log: ?struct { | |
| 47 | ||
| 48 | /// If non-null, ssl secrets are logged to a stream. Creating such a log file | |
| 49 | /// allows other programs with access to that file to decrypt all traffic over | |
| 50 | /// this connection. | |
| 51 | ssl_key_log: ?*SslKeyLog, | |
| 52 | ||
| 53 | pub const ReadError = error{ | |
| 54 | /// The alert description will be stored in `alert`. | |
| 55 | TlsAlert, | |
| 56 | TlsBadLength, | |
| 57 | TlsBadRecordMac, | |
| 58 | TlsConnectionTruncated, | |
| 59 | TlsDecodeError, | |
| 60 | TlsRecordOverflow, | |
| 61 | TlsUnexpectedMessage, | |
| 62 | TlsIllegalParameter, | |
| 63 | TlsSequenceOverflow, | |
| 64 | /// The buffer provided to the read function was not at least | |
| 65 | /// `min_buffer_len`. | |
| 66 | OutputBufferUndersize, | |
| 67 | }; | |
| 68 | ||
| 69 | pub const SslKeyLog = struct { | |
| 50 | 70 | client_key_seq: u64, |
| 51 | 71 | server_key_seq: u64, |
| 52 | 72 | client_random: [32]u8, |
| 53 | file: std.fs.File, | |
| 73 | writer: *Writer, | |
| 54 | 74 | |
| 55 | 75 | fn clientCounter(key_log: *@This()) u64 { |
| 56 | 76 | defer key_log.client_key_seq += 1; |
| ... | ... | @@ -61,51 +81,12 @@ ssl_key_log: ?struct { |
| 61 | 81 | defer key_log.server_key_seq += 1; |
| 62 | 82 | return key_log.server_key_seq; |
| 63 | 83 | } |
| 64 | }, | |
| 65 | ||
| 66 | /// This is an example of the type that is needed by the read and write | |
| 67 | /// functions. It can have any fields but it must at least have these | |
| 68 | /// functions. | |
| 69 | /// | |
| 70 | /// Note that `std.net.Stream` conforms to this interface. | |
| 71 | /// | |
| 72 | /// This declaration serves as documentation only. | |
| 73 | pub const StreamInterface = struct { | |
| 74 | /// Can be any error set. | |
| 75 | pub const ReadError = error{}; | |
| 76 | ||
| 77 | /// Returns the number of bytes read. The number read may be less than the | |
| 78 | /// buffer space provided. End-of-stream is indicated by a return value of 0. | |
| 79 | /// | |
| 80 | /// The `iovecs` parameter is mutable because so that function may to | |
| 81 | /// mutate the fields in order to handle partial reads from the underlying | |
| 82 | /// stream layer. | |
| 83 | pub fn readv(this: @This(), iovecs: []std.posix.iovec) ReadError!usize { | |
| 84 | _ = .{ this, iovecs }; | |
| 85 | @panic("unimplemented"); | |
| 86 | } | |
| 87 | ||
| 88 | /// Can be any error set. | |
| 89 | pub const WriteError = error{}; | |
| 90 | ||
| 91 | /// Returns the number of bytes read, which may be less than the buffer | |
| 92 | /// space provided. A short read does not indicate end-of-stream. | |
| 93 | pub fn writev(this: @This(), iovecs: []const std.posix.iovec_const) WriteError!usize { | |
| 94 | _ = .{ this, iovecs }; | |
| 95 | @panic("unimplemented"); | |
| 96 | } | |
| 97 | ||
| 98 | /// Returns the number of bytes read, which may be less than the buffer | |
| 99 | /// space provided, indicating end-of-stream. | |
| 100 | /// The `iovecs` parameter is mutable in case this function needs to mutate | |
| 101 | /// the fields in order to handle partial writes from the underlying layer. | |
| 102 | pub fn writevAll(this: @This(), iovecs: []std.posix.iovec_const) WriteError!usize { | |
| 103 | // This can be implemented in terms of writev, or specialized if desired. | |
| 104 | _ = .{ this, iovecs }; | |
| 105 | @panic("unimplemented"); | |
| 106 | } | |
| 107 | 84 | }; |
| 108 | 85 | |
| 86 | /// The `Reader` supplied to `init` requires a buffer capacity | |
| 87 | /// at least this amount. | |
| 88 | pub const min_buffer_len = tls.max_ciphertext_record_len; | |
| 89 | ||
| 109 | 90 | pub const Options = struct { |
| 110 | 91 | /// How to perform host verification of server certificates. |
| 111 | 92 | host: union(enum) { |
| ... | ... | @@ -127,64 +108,85 @@ pub const Options = struct { |
| 127 | 108 | /// Verify that the server certificate is authorized by a given ca bundle. |
| 128 | 109 | bundle: Certificate.Bundle, |
| 129 | 110 | }, |
| 130 | /// If non-null, ssl secrets are logged to this file. Creating such a log file allows | |
| 111 | /// If non-null, ssl secrets are logged to this stream. Creating such a log file allows | |
| 131 | 112 | /// other programs with access to that file to decrypt all traffic over this connection. |
| 132 | ssl_key_log_file: ?std.fs.File = null, | |
| 113 | /// | |
| 114 | /// Only the `writer` field is observed during the handshake (`init`). | |
| 115 | /// After that, the other fields are populated. | |
| 116 | ssl_key_log: ?*SslKeyLog = null, | |
| 117 | /// By default, reaching the end-of-stream when reading from the server will | |
| 118 | /// cause `error.TlsConnectionTruncated` to be returned, unless a close_notify | |
| 119 | /// message has been received. By setting this flag to `true`, instead, the | |
| 120 | /// end-of-stream will be forwarded to the application layer above TLS. | |
| 121 | /// | |
| 122 | /// This makes the application vulnerable to truncation attacks unless the | |
| 123 | /// application layer itself verifies that the amount of data received equals | |
| 124 | /// the amount of data expected, such as HTTP with the Content-Length header. | |
| 125 | allow_truncation_attacks: bool = false, | |
| 126 | write_buffer: []u8, | |
| 127 | read_buffer: []u8, | |
| 128 | /// Populated when `error.TlsAlert` is returned from `init`. | |
| 129 | alert: ?*tls.Alert = null, | |
| 133 | 130 | }; |
| 134 | 131 | |
| 135 | pub fn InitError(comptime Stream: type) type { | |
| 136 | return std.mem.Allocator.Error || Stream.WriteError || Stream.ReadError || tls.AlertDescription.Error || error{ | |
| 137 | InsufficientEntropy, | |
| 138 | DiskQuota, | |
| 139 | LockViolation, | |
| 140 | NotOpenForWriting, | |
| 141 | TlsUnexpectedMessage, | |
| 142 | TlsIllegalParameter, | |
| 143 | TlsDecryptFailure, | |
| 144 | TlsRecordOverflow, | |
| 145 | TlsBadRecordMac, | |
| 146 | CertificateFieldHasInvalidLength, | |
| 147 | CertificateHostMismatch, | |
| 148 | CertificatePublicKeyInvalid, | |
| 149 | CertificateExpired, | |
| 150 | CertificateFieldHasWrongDataType, | |
| 151 | CertificateIssuerMismatch, | |
| 152 | CertificateNotYetValid, | |
| 153 | CertificateSignatureAlgorithmMismatch, | |
| 154 | CertificateSignatureAlgorithmUnsupported, | |
| 155 | CertificateSignatureInvalid, | |
| 156 | CertificateSignatureInvalidLength, | |
| 157 | CertificateSignatureNamedCurveUnsupported, | |
| 158 | CertificateSignatureUnsupportedBitCount, | |
| 159 | TlsCertificateNotVerified, | |
| 160 | TlsBadSignatureScheme, | |
| 161 | TlsBadRsaSignatureBitCount, | |
| 162 | InvalidEncoding, | |
| 163 | IdentityElement, | |
| 164 | SignatureVerificationFailed, | |
| 165 | TlsDecryptError, | |
| 166 | TlsConnectionTruncated, | |
| 167 | TlsDecodeError, | |
| 168 | UnsupportedCertificateVersion, | |
| 169 | CertificateTimeInvalid, | |
| 170 | CertificateHasUnrecognizedObjectId, | |
| 171 | CertificateHasInvalidBitString, | |
| 172 | MessageTooLong, | |
| 173 | NegativeIntoUnsigned, | |
| 174 | TargetTooSmall, | |
| 175 | BufferTooSmall, | |
| 176 | InvalidSignature, | |
| 177 | NotSquare, | |
| 178 | NonCanonical, | |
| 179 | WeakPublicKey, | |
| 180 | }; | |
| 181 | } | |
| 132 | const InitError = error{ | |
| 133 | WriteFailed, | |
| 134 | ReadFailed, | |
| 135 | InsufficientEntropy, | |
| 136 | DiskQuota, | |
| 137 | LockViolation, | |
| 138 | NotOpenForWriting, | |
| 139 | /// The alert description will be stored in `alert`. | |
| 140 | TlsAlert, | |
| 141 | TlsUnexpectedMessage, | |
| 142 | TlsIllegalParameter, | |
| 143 | TlsDecryptFailure, | |
| 144 | TlsRecordOverflow, | |
| 145 | TlsBadRecordMac, | |
| 146 | CertificateFieldHasInvalidLength, | |
| 147 | CertificateHostMismatch, | |
| 148 | CertificatePublicKeyInvalid, | |
| 149 | CertificateExpired, | |
| 150 | CertificateFieldHasWrongDataType, | |
| 151 | CertificateIssuerMismatch, | |
| 152 | CertificateNotYetValid, | |
| 153 | CertificateSignatureAlgorithmMismatch, | |
| 154 | CertificateSignatureAlgorithmUnsupported, | |
| 155 | CertificateSignatureInvalid, | |
| 156 | CertificateSignatureInvalidLength, | |
| 157 | CertificateSignatureNamedCurveUnsupported, | |
| 158 | CertificateSignatureUnsupportedBitCount, | |
| 159 | TlsCertificateNotVerified, | |
| 160 | TlsBadSignatureScheme, | |
| 161 | TlsBadRsaSignatureBitCount, | |
| 162 | InvalidEncoding, | |
| 163 | IdentityElement, | |
| 164 | SignatureVerificationFailed, | |
| 165 | TlsDecryptError, | |
| 166 | TlsConnectionTruncated, | |
| 167 | TlsDecodeError, | |
| 168 | UnsupportedCertificateVersion, | |
| 169 | CertificateTimeInvalid, | |
| 170 | CertificateHasUnrecognizedObjectId, | |
| 171 | CertificateHasInvalidBitString, | |
| 172 | MessageTooLong, | |
| 173 | NegativeIntoUnsigned, | |
| 174 | TargetTooSmall, | |
| 175 | BufferTooSmall, | |
| 176 | InvalidSignature, | |
| 177 | NotSquare, | |
| 178 | NonCanonical, | |
| 179 | WeakPublicKey, | |
| 180 | }; | |
| 182 | 181 | |
| 183 | /// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session with `stream`, which | |
| 184 | /// must conform to `StreamInterface`. | |
| 182 | /// Initiates a TLS handshake and establishes a TLSv1.2 or TLSv1.3 session. | |
| 185 | 183 | /// |
| 186 | 184 | /// `host` is only borrowed during this function call. |
| 187 | pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client { | |
| 185 | /// | |
| 186 | /// `input` is asserted to have buffer capacity at least `min_buffer_len`. | |
| 187 | pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client { | |
| 188 | assert(input.buffer.len >= min_buffer_len); | |
| 189 | assert(output.buffer.len >= min_buffer_len); | |
| 188 | 190 | const host = switch (options.host) { |
| 189 | 191 | .no_verification => "", |
| 190 | 192 | .explicit => |host| host, |
| ... | ... | @@ -276,11 +278,9 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 276 | 278 | }; |
| 277 | 279 | |
| 278 | 280 | { |
| 279 | var iovecs = [_]std.posix.iovec_const{ | |
| 280 | .{ .base = cleartext_header.ptr, .len = cleartext_header.len }, | |
| 281 | .{ .base = host.ptr, .len = host.len }, | |
| 282 | }; | |
| 283 | try stream.writevAll(iovecs[0..if (host.len == 0) 1 else 2]); | |
| 281 | var iovecs: [2][]const u8 = .{ cleartext_header, host }; | |
| 282 | try output.writeVecAll(iovecs[0..if (host.len == 0) 1 else 2]); | |
| 283 | try output.flush(); | |
| 284 | 284 | } |
| 285 | 285 | |
| 286 | 286 | var tls_version: tls.ProtocolVersion = undefined; |
| ... | ... | @@ -329,20 +329,28 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 329 | 329 | var cleartext_fragment_start: usize = 0; |
| 330 | 330 | var cleartext_fragment_end: usize = 0; |
| 331 | 331 | var cleartext_bufs: [2][tls.max_ciphertext_inner_record_len]u8 = undefined; |
| 332 | var handshake_buffer: [tls.max_ciphertext_record_len]u8 = undefined; | |
| 333 | var d: tls.Decoder = .{ .buf = &handshake_buffer }; | |
| 334 | 332 | fragment: while (true) { |
| 335 | try d.readAtLeastOurAmt(stream, tls.record_header_len); | |
| 336 | const record_header = d.buf[d.idx..][0..tls.record_header_len]; | |
| 337 | const record_ct = d.decode(tls.ContentType); | |
| 338 | d.skip(2); // legacy_version | |
| 339 | const record_len = d.decode(u16); | |
| 340 | try d.readAtLeast(stream, record_len); | |
| 341 | var record_decoder = try d.sub(record_len); | |
| 333 | // Ensure the input buffer pointer is stable in this scope. | |
| 334 | input.rebase(tls.max_ciphertext_record_len) catch |err| switch (err) { | |
| 335 | error.EndOfStream => {}, // We have assurance the remainder of stream can be buffered. | |
| 336 | }; | |
| 337 | const record_header = input.peek(tls.record_header_len) catch |err| switch (err) { | |
| 338 | error.EndOfStream => return error.TlsConnectionTruncated, | |
| 339 | error.ReadFailed => return error.ReadFailed, | |
| 340 | }; | |
| 341 | const record_ct = input.takeEnumNonexhaustive(tls.ContentType, .big) catch unreachable; // already peeked | |
| 342 | input.toss(2); // legacy_version | |
| 343 | const record_len = input.takeInt(u16, .big) catch unreachable; // already peeked | |
| 344 | if (record_len > tls.max_ciphertext_len) return error.TlsRecordOverflow; | |
| 345 | const record_buffer = input.take(record_len) catch |err| switch (err) { | |
| 346 | error.EndOfStream => return error.TlsConnectionTruncated, | |
| 347 | error.ReadFailed => return error.ReadFailed, | |
| 348 | }; | |
| 349 | var record_decoder: tls.Decoder = .fromTheirSlice(record_buffer); | |
| 342 | 350 | var ctd, const ct = content: switch (cipher_state) { |
| 343 | 351 | .cleartext => .{ record_decoder, record_ct }, |
| 344 | 352 | .handshake => { |
| 345 | std.debug.assert(tls_version == .tls_1_3); | |
| 353 | assert(tls_version == .tls_1_3); | |
| 346 | 354 | if (record_ct != .application_data) return error.TlsUnexpectedMessage; |
| 347 | 355 | try record_decoder.ensure(record_len); |
| 348 | 356 | const cleartext_buf = &cleartext_bufs[cert_buf_index % 2]; |
| ... | ... | @@ -374,7 +382,7 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 374 | 382 | break :content .{ tls.Decoder.fromTheirSlice(@constCast(cleartext_buf[cleartext_fragment_start..cleartext_fragment_end])), ct }; |
| 375 | 383 | }, |
| 376 | 384 | .application => { |
| 377 | std.debug.assert(tls_version == .tls_1_2); | |
| 385 | assert(tls_version == .tls_1_2); | |
| 378 | 386 | if (record_ct != .handshake) return error.TlsUnexpectedMessage; |
| 379 | 387 | try record_decoder.ensure(record_len); |
| 380 | 388 | const cleartext_buf = &cleartext_bufs[cert_buf_index % 2]; |
| ... | ... | @@ -412,14 +420,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 412 | 420 | switch (ct) { |
| 413 | 421 | .alert => { |
| 414 | 422 | ctd.ensure(2) catch continue :fragment; |
| 415 | const level = ctd.decode(tls.AlertLevel); | |
| 416 | const desc = ctd.decode(tls.AlertDescription); | |
| 417 | _ = level; | |
| 418 | ||
| 419 | // if this isn't a error alert, then it's a closure alert, which makes no sense in a handshake | |
| 420 | try desc.toError(); | |
| 421 | // TODO: handle server-side closures | |
| 422 | return error.TlsUnexpectedMessage; | |
| 423 | if (options.alert) |a| a.* = .{ | |
| 424 | .level = ctd.decode(tls.Alert.Level), | |
| 425 | .description = ctd.decode(tls.Alert.Description), | |
| 426 | }; | |
| 427 | return error.TlsAlert; | |
| 423 | 428 | }, |
| 424 | 429 | .change_cipher_spec => { |
| 425 | 430 | ctd.ensure(1) catch continue :fragment; |
| ... | ... | @@ -533,7 +538,7 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 533 | 538 | pv.master_secret = P.Hkdf.extract(&ap_derived_secret, &zeroes); |
| 534 | 539 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "c hs traffic", &hello_hash, P.Hash.digest_length); |
| 535 | 540 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.handshake_secret, "s hs traffic", &hello_hash, P.Hash.digest_length); |
| 536 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ | |
| 541 | if (options.ssl_key_log) |key_log| logSecrets(key_log.writer, .{ | |
| 537 | 542 | .client_random = &client_hello_rand, |
| 538 | 543 | }, .{ |
| 539 | 544 | .SERVER_HANDSHAKE_TRAFFIC_SECRET = &server_secret, |
| ... | ... | @@ -707,7 +712,7 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 707 | 712 | &client_hello_rand, |
| 708 | 713 | &server_hello_rand, |
| 709 | 714 | }, 48); |
| 710 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ | |
| 715 | if (options.ssl_key_log) |key_log| logSecrets(key_log.writer, .{ | |
| 711 | 716 | .client_random = &client_hello_rand, |
| 712 | 717 | }, .{ |
| 713 | 718 | .CLIENT_RANDOM = &master_secret, |
| ... | ... | @@ -755,11 +760,13 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 755 | 760 | nonce, |
| 756 | 761 | pv.app_cipher.client_write_key, |
| 757 | 762 | ); |
| 758 | const all_msgs = client_key_exchange_msg ++ client_change_cipher_spec_msg ++ client_verify_msg; | |
| 759 | var all_msgs_vec = [_]std.posix.iovec_const{ | |
| 760 | .{ .base = &all_msgs, .len = all_msgs.len }, | |
| 763 | var all_msgs_vec: [3][]const u8 = .{ | |
| 764 | &client_key_exchange_msg, | |
| 765 | &client_change_cipher_spec_msg, | |
| 766 | &client_verify_msg, | |
| 761 | 767 | }; |
| 762 | try stream.writevAll(&all_msgs_vec); | |
| 768 | try output.writeVecAll(&all_msgs_vec); | |
| 769 | try output.flush(); | |
| 763 | 770 | }, |
| 764 | 771 | } |
| 765 | 772 | write_seq += 1; |
| ... | ... | @@ -820,15 +827,16 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 820 | 827 | const nonce = pv.client_handshake_iv; |
| 821 | 828 | P.AEAD.encrypt(ciphertext, auth_tag, &out_cleartext, ad, nonce, pv.client_handshake_key); |
| 822 | 829 | |
| 823 | const all_msgs = client_change_cipher_spec_msg ++ finished_msg; | |
| 824 | var all_msgs_vec = [_]std.posix.iovec_const{ | |
| 825 | .{ .base = &all_msgs, .len = all_msgs.len }, | |
| 830 | var all_msgs_vec: [2][]const u8 = .{ | |
| 831 | &client_change_cipher_spec_msg, | |
| 832 | &finished_msg, | |
| 826 | 833 | }; |
| 827 | try stream.writevAll(&all_msgs_vec); | |
| 834 | try output.writeVecAll(&all_msgs_vec); | |
| 835 | try output.flush(); | |
| 828 | 836 | |
| 829 | 837 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length); |
| 830 | 838 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length); |
| 831 | if (options.ssl_key_log_file) |key_log_file| logSecrets(key_log_file, .{ | |
| 839 | if (options.ssl_key_log) |key_log| logSecrets(key_log.writer, .{ | |
| 832 | 840 | .counter = key_seq, |
| 833 | 841 | .client_random = &client_hello_rand, |
| 834 | 842 | }, .{ |
| ... | ... | @@ -855,8 +863,28 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 855 | 863 | else => unreachable, |
| 856 | 864 | }, |
| 857 | 865 | }; |
| 858 | const leftover = d.rest(); | |
| 859 | var client: Client = .{ | |
| 866 | if (options.ssl_key_log) |ssl_key_log| ssl_key_log.* = .{ | |
| 867 | .client_key_seq = key_seq, | |
| 868 | .server_key_seq = key_seq, | |
| 869 | .client_random = client_hello_rand, | |
| 870 | .writer = ssl_key_log.writer, | |
| 871 | }; | |
| 872 | return .{ | |
| 873 | .input = input, | |
| 874 | .reader = .{ | |
| 875 | .buffer = options.read_buffer, | |
| 876 | .vtable = &.{ .stream = stream }, | |
| 877 | .seek = 0, | |
| 878 | .end = 0, | |
| 879 | }, | |
| 880 | .output = output, | |
| 881 | .writer = .{ | |
| 882 | .buffer = options.write_buffer, | |
| 883 | .vtable = &.{ | |
| 884 | .drain = drain, | |
| 885 | .flush = flush, | |
| 886 | }, | |
| 887 | }, | |
| 860 | 888 | .tls_version = tls_version, |
| 861 | 889 | .read_seq = switch (tls_version) { |
| 862 | 890 | .tls_1_3 => 0, |
| ... | ... | @@ -868,22 +896,11 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 868 | 896 | .tls_1_2 => write_seq, |
| 869 | 897 | else => unreachable, |
| 870 | 898 | }, |
| 871 | .partial_cleartext_idx = 0, | |
| 872 | .partial_ciphertext_idx = 0, | |
| 873 | .partial_ciphertext_end = @intCast(leftover.len), | |
| 874 | 899 | .received_close_notify = false, |
| 875 | .allow_truncation_attacks = false, | |
| 900 | .allow_truncation_attacks = options.allow_truncation_attacks, | |
| 876 | 901 | .application_cipher = app_cipher, |
| 877 | .partially_read_buffer = undefined, | |
| 878 | .ssl_key_log = if (options.ssl_key_log_file) |key_log_file| .{ | |
| 879 | .client_key_seq = key_seq, | |
| 880 | .server_key_seq = key_seq, | |
| 881 | .client_random = client_hello_rand, | |
| 882 | .file = key_log_file, | |
| 883 | } else null, | |
| 902 | .ssl_key_log = options.ssl_key_log, | |
| 884 | 903 | }; |
| 885 | @memcpy(client.partially_read_buffer[0..leftover.len], leftover); | |
| 886 | return client; | |
| 887 | 904 | }, |
| 888 | 905 | else => return error.TlsUnexpectedMessage, |
| 889 | 906 | } |
| ... | ... | @@ -897,94 +914,73 @@ pub fn init(stream: anytype, options: Options) InitError(@TypeOf(stream))!Client |
| 897 | 914 | } |
| 898 | 915 | } |
| 899 | 916 | |
| 900 | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. | |
| 901 | /// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`. | |
| 902 | pub fn write(c: *Client, stream: anytype, bytes: []const u8) !usize { | |
| 903 | return writeEnd(c, stream, bytes, false); | |
| 904 | } | |
| 905 | ||
| 906 | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. | |
| 907 | pub fn writeAll(c: *Client, stream: anytype, bytes: []const u8) !void { | |
| 908 | var index: usize = 0; | |
| 909 | while (index < bytes.len) { | |
| 910 | index += try c.write(stream, bytes[index..]); | |
| 917 | fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { | |
| 918 | const c: *Client = @alignCast(@fieldParentPtr("writer", w)); | |
| 919 | const output = c.output; | |
| 920 | const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len); | |
| 921 | var ciphertext_end: usize = 0; | |
| 922 | var total_clear: usize = 0; | |
| 923 | done: { | |
| 924 | { | |
| 925 | const buf = w.buffered(); | |
| 926 | const prepared = prepareCiphertextRecord(c, ciphertext_buf[ciphertext_end..], buf, .application_data); | |
| 927 | total_clear += prepared.cleartext_len; | |
| 928 | ciphertext_end += prepared.ciphertext_end; | |
| 929 | if (prepared.cleartext_len < buf.len) break :done; | |
| 930 | } | |
| 931 | for (data[0 .. data.len - 1]) |buf| { | |
| 932 | if (buf.len < min_buffer_len) break :done; | |
| 933 | const prepared = prepareCiphertextRecord(c, ciphertext_buf[ciphertext_end..], buf, .application_data); | |
| 934 | total_clear += prepared.cleartext_len; | |
| 935 | ciphertext_end += prepared.ciphertext_end; | |
| 936 | if (prepared.cleartext_len < buf.len) break :done; | |
| 937 | } | |
| 938 | const buf = data[data.len - 1]; | |
| 939 | for (0..splat) |_| { | |
| 940 | if (buf.len < min_buffer_len) break :done; | |
| 941 | const prepared = prepareCiphertextRecord(c, ciphertext_buf[ciphertext_end..], buf, .application_data); | |
| 942 | total_clear += prepared.cleartext_len; | |
| 943 | ciphertext_end += prepared.ciphertext_end; | |
| 944 | if (prepared.cleartext_len < buf.len) break :done; | |
| 945 | } | |
| 911 | 946 | } |
| 947 | output.advance(ciphertext_end); | |
| 948 | return w.consume(total_clear); | |
| 912 | 949 | } |
| 913 | 950 | |
| 914 | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. | |
| 915 | /// If `end` is true, then this function additionally sends a `close_notify` alert, | |
| 916 | /// which is necessary for the server to distinguish between a properly finished | |
| 917 | /// TLS session, or a truncation attack. | |
| 918 | pub fn writeAllEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !void { | |
| 919 | var index: usize = 0; | |
| 920 | while (index < bytes.len) { | |
| 921 | index += try c.writeEnd(stream, bytes[index..], end); | |
| 922 | } | |
| 951 | fn flush(w: *Writer) Writer.Error!void { | |
| 952 | const c: *Client = @alignCast(@fieldParentPtr("writer", w)); | |
| 953 | const output = c.output; | |
| 954 | const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len); | |
| 955 | const prepared = prepareCiphertextRecord(c, ciphertext_buf, w.buffered(), .application_data); | |
| 956 | output.advance(prepared.ciphertext_end); | |
| 957 | w.end = 0; | |
| 923 | 958 | } |
| 924 | 959 | |
| 925 | /// Sends TLS-encrypted data to `stream`, which must conform to `StreamInterface`. | |
| 926 | /// Returns the number of cleartext bytes sent, which may be fewer than `bytes.len`. | |
| 927 | /// If `end` is true, then this function additionally sends a `close_notify` alert, | |
| 928 | /// which is necessary for the server to distinguish between a properly finished | |
| 929 | /// TLS session, or a truncation attack. | |
| 930 | pub fn writeEnd(c: *Client, stream: anytype, bytes: []const u8, end: bool) !usize { | |
| 931 | var ciphertext_buf: [tls.max_ciphertext_record_len * 4]u8 = undefined; | |
| 932 | var iovecs_buf: [6]std.posix.iovec_const = undefined; | |
| 933 | var prepared = prepareCiphertextRecord(c, &iovecs_buf, &ciphertext_buf, bytes, .application_data); | |
| 934 | if (end) { | |
| 935 | prepared.iovec_end += prepareCiphertextRecord( | |
| 936 | c, | |
| 937 | iovecs_buf[prepared.iovec_end..], | |
| 938 | ciphertext_buf[prepared.ciphertext_end..], | |
| 939 | &tls.close_notify_alert, | |
| 940 | .alert, | |
| 941 | ).iovec_end; | |
| 942 | } | |
| 943 | ||
| 944 | const iovec_end = prepared.iovec_end; | |
| 945 | const overhead_len = prepared.overhead_len; | |
| 946 | ||
| 947 | // Ideally we would call writev exactly once here, however, we must ensure | |
| 948 | // that we don't return with a record partially written. | |
| 949 | var i: usize = 0; | |
| 950 | var total_amt: usize = 0; | |
| 951 | while (true) { | |
| 952 | var amt = try stream.writev(iovecs_buf[i..iovec_end]); | |
| 953 | while (amt >= iovecs_buf[i].len) { | |
| 954 | const encrypted_amt = iovecs_buf[i].len; | |
| 955 | total_amt += encrypted_amt - overhead_len; | |
| 956 | amt -= encrypted_amt; | |
| 957 | i += 1; | |
| 958 | // Rely on the property that iovecs delineate records, meaning that | |
| 959 | // if amt equals zero here, we have fortunately found ourselves | |
| 960 | // with a short read that aligns at the record boundary. | |
| 961 | if (i >= iovec_end) return total_amt; | |
| 962 | // We also cannot return on a vector boundary if the final close_notify is | |
| 963 | // not sent; otherwise the caller would not know to retry the call. | |
| 964 | if (amt == 0 and (!end or i < iovec_end - 1)) return total_amt; | |
| 965 | } | |
| 966 | iovecs_buf[i].base += amt; | |
| 967 | iovecs_buf[i].len -= amt; | |
| 968 | } | |
| 960 | /// Sends a `close_notify` alert, which is necessary for the server to | |
| 961 | /// distinguish between a properly finished TLS session, or a truncation | |
| 962 | /// attack. | |
| 963 | pub fn end(c: *Client) Writer.Error!void { | |
| 964 | try flush(&c.writer); | |
| 965 | const output = c.output; | |
| 966 | const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len); | |
| 967 | const prepared = prepareCiphertextRecord(c, ciphertext_buf, &tls.close_notify_alert, .alert); | |
| 968 | output.advance(prepared.ciphertext_end); | |
| 969 | 969 | } |
| 970 | 970 | |
| 971 | 971 | fn prepareCiphertextRecord( |
| 972 | 972 | c: *Client, |
| 973 | iovecs: []std.posix.iovec_const, | |
| 974 | 973 | ciphertext_buf: []u8, |
| 975 | 974 | bytes: []const u8, |
| 976 | 975 | inner_content_type: tls.ContentType, |
| 977 | 976 | ) struct { |
| 978 | iovec_end: usize, | |
| 979 | 977 | ciphertext_end: usize, |
| 980 | /// How many bytes are taken up by overhead per record. | |
| 981 | overhead_len: usize, | |
| 978 | cleartext_len: usize, | |
| 982 | 979 | } { |
| 983 | 980 | // Due to the trailing inner content type byte in the ciphertext, we need |
| 984 | 981 | // an additional buffer for storing the cleartext into before encrypting. |
| 985 | 982 | var cleartext_buf: [max_ciphertext_len]u8 = undefined; |
| 986 | 983 | var ciphertext_end: usize = 0; |
| 987 | var iovec_end: usize = 0; | |
| 988 | 984 | var bytes_i: usize = 0; |
| 989 | 985 | switch (c.application_cipher) { |
| 990 | 986 | inline else => |*p| switch (c.tls_version) { |
| ... | ... | @@ -992,18 +988,15 @@ fn prepareCiphertextRecord( |
| 992 | 988 | const pv = &p.tls_1_3; |
| 993 | 989 | const P = @TypeOf(p.*); |
| 994 | 990 | const overhead_len = tls.record_header_len + P.AEAD.tag_length + 1; |
| 995 | const close_notify_alert_reserved = tls.close_notify_alert.len + overhead_len; | |
| 996 | 991 | while (true) { |
| 997 | 992 | const encrypted_content_len: u16 = @min( |
| 998 | 993 | bytes.len - bytes_i, |
| 999 | 994 | tls.max_ciphertext_inner_record_len, |
| 1000 | ciphertext_buf.len -| | |
| 1001 | (close_notify_alert_reserved + overhead_len + ciphertext_end), | |
| 995 | ciphertext_buf.len -| (overhead_len + ciphertext_end), | |
| 1002 | 996 | ); |
| 1003 | 997 | if (encrypted_content_len == 0) return .{ |
| 1004 | .iovec_end = iovec_end, | |
| 1005 | 998 | .ciphertext_end = ciphertext_end, |
| 1006 | .overhead_len = overhead_len, | |
| 999 | .cleartext_len = bytes_i, | |
| 1007 | 1000 | }; |
| 1008 | 1001 | |
| 1009 | 1002 | @memcpy(cleartext_buf[0..encrypted_content_len], bytes[bytes_i..][0..encrypted_content_len]); |
| ... | ... | @@ -1012,7 +1005,6 @@ fn prepareCiphertextRecord( |
| 1012 | 1005 | const ciphertext_len = encrypted_content_len + 1; |
| 1013 | 1006 | const cleartext = cleartext_buf[0..ciphertext_len]; |
| 1014 | 1007 | |
| 1015 | const record_start = ciphertext_end; | |
| 1016 | 1008 | const ad = ciphertext_buf[ciphertext_end..][0..tls.record_header_len]; |
| 1017 | 1009 | ad.* = .{@intFromEnum(tls.ContentType.application_data)} ++ |
| 1018 | 1010 | int(u16, @intFromEnum(tls.ProtocolVersion.tls_1_2)) ++ |
| ... | ... | @@ -1030,38 +1022,27 @@ fn prepareCiphertextRecord( |
| 1030 | 1022 | }; |
| 1031 | 1023 | P.AEAD.encrypt(ciphertext, auth_tag, cleartext, ad, nonce, pv.client_key); |
| 1032 | 1024 | c.write_seq += 1; // TODO send key_update on overflow |
| 1033 | ||
| 1034 | const record = ciphertext_buf[record_start..ciphertext_end]; | |
| 1035 | iovecs[iovec_end] = .{ | |
| 1036 | .base = record.ptr, | |
| 1037 | .len = record.len, | |
| 1038 | }; | |
| 1039 | iovec_end += 1; | |
| 1040 | 1025 | } |
| 1041 | 1026 | }, |
| 1042 | 1027 | .tls_1_2 => { |
| 1043 | 1028 | const pv = &p.tls_1_2; |
| 1044 | 1029 | const P = @TypeOf(p.*); |
| 1045 | 1030 | const overhead_len = tls.record_header_len + P.record_iv_length + P.mac_length; |
| 1046 | const close_notify_alert_reserved = tls.close_notify_alert.len + overhead_len; | |
| 1047 | 1031 | while (true) { |
| 1048 | 1032 | const message_len: u16 = @min( |
| 1049 | 1033 | bytes.len - bytes_i, |
| 1050 | 1034 | tls.max_ciphertext_inner_record_len, |
| 1051 | ciphertext_buf.len -| | |
| 1052 | (close_notify_alert_reserved + overhead_len + ciphertext_end), | |
| 1035 | ciphertext_buf.len -| (overhead_len + ciphertext_end), | |
| 1053 | 1036 | ); |
| 1054 | 1037 | if (message_len == 0) return .{ |
| 1055 | .iovec_end = iovec_end, | |
| 1056 | 1038 | .ciphertext_end = ciphertext_end, |
| 1057 | .overhead_len = overhead_len, | |
| 1039 | .cleartext_len = bytes_i, | |
| 1058 | 1040 | }; |
| 1059 | 1041 | |
| 1060 | 1042 | @memcpy(cleartext_buf[0..message_len], bytes[bytes_i..][0..message_len]); |
| 1061 | 1043 | bytes_i += message_len; |
| 1062 | 1044 | const cleartext = cleartext_buf[0..message_len]; |
| 1063 | 1045 | |
| 1064 | const record_start = ciphertext_end; | |
| 1065 | 1046 | const record_header = ciphertext_buf[ciphertext_end..][0..tls.record_header_len]; |
| 1066 | 1047 | ciphertext_end += tls.record_header_len; |
| 1067 | 1048 | record_header.* = .{@intFromEnum(inner_content_type)} ++ |
| ... | ... | @@ -1083,13 +1064,6 @@ fn prepareCiphertextRecord( |
| 1083 | 1064 | ciphertext_end += P.mac_length; |
| 1084 | 1065 | P.AEAD.encrypt(ciphertext, auth_tag, cleartext, ad, nonce, pv.client_write_key); |
| 1085 | 1066 | c.write_seq += 1; // TODO send key_update on overflow |
| 1086 | ||
| 1087 | const record = ciphertext_buf[record_start..ciphertext_end]; | |
| 1088 | iovecs[iovec_end] = .{ | |
| 1089 | .base = record.ptr, | |
| 1090 | .len = record.len, | |
| 1091 | }; | |
| 1092 | iovec_end += 1; | |
| 1093 | 1067 | } |
| 1094 | 1068 | }, |
| 1095 | 1069 | else => unreachable, |
| ... | ... | @@ -1098,421 +1072,194 @@ fn prepareCiphertextRecord( |
| 1098 | 1072 | } |
| 1099 | 1073 | |
| 1100 | 1074 | pub fn eof(c: Client) bool { |
| 1101 | return c.received_close_notify and | |
| 1102 | c.partial_cleartext_idx >= c.partial_ciphertext_idx and | |
| 1103 | c.partial_ciphertext_idx >= c.partial_ciphertext_end; | |
| 1104 | } | |
| 1105 | ||
| 1106 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1107 | /// Returns the number of bytes read, calling the underlying read function the | |
| 1108 | /// minimal number of times until the buffer has at least `len` bytes filled. | |
| 1109 | /// If the number read is less than `len` it means the stream reached the end. | |
| 1110 | /// Reaching the end of the stream is not an error condition. | |
| 1111 | pub fn readAtLeast(c: *Client, stream: anytype, buffer: []u8, len: usize) !usize { | |
| 1112 | var iovecs = [1]std.posix.iovec{.{ .base = buffer.ptr, .len = buffer.len }}; | |
| 1113 | return readvAtLeast(c, stream, &iovecs, len); | |
| 1114 | } | |
| 1115 | ||
| 1116 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1117 | pub fn read(c: *Client, stream: anytype, buffer: []u8) !usize { | |
| 1118 | return readAtLeast(c, stream, buffer, 1); | |
| 1075 | return c.received_close_notify; | |
| 1119 | 1076 | } |
| 1120 | 1077 | |
| 1121 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1122 | /// Returns the number of bytes read. If the number read is smaller than | |
| 1123 | /// `buffer.len`, it means the stream reached the end. Reaching the end of the | |
| 1124 | /// stream is not an error condition. | |
| 1125 | pub fn readAll(c: *Client, stream: anytype, buffer: []u8) !usize { | |
| 1126 | return readAtLeast(c, stream, buffer, buffer.len); | |
| 1127 | } | |
| 1128 | ||
| 1129 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1130 | /// Returns the number of bytes read. If the number read is less than the space | |
| 1131 | /// provided it means the stream reached the end. Reaching the end of the | |
| 1132 | /// stream is not an error condition. | |
| 1133 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1134 | /// order to handle partial reads from the underlying stream layer. | |
| 1135 | pub fn readv(c: *Client, stream: anytype, iovecs: []std.posix.iovec) !usize { | |
| 1136 | return readvAtLeast(c, stream, iovecs, 1); | |
| 1137 | } | |
| 1138 | ||
| 1139 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1140 | /// Returns the number of bytes read, calling the underlying read function the | |
| 1141 | /// minimal number of times until the iovecs have at least `len` bytes filled. | |
| 1142 | /// If the number read is less than `len` it means the stream reached the end. | |
| 1143 | /// Reaching the end of the stream is not an error condition. | |
| 1144 | /// The `iovecs` parameter is mutable because this function needs to mutate the fields in | |
| 1145 | /// order to handle partial reads from the underlying stream layer. | |
| 1146 | pub fn readvAtLeast(c: *Client, stream: anytype, iovecs: []std.posix.iovec, len: usize) !usize { | |
| 1147 | if (c.eof()) return 0; | |
| 1148 | ||
| 1149 | var off_i: usize = 0; | |
| 1150 | var vec_i: usize = 0; | |
| 1151 | while (true) { | |
| 1152 | var amt = try c.readvAdvanced(stream, iovecs[vec_i..]); | |
| 1153 | off_i += amt; | |
| 1154 | if (c.eof() or off_i >= len) return off_i; | |
| 1155 | while (amt >= iovecs[vec_i].len) { | |
| 1156 | amt -= iovecs[vec_i].len; | |
| 1157 | vec_i += 1; | |
| 1158 | } | |
| 1159 | iovecs[vec_i].base += amt; | |
| 1160 | iovecs[vec_i].len -= amt; | |
| 1161 | } | |
| 1162 | } | |
| 1163 | ||
| 1164 | /// Receives TLS-encrypted data from `stream`, which must conform to `StreamInterface`. | |
| 1165 | /// Returns number of bytes that have been read, populated inside `iovecs`. A | |
| 1166 | /// return value of zero bytes does not mean end of stream. Instead, check the `eof()` | |
| 1167 | /// for the end of stream. The `eof()` may be true after any call to | |
| 1168 | /// `read`, including when greater than zero bytes are returned, and this | |
| 1169 | /// function asserts that `eof()` is `false`. | |
| 1170 | /// See `readv` for a higher level function that has the same, familiar API as | |
| 1171 | /// other read functions, such as `std.fs.File.read`. | |
| 1172 | pub fn readvAdvanced(c: *Client, stream: anytype, iovecs: []const std.posix.iovec) !usize { | |
| 1173 | var vp: VecPut = .{ .iovecs = iovecs }; | |
| 1174 | ||
| 1175 | // Give away the buffered cleartext we have, if any. | |
| 1176 | const partial_cleartext = c.partially_read_buffer[c.partial_cleartext_idx..c.partial_ciphertext_idx]; | |
| 1177 | if (partial_cleartext.len > 0) { | |
| 1178 | const amt: u15 = @intCast(vp.put(partial_cleartext)); | |
| 1179 | c.partial_cleartext_idx += amt; | |
| 1180 | ||
| 1181 | if (c.partial_cleartext_idx == c.partial_ciphertext_idx and | |
| 1182 | c.partial_ciphertext_end == c.partial_ciphertext_idx) | |
| 1183 | { | |
| 1184 | // The buffer is now empty. | |
| 1185 | c.partial_cleartext_idx = 0; | |
| 1186 | c.partial_ciphertext_idx = 0; | |
| 1187 | c.partial_ciphertext_end = 0; | |
| 1188 | } | |
| 1189 | ||
| 1190 | if (c.received_close_notify) { | |
| 1191 | c.partial_ciphertext_end = 0; | |
| 1192 | assert(vp.total == amt); | |
| 1193 | return amt; | |
| 1194 | } else if (amt > 0) { | |
| 1195 | // We don't need more data, so don't call read. | |
| 1196 | assert(vp.total == amt); | |
| 1197 | return amt; | |
| 1198 | } | |
| 1078 | fn stream(r: *Reader, w: *Writer, limit: std.Io.Limit) Reader.StreamError!usize { | |
| 1079 | const c: *Client = @alignCast(@fieldParentPtr("reader", r)); | |
| 1080 | if (c.eof()) return error.EndOfStream; | |
| 1081 | const input = c.input; | |
| 1082 | // If at least one full encrypted record is not buffered, read once. | |
| 1083 | const record_header = input.peek(tls.record_header_len) catch |err| switch (err) { | |
| 1084 | error.EndOfStream => { | |
| 1085 | // This is either a truncation attack, a bug in the server, or an | |
| 1086 | // intentional omission of the close_notify message due to truncation | |
| 1087 | // detection handled above the TLS layer. | |
| 1088 | if (c.allow_truncation_attacks) { | |
| 1089 | c.received_close_notify = true; | |
| 1090 | return error.EndOfStream; | |
| 1091 | } else { | |
| 1092 | return failRead(c, error.TlsConnectionTruncated); | |
| 1093 | } | |
| 1094 | }, | |
| 1095 | error.ReadFailed => return error.ReadFailed, | |
| 1096 | }; | |
| 1097 | const ct: tls.ContentType = @enumFromInt(record_header[0]); | |
| 1098 | const legacy_version = mem.readInt(u16, record_header[1..][0..2], .big); | |
| 1099 | _ = legacy_version; | |
| 1100 | const record_len = mem.readInt(u16, record_header[3..][0..2], .big); | |
| 1101 | if (record_len > max_ciphertext_len) return failRead(c, error.TlsRecordOverflow); | |
| 1102 | const record_end = 5 + record_len; | |
| 1103 | if (record_end > input.buffered().len) { | |
| 1104 | input.fillMore() catch |err| switch (err) { | |
| 1105 | error.EndOfStream => return failRead(c, error.TlsConnectionTruncated), | |
| 1106 | error.ReadFailed => return error.ReadFailed, | |
| 1107 | }; | |
| 1108 | if (record_end > input.buffered().len) return 0; | |
| 1199 | 1109 | } |
| 1200 | 1110 | |
| 1201 | assert(!c.received_close_notify); | |
| 1202 | ||
| 1203 | // Ideally, this buffer would never be used. It is needed when `iovecs` are | |
| 1204 | // too small to fit the cleartext, which may be as large as `max_ciphertext_len`. | |
| 1205 | 1111 | var cleartext_stack_buffer: [max_ciphertext_len]u8 = undefined; |
| 1206 | // Temporarily stores ciphertext before decrypting it and giving it to `iovecs`. | |
| 1207 | var in_stack_buffer: [max_ciphertext_len * 4]u8 = undefined; | |
| 1208 | // How many bytes left in the user's buffer. | |
| 1209 | const free_size = vp.freeSize(); | |
| 1210 | // The amount of the user's buffer that we need to repurpose for storing | |
| 1211 | // ciphertext. The end of the buffer will be used for such purposes. | |
| 1212 | const ciphertext_buf_len = (free_size / 2) -| in_stack_buffer.len; | |
| 1213 | // The amount of the user's buffer that will be used to give cleartext. The | |
| 1214 | // beginning of the buffer will be used for such purposes. | |
| 1215 | const cleartext_buf_len = free_size - ciphertext_buf_len; | |
| 1216 | ||
| 1217 | // Recoup `partially_read_buffer` space. This is necessary because it is assumed | |
| 1218 | // below that `frag0` is big enough to hold at least one record. | |
| 1219 | limitedOverlapCopy(c.partially_read_buffer[0..c.partial_ciphertext_end], c.partial_ciphertext_idx); | |
| 1220 | c.partial_ciphertext_end -= c.partial_ciphertext_idx; | |
| 1221 | c.partial_ciphertext_idx = 0; | |
| 1222 | c.partial_cleartext_idx = 0; | |
| 1223 | const first_iov = c.partially_read_buffer[c.partial_ciphertext_end..]; | |
| 1224 | ||
| 1225 | var ask_iovecs_buf: [2]std.posix.iovec = .{ | |
| 1226 | .{ | |
| 1227 | .base = first_iov.ptr, | |
| 1228 | .len = first_iov.len, | |
| 1229 | }, | |
| 1230 | .{ | |
| 1231 | .base = &in_stack_buffer, | |
| 1232 | .len = in_stack_buffer.len, | |
| 1112 | const cleartext, const inner_ct: tls.ContentType = cleartext: switch (c.application_cipher) { | |
| 1113 | inline else => |*p| switch (c.tls_version) { | |
| 1114 | .tls_1_3 => { | |
| 1115 | const pv = &p.tls_1_3; | |
| 1116 | const P = @TypeOf(p.*); | |
| 1117 | const ad = input.take(tls.record_header_len) catch unreachable; // already peeked | |
| 1118 | const ciphertext_len = record_len - P.AEAD.tag_length; | |
| 1119 | const ciphertext = input.take(ciphertext_len) catch unreachable; // already peeked | |
| 1120 | const auth_tag = (input.takeArray(P.AEAD.tag_length) catch unreachable).*; // already peeked | |
| 1121 | const nonce = nonce: { | |
| 1122 | const V = @Vector(P.AEAD.nonce_length, u8); | |
| 1123 | const pad = [1]u8{0} ** (P.AEAD.nonce_length - 8); | |
| 1124 | const operand: V = pad ++ std.mem.toBytes(big(c.read_seq)); | |
| 1125 | break :nonce @as(V, pv.server_iv) ^ operand; | |
| 1126 | }; | |
| 1127 | const cleartext = cleartext_stack_buffer[0..ciphertext.len]; | |
| 1128 | P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_key) catch | |
| 1129 | return failRead(c, error.TlsBadRecordMac); | |
| 1130 | const msg = mem.trimRight(u8, cleartext, "\x00"); | |
| 1131 | break :cleartext .{ msg[0 .. msg.len - 1], @enumFromInt(msg[msg.len - 1]) }; | |
| 1132 | }, | |
| 1133 | .tls_1_2 => { | |
| 1134 | const pv = &p.tls_1_2; | |
| 1135 | const P = @TypeOf(p.*); | |
| 1136 | const message_len: u16 = record_len - P.record_iv_length - P.mac_length; | |
| 1137 | const ad_header = input.take(tls.record_header_len) catch unreachable; // already peeked | |
| 1138 | const ad = std.mem.toBytes(big(c.read_seq)) ++ | |
| 1139 | ad_header[0 .. 1 + 2] ++ | |
| 1140 | std.mem.toBytes(big(message_len)); | |
| 1141 | const record_iv = (input.takeArray(P.record_iv_length) catch unreachable).*; // already peeked | |
| 1142 | const masked_read_seq = c.read_seq & | |
| 1143 | comptime std.math.shl(u64, std.math.maxInt(u64), 8 * P.record_iv_length); | |
| 1144 | const nonce: [P.AEAD.nonce_length]u8 = nonce: { | |
| 1145 | const V = @Vector(P.AEAD.nonce_length, u8); | |
| 1146 | const pad = [1]u8{0} ** (P.AEAD.nonce_length - 8); | |
| 1147 | const operand: V = pad ++ @as([8]u8, @bitCast(big(masked_read_seq))); | |
| 1148 | break :nonce @as(V, pv.server_write_IV ++ record_iv) ^ operand; | |
| 1149 | }; | |
| 1150 | const ciphertext = input.take(message_len) catch unreachable; // already peeked | |
| 1151 | const auth_tag = (input.takeArray(P.mac_length) catch unreachable).*; // already peeked | |
| 1152 | const cleartext = cleartext_stack_buffer[0..ciphertext.len]; | |
| 1153 | P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_write_key) catch | |
| 1154 | return failRead(c, error.TlsBadRecordMac); | |
| 1155 | break :cleartext .{ cleartext, ct }; | |
| 1156 | }, | |
| 1157 | else => unreachable, | |
| 1233 | 1158 | }, |
| 1234 | 1159 | }; |
| 1235 | ||
| 1236 | // Cleartext capacity of output buffer, in records. Minimum one full record. | |
| 1237 | const buf_cap = @max(cleartext_buf_len / max_ciphertext_len, 1); | |
| 1238 | const wanted_read_len = buf_cap * (max_ciphertext_len + tls.record_header_len); | |
| 1239 | const ask_len = @max(wanted_read_len, cleartext_stack_buffer.len) - c.partial_ciphertext_end; | |
| 1240 | const ask_iovecs = limitVecs(&ask_iovecs_buf, ask_len); | |
| 1241 | const actual_read_len = try stream.readv(ask_iovecs); | |
| 1242 | if (actual_read_len == 0) { | |
| 1243 | // This is either a truncation attack, a bug in the server, or an | |
| 1244 | // intentional omission of the close_notify message due to truncation | |
| 1245 | // detection handled above the TLS layer. | |
| 1246 | if (c.allow_truncation_attacks) { | |
| 1247 | c.received_close_notify = true; | |
| 1248 | } else { | |
| 1249 | return error.TlsConnectionTruncated; | |
| 1250 | } | |
| 1251 | } | |
| 1252 | ||
| 1253 | // There might be more bytes inside `in_stack_buffer` that need to be processed, | |
| 1254 | // but at least frag0 will have one complete ciphertext record. | |
| 1255 | const frag0_end = @min(c.partially_read_buffer.len, c.partial_ciphertext_end + actual_read_len); | |
| 1256 | const frag0 = c.partially_read_buffer[c.partial_ciphertext_idx..frag0_end]; | |
| 1257 | var frag1 = in_stack_buffer[0..actual_read_len -| first_iov.len]; | |
| 1258 | // We need to decipher frag0 and frag1 but there may be a ciphertext record | |
| 1259 | // straddling the boundary. We can handle this with two memcpy() calls to | |
| 1260 | // assemble the straddling record in between handling the two sides. | |
| 1261 | var frag = frag0; | |
| 1262 | var in: usize = 0; | |
| 1263 | while (true) { | |
| 1264 | if (in == frag.len) { | |
| 1265 | // Perfect split. | |
| 1266 | if (frag.ptr == frag1.ptr) { | |
| 1267 | c.partial_ciphertext_end = c.partial_ciphertext_idx; | |
| 1268 | return vp.total; | |
| 1269 | } | |
| 1270 | frag = frag1; | |
| 1271 | in = 0; | |
| 1272 | continue; | |
| 1273 | } | |
| 1274 | ||
| 1275 | if (in + tls.record_header_len > frag.len) { | |
| 1276 | if (frag.ptr == frag1.ptr) | |
| 1277 | return finishRead(c, frag, in, vp.total); | |
| 1278 | ||
| 1279 | const first = frag[in..]; | |
| 1280 | ||
| 1281 | if (frag1.len < tls.record_header_len) | |
| 1282 | return finishRead2(c, first, frag1, vp.total); | |
| 1283 | ||
| 1284 | // A record straddles the two fragments. Copy into the now-empty first fragment. | |
| 1285 | const record_len_byte_0: u16 = straddleByte(frag, frag1, in + 3); | |
| 1286 | const record_len_byte_1: u16 = straddleByte(frag, frag1, in + 4); | |
| 1287 | const record_len = (record_len_byte_0 << 8) | record_len_byte_1; | |
| 1288 | if (record_len > max_ciphertext_len) return error.TlsRecordOverflow; | |
| 1289 | ||
| 1290 | const full_record_len = record_len + tls.record_header_len; | |
| 1291 | const second_len = full_record_len - first.len; | |
| 1292 | if (frag1.len < second_len) | |
| 1293 | return finishRead2(c, first, frag1, vp.total); | |
| 1294 | ||
| 1295 | limitedOverlapCopy(frag, in); | |
| 1296 | @memcpy(frag[first.len..][0..second_len], frag1[0..second_len]); | |
| 1297 | frag = frag[0..full_record_len]; | |
| 1298 | frag1 = frag1[second_len..]; | |
| 1299 | in = 0; | |
| 1300 | continue; | |
| 1301 | } | |
| 1302 | const ct: tls.ContentType = @enumFromInt(frag[in]); | |
| 1303 | in += 1; | |
| 1304 | const legacy_version = mem.readInt(u16, frag[in..][0..2], .big); | |
| 1305 | in += 2; | |
| 1306 | _ = legacy_version; | |
| 1307 | const record_len = mem.readInt(u16, frag[in..][0..2], .big); | |
| 1308 | if (record_len > max_ciphertext_len) return error.TlsRecordOverflow; | |
| 1309 | in += 2; | |
| 1310 | const end = in + record_len; | |
| 1311 | if (end > frag.len) { | |
| 1312 | // We need the record header on the next iteration of the loop. | |
| 1313 | in -= tls.record_header_len; | |
| 1314 | ||
| 1315 | if (frag.ptr == frag1.ptr) | |
| 1316 | return finishRead(c, frag, in, vp.total); | |
| 1317 | ||
| 1318 | // A record straddles the two fragments. Copy into the now-empty first fragment. | |
| 1319 | const first = frag[in..]; | |
| 1320 | const full_record_len = record_len + tls.record_header_len; | |
| 1321 | const second_len = full_record_len - first.len; | |
| 1322 | if (frag1.len < second_len) | |
| 1323 | return finishRead2(c, first, frag1, vp.total); | |
| 1324 | ||
| 1325 | limitedOverlapCopy(frag, in); | |
| 1326 | @memcpy(frag[first.len..][0..second_len], frag1[0..second_len]); | |
| 1327 | frag = frag[0..full_record_len]; | |
| 1328 | frag1 = frag1[second_len..]; | |
| 1329 | in = 0; | |
| 1330 | continue; | |
| 1331 | } | |
| 1332 | const cleartext, const inner_ct: tls.ContentType = cleartext: switch (c.application_cipher) { | |
| 1333 | inline else => |*p| switch (c.tls_version) { | |
| 1334 | .tls_1_3 => { | |
| 1335 | const pv = &p.tls_1_3; | |
| 1336 | const P = @TypeOf(p.*); | |
| 1337 | const ad = frag[in - tls.record_header_len ..][0..tls.record_header_len]; | |
| 1338 | const ciphertext_len = record_len - P.AEAD.tag_length; | |
| 1339 | const ciphertext = frag[in..][0..ciphertext_len]; | |
| 1340 | in += ciphertext_len; | |
| 1341 | const auth_tag = frag[in..][0..P.AEAD.tag_length].*; | |
| 1342 | const nonce = nonce: { | |
| 1343 | const V = @Vector(P.AEAD.nonce_length, u8); | |
| 1344 | const pad = [1]u8{0} ** (P.AEAD.nonce_length - 8); | |
| 1345 | const operand: V = pad ++ std.mem.toBytes(big(c.read_seq)); | |
| 1346 | break :nonce @as(V, pv.server_iv) ^ operand; | |
| 1347 | }; | |
| 1348 | const out_buf = vp.peek(); | |
| 1349 | const cleartext_buf = if (ciphertext.len <= out_buf.len) | |
| 1350 | out_buf | |
| 1351 | else | |
| 1352 | &cleartext_stack_buffer; | |
| 1353 | const cleartext = cleartext_buf[0..ciphertext.len]; | |
| 1354 | P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_key) catch | |
| 1355 | return error.TlsBadRecordMac; | |
| 1356 | const msg = mem.trimEnd(u8, cleartext, "\x00"); | |
| 1357 | break :cleartext .{ msg[0 .. msg.len - 1], @enumFromInt(msg[msg.len - 1]) }; | |
| 1160 | c.read_seq = std.math.add(u64, c.read_seq, 1) catch return failRead(c, error.TlsSequenceOverflow); | |
| 1161 | switch (inner_ct) { | |
| 1162 | .alert => { | |
| 1163 | if (cleartext.len != 2) return failRead(c, error.TlsDecodeError); | |
| 1164 | const alert: tls.Alert = .{ | |
| 1165 | .level = @enumFromInt(cleartext[0]), | |
| 1166 | .description = @enumFromInt(cleartext[1]), | |
| 1167 | }; | |
| 1168 | switch (alert.description) { | |
| 1169 | .close_notify => { | |
| 1170 | c.received_close_notify = true; | |
| 1171 | return 0; | |
| 1358 | 1172 | }, |
| 1359 | .tls_1_2 => { | |
| 1360 | const pv = &p.tls_1_2; | |
| 1361 | const P = @TypeOf(p.*); | |
| 1362 | const message_len: u16 = record_len - P.record_iv_length - P.mac_length; | |
| 1363 | const ad = std.mem.toBytes(big(c.read_seq)) ++ | |
| 1364 | frag[in - tls.record_header_len ..][0 .. 1 + 2] ++ | |
| 1365 | std.mem.toBytes(big(message_len)); | |
| 1366 | const record_iv = frag[in..][0..P.record_iv_length].*; | |
| 1367 | in += P.record_iv_length; | |
| 1368 | const masked_read_seq = c.read_seq & | |
| 1369 | comptime std.math.shl(u64, std.math.maxInt(u64), 8 * P.record_iv_length); | |
| 1370 | const nonce: [P.AEAD.nonce_length]u8 = nonce: { | |
| 1371 | const V = @Vector(P.AEAD.nonce_length, u8); | |
| 1372 | const pad = [1]u8{0} ** (P.AEAD.nonce_length - 8); | |
| 1373 | const operand: V = pad ++ @as([8]u8, @bitCast(big(masked_read_seq))); | |
| 1374 | break :nonce @as(V, pv.server_write_IV ++ record_iv) ^ operand; | |
| 1375 | }; | |
| 1376 | const ciphertext = frag[in..][0..message_len]; | |
| 1377 | in += message_len; | |
| 1378 | const auth_tag = frag[in..][0..P.mac_length].*; | |
| 1379 | in += P.mac_length; | |
| 1380 | const out_buf = vp.peek(); | |
| 1381 | const cleartext_buf = if (message_len <= out_buf.len) | |
| 1382 | out_buf | |
| 1383 | else | |
| 1384 | &cleartext_stack_buffer; | |
| 1385 | const cleartext = cleartext_buf[0..ciphertext.len]; | |
| 1386 | P.AEAD.decrypt(cleartext, ciphertext, auth_tag, ad, nonce, pv.server_write_key) catch | |
| 1387 | return error.TlsBadRecordMac; | |
| 1388 | break :cleartext .{ cleartext, ct }; | |
| 1173 | .user_canceled => { | |
| 1174 | // TODO: handle server-side closures | |
| 1175 | return failRead(c, error.TlsUnexpectedMessage); | |
| 1389 | 1176 | }, |
| 1390 | else => unreachable, | |
| 1391 | }, | |
| 1392 | }; | |
| 1393 | c.read_seq = try std.math.add(u64, c.read_seq, 1); | |
| 1394 | switch (inner_ct) { | |
| 1395 | .alert => { | |
| 1396 | if (cleartext.len != 2) return error.TlsDecodeError; | |
| 1397 | const level: tls.AlertLevel = @enumFromInt(cleartext[0]); | |
| 1398 | const desc: tls.AlertDescription = @enumFromInt(cleartext[1]); | |
| 1399 | if (desc == .close_notify) { | |
| 1400 | c.received_close_notify = true; | |
| 1401 | c.partial_ciphertext_end = c.partial_ciphertext_idx; | |
| 1402 | return vp.total; | |
| 1403 | } | |
| 1404 | _ = level; | |
| 1405 | ||
| 1406 | try desc.toError(); | |
| 1407 | // TODO: handle server-side closures | |
| 1408 | return error.TlsUnexpectedMessage; | |
| 1409 | }, | |
| 1410 | .handshake => { | |
| 1411 | var ct_i: usize = 0; | |
| 1412 | while (true) { | |
| 1413 | const handshake_type: tls.HandshakeType = @enumFromInt(cleartext[ct_i]); | |
| 1414 | ct_i += 1; | |
| 1415 | const handshake_len = mem.readInt(u24, cleartext[ct_i..][0..3], .big); | |
| 1416 | ct_i += 3; | |
| 1417 | const next_handshake_i = ct_i + handshake_len; | |
| 1418 | if (next_handshake_i > cleartext.len) | |
| 1419 | return error.TlsBadLength; | |
| 1420 | const handshake = cleartext[ct_i..next_handshake_i]; | |
| 1421 | switch (handshake_type) { | |
| 1422 | .new_session_ticket => { | |
| 1423 | // This client implementation ignores new session tickets. | |
| 1424 | }, | |
| 1425 | .key_update => { | |
| 1426 | switch (c.application_cipher) { | |
| 1427 | inline else => |*p| { | |
| 1428 | const pv = &p.tls_1_3; | |
| 1429 | const P = @TypeOf(p.*); | |
| 1430 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.server_secret, "traffic upd", "", P.Hash.digest_length); | |
| 1431 | if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{ | |
| 1432 | .counter = key_log.serverCounter(), | |
| 1433 | .client_random = &key_log.client_random, | |
| 1434 | }, .{ | |
| 1435 | .SERVER_TRAFFIC_SECRET = &server_secret, | |
| 1436 | }); | |
| 1437 | pv.server_secret = server_secret; | |
| 1438 | pv.server_key = hkdfExpandLabel(P.Hkdf, server_secret, "key", "", P.AEAD.key_length); | |
| 1439 | pv.server_iv = hkdfExpandLabel(P.Hkdf, server_secret, "iv", "", P.AEAD.nonce_length); | |
| 1440 | }, | |
| 1441 | } | |
| 1442 | c.read_seq = 0; | |
| 1443 | ||
| 1444 | switch (@as(tls.KeyUpdateRequest, @enumFromInt(handshake[0]))) { | |
| 1445 | .update_requested => { | |
| 1446 | switch (c.application_cipher) { | |
| 1447 | inline else => |*p| { | |
| 1448 | const pv = &p.tls_1_3; | |
| 1449 | const P = @TypeOf(p.*); | |
| 1450 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.client_secret, "traffic upd", "", P.Hash.digest_length); | |
| 1451 | if (c.ssl_key_log) |*key_log| logSecrets(key_log.file, .{ | |
| 1452 | .counter = key_log.clientCounter(), | |
| 1453 | .client_random = &key_log.client_random, | |
| 1454 | }, .{ | |
| 1455 | .CLIENT_TRAFFIC_SECRET = &client_secret, | |
| 1456 | }); | |
| 1457 | pv.client_secret = client_secret; | |
| 1458 | pv.client_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length); | |
| 1459 | pv.client_iv = hkdfExpandLabel(P.Hkdf, client_secret, "iv", "", P.AEAD.nonce_length); | |
| 1460 | }, | |
| 1461 | } | |
| 1462 | c.write_seq = 0; | |
| 1463 | }, | |
| 1464 | .update_not_requested => {}, | |
| 1465 | _ => return error.TlsIllegalParameter, | |
| 1466 | } | |
| 1467 | }, | |
| 1468 | else => { | |
| 1469 | return error.TlsUnexpectedMessage; | |
| 1470 | }, | |
| 1471 | } | |
| 1472 | ct_i = next_handshake_i; | |
| 1473 | if (ct_i >= cleartext.len) break; | |
| 1474 | } | |
| 1475 | }, | |
| 1476 | .application_data => { | |
| 1477 | // Determine whether the output buffer or a stack | |
| 1478 | // buffer was used for storing the cleartext. | |
| 1479 | if (cleartext.ptr == &cleartext_stack_buffer) { | |
| 1480 | // Stack buffer was used, so we must copy to the output buffer. | |
| 1481 | if (c.partial_ciphertext_idx > c.partial_cleartext_idx) { | |
| 1482 | // We have already run out of room in iovecs. Continue | |
| 1483 | // appending to `partially_read_buffer`. | |
| 1484 | @memcpy( | |
| 1485 | c.partially_read_buffer[c.partial_ciphertext_idx..][0..cleartext.len], | |
| 1486 | cleartext, | |
| 1487 | ); | |
| 1488 | c.partial_ciphertext_idx = @intCast(c.partial_ciphertext_idx + cleartext.len); | |
| 1489 | } else { | |
| 1490 | const amt = vp.put(cleartext); | |
| 1491 | if (amt < cleartext.len) { | |
| 1492 | const rest = cleartext[amt..]; | |
| 1493 | c.partial_cleartext_idx = 0; | |
| 1494 | c.partial_ciphertext_idx = @intCast(rest.len); | |
| 1495 | @memcpy(c.partially_read_buffer[0..rest.len], rest); | |
| 1177 | else => { | |
| 1178 | c.alert = alert; | |
| 1179 | return failRead(c, error.TlsAlert); | |
| 1180 | }, | |
| 1181 | } | |
| 1182 | }, | |
| 1183 | .handshake => { | |
| 1184 | var ct_i: usize = 0; | |
| 1185 | while (true) { | |
| 1186 | const handshake_type: tls.HandshakeType = @enumFromInt(cleartext[ct_i]); | |
| 1187 | ct_i += 1; | |
| 1188 | const handshake_len = mem.readInt(u24, cleartext[ct_i..][0..3], .big); | |
| 1189 | ct_i += 3; | |
| 1190 | const next_handshake_i = ct_i + handshake_len; | |
| 1191 | if (next_handshake_i > cleartext.len) return failRead(c, error.TlsBadLength); | |
| 1192 | const handshake = cleartext[ct_i..next_handshake_i]; | |
| 1193 | switch (handshake_type) { | |
| 1194 | .new_session_ticket => { | |
| 1195 | // This client implementation ignores new session tickets. | |
| 1196 | }, | |
| 1197 | .key_update => { | |
| 1198 | switch (c.application_cipher) { | |
| 1199 | inline else => |*p| { | |
| 1200 | const pv = &p.tls_1_3; | |
| 1201 | const P = @TypeOf(p.*); | |
| 1202 | const server_secret = hkdfExpandLabel(P.Hkdf, pv.server_secret, "traffic upd", "", P.Hash.digest_length); | |
| 1203 | if (c.ssl_key_log) |key_log| logSecrets(key_log.writer, .{ | |
| 1204 | .counter = key_log.serverCounter(), | |
| 1205 | .client_random = &key_log.client_random, | |
| 1206 | }, .{ | |
| 1207 | .SERVER_TRAFFIC_SECRET = &server_secret, | |
| 1208 | }); | |
| 1209 | pv.server_secret = server_secret; | |
| 1210 | pv.server_key = hkdfExpandLabel(P.Hkdf, server_secret, "key", "", P.AEAD.key_length); | |
| 1211 | pv.server_iv = hkdfExpandLabel(P.Hkdf, server_secret, "iv", "", P.AEAD.nonce_length); | |
| 1212 | }, | |
| 1213 | } | |
| 1214 | c.read_seq = 0; | |
| 1215 | ||
| 1216 | switch (@as(tls.KeyUpdateRequest, @enumFromInt(handshake[0]))) { | |
| 1217 | .update_requested => { | |
| 1218 | switch (c.application_cipher) { | |
| 1219 | inline else => |*p| { | |
| 1220 | const pv = &p.tls_1_3; | |
| 1221 | const P = @TypeOf(p.*); | |
| 1222 | const client_secret = hkdfExpandLabel(P.Hkdf, pv.client_secret, "traffic upd", "", P.Hash.digest_length); | |
| 1223 | if (c.ssl_key_log) |key_log| logSecrets(key_log.writer, .{ | |
| 1224 | .counter = key_log.clientCounter(), | |
| 1225 | .client_random = &key_log.client_random, | |
| 1226 | }, .{ | |
| 1227 | .CLIENT_TRAFFIC_SECRET = &client_secret, | |
| 1228 | }); | |
| 1229 | pv.client_secret = client_secret; | |
| 1230 | pv.client_key = hkdfExpandLabel(P.Hkdf, client_secret, "key", "", P.AEAD.key_length); | |
| 1231 | pv.client_iv = hkdfExpandLabel(P.Hkdf, client_secret, "iv", "", P.AEAD.nonce_length); | |
| 1232 | }, | |
| 1233 | } | |
| 1234 | c.write_seq = 0; | |
| 1235 | }, | |
| 1236 | .update_not_requested => {}, | |
| 1237 | _ => return failRead(c, error.TlsIllegalParameter), | |
| 1496 | 1238 | } |
| 1497 | } | |
| 1498 | } else { | |
| 1499 | // Output buffer was used directly which means no | |
| 1500 | // memory copying needs to occur, and we can move | |
| 1501 | // on to the next ciphertext record. | |
| 1502 | vp.next(cleartext.len); | |
| 1239 | }, | |
| 1240 | else => return failRead(c, error.TlsUnexpectedMessage), | |
| 1503 | 1241 | } |
| 1504 | }, | |
| 1505 | else => return error.TlsUnexpectedMessage, | |
| 1506 | } | |
| 1507 | in = end; | |
| 1242 | ct_i = next_handshake_i; | |
| 1243 | if (ct_i >= cleartext.len) break; | |
| 1244 | } | |
| 1245 | return 0; | |
| 1246 | }, | |
| 1247 | .application_data => { | |
| 1248 | if (@intFromEnum(limit) < cleartext.len) return failRead(c, error.OutputBufferUndersize); | |
| 1249 | try w.writeAll(cleartext); | |
| 1250 | return cleartext.len; | |
| 1251 | }, | |
| 1252 | else => return failRead(c, error.TlsUnexpectedMessage), | |
| 1508 | 1253 | } |
| 1509 | 1254 | } |
| 1510 | 1255 | |
| 1511 | fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) void { | |
| 1512 | const locked = if (key_log_file.lock(.exclusive)) |_| true else |_| false; | |
| 1513 | defer if (locked) key_log_file.unlock(); | |
| 1514 | key_log_file.seekFromEnd(0) catch {}; | |
| 1515 | inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| key_log_file.deprecatedWriter().print("{s}" ++ | |
| 1256 | fn failRead(c: *Client, err: ReadError) error{ReadFailed} { | |
| 1257 | c.read_err = err; | |
| 1258 | return error.ReadFailed; | |
| 1259 | } | |
| 1260 | ||
| 1261 | fn logSecrets(w: *Writer, context: anytype, secrets: anytype) void { | |
| 1262 | inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| w.print("{s}" ++ | |
| 1516 | 1263 | (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++ |
| 1517 | 1264 | (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{ |
| 1518 | 1265 | context.client_random, |
| ... | ... | @@ -1520,62 +1267,6 @@ fn logSecrets(key_log_file: std.fs.File, context: anytype, secrets: anytype) voi |
| 1520 | 1267 | }) catch {}; |
| 1521 | 1268 | } |
| 1522 | 1269 | |
| 1523 | fn finishRead(c: *Client, frag: []const u8, in: usize, out: usize) usize { | |
| 1524 | const saved_buf = frag[in..]; | |
| 1525 | if (c.partial_ciphertext_idx > c.partial_cleartext_idx) { | |
| 1526 | // There is cleartext at the beginning already which we need to preserve. | |
| 1527 | c.partial_ciphertext_end = @intCast(c.partial_ciphertext_idx + saved_buf.len); | |
| 1528 | @memcpy(c.partially_read_buffer[c.partial_ciphertext_idx..][0..saved_buf.len], saved_buf); | |
| 1529 | } else { | |
| 1530 | c.partial_cleartext_idx = 0; | |
| 1531 | c.partial_ciphertext_idx = 0; | |
| 1532 | c.partial_ciphertext_end = @intCast(saved_buf.len); | |
| 1533 | @memcpy(c.partially_read_buffer[0..saved_buf.len], saved_buf); | |
| 1534 | } | |
| 1535 | return out; | |
| 1536 | } | |
| 1537 | ||
| 1538 | /// Note that `first` usually overlaps with `c.partially_read_buffer`. | |
| 1539 | fn finishRead2(c: *Client, first: []const u8, frag1: []const u8, out: usize) usize { | |
| 1540 | if (c.partial_ciphertext_idx > c.partial_cleartext_idx) { | |
| 1541 | // There is cleartext at the beginning already which we need to preserve. | |
| 1542 | c.partial_ciphertext_end = @intCast(c.partial_ciphertext_idx + first.len + frag1.len); | |
| 1543 | // TODO: eliminate this call to copyForwards | |
| 1544 | std.mem.copyForwards(u8, c.partially_read_buffer[c.partial_ciphertext_idx..][0..first.len], first); | |
| 1545 | @memcpy(c.partially_read_buffer[c.partial_ciphertext_idx + first.len ..][0..frag1.len], frag1); | |
| 1546 | } else { | |
| 1547 | c.partial_cleartext_idx = 0; | |
| 1548 | c.partial_ciphertext_idx = 0; | |
| 1549 | c.partial_ciphertext_end = @intCast(first.len + frag1.len); | |
| 1550 | // TODO: eliminate this call to copyForwards | |
| 1551 | std.mem.copyForwards(u8, c.partially_read_buffer[0..first.len], first); | |
| 1552 | @memcpy(c.partially_read_buffer[first.len..][0..frag1.len], frag1); | |
| 1553 | } | |
| 1554 | return out; | |
| 1555 | } | |
| 1556 | ||
| 1557 | fn limitedOverlapCopy(frag: []u8, in: usize) void { | |
| 1558 | const first = frag[in..]; | |
| 1559 | if (first.len <= in) { | |
| 1560 | // A single, non-overlapping memcpy suffices. | |
| 1561 | @memcpy(frag[0..first.len], first); | |
| 1562 | } else { | |
| 1563 | // One memcpy call would overlap, so just do this instead. | |
| 1564 | std.mem.copyForwards(u8, frag, first); | |
| 1565 | } | |
| 1566 | } | |
| 1567 | ||
| 1568 | fn straddleByte(s1: []const u8, s2: []const u8, index: usize) u8 { | |
| 1569 | if (index < s1.len) { | |
| 1570 | return s1[index]; | |
| 1571 | } else { | |
| 1572 | return s2[index - s1.len]; | |
| 1573 | } | |
| 1574 | } | |
| 1575 | ||
| 1576 | const builtin = @import("builtin"); | |
| 1577 | const native_endian = builtin.cpu.arch.endian(); | |
| 1578 | ||
| 1579 | 1270 | fn big(x: anytype) @TypeOf(x) { |
| 1580 | 1271 | return switch (native_endian) { |
| 1581 | 1272 | .big => x, |
| ... | ... | @@ -1836,81 +1527,6 @@ const CertificatePublicKey = struct { |
| 1836 | 1527 | } |
| 1837 | 1528 | }; |
| 1838 | 1529 | |
| 1839 | /// Abstraction for sending multiple byte buffers to a slice of iovecs. | |
| 1840 | const VecPut = struct { | |
| 1841 | iovecs: []const std.posix.iovec, | |
| 1842 | idx: usize = 0, | |
| 1843 | off: usize = 0, | |
| 1844 | total: usize = 0, | |
| 1845 | ||
| 1846 | /// Returns the amount actually put which is always equal to bytes.len | |
| 1847 | /// unless the vectors ran out of space. | |
| 1848 | fn put(vp: *VecPut, bytes: []const u8) usize { | |
| 1849 | if (vp.idx >= vp.iovecs.len) return 0; | |
| 1850 | var bytes_i: usize = 0; | |
| 1851 | while (true) { | |
| 1852 | const v = vp.iovecs[vp.idx]; | |
| 1853 | const dest = v.base[vp.off..v.len]; | |
| 1854 | const src = bytes[bytes_i..][0..@min(dest.len, bytes.len - bytes_i)]; | |
| 1855 | @memcpy(dest[0..src.len], src); | |
| 1856 | bytes_i += src.len; | |
| 1857 | vp.off += src.len; | |
| 1858 | if (vp.off >= v.len) { | |
| 1859 | vp.off = 0; | |
| 1860 | vp.idx += 1; | |
| 1861 | if (vp.idx >= vp.iovecs.len) { | |
| 1862 | vp.total += bytes_i; | |
| 1863 | return bytes_i; | |
| 1864 | } | |
| 1865 | } | |
| 1866 | if (bytes_i >= bytes.len) { | |
| 1867 | vp.total += bytes_i; | |
| 1868 | return bytes_i; | |
| 1869 | } | |
| 1870 | } | |
| 1871 | } | |
| 1872 | ||
| 1873 | /// Returns the next buffer that consecutive bytes can go into. | |
| 1874 | fn peek(vp: VecPut) []u8 { | |
| 1875 | if (vp.idx >= vp.iovecs.len) return &.{}; | |
| 1876 | const v = vp.iovecs[vp.idx]; | |
| 1877 | return v.base[vp.off..v.len]; | |
| 1878 | } | |
| 1879 | ||
| 1880 | // After writing to the result of peek(), one can call next() to | |
| 1881 | // advance the cursor. | |
| 1882 | fn next(vp: *VecPut, len: usize) void { | |
| 1883 | vp.total += len; | |
| 1884 | vp.off += len; | |
| 1885 | if (vp.off >= vp.iovecs[vp.idx].len) { | |
| 1886 | vp.off = 0; | |
| 1887 | vp.idx += 1; | |
| 1888 | } | |
| 1889 | } | |
| 1890 | ||
| 1891 | fn freeSize(vp: VecPut) usize { | |
| 1892 | if (vp.idx >= vp.iovecs.len) return 0; | |
| 1893 | var total: usize = 0; | |
| 1894 | total += vp.iovecs[vp.idx].len - vp.off; | |
| 1895 | if (vp.idx + 1 >= vp.iovecs.len) return total; | |
| 1896 | for (vp.iovecs[vp.idx + 1 ..]) |v| total += v.len; | |
| 1897 | return total; | |
| 1898 | } | |
| 1899 | }; | |
| 1900 | ||
| 1901 | /// Limit iovecs to a specific byte size. | |
| 1902 | fn limitVecs(iovecs: []std.posix.iovec, len: usize) []std.posix.iovec { | |
| 1903 | var bytes_left: usize = len; | |
| 1904 | for (iovecs, 0..) |*iovec, vec_i| { | |
| 1905 | if (bytes_left <= iovec.len) { | |
| 1906 | iovec.len = bytes_left; | |
| 1907 | return iovecs[0 .. vec_i + 1]; | |
| 1908 | } | |
| 1909 | bytes_left -= iovec.len; | |
| 1910 | } | |
| 1911 | return iovecs; | |
| 1912 | } | |
| 1913 | ||
| 1914 | 1530 | /// The priority order here is chosen based on what crypto algorithms Zig has |
| 1915 | 1531 | /// available in the standard library as well as what is faster. Following are |
| 1916 | 1532 | /// a few data points on the relative performance of these algorithms. |
| ... | ... | @@ -1954,7 +1570,3 @@ else |
| 1954 | 1570 | .AES_256_GCM_SHA384, |
| 1955 | 1571 | .ECDHE_RSA_WITH_AES_256_GCM_SHA384, |
| 1956 | 1572 | }); |
| 1957 | ||
| 1958 | test { | |
| 1959 | _ = StreamInterface; | |
| 1960 | } |
lib/std/fifo.zig deleted-548| ... | ... | @@ -1,548 +0,0 @@ |
| 1 | // FIFO of fixed size items | |
| 2 | // Usually used for e.g. byte buffers | |
| 3 | ||
| 4 | const std = @import("std"); | |
| 5 | const math = std.math; | |
| 6 | const mem = std.mem; | |
| 7 | const Allocator = mem.Allocator; | |
| 8 | const assert = std.debug.assert; | |
| 9 | const testing = std.testing; | |
| 10 | ||
| 11 | pub const LinearFifoBufferType = union(enum) { | |
| 12 | /// The buffer is internal to the fifo; it is of the specified size. | |
| 13 | Static: usize, | |
| 14 | ||
| 15 | /// The buffer is passed as a slice to the initialiser. | |
| 16 | Slice, | |
| 17 | ||
| 18 | /// The buffer is managed dynamically using a `mem.Allocator`. | |
| 19 | Dynamic, | |
| 20 | }; | |
| 21 | ||
| 22 | pub fn LinearFifo( | |
| 23 | comptime T: type, | |
| 24 | comptime buffer_type: LinearFifoBufferType, | |
| 25 | ) type { | |
| 26 | const autoalign = false; | |
| 27 | ||
| 28 | const powers_of_two = switch (buffer_type) { | |
| 29 | .Static => std.math.isPowerOfTwo(buffer_type.Static), | |
| 30 | .Slice => false, // Any size slice could be passed in | |
| 31 | .Dynamic => true, // This could be configurable in future | |
| 32 | }; | |
| 33 | ||
| 34 | return struct { | |
| 35 | allocator: if (buffer_type == .Dynamic) Allocator else void, | |
| 36 | buf: if (buffer_type == .Static) [buffer_type.Static]T else []T, | |
| 37 | head: usize, | |
| 38 | count: usize, | |
| 39 | ||
| 40 | const Self = @This(); | |
| 41 | pub const Reader = std.io.GenericReader(*Self, error{}, readFn); | |
| 42 | pub const Writer = std.io.GenericWriter(*Self, error{OutOfMemory}, appendWrite); | |
| 43 | ||
| 44 | // Type of Self argument for slice operations. | |
| 45 | // If buffer is inline (Static) then we need to ensure we haven't | |
| 46 | // returned a slice into a copy on the stack | |
| 47 | const SliceSelfArg = if (buffer_type == .Static) *Self else Self; | |
| 48 | ||
| 49 | pub const init = switch (buffer_type) { | |
| 50 | .Static => initStatic, | |
| 51 | .Slice => initSlice, | |
| 52 | .Dynamic => initDynamic, | |
| 53 | }; | |
| 54 | ||
| 55 | fn initStatic() Self { | |
| 56 | comptime assert(buffer_type == .Static); | |
| 57 | return .{ | |
| 58 | .allocator = {}, | |
| 59 | .buf = undefined, | |
| 60 | .head = 0, | |
| 61 | .count = 0, | |
| 62 | }; | |
| 63 | } | |
| 64 | ||
| 65 | fn initSlice(buf: []T) Self { | |
| 66 | comptime assert(buffer_type == .Slice); | |
| 67 | return .{ | |
| 68 | .allocator = {}, | |
| 69 | .buf = buf, | |
| 70 | .head = 0, | |
| 71 | .count = 0, | |
| 72 | }; | |
| 73 | } | |
| 74 | ||
| 75 | fn initDynamic(allocator: Allocator) Self { | |
| 76 | comptime assert(buffer_type == .Dynamic); | |
| 77 | return .{ | |
| 78 | .allocator = allocator, | |
| 79 | .buf = &.{}, | |
| 80 | .head = 0, | |
| 81 | .count = 0, | |
| 82 | }; | |
| 83 | } | |
| 84 | ||
| 85 | pub fn deinit(self: Self) void { | |
| 86 | if (buffer_type == .Dynamic) self.allocator.free(self.buf); | |
| 87 | } | |
| 88 | ||
| 89 | pub fn realign(self: *Self) void { | |
| 90 | if (self.buf.len - self.head >= self.count) { | |
| 91 | mem.copyForwards(T, self.buf[0..self.count], self.buf[self.head..][0..self.count]); | |
| 92 | self.head = 0; | |
| 93 | } else { | |
| 94 | var tmp: [4096 / 2 / @sizeOf(T)]T = undefined; | |
| 95 | ||
| 96 | while (self.head != 0) { | |
| 97 | const n = @min(self.head, tmp.len); | |
| 98 | const m = self.buf.len - n; | |
| 99 | @memcpy(tmp[0..n], self.buf[0..n]); | |
| 100 | mem.copyForwards(T, self.buf[0..m], self.buf[n..][0..m]); | |
| 101 | @memcpy(self.buf[m..][0..n], tmp[0..n]); | |
| 102 | self.head -= n; | |
| 103 | } | |
| 104 | } | |
| 105 | { // set unused area to undefined | |
| 106 | const unused = mem.sliceAsBytes(self.buf[self.count..]); | |
| 107 | @memset(unused, undefined); | |
| 108 | } | |
| 109 | } | |
| 110 | ||
| 111 | /// Reduce allocated capacity to `size`. | |
| 112 | pub fn shrink(self: *Self, size: usize) void { | |
| 113 | assert(size >= self.count); | |
| 114 | if (buffer_type == .Dynamic) { | |
| 115 | self.realign(); | |
| 116 | self.buf = self.allocator.realloc(self.buf, size) catch |e| switch (e) { | |
| 117 | error.OutOfMemory => return, // no problem, capacity is still correct then. | |
| 118 | }; | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | /// Ensure that the buffer can fit at least `size` items | |
| 123 | pub fn ensureTotalCapacity(self: *Self, size: usize) !void { | |
| 124 | if (self.buf.len >= size) return; | |
| 125 | if (buffer_type == .Dynamic) { | |
| 126 | self.realign(); | |
| 127 | const new_size = if (powers_of_two) math.ceilPowerOfTwo(usize, size) catch return error.OutOfMemory else size; | |
| 128 | self.buf = try self.allocator.realloc(self.buf, new_size); | |
| 129 | } else { | |
| 130 | return error.OutOfMemory; | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | /// Makes sure at least `size` items are unused | |
| 135 | pub fn ensureUnusedCapacity(self: *Self, size: usize) error{OutOfMemory}!void { | |
| 136 | if (self.writableLength() >= size) return; | |
| 137 | ||
| 138 | return try self.ensureTotalCapacity(math.add(usize, self.count, size) catch return error.OutOfMemory); | |
| 139 | } | |
| 140 | ||
| 141 | /// Returns number of items currently in fifo | |
| 142 | pub fn readableLength(self: Self) usize { | |
| 143 | return self.count; | |
| 144 | } | |
| 145 | ||
| 146 | /// Returns a writable slice from the 'read' end of the fifo | |
| 147 | fn readableSliceMut(self: SliceSelfArg, offset: usize) []T { | |
| 148 | if (offset > self.count) return &[_]T{}; | |
| 149 | ||
| 150 | var start = self.head + offset; | |
| 151 | if (start >= self.buf.len) { | |
| 152 | start -= self.buf.len; | |
| 153 | return self.buf[start .. start + (self.count - offset)]; | |
| 154 | } else { | |
| 155 | const end = @min(self.head + self.count, self.buf.len); | |
| 156 | return self.buf[start..end]; | |
| 157 | } | |
| 158 | } | |
| 159 | ||
| 160 | /// Returns a readable slice from `offset` | |
| 161 | pub fn readableSlice(self: SliceSelfArg, offset: usize) []const T { | |
| 162 | return self.readableSliceMut(offset); | |
| 163 | } | |
| 164 | ||
| 165 | pub fn readableSliceOfLen(self: *Self, len: usize) []const T { | |
| 166 | assert(len <= self.count); | |
| 167 | const buf = self.readableSlice(0); | |
| 168 | if (buf.len >= len) { | |
| 169 | return buf[0..len]; | |
| 170 | } else { | |
| 171 | self.realign(); | |
| 172 | return self.readableSlice(0)[0..len]; | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | /// Discard first `count` items in the fifo | |
| 177 | pub fn discard(self: *Self, count: usize) void { | |
| 178 | assert(count <= self.count); | |
| 179 | { // set old range to undefined. Note: may be wrapped around | |
| 180 | const slice = self.readableSliceMut(0); | |
| 181 | if (slice.len >= count) { | |
| 182 | const unused = mem.sliceAsBytes(slice[0..count]); | |
| 183 | @memset(unused, undefined); | |
| 184 | } else { | |
| 185 | const unused = mem.sliceAsBytes(slice[0..]); | |
| 186 | @memset(unused, undefined); | |
| 187 | const unused2 = mem.sliceAsBytes(self.readableSliceMut(slice.len)[0 .. count - slice.len]); | |
| 188 | @memset(unused2, undefined); | |
| 189 | } | |
| 190 | } | |
| 191 | if (autoalign and self.count == count) { | |
| 192 | self.head = 0; | |
| 193 | self.count = 0; | |
| 194 | } else { | |
| 195 | var head = self.head + count; | |
| 196 | if (powers_of_two) { | |
| 197 | // Note it is safe to do a wrapping subtract as | |
| 198 | // bitwise & with all 1s is a noop | |
| 199 | head &= self.buf.len -% 1; | |
| 200 | } else { | |
| 201 | head %= self.buf.len; | |
| 202 | } | |
| 203 | self.head = head; | |
| 204 | self.count -= count; | |
| 205 | } | |
| 206 | } | |
| 207 | ||
| 208 | /// Read the next item from the fifo | |
| 209 | pub fn readItem(self: *Self) ?T { | |
| 210 | if (self.count == 0) return null; | |
| 211 | ||
| 212 | const c = self.buf[self.head]; | |
| 213 | self.discard(1); | |
| 214 | return c; | |
| 215 | } | |
| 216 | ||
| 217 | /// Read data from the fifo into `dst`, returns number of items copied. | |
| 218 | pub fn read(self: *Self, dst: []T) usize { | |
| 219 | var dst_left = dst; | |
| 220 | ||
| 221 | while (dst_left.len > 0) { | |
| 222 | const slice = self.readableSlice(0); | |
| 223 | if (slice.len == 0) break; | |
| 224 | const n = @min(slice.len, dst_left.len); | |
| 225 | @memcpy(dst_left[0..n], slice[0..n]); | |
| 226 | self.discard(n); | |
| 227 | dst_left = dst_left[n..]; | |
| 228 | } | |
| 229 | ||
| 230 | return dst.len - dst_left.len; | |
| 231 | } | |
| 232 | ||
| 233 | /// Same as `read` except it returns an error union | |
| 234 | /// The purpose of this function existing is to match `std.io.GenericReader` API. | |
| 235 | fn readFn(self: *Self, dest: []u8) error{}!usize { | |
| 236 | return self.read(dest); | |
| 237 | } | |
| 238 | ||
| 239 | pub fn reader(self: *Self) Reader { | |
| 240 | return .{ .context = self }; | |
| 241 | } | |
| 242 | ||
| 243 | /// Returns number of items available in fifo | |
| 244 | pub fn writableLength(self: Self) usize { | |
| 245 | return self.buf.len - self.count; | |
| 246 | } | |
| 247 | ||
| 248 | /// Returns the first section of writable buffer. | |
| 249 | /// Note that this may be of length 0 | |
| 250 | pub fn writableSlice(self: SliceSelfArg, offset: usize) []T { | |
| 251 | if (offset > self.buf.len) return &[_]T{}; | |
| 252 | ||
| 253 | const tail = self.head + offset + self.count; | |
| 254 | if (tail < self.buf.len) { | |
| 255 | return self.buf[tail..]; | |
| 256 | } else { | |
| 257 | return self.buf[tail - self.buf.len ..][0 .. self.writableLength() - offset]; | |
| 258 | } | |
| 259 | } | |
| 260 | ||
| 261 | /// Returns a writable buffer of at least `size` items, allocating memory as needed. | |
| 262 | /// Use `fifo.update` once you've written data to it. | |
| 263 | pub fn writableWithSize(self: *Self, size: usize) ![]T { | |
| 264 | try self.ensureUnusedCapacity(size); | |
| 265 | ||
| 266 | // try to avoid realigning buffer | |
| 267 | var slice = self.writableSlice(0); | |
| 268 | if (slice.len < size) { | |
| 269 | self.realign(); | |
| 270 | slice = self.writableSlice(0); | |
| 271 | } | |
| 272 | return slice; | |
| 273 | } | |
| 274 | ||
| 275 | /// Update the tail location of the buffer (usually follows use of writable/writableWithSize) | |
| 276 | pub fn update(self: *Self, count: usize) void { | |
| 277 | assert(self.count + count <= self.buf.len); | |
| 278 | self.count += count; | |
| 279 | } | |
| 280 | ||
| 281 | /// Appends the data in `src` to the fifo. | |
| 282 | /// You must have ensured there is enough space. | |
| 283 | pub fn writeAssumeCapacity(self: *Self, src: []const T) void { | |
| 284 | assert(self.writableLength() >= src.len); | |
| 285 | ||
| 286 | var src_left = src; | |
| 287 | while (src_left.len > 0) { | |
| 288 | const writable_slice = self.writableSlice(0); | |
| 289 | assert(writable_slice.len != 0); | |
| 290 | const n = @min(writable_slice.len, src_left.len); | |
| 291 | @memcpy(writable_slice[0..n], src_left[0..n]); | |
| 292 | self.update(n); | |
| 293 | src_left = src_left[n..]; | |
| 294 | } | |
| 295 | } | |
| 296 | ||
| 297 | /// Write a single item to the fifo | |
| 298 | pub fn writeItem(self: *Self, item: T) !void { | |
| 299 | try self.ensureUnusedCapacity(1); | |
| 300 | return self.writeItemAssumeCapacity(item); | |
| 301 | } | |
| 302 | ||
| 303 | pub fn writeItemAssumeCapacity(self: *Self, item: T) void { | |
| 304 | var tail = self.head + self.count; | |
| 305 | if (powers_of_two) { | |
| 306 | tail &= self.buf.len - 1; | |
| 307 | } else { | |
| 308 | tail %= self.buf.len; | |
| 309 | } | |
| 310 | self.buf[tail] = item; | |
| 311 | self.update(1); | |
| 312 | } | |
| 313 | ||
| 314 | /// Appends the data in `src` to the fifo. | |
| 315 | /// Allocates more memory as necessary | |
| 316 | pub fn write(self: *Self, src: []const T) !void { | |
| 317 | try self.ensureUnusedCapacity(src.len); | |
| 318 | ||
| 319 | return self.writeAssumeCapacity(src); | |
| 320 | } | |
| 321 | ||
| 322 | /// Same as `write` except it returns the number of bytes written, which is always the same | |
| 323 | /// as `bytes.len`. The purpose of this function existing is to match `std.io.GenericWriter` API. | |
| 324 | fn appendWrite(self: *Self, bytes: []const u8) error{OutOfMemory}!usize { | |
| 325 | try self.write(bytes); | |
| 326 | return bytes.len; | |
| 327 | } | |
| 328 | ||
| 329 | pub fn writer(self: *Self) Writer { | |
| 330 | return .{ .context = self }; | |
| 331 | } | |
| 332 | ||
| 333 | /// Make `count` items available before the current read location | |
| 334 | fn rewind(self: *Self, count: usize) void { | |
| 335 | assert(self.writableLength() >= count); | |
| 336 | ||
| 337 | var head = self.head + (self.buf.len - count); | |
| 338 | if (powers_of_two) { | |
| 339 | head &= self.buf.len - 1; | |
| 340 | } else { | |
| 341 | head %= self.buf.len; | |
| 342 | } | |
| 343 | self.head = head; | |
| 344 | self.count += count; | |
| 345 | } | |
| 346 | ||
| 347 | /// Place data back into the read stream | |
| 348 | pub fn unget(self: *Self, src: []const T) !void { | |
| 349 | try self.ensureUnusedCapacity(src.len); | |
| 350 | ||
| 351 | self.rewind(src.len); | |
| 352 | ||
| 353 | const slice = self.readableSliceMut(0); | |
| 354 | if (src.len < slice.len) { | |
| 355 | @memcpy(slice[0..src.len], src); | |
| 356 | } else { | |
| 357 | @memcpy(slice, src[0..slice.len]); | |
| 358 | const slice2 = self.readableSliceMut(slice.len); | |
| 359 | @memcpy(slice2[0 .. src.len - slice.len], src[slice.len..]); | |
| 360 | } | |
| 361 | } | |
| 362 | ||
| 363 | /// Returns the item at `offset`. | |
| 364 | /// Asserts offset is within bounds. | |
| 365 | pub fn peekItem(self: Self, offset: usize) T { | |
| 366 | assert(offset < self.count); | |
| 367 | ||
| 368 | var index = self.head + offset; | |
| 369 | if (powers_of_two) { | |
| 370 | index &= self.buf.len - 1; | |
| 371 | } else { | |
| 372 | index %= self.buf.len; | |
| 373 | } | |
| 374 | return self.buf[index]; | |
| 375 | } | |
| 376 | ||
| 377 | /// Pump data from a reader into a writer. | |
| 378 | /// Stops when reader returns 0 bytes (EOF). | |
| 379 | /// Buffer size must be set before calling; a buffer length of 0 is invalid. | |
| 380 | pub fn pump(self: *Self, src_reader: anytype, dest_writer: anytype) !void { | |
| 381 | assert(self.buf.len > 0); | |
| 382 | while (true) { | |
| 383 | if (self.writableLength() > 0) { | |
| 384 | const n = try src_reader.read(self.writableSlice(0)); | |
| 385 | if (n == 0) break; // EOF | |
| 386 | self.update(n); | |
| 387 | } | |
| 388 | self.discard(try dest_writer.write(self.readableSlice(0))); | |
| 389 | } | |
| 390 | // flush remaining data | |
| 391 | while (self.readableLength() > 0) { | |
| 392 | self.discard(try dest_writer.write(self.readableSlice(0))); | |
| 393 | } | |
| 394 | } | |
| 395 | ||
| 396 | pub fn toOwnedSlice(self: *Self) Allocator.Error![]T { | |
| 397 | if (self.head != 0) self.realign(); | |
| 398 | assert(self.head == 0); | |
| 399 | assert(self.count <= self.buf.len); | |
| 400 | const allocator = self.allocator; | |
| 401 | if (allocator.resize(self.buf, self.count)) { | |
| 402 | const result = self.buf[0..self.count]; | |
| 403 | self.* = Self.init(allocator); | |
| 404 | return result; | |
| 405 | } | |
| 406 | const new_memory = try allocator.dupe(T, self.buf[0..self.count]); | |
| 407 | allocator.free(self.buf); | |
| 408 | self.* = Self.init(allocator); | |
| 409 | return new_memory; | |
| 410 | } | |
| 411 | }; | |
| 412 | } | |
| 413 | ||
| 414 | test "LinearFifo(u8, .Dynamic) discard(0) from empty buffer should not error on overflow" { | |
| 415 | var fifo = LinearFifo(u8, .Dynamic).init(testing.allocator); | |
| 416 | defer fifo.deinit(); | |
| 417 | ||
| 418 | // If overflow is not explicitly allowed this will crash in debug / safe mode | |
| 419 | fifo.discard(0); | |
| 420 | } | |
| 421 | ||
| 422 | test "LinearFifo(u8, .Dynamic)" { | |
| 423 | var fifo = LinearFifo(u8, .Dynamic).init(testing.allocator); | |
| 424 | defer fifo.deinit(); | |
| 425 | ||
| 426 | try fifo.write("HELLO"); | |
| 427 | try testing.expectEqual(@as(usize, 5), fifo.readableLength()); | |
| 428 | try testing.expectEqualSlices(u8, "HELLO", fifo.readableSlice(0)); | |
| 429 | ||
| 430 | { | |
| 431 | var i: usize = 0; | |
| 432 | while (i < 5) : (i += 1) { | |
| 433 | try fifo.write(&[_]u8{fifo.peekItem(i)}); | |
| 434 | } | |
| 435 | try testing.expectEqual(@as(usize, 10), fifo.readableLength()); | |
| 436 | try testing.expectEqualSlices(u8, "HELLOHELLO", fifo.readableSlice(0)); | |
| 437 | } | |
| 438 | ||
| 439 | { | |
| 440 | try testing.expectEqual(@as(u8, 'H'), fifo.readItem().?); | |
| 441 | try testing.expectEqual(@as(u8, 'E'), fifo.readItem().?); | |
| 442 | try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?); | |
| 443 | try testing.expectEqual(@as(u8, 'L'), fifo.readItem().?); | |
| 444 | try testing.expectEqual(@as(u8, 'O'), fifo.readItem().?); | |
| 445 | } | |
| 446 | try testing.expectEqual(@as(usize, 5), fifo.readableLength()); | |
| 447 | ||
| 448 | { // Writes that wrap around | |
| 449 | try testing.expectEqual(@as(usize, 11), fifo.writableLength()); | |
| 450 | try testing.expectEqual(@as(usize, 6), fifo.writableSlice(0).len); | |
| 451 | fifo.writeAssumeCapacity("6<chars<11"); | |
| 452 | try testing.expectEqualSlices(u8, "HELLO6<char", fifo.readableSlice(0)); | |
| 453 | try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(11)); | |
| 454 | try testing.expectEqualSlices(u8, "11", fifo.readableSlice(13)); | |
| 455 | try testing.expectEqualSlices(u8, "", fifo.readableSlice(15)); | |
| 456 | fifo.discard(11); | |
| 457 | try testing.expectEqualSlices(u8, "s<11", fifo.readableSlice(0)); | |
| 458 | fifo.discard(4); | |
| 459 | try testing.expectEqual(@as(usize, 0), fifo.readableLength()); | |
| 460 | } | |
| 461 | ||
| 462 | { | |
| 463 | const buf = try fifo.writableWithSize(12); | |
| 464 | try testing.expectEqual(@as(usize, 12), buf.len); | |
| 465 | var i: u8 = 0; | |
| 466 | while (i < 10) : (i += 1) { | |
| 467 | buf[i] = i + 'a'; | |
| 468 | } | |
| 469 | fifo.update(10); | |
| 470 | try testing.expectEqualSlices(u8, "abcdefghij", fifo.readableSlice(0)); | |
| 471 | } | |
| 472 | ||
| 473 | { | |
| 474 | try fifo.unget("prependedstring"); | |
| 475 | var result: [30]u8 = undefined; | |
| 476 | try testing.expectEqualSlices(u8, "prependedstringabcdefghij", result[0..fifo.read(&result)]); | |
| 477 | try fifo.unget("b"); | |
| 478 | try fifo.unget("a"); | |
| 479 | try testing.expectEqualSlices(u8, "ab", result[0..fifo.read(&result)]); | |
| 480 | } | |
| 481 | ||
| 482 | fifo.shrink(0); | |
| 483 | ||
| 484 | { | |
| 485 | try fifo.writer().print("{s}, {s}!", .{ "Hello", "World" }); | |
| 486 | var result: [30]u8 = undefined; | |
| 487 | try testing.expectEqualSlices(u8, "Hello, World!", result[0..fifo.read(&result)]); | |
| 488 | try testing.expectEqual(@as(usize, 0), fifo.readableLength()); | |
| 489 | } | |
| 490 | ||
| 491 | { | |
| 492 | try fifo.writer().writeAll("This is a test"); | |
| 493 | var result: [30]u8 = undefined; | |
| 494 | try testing.expectEqualSlices(u8, "This", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?); | |
| 495 | try testing.expectEqualSlices(u8, "is", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?); | |
| 496 | try testing.expectEqualSlices(u8, "a", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?); | |
| 497 | try testing.expectEqualSlices(u8, "test", (try fifo.reader().readUntilDelimiterOrEof(&result, ' ')).?); | |
| 498 | } | |
| 499 | ||
| 500 | { | |
| 501 | try fifo.ensureTotalCapacity(1); | |
| 502 | var in_fbs = std.io.fixedBufferStream("pump test"); | |
| 503 | var out_buf: [50]u8 = undefined; | |
| 504 | var out_fbs = std.io.fixedBufferStream(&out_buf); | |
| 505 | try fifo.pump(in_fbs.reader(), out_fbs.writer()); | |
| 506 | try testing.expectEqualSlices(u8, in_fbs.buffer, out_fbs.getWritten()); | |
| 507 | } | |
| 508 | } | |
| 509 | ||
| 510 | test LinearFifo { | |
| 511 | inline for ([_]type{ u1, u8, u16, u64 }) |T| { | |
| 512 | inline for ([_]LinearFifoBufferType{ LinearFifoBufferType{ .Static = 32 }, .Slice, .Dynamic }) |bt| { | |
| 513 | const FifoType = LinearFifo(T, bt); | |
| 514 | var buf: if (bt == .Slice) [32]T else void = undefined; | |
| 515 | var fifo = switch (bt) { | |
| 516 | .Static => FifoType.init(), | |
| 517 | .Slice => FifoType.init(buf[0..]), | |
| 518 | .Dynamic => FifoType.init(testing.allocator), | |
| 519 | }; | |
| 520 | defer fifo.deinit(); | |
| 521 | ||
| 522 | try fifo.write(&[_]T{ 0, 1, 1, 0, 1 }); | |
| 523 | try testing.expectEqual(@as(usize, 5), fifo.readableLength()); | |
| 524 | ||
| 525 | { | |
| 526 | try testing.expectEqual(@as(T, 0), fifo.readItem().?); | |
| 527 | try testing.expectEqual(@as(T, 1), fifo.readItem().?); | |
| 528 | try testing.expectEqual(@as(T, 1), fifo.readItem().?); | |
| 529 | try testing.expectEqual(@as(T, 0), fifo.readItem().?); | |
| 530 | try testing.expectEqual(@as(T, 1), fifo.readItem().?); | |
| 531 | try testing.expectEqual(@as(usize, 0), fifo.readableLength()); | |
| 532 | } | |
| 533 | ||
| 534 | { | |
| 535 | try fifo.writeItem(1); | |
| 536 | try fifo.writeItem(1); | |
| 537 | try fifo.writeItem(1); | |
| 538 | try testing.expectEqual(@as(usize, 3), fifo.readableLength()); | |
| 539 | } | |
| 540 | ||
| 541 | { | |
| 542 | var readBuf: [3]T = undefined; | |
| 543 | const n = fifo.read(&readBuf); | |
| 544 | try testing.expectEqual(@as(usize, 3), n); // NOTE: It should be the number of items. | |
| 545 | } | |
| 546 | } | |
| 547 | } | |
| 548 | } |
lib/std/fs/File.zig+2-4| ... | ... | @@ -1351,8 +1351,7 @@ pub const Reader = struct { |
| 1351 | 1351 | } |
| 1352 | 1352 | r.pos += n; |
| 1353 | 1353 | if (n > data_size) { |
| 1354 | io_reader.seek = 0; | |
| 1355 | io_reader.end = n - data_size; | |
| 1354 | io_reader.end += n - data_size; | |
| 1356 | 1355 | return data_size; |
| 1357 | 1356 | } |
| 1358 | 1357 | return n; |
| ... | ... | @@ -1386,8 +1385,7 @@ pub const Reader = struct { |
| 1386 | 1385 | } |
| 1387 | 1386 | r.pos += n; |
| 1388 | 1387 | if (n > data_size) { |
| 1389 | io_reader.seek = 0; | |
| 1390 | io_reader.end = n - data_size; | |
| 1388 | io_reader.end += n - data_size; | |
| 1391 | 1389 | return data_size; |
| 1392 | 1390 | } |
| 1393 | 1391 | return n; |
lib/std/http.zig+820-56| ... | ... | @@ -1,14 +1,14 @@ |
| 1 | 1 | const builtin = @import("builtin"); |
| 2 | 2 | const std = @import("std.zig"); |
| 3 | 3 | const assert = std.debug.assert; |
| 4 | const Writer = std.Io.Writer; | |
| 5 | const File = std.fs.File; | |
| 4 | 6 | |
| 5 | 7 | pub const Client = @import("http/Client.zig"); |
| 6 | 8 | pub const Server = @import("http/Server.zig"); |
| 7 | pub const protocol = @import("http/protocol.zig"); | |
| 8 | 9 | pub const HeadParser = @import("http/HeadParser.zig"); |
| 9 | 10 | pub const ChunkParser = @import("http/ChunkParser.zig"); |
| 10 | 11 | pub const HeaderIterator = @import("http/HeaderIterator.zig"); |
| 11 | pub const WebSocket = @import("http/WebSocket.zig"); | |
| 12 | 12 | |
| 13 | 13 | pub const Version = enum { |
| 14 | 14 | @"HTTP/1.0", |
| ... | ... | @@ -20,51 +20,32 @@ pub const Version = enum { |
| 20 | 20 | /// https://datatracker.ietf.org/doc/html/rfc7231#section-4 Initial definition |
| 21 | 21 | /// |
| 22 | 22 | /// https://datatracker.ietf.org/doc/html/rfc5789#section-2 PATCH |
| 23 | pub const Method = enum(u64) { | |
| 24 | GET = parse("GET"), | |
| 25 | HEAD = parse("HEAD"), | |
| 26 | POST = parse("POST"), | |
| 27 | PUT = parse("PUT"), | |
| 28 | DELETE = parse("DELETE"), | |
| 29 | CONNECT = parse("CONNECT"), | |
| 30 | OPTIONS = parse("OPTIONS"), | |
| 31 | TRACE = parse("TRACE"), | |
| 32 | PATCH = parse("PATCH"), | |
| 33 | ||
| 34 | _, | |
| 35 | ||
| 36 | /// Converts `s` into a type that may be used as a `Method` field. | |
| 37 | /// Asserts that `s` is 24 or fewer bytes. | |
| 38 | pub fn parse(s: []const u8) u64 { | |
| 39 | var x: u64 = 0; | |
| 40 | const len = @min(s.len, @sizeOf(@TypeOf(x))); | |
| 41 | @memcpy(std.mem.asBytes(&x)[0..len], s[0..len]); | |
| 42 | return x; | |
| 43 | } | |
| 44 | ||
| 45 | pub fn format(self: Method, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 46 | const bytes: []const u8 = @ptrCast(&@intFromEnum(self)); | |
| 47 | const str = std.mem.sliceTo(bytes, 0); | |
| 48 | try w.writeAll(str); | |
| 49 | } | |
| 23 | pub const Method = enum { | |
| 24 | GET, | |
| 25 | HEAD, | |
| 26 | POST, | |
| 27 | PUT, | |
| 28 | DELETE, | |
| 29 | CONNECT, | |
| 30 | OPTIONS, | |
| 31 | TRACE, | |
| 32 | PATCH, | |
| 50 | 33 | |
| 51 | 34 | /// Returns true if a request of this method is allowed to have a body |
| 52 | 35 | /// Actual behavior from servers may vary and should still be checked |
| 53 | pub fn requestHasBody(self: Method) bool { | |
| 54 | return switch (self) { | |
| 36 | pub fn requestHasBody(m: Method) bool { | |
| 37 | return switch (m) { | |
| 55 | 38 | .POST, .PUT, .PATCH => true, |
| 56 | 39 | .GET, .HEAD, .DELETE, .CONNECT, .OPTIONS, .TRACE => false, |
| 57 | else => true, | |
| 58 | 40 | }; |
| 59 | 41 | } |
| 60 | 42 | |
| 61 | 43 | /// Returns true if a response to this method is allowed to have a body |
| 62 | 44 | /// Actual behavior from clients may vary and should still be checked |
| 63 | pub fn responseHasBody(self: Method) bool { | |
| 64 | return switch (self) { | |
| 45 | pub fn responseHasBody(m: Method) bool { | |
| 46 | return switch (m) { | |
| 65 | 47 | .GET, .POST, .DELETE, .CONNECT, .OPTIONS, .PATCH => true, |
| 66 | 48 | .HEAD, .PUT, .TRACE => false, |
| 67 | else => true, | |
| 68 | 49 | }; |
| 69 | 50 | } |
| 70 | 51 | |
| ... | ... | @@ -73,11 +54,10 @@ pub const Method = enum(u64) { |
| 73 | 54 | /// https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP |
| 74 | 55 | /// |
| 75 | 56 | /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1 |
| 76 | pub fn safe(self: Method) bool { | |
| 77 | return switch (self) { | |
| 57 | pub fn safe(m: Method) bool { | |
| 58 | return switch (m) { | |
| 78 | 59 | .GET, .HEAD, .OPTIONS, .TRACE => true, |
| 79 | 60 | .POST, .PUT, .DELETE, .CONNECT, .PATCH => false, |
| 80 | else => false, | |
| 81 | 61 | }; |
| 82 | 62 | } |
| 83 | 63 | |
| ... | ... | @@ -88,11 +68,10 @@ pub const Method = enum(u64) { |
| 88 | 68 | /// https://developer.mozilla.org/en-US/docs/Glossary/Idempotent |
| 89 | 69 | /// |
| 90 | 70 | /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2 |
| 91 | pub fn idempotent(self: Method) bool { | |
| 92 | return switch (self) { | |
| 71 | pub fn idempotent(m: Method) bool { | |
| 72 | return switch (m) { | |
| 93 | 73 | .GET, .HEAD, .PUT, .DELETE, .OPTIONS, .TRACE => true, |
| 94 | 74 | .CONNECT, .POST, .PATCH => false, |
| 95 | else => false, | |
| 96 | 75 | }; |
| 97 | 76 | } |
| 98 | 77 | |
| ... | ... | @@ -102,11 +81,10 @@ pub const Method = enum(u64) { |
| 102 | 81 | /// https://developer.mozilla.org/en-US/docs/Glossary/cacheable |
| 103 | 82 | /// |
| 104 | 83 | /// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3 |
| 105 | pub fn cacheable(self: Method) bool { | |
| 106 | return switch (self) { | |
| 84 | pub fn cacheable(m: Method) bool { | |
| 85 | return switch (m) { | |
| 107 | 86 | .GET, .HEAD => true, |
| 108 | 87 | .POST, .PUT, .DELETE, .CONNECT, .OPTIONS, .TRACE, .PATCH => false, |
| 109 | else => false, | |
| 110 | 88 | }; |
| 111 | 89 | } |
| 112 | 90 | }; |
| ... | ... | @@ -296,13 +274,24 @@ pub const TransferEncoding = enum { |
| 296 | 274 | }; |
| 297 | 275 | |
| 298 | 276 | pub const ContentEncoding = enum { |
| 299 | identity, | |
| 300 | compress, | |
| 301 | @"x-compress", | |
| 302 | deflate, | |
| 303 | gzip, | |
| 304 | @"x-gzip", | |
| 305 | 277 | zstd, |
| 278 | gzip, | |
| 279 | deflate, | |
| 280 | compress, | |
| 281 | identity, | |
| 282 | ||
| 283 | pub fn fromString(s: []const u8) ?ContentEncoding { | |
| 284 | const map = std.StaticStringMap(ContentEncoding).initComptime(.{ | |
| 285 | .{ "zstd", .zstd }, | |
| 286 | .{ "gzip", .gzip }, | |
| 287 | .{ "x-gzip", .gzip }, | |
| 288 | .{ "deflate", .deflate }, | |
| 289 | .{ "compress", .compress }, | |
| 290 | .{ "x-compress", .compress }, | |
| 291 | .{ "identity", .identity }, | |
| 292 | }); | |
| 293 | return map.get(s); | |
| 294 | } | |
| 306 | 295 | }; |
| 307 | 296 | |
| 308 | 297 | pub const Connection = enum { |
| ... | ... | @@ -315,15 +304,790 @@ pub const Header = struct { |
| 315 | 304 | value: []const u8, |
| 316 | 305 | }; |
| 317 | 306 | |
| 307 | pub const Reader = struct { | |
| 308 | in: *std.Io.Reader, | |
| 309 | /// This is preallocated memory that might be used by `bodyReader`. That | |
| 310 | /// function might return a pointer to this field, or a different | |
| 311 | /// `*std.Io.Reader`. Advisable to not access this field directly. | |
| 312 | interface: std.Io.Reader, | |
| 313 | /// Keeps track of whether the stream is ready to accept a new request, | |
| 314 | /// making invalid API usage cause assertion failures rather than HTTP | |
| 315 | /// protocol violations. | |
| 316 | state: State, | |
| 317 | /// HTTP trailer bytes. These are at the end of a transfer-encoding: | |
| 318 | /// chunked message. This data is available only after calling one of the | |
| 319 | /// "end" functions and points to data inside the buffer of `in`, and is | |
| 320 | /// therefore invalidated on the next call to `receiveHead`, or any other | |
| 321 | /// read from `in`. | |
| 322 | trailers: []const u8 = &.{}, | |
| 323 | body_err: ?BodyError = null, | |
| 324 | ||
| 325 | pub const RemainingChunkLen = enum(u64) { | |
| 326 | head = 0, | |
| 327 | n = 1, | |
| 328 | rn = 2, | |
| 329 | _, | |
| 330 | ||
| 331 | pub fn init(integer: u64) RemainingChunkLen { | |
| 332 | return @enumFromInt(integer); | |
| 333 | } | |
| 334 | ||
| 335 | pub fn int(rcl: RemainingChunkLen) u64 { | |
| 336 | return @intFromEnum(rcl); | |
| 337 | } | |
| 338 | }; | |
| 339 | ||
| 340 | pub const State = union(enum) { | |
| 341 | /// The stream is available to be used for the first time, or reused. | |
| 342 | ready, | |
| 343 | received_head, | |
| 344 | /// The stream goes until the connection is closed. | |
| 345 | body_none, | |
| 346 | body_remaining_content_length: u64, | |
| 347 | body_remaining_chunk_len: RemainingChunkLen, | |
| 348 | /// The stream would be eligible for another HTTP request, however the | |
| 349 | /// client and server did not negotiate a persistent connection. | |
| 350 | closing, | |
| 351 | }; | |
| 352 | ||
| 353 | pub const BodyError = error{ | |
| 354 | HttpChunkInvalid, | |
| 355 | HttpChunkTruncated, | |
| 356 | HttpHeadersOversize, | |
| 357 | }; | |
| 358 | ||
| 359 | pub const HeadError = error{ | |
| 360 | /// Too many bytes of HTTP headers. | |
| 361 | /// | |
| 362 | /// The HTTP specification suggests to respond with a 431 status code | |
| 363 | /// before closing the connection. | |
| 364 | HttpHeadersOversize, | |
| 365 | /// Partial HTTP request was received but the connection was closed | |
| 366 | /// before fully receiving the headers. | |
| 367 | HttpRequestTruncated, | |
| 368 | /// The client sent 0 bytes of headers before closing the stream. This | |
| 369 | /// happens when a keep-alive connection is finally closed. | |
| 370 | HttpConnectionClosing, | |
| 371 | /// Transitive error occurred reading from `in`. | |
| 372 | ReadFailed, | |
| 373 | }; | |
| 374 | ||
| 375 | /// Buffers the entire head inside `in`. | |
| 376 | /// | |
| 377 | /// The resulting memory is invalidated by any subsequent consumption of | |
| 378 | /// the input stream. | |
| 379 | pub fn receiveHead(reader: *Reader) HeadError![]const u8 { | |
| 380 | reader.trailers = &.{}; | |
| 381 | const in = reader.in; | |
| 382 | var hp: HeadParser = .{}; | |
| 383 | var head_len: usize = 0; | |
| 384 | while (true) { | |
| 385 | if (in.buffer.len - head_len == 0) return error.HttpHeadersOversize; | |
| 386 | const remaining = in.buffered()[head_len..]; | |
| 387 | if (remaining.len == 0) { | |
| 388 | in.fillMore() catch |err| switch (err) { | |
| 389 | error.EndOfStream => switch (head_len) { | |
| 390 | 0 => return error.HttpConnectionClosing, | |
| 391 | else => return error.HttpRequestTruncated, | |
| 392 | }, | |
| 393 | error.ReadFailed => return error.ReadFailed, | |
| 394 | }; | |
| 395 | continue; | |
| 396 | } | |
| 397 | head_len += hp.feed(remaining); | |
| 398 | if (hp.state == .finished) { | |
| 399 | reader.state = .received_head; | |
| 400 | const head_buffer = in.buffered()[0..head_len]; | |
| 401 | in.toss(head_len); | |
| 402 | return head_buffer; | |
| 403 | } | |
| 404 | } | |
| 405 | } | |
| 406 | ||
| 407 | /// If compressed body has been negotiated this will return compressed bytes. | |
| 408 | /// | |
| 409 | /// Asserts only called once and after `receiveHead`. | |
| 410 | /// | |
| 411 | /// See also: | |
| 412 | /// * `interfaceDecompressing` | |
| 413 | pub fn bodyReader( | |
| 414 | reader: *Reader, | |
| 415 | buffer: []u8, | |
| 416 | transfer_encoding: TransferEncoding, | |
| 417 | content_length: ?u64, | |
| 418 | ) *std.Io.Reader { | |
| 419 | assert(reader.state == .received_head); | |
| 420 | switch (transfer_encoding) { | |
| 421 | .chunked => { | |
| 422 | reader.state = .{ .body_remaining_chunk_len = .head }; | |
| 423 | reader.interface = .{ | |
| 424 | .buffer = buffer, | |
| 425 | .seek = 0, | |
| 426 | .end = 0, | |
| 427 | .vtable = &.{ | |
| 428 | .stream = chunkedStream, | |
| 429 | .discard = chunkedDiscard, | |
| 430 | }, | |
| 431 | }; | |
| 432 | return &reader.interface; | |
| 433 | }, | |
| 434 | .none => { | |
| 435 | if (content_length) |len| { | |
| 436 | reader.state = .{ .body_remaining_content_length = len }; | |
| 437 | reader.interface = .{ | |
| 438 | .buffer = buffer, | |
| 439 | .seek = 0, | |
| 440 | .end = 0, | |
| 441 | .vtable = &.{ | |
| 442 | .stream = contentLengthStream, | |
| 443 | .discard = contentLengthDiscard, | |
| 444 | }, | |
| 445 | }; | |
| 446 | return &reader.interface; | |
| 447 | } else { | |
| 448 | reader.state = .body_none; | |
| 449 | return reader.in; | |
| 450 | } | |
| 451 | }, | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | /// If compressed body has been negotiated this will return decompressed bytes. | |
| 456 | /// | |
| 457 | /// Asserts only called once and after `receiveHead`. | |
| 458 | /// | |
| 459 | /// See also: | |
| 460 | /// * `interface` | |
| 461 | pub fn bodyReaderDecompressing( | |
| 462 | reader: *Reader, | |
| 463 | transfer_encoding: TransferEncoding, | |
| 464 | content_length: ?u64, | |
| 465 | content_encoding: ContentEncoding, | |
| 466 | decompressor: *Decompressor, | |
| 467 | decompression_buffer: []u8, | |
| 468 | ) *std.Io.Reader { | |
| 469 | if (transfer_encoding == .none and content_length == null) { | |
| 470 | assert(reader.state == .received_head); | |
| 471 | reader.state = .body_none; | |
| 472 | switch (content_encoding) { | |
| 473 | .identity => { | |
| 474 | return reader.in; | |
| 475 | }, | |
| 476 | .deflate => { | |
| 477 | decompressor.* = .{ .flate = .init(reader.in, .zlib, decompression_buffer) }; | |
| 478 | return &decompressor.flate.reader; | |
| 479 | }, | |
| 480 | .gzip => { | |
| 481 | decompressor.* = .{ .flate = .init(reader.in, .gzip, decompression_buffer) }; | |
| 482 | return &decompressor.flate.reader; | |
| 483 | }, | |
| 484 | .zstd => { | |
| 485 | decompressor.* = .{ .zstd = .init(reader.in, decompression_buffer, .{ .verify_checksum = false }) }; | |
| 486 | return &decompressor.zstd.reader; | |
| 487 | }, | |
| 488 | .compress => unreachable, | |
| 489 | } | |
| 490 | } | |
| 491 | const transfer_reader = bodyReader(reader, &.{}, transfer_encoding, content_length); | |
| 492 | return decompressor.init(transfer_reader, decompression_buffer, content_encoding); | |
| 493 | } | |
| 494 | ||
| 495 | fn contentLengthStream( | |
| 496 | io_r: *std.Io.Reader, | |
| 497 | w: *Writer, | |
| 498 | limit: std.Io.Limit, | |
| 499 | ) std.Io.Reader.StreamError!usize { | |
| 500 | const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); | |
| 501 | const remaining_content_length = &reader.state.body_remaining_content_length; | |
| 502 | const remaining = remaining_content_length.*; | |
| 503 | if (remaining == 0) { | |
| 504 | reader.state = .ready; | |
| 505 | return error.EndOfStream; | |
| 506 | } | |
| 507 | const n = try reader.in.stream(w, limit.min(.limited64(remaining))); | |
| 508 | remaining_content_length.* = remaining - n; | |
| 509 | return n; | |
| 510 | } | |
| 511 | ||
| 512 | fn contentLengthDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { | |
| 513 | const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); | |
| 514 | const remaining_content_length = &reader.state.body_remaining_content_length; | |
| 515 | const remaining = remaining_content_length.*; | |
| 516 | if (remaining == 0) { | |
| 517 | reader.state = .ready; | |
| 518 | return error.EndOfStream; | |
| 519 | } | |
| 520 | const n = try reader.in.discard(limit.min(.limited64(remaining))); | |
| 521 | remaining_content_length.* = remaining - n; | |
| 522 | return n; | |
| 523 | } | |
| 524 | ||
| 525 | fn chunkedStream(io_r: *std.Io.Reader, w: *Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { | |
| 526 | const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); | |
| 527 | const chunk_len_ptr = switch (reader.state) { | |
| 528 | .ready => return error.EndOfStream, | |
| 529 | .body_remaining_chunk_len => |*x| x, | |
| 530 | else => unreachable, | |
| 531 | }; | |
| 532 | return chunkedReadEndless(reader, w, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 533 | error.ReadFailed => return error.ReadFailed, | |
| 534 | error.WriteFailed => return error.WriteFailed, | |
| 535 | error.EndOfStream => { | |
| 536 | reader.body_err = error.HttpChunkTruncated; | |
| 537 | return error.ReadFailed; | |
| 538 | }, | |
| 539 | else => |e| { | |
| 540 | reader.body_err = e; | |
| 541 | return error.ReadFailed; | |
| 542 | }, | |
| 543 | }; | |
| 544 | } | |
| 545 | ||
| 546 | fn chunkedReadEndless( | |
| 547 | reader: *Reader, | |
| 548 | w: *Writer, | |
| 549 | limit: std.Io.Limit, | |
| 550 | chunk_len_ptr: *RemainingChunkLen, | |
| 551 | ) (BodyError || std.Io.Reader.StreamError)!usize { | |
| 552 | const in = reader.in; | |
| 553 | len: switch (chunk_len_ptr.*) { | |
| 554 | .head => { | |
| 555 | var cp: ChunkParser = .init; | |
| 556 | while (true) { | |
| 557 | const i = cp.feed(in.buffered()); | |
| 558 | switch (cp.state) { | |
| 559 | .invalid => return error.HttpChunkInvalid, | |
| 560 | .data => { | |
| 561 | in.toss(i); | |
| 562 | break; | |
| 563 | }, | |
| 564 | else => { | |
| 565 | in.toss(i); | |
| 566 | try in.fillMore(); | |
| 567 | continue; | |
| 568 | }, | |
| 569 | } | |
| 570 | } | |
| 571 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | |
| 572 | const n = try in.stream(w, limit.min(.limited64(cp.chunk_len))); | |
| 573 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | |
| 574 | return n; | |
| 575 | }, | |
| 576 | .n => { | |
| 577 | if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; | |
| 578 | in.toss(1); | |
| 579 | continue :len .head; | |
| 580 | }, | |
| 581 | .rn => { | |
| 582 | const rn = try in.peekArray(2); | |
| 583 | if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; | |
| 584 | in.toss(2); | |
| 585 | continue :len .head; | |
| 586 | }, | |
| 587 | else => |remaining_chunk_len| { | |
| 588 | const n = try in.stream(w, limit.min(.limited64(@intFromEnum(remaining_chunk_len) - 2))); | |
| 589 | chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); | |
| 590 | return n; | |
| 591 | }, | |
| 592 | } | |
| 593 | } | |
| 594 | ||
| 595 | fn chunkedDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { | |
| 596 | const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); | |
| 597 | const chunk_len_ptr = switch (reader.state) { | |
| 598 | .ready => return error.EndOfStream, | |
| 599 | .body_remaining_chunk_len => |*x| x, | |
| 600 | else => unreachable, | |
| 601 | }; | |
| 602 | return chunkedDiscardEndless(reader, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 603 | error.ReadFailed => return error.ReadFailed, | |
| 604 | error.EndOfStream => { | |
| 605 | reader.body_err = error.HttpChunkTruncated; | |
| 606 | return error.ReadFailed; | |
| 607 | }, | |
| 608 | else => |e| { | |
| 609 | reader.body_err = e; | |
| 610 | return error.ReadFailed; | |
| 611 | }, | |
| 612 | }; | |
| 613 | } | |
| 614 | ||
| 615 | fn chunkedDiscardEndless( | |
| 616 | reader: *Reader, | |
| 617 | limit: std.Io.Limit, | |
| 618 | chunk_len_ptr: *RemainingChunkLen, | |
| 619 | ) (BodyError || std.Io.Reader.Error)!usize { | |
| 620 | const in = reader.in; | |
| 621 | len: switch (chunk_len_ptr.*) { | |
| 622 | .head => { | |
| 623 | var cp: ChunkParser = .init; | |
| 624 | while (true) { | |
| 625 | const i = cp.feed(in.buffered()); | |
| 626 | switch (cp.state) { | |
| 627 | .invalid => return error.HttpChunkInvalid, | |
| 628 | .data => { | |
| 629 | in.toss(i); | |
| 630 | break; | |
| 631 | }, | |
| 632 | else => { | |
| 633 | in.toss(i); | |
| 634 | try in.fillMore(); | |
| 635 | continue; | |
| 636 | }, | |
| 637 | } | |
| 638 | } | |
| 639 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | |
| 640 | const n = try in.discard(limit.min(.limited64(cp.chunk_len))); | |
| 641 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | |
| 642 | return n; | |
| 643 | }, | |
| 644 | .n => { | |
| 645 | if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; | |
| 646 | in.toss(1); | |
| 647 | continue :len .head; | |
| 648 | }, | |
| 649 | .rn => { | |
| 650 | const rn = try in.peekArray(2); | |
| 651 | if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; | |
| 652 | in.toss(2); | |
| 653 | continue :len .head; | |
| 654 | }, | |
| 655 | else => |remaining_chunk_len| { | |
| 656 | const n = try in.discard(limit.min(.limited64(remaining_chunk_len.int() - 2))); | |
| 657 | chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); | |
| 658 | return n; | |
| 659 | }, | |
| 660 | } | |
| 661 | } | |
| 662 | ||
| 663 | /// Called when next bytes in the stream are trailers, or "\r\n" to indicate | |
| 664 | /// end of chunked body. | |
| 665 | fn parseTrailers(reader: *Reader, amt_read: usize) (BodyError || std.Io.Reader.Error)!usize { | |
| 666 | const in = reader.in; | |
| 667 | const rn = try in.peekArray(2); | |
| 668 | if (rn[0] == '\r' and rn[1] == '\n') { | |
| 669 | in.toss(2); | |
| 670 | reader.state = .ready; | |
| 671 | assert(reader.trailers.len == 0); | |
| 672 | return amt_read; | |
| 673 | } | |
| 674 | var hp: HeadParser = .{ .state = .seen_rn }; | |
| 675 | var trailers_len: usize = 2; | |
| 676 | while (true) { | |
| 677 | if (in.buffer.len - trailers_len == 0) return error.HttpHeadersOversize; | |
| 678 | const remaining = in.buffered()[trailers_len..]; | |
| 679 | if (remaining.len == 0) { | |
| 680 | try in.fillMore(); | |
| 681 | continue; | |
| 682 | } | |
| 683 | trailers_len += hp.feed(remaining); | |
| 684 | if (hp.state == .finished) { | |
| 685 | reader.state = .ready; | |
| 686 | reader.trailers = in.buffered()[0..trailers_len]; | |
| 687 | in.toss(trailers_len); | |
| 688 | return amt_read; | |
| 689 | } | |
| 690 | } | |
| 691 | } | |
| 692 | }; | |
| 693 | ||
| 694 | pub const Decompressor = union(enum) { | |
| 695 | flate: std.compress.flate.Decompress, | |
| 696 | zstd: std.compress.zstd.Decompress, | |
| 697 | none: *std.Io.Reader, | |
| 698 | ||
| 699 | pub fn init( | |
| 700 | decompressor: *Decompressor, | |
| 701 | transfer_reader: *std.Io.Reader, | |
| 702 | buffer: []u8, | |
| 703 | content_encoding: ContentEncoding, | |
| 704 | ) *std.Io.Reader { | |
| 705 | switch (content_encoding) { | |
| 706 | .identity => { | |
| 707 | decompressor.* = .{ .none = transfer_reader }; | |
| 708 | return transfer_reader; | |
| 709 | }, | |
| 710 | .deflate => { | |
| 711 | decompressor.* = .{ .flate = .init(transfer_reader, .zlib, buffer) }; | |
| 712 | return &decompressor.flate.reader; | |
| 713 | }, | |
| 714 | .gzip => { | |
| 715 | decompressor.* = .{ .flate = .init(transfer_reader, .gzip, buffer) }; | |
| 716 | return &decompressor.flate.reader; | |
| 717 | }, | |
| 718 | .zstd => { | |
| 719 | decompressor.* = .{ .zstd = .init(transfer_reader, buffer, .{ .verify_checksum = false }) }; | |
| 720 | return &decompressor.zstd.reader; | |
| 721 | }, | |
| 722 | .compress => unreachable, | |
| 723 | } | |
| 724 | } | |
| 725 | }; | |
| 726 | ||
| 727 | /// Request or response body. | |
| 728 | pub const BodyWriter = struct { | |
| 729 | /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the | |
| 730 | /// state of this other than via methods of `BodyWriter`. | |
| 731 | http_protocol_output: *Writer, | |
| 732 | state: State, | |
| 733 | writer: Writer, | |
| 734 | ||
| 735 | pub const Error = Writer.Error; | |
| 736 | ||
| 737 | /// How many zeroes to reserve for hex-encoded chunk length. | |
| 738 | const chunk_len_digits = 8; | |
| 739 | const max_chunk_len: usize = std.math.pow(u64, 16, chunk_len_digits) - 1; | |
| 740 | const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; | |
| 741 | ||
| 742 | comptime { | |
| 743 | assert(max_chunk_len == std.math.maxInt(u32)); | |
| 744 | } | |
| 745 | ||
| 746 | pub const State = union(enum) { | |
| 747 | /// End of connection signals the end of the stream. | |
| 748 | none, | |
| 749 | /// As a debugging utility, counts down to zero as bytes are written. | |
| 750 | content_length: u64, | |
| 751 | /// Each chunk is wrapped in a header and trailer. | |
| 752 | chunked: Chunked, | |
| 753 | /// Cleanly finished stream; connection can be reused. | |
| 754 | end, | |
| 755 | ||
| 756 | pub const Chunked = union(enum) { | |
| 757 | /// Index to the start of the hex-encoded chunk length in the chunk | |
| 758 | /// header within the buffer of `BodyWriter.http_protocol_output`. | |
| 759 | /// Buffered chunk data starts here plus length of `chunk_header_template`. | |
| 760 | offset: usize, | |
| 761 | /// We are in the middle of a chunk and this is how many bytes are | |
| 762 | /// left until the next header. This includes +2 for "\r"\n", and | |
| 763 | /// is zero for the beginning of the stream. | |
| 764 | chunk_len: usize, | |
| 765 | ||
| 766 | pub const init: Chunked = .{ .chunk_len = 0 }; | |
| 767 | }; | |
| 768 | }; | |
| 769 | ||
| 770 | pub fn isEliding(w: *const BodyWriter) bool { | |
| 771 | return w.writer.vtable.drain == elidingDrain; | |
| 772 | } | |
| 773 | ||
| 774 | /// Sends all buffered data across `BodyWriter.http_protocol_output`. | |
| 775 | pub fn flush(w: *BodyWriter) Error!void { | |
| 776 | const out = w.http_protocol_output; | |
| 777 | switch (w.state) { | |
| 778 | .end, .none, .content_length => return out.flush(), | |
| 779 | .chunked => |*chunked| switch (chunked.*) { | |
| 780 | .offset => |offset| { | |
| 781 | const chunk_len = out.end - offset - chunk_header_template.len; | |
| 782 | if (chunk_len > 0) { | |
| 783 | writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 784 | chunked.* = .{ .chunk_len = 2 }; | |
| 785 | } else { | |
| 786 | out.end = offset; | |
| 787 | chunked.* = .{ .chunk_len = 0 }; | |
| 788 | } | |
| 789 | try out.flush(); | |
| 790 | }, | |
| 791 | .chunk_len => return out.flush(), | |
| 792 | }, | |
| 793 | } | |
| 794 | } | |
| 795 | ||
| 796 | /// When using content-length, asserts that the amount of data sent matches | |
| 797 | /// the value sent in the header, then flushes. | |
| 798 | /// | |
| 799 | /// When using transfer-encoding: chunked, writes the end-of-stream message | |
| 800 | /// with empty trailers, then flushes the stream to the system. Asserts any | |
| 801 | /// started chunk has been completely finished. | |
| 802 | /// | |
| 803 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 804 | /// | |
| 805 | /// See also: | |
| 806 | /// * `endUnflushed` | |
| 807 | /// * `endChunked` | |
| 808 | pub fn end(w: *BodyWriter) Error!void { | |
| 809 | try endUnflushed(w); | |
| 810 | try w.http_protocol_output.flush(); | |
| 811 | } | |
| 812 | ||
| 813 | /// When using content-length, asserts that the amount of data sent matches | |
| 814 | /// the value sent in the header. | |
| 815 | /// | |
| 816 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the | |
| 817 | /// end-of-stream message with empty trailers. | |
| 818 | /// | |
| 819 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 820 | /// | |
| 821 | /// See also: | |
| 822 | /// * `end` | |
| 823 | /// * `endChunked` | |
| 824 | pub fn endUnflushed(w: *BodyWriter) Error!void { | |
| 825 | switch (w.state) { | |
| 826 | .end => unreachable, | |
| 827 | .content_length => |len| { | |
| 828 | assert(len == 0); // Trips when end() called before all bytes written. | |
| 829 | w.state = .end; | |
| 830 | }, | |
| 831 | .none => {}, | |
| 832 | .chunked => return endChunkedUnflushed(w, .{}), | |
| 833 | } | |
| 834 | } | |
| 835 | ||
| 836 | pub const EndChunkedOptions = struct { | |
| 837 | trailers: []const Header = &.{}, | |
| 838 | }; | |
| 839 | ||
| 840 | /// Writes the end-of-stream message and any optional trailers, flushing | |
| 841 | /// the underlying stream. | |
| 842 | /// | |
| 843 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. | |
| 844 | /// | |
| 845 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 846 | /// | |
| 847 | /// See also: | |
| 848 | /// * `endChunkedUnflushed` | |
| 849 | /// * `end` | |
| 850 | pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) Error!void { | |
| 851 | try endChunkedUnflushed(w, options); | |
| 852 | try w.http_protocol_output.flush(); | |
| 853 | } | |
| 854 | ||
| 855 | /// Writes the end-of-stream message and any optional trailers. | |
| 856 | /// | |
| 857 | /// Does not flush. | |
| 858 | /// | |
| 859 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. | |
| 860 | /// | |
| 861 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 862 | /// | |
| 863 | /// See also: | |
| 864 | /// * `endChunked` | |
| 865 | /// * `endUnflushed` | |
| 866 | /// * `end` | |
| 867 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { | |
| 868 | const chunked = &w.state.chunked; | |
| 869 | if (w.isEliding()) { | |
| 870 | w.state = .end; | |
| 871 | return; | |
| 872 | } | |
| 873 | const bw = w.http_protocol_output; | |
| 874 | switch (chunked.*) { | |
| 875 | .offset => |offset| { | |
| 876 | const chunk_len = bw.end - offset - chunk_header_template.len; | |
| 877 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 878 | try bw.writeAll("\r\n"); | |
| 879 | }, | |
| 880 | .chunk_len => |chunk_len| switch (chunk_len) { | |
| 881 | 0 => {}, | |
| 882 | 1 => try bw.writeByte('\n'), | |
| 883 | 2 => try bw.writeAll("\r\n"), | |
| 884 | else => unreachable, // An earlier write call indicated more data would follow. | |
| 885 | }, | |
| 886 | } | |
| 887 | try bw.writeAll("0\r\n"); | |
| 888 | for (options.trailers) |trailer| { | |
| 889 | try bw.writeAll(trailer.name); | |
| 890 | try bw.writeAll(": "); | |
| 891 | try bw.writeAll(trailer.value); | |
| 892 | try bw.writeAll("\r\n"); | |
| 893 | } | |
| 894 | try bw.writeAll("\r\n"); | |
| 895 | w.state = .end; | |
| 896 | } | |
| 897 | ||
| 898 | pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 899 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 900 | assert(!bw.isEliding()); | |
| 901 | const out = bw.http_protocol_output; | |
| 902 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 903 | bw.state.content_length -= n; | |
| 904 | return w.consume(n); | |
| 905 | } | |
| 906 | ||
| 907 | pub fn noneDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 908 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 909 | assert(!bw.isEliding()); | |
| 910 | const out = bw.http_protocol_output; | |
| 911 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 912 | return w.consume(n); | |
| 913 | } | |
| 914 | ||
| 915 | pub fn elidingDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 916 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 917 | const slice = data[0 .. data.len - 1]; | |
| 918 | const pattern = data[slice.len]; | |
| 919 | var written: usize = pattern.len * splat; | |
| 920 | for (slice) |bytes| written += bytes.len; | |
| 921 | switch (bw.state) { | |
| 922 | .content_length => |*len| len.* -= written + w.end, | |
| 923 | else => {}, | |
| 924 | } | |
| 925 | w.end = 0; | |
| 926 | return written; | |
| 927 | } | |
| 928 | ||
| 929 | pub fn elidingSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 930 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 931 | if (File.Handle == void) return error.Unimplemented; | |
| 932 | if (builtin.zig_backend == .stage2_aarch64) return error.Unimplemented; | |
| 933 | switch (bw.state) { | |
| 934 | .content_length => |*len| len.* -= w.end, | |
| 935 | else => {}, | |
| 936 | } | |
| 937 | w.end = 0; | |
| 938 | if (limit == .nothing) return 0; | |
| 939 | if (file_reader.getSize()) |size| { | |
| 940 | const n = limit.minInt64(size - file_reader.pos); | |
| 941 | if (n == 0) return error.EndOfStream; | |
| 942 | file_reader.seekBy(@intCast(n)) catch return error.Unimplemented; | |
| 943 | switch (bw.state) { | |
| 944 | .content_length => |*len| len.* -= n, | |
| 945 | else => {}, | |
| 946 | } | |
| 947 | return n; | |
| 948 | } else |_| { | |
| 949 | // Error is observable on `file_reader` instance, and it is better to | |
| 950 | // treat the file as a pipe. | |
| 951 | return error.Unimplemented; | |
| 952 | } | |
| 953 | } | |
| 954 | ||
| 955 | /// Returns `null` if size cannot be computed without making any syscalls. | |
| 956 | pub fn noneSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 957 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 958 | assert(!bw.isEliding()); | |
| 959 | const out = bw.http_protocol_output; | |
| 960 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 961 | return w.consume(n); | |
| 962 | } | |
| 963 | ||
| 964 | pub fn contentLengthSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 965 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 966 | assert(!bw.isEliding()); | |
| 967 | const out = bw.http_protocol_output; | |
| 968 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 969 | bw.state.content_length -= n; | |
| 970 | return w.consume(n); | |
| 971 | } | |
| 972 | ||
| 973 | pub fn chunkedSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 974 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 975 | assert(!bw.isEliding()); | |
| 976 | const data_len = Writer.countSendFileLowerBound(w.end, file_reader, limit) orelse { | |
| 977 | // If the file size is unknown, we cannot lower to a `sendFile` since we would | |
| 978 | // have to flush the chunk header before knowing the chunk length. | |
| 979 | return error.Unimplemented; | |
| 980 | }; | |
| 981 | const out = bw.http_protocol_output; | |
| 982 | const chunked = &bw.state.chunked; | |
| 983 | state: switch (chunked.*) { | |
| 984 | .offset => |off| { | |
| 985 | // TODO: is it better perf to read small files into the buffer? | |
| 986 | const buffered_len = out.end - off - chunk_header_template.len; | |
| 987 | const chunk_len = data_len + buffered_len; | |
| 988 | writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len); | |
| 989 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 990 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | |
| 991 | return w.consume(n); | |
| 992 | }, | |
| 993 | .chunk_len => |chunk_len| l: switch (chunk_len) { | |
| 994 | 0 => { | |
| 995 | const off = out.end; | |
| 996 | const header_buf = try out.writableArray(chunk_header_template.len); | |
| 997 | @memcpy(header_buf, chunk_header_template); | |
| 998 | chunked.* = .{ .offset = off }; | |
| 999 | continue :state .{ .offset = off }; | |
| 1000 | }, | |
| 1001 | 1 => { | |
| 1002 | try out.writeByte('\n'); | |
| 1003 | chunked.chunk_len = 0; | |
| 1004 | continue :l 0; | |
| 1005 | }, | |
| 1006 | 2 => { | |
| 1007 | try out.writeByte('\r'); | |
| 1008 | chunked.chunk_len = 1; | |
| 1009 | continue :l 1; | |
| 1010 | }, | |
| 1011 | else => { | |
| 1012 | const new_limit = limit.min(.limited(chunk_len - 2)); | |
| 1013 | const n = try out.sendFileHeader(w.buffered(), file_reader, new_limit); | |
| 1014 | chunked.chunk_len = chunk_len - n; | |
| 1015 | return w.consume(n); | |
| 1016 | }, | |
| 1017 | }, | |
| 1018 | } | |
| 1019 | } | |
| 1020 | ||
| 1021 | pub fn chunkedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 1022 | const bw: *BodyWriter = @alignCast(@fieldParentPtr("writer", w)); | |
| 1023 | assert(!bw.isEliding()); | |
| 1024 | const out = bw.http_protocol_output; | |
| 1025 | const data_len = w.end + Writer.countSplat(data, splat); | |
| 1026 | const chunked = &bw.state.chunked; | |
| 1027 | state: switch (chunked.*) { | |
| 1028 | .offset => |offset| { | |
| 1029 | if (out.unusedCapacityLen() >= data_len) { | |
| 1030 | return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable); | |
| 1031 | } | |
| 1032 | const buffered_len = out.end - offset - chunk_header_template.len; | |
| 1033 | const chunk_len = data_len + buffered_len; | |
| 1034 | writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 1035 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 1036 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | |
| 1037 | return w.consume(n); | |
| 1038 | }, | |
| 1039 | .chunk_len => |chunk_len| l: switch (chunk_len) { | |
| 1040 | 0 => { | |
| 1041 | const offset = out.end; | |
| 1042 | const header_buf = try out.writableArray(chunk_header_template.len); | |
| 1043 | @memcpy(header_buf, chunk_header_template); | |
| 1044 | chunked.* = .{ .offset = offset }; | |
| 1045 | continue :state .{ .offset = offset }; | |
| 1046 | }, | |
| 1047 | 1 => { | |
| 1048 | try out.writeByte('\n'); | |
| 1049 | chunked.chunk_len = 0; | |
| 1050 | continue :l 0; | |
| 1051 | }, | |
| 1052 | 2 => { | |
| 1053 | try out.writeByte('\r'); | |
| 1054 | chunked.chunk_len = 1; | |
| 1055 | continue :l 1; | |
| 1056 | }, | |
| 1057 | else => { | |
| 1058 | const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2)); | |
| 1059 | chunked.chunk_len = chunk_len - n; | |
| 1060 | return w.consume(n); | |
| 1061 | }, | |
| 1062 | }, | |
| 1063 | } | |
| 1064 | } | |
| 1065 | ||
| 1066 | /// Writes an integer as base 16 to `buf`, right-aligned, assuming the | |
| 1067 | /// buffer has already been filled with zeroes. | |
| 1068 | fn writeHex(buf: []u8, x: usize) void { | |
| 1069 | assert(std.mem.allEqual(u8, buf, '0')); | |
| 1070 | const base = 16; | |
| 1071 | var index: usize = buf.len; | |
| 1072 | var a = x; | |
| 1073 | while (a > 0) { | |
| 1074 | const digit = a % base; | |
| 1075 | index -= 1; | |
| 1076 | buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); | |
| 1077 | a /= base; | |
| 1078 | } | |
| 1079 | } | |
| 1080 | }; | |
| 1081 | ||
| 318 | 1082 | test { |
| 1083 | _ = Server; | |
| 1084 | _ = Status; | |
| 1085 | _ = Method; | |
| 1086 | _ = ChunkParser; | |
| 1087 | _ = HeadParser; | |
| 1088 | ||
| 319 | 1089 | if (builtin.os.tag != .wasi) { |
| 320 | 1090 | _ = Client; |
| 321 | _ = Method; | |
| 322 | _ = Server; | |
| 323 | _ = Status; | |
| 324 | _ = HeadParser; | |
| 325 | _ = ChunkParser; | |
| 326 | _ = WebSocket; | |
| 327 | 1091 | _ = @import("http/test.zig"); |
| 328 | 1092 | } |
| 329 | 1093 | } |
lib/std/http/ChunkParser.zig+3-3| ... | ... | @@ -1,5 +1,8 @@ |
| 1 | 1 | //! Parser for transfer-encoding: chunked. |
| 2 | 2 | |
| 3 | const ChunkParser = @This(); | |
| 4 | const std = @import("std"); | |
| 5 | ||
| 3 | 6 | state: State, |
| 4 | 7 | chunk_len: u64, |
| 5 | 8 | |
| ... | ... | @@ -97,9 +100,6 @@ pub fn feed(p: *ChunkParser, bytes: []const u8) usize { |
| 97 | 100 | return bytes.len; |
| 98 | 101 | } |
| 99 | 102 | |
| 100 | const ChunkParser = @This(); | |
| 101 | const std = @import("std"); | |
| 102 | ||
| 103 | 103 | test feed { |
| 104 | 104 | const testing = std.testing; |
| 105 | 105 |
lib/std/http/Client.zig+1039-1011| ... | ... | @@ -13,9 +13,10 @@ const net = std.net; |
| 13 | 13 | const Uri = std.Uri; |
| 14 | 14 | const Allocator = mem.Allocator; |
| 15 | 15 | const assert = std.debug.assert; |
| 16 | const Writer = std.io.Writer; | |
| 17 | const Reader = std.io.Reader; | |
| 16 | 18 | |
| 17 | 19 | const Client = @This(); |
| 18 | const proto = @import("protocol.zig"); | |
| 19 | 20 | |
| 20 | 21 | pub const disable_tls = std.options.http_disable_tls; |
| 21 | 22 | |
| ... | ... | @@ -24,6 +25,12 @@ allocator: Allocator, |
| 24 | 25 | |
| 25 | 26 | ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, |
| 26 | 27 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| 28 | /// Used both for the reader and writer buffers. | |
| 29 | tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len, | |
| 30 | /// If non-null, ssl secrets are logged to a stream. Creating such a stream | |
| 31 | /// allows other processes with access to that stream to decrypt all | |
| 32 | /// traffic over connections created with this `Client`. | |
| 33 | ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null, | |
| 27 | 34 | |
| 28 | 35 | /// When this is `true`, the next time this client performs an HTTPS request, |
| 29 | 36 | /// it will first rescan the system for root certificates. |
| ... | ... | @@ -31,6 +38,13 @@ next_https_rescan_certs: bool = true, |
| 31 | 38 | |
| 32 | 39 | /// The pool of connections that can be reused (and currently in use). |
| 33 | 40 | connection_pool: ConnectionPool = .{}, |
| 41 | /// Each `Connection` allocates this amount for the reader buffer. | |
| 42 | /// | |
| 43 | /// If the entire HTTP header cannot fit in this amount of bytes, | |
| 44 | /// `error.HttpHeadersOversize` will be returned from `Request.wait`. | |
| 45 | read_buffer_size: usize = 4096 + if (disable_tls) 0 else std.crypto.tls.Client.min_buffer_len, | |
| 46 | /// Each `Connection` allocates this amount for the writer buffer. | |
| 47 | write_buffer_size: usize = 1024, | |
| 34 | 48 | |
| 35 | 49 | /// If populated, all http traffic travels through this third party. |
| 36 | 50 | /// This field cannot be modified while the client has active connections. |
| ... | ... | @@ -41,7 +55,7 @@ http_proxy: ?*Proxy = null, |
| 41 | 55 | /// Pointer to externally-owned memory. |
| 42 | 56 | https_proxy: ?*Proxy = null, |
| 43 | 57 | |
| 44 | /// A set of linked lists of connections that can be reused. | |
| 58 | /// A Least-Recently-Used cache of open connections to be reused. | |
| 45 | 59 | pub const ConnectionPool = struct { |
| 46 | 60 | mutex: std.Thread.Mutex = .{}, |
| 47 | 61 | /// Open connections that are currently in use. |
| ... | ... | @@ -55,23 +69,25 @@ pub const ConnectionPool = struct { |
| 55 | 69 | pub const Criteria = struct { |
| 56 | 70 | host: []const u8, |
| 57 | 71 | port: u16, |
| 58 | protocol: Connection.Protocol, | |
| 72 | protocol: Protocol, | |
| 59 | 73 | }; |
| 60 | 74 | |
| 61 | /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. | |
| 75 | /// Finds and acquires a connection from the connection pool matching the criteria. | |
| 62 | 76 | /// If no connection is found, null is returned. |
| 77 | /// | |
| 78 | /// Threadsafe. | |
| 63 | 79 | pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection { |
| 64 | 80 | pool.mutex.lock(); |
| 65 | 81 | defer pool.mutex.unlock(); |
| 66 | 82 | |
| 67 | 83 | var next = pool.free.last; |
| 68 | 84 | while (next) |node| : (next = node.prev) { |
| 69 | const connection: *Connection = @fieldParentPtr("pool_node", node); | |
| 85 | const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); | |
| 70 | 86 | if (connection.protocol != criteria.protocol) continue; |
| 71 | 87 | if (connection.port != criteria.port) continue; |
| 72 | 88 | |
| 73 | 89 | // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) |
| 74 | if (!std.ascii.eqlIgnoreCase(connection.host, criteria.host)) continue; | |
| 90 | if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue; | |
| 75 | 91 | |
| 76 | 92 | pool.acquireUnsafe(connection); |
| 77 | 93 | return connection; |
| ... | ... | @@ -96,28 +112,23 @@ pub const ConnectionPool = struct { |
| 96 | 112 | return pool.acquireUnsafe(connection); |
| 97 | 113 | } |
| 98 | 114 | |
| 99 | /// Tries to release a connection back to the connection pool. This function is threadsafe. | |
| 115 | /// Tries to release a connection back to the connection pool. | |
| 100 | 116 | /// If the connection is marked as closing, it will be closed instead. |
| 101 | 117 | /// |
| 102 | /// The allocator must be the owner of all nodes in this pool. | |
| 103 | /// The allocator must be the owner of all resources associated with the connection. | |
| 104 | pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void { | |
| 118 | /// Threadsafe. | |
| 119 | pub fn release(pool: *ConnectionPool, connection: *Connection) void { | |
| 105 | 120 | pool.mutex.lock(); |
| 106 | 121 | defer pool.mutex.unlock(); |
| 107 | 122 | |
| 108 | 123 | pool.used.remove(&connection.pool_node); |
| 109 | 124 | |
| 110 | if (connection.closing or pool.free_size == 0) { | |
| 111 | connection.close(allocator); | |
| 112 | return allocator.destroy(connection); | |
| 113 | } | |
| 125 | if (connection.closing or pool.free_size == 0) return connection.destroy(); | |
| 114 | 126 | |
| 115 | 127 | if (pool.free_len >= pool.free_size) { |
| 116 | const popped: *Connection = @fieldParentPtr("pool_node", pool.free.popFirst().?); | |
| 128 | const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?)); | |
| 117 | 129 | pool.free_len -= 1; |
| 118 | 130 | |
| 119 | popped.close(allocator); | |
| 120 | allocator.destroy(popped); | |
| 131 | popped.destroy(); | |
| 121 | 132 | } |
| 122 | 133 | |
| 123 | 134 | if (connection.proxied) { |
| ... | ... | @@ -138,9 +149,11 @@ pub const ConnectionPool = struct { |
| 138 | 149 | pool.used.append(&connection.pool_node); |
| 139 | 150 | } |
| 140 | 151 | |
| 141 | /// Resizes the connection pool. This function is threadsafe. | |
| 152 | /// Resizes the connection pool. | |
| 142 | 153 | /// |
| 143 | 154 | /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. |
| 155 | /// | |
| 156 | /// Threadsafe. | |
| 144 | 157 | pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { |
| 145 | 158 | pool.mutex.lock(); |
| 146 | 159 | defer pool.mutex.unlock(); |
| ... | ... | @@ -158,538 +171,612 @@ pub const ConnectionPool = struct { |
| 158 | 171 | pool.free_size = new_size; |
| 159 | 172 | } |
| 160 | 173 | |
| 161 | /// Frees the connection pool and closes all connections within. This function is threadsafe. | |
| 174 | /// Frees the connection pool and closes all connections within. | |
| 162 | 175 | /// |
| 163 | 176 | /// All future operations on the connection pool will deadlock. |
| 164 | pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void { | |
| 177 | /// | |
| 178 | /// Threadsafe. | |
| 179 | pub fn deinit(pool: *ConnectionPool) void { | |
| 165 | 180 | pool.mutex.lock(); |
| 166 | 181 | |
| 167 | 182 | var next = pool.free.first; |
| 168 | 183 | while (next) |node| { |
| 169 | const connection: *Connection = @fieldParentPtr("pool_node", node); | |
| 184 | const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); | |
| 170 | 185 | next = node.next; |
| 171 | connection.close(allocator); | |
| 172 | allocator.destroy(connection); | |
| 186 | connection.destroy(); | |
| 173 | 187 | } |
| 174 | 188 | |
| 175 | 189 | next = pool.used.first; |
| 176 | 190 | while (next) |node| { |
| 177 | const connection: *Connection = @fieldParentPtr("pool_node", node); | |
| 191 | const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); | |
| 178 | 192 | next = node.next; |
| 179 | connection.close(allocator); | |
| 180 | allocator.destroy(node); | |
| 193 | connection.destroy(); | |
| 181 | 194 | } |
| 182 | 195 | |
| 183 | 196 | pool.* = undefined; |
| 184 | 197 | } |
| 185 | 198 | }; |
| 186 | 199 | |
| 187 | /// An interface to either a plain or TLS connection. | |
| 188 | pub const Connection = struct { | |
| 189 | stream: net.Stream, | |
| 190 | /// undefined unless protocol is tls. | |
| 191 | tls_client: if (!disable_tls) *std.crypto.tls.Client else void, | |
| 192 | ||
| 193 | /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. | |
| 194 | pool_node: std.DoublyLinkedList.Node, | |
| 195 | ||
| 196 | /// The protocol that this connection is using. | |
| 197 | protocol: Protocol, | |
| 198 | ||
| 199 | /// The host that this connection is connected to. | |
| 200 | host: []u8, | |
| 200 | pub const Protocol = enum { | |
| 201 | plain, | |
| 202 | tls, | |
| 201 | 203 | |
| 202 | /// The port that this connection is connected to. | |
| 203 | port: u16, | |
| 204 | ||
| 205 | /// Whether this connection is proxied and is not directly connected. | |
| 206 | proxied: bool = false, | |
| 207 | ||
| 208 | /// Whether this connection is closing when we're done with it. | |
| 209 | closing: bool = false, | |
| 210 | ||
| 211 | read_start: BufferSize = 0, | |
| 212 | read_end: BufferSize = 0, | |
| 213 | write_end: BufferSize = 0, | |
| 214 | read_buf: [buffer_size]u8 = undefined, | |
| 215 | write_buf: [buffer_size]u8 = undefined, | |
| 216 | ||
| 217 | pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; | |
| 218 | const BufferSize = std.math.IntFittingRange(0, buffer_size); | |
| 219 | ||
| 220 | pub const Protocol = enum { plain, tls }; | |
| 221 | ||
| 222 | pub fn readvDirectTls(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize { | |
| 223 | return conn.tls_client.readv(conn.stream, buffers) catch |err| { | |
| 224 | // https://github.com/ziglang/zig/issues/2473 | |
| 225 | if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert; | |
| 226 | ||
| 227 | switch (err) { | |
| 228 | error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure, | |
| 229 | error.ConnectionTimedOut => return error.ConnectionTimedOut, | |
| 230 | error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, | |
| 231 | else => return error.UnexpectedReadFailure, | |
| 232 | } | |
| 204 | fn port(protocol: Protocol) u16 { | |
| 205 | return switch (protocol) { | |
| 206 | .plain => 80, | |
| 207 | .tls => 443, | |
| 233 | 208 | }; |
| 234 | 209 | } |
| 235 | 210 | |
| 236 | pub fn readvDirect(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize { | |
| 237 | if (conn.protocol == .tls) { | |
| 238 | if (disable_tls) unreachable; | |
| 239 | ||
| 240 | return conn.readvDirectTls(buffers); | |
| 241 | } | |
| 242 | ||
| 243 | return conn.stream.readv(buffers) catch |err| switch (err) { | |
| 244 | error.ConnectionTimedOut => return error.ConnectionTimedOut, | |
| 245 | error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, | |
| 246 | else => return error.UnexpectedReadFailure, | |
| 247 | }; | |
| 248 | } | |
| 249 | ||
| 250 | /// Refills the read buffer with data from the connection. | |
| 251 | pub fn fill(conn: *Connection) ReadError!void { | |
| 252 | if (conn.read_end != conn.read_start) return; | |
| 253 | ||
| 254 | var iovecs = [1]std.posix.iovec{ | |
| 255 | .{ .base = &conn.read_buf, .len = conn.read_buf.len }, | |
| 256 | }; | |
| 257 | const nread = try conn.readvDirect(&iovecs); | |
| 258 | if (nread == 0) return error.EndOfStream; | |
| 259 | conn.read_start = 0; | |
| 260 | conn.read_end = @intCast(nread); | |
| 211 | pub fn fromScheme(scheme: []const u8) ?Protocol { | |
| 212 | const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ | |
| 213 | .{ "http", .plain }, | |
| 214 | .{ "ws", .plain }, | |
| 215 | .{ "https", .tls }, | |
| 216 | .{ "wss", .tls }, | |
| 217 | }); | |
| 218 | return protocol_map.get(scheme); | |
| 261 | 219 | } |
| 262 | 220 | |
| 263 | /// Returns the current slice of buffered data. | |
| 264 | pub fn peek(conn: *Connection) []const u8 { | |
| 265 | return conn.read_buf[conn.read_start..conn.read_end]; | |
| 221 | pub fn fromUri(uri: Uri) ?Protocol { | |
| 222 | return fromScheme(uri.scheme); | |
| 266 | 223 | } |
| 224 | }; | |
| 267 | 225 | |
| 268 | /// Discards the given number of bytes from the read buffer. | |
| 269 | pub fn drop(conn: *Connection, num: BufferSize) void { | |
| 270 | conn.read_start += num; | |
| 271 | } | |
| 226 | pub const Connection = struct { | |
| 227 | client: *Client, | |
| 228 | stream_writer: net.Stream.Writer, | |
| 229 | stream_reader: net.Stream.Reader, | |
| 230 | /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. | |
| 231 | pool_node: std.DoublyLinkedList.Node, | |
| 232 | port: u16, | |
| 233 | host_len: u8, | |
| 234 | proxied: bool, | |
| 235 | closing: bool, | |
| 236 | protocol: Protocol, | |
| 272 | 237 | |
| 273 | /// Reads data from the connection into the given buffer. | |
| 274 | pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { | |
| 275 | const available_read = conn.read_end - conn.read_start; | |
| 276 | const available_buffer = buffer.len; | |
| 238 | const Plain = struct { | |
| 239 | connection: Connection, | |
| 240 | ||
| 241 | fn create( | |
| 242 | client: *Client, | |
| 243 | remote_host: []const u8, | |
| 244 | port: u16, | |
| 245 | stream: net.Stream, | |
| 246 | ) error{OutOfMemory}!*Plain { | |
| 247 | const gpa = client.allocator; | |
| 248 | const alloc_len = allocLen(client, remote_host.len); | |
| 249 | const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); | |
| 250 | errdefer gpa.free(base); | |
| 251 | const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; | |
| 252 | const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; | |
| 253 | const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; | |
| 254 | assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); | |
| 255 | @memcpy(host_buffer, remote_host); | |
| 256 | const plain: *Plain = @ptrCast(base); | |
| 257 | plain.* = .{ | |
| 258 | .connection = .{ | |
| 259 | .client = client, | |
| 260 | .stream_writer = stream.writer(socket_write_buffer), | |
| 261 | .stream_reader = stream.reader(socket_read_buffer), | |
| 262 | .pool_node = .{}, | |
| 263 | .port = port, | |
| 264 | .host_len = @intCast(remote_host.len), | |
| 265 | .proxied = false, | |
| 266 | .closing = false, | |
| 267 | .protocol = .plain, | |
| 268 | }, | |
| 269 | }; | |
| 270 | return plain; | |
| 271 | } | |
| 277 | 272 | |
| 278 | if (available_read > available_buffer) { // partially read buffered data | |
| 279 | @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]); | |
| 280 | conn.read_start += @intCast(available_buffer); | |
| 273 | fn destroy(plain: *Plain) void { | |
| 274 | const c = &plain.connection; | |
| 275 | const gpa = c.client.allocator; | |
| 276 | const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain); | |
| 277 | gpa.free(base[0..allocLen(c.client, c.host_len)]); | |
| 278 | } | |
| 281 | 279 | |
| 282 | return available_buffer; | |
| 283 | } else if (available_read > 0) { // fully read buffered data | |
| 284 | @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]); | |
| 285 | conn.read_start += available_read; | |
| 280 | fn allocLen(client: *Client, host_len: usize) usize { | |
| 281 | return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; | |
| 282 | } | |
| 286 | 283 | |
| 287 | return available_read; | |
| 284 | fn host(plain: *Plain) []u8 { | |
| 285 | const base: [*]u8 = @ptrCast(plain); | |
| 286 | return base[@sizeOf(Plain)..][0..plain.connection.host_len]; | |
| 288 | 287 | } |
| 288 | }; | |
| 289 | 289 | |
| 290 | var iovecs = [2]std.posix.iovec{ | |
| 291 | .{ .base = buffer.ptr, .len = buffer.len }, | |
| 292 | .{ .base = &conn.read_buf, .len = conn.read_buf.len }, | |
| 293 | }; | |
| 294 | const nread = try conn.readvDirect(&iovecs); | |
| 290 | const Tls = struct { | |
| 291 | client: std.crypto.tls.Client, | |
| 292 | connection: Connection, | |
| 293 | ||
| 294 | fn create( | |
| 295 | client: *Client, | |
| 296 | remote_host: []const u8, | |
| 297 | port: u16, | |
| 298 | stream: net.Stream, | |
| 299 | ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { | |
| 300 | const gpa = client.allocator; | |
| 301 | const alloc_len = allocLen(client, remote_host.len); | |
| 302 | const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); | |
| 303 | errdefer gpa.free(base); | |
| 304 | const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; | |
| 305 | const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.tls_buffer_size]; | |
| 306 | const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size]; | |
| 307 | const write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; | |
| 308 | const read_buffer = write_buffer.ptr[write_buffer.len..][0..client.read_buffer_size]; | |
| 309 | assert(base.ptr + alloc_len == read_buffer.ptr + read_buffer.len); | |
| 310 | @memcpy(host_buffer, remote_host); | |
| 311 | const tls: *Tls = @ptrCast(base); | |
| 312 | tls.* = .{ | |
| 313 | .connection = .{ | |
| 314 | .client = client, | |
| 315 | .stream_writer = stream.writer(tls_write_buffer), | |
| 316 | .stream_reader = stream.reader(tls_read_buffer), | |
| 317 | .pool_node = .{}, | |
| 318 | .port = port, | |
| 319 | .host_len = @intCast(remote_host.len), | |
| 320 | .proxied = false, | |
| 321 | .closing = false, | |
| 322 | .protocol = .tls, | |
| 323 | }, | |
| 324 | // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true | |
| 325 | .client = std.crypto.tls.Client.init( | |
| 326 | tls.connection.stream_reader.interface(), | |
| 327 | &tls.connection.stream_writer.interface, | |
| 328 | .{ | |
| 329 | .host = .{ .explicit = remote_host }, | |
| 330 | .ca = .{ .bundle = client.ca_bundle }, | |
| 331 | .ssl_key_log = client.ssl_key_log, | |
| 332 | .read_buffer = read_buffer, | |
| 333 | .write_buffer = write_buffer, | |
| 334 | // This is appropriate for HTTPS because the HTTP headers contain | |
| 335 | // the content length which is used to detect truncation attacks. | |
| 336 | .allow_truncation_attacks = true, | |
| 337 | }, | |
| 338 | ) catch return error.TlsInitializationFailed, | |
| 339 | }; | |
| 340 | return tls; | |
| 341 | } | |
| 295 | 342 | |
| 296 | if (nread > buffer.len) { | |
| 297 | conn.read_start = 0; | |
| 298 | conn.read_end = @intCast(nread - buffer.len); | |
| 299 | return buffer.len; | |
| 343 | fn destroy(tls: *Tls) void { | |
| 344 | const c = &tls.connection; | |
| 345 | const gpa = c.client.allocator; | |
| 346 | const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls); | |
| 347 | gpa.free(base[0..allocLen(c.client, c.host_len)]); | |
| 300 | 348 | } |
| 301 | 349 | |
| 302 | return nread; | |
| 303 | } | |
| 350 | fn allocLen(client: *Client, host_len: usize) usize { | |
| 351 | return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size + | |
| 352 | client.write_buffer_size + client.read_buffer_size; | |
| 353 | } | |
| 304 | 354 | |
| 305 | pub const ReadError = error{ | |
| 306 | TlsFailure, | |
| 307 | TlsAlert, | |
| 308 | ConnectionTimedOut, | |
| 309 | ConnectionResetByPeer, | |
| 310 | UnexpectedReadFailure, | |
| 311 | EndOfStream, | |
| 355 | fn host(tls: *Tls) []u8 { | |
| 356 | const base: [*]u8 = @ptrCast(tls); | |
| 357 | return base[@sizeOf(Tls)..][0..tls.connection.host_len]; | |
| 358 | } | |
| 312 | 359 | }; |
| 313 | 360 | |
| 314 | pub const Reader = std.io.GenericReader(*Connection, ReadError, read); | |
| 315 | ||
| 316 | pub fn reader(conn: *Connection) Reader { | |
| 317 | return Reader{ .context = conn }; | |
| 318 | } | |
| 361 | pub const ReadError = std.crypto.tls.Client.ReadError || std.net.Stream.ReadError; | |
| 319 | 362 | |
| 320 | pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void { | |
| 321 | return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) { | |
| 322 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | |
| 323 | else => return error.UnexpectedWriteFailure, | |
| 363 | pub fn getReadError(c: *const Connection) ?ReadError { | |
| 364 | return switch (c.protocol) { | |
| 365 | .tls => { | |
| 366 | if (disable_tls) unreachable; | |
| 367 | const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 368 | return tls.client.read_err orelse c.stream_reader.getError(); | |
| 369 | }, | |
| 370 | .plain => { | |
| 371 | return c.stream_reader.getError(); | |
| 372 | }, | |
| 324 | 373 | }; |
| 325 | 374 | } |
| 326 | 375 | |
| 327 | pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void { | |
| 328 | if (conn.protocol == .tls) { | |
| 329 | if (disable_tls) unreachable; | |
| 330 | ||
| 331 | return conn.writeAllDirectTls(buffer); | |
| 332 | } | |
| 376 | fn getStream(c: *Connection) net.Stream { | |
| 377 | return c.stream_reader.getStream(); | |
| 378 | } | |
| 333 | 379 | |
| 334 | return conn.stream.writeAll(buffer) catch |err| switch (err) { | |
| 335 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | |
| 336 | else => return error.UnexpectedWriteFailure, | |
| 380 | fn host(c: *Connection) []u8 { | |
| 381 | return switch (c.protocol) { | |
| 382 | .tls => { | |
| 383 | if (disable_tls) unreachable; | |
| 384 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 385 | return tls.host(); | |
| 386 | }, | |
| 387 | .plain => { | |
| 388 | const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); | |
| 389 | return plain.host(); | |
| 390 | }, | |
| 337 | 391 | }; |
| 338 | 392 | } |
| 339 | 393 | |
| 340 | /// Writes the given buffer to the connection. | |
| 341 | pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize { | |
| 342 | if (conn.write_buf.len - conn.write_end < buffer.len) { | |
| 343 | try conn.flush(); | |
| 344 | ||
| 345 | if (buffer.len > conn.write_buf.len) { | |
| 346 | try conn.writeAllDirect(buffer); | |
| 347 | return buffer.len; | |
| 348 | } | |
| 394 | /// If this is called without calling `flush` or `end`, data will be | |
| 395 | /// dropped unsent. | |
| 396 | pub fn destroy(c: *Connection) void { | |
| 397 | c.getStream().close(); | |
| 398 | switch (c.protocol) { | |
| 399 | .tls => { | |
| 400 | if (disable_tls) unreachable; | |
| 401 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 402 | tls.destroy(); | |
| 403 | }, | |
| 404 | .plain => { | |
| 405 | const plain: *Plain = @alignCast(@fieldParentPtr("connection", c)); | |
| 406 | plain.destroy(); | |
| 407 | }, | |
| 349 | 408 | } |
| 350 | ||
| 351 | @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer); | |
| 352 | conn.write_end += @intCast(buffer.len); | |
| 353 | ||
| 354 | return buffer.len; | |
| 355 | 409 | } |
| 356 | 410 | |
| 357 | /// Returns a buffer to be filled with exactly len bytes to write to the connection. | |
| 358 | pub fn allocWriteBuffer(conn: *Connection, len: BufferSize) WriteError![]u8 { | |
| 359 | if (conn.write_buf.len - conn.write_end < len) try conn.flush(); | |
| 360 | defer conn.write_end += len; | |
| 361 | return conn.write_buf[conn.write_end..][0..len]; | |
| 411 | /// HTTP protocol from client to server. | |
| 412 | /// This either goes directly to `stream_writer`, or to a TLS client. | |
| 413 | pub fn writer(c: *Connection) *Writer { | |
| 414 | return switch (c.protocol) { | |
| 415 | .tls => { | |
| 416 | if (disable_tls) unreachable; | |
| 417 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 418 | return &tls.client.writer; | |
| 419 | }, | |
| 420 | .plain => &c.stream_writer.interface, | |
| 421 | }; | |
| 362 | 422 | } |
| 363 | 423 | |
| 364 | /// Flushes the write buffer to the connection. | |
| 365 | pub fn flush(conn: *Connection) WriteError!void { | |
| 366 | if (conn.write_end == 0) return; | |
| 367 | ||
| 368 | try conn.writeAllDirect(conn.write_buf[0..conn.write_end]); | |
| 369 | conn.write_end = 0; | |
| 424 | /// HTTP protocol from server to client. | |
| 425 | /// This either comes directly from `stream_reader`, or from a TLS client. | |
| 426 | pub fn reader(c: *Connection) *Reader { | |
| 427 | return switch (c.protocol) { | |
| 428 | .tls => { | |
| 429 | if (disable_tls) unreachable; | |
| 430 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 431 | return &tls.client.reader; | |
| 432 | }, | |
| 433 | .plain => c.stream_reader.interface(), | |
| 434 | }; | |
| 370 | 435 | } |
| 371 | 436 | |
| 372 | pub const WriteError = error{ | |
| 373 | ConnectionResetByPeer, | |
| 374 | UnexpectedWriteFailure, | |
| 375 | }; | |
| 376 | ||
| 377 | pub const Writer = std.io.GenericWriter(*Connection, WriteError, write); | |
| 378 | ||
| 379 | pub fn writer(conn: *Connection) Writer { | |
| 380 | return Writer{ .context = conn }; | |
| 437 | pub fn flush(c: *Connection) Writer.Error!void { | |
| 438 | if (c.protocol == .tls) { | |
| 439 | if (disable_tls) unreachable; | |
| 440 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 441 | try tls.client.writer.flush(); | |
| 442 | } | |
| 443 | try c.stream_writer.interface.flush(); | |
| 381 | 444 | } |
| 382 | 445 | |
| 383 | /// Closes the connection. | |
| 384 | pub fn close(conn: *Connection, allocator: Allocator) void { | |
| 385 | if (conn.protocol == .tls) { | |
| 446 | /// If the connection is a TLS connection, sends the close_notify alert. | |
| 447 | /// | |
| 448 | /// Flushes all buffers. | |
| 449 | pub fn end(c: *Connection) Writer.Error!void { | |
| 450 | if (c.protocol == .tls) { | |
| 386 | 451 | if (disable_tls) unreachable; |
| 387 | ||
| 388 | // try to cleanly close the TLS connection, for any server that cares. | |
| 389 | _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {}; | |
| 390 | if (conn.tls_client.ssl_key_log) |key_log| key_log.file.close(); | |
| 391 | allocator.destroy(conn.tls_client); | |
| 452 | const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); | |
| 453 | try tls.client.end(); | |
| 392 | 454 | } |
| 393 | ||
| 394 | conn.stream.close(); | |
| 395 | allocator.free(conn.host); | |
| 455 | try c.stream_writer.interface.flush(); | |
| 396 | 456 | } |
| 397 | 457 | }; |
| 398 | 458 | |
| 399 | /// The mode of transport for requests. | |
| 400 | pub const RequestTransfer = union(enum) { | |
| 401 | content_length: u64, | |
| 402 | chunked: void, | |
| 403 | none: void, | |
| 404 | }; | |
| 405 | ||
| 406 | /// The decompressor for response messages. | |
| 407 | pub const Compression = union(enum) { | |
| 408 | //deflate: std.compress.flate.Decompress, | |
| 409 | //gzip: std.compress.flate.Decompress, | |
| 410 | // https://github.com/ziglang/zig/issues/18937 | |
| 411 | //zstd: ZstdDecompressor, | |
| 412 | none: void, | |
| 413 | }; | |
| 414 | ||
| 415 | /// A HTTP response originating from a server. | |
| 416 | 459 | pub const Response = struct { |
| 417 | version: http.Version, | |
| 418 | status: http.Status, | |
| 419 | reason: []const u8, | |
| 460 | request: *Request, | |
| 461 | /// Pointers in this struct are invalidated when the response body stream | |
| 462 | /// is initialized. | |
| 463 | head: Head, | |
| 464 | ||
| 465 | pub const Head = struct { | |
| 466 | bytes: []const u8, | |
| 467 | version: http.Version, | |
| 468 | status: http.Status, | |
| 469 | reason: []const u8, | |
| 470 | location: ?[]const u8 = null, | |
| 471 | content_type: ?[]const u8 = null, | |
| 472 | content_disposition: ?[]const u8 = null, | |
| 473 | ||
| 474 | keep_alive: bool, | |
| 475 | ||
| 476 | /// If present, the number of bytes in the response body. | |
| 477 | content_length: ?u64 = null, | |
| 478 | ||
| 479 | transfer_encoding: http.TransferEncoding = .none, | |
| 480 | content_encoding: http.ContentEncoding = .identity, | |
| 481 | ||
| 482 | pub const ParseError = error{ | |
| 483 | HttpConnectionHeaderUnsupported, | |
| 484 | HttpContentEncodingUnsupported, | |
| 485 | HttpHeaderContinuationsUnsupported, | |
| 486 | HttpHeadersInvalid, | |
| 487 | HttpTransferEncodingUnsupported, | |
| 488 | InvalidContentLength, | |
| 489 | }; | |
| 420 | 490 | |
| 421 | /// Points into the user-provided `server_header_buffer`. | |
| 422 | location: ?[]const u8 = null, | |
| 423 | /// Points into the user-provided `server_header_buffer`. | |
| 424 | content_type: ?[]const u8 = null, | |
| 425 | /// Points into the user-provided `server_header_buffer`. | |
| 426 | content_disposition: ?[]const u8 = null, | |
| 491 | pub fn parse(bytes: []const u8) ParseError!Head { | |
| 492 | var res: Head = .{ | |
| 493 | .bytes = bytes, | |
| 494 | .status = undefined, | |
| 495 | .reason = undefined, | |
| 496 | .version = undefined, | |
| 497 | .keep_alive = false, | |
| 498 | }; | |
| 499 | var it = mem.splitSequence(u8, bytes, "\r\n"); | |
| 427 | 500 | |
| 428 | keep_alive: bool, | |
| 501 | const first_line = it.first(); | |
| 502 | if (first_line.len < 12) return error.HttpHeadersInvalid; | |
| 429 | 503 | |
| 430 | /// If present, the number of bytes in the response body. | |
| 431 | content_length: ?u64 = null, | |
| 504 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 505 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 506 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 507 | else => return error.HttpHeadersInvalid, | |
| 508 | }; | |
| 509 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 510 | const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); | |
| 511 | const reason = mem.trimLeft(u8, first_line[12..], " "); | |
| 512 | ||
| 513 | res.version = version; | |
| 514 | res.status = status; | |
| 515 | res.reason = reason; | |
| 516 | res.keep_alive = switch (version) { | |
| 517 | .@"HTTP/1.0" => false, | |
| 518 | .@"HTTP/1.1" => true, | |
| 519 | }; | |
| 432 | 520 | |
| 433 | /// If present, the transfer encoding of the response body, otherwise none. | |
| 434 | transfer_encoding: http.TransferEncoding = .none, | |
| 521 | while (it.next()) |line| { | |
| 522 | if (line.len == 0) return res; | |
| 523 | switch (line[0]) { | |
| 524 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 525 | else => {}, | |
| 526 | } | |
| 435 | 527 | |
| 436 | /// If present, the compression of the response body, otherwise identity (no compression). | |
| 437 | transfer_compression: http.ContentEncoding = .identity, | |
| 528 | var line_it = mem.splitScalar(u8, line, ':'); | |
| 529 | const header_name = line_it.next().?; | |
| 530 | const header_value = mem.trim(u8, line_it.rest(), " \t"); | |
| 531 | if (header_name.len == 0) return error.HttpHeadersInvalid; | |
| 532 | ||
| 533 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 534 | res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); | |
| 535 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { | |
| 536 | res.content_type = header_value; | |
| 537 | } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { | |
| 538 | res.location = header_value; | |
| 539 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { | |
| 540 | res.content_disposition = header_value; | |
| 541 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 542 | // Transfer-Encoding: second, first | |
| 543 | // Transfer-Encoding: deflate, chunked | |
| 544 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | |
| 545 | ||
| 546 | const first = iter.first(); | |
| 547 | const trimmed_first = mem.trim(u8, first, " "); | |
| 548 | ||
| 549 | var next: ?[]const u8 = first; | |
| 550 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { | |
| 551 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding | |
| 552 | res.transfer_encoding = transfer; | |
| 553 | ||
| 554 | next = iter.next(); | |
| 555 | } | |
| 438 | 556 | |
| 439 | parser: proto.HeadersParser, | |
| 440 | compression: Compression = .none, | |
| 557 | if (next) |second| { | |
| 558 | const trimmed_second = mem.trim(u8, second, " "); | |
| 441 | 559 | |
| 442 | /// Whether the response body should be skipped. Any data read from the | |
| 443 | /// response body will be discarded. | |
| 444 | skip: bool = false, | |
| 560 | if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { | |
| 561 | if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported | |
| 562 | res.content_encoding = transfer; | |
| 563 | } else { | |
| 564 | return error.HttpTransferEncodingUnsupported; | |
| 565 | } | |
| 566 | } | |
| 445 | 567 | |
| 446 | pub const ParseError = error{ | |
| 447 | HttpHeadersInvalid, | |
| 448 | HttpHeaderContinuationsUnsupported, | |
| 449 | HttpTransferEncodingUnsupported, | |
| 450 | HttpConnectionHeaderUnsupported, | |
| 451 | InvalidContentLength, | |
| 452 | CompressionUnsupported, | |
| 453 | }; | |
| 568 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 569 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 570 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | |
| 454 | 571 | |
| 455 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { | |
| 456 | var it = mem.splitSequence(u8, bytes, "\r\n"); | |
| 572 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; | |
| 457 | 573 | |
| 458 | const first_line = it.next().?; | |
| 459 | if (first_line.len < 12) { | |
| 460 | return error.HttpHeadersInvalid; | |
| 461 | } | |
| 574 | res.content_length = content_length; | |
| 575 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 576 | if (res.content_encoding != .identity) return error.HttpHeadersInvalid; | |
| 462 | 577 | |
| 463 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 464 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 465 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 466 | else => return error.HttpHeadersInvalid, | |
| 467 | }; | |
| 468 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 469 | const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); | |
| 470 | const reason = mem.trimStart(u8, first_line[12..], " "); | |
| 471 | ||
| 472 | res.version = version; | |
| 473 | res.status = status; | |
| 474 | res.reason = reason; | |
| 475 | res.keep_alive = switch (version) { | |
| 476 | .@"HTTP/1.0" => false, | |
| 477 | .@"HTTP/1.1" => true, | |
| 478 | }; | |
| 578 | const trimmed = mem.trim(u8, header_value, " "); | |
| 479 | 579 | |
| 480 | while (it.next()) |line| { | |
| 481 | if (line.len == 0) return; | |
| 482 | switch (line[0]) { | |
| 483 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 484 | else => {}, | |
| 485 | } | |
| 486 | ||
| 487 | var line_it = mem.splitScalar(u8, line, ':'); | |
| 488 | const header_name = line_it.next().?; | |
| 489 | const header_value = mem.trim(u8, line_it.rest(), " \t"); | |
| 490 | if (header_name.len == 0) return error.HttpHeadersInvalid; | |
| 491 | ||
| 492 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 493 | res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); | |
| 494 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { | |
| 495 | res.content_type = header_value; | |
| 496 | } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { | |
| 497 | res.location = header_value; | |
| 498 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { | |
| 499 | res.content_disposition = header_value; | |
| 500 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 501 | // Transfer-Encoding: second, first | |
| 502 | // Transfer-Encoding: deflate, chunked | |
| 503 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | |
| 504 | ||
| 505 | const first = iter.first(); | |
| 506 | const trimmed_first = mem.trim(u8, first, " "); | |
| 507 | ||
| 508 | var next: ?[]const u8 = first; | |
| 509 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { | |
| 510 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding | |
| 511 | res.transfer_encoding = transfer; | |
| 512 | ||
| 513 | next = iter.next(); | |
| 514 | } | |
| 515 | ||
| 516 | if (next) |second| { | |
| 517 | const trimmed_second = mem.trim(u8, second, " "); | |
| 518 | ||
| 519 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { | |
| 520 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported | |
| 521 | res.transfer_compression = transfer; | |
| 580 | if (http.ContentEncoding.fromString(trimmed)) |ce| { | |
| 581 | res.content_encoding = ce; | |
| 522 | 582 | } else { |
| 523 | return error.HttpTransferEncodingUnsupported; | |
| 583 | return error.HttpContentEncodingUnsupported; | |
| 524 | 584 | } |
| 525 | 585 | } |
| 526 | ||
| 527 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 528 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 529 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | |
| 530 | ||
| 531 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; | |
| 532 | ||
| 533 | res.content_length = content_length; | |
| 534 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 535 | if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; | |
| 536 | ||
| 537 | const trimmed = mem.trim(u8, header_value, " "); | |
| 538 | ||
| 539 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 540 | res.transfer_compression = ce; | |
| 541 | } else { | |
| 542 | return error.HttpTransferEncodingUnsupported; | |
| 543 | } | |
| 544 | 586 | } |
| 587 | return error.HttpHeadersInvalid; // missing empty line | |
| 545 | 588 | } |
| 546 | return error.HttpHeadersInvalid; // missing empty line | |
| 547 | } | |
| 548 | 589 | |
| 549 | test parse { | |
| 550 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 551 | "LOcation:url\r\n" ++ | |
| 552 | "content-tYpe: text/plain\r\n" ++ | |
| 553 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 554 | "content-Length:10\r\n" ++ | |
| 555 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 556 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 557 | ||
| 558 | var header_buffer: [1024]u8 = undefined; | |
| 559 | var res = Response{ | |
| 560 | .status = undefined, | |
| 561 | .reason = undefined, | |
| 562 | .version = undefined, | |
| 563 | .keep_alive = false, | |
| 564 | .parser = .init(&header_buffer), | |
| 565 | }; | |
| 590 | test parse { | |
| 591 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 592 | "LOcation:url\r\n" ++ | |
| 593 | "content-tYpe: text/plain\r\n" ++ | |
| 594 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 595 | "content-Length:10\r\n" ++ | |
| 596 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 597 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 598 | ||
| 599 | const head = try Head.parse(response_bytes); | |
| 600 | ||
| 601 | try testing.expectEqual(.@"HTTP/1.1", head.version); | |
| 602 | try testing.expectEqualStrings("OK", head.reason); | |
| 603 | try testing.expectEqual(.ok, head.status); | |
| 604 | ||
| 605 | try testing.expectEqualStrings("url", head.location.?); | |
| 606 | try testing.expectEqualStrings("text/plain", head.content_type.?); | |
| 607 | try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); | |
| 608 | ||
| 609 | try testing.expectEqual(true, head.keep_alive); | |
| 610 | try testing.expectEqual(10, head.content_length.?); | |
| 611 | try testing.expectEqual(.chunked, head.transfer_encoding); | |
| 612 | try testing.expectEqual(.deflate, head.content_encoding); | |
| 613 | } | |
| 566 | 614 | |
| 567 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); | |
| 568 | res.parser.header_bytes_len = response_bytes.len; | |
| 615 | pub fn iterateHeaders(h: Head) http.HeaderIterator { | |
| 616 | return .init(h.bytes); | |
| 617 | } | |
| 569 | 618 | |
| 570 | try res.parse(response_bytes); | |
| 619 | test iterateHeaders { | |
| 620 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 621 | "LOcation:url\r\n" ++ | |
| 622 | "content-tYpe: text/plain\r\n" ++ | |
| 623 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 624 | "content-Length:10\r\n" ++ | |
| 625 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 626 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 627 | ||
| 628 | const head = try Head.parse(response_bytes); | |
| 629 | var it = head.iterateHeaders(); | |
| 630 | { | |
| 631 | const header = it.next().?; | |
| 632 | try testing.expectEqualStrings("LOcation", header.name); | |
| 633 | try testing.expectEqualStrings("url", header.value); | |
| 634 | try testing.expect(!it.is_trailer); | |
| 635 | } | |
| 636 | { | |
| 637 | const header = it.next().?; | |
| 638 | try testing.expectEqualStrings("content-tYpe", header.name); | |
| 639 | try testing.expectEqualStrings("text/plain", header.value); | |
| 640 | try testing.expect(!it.is_trailer); | |
| 641 | } | |
| 642 | { | |
| 643 | const header = it.next().?; | |
| 644 | try testing.expectEqualStrings("content-disposition", header.name); | |
| 645 | try testing.expectEqualStrings("attachment; filename=example.txt", header.value); | |
| 646 | try testing.expect(!it.is_trailer); | |
| 647 | } | |
| 648 | { | |
| 649 | const header = it.next().?; | |
| 650 | try testing.expectEqualStrings("content-Length", header.name); | |
| 651 | try testing.expectEqualStrings("10", header.value); | |
| 652 | try testing.expect(!it.is_trailer); | |
| 653 | } | |
| 654 | { | |
| 655 | const header = it.next().?; | |
| 656 | try testing.expectEqualStrings("TRansfer-encoding", header.name); | |
| 657 | try testing.expectEqualStrings("deflate, chunked", header.value); | |
| 658 | try testing.expect(!it.is_trailer); | |
| 659 | } | |
| 660 | { | |
| 661 | const header = it.next().?; | |
| 662 | try testing.expectEqualStrings("connectioN", header.name); | |
| 663 | try testing.expectEqualStrings("keep-alive", header.value); | |
| 664 | try testing.expect(!it.is_trailer); | |
| 665 | } | |
| 666 | try testing.expectEqual(null, it.next()); | |
| 667 | } | |
| 571 | 668 | |
| 572 | try testing.expectEqual(.@"HTTP/1.1", res.version); | |
| 573 | try testing.expectEqualStrings("OK", res.reason); | |
| 574 | try testing.expectEqual(.ok, res.status); | |
| 669 | inline fn int64(array: *const [8]u8) u64 { | |
| 670 | return @bitCast(array.*); | |
| 671 | } | |
| 575 | 672 | |
| 576 | try testing.expectEqualStrings("url", res.location.?); | |
| 577 | try testing.expectEqualStrings("text/plain", res.content_type.?); | |
| 578 | try testing.expectEqualStrings("attachment; filename=example.txt", res.content_disposition.?); | |
| 673 | fn parseInt3(text: *const [3]u8) u10 { | |
| 674 | const nnn: @Vector(3, u8) = text.*; | |
| 675 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 676 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 677 | return @reduce(.Add, (nnn -% zero) *% mmm); | |
| 678 | } | |
| 579 | 679 | |
| 580 | try testing.expectEqual(true, res.keep_alive); | |
| 581 | try testing.expectEqual(10, res.content_length.?); | |
| 582 | try testing.expectEqual(.chunked, res.transfer_encoding); | |
| 583 | try testing.expectEqual(.deflate, res.transfer_compression); | |
| 584 | } | |
| 680 | test parseInt3 { | |
| 681 | const expectEqual = testing.expectEqual; | |
| 682 | try expectEqual(@as(u10, 0), parseInt3("000")); | |
| 683 | try expectEqual(@as(u10, 418), parseInt3("418")); | |
| 684 | try expectEqual(@as(u10, 999), parseInt3("999")); | |
| 685 | } | |
| 585 | 686 | |
| 586 | inline fn int64(array: *const [8]u8) u64 { | |
| 587 | return @bitCast(array.*); | |
| 588 | } | |
| 687 | /// Help the programmer avoid bugs by calling this when the string | |
| 688 | /// memory of `Head` becomes invalidated. | |
| 689 | fn invalidateStrings(h: *Head) void { | |
| 690 | h.bytes = undefined; | |
| 691 | h.reason = undefined; | |
| 692 | if (h.location) |*s| s.* = undefined; | |
| 693 | if (h.content_type) |*s| s.* = undefined; | |
| 694 | if (h.content_disposition) |*s| s.* = undefined; | |
| 695 | } | |
| 696 | }; | |
| 589 | 697 | |
| 590 | fn parseInt3(text: *const [3]u8) u10 { | |
| 591 | const nnn: @Vector(3, u8) = text.*; | |
| 592 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 593 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 594 | return @reduce(.Add, (nnn -% zero) *% mmm); | |
| 698 | /// If compressed body has been negotiated this will return compressed bytes. | |
| 699 | /// | |
| 700 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 701 | /// available via `bodyErr`. | |
| 702 | /// | |
| 703 | /// Asserts that this function is only called once. | |
| 704 | /// | |
| 705 | /// See also: | |
| 706 | /// * `readerDecompressing` | |
| 707 | pub fn reader(response: *Response, buffer: []u8) *Reader { | |
| 708 | response.head.invalidateStrings(); | |
| 709 | const req = response.request; | |
| 710 | if (!req.method.responseHasBody()) return .ending; | |
| 711 | const head = &response.head; | |
| 712 | return req.reader.bodyReader(buffer, head.transfer_encoding, head.content_length); | |
| 595 | 713 | } |
| 596 | 714 | |
| 597 | test parseInt3 { | |
| 598 | const expectEqual = testing.expectEqual; | |
| 599 | try expectEqual(@as(u10, 0), parseInt3("000")); | |
| 600 | try expectEqual(@as(u10, 418), parseInt3("418")); | |
| 601 | try expectEqual(@as(u10, 999), parseInt3("999")); | |
| 715 | /// If compressed body has been negotiated this will return decompressed bytes. | |
| 716 | /// | |
| 717 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 718 | /// available via `bodyErr`. | |
| 719 | /// | |
| 720 | /// Asserts that this function is only called once. | |
| 721 | /// | |
| 722 | /// See also: | |
| 723 | /// * `reader` | |
| 724 | pub fn readerDecompressing( | |
| 725 | response: *Response, | |
| 726 | decompressor: *http.Decompressor, | |
| 727 | decompression_buffer: []u8, | |
| 728 | ) *Reader { | |
| 729 | response.head.invalidateStrings(); | |
| 730 | const head = &response.head; | |
| 731 | return response.request.reader.bodyReaderDecompressing( | |
| 732 | head.transfer_encoding, | |
| 733 | head.content_length, | |
| 734 | head.content_encoding, | |
| 735 | decompressor, | |
| 736 | decompression_buffer, | |
| 737 | ); | |
| 602 | 738 | } |
| 603 | 739 | |
| 604 | pub fn iterateHeaders(r: Response) http.HeaderIterator { | |
| 605 | return .init(r.parser.get()); | |
| 740 | /// After receiving `error.ReadFailed` from the `Reader` returned by | |
| 741 | /// `reader` or `readerDecompressing`, this function accesses the | |
| 742 | /// more specific error code. | |
| 743 | pub fn bodyErr(response: *const Response) ?http.Reader.BodyError { | |
| 744 | return response.request.reader.body_err; | |
| 606 | 745 | } |
| 607 | 746 | |
| 608 | test iterateHeaders { | |
| 609 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 610 | "LOcation:url\r\n" ++ | |
| 611 | "content-tYpe: text/plain\r\n" ++ | |
| 612 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 613 | "content-Length:10\r\n" ++ | |
| 614 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 615 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 616 | ||
| 617 | var header_buffer: [1024]u8 = undefined; | |
| 618 | var res = Response{ | |
| 619 | .status = undefined, | |
| 620 | .reason = undefined, | |
| 621 | .version = undefined, | |
| 622 | .keep_alive = false, | |
| 623 | .parser = .init(&header_buffer), | |
| 747 | pub fn iterateTrailers(response: *const Response) http.HeaderIterator { | |
| 748 | const r = &response.request.reader; | |
| 749 | assert(r.state == .ready); | |
| 750 | return .{ | |
| 751 | .bytes = r.trailers, | |
| 752 | .index = 0, | |
| 753 | .is_trailer = true, | |
| 624 | 754 | }; |
| 625 | ||
| 626 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); | |
| 627 | res.parser.header_bytes_len = response_bytes.len; | |
| 628 | ||
| 629 | var it = res.iterateHeaders(); | |
| 630 | { | |
| 631 | const header = it.next().?; | |
| 632 | try testing.expectEqualStrings("LOcation", header.name); | |
| 633 | try testing.expectEqualStrings("url", header.value); | |
| 634 | try testing.expect(!it.is_trailer); | |
| 635 | } | |
| 636 | { | |
| 637 | const header = it.next().?; | |
| 638 | try testing.expectEqualStrings("content-tYpe", header.name); | |
| 639 | try testing.expectEqualStrings("text/plain", header.value); | |
| 640 | try testing.expect(!it.is_trailer); | |
| 641 | } | |
| 642 | { | |
| 643 | const header = it.next().?; | |
| 644 | try testing.expectEqualStrings("content-disposition", header.name); | |
| 645 | try testing.expectEqualStrings("attachment; filename=example.txt", header.value); | |
| 646 | try testing.expect(!it.is_trailer); | |
| 647 | } | |
| 648 | { | |
| 649 | const header = it.next().?; | |
| 650 | try testing.expectEqualStrings("content-Length", header.name); | |
| 651 | try testing.expectEqualStrings("10", header.value); | |
| 652 | try testing.expect(!it.is_trailer); | |
| 653 | } | |
| 654 | { | |
| 655 | const header = it.next().?; | |
| 656 | try testing.expectEqualStrings("TRansfer-encoding", header.name); | |
| 657 | try testing.expectEqualStrings("deflate, chunked", header.value); | |
| 658 | try testing.expect(!it.is_trailer); | |
| 659 | } | |
| 660 | { | |
| 661 | const header = it.next().?; | |
| 662 | try testing.expectEqualStrings("connectioN", header.name); | |
| 663 | try testing.expectEqualStrings("keep-alive", header.value); | |
| 664 | try testing.expect(!it.is_trailer); | |
| 665 | } | |
| 666 | try testing.expectEqual(null, it.next()); | |
| 667 | 755 | } |
| 668 | 756 | }; |
| 669 | 757 | |
| 670 | /// A HTTP request that has been sent. | |
| 671 | /// | |
| 672 | /// Order of operations: open -> send[ -> write -> finish] -> wait -> read | |
| 673 | 758 | pub const Request = struct { |
| 759 | /// This field is provided so that clients can observe redirected URIs. | |
| 760 | /// | |
| 761 | /// Its backing memory is externally provided by API users when creating a | |
| 762 | /// request, and then again provided externally via `redirect_buffer` to | |
| 763 | /// `receiveHead`. | |
| 674 | 764 | uri: Uri, |
| 675 | 765 | client: *Client, |
| 676 | 766 | /// This is null when the connection is released. |
| 677 | 767 | connection: ?*Connection, |
| 768 | reader: http.Reader, | |
| 678 | 769 | keep_alive: bool, |
| 679 | 770 | |
| 680 | 771 | method: http.Method, |
| 681 | 772 | version: http.Version = .@"HTTP/1.1", |
| 682 | transfer_encoding: RequestTransfer, | |
| 773 | transfer_encoding: TransferEncoding, | |
| 683 | 774 | redirect_behavior: RedirectBehavior, |
| 775 | accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding, | |
| 684 | 776 | |
| 685 | 777 | /// Whether the request should handle a 100-continue response before sending the request body. |
| 686 | 778 | handle_continue: bool, |
| 687 | 779 | |
| 688 | /// The response associated with this request. | |
| 689 | /// | |
| 690 | /// This field is undefined until `wait` is called. | |
| 691 | response: Response, | |
| 692 | ||
| 693 | 780 | /// Standard headers that have default, but overridable, behavior. |
| 694 | 781 | headers: Headers, |
| 695 | 782 | |
| ... | ... | @@ -703,6 +790,20 @@ pub const Request = struct { |
| 703 | 790 | /// Externally-owned; must outlive the Request. |
| 704 | 791 | privileged_headers: []const http.Header, |
| 705 | 792 | |
| 793 | pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: { | |
| 794 | var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false); | |
| 795 | result[@intFromEnum(http.ContentEncoding.gzip)] = true; | |
| 796 | result[@intFromEnum(http.ContentEncoding.deflate)] = true; | |
| 797 | result[@intFromEnum(http.ContentEncoding.identity)] = true; | |
| 798 | break :b result; | |
| 799 | }; | |
| 800 | ||
| 801 | pub const TransferEncoding = union(enum) { | |
| 802 | content_length: u64, | |
| 803 | chunked: void, | |
| 804 | none: void, | |
| 805 | }; | |
| 806 | ||
| 706 | 807 | pub const Headers = struct { |
| 707 | 808 | host: Value = .default, |
| 708 | 809 | authorization: Value = .default, |
| ... | ... | @@ -728,6 +829,11 @@ pub const Request = struct { |
| 728 | 829 | unhandled = std.math.maxInt(u16), |
| 729 | 830 | _, |
| 730 | 831 | |
| 832 | pub fn init(n: u16) RedirectBehavior { | |
| 833 | assert(n != std.math.maxInt(u16)); | |
| 834 | return @enumFromInt(n); | |
| 835 | } | |
| 836 | ||
| 731 | 837 | pub fn subtractOne(rb: *RedirectBehavior) void { |
| 732 | 838 | switch (rb.*) { |
| 733 | 839 | .not_allowed => unreachable, |
| ... | ... | @@ -742,98 +848,110 @@ pub const Request = struct { |
| 742 | 848 | } |
| 743 | 849 | }; |
| 744 | 850 | |
| 745 | /// Frees all resources associated with the request. | |
| 746 | pub fn deinit(req: *Request) void { | |
| 747 | if (req.connection) |connection| { | |
| 748 | if (!req.response.parser.done) { | |
| 749 | // If the response wasn't fully read, then we need to close the connection. | |
| 750 | connection.closing = true; | |
| 751 | } | |
| 752 | req.client.connection_pool.release(req.client.allocator, connection); | |
| 851 | /// Returns the request's `Connection` back to the pool of the `Client`. | |
| 852 | pub fn deinit(r: *Request) void { | |
| 853 | if (r.connection) |connection| { | |
| 854 | connection.closing = connection.closing or switch (r.reader.state) { | |
| 855 | .ready => false, | |
| 856 | .received_head => r.method.requestHasBody(), | |
| 857 | else => true, | |
| 858 | }; | |
| 859 | r.client.connection_pool.release(connection); | |
| 753 | 860 | } |
| 754 | req.* = undefined; | |
| 861 | r.* = undefined; | |
| 755 | 862 | } |
| 756 | 863 | |
| 757 | // This function must deallocate all resources associated with the request, | |
| 758 | // or keep those which will be used. | |
| 759 | // This needs to be kept in sync with deinit and request. | |
| 760 | fn redirect(req: *Request, uri: Uri) !void { | |
| 761 | assert(req.response.parser.done); | |
| 762 | ||
| 763 | req.client.connection_pool.release(req.client.allocator, req.connection.?); | |
| 764 | req.connection = null; | |
| 765 | ||
| 766 | var server_header: std.heap.FixedBufferAllocator = .init(req.response.parser.header_bytes_buffer); | |
| 767 | defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..]; | |
| 768 | const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); | |
| 769 | ||
| 770 | const new_host = valid_uri.host.?.raw; | |
| 771 | const prev_host = req.uri.host.?.raw; | |
| 772 | const keep_privileged_headers = | |
| 773 | std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and | |
| 774 | std.ascii.endsWithIgnoreCase(new_host, prev_host) and | |
| 775 | (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.'); | |
| 776 | if (!keep_privileged_headers) { | |
| 777 | // When redirecting to a different domain, strip privileged headers. | |
| 778 | req.privileged_headers = &.{}; | |
| 779 | } | |
| 780 | ||
| 781 | if (switch (req.response.status) { | |
| 782 | .see_other => true, | |
| 783 | .moved_permanently, .found => req.method == .POST, | |
| 784 | else => false, | |
| 785 | }) { | |
| 786 | // A redirect to a GET must change the method and remove the body. | |
| 787 | req.method = .GET; | |
| 788 | req.transfer_encoding = .none; | |
| 789 | req.headers.content_type = .omit; | |
| 790 | } | |
| 791 | ||
| 792 | if (req.transfer_encoding != .none) { | |
| 793 | // The request body has already been sent. The request is | |
| 794 | // still in a valid state, but the redirect must be handled | |
| 795 | // manually. | |
| 796 | return error.RedirectRequiresResend; | |
| 797 | } | |
| 864 | /// Sends and flushes a complete request as only HTTP head, no body. | |
| 865 | pub fn sendBodiless(r: *Request) Writer.Error!void { | |
| 866 | try sendBodilessUnflushed(r); | |
| 867 | try r.connection.?.flush(); | |
| 868 | } | |
| 798 | 869 | |
| 799 | req.uri = valid_uri; | |
| 800 | req.connection = try req.client.connect(new_host, uriPort(valid_uri, protocol), protocol); | |
| 801 | req.redirect_behavior.subtractOne(); | |
| 802 | req.response.parser.reset(); | |
| 803 | ||
| 804 | req.response = .{ | |
| 805 | .version = undefined, | |
| 806 | .status = undefined, | |
| 807 | .reason = undefined, | |
| 808 | .keep_alive = undefined, | |
| 809 | .parser = req.response.parser, | |
| 810 | }; | |
| 870 | /// Sends but does not flush a complete request as only HTTP head, no body. | |
| 871 | pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { | |
| 872 | assert(r.transfer_encoding == .none); | |
| 873 | assert(!r.method.requestHasBody()); | |
| 874 | try sendHead(r); | |
| 811 | 875 | } |
| 812 | 876 | |
| 813 | pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; | |
| 877 | /// Transfers the HTTP head over the connection and flushes. | |
| 878 | /// | |
| 879 | /// See also: | |
| 880 | /// * `sendBodyUnflushed` | |
| 881 | pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 882 | const result = try sendBodyUnflushed(r, buffer); | |
| 883 | try r.connection.?.flush(); | |
| 884 | return result; | |
| 885 | } | |
| 814 | 886 | |
| 815 | /// Send the HTTP request headers to the server. | |
| 816 | pub fn send(req: *Request) SendError!void { | |
| 817 | if (!req.method.requestHasBody() and req.transfer_encoding != .none) | |
| 818 | return error.UnsupportedTransferEncoding; | |
| 887 | /// Transfers the HTTP head and body over the connection and flushes. | |
| 888 | pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void { | |
| 889 | r.transfer_encoding = .{ .content_length = body.len }; | |
| 890 | var bw = try sendBodyUnflushed(r, body); | |
| 891 | bw.writer.end = body.len; | |
| 892 | try bw.end(); | |
| 893 | try r.connection.?.flush(); | |
| 894 | } | |
| 819 | 895 | |
| 820 | const connection = req.connection.?; | |
| 821 | var connection_writer_adapter = connection.writer().adaptToNewApi(); | |
| 822 | const w = &connection_writer_adapter.new_interface; | |
| 823 | sendAdapted(req, connection, w) catch |err| switch (err) { | |
| 824 | error.WriteFailed => return connection_writer_adapter.err.?, | |
| 825 | else => |e| return e, | |
| 896 | /// Transfers the HTTP head over the connection, which is not flushed until | |
| 897 | /// `BodyWriter.flush` or `BodyWriter.end` is called. | |
| 898 | /// | |
| 899 | /// See also: | |
| 900 | /// * `sendBody` | |
| 901 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 902 | assert(r.method.requestHasBody()); | |
| 903 | try sendHead(r); | |
| 904 | const http_protocol_output = r.connection.?.writer(); | |
| 905 | return switch (r.transfer_encoding) { | |
| 906 | .chunked => .{ | |
| 907 | .http_protocol_output = http_protocol_output, | |
| 908 | .state = .{ .chunked = .init }, | |
| 909 | .writer = .{ | |
| 910 | .buffer = buffer, | |
| 911 | .vtable = &.{ | |
| 912 | .drain = http.BodyWriter.chunkedDrain, | |
| 913 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 914 | }, | |
| 915 | }, | |
| 916 | }, | |
| 917 | .content_length => |len| .{ | |
| 918 | .http_protocol_output = http_protocol_output, | |
| 919 | .state = .{ .content_length = len }, | |
| 920 | .writer = .{ | |
| 921 | .buffer = buffer, | |
| 922 | .vtable = &.{ | |
| 923 | .drain = http.BodyWriter.contentLengthDrain, | |
| 924 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 925 | }, | |
| 926 | }, | |
| 927 | }, | |
| 928 | .none => .{ | |
| 929 | .http_protocol_output = http_protocol_output, | |
| 930 | .state = .none, | |
| 931 | .writer = .{ | |
| 932 | .buffer = buffer, | |
| 933 | .vtable = &.{ | |
| 934 | .drain = http.BodyWriter.noneDrain, | |
| 935 | .sendFile = http.BodyWriter.noneSendFile, | |
| 936 | }, | |
| 937 | }, | |
| 938 | }, | |
| 826 | 939 | }; |
| 827 | 940 | } |
| 828 | 941 | |
| 829 | fn sendAdapted(req: *Request, connection: *Connection, w: *std.io.Writer) !void { | |
| 830 | try req.method.format(w); | |
| 942 | /// Sends HTTP headers without flushing. | |
| 943 | fn sendHead(r: *Request) Writer.Error!void { | |
| 944 | const uri = r.uri; | |
| 945 | const connection = r.connection.?; | |
| 946 | const w = connection.writer(); | |
| 947 | ||
| 948 | try w.writeAll(@tagName(r.method)); | |
| 831 | 949 | try w.writeByte(' '); |
| 832 | 950 | |
| 833 | if (req.method == .CONNECT) { | |
| 834 | try req.uri.writeToStream(w, .{ .authority = true }); | |
| 951 | if (r.method == .CONNECT) { | |
| 952 | try uri.writeToStream(w, .{ .authority = true }); | |
| 835 | 953 | } else { |
| 836 | try req.uri.writeToStream(w, .{ | |
| 954 | try uri.writeToStream(w, .{ | |
| 837 | 955 | .scheme = connection.proxied, |
| 838 | 956 | .authentication = connection.proxied, |
| 839 | 957 | .authority = connection.proxied, |
| ... | ... | @@ -842,58 +960,64 @@ pub const Request = struct { |
| 842 | 960 | }); |
| 843 | 961 | } |
| 844 | 962 | try w.writeByte(' '); |
| 845 | try w.writeAll(@tagName(req.version)); | |
| 963 | try w.writeAll(@tagName(r.version)); | |
| 846 | 964 | try w.writeAll("\r\n"); |
| 847 | 965 | |
| 848 | if (try emitOverridableHeader("host: ", req.headers.host, w)) { | |
| 966 | if (try emitOverridableHeader("host: ", r.headers.host, w)) { | |
| 849 | 967 | try w.writeAll("host: "); |
| 850 | try req.uri.writeToStream(w, .{ .authority = true }); | |
| 968 | try uri.writeToStream(w, .{ .authority = true }); | |
| 851 | 969 | try w.writeAll("\r\n"); |
| 852 | 970 | } |
| 853 | 971 | |
| 854 | if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) { | |
| 855 | if (req.uri.user != null or req.uri.password != null) { | |
| 972 | if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { | |
| 973 | if (uri.user != null or uri.password != null) { | |
| 856 | 974 | try w.writeAll("authorization: "); |
| 857 | const authorization = try connection.allocWriteBuffer( | |
| 858 | @intCast(basic_authorization.valueLengthFromUri(req.uri)), | |
| 859 | ); | |
| 860 | assert(basic_authorization.value(req.uri, authorization).len == authorization.len); | |
| 975 | try basic_authorization.write(uri, w); | |
| 861 | 976 | try w.writeAll("\r\n"); |
| 862 | 977 | } |
| 863 | 978 | } |
| 864 | 979 | |
| 865 | if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) { | |
| 980 | if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { | |
| 866 | 981 | try w.writeAll("user-agent: zig/"); |
| 867 | 982 | try w.writeAll(builtin.zig_version_string); |
| 868 | 983 | try w.writeAll(" (std.http)\r\n"); |
| 869 | 984 | } |
| 870 | 985 | |
| 871 | if (try emitOverridableHeader("connection: ", req.headers.connection, w)) { | |
| 872 | if (req.keep_alive) { | |
| 986 | if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { | |
| 987 | if (r.keep_alive) { | |
| 873 | 988 | try w.writeAll("connection: keep-alive\r\n"); |
| 874 | 989 | } else { |
| 875 | 990 | try w.writeAll("connection: close\r\n"); |
| 876 | 991 | } |
| 877 | 992 | } |
| 878 | 993 | |
| 879 | if (try emitOverridableHeader("accept-encoding: ", req.headers.accept_encoding, w)) { | |
| 880 | // https://github.com/ziglang/zig/issues/18937 | |
| 881 | //try w.writeAll("accept-encoding: gzip, deflate, zstd\r\n"); | |
| 882 | try w.writeAll("accept-encoding: gzip, deflate\r\n"); | |
| 994 | if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { | |
| 995 | try w.writeAll("accept-encoding: "); | |
| 996 | for (r.accept_encoding, 0..) |enabled, i| { | |
| 997 | if (!enabled) continue; | |
| 998 | const tag: http.ContentEncoding = @enumFromInt(i); | |
| 999 | if (tag == .identity) continue; | |
| 1000 | const tag_name = @tagName(tag); | |
| 1001 | try w.ensureUnusedCapacity(tag_name.len + 2); | |
| 1002 | try w.writeAll(tag_name); | |
| 1003 | try w.writeAll(", "); | |
| 1004 | } | |
| 1005 | w.undo(2); | |
| 1006 | try w.writeAll("\r\n"); | |
| 883 | 1007 | } |
| 884 | 1008 | |
| 885 | switch (req.transfer_encoding) { | |
| 1009 | switch (r.transfer_encoding) { | |
| 886 | 1010 | .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), |
| 887 | 1011 | .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), |
| 888 | 1012 | .none => {}, |
| 889 | 1013 | } |
| 890 | 1014 | |
| 891 | if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) { | |
| 1015 | if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { | |
| 892 | 1016 | // The default is to omit content-type if not provided because |
| 893 | 1017 | // "application/octet-stream" is redundant. |
| 894 | 1018 | } |
| 895 | 1019 | |
| 896 | for (req.extra_headers) |header| { | |
| 1020 | for (r.extra_headers) |header| { | |
| 897 | 1021 | assert(header.name.len != 0); |
| 898 | 1022 | |
| 899 | 1023 | try w.writeAll(header.name); |
| ... | ... | @@ -904,8 +1028,8 @@ pub const Request = struct { |
| 904 | 1028 | |
| 905 | 1029 | if (connection.proxied) proxy: { |
| 906 | 1030 | const proxy = switch (connection.protocol) { |
| 907 | .plain => req.client.http_proxy, | |
| 908 | .tls => req.client.https_proxy, | |
| 1031 | .plain => r.client.http_proxy, | |
| 1032 | .tls => r.client.https_proxy, | |
| 909 | 1033 | } orelse break :proxy; |
| 910 | 1034 | |
| 911 | 1035 | const authorization = proxy.authorization orelse break :proxy; |
| ... | ... | @@ -915,282 +1039,200 @@ pub const Request = struct { |
| 915 | 1039 | } |
| 916 | 1040 | |
| 917 | 1041 | try w.writeAll("\r\n"); |
| 918 | ||
| 919 | try connection.flush(); | |
| 920 | } | |
| 921 | ||
| 922 | /// Returns true if the default behavior is required, otherwise handles | |
| 923 | /// writing (or not writing) the header. | |
| 924 | fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, w: anytype) !bool { | |
| 925 | switch (v) { | |
| 926 | .default => return true, | |
| 927 | .omit => return false, | |
| 928 | .override => |x| { | |
| 929 | try w.writeAll(prefix); | |
| 930 | try w.writeAll(x); | |
| 931 | try w.writeAll("\r\n"); | |
| 932 | return false; | |
| 933 | }, | |
| 934 | } | |
| 935 | 1042 | } |
| 936 | 1043 | |
| 937 | const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; | |
| 938 | ||
| 939 | const TransferReader = std.io.GenericReader(*Request, TransferReadError, transferRead); | |
| 940 | ||
| 941 | fn transferReader(req: *Request) TransferReader { | |
| 942 | return .{ .context = req }; | |
| 943 | } | |
| 944 | ||
| 945 | fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { | |
| 946 | if (req.response.parser.done) return 0; | |
| 947 | ||
| 948 | var index: usize = 0; | |
| 949 | while (index == 0) { | |
| 950 | const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); | |
| 951 | if (amt == 0 and req.response.parser.done) break; | |
| 952 | index += amt; | |
| 953 | } | |
| 954 | ||
| 955 | return index; | |
| 956 | } | |
| 1044 | pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{ | |
| 1045 | /// Server sent headers that did not conform to the HTTP protocol. | |
| 1046 | /// | |
| 1047 | /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be | |
| 1048 | /// passed directly to `Request.Head.parse`. | |
| 1049 | HttpHeadersInvalid, | |
| 1050 | TooManyHttpRedirects, | |
| 1051 | /// This can be avoided by calling `receiveHead` before sending the | |
| 1052 | /// request body. | |
| 1053 | RedirectRequiresResend, | |
| 1054 | HttpRedirectLocationMissing, | |
| 1055 | HttpRedirectLocationOversize, | |
| 1056 | HttpRedirectLocationInvalid, | |
| 1057 | HttpContentEncodingUnsupported, | |
| 1058 | HttpChunkInvalid, | |
| 1059 | HttpChunkTruncated, | |
| 1060 | HttpHeadersOversize, | |
| 1061 | UnsupportedUriScheme, | |
| 957 | 1062 | |
| 958 | pub const WaitError = RequestError || SendError || TransferReadError || | |
| 959 | proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || | |
| 960 | error{ | |
| 961 | TooManyHttpRedirects, | |
| 962 | RedirectRequiresResend, | |
| 963 | HttpRedirectLocationMissing, | |
| 964 | HttpRedirectLocationInvalid, | |
| 965 | CompressionInitializationFailed, | |
| 966 | CompressionUnsupported, | |
| 967 | }; | |
| 1063 | /// Sending the request failed. Error code can be found on the | |
| 1064 | /// `Connection` object. | |
| 1065 | WriteFailed, | |
| 1066 | }; | |
| 968 | 1067 | |
| 969 | /// Waits for a response from the server and parses any headers that are sent. | |
| 970 | /// This function will block until the final response is received. | |
| 971 | /// | |
| 972 | 1068 | /// If handling redirects and the request has no payload, then this |
| 973 | /// function will automatically follow redirects. If a request payload is | |
| 974 | /// present, then this function will error with | |
| 975 | /// error.RedirectRequiresResend. | |
| 1069 | /// function will automatically follow redirects. | |
| 1070 | /// | |
| 1071 | /// If a request payload is present, then this function will error with | |
| 1072 | /// `error.RedirectRequiresResend`. | |
| 1073 | /// | |
| 1074 | /// This function takes an auxiliary buffer to store the arbitrarily large | |
| 1075 | /// URI which may need to be merged with the previous URI, and that data | |
| 1076 | /// needs to survive across different connections, which is where the input | |
| 1077 | /// buffer lives. | |
| 976 | 1078 | /// |
| 977 | /// Must be called after `send` and, if any data was written to the request | |
| 978 | /// body, then also after `finish`. | |
| 979 | pub fn wait(req: *Request) WaitError!void { | |
| 1079 | /// `redirect_buffer` must outlive accesses to `Request.uri`. If this | |
| 1080 | /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` | |
| 1081 | /// is returned instead. This buffer may be empty if no redirects are to be | |
| 1082 | /// handled. | |
| 1083 | /// | |
| 1084 | /// If this fails with `error.ReadFailed` then the `Connection.getReadError` | |
| 1085 | /// method of `r.connection` can be used to get more detailed information. | |
| 1086 | pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { | |
| 1087 | var aux_buf = redirect_buffer; | |
| 980 | 1088 | while (true) { |
| 981 | // This while loop is for handling redirects, which means the request's | |
| 982 | // connection may be different than the previous iteration. However, it | |
| 983 | // is still guaranteed to be non-null with each iteration of this loop. | |
| 984 | const connection = req.connection.?; | |
| 985 | ||
| 986 | while (true) { // read headers | |
| 987 | try connection.fill(); | |
| 988 | ||
| 989 | const nchecked = try req.response.parser.checkCompleteHead(connection.peek()); | |
| 990 | connection.drop(@intCast(nchecked)); | |
| 1089 | const head_buffer = try r.reader.receiveHead(); | |
| 1090 | const response: Response = .{ | |
| 1091 | .request = r, | |
| 1092 | .head = Response.Head.parse(head_buffer) catch return error.HttpHeadersInvalid, | |
| 1093 | }; | |
| 1094 | const head = &response.head; | |
| 991 | 1095 | |
| 992 | if (req.response.parser.state.isContent()) break; | |
| 1096 | if (head.status == .@"continue") { | |
| 1097 | if (r.handle_continue) continue; | |
| 1098 | return response; // we're not handling the 100-continue | |
| 993 | 1099 | } |
| 994 | 1100 | |
| 995 | try req.response.parse(req.response.parser.get()); | |
| 996 | ||
| 997 | if (req.response.status == .@"continue") { | |
| 998 | // We're done parsing the continue response; reset to prepare | |
| 999 | // for the real response. | |
| 1000 | req.response.parser.done = true; | |
| 1001 | req.response.parser.reset(); | |
| 1002 | ||
| 1003 | if (req.handle_continue) | |
| 1004 | continue; | |
| 1005 | ||
| 1006 | return; // we're not handling the 100-continue | |
| 1007 | } | |
| 1101 | // This while loop is for handling redirects, which means the request's | |
| 1102 | // connection may be different than the previous iteration. However, it | |
| 1103 | // is still guaranteed to be non-null with each iteration of this loop. | |
| 1104 | const connection = r.connection.?; | |
| 1008 | 1105 | |
| 1009 | // we're switching protocols, so this connection is no longer doing http | |
| 1010 | if (req.method == .CONNECT and req.response.status.class() == .success) { | |
| 1106 | if (r.method == .CONNECT and head.status.class() == .success) { | |
| 1107 | // This connection is no longer doing HTTP. | |
| 1011 | 1108 | connection.closing = false; |
| 1012 | req.response.parser.done = true; | |
| 1013 | return; // the connection is not HTTP past this point | |
| 1109 | return response; | |
| 1014 | 1110 | } |
| 1015 | 1111 | |
| 1016 | connection.closing = !req.response.keep_alive or !req.keep_alive; | |
| 1112 | connection.closing = !head.keep_alive or !r.keep_alive; | |
| 1017 | 1113 | |
| 1018 | 1114 | // Any response to a HEAD request and any response with a 1xx |
| 1019 | 1115 | // (Informational), 204 (No Content), or 304 (Not Modified) status |
| 1020 | 1116 | // code is always terminated by the first empty line after the |
| 1021 | 1117 | // header fields, regardless of the header fields present in the |
| 1022 | 1118 | // message. |
| 1023 | if (req.method == .HEAD or req.response.status.class() == .informational or | |
| 1024 | req.response.status == .no_content or req.response.status == .not_modified) | |
| 1119 | if (r.method == .HEAD or head.status.class() == .informational or | |
| 1120 | head.status == .no_content or head.status == .not_modified) | |
| 1025 | 1121 | { |
| 1026 | req.response.parser.done = true; | |
| 1027 | return; // The response is empty; no further setup or redirection is necessary. | |
| 1028 | } | |
| 1029 | ||
| 1030 | switch (req.response.transfer_encoding) { | |
| 1031 | .none => { | |
| 1032 | if (req.response.content_length) |cl| { | |
| 1033 | req.response.parser.next_chunk_length = cl; | |
| 1034 | ||
| 1035 | if (cl == 0) req.response.parser.done = true; | |
| 1036 | } else { | |
| 1037 | // read until the connection is closed | |
| 1038 | req.response.parser.next_chunk_length = std.math.maxInt(u64); | |
| 1039 | } | |
| 1040 | }, | |
| 1041 | .chunked => { | |
| 1042 | req.response.parser.next_chunk_length = 0; | |
| 1043 | req.response.parser.state = .chunk_head_size; | |
| 1044 | }, | |
| 1122 | return response; | |
| 1045 | 1123 | } |
| 1046 | 1124 | |
| 1047 | if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) { | |
| 1048 | // skip the body of the redirect response, this will at least | |
| 1049 | // leave the connection in a known good state. | |
| 1050 | req.response.skip = true; | |
| 1051 | assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary | |
| 1052 | ||
| 1053 | if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; | |
| 1054 | ||
| 1055 | const location = req.response.location orelse | |
| 1056 | return error.HttpRedirectLocationMissing; | |
| 1057 | ||
| 1058 | // This mutates the beginning of header_bytes_buffer and uses that | |
| 1059 | // for the backing memory of the returned Uri. | |
| 1060 | try req.redirect(req.uri.resolve_inplace( | |
| 1061 | location, | |
| 1062 | &req.response.parser.header_bytes_buffer, | |
| 1063 | ) catch |err| switch (err) { | |
| 1064 | error.UnexpectedCharacter, | |
| 1065 | error.InvalidFormat, | |
| 1066 | error.InvalidPort, | |
| 1067 | => return error.HttpRedirectLocationInvalid, | |
| 1068 | error.NoSpaceLeft => return error.HttpHeadersOversize, | |
| 1069 | }); | |
| 1070 | try req.send(); | |
| 1071 | } else { | |
| 1072 | req.response.skip = false; | |
| 1073 | if (!req.response.parser.done) { | |
| 1074 | switch (req.response.transfer_compression) { | |
| 1075 | .identity => req.response.compression = .none, | |
| 1076 | .compress, .@"x-compress" => return error.CompressionUnsupported, | |
| 1077 | // I'm about to upstream my http.Client rewrite | |
| 1078 | .deflate => return error.CompressionUnsupported, | |
| 1079 | // I'm about to upstream my http.Client rewrite | |
| 1080 | .gzip, .@"x-gzip" => return error.CompressionUnsupported, | |
| 1081 | // https://github.com/ziglang/zig/issues/18937 | |
| 1082 | //.zstd => req.response.compression = .{ | |
| 1083 | // .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), | |
| 1084 | //}, | |
| 1085 | .zstd => return error.CompressionUnsupported, | |
| 1086 | } | |
| 1125 | if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { | |
| 1126 | if (r.redirect_behavior == .not_allowed) { | |
| 1127 | // Connection can still be reused by skipping the body. | |
| 1128 | const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); | |
| 1129 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1130 | error.ReadFailed => connection.closing = true, | |
| 1131 | }; | |
| 1132 | return error.TooManyHttpRedirects; | |
| 1087 | 1133 | } |
| 1088 | ||
| 1089 | break; | |
| 1134 | try r.redirect(head, &aux_buf); | |
| 1135 | try r.sendBodiless(); | |
| 1136 | continue; | |
| 1090 | 1137 | } |
| 1091 | } | |
| 1092 | } | |
| 1093 | ||
| 1094 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || | |
| 1095 | error{ DecompressionFailure, InvalidTrailers }; | |
| 1096 | 1138 | |
| 1097 | pub const Reader = std.io.GenericReader(*Request, ReadError, read); | |
| 1139 | if (!r.accept_encoding[@intFromEnum(head.content_encoding)]) | |
| 1140 | return error.HttpContentEncodingUnsupported; | |
| 1098 | 1141 | |
| 1099 | pub fn reader(req: *Request) Reader { | |
| 1100 | return .{ .context = req }; | |
| 1101 | } | |
| 1102 | ||
| 1103 | /// Reads data from the response body. Must be called after `wait`. | |
| 1104 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | |
| 1105 | const out_index = switch (req.response.compression) { | |
| 1106 | // I'm about to upstream my http client rewrite | |
| 1107 | //.deflate => |*deflate| deflate.readSlice(buffer) catch return error.DecompressionFailure, | |
| 1108 | //.gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, | |
| 1109 | // https://github.com/ziglang/zig/issues/18937 | |
| 1110 | //.zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure, | |
| 1111 | else => try req.transferRead(buffer), | |
| 1112 | }; | |
| 1113 | if (out_index > 0) return out_index; | |
| 1114 | ||
| 1115 | while (!req.response.parser.state.isContent()) { // read trailing headers | |
| 1116 | try req.connection.?.fill(); | |
| 1117 | ||
| 1118 | const nchecked = try req.response.parser.checkCompleteHead(req.connection.?.peek()); | |
| 1119 | req.connection.?.drop(@intCast(nchecked)); | |
| 1142 | return response; | |
| 1120 | 1143 | } |
| 1121 | ||
| 1122 | return 0; | |
| 1123 | 1144 | } |
| 1124 | 1145 | |
| 1125 | /// Reads data from the response body. Must be called after `wait`. | |
| 1126 | pub fn readAll(req: *Request, buffer: []u8) !usize { | |
| 1127 | var index: usize = 0; | |
| 1128 | while (index < buffer.len) { | |
| 1129 | const amt = try read(req, buffer[index..]); | |
| 1130 | if (amt == 0) break; | |
| 1131 | index += amt; | |
| 1146 | /// This function takes an auxiliary buffer to store the arbitrarily large | |
| 1147 | /// URI which may need to be merged with the previous URI, and that data | |
| 1148 | /// needs to survive across different connections, which is where the input | |
| 1149 | /// buffer lives. | |
| 1150 | /// | |
| 1151 | /// `aux_buf` must outlive accesses to `Request.uri`. | |
| 1152 | fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { | |
| 1153 | const new_location = head.location orelse return error.HttpRedirectLocationMissing; | |
| 1154 | if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; | |
| 1155 | const location = aux_buf.*[0..new_location.len]; | |
| 1156 | @memcpy(location, new_location); | |
| 1157 | { | |
| 1158 | // Skip the body of the redirect response to leave the connection in | |
| 1159 | // the correct state. This causes `new_location` to be invalidated. | |
| 1160 | const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); | |
| 1161 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1162 | error.ReadFailed => return r.reader.body_err.?, | |
| 1163 | }; | |
| 1132 | 1164 | } |
| 1133 | return index; | |
| 1134 | } | |
| 1135 | ||
| 1136 | pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; | |
| 1137 | ||
| 1138 | pub const Writer = std.io.GenericWriter(*Request, WriteError, write); | |
| 1165 | const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { | |
| 1166 | error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, | |
| 1167 | error.InvalidFormat => return error.HttpRedirectLocationInvalid, | |
| 1168 | error.InvalidPort => return error.HttpRedirectLocationInvalid, | |
| 1169 | error.NoSpaceLeft => return error.HttpRedirectLocationOversize, | |
| 1170 | }; | |
| 1139 | 1171 | |
| 1140 | pub fn writer(req: *Request) Writer { | |
| 1141 | return .{ .context = req }; | |
| 1142 | } | |
| 1172 | const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; | |
| 1173 | const old_connection = r.connection.?; | |
| 1174 | const old_host = old_connection.host(); | |
| 1175 | var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; | |
| 1176 | const new_host = try new_uri.getHost(&new_host_name_buffer); | |
| 1177 | const keep_privileged_headers = | |
| 1178 | std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and | |
| 1179 | sameParentDomain(old_host, new_host); | |
| 1143 | 1180 | |
| 1144 | /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. | |
| 1145 | /// Must be called after `send` and before `finish`. | |
| 1146 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { | |
| 1147 | switch (req.transfer_encoding) { | |
| 1148 | .chunked => { | |
| 1149 | if (bytes.len > 0) { | |
| 1150 | try req.connection.?.writer().print("{x}\r\n", .{bytes.len}); | |
| 1151 | try req.connection.?.writer().writeAll(bytes); | |
| 1152 | try req.connection.?.writer().writeAll("\r\n"); | |
| 1153 | } | |
| 1181 | r.client.connection_pool.release(old_connection); | |
| 1182 | r.connection = null; | |
| 1154 | 1183 | |
| 1155 | return bytes.len; | |
| 1156 | }, | |
| 1157 | .content_length => |*len| { | |
| 1158 | if (len.* < bytes.len) return error.MessageTooLong; | |
| 1184 | if (!keep_privileged_headers) { | |
| 1185 | // When redirecting to a different domain, strip privileged headers. | |
| 1186 | r.privileged_headers = &.{}; | |
| 1187 | } | |
| 1159 | 1188 | |
| 1160 | const amt = try req.connection.?.write(bytes); | |
| 1161 | len.* -= amt; | |
| 1162 | return amt; | |
| 1163 | }, | |
| 1164 | .none => return error.NotWriteable, | |
| 1189 | if (switch (head.status) { | |
| 1190 | .see_other => true, | |
| 1191 | .moved_permanently, .found => r.method == .POST, | |
| 1192 | else => false, | |
| 1193 | }) { | |
| 1194 | // A redirect to a GET must change the method and remove the body. | |
| 1195 | r.method = .GET; | |
| 1196 | r.transfer_encoding = .none; | |
| 1197 | r.headers.content_type = .omit; | |
| 1165 | 1198 | } |
| 1166 | } | |
| 1167 | 1199 | |
| 1168 | /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. | |
| 1169 | /// Must be called after `send` and before `finish`. | |
| 1170 | pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { | |
| 1171 | var index: usize = 0; | |
| 1172 | while (index < bytes.len) { | |
| 1173 | index += try write(req, bytes[index..]); | |
| 1200 | if (r.transfer_encoding != .none) { | |
| 1201 | // The request body has already been sent. The request is | |
| 1202 | // still in a valid state, but the redirect must be handled | |
| 1203 | // manually. | |
| 1204 | return error.RedirectRequiresResend; | |
| 1174 | 1205 | } |
| 1175 | } | |
| 1176 | 1206 | |
| 1177 | pub const FinishError = WriteError || error{MessageNotCompleted}; | |
| 1207 | const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); | |
| 1208 | r.uri = new_uri; | |
| 1209 | r.connection = new_connection; | |
| 1210 | r.reader = .{ | |
| 1211 | .in = new_connection.reader(), | |
| 1212 | .state = .ready, | |
| 1213 | // Populated when `http.Reader.bodyReader` is called. | |
| 1214 | .interface = undefined, | |
| 1215 | }; | |
| 1216 | r.redirect_behavior.subtractOne(); | |
| 1217 | } | |
| 1178 | 1218 | |
| 1179 | /// Finish the body of a request. This notifies the server that you have no more data to send. | |
| 1180 | /// Must be called after `send`. | |
| 1181 | pub fn finish(req: *Request) FinishError!void { | |
| 1182 | switch (req.transfer_encoding) { | |
| 1183 | .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"), | |
| 1184 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | |
| 1185 | .none => {}, | |
| 1219 | /// Returns true if the default behavior is required, otherwise handles | |
| 1220 | /// writing (or not writing) the header. | |
| 1221 | fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool { | |
| 1222 | switch (v) { | |
| 1223 | .default => return true, | |
| 1224 | .omit => return false, | |
| 1225 | .override => |x| { | |
| 1226 | var vecs: [3][]const u8 = .{ prefix, x, "\r\n" }; | |
| 1227 | try bw.writeVecAll(&vecs); | |
| 1228 | return false; | |
| 1229 | }, | |
| 1186 | 1230 | } |
| 1187 | ||
| 1188 | try req.connection.?.flush(); | |
| 1189 | 1231 | } |
| 1190 | 1232 | }; |
| 1191 | 1233 | |
| 1192 | 1234 | pub const Proxy = struct { |
| 1193 | protocol: Connection.Protocol, | |
| 1235 | protocol: Protocol, | |
| 1194 | 1236 | host: []const u8, |
| 1195 | 1237 | authorization: ?[]const u8, |
| 1196 | 1238 | port: u16, |
| ... | ... | @@ -1204,10 +1246,8 @@ pub const Proxy = struct { |
| 1204 | 1246 | pub fn deinit(client: *Client) void { |
| 1205 | 1247 | assert(client.connection_pool.used.first == null); // There are still active requests. |
| 1206 | 1248 | |
| 1207 | client.connection_pool.deinit(client.allocator); | |
| 1208 | ||
| 1209 | if (!disable_tls) | |
| 1210 | client.ca_bundle.deinit(client.allocator); | |
| 1249 | client.connection_pool.deinit(); | |
| 1250 | if (!disable_tls) client.ca_bundle.deinit(client.allocator); | |
| 1211 | 1251 | |
| 1212 | 1252 | client.* = undefined; |
| 1213 | 1253 | } |
| ... | ... | @@ -1249,24 +1289,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !? |
| 1249 | 1289 | } else return null; |
| 1250 | 1290 | |
| 1251 | 1291 | const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content); |
| 1252 | const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) { | |
| 1253 | error.UnsupportedUriScheme => return null, | |
| 1254 | error.UriMissingHost => return error.HttpProxyMissingHost, | |
| 1255 | error.OutOfMemory => |e| return e, | |
| 1256 | }; | |
| 1292 | const protocol = Protocol.fromUri(uri) orelse return null; | |
| 1293 | const raw_host = try uri.getHostAlloc(arena); | |
| 1257 | 1294 | |
| 1258 | const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: { | |
| 1259 | const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri)); | |
| 1260 | assert(basic_authorization.value(valid_uri, authorization).len == authorization.len); | |
| 1295 | const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { | |
| 1296 | const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); | |
| 1297 | assert(basic_authorization.value(uri, authorization).len == authorization.len); | |
| 1261 | 1298 | break :a authorization; |
| 1262 | 1299 | } else null; |
| 1263 | 1300 | |
| 1264 | 1301 | const proxy = try arena.create(Proxy); |
| 1265 | 1302 | proxy.* = .{ |
| 1266 | 1303 | .protocol = protocol, |
| 1267 | .host = valid_uri.host.?.raw, | |
| 1304 | .host = raw_host, | |
| 1268 | 1305 | .authorization = authorization, |
| 1269 | .port = uriPort(valid_uri, protocol), | |
| 1306 | .port = uriPort(uri, protocol), | |
| 1270 | 1307 | .supports_connect = true, |
| 1271 | 1308 | }; |
| 1272 | 1309 | return proxy; |
| ... | ... | @@ -1277,10 +1314,8 @@ pub const basic_authorization = struct { |
| 1277 | 1314 | pub const max_password_len = 255; |
| 1278 | 1315 | pub const max_value_len = valueLength(max_user_len, max_password_len); |
| 1279 | 1316 | |
| 1280 | const prefix = "Basic "; | |
| 1281 | ||
| 1282 | 1317 | pub fn valueLength(user_len: usize, password_len: usize) usize { |
| 1283 | return prefix.len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); | |
| 1318 | return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); | |
| 1284 | 1319 | } |
| 1285 | 1320 | |
| 1286 | 1321 | pub fn valueLengthFromUri(uri: Uri) usize { |
| ... | ... | @@ -1300,37 +1335,70 @@ pub const basic_authorization = struct { |
| 1300 | 1335 | } |
| 1301 | 1336 | |
| 1302 | 1337 | pub fn value(uri: Uri, out: []u8) []u8 { |
| 1303 | const user: Uri.Component = uri.user orelse .empty; | |
| 1304 | const password: Uri.Component = uri.password orelse .empty; | |
| 1305 | ||
| 1306 | var buf: [max_user_len + ":".len + max_password_len]u8 = undefined; | |
| 1307 | var w: std.io.Writer = .fixed(&buf); | |
| 1308 | user.formatUser(&w) catch unreachable; // fixed | |
| 1309 | password.formatPassword(&w) catch unreachable; // fixed | |
| 1338 | var bw: Writer = .fixed(out); | |
| 1339 | write(uri, &bw) catch unreachable; | |
| 1340 | return bw.buffered(); | |
| 1341 | } | |
| 1310 | 1342 | |
| 1311 | @memcpy(out[0..prefix.len], prefix); | |
| 1312 | const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], w.buffered()); | |
| 1313 | return out[0 .. prefix.len + base64.len]; | |
| 1343 | pub fn write(uri: Uri, out: *Writer) Writer.Error!void { | |
| 1344 | var buf: [max_user_len + 1 + max_password_len]u8 = undefined; | |
| 1345 | var w: Writer = .fixed(&buf); | |
| 1346 | const user: Uri.Component = uri.user orelse .empty; | |
| 1347 | const password: Uri.Component = uri.user orelse .empty; | |
| 1348 | user.formatUser(&w) catch unreachable; | |
| 1349 | w.writeByte(':') catch unreachable; | |
| 1350 | password.formatPassword(&w) catch unreachable; | |
| 1351 | try out.print("Basic {b64}", .{w.buffered()}); | |
| 1314 | 1352 | } |
| 1315 | 1353 | }; |
| 1316 | 1354 | |
| 1317 | pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; | |
| 1355 | pub const ConnectTcpError = Allocator.Error || error{ | |
| 1356 | ConnectionRefused, | |
| 1357 | NetworkUnreachable, | |
| 1358 | ConnectionTimedOut, | |
| 1359 | ConnectionResetByPeer, | |
| 1360 | TemporaryNameServerFailure, | |
| 1361 | NameServerFailure, | |
| 1362 | UnknownHostName, | |
| 1363 | HostLacksNetworkAddresses, | |
| 1364 | UnexpectedConnectFailure, | |
| 1365 | TlsInitializationFailed, | |
| 1366 | }; | |
| 1318 | 1367 | |
| 1319 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. | |
| 1368 | /// Reuses a `Connection` if one matching `host` and `port` is already open. | |
| 1320 | 1369 | /// |
| 1321 | /// This function is threadsafe. | |
| 1322 | pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection { | |
| 1323 | if (client.connection_pool.findConnection(.{ | |
| 1324 | .host = host, | |
| 1325 | .port = port, | |
| 1326 | .protocol = protocol, | |
| 1327 | })) |node| return node; | |
| 1370 | /// Threadsafe. | |
| 1371 | pub fn connectTcp( | |
| 1372 | client: *Client, | |
| 1373 | host: []const u8, | |
| 1374 | port: u16, | |
| 1375 | protocol: Protocol, | |
| 1376 | ) ConnectTcpError!*Connection { | |
| 1377 | return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol }); | |
| 1378 | } | |
| 1379 | ||
| 1380 | pub const ConnectTcpOptions = struct { | |
| 1381 | host: []const u8, | |
| 1382 | port: u16, | |
| 1383 | protocol: Protocol, | |
| 1328 | 1384 | |
| 1329 | if (disable_tls and protocol == .tls) | |
| 1330 | return error.TlsInitializationFailed; | |
| 1385 | proxied_host: ?[]const u8 = null, | |
| 1386 | proxied_port: ?u16 = null, | |
| 1387 | }; | |
| 1331 | 1388 | |
| 1332 | const conn = try client.allocator.create(Connection); | |
| 1333 | errdefer client.allocator.destroy(conn); | |
| 1389 | pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { | |
| 1390 | const host = options.host; | |
| 1391 | const port = options.port; | |
| 1392 | const protocol = options.protocol; | |
| 1393 | ||
| 1394 | const proxied_host = options.proxied_host orelse host; | |
| 1395 | const proxied_port = options.proxied_port orelse port; | |
| 1396 | ||
| 1397 | if (client.connection_pool.findConnection(.{ | |
| 1398 | .host = proxied_host, | |
| 1399 | .port = proxied_port, | |
| 1400 | .protocol = protocol, | |
| 1401 | })) |conn| return conn; | |
| 1334 | 1402 | |
| 1335 | 1403 | const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { |
| 1336 | 1404 | error.ConnectionRefused => return error.ConnectionRefused, |
| ... | ... | @@ -1345,53 +1413,19 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec |
| 1345 | 1413 | }; |
| 1346 | 1414 | errdefer stream.close(); |
| 1347 | 1415 | |
| 1348 | conn.* = .{ | |
| 1349 | .stream = stream, | |
| 1350 | .tls_client = undefined, | |
| 1351 | ||
| 1352 | .protocol = protocol, | |
| 1353 | .host = try client.allocator.dupe(u8, host), | |
| 1354 | .port = port, | |
| 1355 | ||
| 1356 | .pool_node = .{}, | |
| 1357 | }; | |
| 1358 | errdefer client.allocator.free(conn.host); | |
| 1359 | ||
| 1360 | if (protocol == .tls) { | |
| 1361 | if (disable_tls) unreachable; | |
| 1362 | ||
| 1363 | conn.tls_client = try client.allocator.create(std.crypto.tls.Client); | |
| 1364 | errdefer client.allocator.destroy(conn.tls_client); | |
| 1365 | ||
| 1366 | const ssl_key_log_file: ?std.fs.File = if (std.options.http_enable_ssl_key_log_file) ssl_key_log_file: { | |
| 1367 | const ssl_key_log_path = std.process.getEnvVarOwned(client.allocator, "SSLKEYLOGFILE") catch |err| switch (err) { | |
| 1368 | error.EnvironmentVariableNotFound, error.InvalidWtf8 => break :ssl_key_log_file null, | |
| 1369 | error.OutOfMemory => return error.OutOfMemory, | |
| 1370 | }; | |
| 1371 | defer client.allocator.free(ssl_key_log_path); | |
| 1372 | break :ssl_key_log_file std.fs.cwd().createFile(ssl_key_log_path, .{ | |
| 1373 | .truncate = false, | |
| 1374 | .mode = switch (builtin.os.tag) { | |
| 1375 | .windows, .wasi => 0, | |
| 1376 | else => 0o600, | |
| 1377 | }, | |
| 1378 | }) catch null; | |
| 1379 | } else null; | |
| 1380 | errdefer if (ssl_key_log_file) |key_log_file| key_log_file.close(); | |
| 1381 | ||
| 1382 | conn.tls_client.* = std.crypto.tls.Client.init(stream, .{ | |
| 1383 | .host = .{ .explicit = host }, | |
| 1384 | .ca = .{ .bundle = client.ca_bundle }, | |
| 1385 | .ssl_key_log_file = ssl_key_log_file, | |
| 1386 | }) catch return error.TlsInitializationFailed; | |
| 1387 | // This is appropriate for HTTPS because the HTTP headers contain | |
| 1388 | // the content length which is used to detect truncation attacks. | |
| 1389 | conn.tls_client.allow_truncation_attacks = true; | |
| 1416 | switch (protocol) { | |
| 1417 | .tls => { | |
| 1418 | if (disable_tls) return error.TlsInitializationFailed; | |
| 1419 | const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); | |
| 1420 | client.connection_pool.addUsed(&tc.connection); | |
| 1421 | return &tc.connection; | |
| 1422 | }, | |
| 1423 | .plain => { | |
| 1424 | const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream); | |
| 1425 | client.connection_pool.addUsed(&pc.connection); | |
| 1426 | return &pc.connection; | |
| 1427 | }, | |
| 1390 | 1428 | } |
| 1391 | ||
| 1392 | client.connection_pool.addUsed(conn); | |
| 1393 | ||
| 1394 | return conn; | |
| 1395 | 1429 | } |
| 1396 | 1430 | |
| 1397 | 1431 | pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError; |
| ... | ... | @@ -1429,69 +1463,67 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti |
| 1429 | 1463 | return &conn.data; |
| 1430 | 1464 | } |
| 1431 | 1465 | |
| 1432 | /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP | |
| 1466 | /// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP | |
| 1433 | 1467 | /// CONNECT. This will reuse a connection if one is already open. |
| 1434 | 1468 | /// |
| 1435 | 1469 | /// This function is threadsafe. |
| 1436 | pub fn connectTunnel( | |
| 1470 | pub fn connectProxied( | |
| 1437 | 1471 | client: *Client, |
| 1438 | 1472 | proxy: *Proxy, |
| 1439 | tunnel_host: []const u8, | |
| 1440 | tunnel_port: u16, | |
| 1473 | proxied_host: []const u8, | |
| 1474 | proxied_port: u16, | |
| 1441 | 1475 | ) !*Connection { |
| 1442 | 1476 | if (!proxy.supports_connect) return error.TunnelNotSupported; |
| 1443 | 1477 | |
| 1444 | 1478 | if (client.connection_pool.findConnection(.{ |
| 1445 | .host = tunnel_host, | |
| 1446 | .port = tunnel_port, | |
| 1479 | .host = proxied_host, | |
| 1480 | .port = proxied_port, | |
| 1447 | 1481 | .protocol = proxy.protocol, |
| 1448 | })) |node| | |
| 1449 | return node; | |
| 1482 | })) |node| return node; | |
| 1450 | 1483 | |
| 1451 | 1484 | var maybe_valid = false; |
| 1452 | 1485 | (tunnel: { |
| 1453 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); | |
| 1486 | const connection = try client.connectTcpOptions(.{ | |
| 1487 | .host = proxy.host, | |
| 1488 | .port = proxy.port, | |
| 1489 | .protocol = proxy.protocol, | |
| 1490 | .proxied_host = proxied_host, | |
| 1491 | .proxied_port = proxied_port, | |
| 1492 | }); | |
| 1454 | 1493 | errdefer { |
| 1455 | conn.closing = true; | |
| 1456 | client.connection_pool.release(client.allocator, conn); | |
| 1494 | connection.closing = true; | |
| 1495 | client.connection_pool.release(connection); | |
| 1457 | 1496 | } |
| 1458 | 1497 | |
| 1459 | var buffer: [8096]u8 = undefined; | |
| 1460 | var req = client.open(.CONNECT, .{ | |
| 1498 | var req = client.request(.CONNECT, .{ | |
| 1461 | 1499 | .scheme = "http", |
| 1462 | .host = .{ .raw = tunnel_host }, | |
| 1463 | .port = tunnel_port, | |
| 1500 | .host = .{ .raw = proxied_host }, | |
| 1501 | .port = proxied_port, | |
| 1464 | 1502 | }, .{ |
| 1465 | 1503 | .redirect_behavior = .unhandled, |
| 1466 | .connection = conn, | |
| 1467 | .server_header_buffer = &buffer, | |
| 1504 | .connection = connection, | |
| 1468 | 1505 | }) catch |err| { |
| 1469 | std.log.debug("err {}", .{err}); | |
| 1470 | 1506 | break :tunnel err; |
| 1471 | 1507 | }; |
| 1472 | 1508 | defer req.deinit(); |
| 1473 | 1509 | |
| 1474 | req.send() catch |err| break :tunnel err; | |
| 1475 | req.wait() catch |err| break :tunnel err; | |
| 1510 | req.sendBodiless() catch |err| break :tunnel err; | |
| 1511 | const response = req.receiveHead(&.{}) catch |err| break :tunnel err; | |
| 1476 | 1512 | |
| 1477 | if (req.response.status.class() == .server_error) { | |
| 1513 | if (response.head.status.class() == .server_error) { | |
| 1478 | 1514 | maybe_valid = true; |
| 1479 | 1515 | break :tunnel error.ServerError; |
| 1480 | 1516 | } |
| 1481 | 1517 | |
| 1482 | if (req.response.status != .ok) break :tunnel error.ConnectionRefused; | |
| 1518 | if (response.head.status != .ok) break :tunnel error.ConnectionRefused; | |
| 1483 | 1519 | |
| 1484 | // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized. | |
| 1520 | // this connection is now a tunnel, so we can't use it for anything | |
| 1521 | // else, it will only be released when the client is de-initialized. | |
| 1485 | 1522 | req.connection = null; |
| 1486 | 1523 | |
| 1487 | client.allocator.free(conn.host); | |
| 1488 | conn.host = try client.allocator.dupe(u8, tunnel_host); | |
| 1489 | errdefer client.allocator.free(conn.host); | |
| 1524 | connection.closing = false; | |
| 1490 | 1525 | |
| 1491 | conn.port = tunnel_port; | |
| 1492 | conn.closing = false; | |
| 1493 | ||
| 1494 | return conn; | |
| 1526 | return connection; | |
| 1495 | 1527 | }) catch { |
| 1496 | 1528 | // something went wrong with the tunnel |
| 1497 | 1529 | proxy.supports_connect = maybe_valid; |
| ... | ... | @@ -1499,12 +1531,11 @@ pub fn connectTunnel( |
| 1499 | 1531 | }; |
| 1500 | 1532 | } |
| 1501 | 1533 | |
| 1502 | // Prevents a dependency loop in open() | |
| 1503 | const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUriScheme, ConnectionRefused }; | |
| 1504 | pub const ConnectError = ConnectErrorPartial || RequestError; | |
| 1534 | pub const ConnectError = ConnectTcpError || RequestError; | |
| 1505 | 1535 | |
| 1506 | 1536 | /// Connect to `host:port` using the specified protocol. This will reuse a |
| 1507 | 1537 | /// connection if one is already open. |
| 1538 | /// | |
| 1508 | 1539 | /// If a proxy is configured for the client, then the proxy will be used to |
| 1509 | 1540 | /// connect to the host. |
| 1510 | 1541 | /// |
| ... | ... | @@ -1513,7 +1544,7 @@ pub fn connect( |
| 1513 | 1544 | client: *Client, |
| 1514 | 1545 | host: []const u8, |
| 1515 | 1546 | port: u16, |
| 1516 | protocol: Connection.Protocol, | |
| 1547 | protocol: Protocol, | |
| 1517 | 1548 | ) ConnectError!*Connection { |
| 1518 | 1549 | const proxy = switch (protocol) { |
| 1519 | 1550 | .plain => client.http_proxy, |
| ... | ... | @@ -1528,32 +1559,24 @@ pub fn connect( |
| 1528 | 1559 | } |
| 1529 | 1560 | |
| 1530 | 1561 | if (proxy.supports_connect) tunnel: { |
| 1531 | return connectTunnel(client, proxy, host, port) catch |err| switch (err) { | |
| 1562 | return connectProxied(client, proxy, host, port) catch |err| switch (err) { | |
| 1532 | 1563 | error.TunnelNotSupported => break :tunnel, |
| 1533 | 1564 | else => |e| return e, |
| 1534 | 1565 | }; |
| 1535 | 1566 | } |
| 1536 | 1567 | |
| 1537 | 1568 | // fall back to using the proxy as a normal http proxy |
| 1538 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); | |
| 1539 | errdefer { | |
| 1540 | conn.closing = true; | |
| 1541 | client.connection_pool.release(conn); | |
| 1542 | } | |
| 1543 | ||
| 1544 | conn.proxied = true; | |
| 1545 | return conn; | |
| 1569 | const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); | |
| 1570 | connection.proxied = true; | |
| 1571 | return connection; | |
| 1546 | 1572 | } |
| 1547 | 1573 | |
| 1548 | pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || | |
| 1549 | std.fmt.ParseIntError || Connection.WriteError || | |
| 1550 | error{ | |
| 1551 | UnsupportedUriScheme, | |
| 1552 | UriMissingHost, | |
| 1553 | ||
| 1554 | CertificateBundleLoadFailure, | |
| 1555 | UnsupportedTransferEncoding, | |
| 1556 | }; | |
| 1574 | pub const RequestError = ConnectTcpError || error{ | |
| 1575 | UnsupportedUriScheme, | |
| 1576 | UriMissingHost, | |
| 1577 | UriHostTooLong, | |
| 1578 | CertificateBundleLoadFailure, | |
| 1579 | }; | |
| 1557 | 1580 | |
| 1558 | 1581 | pub const RequestOptions = struct { |
| 1559 | 1582 | version: http.Version = .@"HTTP/1.1", |
| ... | ... | @@ -1578,11 +1601,6 @@ pub const RequestOptions = struct { |
| 1578 | 1601 | /// payload or the server has acknowledged the payload). |
| 1579 | 1602 | redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), |
| 1580 | 1603 | |
| 1581 | /// Externally-owned memory used to store the server's entire HTTP header. | |
| 1582 | /// `error.HttpHeadersOversize` is returned from read() when a | |
| 1583 | /// client sends too many bytes of HTTP headers. | |
| 1584 | server_header_buffer: []u8, | |
| 1585 | ||
| 1586 | 1604 | /// Must be an already acquired connection. |
| 1587 | 1605 | connection: ?*Connection = null, |
| 1588 | 1606 | |
| ... | ... | @@ -1598,38 +1616,17 @@ pub const RequestOptions = struct { |
| 1598 | 1616 | privileged_headers: []const http.Header = &.{}, |
| 1599 | 1617 | }; |
| 1600 | 1618 | |
| 1601 | fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } { | |
| 1602 | const protocol_map = std.StaticStringMap(Connection.Protocol).initComptime(.{ | |
| 1603 | .{ "http", .plain }, | |
| 1604 | .{ "ws", .plain }, | |
| 1605 | .{ "https", .tls }, | |
| 1606 | .{ "wss", .tls }, | |
| 1607 | }); | |
| 1608 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme; | |
| 1609 | var valid_uri = uri; | |
| 1610 | // The host is always going to be needed as a raw string for hostname resolution anyway. | |
| 1611 | valid_uri.host = .{ | |
| 1612 | .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena), | |
| 1613 | }; | |
| 1614 | return .{ protocol, valid_uri }; | |
| 1615 | } | |
| 1616 | ||
| 1617 | fn uriPort(uri: Uri, protocol: Connection.Protocol) u16 { | |
| 1618 | return uri.port orelse switch (protocol) { | |
| 1619 | .plain => 80, | |
| 1620 | .tls => 443, | |
| 1621 | }; | |
| 1619 | fn uriPort(uri: Uri, protocol: Protocol) u16 { | |
| 1620 | return uri.port orelse protocol.port(); | |
| 1622 | 1621 | } |
| 1623 | 1622 | |
| 1624 | 1623 | /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. |
| 1625 | 1624 | /// |
| 1626 | /// `uri` must remain alive during the entire request. | |
| 1627 | /// | |
| 1628 | 1625 | /// The caller is responsible for calling `deinit()` on the `Request`. |
| 1629 | 1626 | /// This function is threadsafe. |
| 1630 | 1627 | /// |
| 1631 | 1628 | /// Asserts that "\r\n" does not occur in any header name or value. |
| 1632 | pub fn open( | |
| 1629 | pub fn request( | |
| 1633 | 1630 | client: *Client, |
| 1634 | 1631 | method: http.Method, |
| 1635 | 1632 | uri: Uri, |
| ... | ... | @@ -1649,59 +1646,58 @@ pub fn open( |
| 1649 | 1646 | } |
| 1650 | 1647 | } |
| 1651 | 1648 | |
| 1652 | var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer); | |
| 1653 | const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); | |
| 1649 | const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; | |
| 1654 | 1650 | |
| 1655 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { | |
| 1651 | if (protocol == .tls) { | |
| 1656 | 1652 | if (disable_tls) unreachable; |
| 1657 | ||
| 1658 | client.ca_bundle_mutex.lock(); | |
| 1659 | defer client.ca_bundle_mutex.unlock(); | |
| 1660 | ||
| 1661 | if (client.next_https_rescan_certs) { | |
| 1662 | client.ca_bundle.rescan(client.allocator) catch | |
| 1663 | return error.CertificateBundleLoadFailure; | |
| 1664 | @atomicStore(bool, &client.next_https_rescan_certs, false, .release); | |
| 1653 | if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { | |
| 1654 | client.ca_bundle_mutex.lock(); | |
| 1655 | defer client.ca_bundle_mutex.unlock(); | |
| 1656 | ||
| 1657 | if (client.next_https_rescan_certs) { | |
| 1658 | client.ca_bundle.rescan(client.allocator) catch | |
| 1659 | return error.CertificateBundleLoadFailure; | |
| 1660 | @atomicStore(bool, &client.next_https_rescan_certs, false, .release); | |
| 1661 | } | |
| 1665 | 1662 | } |
| 1666 | 1663 | } |
| 1667 | 1664 | |
| 1668 | const conn = options.connection orelse | |
| 1669 | try client.connect(valid_uri.host.?.raw, uriPort(valid_uri, protocol), protocol); | |
| 1665 | const connection = options.connection orelse c: { | |
| 1666 | var host_name_buffer: [Uri.host_name_max]u8 = undefined; | |
| 1667 | const host_name = try uri.getHost(&host_name_buffer); | |
| 1668 | break :c try client.connect(host_name, uriPort(uri, protocol), protocol); | |
| 1669 | }; | |
| 1670 | 1670 | |
| 1671 | var req: Request = .{ | |
| 1672 | .uri = valid_uri, | |
| 1671 | return .{ | |
| 1672 | .uri = uri, | |
| 1673 | 1673 | .client = client, |
| 1674 | .connection = conn, | |
| 1674 | .connection = connection, | |
| 1675 | .reader = .{ | |
| 1676 | .in = connection.reader(), | |
| 1677 | .state = .ready, | |
| 1678 | // Populated when `http.Reader.bodyReader` is called. | |
| 1679 | .interface = undefined, | |
| 1680 | }, | |
| 1675 | 1681 | .keep_alive = options.keep_alive, |
| 1676 | 1682 | .method = method, |
| 1677 | 1683 | .version = options.version, |
| 1678 | 1684 | .transfer_encoding = .none, |
| 1679 | 1685 | .redirect_behavior = options.redirect_behavior, |
| 1680 | 1686 | .handle_continue = options.handle_continue, |
| 1681 | .response = .{ | |
| 1682 | .version = undefined, | |
| 1683 | .status = undefined, | |
| 1684 | .reason = undefined, | |
| 1685 | .keep_alive = undefined, | |
| 1686 | .parser = .init(server_header.buffer[server_header.end_index..]), | |
| 1687 | }, | |
| 1688 | 1687 | .headers = options.headers, |
| 1689 | 1688 | .extra_headers = options.extra_headers, |
| 1690 | 1689 | .privileged_headers = options.privileged_headers, |
| 1691 | 1690 | }; |
| 1692 | errdefer req.deinit(); | |
| 1693 | ||
| 1694 | return req; | |
| 1695 | 1691 | } |
| 1696 | 1692 | |
| 1697 | 1693 | pub const FetchOptions = struct { |
| 1698 | server_header_buffer: ?[]u8 = null, | |
| 1694 | /// `null` means it will be heap-allocated. | |
| 1695 | redirect_buffer: ?[]u8 = null, | |
| 1696 | /// `null` means it will be heap-allocated. | |
| 1697 | decompress_buffer: ?[]u8 = null, | |
| 1699 | 1698 | redirect_behavior: ?Request.RedirectBehavior = null, |
| 1700 | ||
| 1701 | /// If the server sends a body, it will be appended to this ArrayList. | |
| 1702 | /// `max_append_size` provides an upper limit for how much they can grow. | |
| 1703 | response_storage: ResponseStorage = .ignore, | |
| 1704 | max_append_size: ?usize = null, | |
| 1699 | /// If the server sends a body, it will be stored here. | |
| 1700 | response_storage: ?ResponseStorage = null, | |
| 1705 | 1701 | |
| 1706 | 1702 | location: Location, |
| 1707 | 1703 | method: ?http.Method = null, |
| ... | ... | @@ -1725,11 +1721,11 @@ pub const FetchOptions = struct { |
| 1725 | 1721 | uri: Uri, |
| 1726 | 1722 | }; |
| 1727 | 1723 | |
| 1728 | pub const ResponseStorage = union(enum) { | |
| 1729 | ignore, | |
| 1730 | /// Only the existing capacity will be used. | |
| 1731 | static: *std.ArrayListUnmanaged(u8), | |
| 1732 | dynamic: *std.ArrayList(u8), | |
| 1724 | pub const ResponseStorage = struct { | |
| 1725 | list: *std.ArrayListUnmanaged(u8), | |
| 1726 | /// If null then only the existing capacity will be used. | |
| 1727 | allocator: ?Allocator = null, | |
| 1728 | append_limit: std.io.Limit = .unlimited, | |
| 1733 | 1729 | }; |
| 1734 | 1730 | }; |
| 1735 | 1731 | |
| ... | ... | @@ -1737,23 +1733,29 @@ pub const FetchResult = struct { |
| 1737 | 1733 | status: http.Status, |
| 1738 | 1734 | }; |
| 1739 | 1735 | |
| 1736 | pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{ | |
| 1737 | StreamTooLong, | |
| 1738 | /// TODO provide optional diagnostics when this occurs or break into more error codes | |
| 1739 | WriteFailed, | |
| 1740 | UnsupportedCompressionMethod, | |
| 1741 | }; | |
| 1742 | ||
| 1740 | 1743 | /// Perform a one-shot HTTP request with the provided options. |
| 1741 | 1744 | /// |
| 1742 | 1745 | /// This function is threadsafe. |
| 1743 | pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { | |
| 1746 | pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { | |
| 1744 | 1747 | const uri = switch (options.location) { |
| 1745 | 1748 | .url => |u| try Uri.parse(u), |
| 1746 | 1749 | .uri => |u| u, |
| 1747 | 1750 | }; |
| 1748 | var server_header_buffer: [16 * 1024]u8 = undefined; | |
| 1749 | ||
| 1750 | 1751 | const method: http.Method = options.method orelse |
| 1751 | 1752 | if (options.payload != null) .POST else .GET; |
| 1752 | 1753 | |
| 1753 | var req = try open(client, method, uri, .{ | |
| 1754 | .server_header_buffer = options.server_header_buffer orelse &server_header_buffer, | |
| 1755 | .redirect_behavior = options.redirect_behavior orelse | |
| 1756 | if (options.payload == null) @enumFromInt(3) else .unhandled, | |
| 1754 | const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse | |
| 1755 | if (options.payload == null) @enumFromInt(3) else .unhandled; | |
| 1756 | ||
| 1757 | var req = try request(client, method, uri, .{ | |
| 1758 | .redirect_behavior = redirect_behavior, | |
| 1757 | 1759 | .headers = options.headers, |
| 1758 | 1760 | .extra_headers = options.extra_headers, |
| 1759 | 1761 | .privileged_headers = options.privileged_headers, |
| ... | ... | @@ -1761,44 +1763,70 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { |
| 1761 | 1763 | }); |
| 1762 | 1764 | defer req.deinit(); |
| 1763 | 1765 | |
| 1764 | if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len }; | |
| 1766 | if (options.payload) |payload| { | |
| 1767 | req.transfer_encoding = .{ .content_length = payload.len }; | |
| 1768 | var body = try req.sendBody(&.{}); | |
| 1769 | try body.writer.writeAll(payload); | |
| 1770 | try body.end(); | |
| 1771 | } else { | |
| 1772 | try req.sendBodiless(); | |
| 1773 | } | |
| 1765 | 1774 | |
| 1766 | try req.send(); | |
| 1775 | const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse | |
| 1776 | try client.allocator.alloc(u8, 8 * 1024); | |
| 1777 | defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer); | |
| 1767 | 1778 | |
| 1768 | if (options.payload) |payload| try req.writeAll(payload); | |
| 1779 | var response = try req.receiveHead(redirect_buffer); | |
| 1769 | 1780 | |
| 1770 | try req.finish(); | |
| 1771 | try req.wait(); | |
| 1781 | const storage = options.response_storage orelse { | |
| 1782 | const reader = response.reader(&.{}); | |
| 1783 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1784 | error.ReadFailed => return response.bodyErr().?, | |
| 1785 | }; | |
| 1786 | return .{ .status = response.head.status }; | |
| 1787 | }; | |
| 1772 | 1788 | |
| 1773 | switch (options.response_storage) { | |
| 1774 | .ignore => { | |
| 1775 | // Take advantage of request internals to discard the response body | |
| 1776 | // and make the connection available for another request. | |
| 1777 | req.response.skip = true; | |
| 1778 | assert(try req.transferRead(&.{}) == 0); // No buffer is necessary when skipping. | |
| 1779 | }, | |
| 1780 | .dynamic => |list| { | |
| 1781 | const max_append_size = options.max_append_size orelse 2 * 1024 * 1024; | |
| 1782 | try req.reader().readAllArrayList(list, max_append_size); | |
| 1783 | }, | |
| 1784 | .static => |list| { | |
| 1785 | const buf = b: { | |
| 1786 | const buf = list.unusedCapacitySlice(); | |
| 1787 | if (options.max_append_size) |len| { | |
| 1788 | if (len < buf.len) break :b buf[0..len]; | |
| 1789 | } | |
| 1790 | break :b buf; | |
| 1791 | }; | |
| 1792 | list.items.len += try req.reader().readAll(buf); | |
| 1793 | }, | |
| 1789 | const decompress_buffer: []u8 = switch (response.head.content_encoding) { | |
| 1790 | .identity => &.{}, | |
| 1791 | .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len), | |
| 1792 | .deflate, .gzip => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.flate.max_window_len), | |
| 1793 | .compress => return error.UnsupportedCompressionMethod, | |
| 1794 | }; | |
| 1795 | defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer); | |
| 1796 | ||
| 1797 | var decompressor: http.Decompressor = undefined; | |
| 1798 | const reader = response.readerDecompressing(&decompressor, decompress_buffer); | |
| 1799 | const list = storage.list; | |
| 1800 | ||
| 1801 | if (storage.allocator) |allocator| { | |
| 1802 | reader.appendRemaining(allocator, null, list, storage.append_limit) catch |err| switch (err) { | |
| 1803 | error.ReadFailed => return response.bodyErr().?, | |
| 1804 | else => |e| return e, | |
| 1805 | }; | |
| 1806 | } else { | |
| 1807 | const buf = storage.append_limit.slice(list.unusedCapacitySlice()); | |
| 1808 | list.items.len += reader.readSliceShort(buf) catch |err| switch (err) { | |
| 1809 | error.ReadFailed => return response.bodyErr().?, | |
| 1810 | }; | |
| 1794 | 1811 | } |
| 1795 | 1812 | |
| 1796 | return .{ | |
| 1797 | .status = req.response.status, | |
| 1798 | }; | |
| 1813 | return .{ .status = response.head.status }; | |
| 1814 | } | |
| 1815 | ||
| 1816 | pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { | |
| 1817 | if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; | |
| 1818 | if (child_host.len == parent_host.len) return true; | |
| 1819 | if (parent_host.len > child_host.len) return false; | |
| 1820 | return child_host[child_host.len - parent_host.len - 1] == '.'; | |
| 1821 | } | |
| 1822 | ||
| 1823 | test sameParentDomain { | |
| 1824 | try testing.expect(!sameParentDomain("foo.com", "bar.com")); | |
| 1825 | try testing.expect(sameParentDomain("foo.com", "foo.com")); | |
| 1826 | try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); | |
| 1827 | try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); | |
| 1799 | 1828 | } |
| 1800 | 1829 | |
| 1801 | 1830 | test { |
| 1802 | 1831 | _ = Response; |
| 1803 | _ = &initDefaultProxies; | |
| 1804 | 1832 | } |
lib/std/http/Server.zig+406-751| ... | ... | @@ -1,139 +1,69 @@ |
| 1 | //! Blocking HTTP server implementation. | |
| 2 | //! Handles a single connection's lifecycle. | |
| 3 | ||
| 4 | connection: net.Server.Connection, | |
| 5 | /// Keeps track of whether the Server is ready to accept a new request on the | |
| 6 | /// same connection, and makes invalid API usage cause assertion failures | |
| 7 | /// rather than HTTP protocol violations. | |
| 8 | state: State, | |
| 9 | /// User-provided buffer that must outlive this Server. | |
| 10 | /// Used to store the client's entire HTTP header. | |
| 11 | read_buffer: []u8, | |
| 12 | /// Amount of available data inside read_buffer. | |
| 13 | read_buffer_len: usize, | |
| 14 | /// Index into `read_buffer` of the first byte of the next HTTP request. | |
| 15 | next_request_start: usize, | |
| 16 | ||
| 17 | pub const State = enum { | |
| 18 | /// The connection is available to be used for the first time, or reused. | |
| 19 | ready, | |
| 20 | /// An error occurred in `receiveHead`. | |
| 21 | receiving_head, | |
| 22 | /// A Request object has been obtained and from there a Response can be | |
| 23 | /// opened. | |
| 24 | received_head, | |
| 25 | /// The client is uploading something to this Server. | |
| 26 | receiving_body, | |
| 27 | /// The connection is eligible for another HTTP request, however the client | |
| 28 | /// and server did not negotiate a persistent connection. | |
| 29 | closing, | |
| 30 | }; | |
| 1 | //! Handles a single connection lifecycle. | |
| 2 | ||
| 3 | const std = @import("../std.zig"); | |
| 4 | const http = std.http; | |
| 5 | const mem = std.mem; | |
| 6 | const Uri = std.Uri; | |
| 7 | const assert = std.debug.assert; | |
| 8 | const testing = std.testing; | |
| 9 | const Writer = std.Io.Writer; | |
| 10 | const Reader = std.Io.Reader; | |
| 11 | ||
| 12 | const Server = @This(); | |
| 13 | ||
| 14 | /// Data from the HTTP server to the HTTP client. | |
| 15 | out: *Writer, | |
| 16 | reader: http.Reader, | |
| 31 | 17 | |
| 32 | 18 | /// Initialize an HTTP server that can respond to multiple requests on the same |
| 33 | 19 | /// connection. |
| 20 | /// | |
| 21 | /// The buffer of `in` must be large enough to store the client's entire HTTP | |
| 22 | /// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. | |
| 23 | /// | |
| 34 | 24 | /// The returned `Server` is ready for `receiveHead` to be called. |
| 35 | pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server { | |
| 25 | pub fn init(in: *Reader, out: *Writer) Server { | |
| 36 | 26 | return .{ |
| 37 | .connection = connection, | |
| 38 | .state = .ready, | |
| 39 | .read_buffer = read_buffer, | |
| 40 | .read_buffer_len = 0, | |
| 41 | .next_request_start = 0, | |
| 27 | .reader = .{ | |
| 28 | .in = in, | |
| 29 | .state = .ready, | |
| 30 | // Populated when `http.Reader.bodyReader` is called. | |
| 31 | .interface = undefined, | |
| 32 | }, | |
| 33 | .out = out, | |
| 42 | 34 | }; |
| 43 | 35 | } |
| 44 | 36 | |
| 45 | pub const ReceiveHeadError = error{ | |
| 46 | /// Client sent too many bytes of HTTP headers. | |
| 47 | /// The HTTP specification suggests to respond with a 431 status code | |
| 48 | /// before closing the connection. | |
| 49 | HttpHeadersOversize, | |
| 37 | pub const ReceiveHeadError = http.Reader.HeadError || error{ | |
| 50 | 38 | /// Client sent headers that did not conform to the HTTP protocol. |
| 39 | /// | |
| 40 | /// To find out more detailed diagnostics, `Request.head_buffer` can be | |
| 41 | /// passed directly to `Request.Head.parse`. | |
| 51 | 42 | HttpHeadersInvalid, |
| 52 | /// A low level I/O error occurred trying to read the headers. | |
| 53 | HttpHeadersUnreadable, | |
| 54 | /// Partial HTTP request was received but the connection was closed before | |
| 55 | /// fully receiving the headers. | |
| 56 | HttpRequestTruncated, | |
| 57 | /// The client sent 0 bytes of headers before closing the stream. | |
| 58 | /// In other words, a keep-alive connection was finally closed. | |
| 59 | HttpConnectionClosing, | |
| 60 | 43 | }; |
| 61 | 44 | |
| 62 | /// The header bytes reference the read buffer that Server was initialized with | |
| 63 | /// and remain alive until the next call to receiveHead. | |
| 64 | 45 | pub fn receiveHead(s: *Server) ReceiveHeadError!Request { |
| 65 | assert(s.state == .ready); | |
| 66 | s.state = .received_head; | |
| 67 | errdefer s.state = .receiving_head; | |
| 68 | ||
| 69 | // In case of a reused connection, move the next request's bytes to the | |
| 70 | // beginning of the buffer. | |
| 71 | if (s.next_request_start > 0) { | |
| 72 | if (s.read_buffer_len > s.next_request_start) { | |
| 73 | rebase(s, 0); | |
| 74 | } else { | |
| 75 | s.read_buffer_len = 0; | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | var hp: http.HeadParser = .{}; | |
| 80 | ||
| 81 | if (s.read_buffer_len > 0) { | |
| 82 | const bytes = s.read_buffer[0..s.read_buffer_len]; | |
| 83 | const end = hp.feed(bytes); | |
| 84 | if (hp.state == .finished) | |
| 85 | return finishReceivingHead(s, end); | |
| 86 | } | |
| 87 | ||
| 88 | while (true) { | |
| 89 | const buf = s.read_buffer[s.read_buffer_len..]; | |
| 90 | if (buf.len == 0) | |
| 91 | return error.HttpHeadersOversize; | |
| 92 | const read_n = s.connection.stream.read(buf) catch | |
| 93 | return error.HttpHeadersUnreadable; | |
| 94 | if (read_n == 0) { | |
| 95 | if (s.read_buffer_len > 0) { | |
| 96 | return error.HttpRequestTruncated; | |
| 97 | } else { | |
| 98 | return error.HttpConnectionClosing; | |
| 99 | } | |
| 100 | } | |
| 101 | s.read_buffer_len += read_n; | |
| 102 | const bytes = buf[0..read_n]; | |
| 103 | const end = hp.feed(bytes); | |
| 104 | if (hp.state == .finished) | |
| 105 | return finishReceivingHead(s, s.read_buffer_len - bytes.len + end); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | fn finishReceivingHead(s: *Server, head_end: usize) ReceiveHeadError!Request { | |
| 46 | const head_buffer = try s.reader.receiveHead(); | |
| 110 | 47 | return .{ |
| 111 | 48 | .server = s, |
| 112 | .head_end = head_end, | |
| 113 | .head = Request.Head.parse(s.read_buffer[0..head_end]) catch | |
| 114 | return error.HttpHeadersInvalid, | |
| 115 | .reader_state = undefined, | |
| 49 | .head_buffer = head_buffer, | |
| 50 | // No need to track the returned error here since users can repeat the | |
| 51 | // parse with the header buffer to get detailed diagnostics. | |
| 52 | .head = Request.Head.parse(head_buffer) catch return error.HttpHeadersInvalid, | |
| 116 | 53 | }; |
| 117 | 54 | } |
| 118 | 55 | |
| 119 | 56 | pub const Request = struct { |
| 120 | 57 | server: *Server, |
| 121 | /// Index into Server's read_buffer. | |
| 122 | head_end: usize, | |
| 58 | /// Pointers in this struct are invalidated when the request body stream is | |
| 59 | /// initialized. | |
| 123 | 60 | head: Head, |
| 124 | reader_state: union { | |
| 125 | remaining_content_length: u64, | |
| 126 | chunk_parser: http.ChunkParser, | |
| 127 | }, | |
| 128 | ||
| 129 | pub const Compression = union(enum) { | |
| 130 | pub const DeflateDecompressor = std.compress.zlib.Decompressor(std.io.AnyReader); | |
| 131 | pub const GzipDecompressor = std.compress.gzip.Decompressor(std.io.AnyReader); | |
| 132 | ||
| 133 | deflate: std.compress.flate.Decompress, | |
| 134 | gzip: std.compress.flate.Decompress, | |
| 135 | zstd: std.compress.zstd.Decompress, | |
| 136 | none: void, | |
| 61 | head_buffer: []const u8, | |
| 62 | respond_err: ?RespondError = null, | |
| 63 | ||
| 64 | pub const RespondError = error{ | |
| 65 | /// The request contained an `expect` header with an unrecognized value. | |
| 66 | HttpExpectationFailed, | |
| 137 | 67 | }; |
| 138 | 68 | |
| 139 | 69 | pub const Head = struct { |
| ... | ... | @@ -146,7 +76,6 @@ pub const Request = struct { |
| 146 | 76 | transfer_encoding: http.TransferEncoding, |
| 147 | 77 | transfer_compression: http.ContentEncoding, |
| 148 | 78 | keep_alive: bool, |
| 149 | compression: Compression, | |
| 150 | 79 | |
| 151 | 80 | pub const ParseError = error{ |
| 152 | 81 | UnknownHttpMethod, |
| ... | ... | @@ -168,10 +97,9 @@ pub const Request = struct { |
| 168 | 97 | |
| 169 | 98 | const method_end = mem.indexOfScalar(u8, first_line, ' ') orelse |
| 170 | 99 | return error.HttpHeadersInvalid; |
| 171 | if (method_end > 24) return error.HttpHeadersInvalid; | |
| 172 | 100 | |
| 173 | const method_str = first_line[0..method_end]; | |
| 174 | const method: http.Method = @enumFromInt(http.Method.parse(method_str)); | |
| 101 | const method = std.meta.stringToEnum(http.Method, first_line[0..method_end]) orelse | |
| 102 | return error.UnknownHttpMethod; | |
| 175 | 103 | |
| 176 | 104 | const version_start = mem.lastIndexOfScalar(u8, first_line, ' ') orelse |
| 177 | 105 | return error.HttpHeadersInvalid; |
| ... | ... | @@ -200,7 +128,6 @@ pub const Request = struct { |
| 200 | 128 | .@"HTTP/1.0" => false, |
| 201 | 129 | .@"HTTP/1.1" => true, |
| 202 | 130 | }, |
| 203 | .compression = .none, | |
| 204 | 131 | }; |
| 205 | 132 | |
| 206 | 133 | while (it.next()) |line| { |
| ... | ... | @@ -230,7 +157,7 @@ pub const Request = struct { |
| 230 | 157 | |
| 231 | 158 | const trimmed = mem.trim(u8, header_value, " "); |
| 232 | 159 | |
| 233 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 160 | if (http.ContentEncoding.fromString(trimmed)) |ce| { | |
| 234 | 161 | head.transfer_compression = ce; |
| 235 | 162 | } else { |
| 236 | 163 | return error.HttpTransferEncodingUnsupported; |
| ... | ... | @@ -255,7 +182,7 @@ pub const Request = struct { |
| 255 | 182 | if (next) |second| { |
| 256 | 183 | const trimmed_second = mem.trim(u8, second, " "); |
| 257 | 184 | |
| 258 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { | |
| 185 | if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { | |
| 259 | 186 | if (head.transfer_compression != .identity) |
| 260 | 187 | return error.HttpHeadersInvalid; // double compression is not supported |
| 261 | 188 | head.transfer_compression = transfer; |
| ... | ... | @@ -296,10 +223,19 @@ pub const Request = struct { |
| 296 | 223 | inline fn int64(array: *const [8]u8) u64 { |
| 297 | 224 | return @bitCast(array.*); |
| 298 | 225 | } |
| 226 | ||
| 227 | /// Help the programmer avoid bugs by calling this when the string | |
| 228 | /// memory of `Head` becomes invalidated. | |
| 229 | fn invalidateStrings(h: *Head) void { | |
| 230 | h.target = undefined; | |
| 231 | if (h.expect) |*s| s.* = undefined; | |
| 232 | if (h.content_type) |*s| s.* = undefined; | |
| 233 | } | |
| 299 | 234 | }; |
| 300 | 235 | |
| 301 | pub fn iterateHeaders(r: *Request) http.HeaderIterator { | |
| 302 | return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]); | |
| 236 | pub fn iterateHeaders(r: *const Request) http.HeaderIterator { | |
| 237 | assert(r.server.reader.state == .received_head); | |
| 238 | return http.HeaderIterator.init(r.head_buffer); | |
| 303 | 239 | } |
| 304 | 240 | |
| 305 | 241 | test iterateHeaders { |
| ... | ... | @@ -310,22 +246,19 @@ pub const Request = struct { |
| 310 | 246 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ |
| 311 | 247 | "connectioN:\t keep-alive \r\n\r\n"; |
| 312 | 248 | |
| 313 | var read_buffer: [500]u8 = undefined; | |
| 314 | @memcpy(read_buffer[0..request_bytes.len], request_bytes); | |
| 315 | ||
| 316 | 249 | var server: Server = .{ |
| 317 | .connection = undefined, | |
| 318 | .state = .ready, | |
| 319 | .read_buffer = &read_buffer, | |
| 320 | .read_buffer_len = request_bytes.len, | |
| 321 | .next_request_start = 0, | |
| 250 | .reader = .{ | |
| 251 | .in = undefined, | |
| 252 | .state = .received_head, | |
| 253 | .interface = undefined, | |
| 254 | }, | |
| 255 | .out = undefined, | |
| 322 | 256 | }; |
| 323 | 257 | |
| 324 | 258 | var request: Request = .{ |
| 325 | 259 | .server = &server, |
| 326 | .head_end = request_bytes.len, | |
| 327 | 260 | .head = undefined, |
| 328 | .reader_state = undefined, | |
| 261 | .head_buffer = @constCast(request_bytes), | |
| 329 | 262 | }; |
| 330 | 263 | |
| 331 | 264 | var it = request.iterateHeaders(); |
| ... | ... | @@ -384,16 +317,22 @@ pub const Request = struct { |
| 384 | 317 | /// no error is surfaced. |
| 385 | 318 | /// |
| 386 | 319 | /// Asserts status is not `continue`. |
| 387 | /// Asserts there are at most 25 extra_headers. | |
| 388 | 320 | /// Asserts that "\r\n" does not occur in any header name or value. |
| 389 | 321 | pub fn respond( |
| 390 | 322 | request: *Request, |
| 391 | 323 | content: []const u8, |
| 392 | 324 | options: RespondOptions, |
| 393 | ) Response.WriteError!void { | |
| 394 | const max_extra_headers = 25; | |
| 325 | ) ExpectContinueError!void { | |
| 326 | try respondUnflushed(request, content, options); | |
| 327 | try request.server.out.flush(); | |
| 328 | } | |
| 329 | ||
| 330 | pub fn respondUnflushed( | |
| 331 | request: *Request, | |
| 332 | content: []const u8, | |
| 333 | options: RespondOptions, | |
| 334 | ) ExpectContinueError!void { | |
| 395 | 335 | assert(options.status != .@"continue"); |
| 396 | assert(options.extra_headers.len <= max_extra_headers); | |
| 397 | 336 | if (std.debug.runtime_safety) { |
| 398 | 337 | for (options.extra_headers) |header| { |
| 399 | 338 | assert(header.name.len != 0); |
| ... | ... | @@ -402,6 +341,7 @@ pub const Request = struct { |
| 402 | 341 | assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); |
| 403 | 342 | } |
| 404 | 343 | } |
| 344 | try writeExpectContinue(request); | |
| 405 | 345 | |
| 406 | 346 | const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none; |
| 407 | 347 | const server_keep_alive = !transfer_encoding_none and options.keep_alive; |
| ... | ... | @@ -409,130 +349,42 @@ pub const Request = struct { |
| 409 | 349 | |
| 410 | 350 | const phrase = options.reason orelse options.status.phrase() orelse ""; |
| 411 | 351 | |
| 412 | var first_buffer: [500]u8 = undefined; | |
| 413 | var h = std.ArrayListUnmanaged(u8).initBuffer(&first_buffer); | |
| 414 | if (request.head.expect != null) { | |
| 415 | // reader() and hence discardBody() above sets expect to null if it | |
| 416 | // is handled. So the fact that it is not null here means unhandled. | |
| 417 | h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n"); | |
| 418 | if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"); | |
| 419 | h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n"); | |
| 420 | try request.server.connection.stream.writeAll(h.items); | |
| 421 | return; | |
| 422 | } | |
| 423 | h.fixedWriter().print("{s} {d} {s}\r\n", .{ | |
| 352 | const out = request.server.out; | |
| 353 | try out.print("{s} {d} {s}\r\n", .{ | |
| 424 | 354 | @tagName(options.version), @intFromEnum(options.status), phrase, |
| 425 | }) catch unreachable; | |
| 355 | }); | |
| 426 | 356 | |
| 427 | 357 | switch (options.version) { |
| 428 | .@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"), | |
| 429 | .@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"), | |
| 358 | .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), | |
| 359 | .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), | |
| 430 | 360 | } |
| 431 | 361 | |
| 432 | 362 | if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { |
| 433 | 363 | .none => {}, |
| 434 | .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"), | |
| 364 | .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), | |
| 435 | 365 | } else { |
| 436 | h.fixedWriter().print("content-length: {d}\r\n", .{content.len}) catch unreachable; | |
| 366 | try out.print("content-length: {d}\r\n", .{content.len}); | |
| 437 | 367 | } |
| 438 | 368 | |
| 439 | var chunk_header_buffer: [18]u8 = undefined; | |
| 440 | var iovecs: [max_extra_headers * 4 + 3]std.posix.iovec_const = undefined; | |
| 441 | var iovecs_len: usize = 0; | |
| 442 | ||
| 443 | iovecs[iovecs_len] = .{ | |
| 444 | .base = h.items.ptr, | |
| 445 | .len = h.items.len, | |
| 446 | }; | |
| 447 | iovecs_len += 1; | |
| 448 | ||
| 449 | 369 | for (options.extra_headers) |header| { |
| 450 | iovecs[iovecs_len] = .{ | |
| 451 | .base = header.name.ptr, | |
| 452 | .len = header.name.len, | |
| 453 | }; | |
| 454 | iovecs_len += 1; | |
| 455 | ||
| 456 | iovecs[iovecs_len] = .{ | |
| 457 | .base = ": ", | |
| 458 | .len = 2, | |
| 459 | }; | |
| 460 | iovecs_len += 1; | |
| 461 | ||
| 462 | if (header.value.len != 0) { | |
| 463 | iovecs[iovecs_len] = .{ | |
| 464 | .base = header.value.ptr, | |
| 465 | .len = header.value.len, | |
| 466 | }; | |
| 467 | iovecs_len += 1; | |
| 468 | } | |
| 469 | ||
| 470 | iovecs[iovecs_len] = .{ | |
| 471 | .base = "\r\n", | |
| 472 | .len = 2, | |
| 473 | }; | |
| 474 | iovecs_len += 1; | |
| 370 | var vecs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; | |
| 371 | try out.writeVecAll(&vecs); | |
| 475 | 372 | } |
| 476 | 373 | |
| 477 | iovecs[iovecs_len] = .{ | |
| 478 | .base = "\r\n", | |
| 479 | .len = 2, | |
| 480 | }; | |
| 481 | iovecs_len += 1; | |
| 374 | try out.writeAll("\r\n"); | |
| 482 | 375 | |
| 483 | 376 | if (request.head.method != .HEAD) { |
| 484 | 377 | const is_chunked = (options.transfer_encoding orelse .none) == .chunked; |
| 485 | 378 | if (is_chunked) { |
| 486 | if (content.len > 0) { | |
| 487 | const chunk_header = std.fmt.bufPrint( | |
| 488 | &chunk_header_buffer, | |
| 489 | "{x}\r\n", | |
| 490 | .{content.len}, | |
| 491 | ) catch unreachable; | |
| 492 | ||
| 493 | iovecs[iovecs_len] = .{ | |
| 494 | .base = chunk_header.ptr, | |
| 495 | .len = chunk_header.len, | |
| 496 | }; | |
| 497 | iovecs_len += 1; | |
| 498 | ||
| 499 | iovecs[iovecs_len] = .{ | |
| 500 | .base = content.ptr, | |
| 501 | .len = content.len, | |
| 502 | }; | |
| 503 | iovecs_len += 1; | |
| 504 | ||
| 505 | iovecs[iovecs_len] = .{ | |
| 506 | .base = "\r\n", | |
| 507 | .len = 2, | |
| 508 | }; | |
| 509 | iovecs_len += 1; | |
| 510 | } | |
| 511 | ||
| 512 | iovecs[iovecs_len] = .{ | |
| 513 | .base = "0\r\n\r\n", | |
| 514 | .len = 5, | |
| 515 | }; | |
| 516 | iovecs_len += 1; | |
| 379 | if (content.len > 0) try out.print("{x}\r\n{s}\r\n", .{ content.len, content }); | |
| 380 | try out.writeAll("0\r\n\r\n"); | |
| 517 | 381 | } else if (content.len > 0) { |
| 518 | iovecs[iovecs_len] = .{ | |
| 519 | .base = content.ptr, | |
| 520 | .len = content.len, | |
| 521 | }; | |
| 522 | iovecs_len += 1; | |
| 382 | try out.writeAll(content); | |
| 523 | 383 | } |
| 524 | 384 | } |
| 525 | ||
| 526 | try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]); | |
| 527 | 385 | } |
| 528 | 386 | |
| 529 | 387 | pub const RespondStreamingOptions = struct { |
| 530 | /// An externally managed slice of memory used to batch bytes before | |
| 531 | /// sending. `respondStreaming` asserts this is large enough to store | |
| 532 | /// the full HTTP response head. | |
| 533 | /// | |
| 534 | /// Must outlive the returned Response. | |
| 535 | send_buffer: []u8, | |
| 536 | 388 | /// If provided, the response will use the content-length header; |
| 537 | 389 | /// otherwise it will use transfer-encoding: chunked. |
| 538 | 390 | content_length: ?u64 = null, |
| ... | ... | @@ -540,254 +392,227 @@ pub const Request = struct { |
| 540 | 392 | respond_options: RespondOptions = .{}, |
| 541 | 393 | }; |
| 542 | 394 | |
| 543 | /// The header is buffered but not sent until Response.flush is called. | |
| 395 | /// The header is not guaranteed to be sent until `BodyWriter.flush` or | |
| 396 | /// `BodyWriter.end` is called. | |
| 544 | 397 | /// |
| 545 | 398 | /// If the request contains a body and the connection is to be reused, |
| 546 | 399 | /// discards the request body, leaving the Server in the `ready` state. If |
| 547 | 400 | /// this discarding fails, the connection is marked as not to be reused and |
| 548 | 401 | /// no error is surfaced. |
| 549 | 402 | /// |
| 550 | /// HEAD requests are handled transparently by setting a flag on the | |
| 551 | /// returned Response to omit the body. However it may be worth noticing | |
| 403 | /// HEAD requests are handled transparently by setting the | |
| 404 | /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing | |
| 405 | /// the response stream to omit the body. However, it may be worth noticing | |
| 552 | 406 | /// that flag and skipping any expensive work that would otherwise need to |
| 553 | 407 | /// be done to satisfy the request. |
| 554 | 408 | /// |
| 555 | /// Asserts `send_buffer` is large enough to store the entire response header. | |
| 556 | 409 | /// Asserts status is not `continue`. |
| 557 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response { | |
| 410 | pub fn respondStreaming( | |
| 411 | request: *Request, | |
| 412 | buffer: []u8, | |
| 413 | options: RespondStreamingOptions, | |
| 414 | ) ExpectContinueError!http.BodyWriter { | |
| 415 | try writeExpectContinue(request); | |
| 558 | 416 | const o = options.respond_options; |
| 559 | 417 | assert(o.status != .@"continue"); |
| 560 | 418 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; |
| 561 | 419 | const server_keep_alive = !transfer_encoding_none and o.keep_alive; |
| 562 | 420 | const keep_alive = request.discardBody(server_keep_alive); |
| 563 | 421 | const phrase = o.reason orelse o.status.phrase() orelse ""; |
| 422 | const out = request.server.out; | |
| 564 | 423 | |
| 565 | var h = std.ArrayListUnmanaged(u8).initBuffer(options.send_buffer); | |
| 566 | ||
| 567 | const elide_body = if (request.head.expect != null) eb: { | |
| 568 | // reader() and hence discardBody() above sets expect to null if it | |
| 569 | // is handled. So the fact that it is not null here means unhandled. | |
| 570 | h.appendSliceAssumeCapacity("HTTP/1.1 417 Expectation Failed\r\n"); | |
| 571 | if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"); | |
| 572 | h.appendSliceAssumeCapacity("content-length: 0\r\n\r\n"); | |
| 573 | break :eb true; | |
| 574 | } else eb: { | |
| 575 | h.fixedWriter().print("{s} {d} {s}\r\n", .{ | |
| 576 | @tagName(o.version), @intFromEnum(o.status), phrase, | |
| 577 | }) catch unreachable; | |
| 578 | ||
| 579 | switch (o.version) { | |
| 580 | .@"HTTP/1.0" => if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n"), | |
| 581 | .@"HTTP/1.1" => if (!keep_alive) h.appendSliceAssumeCapacity("connection: close\r\n"), | |
| 582 | } | |
| 424 | try out.print("{s} {d} {s}\r\n", .{ | |
| 425 | @tagName(o.version), @intFromEnum(o.status), phrase, | |
| 426 | }); | |
| 583 | 427 | |
| 584 | if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { | |
| 585 | .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"), | |
| 586 | .none => {}, | |
| 587 | } else if (options.content_length) |len| { | |
| 588 | h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable; | |
| 589 | } else { | |
| 590 | h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"); | |
| 591 | } | |
| 428 | switch (o.version) { | |
| 429 | .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), | |
| 430 | .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), | |
| 431 | } | |
| 592 | 432 | |
| 593 | for (o.extra_headers) |header| { | |
| 594 | assert(header.name.len != 0); | |
| 595 | h.appendSliceAssumeCapacity(header.name); | |
| 596 | h.appendSliceAssumeCapacity(": "); | |
| 597 | h.appendSliceAssumeCapacity(header.value); | |
| 598 | h.appendSliceAssumeCapacity("\r\n"); | |
| 599 | } | |
| 433 | if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { | |
| 434 | .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), | |
| 435 | .none => {}, | |
| 436 | } else if (options.content_length) |len| { | |
| 437 | try out.print("content-length: {d}\r\n", .{len}); | |
| 438 | } else { | |
| 439 | try out.writeAll("transfer-encoding: chunked\r\n"); | |
| 440 | } | |
| 600 | 441 | |
| 601 | h.appendSliceAssumeCapacity("\r\n"); | |
| 602 | break :eb request.head.method == .HEAD; | |
| 603 | }; | |
| 442 | for (o.extra_headers) |header| { | |
| 443 | assert(header.name.len != 0); | |
| 444 | var bufs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; | |
| 445 | try out.writeVecAll(&bufs); | |
| 446 | } | |
| 604 | 447 | |
| 605 | return .{ | |
| 606 | .stream = request.server.connection.stream, | |
| 607 | .send_buffer = options.send_buffer, | |
| 608 | .send_buffer_start = 0, | |
| 609 | .send_buffer_end = h.items.len, | |
| 610 | .transfer_encoding = if (o.transfer_encoding) |te| switch (te) { | |
| 611 | .chunked => .chunked, | |
| 612 | .none => .none, | |
| 613 | } else if (options.content_length) |len| .{ | |
| 614 | .content_length = len, | |
| 615 | } else .chunked, | |
| 616 | .elide_body = elide_body, | |
| 617 | .chunk_len = 0, | |
| 448 | try out.writeAll("\r\n"); | |
| 449 | const elide_body = request.head.method == .HEAD; | |
| 450 | const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) { | |
| 451 | .chunked => .{ .chunked = .init }, | |
| 452 | .none => .none, | |
| 453 | } else if (options.content_length) |len| .{ | |
| 454 | .content_length = len, | |
| 455 | } else .{ .chunked = .init }; | |
| 456 | ||
| 457 | return if (elide_body) .{ | |
| 458 | .http_protocol_output = request.server.out, | |
| 459 | .state = state, | |
| 460 | .writer = .{ | |
| 461 | .buffer = buffer, | |
| 462 | .vtable = &.{ | |
| 463 | .drain = http.BodyWriter.elidingDrain, | |
| 464 | .sendFile = http.BodyWriter.elidingSendFile, | |
| 465 | }, | |
| 466 | }, | |
| 467 | } else .{ | |
| 468 | .http_protocol_output = request.server.out, | |
| 469 | .state = state, | |
| 470 | .writer = .{ | |
| 471 | .buffer = buffer, | |
| 472 | .vtable = switch (state) { | |
| 473 | .none => &.{ | |
| 474 | .drain = http.BodyWriter.noneDrain, | |
| 475 | .sendFile = http.BodyWriter.noneSendFile, | |
| 476 | }, | |
| 477 | .content_length => &.{ | |
| 478 | .drain = http.BodyWriter.contentLengthDrain, | |
| 479 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 480 | }, | |
| 481 | .chunked => &.{ | |
| 482 | .drain = http.BodyWriter.chunkedDrain, | |
| 483 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 484 | }, | |
| 485 | .end => unreachable, | |
| 486 | }, | |
| 487 | }, | |
| 618 | 488 | }; |
| 619 | 489 | } |
| 620 | 490 | |
| 621 | pub const ReadError = net.Stream.ReadError || error{ | |
| 622 | HttpChunkInvalid, | |
| 623 | HttpHeadersOversize, | |
| 491 | pub const UpgradeRequest = union(enum) { | |
| 492 | websocket: ?[]const u8, | |
| 493 | other: []const u8, | |
| 494 | none, | |
| 624 | 495 | }; |
| 625 | 496 | |
| 626 | fn read_cl(context: *const anyopaque, buffer: []u8) ReadError!usize { | |
| 627 | const request: *Request = @ptrCast(@alignCast(@constCast(context))); | |
| 628 | const s = request.server; | |
| 629 | ||
| 630 | const remaining_content_length = &request.reader_state.remaining_content_length; | |
| 631 | if (remaining_content_length.* == 0) { | |
| 632 | s.state = .ready; | |
| 633 | return 0; | |
| 497 | /// Does not invalidate `request.head`. | |
| 498 | pub fn upgradeRequested(request: *const Request) UpgradeRequest { | |
| 499 | switch (request.head.version) { | |
| 500 | .@"HTTP/1.0" => return .none, | |
| 501 | .@"HTTP/1.1" => if (request.head.method != .GET) return .none, | |
| 634 | 502 | } |
| 635 | assert(s.state == .receiving_body); | |
| 636 | const available = try fill(s, request.head_end); | |
| 637 | const len = @min(remaining_content_length.*, available.len, buffer.len); | |
| 638 | @memcpy(buffer[0..len], available[0..len]); | |
| 639 | remaining_content_length.* -= len; | |
| 640 | s.next_request_start += len; | |
| 641 | if (remaining_content_length.* == 0) | |
| 642 | s.state = .ready; | |
| 643 | return len; | |
| 644 | } | |
| 645 | ||
| 646 | fn fill(s: *Server, head_end: usize) ReadError![]u8 { | |
| 647 | const available = s.read_buffer[s.next_request_start..s.read_buffer_len]; | |
| 648 | if (available.len > 0) return available; | |
| 649 | s.next_request_start = head_end; | |
| 650 | s.read_buffer_len = head_end + try s.connection.stream.read(s.read_buffer[head_end..]); | |
| 651 | return s.read_buffer[head_end..s.read_buffer_len]; | |
| 652 | } | |
| 653 | 503 | |
| 654 | fn read_chunked(context: *const anyopaque, buffer: []u8) ReadError!usize { | |
| 655 | const request: *Request = @ptrCast(@alignCast(@constCast(context))); | |
| 656 | const s = request.server; | |
| 657 | ||
| 658 | const cp = &request.reader_state.chunk_parser; | |
| 659 | const head_end = request.head_end; | |
| 660 | ||
| 661 | // Protect against returning 0 before the end of stream. | |
| 662 | var out_end: usize = 0; | |
| 663 | while (out_end == 0) { | |
| 664 | switch (cp.state) { | |
| 665 | .invalid => return 0, | |
| 666 | .data => { | |
| 667 | assert(s.state == .receiving_body); | |
| 668 | const available = try fill(s, head_end); | |
| 669 | const len = @min(cp.chunk_len, available.len, buffer.len); | |
| 670 | @memcpy(buffer[0..len], available[0..len]); | |
| 671 | cp.chunk_len -= len; | |
| 672 | if (cp.chunk_len == 0) | |
| 673 | cp.state = .data_suffix; | |
| 674 | out_end += len; | |
| 675 | s.next_request_start += len; | |
| 676 | continue; | |
| 677 | }, | |
| 678 | else => { | |
| 679 | assert(s.state == .receiving_body); | |
| 680 | const available = try fill(s, head_end); | |
| 681 | const n = cp.feed(available); | |
| 682 | switch (cp.state) { | |
| 683 | .invalid => return error.HttpChunkInvalid, | |
| 684 | .data => { | |
| 685 | if (cp.chunk_len == 0) { | |
| 686 | // The next bytes in the stream are trailers, | |
| 687 | // or \r\n to indicate end of chunked body. | |
| 688 | // | |
| 689 | // This function must append the trailers at | |
| 690 | // head_end so that headers and trailers are | |
| 691 | // together. | |
| 692 | // | |
| 693 | // Since returning 0 would indicate end of | |
| 694 | // stream, this function must read all the | |
| 695 | // trailers before returning. | |
| 696 | if (s.next_request_start > head_end) rebase(s, head_end); | |
| 697 | var hp: http.HeadParser = .{}; | |
| 698 | { | |
| 699 | const bytes = s.read_buffer[head_end..s.read_buffer_len]; | |
| 700 | const end = hp.feed(bytes); | |
| 701 | if (hp.state == .finished) { | |
| 702 | cp.state = .invalid; | |
| 703 | s.state = .ready; | |
| 704 | s.next_request_start = s.read_buffer_len - bytes.len + end; | |
| 705 | return out_end; | |
| 706 | } | |
| 707 | } | |
| 708 | while (true) { | |
| 709 | const buf = s.read_buffer[s.read_buffer_len..]; | |
| 710 | if (buf.len == 0) | |
| 711 | return error.HttpHeadersOversize; | |
| 712 | const read_n = try s.connection.stream.read(buf); | |
| 713 | s.read_buffer_len += read_n; | |
| 714 | const bytes = buf[0..read_n]; | |
| 715 | const end = hp.feed(bytes); | |
| 716 | if (hp.state == .finished) { | |
| 717 | cp.state = .invalid; | |
| 718 | s.state = .ready; | |
| 719 | s.next_request_start = s.read_buffer_len - bytes.len + end; | |
| 720 | return out_end; | |
| 721 | } | |
| 722 | } | |
| 723 | } | |
| 724 | const data = available[n..]; | |
| 725 | const len = @min(cp.chunk_len, data.len, buffer.len); | |
| 726 | @memcpy(buffer[0..len], data[0..len]); | |
| 727 | cp.chunk_len -= len; | |
| 728 | if (cp.chunk_len == 0) | |
| 729 | cp.state = .data_suffix; | |
| 730 | out_end += len; | |
| 731 | s.next_request_start += n + len; | |
| 732 | continue; | |
| 733 | }, | |
| 734 | else => continue, | |
| 735 | } | |
| 736 | }, | |
| 504 | var sec_websocket_key: ?[]const u8 = null; | |
| 505 | var upgrade_name: ?[]const u8 = null; | |
| 506 | var it = request.iterateHeaders(); | |
| 507 | while (it.next()) |header| { | |
| 508 | if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { | |
| 509 | sec_websocket_key = header.value; | |
| 510 | } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) { | |
| 511 | upgrade_name = header.value; | |
| 737 | 512 | } |
| 738 | 513 | } |
| 739 | return out_end; | |
| 514 | ||
| 515 | const name = upgrade_name orelse return .none; | |
| 516 | if (std.ascii.eqlIgnoreCase(name, "websocket")) return .{ .websocket = sec_websocket_key }; | |
| 517 | return .{ .other = name }; | |
| 740 | 518 | } |
| 741 | 519 | |
| 742 | pub const ReaderError = Response.WriteError || error{ | |
| 743 | /// The client sent an expect HTTP header value other than | |
| 744 | /// "100-continue". | |
| 745 | HttpExpectationFailed, | |
| 520 | pub const WebSocketOptions = struct { | |
| 521 | /// The value from `UpgradeRequest.websocket` (sec-websocket-key header value). | |
| 522 | key: []const u8, | |
| 523 | reason: ?[]const u8 = null, | |
| 524 | extra_headers: []const http.Header = &.{}, | |
| 746 | 525 | }; |
| 747 | 526 | |
| 527 | /// The header is not guaranteed to be sent until `WebSocket.flush` is | |
| 528 | /// called on the returned struct. | |
| 529 | pub fn respondWebSocket(request: *Request, options: WebSocketOptions) ExpectContinueError!WebSocket { | |
| 530 | if (request.head.expect != null) return error.HttpExpectationFailed; | |
| 531 | ||
| 532 | const out = request.server.out; | |
| 533 | const version: http.Version = .@"HTTP/1.1"; | |
| 534 | const status: http.Status = .switching_protocols; | |
| 535 | const phrase = options.reason orelse status.phrase() orelse ""; | |
| 536 | ||
| 537 | assert(request.head.version == version); | |
| 538 | assert(request.head.method == .GET); | |
| 539 | ||
| 540 | var sha1 = std.crypto.hash.Sha1.init(.{}); | |
| 541 | sha1.update(options.key); | |
| 542 | sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | |
| 543 | var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined; | |
| 544 | sha1.final(&digest); | |
| 545 | try out.print("{s} {d} {s}\r\n", .{ @tagName(version), @intFromEnum(status), phrase }); | |
| 546 | try out.writeAll("connection: upgrade\r\nupgrade: websocket\r\nsec-websocket-accept: "); | |
| 547 | const base64_digest = try out.writableArray(28); | |
| 548 | assert(std.base64.standard.Encoder.encode(base64_digest, &digest).len == base64_digest.len); | |
| 549 | out.advance(base64_digest.len); | |
| 550 | try out.writeAll("\r\n"); | |
| 551 | ||
| 552 | for (options.extra_headers) |header| { | |
| 553 | assert(header.name.len != 0); | |
| 554 | var bufs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; | |
| 555 | try out.writeVecAll(&bufs); | |
| 556 | } | |
| 557 | ||
| 558 | try out.writeAll("\r\n"); | |
| 559 | ||
| 560 | return .{ | |
| 561 | .input = request.server.reader.in, | |
| 562 | .output = request.server.out, | |
| 563 | .key = options.key, | |
| 564 | }; | |
| 565 | } | |
| 566 | ||
| 748 | 567 | /// In the case that the request contains "expect: 100-continue", this |
| 749 | 568 | /// function writes the continuation header, which means it can fail with a |
| 750 | 569 | /// write error. After sending the continuation header, it sets the |
| 751 | 570 | /// request's expect field to `null`. |
| 752 | 571 | /// |
| 753 | 572 | /// Asserts that this function is only called once. |
| 754 | pub fn reader(request: *Request) ReaderError!std.io.AnyReader { | |
| 755 | const s = request.server; | |
| 756 | assert(s.state == .received_head); | |
| 757 | s.state = .receiving_body; | |
| 758 | s.next_request_start = request.head_end; | |
| 759 | ||
| 760 | if (request.head.expect) |expect| { | |
| 761 | if (mem.eql(u8, expect, "100-continue")) { | |
| 762 | try request.server.connection.stream.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); | |
| 763 | request.head.expect = null; | |
| 764 | } else { | |
| 765 | return error.HttpExpectationFailed; | |
| 766 | } | |
| 767 | } | |
| 573 | /// | |
| 574 | /// See `readerExpectNone` for an infallible alternative that cannot write | |
| 575 | /// to the server output stream. | |
| 576 | pub fn readerExpectContinue(request: *Request, buffer: []u8) ExpectContinueError!*Reader { | |
| 577 | const flush = request.head.expect != null; | |
| 578 | try writeExpectContinue(request); | |
| 579 | if (flush) try request.server.out.flush(); | |
| 580 | return readerExpectNone(request, buffer); | |
| 581 | } | |
| 768 | 582 | |
| 769 | switch (request.head.transfer_encoding) { | |
| 770 | .chunked => { | |
| 771 | request.reader_state = .{ .chunk_parser = http.ChunkParser.init }; | |
| 772 | return .{ | |
| 773 | .readFn = read_chunked, | |
| 774 | .context = request, | |
| 775 | }; | |
| 776 | }, | |
| 777 | .none => { | |
| 778 | request.reader_state = .{ | |
| 779 | .remaining_content_length = request.head.content_length orelse 0, | |
| 780 | }; | |
| 781 | return .{ | |
| 782 | .readFn = read_cl, | |
| 783 | .context = request, | |
| 784 | }; | |
| 785 | }, | |
| 786 | } | |
| 583 | /// Asserts the expect header is `null`. The caller must handle the | |
| 584 | /// expectation manually and then set the value to `null` prior to calling | |
| 585 | /// this function. | |
| 586 | /// | |
| 587 | /// Asserts that this function is only called once. | |
| 588 | /// | |
| 589 | /// Invalidates the string memory inside `Head`. | |
| 590 | pub fn readerExpectNone(request: *Request, buffer: []u8) *Reader { | |
| 591 | assert(request.server.reader.state == .received_head); | |
| 592 | assert(request.head.expect == null); | |
| 593 | request.head.invalidateStrings(); | |
| 594 | if (!request.head.method.requestHasBody()) return .ending; | |
| 595 | return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length); | |
| 596 | } | |
| 597 | ||
| 598 | pub const ExpectContinueError = error{ | |
| 599 | /// Failed to write "HTTP/1.1 100 Continue\r\n\r\n" to the stream. | |
| 600 | WriteFailed, | |
| 601 | /// The client sent an expect HTTP header value other than | |
| 602 | /// "100-continue". | |
| 603 | HttpExpectationFailed, | |
| 604 | }; | |
| 605 | ||
| 606 | pub fn writeExpectContinue(request: *Request) ExpectContinueError!void { | |
| 607 | const expect = request.head.expect orelse return; | |
| 608 | if (!mem.eql(u8, expect, "100-continue")) return error.HttpExpectationFailed; | |
| 609 | try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); | |
| 610 | request.head.expect = null; | |
| 787 | 611 | } |
| 788 | 612 | |
| 789 | 613 | /// Returns whether the connection should remain persistent. |
| 790 | /// If it would fail, it instead sets the Server state to `receiving_body` | |
| 614 | /// | |
| 615 | /// If it would fail, it instead sets the Server state to receiving body | |
| 791 | 616 | /// and returns false. |
| 792 | 617 | fn discardBody(request: *Request, keep_alive: bool) bool { |
| 793 | 618 | // Prepare to receive another request on the same connection. |
| ... | ... | @@ -798,350 +623,180 @@ pub const Request = struct { |
| 798 | 623 | // or the request body. |
| 799 | 624 | // If the connection won't be kept alive, then none of this matters |
| 800 | 625 | // because the connection will be severed after the response is sent. |
| 801 | const s = request.server; | |
| 802 | if (keep_alive and request.head.keep_alive) switch (s.state) { | |
| 626 | const r = &request.server.reader; | |
| 627 | if (keep_alive and request.head.keep_alive) switch (r.state) { | |
| 803 | 628 | .received_head => { |
| 804 | const r = request.reader() catch return false; | |
| 805 | _ = r.discard() catch return false; | |
| 806 | assert(s.state == .ready); | |
| 629 | if (request.head.method.requestHasBody()) { | |
| 630 | assert(request.head.transfer_encoding != .none or request.head.content_length != null); | |
| 631 | const reader_interface = request.readerExpectContinue(&.{}) catch return false; | |
| 632 | _ = reader_interface.discardRemaining() catch return false; | |
| 633 | assert(r.state == .ready); | |
| 634 | } else { | |
| 635 | r.state = .ready; | |
| 636 | } | |
| 807 | 637 | return true; |
| 808 | 638 | }, |
| 809 | .receiving_body, .ready => return true, | |
| 639 | .body_remaining_content_length, .body_remaining_chunk_len, .body_none, .ready => return true, | |
| 810 | 640 | else => unreachable, |
| 811 | 641 | }; |
| 812 | 642 | |
| 813 | 643 | // Avoid clobbering the state in case a reading stream already exists. |
| 814 | switch (s.state) { | |
| 815 | .received_head => s.state = .closing, | |
| 644 | switch (r.state) { | |
| 645 | .received_head => r.state = .closing, | |
| 816 | 646 | else => {}, |
| 817 | 647 | } |
| 818 | 648 | return false; |
| 819 | 649 | } |
| 820 | 650 | }; |
| 821 | 651 | |
| 822 | pub const Response = struct { | |
| 823 | stream: net.Stream, | |
| 824 | send_buffer: []u8, | |
| 825 | /// Index of the first byte in `send_buffer`. | |
| 826 | /// This is 0 unless a short write happens in `write`. | |
| 827 | send_buffer_start: usize, | |
| 828 | /// Index of the last byte + 1 in `send_buffer`. | |
| 829 | send_buffer_end: usize, | |
| 830 | /// `null` means transfer-encoding: chunked. | |
| 831 | /// As a debugging utility, counts down to zero as bytes are written. | |
| 832 | transfer_encoding: TransferEncoding, | |
| 833 | elide_body: bool, | |
| 834 | /// Indicates how much of the end of the `send_buffer` corresponds to a | |
| 835 | /// chunk. This amount of data will be wrapped by an HTTP chunk header. | |
| 836 | chunk_len: usize, | |
| 837 | ||
| 838 | pub const TransferEncoding = union(enum) { | |
| 839 | /// End of connection signals the end of the stream. | |
| 840 | none, | |
| 841 | /// As a debugging utility, counts down to zero as bytes are written. | |
| 842 | content_length: u64, | |
| 843 | /// Each chunk is wrapped in a header and trailer. | |
| 844 | chunked, | |
| 652 | /// See https://tools.ietf.org/html/rfc6455 | |
| 653 | pub const WebSocket = struct { | |
| 654 | key: []const u8, | |
| 655 | input: *Reader, | |
| 656 | output: *Writer, | |
| 657 | ||
| 658 | pub const Header0 = packed struct(u8) { | |
| 659 | opcode: Opcode, | |
| 660 | rsv3: u1 = 0, | |
| 661 | rsv2: u1 = 0, | |
| 662 | rsv1: u1 = 0, | |
| 663 | fin: bool, | |
| 845 | 664 | }; |
| 846 | 665 | |
| 847 | pub const WriteError = net.Stream.WriteError; | |
| 848 | ||
| 849 | /// When using content-length, asserts that the amount of data sent matches | |
| 850 | /// the value sent in the header, then calls `flush`. | |
| 851 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the | |
| 852 | /// end-of-stream message, then flushes the stream to the system. | |
| 853 | /// Respects the value of `elide_body` to omit all data after the headers. | |
| 854 | pub fn end(r: *Response) WriteError!void { | |
| 855 | switch (r.transfer_encoding) { | |
| 856 | .content_length => |len| { | |
| 857 | assert(len == 0); // Trips when end() called before all bytes written. | |
| 858 | try flush_cl(r); | |
| 859 | }, | |
| 860 | .none => { | |
| 861 | try flush_cl(r); | |
| 862 | }, | |
| 863 | .chunked => { | |
| 864 | try flush_chunked(r, &.{}); | |
| 865 | }, | |
| 866 | } | |
| 867 | r.* = undefined; | |
| 868 | } | |
| 869 | ||
| 870 | pub const EndChunkedOptions = struct { | |
| 871 | trailers: []const http.Header = &.{}, | |
| 666 | pub const Header1 = packed struct(u8) { | |
| 667 | payload_len: enum(u7) { | |
| 668 | len16 = 126, | |
| 669 | len64 = 127, | |
| 670 | _, | |
| 671 | }, | |
| 672 | mask: bool, | |
| 872 | 673 | }; |
| 873 | 674 | |
| 874 | /// Asserts that the Response is using transfer-encoding: chunked. | |
| 875 | /// Writes the end-of-stream message and any optional trailers, then | |
| 876 | /// flushes the stream to the system. | |
| 877 | /// Respects the value of `elide_body` to omit all data after the headers. | |
| 878 | /// Asserts there are at most 25 trailers. | |
| 879 | pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void { | |
| 880 | assert(r.transfer_encoding == .chunked); | |
| 881 | try flush_chunked(r, options.trailers); | |
| 882 | r.* = undefined; | |
| 883 | } | |
| 884 | ||
| 885 | /// If using content-length, asserts that writing these bytes to the client | |
| 886 | /// would not exceed the content-length value sent in the HTTP header. | |
| 887 | /// May return 0, which does not indicate end of stream. The caller decides | |
| 888 | /// when the end of stream occurs by calling `end`. | |
| 889 | pub fn write(r: *Response, bytes: []const u8) WriteError!usize { | |
| 890 | switch (r.transfer_encoding) { | |
| 891 | .content_length, .none => return write_cl(r, bytes), | |
| 892 | .chunked => return write_chunked(r, bytes), | |
| 893 | } | |
| 894 | } | |
| 895 | ||
| 896 | fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize { | |
| 897 | const r: *Response = @ptrCast(@alignCast(@constCast(context))); | |
| 675 | pub const Opcode = enum(u4) { | |
| 676 | continuation = 0, | |
| 677 | text = 1, | |
| 678 | binary = 2, | |
| 679 | connection_close = 8, | |
| 680 | ping = 9, | |
| 681 | /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional | |
| 682 | /// heartbeat. A response to an unsolicited Pong frame is not expected." | |
| 683 | pong = 10, | |
| 684 | _, | |
| 685 | }; | |
| 898 | 686 | |
| 899 | var trash: u64 = std.math.maxInt(u64); | |
| 900 | const len = switch (r.transfer_encoding) { | |
| 901 | .content_length => |*len| len, | |
| 902 | else => &trash, | |
| 903 | }; | |
| 687 | pub const ReadSmallTextMessageError = error{ | |
| 688 | ConnectionClose, | |
| 689 | UnexpectedOpCode, | |
| 690 | MessageTooBig, | |
| 691 | MissingMaskBit, | |
| 692 | ReadFailed, | |
| 693 | EndOfStream, | |
| 694 | }; | |
| 904 | 695 | |
| 905 | if (r.elide_body) { | |
| 906 | len.* -= bytes.len; | |
| 907 | return bytes.len; | |
| 908 | } | |
| 696 | pub const SmallMessage = struct { | |
| 697 | /// Can be text, binary, or ping. | |
| 698 | opcode: Opcode, | |
| 699 | data: []u8, | |
| 700 | }; | |
| 909 | 701 | |
| 910 | if (bytes.len + r.send_buffer_end > r.send_buffer.len) { | |
| 911 | const send_buffer_len = r.send_buffer_end - r.send_buffer_start; | |
| 912 | var iovecs: [2]std.posix.iovec_const = .{ | |
| 913 | .{ | |
| 914 | .base = r.send_buffer.ptr + r.send_buffer_start, | |
| 915 | .len = send_buffer_len, | |
| 916 | }, | |
| 917 | .{ | |
| 918 | .base = bytes.ptr, | |
| 919 | .len = bytes.len, | |
| 920 | }, | |
| 921 | }; | |
| 922 | const n = try r.stream.writev(&iovecs); | |
| 923 | ||
| 924 | if (n >= send_buffer_len) { | |
| 925 | // It was enough to reset the buffer. | |
| 926 | r.send_buffer_start = 0; | |
| 927 | r.send_buffer_end = 0; | |
| 928 | const bytes_n = n - send_buffer_len; | |
| 929 | len.* -= bytes_n; | |
| 930 | return bytes_n; | |
| 702 | /// Reads the next message from the WebSocket stream, failing if the | |
| 703 | /// message does not fit into the input buffer. The returned memory points | |
| 704 | /// into the input buffer and is invalidated on the next read. | |
| 705 | pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage { | |
| 706 | const in = ws.input; | |
| 707 | while (true) { | |
| 708 | const header = try in.takeArray(2); | |
| 709 | const h0: Header0 = @bitCast(header[0]); | |
| 710 | const h1: Header1 = @bitCast(header[1]); | |
| 711 | ||
| 712 | switch (h0.opcode) { | |
| 713 | .text, .binary, .pong, .ping => {}, | |
| 714 | .connection_close => return error.ConnectionClose, | |
| 715 | .continuation => return error.UnexpectedOpCode, | |
| 716 | _ => return error.UnexpectedOpCode, | |
| 931 | 717 | } |
| 932 | 718 | |
| 933 | // It didn't even make it through the existing buffer, let | |
| 934 | // alone the new bytes provided. | |
| 935 | r.send_buffer_start += n; | |
| 936 | return 0; | |
| 937 | } | |
| 938 | ||
| 939 | // All bytes can be stored in the remaining space of the buffer. | |
| 940 | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); | |
| 941 | r.send_buffer_end += bytes.len; | |
| 942 | len.* -= bytes.len; | |
| 943 | return bytes.len; | |
| 944 | } | |
| 719 | if (!h0.fin) return error.MessageTooBig; | |
| 720 | if (!h1.mask) return error.MissingMaskBit; | |
| 945 | 721 | |
| 946 | fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize { | |
| 947 | const r: *Response = @ptrCast(@alignCast(@constCast(context))); | |
| 948 | assert(r.transfer_encoding == .chunked); | |
| 949 | ||
| 950 | if (r.elide_body) | |
| 951 | return bytes.len; | |
| 952 | ||
| 953 | if (bytes.len + r.send_buffer_end > r.send_buffer.len) { | |
| 954 | const send_buffer_len = r.send_buffer_end - r.send_buffer_start; | |
| 955 | const chunk_len = r.chunk_len + bytes.len; | |
| 956 | var header_buf: [18]u8 = undefined; | |
| 957 | const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{chunk_len}) catch unreachable; | |
| 958 | ||
| 959 | var iovecs: [5]std.posix.iovec_const = .{ | |
| 960 | .{ | |
| 961 | .base = r.send_buffer.ptr + r.send_buffer_start, | |
| 962 | .len = send_buffer_len - r.chunk_len, | |
| 963 | }, | |
| 964 | .{ | |
| 965 | .base = chunk_header.ptr, | |
| 966 | .len = chunk_header.len, | |
| 967 | }, | |
| 968 | .{ | |
| 969 | .base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len, | |
| 970 | .len = r.chunk_len, | |
| 971 | }, | |
| 972 | .{ | |
| 973 | .base = bytes.ptr, | |
| 974 | .len = bytes.len, | |
| 975 | }, | |
| 976 | .{ | |
| 977 | .base = "\r\n", | |
| 978 | .len = 2, | |
| 979 | }, | |
| 722 | const len: usize = switch (h1.payload_len) { | |
| 723 | .len16 => try in.takeInt(u16, .big), | |
| 724 | .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageTooBig, | |
| 725 | else => @intFromEnum(h1.payload_len), | |
| 726 | }; | |
| 727 | if (len > in.buffer.len) return error.MessageTooBig; | |
| 728 | const mask: u32 = @bitCast((try in.takeArray(4)).*); | |
| 729 | const payload = try in.take(len); | |
| 730 | ||
| 731 | // Skip pongs. | |
| 732 | if (h0.opcode == .pong) continue; | |
| 733 | ||
| 734 | // The last item may contain a partial word of unused data. | |
| 735 | const floored_len = (payload.len / 4) * 4; | |
| 736 | const u32_payload: []align(1) u32 = @ptrCast(payload[0..floored_len]); | |
| 737 | for (u32_payload) |*elem| elem.* ^= mask; | |
| 738 | const mask_bytes: []const u8 = @ptrCast(&mask); | |
| 739 | for (payload[floored_len..], mask_bytes[0 .. payload.len - floored_len]) |*leftover, m| | |
| 740 | leftover.* ^= m; | |
| 741 | ||
| 742 | return .{ | |
| 743 | .opcode = h0.opcode, | |
| 744 | .data = payload, | |
| 980 | 745 | }; |
| 981 | // TODO make this writev instead of writevAll, which involves | |
| 982 | // complicating the logic of this function. | |
| 983 | try r.stream.writevAll(&iovecs); | |
| 984 | r.send_buffer_start = 0; | |
| 985 | r.send_buffer_end = 0; | |
| 986 | r.chunk_len = 0; | |
| 987 | return bytes.len; | |
| 988 | 746 | } |
| 989 | ||
| 990 | // All bytes can be stored in the remaining space of the buffer. | |
| 991 | @memcpy(r.send_buffer[r.send_buffer_end..][0..bytes.len], bytes); | |
| 992 | r.send_buffer_end += bytes.len; | |
| 993 | r.chunk_len += bytes.len; | |
| 994 | return bytes.len; | |
| 995 | 747 | } |
| 996 | 748 | |
| 997 | /// If using content-length, asserts that writing these bytes to the client | |
| 998 | /// would not exceed the content-length value sent in the HTTP header. | |
| 999 | pub fn writeAll(r: *Response, bytes: []const u8) WriteError!void { | |
| 1000 | var index: usize = 0; | |
| 1001 | while (index < bytes.len) { | |
| 1002 | index += try write(r, bytes[index..]); | |
| 1003 | } | |
| 749 | pub fn writeMessage(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void { | |
| 750 | var bufs: [1][]const u8 = .{data}; | |
| 751 | try writeMessageVecUnflushed(ws, &bufs, op); | |
| 752 | try ws.output.flush(); | |
| 1004 | 753 | } |
| 1005 | 754 | |
| 1006 | /// Sends all buffered data to the client. | |
| 1007 | /// This is redundant after calling `end`. | |
| 1008 | /// Respects the value of `elide_body` to omit all data after the headers. | |
| 1009 | pub fn flush(r: *Response) WriteError!void { | |
| 1010 | switch (r.transfer_encoding) { | |
| 1011 | .none, .content_length => return flush_cl(r), | |
| 1012 | .chunked => return flush_chunked(r, null), | |
| 1013 | } | |
| 755 | pub fn writeMessageUnflushed(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void { | |
| 756 | var bufs: [1][]const u8 = .{data}; | |
| 757 | try writeMessageVecUnflushed(ws, &bufs, op); | |
| 1014 | 758 | } |
| 1015 | 759 | |
| 1016 | fn flush_cl(r: *Response) WriteError!void { | |
| 1017 | try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]); | |
| 1018 | r.send_buffer_start = 0; | |
| 1019 | r.send_buffer_end = 0; | |
| 760 | pub fn writeMessageVec(ws: *WebSocket, data: [][]const u8, op: Opcode) Writer.Error!void { | |
| 761 | try writeMessageVecUnflushed(ws, data, op); | |
| 762 | try ws.output.flush(); | |
| 1020 | 763 | } |
| 1021 | 764 | |
| 1022 | fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void { | |
| 1023 | const max_trailers = 25; | |
| 1024 | if (end_trailers) |trailers| assert(trailers.len <= max_trailers); | |
| 1025 | assert(r.transfer_encoding == .chunked); | |
| 1026 | ||
| 1027 | const http_headers = r.send_buffer[r.send_buffer_start .. r.send_buffer_end - r.chunk_len]; | |
| 1028 | ||
| 1029 | if (r.elide_body) { | |
| 1030 | try r.stream.writeAll(http_headers); | |
| 1031 | r.send_buffer_start = 0; | |
| 1032 | r.send_buffer_end = 0; | |
| 1033 | r.chunk_len = 0; | |
| 1034 | return; | |
| 1035 | } | |
| 1036 | ||
| 1037 | var header_buf: [18]u8 = undefined; | |
| 1038 | const chunk_header = std.fmt.bufPrint(&header_buf, "{x}\r\n", .{r.chunk_len}) catch unreachable; | |
| 1039 | ||
| 1040 | var iovecs: [max_trailers * 4 + 5]std.posix.iovec_const = undefined; | |
| 1041 | var iovecs_len: usize = 0; | |
| 1042 | ||
| 1043 | iovecs[iovecs_len] = .{ | |
| 1044 | .base = http_headers.ptr, | |
| 1045 | .len = http_headers.len, | |
| 765 | pub fn writeMessageVecUnflushed(ws: *WebSocket, data: [][]const u8, op: Opcode) Writer.Error!void { | |
| 766 | const total_len = l: { | |
| 767 | var total_len: u64 = 0; | |
| 768 | for (data) |iovec| total_len += iovec.len; | |
| 769 | break :l total_len; | |
| 1046 | 770 | }; |
| 1047 | iovecs_len += 1; | |
| 1048 | ||
| 1049 | if (r.chunk_len > 0) { | |
| 1050 | iovecs[iovecs_len] = .{ | |
| 1051 | .base = chunk_header.ptr, | |
| 1052 | .len = chunk_header.len, | |
| 1053 | }; | |
| 1054 | iovecs_len += 1; | |
| 1055 | ||
| 1056 | iovecs[iovecs_len] = .{ | |
| 1057 | .base = r.send_buffer.ptr + r.send_buffer_end - r.chunk_len, | |
| 1058 | .len = r.chunk_len, | |
| 1059 | }; | |
| 1060 | iovecs_len += 1; | |
| 1061 | ||
| 1062 | iovecs[iovecs_len] = .{ | |
| 1063 | .base = "\r\n", | |
| 1064 | .len = 2, | |
| 1065 | }; | |
| 1066 | iovecs_len += 1; | |
| 1067 | } | |
| 1068 | ||
| 1069 | if (end_trailers) |trailers| { | |
| 1070 | iovecs[iovecs_len] = .{ | |
| 1071 | .base = "0\r\n", | |
| 1072 | .len = 3, | |
| 1073 | }; | |
| 1074 | iovecs_len += 1; | |
| 1075 | ||
| 1076 | for (trailers) |trailer| { | |
| 1077 | iovecs[iovecs_len] = .{ | |
| 1078 | .base = trailer.name.ptr, | |
| 1079 | .len = trailer.name.len, | |
| 1080 | }; | |
| 1081 | iovecs_len += 1; | |
| 1082 | ||
| 1083 | iovecs[iovecs_len] = .{ | |
| 1084 | .base = ": ", | |
| 1085 | .len = 2, | |
| 1086 | }; | |
| 1087 | iovecs_len += 1; | |
| 1088 | ||
| 1089 | if (trailer.value.len != 0) { | |
| 1090 | iovecs[iovecs_len] = .{ | |
| 1091 | .base = trailer.value.ptr, | |
| 1092 | .len = trailer.value.len, | |
| 1093 | }; | |
| 1094 | iovecs_len += 1; | |
| 1095 | } | |
| 1096 | ||
| 1097 | iovecs[iovecs_len] = .{ | |
| 1098 | .base = "\r\n", | |
| 1099 | .len = 2, | |
| 1100 | }; | |
| 1101 | iovecs_len += 1; | |
| 1102 | } | |
| 1103 | ||
| 1104 | iovecs[iovecs_len] = .{ | |
| 1105 | .base = "\r\n", | |
| 1106 | .len = 2, | |
| 1107 | }; | |
| 1108 | iovecs_len += 1; | |
| 771 | const out = ws.output; | |
| 772 | try out.writeByte(@bitCast(@as(Header0, .{ | |
| 773 | .opcode = op, | |
| 774 | .fin = true, | |
| 775 | }))); | |
| 776 | switch (total_len) { | |
| 777 | 0...125 => try out.writeByte(@bitCast(@as(Header1, .{ | |
| 778 | .payload_len = @enumFromInt(total_len), | |
| 779 | .mask = false, | |
| 780 | }))), | |
| 781 | 126...0xffff => { | |
| 782 | try out.writeByte(@bitCast(@as(Header1, .{ | |
| 783 | .payload_len = .len16, | |
| 784 | .mask = false, | |
| 785 | }))); | |
| 786 | try out.writeInt(u16, @intCast(total_len), .big); | |
| 787 | }, | |
| 788 | else => { | |
| 789 | try out.writeByte(@bitCast(@as(Header1, .{ | |
| 790 | .payload_len = .len64, | |
| 791 | .mask = false, | |
| 792 | }))); | |
| 793 | try out.writeInt(u64, total_len, .big); | |
| 794 | }, | |
| 1109 | 795 | } |
| 1110 | ||
| 1111 | try r.stream.writevAll(iovecs[0..iovecs_len]); | |
| 1112 | r.send_buffer_start = 0; | |
| 1113 | r.send_buffer_end = 0; | |
| 1114 | r.chunk_len = 0; | |
| 796 | try out.writeVecAll(data); | |
| 1115 | 797 | } |
| 1116 | 798 | |
| 1117 | pub fn writer(r: *Response) std.io.AnyWriter { | |
| 1118 | return .{ | |
| 1119 | .writeFn = switch (r.transfer_encoding) { | |
| 1120 | .none, .content_length => write_cl, | |
| 1121 | .chunked => write_chunked, | |
| 1122 | }, | |
| 1123 | .context = r, | |
| 1124 | }; | |
| 799 | pub fn flush(ws: *WebSocket) Writer.Error!void { | |
| 800 | try ws.output.flush(); | |
| 1125 | 801 | } |
| 1126 | 802 | }; |
| 1127 | ||
| 1128 | fn rebase(s: *Server, index: usize) void { | |
| 1129 | const leftover = s.read_buffer[s.next_request_start..s.read_buffer_len]; | |
| 1130 | const dest = s.read_buffer[index..][0..leftover.len]; | |
| 1131 | if (leftover.len <= s.next_request_start - index) { | |
| 1132 | @memcpy(dest, leftover); | |
| 1133 | } else { | |
| 1134 | mem.copyBackwards(u8, dest, leftover); | |
| 1135 | } | |
| 1136 | s.read_buffer_len = index + leftover.len; | |
| 1137 | } | |
| 1138 | ||
| 1139 | const std = @import("../std.zig"); | |
| 1140 | const http = std.http; | |
| 1141 | const mem = std.mem; | |
| 1142 | const net = std.net; | |
| 1143 | const Uri = std.Uri; | |
| 1144 | const assert = std.debug.assert; | |
| 1145 | const testing = std.testing; | |
| 1146 | ||
| 1147 | const Server = @This(); |
lib/std/http/WebSocket.zig deleted-246| ... | ... | @@ -1,246 +0,0 @@ |
| 1 | //! See https://tools.ietf.org/html/rfc6455 | |
| 2 | ||
| 3 | const builtin = @import("builtin"); | |
| 4 | const std = @import("std"); | |
| 5 | const WebSocket = @This(); | |
| 6 | const assert = std.debug.assert; | |
| 7 | const native_endian = builtin.cpu.arch.endian(); | |
| 8 | ||
| 9 | key: []const u8, | |
| 10 | request: *std.http.Server.Request, | |
| 11 | recv_fifo: std.fifo.LinearFifo(u8, .Slice), | |
| 12 | reader: std.io.AnyReader, | |
| 13 | response: std.http.Server.Response, | |
| 14 | /// Number of bytes that have been peeked but not discarded yet. | |
| 15 | outstanding_len: usize, | |
| 16 | ||
| 17 | pub const InitError = error{WebSocketUpgradeMissingKey} || | |
| 18 | std.http.Server.Request.ReaderError; | |
| 19 | ||
| 20 | pub fn init( | |
| 21 | request: *std.http.Server.Request, | |
| 22 | send_buffer: []u8, | |
| 23 | recv_buffer: []align(4) u8, | |
| 24 | ) InitError!?WebSocket { | |
| 25 | switch (request.head.version) { | |
| 26 | .@"HTTP/1.0" => return null, | |
| 27 | .@"HTTP/1.1" => if (request.head.method != .GET) return null, | |
| 28 | } | |
| 29 | ||
| 30 | var sec_websocket_key: ?[]const u8 = null; | |
| 31 | var upgrade_websocket: bool = false; | |
| 32 | var it = request.iterateHeaders(); | |
| 33 | while (it.next()) |header| { | |
| 34 | if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { | |
| 35 | sec_websocket_key = header.value; | |
| 36 | } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) { | |
| 37 | if (!std.ascii.eqlIgnoreCase(header.value, "websocket")) | |
| 38 | return null; | |
| 39 | upgrade_websocket = true; | |
| 40 | } | |
| 41 | } | |
| 42 | if (!upgrade_websocket) | |
| 43 | return null; | |
| 44 | ||
| 45 | const key = sec_websocket_key orelse return error.WebSocketUpgradeMissingKey; | |
| 46 | ||
| 47 | var sha1 = std.crypto.hash.Sha1.init(.{}); | |
| 48 | sha1.update(key); | |
| 49 | sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | |
| 50 | var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined; | |
| 51 | sha1.final(&digest); | |
| 52 | var base64_digest: [28]u8 = undefined; | |
| 53 | assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len); | |
| 54 | ||
| 55 | request.head.content_length = std.math.maxInt(u64); | |
| 56 | ||
| 57 | return .{ | |
| 58 | .key = key, | |
| 59 | .recv_fifo = std.fifo.LinearFifo(u8, .Slice).init(recv_buffer), | |
| 60 | .reader = try request.reader(), | |
| 61 | .response = request.respondStreaming(.{ | |
| 62 | .send_buffer = send_buffer, | |
| 63 | .respond_options = .{ | |
| 64 | .status = .switching_protocols, | |
| 65 | .extra_headers = &.{ | |
| 66 | .{ .name = "upgrade", .value = "websocket" }, | |
| 67 | .{ .name = "connection", .value = "upgrade" }, | |
| 68 | .{ .name = "sec-websocket-accept", .value = &base64_digest }, | |
| 69 | }, | |
| 70 | .transfer_encoding = .none, | |
| 71 | }, | |
| 72 | }), | |
| 73 | .request = request, | |
| 74 | .outstanding_len = 0, | |
| 75 | }; | |
| 76 | } | |
| 77 | ||
| 78 | pub const Header0 = packed struct(u8) { | |
| 79 | opcode: Opcode, | |
| 80 | rsv3: u1 = 0, | |
| 81 | rsv2: u1 = 0, | |
| 82 | rsv1: u1 = 0, | |
| 83 | fin: bool, | |
| 84 | }; | |
| 85 | ||
| 86 | pub const Header1 = packed struct(u8) { | |
| 87 | payload_len: enum(u7) { | |
| 88 | len16 = 126, | |
| 89 | len64 = 127, | |
| 90 | _, | |
| 91 | }, | |
| 92 | mask: bool, | |
| 93 | }; | |
| 94 | ||
| 95 | pub const Opcode = enum(u4) { | |
| 96 | continuation = 0, | |
| 97 | text = 1, | |
| 98 | binary = 2, | |
| 99 | connection_close = 8, | |
| 100 | ping = 9, | |
| 101 | /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional | |
| 102 | /// heartbeat. A response to an unsolicited Pong frame is not expected." | |
| 103 | pong = 10, | |
| 104 | _, | |
| 105 | }; | |
| 106 | ||
| 107 | pub const ReadSmallTextMessageError = error{ | |
| 108 | ConnectionClose, | |
| 109 | UnexpectedOpCode, | |
| 110 | MessageTooBig, | |
| 111 | MissingMaskBit, | |
| 112 | } || RecvError; | |
| 113 | ||
| 114 | pub const SmallMessage = struct { | |
| 115 | /// Can be text, binary, or ping. | |
| 116 | opcode: Opcode, | |
| 117 | data: []u8, | |
| 118 | }; | |
| 119 | ||
| 120 | /// Reads the next message from the WebSocket stream, failing if the message does not fit | |
| 121 | /// into `recv_buffer`. | |
| 122 | pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage { | |
| 123 | while (true) { | |
| 124 | const header_bytes = (try recv(ws, 2))[0..2]; | |
| 125 | const h0: Header0 = @bitCast(header_bytes[0]); | |
| 126 | const h1: Header1 = @bitCast(header_bytes[1]); | |
| 127 | ||
| 128 | switch (h0.opcode) { | |
| 129 | .text, .binary, .pong, .ping => {}, | |
| 130 | .connection_close => return error.ConnectionClose, | |
| 131 | .continuation => return error.UnexpectedOpCode, | |
| 132 | _ => return error.UnexpectedOpCode, | |
| 133 | } | |
| 134 | ||
| 135 | if (!h0.fin) return error.MessageTooBig; | |
| 136 | if (!h1.mask) return error.MissingMaskBit; | |
| 137 | ||
| 138 | const len: usize = switch (h1.payload_len) { | |
| 139 | .len16 => try recvReadInt(ws, u16), | |
| 140 | .len64 => std.math.cast(usize, try recvReadInt(ws, u64)) orelse return error.MessageTooBig, | |
| 141 | else => @intFromEnum(h1.payload_len), | |
| 142 | }; | |
| 143 | if (len > ws.recv_fifo.buf.len) return error.MessageTooBig; | |
| 144 | ||
| 145 | const mask: u32 = @bitCast((try recv(ws, 4))[0..4].*); | |
| 146 | const payload = try recv(ws, len); | |
| 147 | ||
| 148 | // Skip pongs. | |
| 149 | if (h0.opcode == .pong) continue; | |
| 150 | ||
| 151 | // The last item may contain a partial word of unused data. | |
| 152 | const floored_len = (payload.len / 4) * 4; | |
| 153 | const u32_payload: []align(1) u32 = @alignCast(std.mem.bytesAsSlice(u32, payload[0..floored_len])); | |
| 154 | for (u32_payload) |*elem| elem.* ^= mask; | |
| 155 | const mask_bytes = std.mem.asBytes(&mask)[0 .. payload.len - floored_len]; | |
| 156 | for (payload[floored_len..], mask_bytes) |*leftover, m| leftover.* ^= m; | |
| 157 | ||
| 158 | return .{ | |
| 159 | .opcode = h0.opcode, | |
| 160 | .data = payload, | |
| 161 | }; | |
| 162 | } | |
| 163 | } | |
| 164 | ||
| 165 | const RecvError = std.http.Server.Request.ReadError || error{EndOfStream}; | |
| 166 | ||
| 167 | fn recv(ws: *WebSocket, len: usize) RecvError![]u8 { | |
| 168 | ws.recv_fifo.discard(ws.outstanding_len); | |
| 169 | assert(len <= ws.recv_fifo.buf.len); | |
| 170 | if (len > ws.recv_fifo.count) { | |
| 171 | const small_buf = ws.recv_fifo.writableSlice(0); | |
| 172 | const needed = len - ws.recv_fifo.count; | |
| 173 | const buf = if (small_buf.len >= needed) small_buf else b: { | |
| 174 | ws.recv_fifo.realign(); | |
| 175 | break :b ws.recv_fifo.writableSlice(0); | |
| 176 | }; | |
| 177 | const n = try @as(RecvError!usize, @errorCast(ws.reader.readAtLeast(buf, needed))); | |
| 178 | if (n < needed) return error.EndOfStream; | |
| 179 | ws.recv_fifo.update(n); | |
| 180 | } | |
| 181 | ws.outstanding_len = len; | |
| 182 | // TODO: improve the std lib API so this cast isn't necessary. | |
| 183 | return @constCast(ws.recv_fifo.readableSliceOfLen(len)); | |
| 184 | } | |
| 185 | ||
| 186 | fn recvReadInt(ws: *WebSocket, comptime I: type) !I { | |
| 187 | const unswapped: I = @bitCast((try recv(ws, @sizeOf(I)))[0..@sizeOf(I)].*); | |
| 188 | return switch (native_endian) { | |
| 189 | .little => @byteSwap(unswapped), | |
| 190 | .big => unswapped, | |
| 191 | }; | |
| 192 | } | |
| 193 | ||
| 194 | pub const WriteError = std.http.Server.Response.WriteError; | |
| 195 | ||
| 196 | pub fn writeMessage(ws: *WebSocket, message: []const u8, opcode: Opcode) WriteError!void { | |
| 197 | const iovecs: [1]std.posix.iovec_const = .{ | |
| 198 | .{ .base = message.ptr, .len = message.len }, | |
| 199 | }; | |
| 200 | return writeMessagev(ws, &iovecs, opcode); | |
| 201 | } | |
| 202 | ||
| 203 | pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opcode: Opcode) WriteError!void { | |
| 204 | const total_len = l: { | |
| 205 | var total_len: u64 = 0; | |
| 206 | for (message) |iovec| total_len += iovec.len; | |
| 207 | break :l total_len; | |
| 208 | }; | |
| 209 | ||
| 210 | var header_buf: [2 + 8]u8 = undefined; | |
| 211 | header_buf[0] = @bitCast(@as(Header0, .{ | |
| 212 | .opcode = opcode, | |
| 213 | .fin = true, | |
| 214 | })); | |
| 215 | const header = switch (total_len) { | |
| 216 | 0...125 => blk: { | |
| 217 | header_buf[1] = @bitCast(@as(Header1, .{ | |
| 218 | .payload_len = @enumFromInt(total_len), | |
| 219 | .mask = false, | |
| 220 | })); | |
| 221 | break :blk header_buf[0..2]; | |
| 222 | }, | |
| 223 | 126...0xffff => blk: { | |
| 224 | header_buf[1] = @bitCast(@as(Header1, .{ | |
| 225 | .payload_len = .len16, | |
| 226 | .mask = false, | |
| 227 | })); | |
| 228 | std.mem.writeInt(u16, header_buf[2..4], @intCast(total_len), .big); | |
| 229 | break :blk header_buf[0..4]; | |
| 230 | }, | |
| 231 | else => blk: { | |
| 232 | header_buf[1] = @bitCast(@as(Header1, .{ | |
| 233 | .payload_len = .len64, | |
| 234 | .mask = false, | |
| 235 | })); | |
| 236 | std.mem.writeInt(u64, header_buf[2..10], total_len, .big); | |
| 237 | break :blk header_buf[0..10]; | |
| 238 | }, | |
| 239 | }; | |
| 240 | ||
| 241 | const response = &ws.response; | |
| 242 | try response.writeAll(header); | |
| 243 | for (message) |iovec| | |
| 244 | try response.writeAll(iovec.base[0..iovec.len]); | |
| 245 | try response.flush(); | |
| 246 | } |
lib/std/http/protocol.zig deleted-464| ... | ... | @@ -1,464 +0,0 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const builtin = @import("builtin"); | |
| 3 | const testing = std.testing; | |
| 4 | const mem = std.mem; | |
| 5 | ||
| 6 | const assert = std.debug.assert; | |
| 7 | ||
| 8 | pub const State = enum { | |
| 9 | invalid, | |
| 10 | ||
| 11 | // Begin header and trailer parsing states. | |
| 12 | ||
| 13 | start, | |
| 14 | seen_n, | |
| 15 | seen_r, | |
| 16 | seen_rn, | |
| 17 | seen_rnr, | |
| 18 | finished, | |
| 19 | ||
| 20 | // Begin transfer-encoding: chunked parsing states. | |
| 21 | ||
| 22 | chunk_head_size, | |
| 23 | chunk_head_ext, | |
| 24 | chunk_head_r, | |
| 25 | chunk_data, | |
| 26 | chunk_data_suffix, | |
| 27 | chunk_data_suffix_r, | |
| 28 | ||
| 29 | /// Returns true if the parser is in a content state (ie. not waiting for more headers). | |
| 30 | pub fn isContent(self: State) bool { | |
| 31 | return switch (self) { | |
| 32 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => false, | |
| 33 | .finished, .chunk_head_size, .chunk_head_ext, .chunk_head_r, .chunk_data, .chunk_data_suffix, .chunk_data_suffix_r => true, | |
| 34 | }; | |
| 35 | } | |
| 36 | }; | |
| 37 | ||
| 38 | pub const HeadersParser = struct { | |
| 39 | state: State = .start, | |
| 40 | /// A fixed buffer of len `max_header_bytes`. | |
| 41 | /// Pointers into this buffer are not stable until after a message is complete. | |
| 42 | header_bytes_buffer: []u8, | |
| 43 | header_bytes_len: u32, | |
| 44 | next_chunk_length: u64, | |
| 45 | /// `false`: headers. `true`: trailers. | |
| 46 | done: bool, | |
| 47 | ||
| 48 | /// Initializes the parser with a provided buffer `buf`. | |
| 49 | pub fn init(buf: []u8) HeadersParser { | |
| 50 | return .{ | |
| 51 | .header_bytes_buffer = buf, | |
| 52 | .header_bytes_len = 0, | |
| 53 | .done = false, | |
| 54 | .next_chunk_length = 0, | |
| 55 | }; | |
| 56 | } | |
| 57 | ||
| 58 | /// Reinitialize the parser. | |
| 59 | /// Asserts the parser is in the "done" state. | |
| 60 | pub fn reset(hp: *HeadersParser) void { | |
| 61 | assert(hp.done); | |
| 62 | hp.* = .{ | |
| 63 | .state = .start, | |
| 64 | .header_bytes_buffer = hp.header_bytes_buffer, | |
| 65 | .header_bytes_len = 0, | |
| 66 | .done = false, | |
| 67 | .next_chunk_length = 0, | |
| 68 | }; | |
| 69 | } | |
| 70 | ||
| 71 | pub fn get(hp: HeadersParser) []u8 { | |
| 72 | return hp.header_bytes_buffer[0..hp.header_bytes_len]; | |
| 73 | } | |
| 74 | ||
| 75 | pub fn findHeadersEnd(r: *HeadersParser, bytes: []const u8) u32 { | |
| 76 | var hp: std.http.HeadParser = .{ | |
| 77 | .state = switch (r.state) { | |
| 78 | .start => .start, | |
| 79 | .seen_n => .seen_n, | |
| 80 | .seen_r => .seen_r, | |
| 81 | .seen_rn => .seen_rn, | |
| 82 | .seen_rnr => .seen_rnr, | |
| 83 | .finished => .finished, | |
| 84 | else => unreachable, | |
| 85 | }, | |
| 86 | }; | |
| 87 | const result = hp.feed(bytes); | |
| 88 | r.state = switch (hp.state) { | |
| 89 | .start => .start, | |
| 90 | .seen_n => .seen_n, | |
| 91 | .seen_r => .seen_r, | |
| 92 | .seen_rn => .seen_rn, | |
| 93 | .seen_rnr => .seen_rnr, | |
| 94 | .finished => .finished, | |
| 95 | }; | |
| 96 | return @intCast(result); | |
| 97 | } | |
| 98 | ||
| 99 | pub fn findChunkedLen(r: *HeadersParser, bytes: []const u8) u32 { | |
| 100 | var cp: std.http.ChunkParser = .{ | |
| 101 | .state = switch (r.state) { | |
| 102 | .chunk_head_size => .head_size, | |
| 103 | .chunk_head_ext => .head_ext, | |
| 104 | .chunk_head_r => .head_r, | |
| 105 | .chunk_data => .data, | |
| 106 | .chunk_data_suffix => .data_suffix, | |
| 107 | .chunk_data_suffix_r => .data_suffix_r, | |
| 108 | .invalid => .invalid, | |
| 109 | else => unreachable, | |
| 110 | }, | |
| 111 | .chunk_len = r.next_chunk_length, | |
| 112 | }; | |
| 113 | const result = cp.feed(bytes); | |
| 114 | r.state = switch (cp.state) { | |
| 115 | .head_size => .chunk_head_size, | |
| 116 | .head_ext => .chunk_head_ext, | |
| 117 | .head_r => .chunk_head_r, | |
| 118 | .data => .chunk_data, | |
| 119 | .data_suffix => .chunk_data_suffix, | |
| 120 | .data_suffix_r => .chunk_data_suffix_r, | |
| 121 | .invalid => .invalid, | |
| 122 | }; | |
| 123 | r.next_chunk_length = cp.chunk_len; | |
| 124 | return @intCast(result); | |
| 125 | } | |
| 126 | ||
| 127 | /// Returns whether or not the parser has finished parsing a complete | |
| 128 | /// message. A message is only complete after the entire body has been read | |
| 129 | /// and any trailing headers have been parsed. | |
| 130 | pub fn isComplete(r: *HeadersParser) bool { | |
| 131 | return r.done and r.state == .finished; | |
| 132 | } | |
| 133 | ||
| 134 | pub const CheckCompleteHeadError = error{HttpHeadersOversize}; | |
| 135 | ||
| 136 | /// Pushes `in` into the parser. Returns the number of bytes consumed by | |
| 137 | /// the header. Any header bytes are appended to `header_bytes_buffer`. | |
| 138 | pub fn checkCompleteHead(hp: *HeadersParser, in: []const u8) CheckCompleteHeadError!u32 { | |
| 139 | if (hp.state.isContent()) return 0; | |
| 140 | ||
| 141 | const i = hp.findHeadersEnd(in); | |
| 142 | const data = in[0..i]; | |
| 143 | if (hp.header_bytes_len + data.len > hp.header_bytes_buffer.len) | |
| 144 | return error.HttpHeadersOversize; | |
| 145 | ||
| 146 | @memcpy(hp.header_bytes_buffer[hp.header_bytes_len..][0..data.len], data); | |
| 147 | hp.header_bytes_len += @intCast(data.len); | |
| 148 | ||
| 149 | return i; | |
| 150 | } | |
| 151 | ||
| 152 | pub const ReadError = error{ | |
| 153 | HttpChunkInvalid, | |
| 154 | }; | |
| 155 | ||
| 156 | /// Reads the body of the message into `buffer`. Returns the number of | |
| 157 | /// bytes placed in the buffer. | |
| 158 | /// | |
| 159 | /// If `skip` is true, the buffer will be unused and the body will be skipped. | |
| 160 | /// | |
| 161 | /// See `std.http.Client.Connection for an example of `conn`. | |
| 162 | pub fn read(r: *HeadersParser, conn: anytype, buffer: []u8, skip: bool) !usize { | |
| 163 | assert(r.state.isContent()); | |
| 164 | if (r.done) return 0; | |
| 165 | ||
| 166 | var out_index: usize = 0; | |
| 167 | while (true) { | |
| 168 | switch (r.state) { | |
| 169 | .invalid, .start, .seen_n, .seen_r, .seen_rn, .seen_rnr => unreachable, | |
| 170 | .finished => { | |
| 171 | const data_avail = r.next_chunk_length; | |
| 172 | ||
| 173 | if (skip) { | |
| 174 | conn.fill() catch |err| switch (err) { | |
| 175 | error.EndOfStream => { | |
| 176 | r.done = true; | |
| 177 | return 0; | |
| 178 | }, | |
| 179 | else => |e| return e, | |
| 180 | }; | |
| 181 | ||
| 182 | const nread = @min(conn.peek().len, data_avail); | |
| 183 | conn.drop(@intCast(nread)); | |
| 184 | r.next_chunk_length -= nread; | |
| 185 | ||
| 186 | if (r.next_chunk_length == 0 or nread == 0) r.done = true; | |
| 187 | ||
| 188 | return out_index; | |
| 189 | } else if (out_index < buffer.len) { | |
| 190 | const out_avail = buffer.len - out_index; | |
| 191 | ||
| 192 | const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); | |
| 193 | const nread = try conn.read(buffer[0..can_read]); | |
| 194 | r.next_chunk_length -= nread; | |
| 195 | ||
| 196 | if (r.next_chunk_length == 0 or nread == 0) r.done = true; | |
| 197 | ||
| 198 | return nread; | |
| 199 | } else { | |
| 200 | return out_index; | |
| 201 | } | |
| 202 | }, | |
| 203 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { | |
| 204 | conn.fill() catch |err| switch (err) { | |
| 205 | error.EndOfStream => { | |
| 206 | r.done = true; | |
| 207 | return 0; | |
| 208 | }, | |
| 209 | else => |e| return e, | |
| 210 | }; | |
| 211 | ||
| 212 | const i = r.findChunkedLen(conn.peek()); | |
| 213 | conn.drop(@intCast(i)); | |
| 214 | ||
| 215 | switch (r.state) { | |
| 216 | .invalid => return error.HttpChunkInvalid, | |
| 217 | .chunk_data => if (r.next_chunk_length == 0) { | |
| 218 | if (std.mem.eql(u8, conn.peek(), "\r\n")) { | |
| 219 | r.state = .finished; | |
| 220 | conn.drop(2); | |
| 221 | } else { | |
| 222 | // The trailer section is formatted identically | |
| 223 | // to the header section. | |
| 224 | r.state = .seen_rn; | |
| 225 | } | |
| 226 | r.done = true; | |
| 227 | ||
| 228 | return out_index; | |
| 229 | }, | |
| 230 | else => return out_index, | |
| 231 | } | |
| 232 | ||
| 233 | continue; | |
| 234 | }, | |
| 235 | .chunk_data => { | |
| 236 | const data_avail = r.next_chunk_length; | |
| 237 | const out_avail = buffer.len - out_index; | |
| 238 | ||
| 239 | if (skip) { | |
| 240 | conn.fill() catch |err| switch (err) { | |
| 241 | error.EndOfStream => { | |
| 242 | r.done = true; | |
| 243 | return 0; | |
| 244 | }, | |
| 245 | else => |e| return e, | |
| 246 | }; | |
| 247 | ||
| 248 | const nread = @min(conn.peek().len, data_avail); | |
| 249 | conn.drop(@intCast(nread)); | |
| 250 | r.next_chunk_length -= nread; | |
| 251 | } else if (out_avail > 0) { | |
| 252 | const can_read: usize = @intCast(@min(data_avail, out_avail)); | |
| 253 | const nread = try conn.read(buffer[out_index..][0..can_read]); | |
| 254 | r.next_chunk_length -= nread; | |
| 255 | out_index += nread; | |
| 256 | } | |
| 257 | ||
| 258 | if (r.next_chunk_length == 0) { | |
| 259 | r.state = .chunk_data_suffix; | |
| 260 | continue; | |
| 261 | } | |
| 262 | ||
| 263 | return out_index; | |
| 264 | }, | |
| 265 | } | |
| 266 | } | |
| 267 | } | |
| 268 | }; | |
| 269 | ||
| 270 | inline fn int16(array: *const [2]u8) u16 { | |
| 271 | return @as(u16, @bitCast(array.*)); | |
| 272 | } | |
| 273 | ||
| 274 | inline fn int24(array: *const [3]u8) u24 { | |
| 275 | return @as(u24, @bitCast(array.*)); | |
| 276 | } | |
| 277 | ||
| 278 | inline fn int32(array: *const [4]u8) u32 { | |
| 279 | return @as(u32, @bitCast(array.*)); | |
| 280 | } | |
| 281 | ||
| 282 | inline fn intShift(comptime T: type, x: anytype) T { | |
| 283 | switch (@import("builtin").cpu.arch.endian()) { | |
| 284 | .little => return @as(T, @truncate(x >> (@bitSizeOf(@TypeOf(x)) - @bitSizeOf(T)))), | |
| 285 | .big => return @as(T, @truncate(x)), | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | /// A buffered (and peekable) Connection. | |
| 290 | const MockBufferedConnection = struct { | |
| 291 | pub const buffer_size = 0x2000; | |
| 292 | ||
| 293 | conn: std.io.FixedBufferStream([]const u8), | |
| 294 | buf: [buffer_size]u8 = undefined, | |
| 295 | start: u16 = 0, | |
| 296 | end: u16 = 0, | |
| 297 | ||
| 298 | pub fn fill(conn: *MockBufferedConnection) ReadError!void { | |
| 299 | if (conn.end != conn.start) return; | |
| 300 | ||
| 301 | const nread = try conn.conn.read(conn.buf[0..]); | |
| 302 | if (nread == 0) return error.EndOfStream; | |
| 303 | conn.start = 0; | |
| 304 | conn.end = @as(u16, @truncate(nread)); | |
| 305 | } | |
| 306 | ||
| 307 | pub fn peek(conn: *MockBufferedConnection) []const u8 { | |
| 308 | return conn.buf[conn.start..conn.end]; | |
| 309 | } | |
| 310 | ||
| 311 | pub fn drop(conn: *MockBufferedConnection, num: u16) void { | |
| 312 | conn.start += num; | |
| 313 | } | |
| 314 | ||
| 315 | pub fn readAtLeast(conn: *MockBufferedConnection, buffer: []u8, len: usize) ReadError!usize { | |
| 316 | var out_index: u16 = 0; | |
| 317 | while (out_index < len) { | |
| 318 | const available = conn.end - conn.start; | |
| 319 | const left = buffer.len - out_index; | |
| 320 | ||
| 321 | if (available > 0) { | |
| 322 | const can_read = @as(u16, @truncate(@min(available, left))); | |
| 323 | ||
| 324 | @memcpy(buffer[out_index..][0..can_read], conn.buf[conn.start..][0..can_read]); | |
| 325 | out_index += can_read; | |
| 326 | conn.start += can_read; | |
| 327 | ||
| 328 | continue; | |
| 329 | } | |
| 330 | ||
| 331 | if (left > conn.buf.len) { | |
| 332 | // skip the buffer if the output is large enough | |
| 333 | return conn.conn.read(buffer[out_index..]); | |
| 334 | } | |
| 335 | ||
| 336 | try conn.fill(); | |
| 337 | } | |
| 338 | ||
| 339 | return out_index; | |
| 340 | } | |
| 341 | ||
| 342 | pub fn read(conn: *MockBufferedConnection, buffer: []u8) ReadError!usize { | |
| 343 | return conn.readAtLeast(buffer, 1); | |
| 344 | } | |
| 345 | ||
| 346 | pub const ReadError = std.io.FixedBufferStream([]const u8).ReadError || error{EndOfStream}; | |
| 347 | pub const Reader = std.io.GenericReader(*MockBufferedConnection, ReadError, read); | |
| 348 | ||
| 349 | pub fn reader(conn: *MockBufferedConnection) Reader { | |
| 350 | return Reader{ .context = conn }; | |
| 351 | } | |
| 352 | ||
| 353 | pub fn writeAll(conn: *MockBufferedConnection, buffer: []const u8) WriteError!void { | |
| 354 | return conn.conn.writeAll(buffer); | |
| 355 | } | |
| 356 | ||
| 357 | pub fn write(conn: *MockBufferedConnection, buffer: []const u8) WriteError!usize { | |
| 358 | return conn.conn.write(buffer); | |
| 359 | } | |
| 360 | ||
| 361 | pub const WriteError = std.io.FixedBufferStream([]const u8).WriteError; | |
| 362 | pub const Writer = std.io.GenericWriter(*MockBufferedConnection, WriteError, write); | |
| 363 | ||
| 364 | pub fn writer(conn: *MockBufferedConnection) Writer { | |
| 365 | return Writer{ .context = conn }; | |
| 366 | } | |
| 367 | }; | |
| 368 | ||
| 369 | test "HeadersParser.read length" { | |
| 370 | // mock BufferedConnection for read | |
| 371 | var headers_buf: [256]u8 = undefined; | |
| 372 | ||
| 373 | var r = HeadersParser.init(&headers_buf); | |
| 374 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\nHello"; | |
| 375 | ||
| 376 | var conn: MockBufferedConnection = .{ | |
| 377 | .conn = std.io.fixedBufferStream(data), | |
| 378 | }; | |
| 379 | ||
| 380 | while (true) { // read headers | |
| 381 | try conn.fill(); | |
| 382 | ||
| 383 | const nchecked = try r.checkCompleteHead(conn.peek()); | |
| 384 | conn.drop(@intCast(nchecked)); | |
| 385 | ||
| 386 | if (r.state.isContent()) break; | |
| 387 | } | |
| 388 | ||
| 389 | var buf: [8]u8 = undefined; | |
| 390 | ||
| 391 | r.next_chunk_length = 5; | |
| 392 | const len = try r.read(&conn, &buf, false); | |
| 393 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 394 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 395 | ||
| 396 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 5\r\n\r\n", r.get()); | |
| 397 | } | |
| 398 | ||
| 399 | test "HeadersParser.read chunked" { | |
| 400 | // mock BufferedConnection for read | |
| 401 | ||
| 402 | var headers_buf: [256]u8 = undefined; | |
| 403 | var r = HeadersParser.init(&headers_buf); | |
| 404 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\n\r\n"; | |
| 405 | ||
| 406 | var conn: MockBufferedConnection = .{ | |
| 407 | .conn = std.io.fixedBufferStream(data), | |
| 408 | }; | |
| 409 | ||
| 410 | while (true) { // read headers | |
| 411 | try conn.fill(); | |
| 412 | ||
| 413 | const nchecked = try r.checkCompleteHead(conn.peek()); | |
| 414 | conn.drop(@intCast(nchecked)); | |
| 415 | ||
| 416 | if (r.state.isContent()) break; | |
| 417 | } | |
| 418 | var buf: [8]u8 = undefined; | |
| 419 | ||
| 420 | r.state = .chunk_head_size; | |
| 421 | const len = try r.read(&conn, &buf, false); | |
| 422 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 423 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 424 | ||
| 425 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n", r.get()); | |
| 426 | } | |
| 427 | ||
| 428 | test "HeadersParser.read chunked trailer" { | |
| 429 | // mock BufferedConnection for read | |
| 430 | ||
| 431 | var headers_buf: [256]u8 = undefined; | |
| 432 | var r = HeadersParser.init(&headers_buf); | |
| 433 | const data = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n2\r\nHe\r\n2\r\nll\r\n1\r\no\r\n0\r\nContent-Type: text/plain\r\n\r\n"; | |
| 434 | ||
| 435 | var conn: MockBufferedConnection = .{ | |
| 436 | .conn = std.io.fixedBufferStream(data), | |
| 437 | }; | |
| 438 | ||
| 439 | while (true) { // read headers | |
| 440 | try conn.fill(); | |
| 441 | ||
| 442 | const nchecked = try r.checkCompleteHead(conn.peek()); | |
| 443 | conn.drop(@intCast(nchecked)); | |
| 444 | ||
| 445 | if (r.state.isContent()) break; | |
| 446 | } | |
| 447 | var buf: [8]u8 = undefined; | |
| 448 | ||
| 449 | r.state = .chunk_head_size; | |
| 450 | const len = try r.read(&conn, &buf, false); | |
| 451 | try std.testing.expectEqual(@as(usize, 5), len); | |
| 452 | try std.testing.expectEqualStrings("Hello", buf[0..len]); | |
| 453 | ||
| 454 | while (true) { // read headers | |
| 455 | try conn.fill(); | |
| 456 | ||
| 457 | const nchecked = try r.checkCompleteHead(conn.peek()); | |
| 458 | conn.drop(@intCast(nchecked)); | |
| 459 | ||
| 460 | if (r.state.isContent()) break; | |
| 461 | } | |
| 462 | ||
| 463 | try std.testing.expectEqualStrings("GET / HTTP/1.1\r\nHost: localhost\r\n\r\nContent-Type: text/plain\r\n\r\n", r.get()); | |
| 464 | } |
lib/std/http/test.zig+315-353| ... | ... | @@ -10,32 +10,33 @@ const expectError = std.testing.expectError; |
| 10 | 10 | |
| 11 | 11 | test "trailers" { |
| 12 | 12 | const test_server = try createTestServer(struct { |
| 13 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 14 | var header_buffer: [1024]u8 = undefined; | |
| 13 | fn run(test_server: *TestServer) anyerror!void { | |
| 14 | const net_server = &test_server.net_server; | |
| 15 | var recv_buffer: [1024]u8 = undefined; | |
| 16 | var send_buffer: [1024]u8 = undefined; | |
| 15 | 17 | var remaining: usize = 1; |
| 16 | 18 | while (remaining != 0) : (remaining -= 1) { |
| 17 | const conn = try net_server.accept(); | |
| 18 | defer conn.stream.close(); | |
| 19 | const connection = try net_server.accept(); | |
| 20 | defer connection.stream.close(); | |
| 19 | 21 | |
| 20 | var server = http.Server.init(conn, &header_buffer); | |
| 22 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 23 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 24 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 21 | 25 | |
| 22 | try expectEqual(.ready, server.state); | |
| 26 | try expectEqual(.ready, server.reader.state); | |
| 23 | 27 | var request = try server.receiveHead(); |
| 24 | 28 | try serve(&request); |
| 25 | try expectEqual(.ready, server.state); | |
| 29 | try expectEqual(.ready, server.reader.state); | |
| 26 | 30 | } |
| 27 | 31 | } |
| 28 | 32 | |
| 29 | 33 | fn serve(request: *http.Server.Request) !void { |
| 30 | 34 | try expectEqualStrings(request.head.target, "/trailer"); |
| 31 | 35 | |
| 32 | var send_buffer: [1024]u8 = undefined; | |
| 33 | var response = request.respondStreaming(.{ | |
| 34 | .send_buffer = &send_buffer, | |
| 35 | }); | |
| 36 | try response.writeAll("Hello, "); | |
| 36 | var response = try request.respondStreaming(&.{}, .{}); | |
| 37 | try response.writer.writeAll("Hello, "); | |
| 37 | 38 | try response.flush(); |
| 38 | try response.writeAll("World!\n"); | |
| 39 | try response.writer.writeAll("World!\n"); | |
| 39 | 40 | try response.flush(); |
| 40 | 41 | try response.endChunked(.{ |
| 41 | 42 | .trailers = &.{ |
| ... | ... | @@ -58,34 +59,32 @@ test "trailers" { |
| 58 | 59 | const uri = try std.Uri.parse(location); |
| 59 | 60 | |
| 60 | 61 | { |
| 61 | var server_header_buffer: [1024]u8 = undefined; | |
| 62 | var req = try client.open(.GET, uri, .{ | |
| 63 | .server_header_buffer = &server_header_buffer, | |
| 64 | }); | |
| 62 | var req = try client.request(.GET, uri, .{}); | |
| 65 | 63 | defer req.deinit(); |
| 66 | 64 | |
| 67 | try req.send(); | |
| 68 | try req.wait(); | |
| 69 | ||
| 70 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 71 | defer gpa.free(body); | |
| 65 | try req.sendBodiless(); | |
| 66 | var response = try req.receiveHead(&.{}); | |
| 72 | 67 | |
| 73 | try expectEqualStrings("Hello, World!\n", body); | |
| 74 | ||
| 75 | var it = req.response.iterateHeaders(); | |
| 76 | 68 | { |
| 69 | var it = response.head.iterateHeaders(); | |
| 77 | 70 | const header = it.next().?; |
| 78 | try expect(!it.is_trailer); | |
| 79 | 71 | try expectEqualStrings("transfer-encoding", header.name); |
| 80 | 72 | try expectEqualStrings("chunked", header.value); |
| 73 | try expectEqual(null, it.next()); | |
| 81 | 74 | } |
| 75 | ||
| 76 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 77 | defer gpa.free(body); | |
| 78 | ||
| 79 | try expectEqualStrings("Hello, World!\n", body); | |
| 80 | ||
| 82 | 81 | { |
| 82 | var it = response.iterateTrailers(); | |
| 83 | 83 | const header = it.next().?; |
| 84 | try expect(it.is_trailer); | |
| 85 | 84 | try expectEqualStrings("X-Checksum", header.name); |
| 86 | 85 | try expectEqualStrings("aaaa", header.value); |
| 86 | try expectEqual(null, it.next()); | |
| 87 | 87 | } |
| 88 | try expectEqual(null, it.next()); | |
| 89 | 88 | } |
| 90 | 89 | |
| 91 | 90 | // connection has been kept alive |
| ... | ... | @@ -94,19 +93,24 @@ test "trailers" { |
| 94 | 93 | |
| 95 | 94 | test "HTTP server handles a chunked transfer coding request" { |
| 96 | 95 | const test_server = try createTestServer(struct { |
| 97 | fn run(net_server: *std.net.Server) !void { | |
| 98 | var header_buffer: [8192]u8 = undefined; | |
| 99 | const conn = try net_server.accept(); | |
| 100 | defer conn.stream.close(); | |
| 101 | ||
| 102 | var server = http.Server.init(conn, &header_buffer); | |
| 96 | fn run(test_server: *TestServer) anyerror!void { | |
| 97 | const net_server = &test_server.net_server; | |
| 98 | var recv_buffer: [8192]u8 = undefined; | |
| 99 | var send_buffer: [500]u8 = undefined; | |
| 100 | const connection = try net_server.accept(); | |
| 101 | defer connection.stream.close(); | |
| 102 | ||
| 103 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 104 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 105 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 103 | 106 | var request = try server.receiveHead(); |
| 104 | 107 | |
| 105 | 108 | try expect(request.head.transfer_encoding == .chunked); |
| 106 | 109 | |
| 107 | 110 | var buf: [128]u8 = undefined; |
| 108 | const n = try (try request.reader()).readAll(&buf); | |
| 109 | try expect(mem.eql(u8, buf[0..n], "ABCD")); | |
| 111 | var br = try request.readerExpectContinue(&.{}); | |
| 112 | const n = try br.readSliceShort(&buf); | |
| 113 | try expectEqualStrings("ABCD", buf[0..n]); | |
| 110 | 114 | |
| 111 | 115 | try request.respond("message from server!\n", .{ |
| 112 | 116 | .extra_headers = &.{ |
| ... | ... | @@ -154,16 +158,20 @@ test "HTTP server handles a chunked transfer coding request" { |
| 154 | 158 | |
| 155 | 159 | test "echo content server" { |
| 156 | 160 | const test_server = try createTestServer(struct { |
| 157 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 158 | var read_buffer: [1024]u8 = undefined; | |
| 161 | fn run(test_server: *TestServer) anyerror!void { | |
| 162 | const net_server = &test_server.net_server; | |
| 163 | var recv_buffer: [1024]u8 = undefined; | |
| 164 | var send_buffer: [100]u8 = undefined; | |
| 159 | 165 | |
| 160 | accept: while (true) { | |
| 161 | const conn = try net_server.accept(); | |
| 162 | defer conn.stream.close(); | |
| 166 | accept: while (!test_server.shutting_down) { | |
| 167 | const connection = try net_server.accept(); | |
| 168 | defer connection.stream.close(); | |
| 163 | 169 | |
| 164 | var http_server = http.Server.init(conn, &read_buffer); | |
| 170 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 171 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 172 | var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 165 | 173 | |
| 166 | while (http_server.state == .ready) { | |
| 174 | while (http_server.reader.state == .ready) { | |
| 167 | 175 | var request = http_server.receiveHead() catch |err| switch (err) { |
| 168 | 176 | error.HttpConnectionClosing => continue :accept, |
| 169 | 177 | else => |e| return e, |
| ... | ... | @@ -173,8 +181,12 @@ test "echo content server" { |
| 173 | 181 | } |
| 174 | 182 | if (request.head.expect) |expect_header_value| { |
| 175 | 183 | if (mem.eql(u8, expect_header_value, "garbage")) { |
| 176 | try expectError(error.HttpExpectationFailed, request.reader()); | |
| 177 | try request.respond("", .{ .keep_alive = false }); | |
| 184 | try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{})); | |
| 185 | request.head.expect = null; | |
| 186 | try request.respond("", .{ | |
| 187 | .keep_alive = false, | |
| 188 | .status = .expectation_failed, | |
| 189 | }); | |
| 178 | 190 | continue; |
| 179 | 191 | } |
| 180 | 192 | } |
| ... | ... | @@ -195,16 +207,16 @@ test "echo content server" { |
| 195 | 207 | // request.head.target, |
| 196 | 208 | //}); |
| 197 | 209 | |
| 198 | const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192); | |
| 210 | try expect(mem.startsWith(u8, request.head.target, "/echo-content")); | |
| 211 | try expectEqualStrings("text/plain", request.head.content_type.?); | |
| 212 | ||
| 213 | // head strings expire here | |
| 214 | const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited); | |
| 199 | 215 | defer std.testing.allocator.free(body); |
| 200 | 216 | |
| 201 | try expect(mem.startsWith(u8, request.head.target, "/echo-content")); | |
| 202 | 217 | try expectEqualStrings("Hello, World!\n", body); |
| 203 | try expectEqualStrings("text/plain", request.head.content_type.?); | |
| 204 | 218 | |
| 205 | var send_buffer: [100]u8 = undefined; | |
| 206 | var response = request.respondStreaming(.{ | |
| 207 | .send_buffer = &send_buffer, | |
| 219 | var response = try request.respondStreaming(&.{}, .{ | |
| 208 | 220 | .content_length = switch (request.head.transfer_encoding) { |
| 209 | 221 | .chunked => null, |
| 210 | 222 | .none => len: { |
| ... | ... | @@ -213,9 +225,8 @@ test "echo content server" { |
| 213 | 225 | }, |
| 214 | 226 | }, |
| 215 | 227 | }); |
| 216 | ||
| 217 | 228 | try response.flush(); // Test an early flush to send the HTTP headers before the body. |
| 218 | const w = response.writer(); | |
| 229 | const w = &response.writer; | |
| 219 | 230 | try w.writeAll("Hello, "); |
| 220 | 231 | try w.writeAll("World!\n"); |
| 221 | 232 | try response.end(); |
| ... | ... | @@ -241,35 +252,35 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { |
| 241 | 252 | // In this case, the response is expected to stream until the connection is |
| 242 | 253 | // closed, indicating the end of the body. |
| 243 | 254 | const test_server = try createTestServer(struct { |
| 244 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 245 | var header_buffer: [1000]u8 = undefined; | |
| 255 | fn run(test_server: *TestServer) anyerror!void { | |
| 256 | const net_server = &test_server.net_server; | |
| 257 | var recv_buffer: [1000]u8 = undefined; | |
| 258 | var send_buffer: [500]u8 = undefined; | |
| 246 | 259 | var remaining: usize = 1; |
| 247 | 260 | while (remaining != 0) : (remaining -= 1) { |
| 248 | const conn = try net_server.accept(); | |
| 249 | defer conn.stream.close(); | |
| 261 | const connection = try net_server.accept(); | |
| 262 | defer connection.stream.close(); | |
| 250 | 263 | |
| 251 | var server = http.Server.init(conn, &header_buffer); | |
| 264 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 265 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 266 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 252 | 267 | |
| 253 | try expectEqual(.ready, server.state); | |
| 268 | try expectEqual(.ready, server.reader.state); | |
| 254 | 269 | var request = try server.receiveHead(); |
| 255 | 270 | try expectEqualStrings(request.head.target, "/foo"); |
| 256 | var send_buffer: [500]u8 = undefined; | |
| 257 | var response = request.respondStreaming(.{ | |
| 258 | .send_buffer = &send_buffer, | |
| 271 | var buf: [30]u8 = undefined; | |
| 272 | var response = try request.respondStreaming(&buf, .{ | |
| 259 | 273 | .respond_options = .{ |
| 260 | 274 | .transfer_encoding = .none, |
| 261 | 275 | }, |
| 262 | 276 | }); |
| 263 | var total: usize = 0; | |
| 277 | const w = &response.writer; | |
| 264 | 278 | for (0..500) |i| { |
| 265 | var buf: [30]u8 = undefined; | |
| 266 | const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i}); | |
| 267 | try response.writeAll(line); | |
| 268 | total += line.len; | |
| 279 | try w.print("{d}, ah ha ha!\n", .{i}); | |
| 269 | 280 | } |
| 270 | try expectEqual(7390, total); | |
| 281 | try w.flush(); | |
| 271 | 282 | try response.end(); |
| 272 | try expectEqual(.closing, server.state); | |
| 283 | try expectEqual(.closing, server.reader.state); | |
| 273 | 284 | } |
| 274 | 285 | } |
| 275 | 286 | }); |
| ... | ... | @@ -284,7 +295,7 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { |
| 284 | 295 | |
| 285 | 296 | var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded |
| 286 | 297 | var stream_reader = stream.reader(&tiny_buffer); |
| 287 | const response = try stream_reader.interface().allocRemaining(gpa, .limited(8192)); | |
| 298 | const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); | |
| 288 | 299 | defer gpa.free(response); |
| 289 | 300 | |
| 290 | 301 | var expected_response = std.ArrayList(u8).init(gpa); |
| ... | ... | @@ -308,15 +319,20 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { |
| 308 | 319 | |
| 309 | 320 | test "receiving arbitrary http headers from the client" { |
| 310 | 321 | const test_server = try createTestServer(struct { |
| 311 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 312 | var read_buffer: [666]u8 = undefined; | |
| 322 | fn run(test_server: *TestServer) anyerror!void { | |
| 323 | const net_server = &test_server.net_server; | |
| 324 | var recv_buffer: [666]u8 = undefined; | |
| 325 | var send_buffer: [777]u8 = undefined; | |
| 313 | 326 | var remaining: usize = 1; |
| 314 | 327 | while (remaining != 0) : (remaining -= 1) { |
| 315 | const conn = try net_server.accept(); | |
| 316 | defer conn.stream.close(); | |
| 328 | const connection = try net_server.accept(); | |
| 329 | defer connection.stream.close(); | |
| 317 | 330 | |
| 318 | var server = http.Server.init(conn, &read_buffer); | |
| 319 | try expectEqual(.ready, server.state); | |
| 331 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 332 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 333 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 334 | ||
| 335 | try expectEqual(.ready, server.reader.state); | |
| 320 | 336 | var request = try server.receiveHead(); |
| 321 | 337 | try expectEqualStrings("/bar", request.head.target); |
| 322 | 338 | var it = request.iterateHeaders(); |
| ... | ... | @@ -350,7 +366,7 @@ test "receiving arbitrary http headers from the client" { |
| 350 | 366 | |
| 351 | 367 | var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded |
| 352 | 368 | var stream_reader = stream.reader(&tiny_buffer); |
| 353 | const response = try stream_reader.interface().allocRemaining(gpa, .limited(8192)); | |
| 369 | const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); | |
| 354 | 370 | defer gpa.free(response); |
| 355 | 371 | |
| 356 | 372 | var expected_response = std.ArrayList(u8).init(gpa); |
| ... | ... | @@ -368,19 +384,21 @@ test "general client/server API coverage" { |
| 368 | 384 | return error.SkipZigTest; |
| 369 | 385 | } |
| 370 | 386 | |
| 371 | const global = struct { | |
| 372 | var handle_new_requests = true; | |
| 373 | }; | |
| 374 | 387 | const test_server = try createTestServer(struct { |
| 375 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 376 | var client_header_buffer: [1024]u8 = undefined; | |
| 377 | outer: while (global.handle_new_requests) { | |
| 388 | fn run(test_server: *TestServer) anyerror!void { | |
| 389 | const net_server = &test_server.net_server; | |
| 390 | var recv_buffer: [1024]u8 = undefined; | |
| 391 | var send_buffer: [100]u8 = undefined; | |
| 392 | ||
| 393 | outer: while (!test_server.shutting_down) { | |
| 378 | 394 | var connection = try net_server.accept(); |
| 379 | 395 | defer connection.stream.close(); |
| 380 | 396 | |
| 381 | var http_server = http.Server.init(connection, &client_header_buffer); | |
| 397 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 398 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 399 | var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 382 | 400 | |
| 383 | while (http_server.state == .ready) { | |
| 401 | while (http_server.reader.state == .ready) { | |
| 384 | 402 | var request = http_server.receiveHead() catch |err| switch (err) { |
| 385 | 403 | error.HttpConnectionClosing => continue :outer, |
| 386 | 404 | else => |e| return e, |
| ... | ... | @@ -393,21 +411,19 @@ test "general client/server API coverage" { |
| 393 | 411 | |
| 394 | 412 | fn handleRequest(request: *http.Server.Request, listen_port: u16) !void { |
| 395 | 413 | const log = std.log.scoped(.server); |
| 414 | const gpa = std.testing.allocator; | |
| 396 | 415 | |
| 397 | log.info("{f} {s} {s}", .{ | |
| 398 | request.head.method, @tagName(request.head.version), request.head.target, | |
| 399 | }); | |
| 416 | log.info("{t} {t} {s}", .{ request.head.method, request.head.version, request.head.target }); | |
| 417 | const target = try gpa.dupe(u8, request.head.target); | |
| 418 | defer gpa.free(target); | |
| 400 | 419 | |
| 401 | const gpa = std.testing.allocator; | |
| 402 | const body = try (try request.reader()).readAllAlloc(gpa, 8192); | |
| 420 | const reader = (try request.readerExpectContinue(&.{})); | |
| 421 | const body = try reader.allocRemaining(gpa, .unlimited); | |
| 403 | 422 | defer gpa.free(body); |
| 404 | 423 | |
| 405 | var send_buffer: [100]u8 = undefined; | |
| 406 | ||
| 407 | if (mem.startsWith(u8, request.head.target, "/get")) { | |
| 408 | var response = request.respondStreaming(.{ | |
| 409 | .send_buffer = &send_buffer, | |
| 410 | .content_length = if (mem.indexOf(u8, request.head.target, "?chunked") == null) | |
| 424 | if (mem.startsWith(u8, target, "/get")) { | |
| 425 | var response = try request.respondStreaming(&.{}, .{ | |
| 426 | .content_length = if (mem.indexOf(u8, target, "?chunked") == null) | |
| 411 | 427 | 14 |
| 412 | 428 | else |
| 413 | 429 | null, |
| ... | ... | @@ -417,27 +433,27 @@ test "general client/server API coverage" { |
| 417 | 433 | }, |
| 418 | 434 | }, |
| 419 | 435 | }); |
| 420 | const w = response.writer(); | |
| 436 | const w = &response.writer; | |
| 421 | 437 | try w.writeAll("Hello, "); |
| 422 | 438 | try w.writeAll("World!\n"); |
| 423 | 439 | try response.end(); |
| 424 | 440 | // Writing again would cause an assertion failure. |
| 425 | } else if (mem.startsWith(u8, request.head.target, "/large")) { | |
| 426 | var response = request.respondStreaming(.{ | |
| 427 | .send_buffer = &send_buffer, | |
| 441 | } else if (mem.startsWith(u8, target, "/large")) { | |
| 442 | var response = try request.respondStreaming(&.{}, .{ | |
| 428 | 443 | .content_length = 14 * 1024 + 14 * 10, |
| 429 | 444 | }); |
| 430 | 445 | |
| 431 | 446 | try response.flush(); // Test an early flush to send the HTTP headers before the body. |
| 432 | 447 | |
| 433 | const w = response.writer(); | |
| 448 | const w = &response.writer; | |
| 434 | 449 | |
| 435 | 450 | var i: u32 = 0; |
| 436 | 451 | while (i < 5) : (i += 1) { |
| 437 | 452 | try w.writeAll("Hello, World!\n"); |
| 438 | 453 | } |
| 439 | 454 | |
| 440 | try w.writeAll("Hello, World!\n" ** 1024); | |
| 455 | var vec: [1][]const u8 = .{"Hello, World!\n"}; | |
| 456 | try w.writeSplatAll(&vec, 1024); | |
| 441 | 457 | |
| 442 | 458 | i = 0; |
| 443 | 459 | while (i < 5) : (i += 1) { |
| ... | ... | @@ -445,9 +461,8 @@ test "general client/server API coverage" { |
| 445 | 461 | } |
| 446 | 462 | |
| 447 | 463 | try response.end(); |
| 448 | } else if (mem.eql(u8, request.head.target, "/redirect/1")) { | |
| 449 | var response = request.respondStreaming(.{ | |
| 450 | .send_buffer = &send_buffer, | |
| 464 | } else if (mem.eql(u8, target, "/redirect/1")) { | |
| 465 | var response = try request.respondStreaming(&.{}, .{ | |
| 451 | 466 | .respond_options = .{ |
| 452 | 467 | .status = .found, |
| 453 | 468 | .extra_headers = &.{ |
| ... | ... | @@ -456,18 +471,18 @@ test "general client/server API coverage" { |
| 456 | 471 | }, |
| 457 | 472 | }); |
| 458 | 473 | |
| 459 | const w = response.writer(); | |
| 474 | const w = &response.writer; | |
| 460 | 475 | try w.writeAll("Hello, "); |
| 461 | 476 | try w.writeAll("Redirected!\n"); |
| 462 | 477 | try response.end(); |
| 463 | } else if (mem.eql(u8, request.head.target, "/redirect/2")) { | |
| 478 | } else if (mem.eql(u8, target, "/redirect/2")) { | |
| 464 | 479 | try request.respond("Hello, Redirected!\n", .{ |
| 465 | 480 | .status = .found, |
| 466 | 481 | .extra_headers = &.{ |
| 467 | 482 | .{ .name = "location", .value = "/redirect/1" }, |
| 468 | 483 | }, |
| 469 | 484 | }); |
| 470 | } else if (mem.eql(u8, request.head.target, "/redirect/3")) { | |
| 485 | } else if (mem.eql(u8, target, "/redirect/3")) { | |
| 471 | 486 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/2", .{ |
| 472 | 487 | listen_port, |
| 473 | 488 | }); |
| ... | ... | @@ -479,23 +494,23 @@ test "general client/server API coverage" { |
| 479 | 494 | .{ .name = "location", .value = location }, |
| 480 | 495 | }, |
| 481 | 496 | }); |
| 482 | } else if (mem.eql(u8, request.head.target, "/redirect/4")) { | |
| 497 | } else if (mem.eql(u8, target, "/redirect/4")) { | |
| 483 | 498 | try request.respond("Hello, Redirected!\n", .{ |
| 484 | 499 | .status = .found, |
| 485 | 500 | .extra_headers = &.{ |
| 486 | 501 | .{ .name = "location", .value = "/redirect/3" }, |
| 487 | 502 | }, |
| 488 | 503 | }); |
| 489 | } else if (mem.eql(u8, request.head.target, "/redirect/5")) { | |
| 504 | } else if (mem.eql(u8, target, "/redirect/5")) { | |
| 490 | 505 | try request.respond("Hello, Redirected!\n", .{ |
| 491 | 506 | .status = .found, |
| 492 | 507 | .extra_headers = &.{ |
| 493 | 508 | .{ .name = "location", .value = "/%2525" }, |
| 494 | 509 | }, |
| 495 | 510 | }); |
| 496 | } else if (mem.eql(u8, request.head.target, "/%2525")) { | |
| 511 | } else if (mem.eql(u8, target, "/%2525")) { | |
| 497 | 512 | try request.respond("Encoded redirect successful!\n", .{}); |
| 498 | } else if (mem.eql(u8, request.head.target, "/redirect/invalid")) { | |
| 513 | } else if (mem.eql(u8, target, "/redirect/invalid")) { | |
| 499 | 514 | const invalid_port = try getUnusedTcpPort(); |
| 500 | 515 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port}); |
| 501 | 516 | defer gpa.free(location); |
| ... | ... | @@ -506,7 +521,7 @@ test "general client/server API coverage" { |
| 506 | 521 | .{ .name = "location", .value = location }, |
| 507 | 522 | }, |
| 508 | 523 | }); |
| 509 | } else if (mem.eql(u8, request.head.target, "/empty")) { | |
| 524 | } else if (mem.eql(u8, target, "/empty")) { | |
| 510 | 525 | try request.respond("", .{ |
| 511 | 526 | .extra_headers = &.{ |
| 512 | 527 | .{ .name = "empty", .value = "" }, |
| ... | ... | @@ -524,17 +539,13 @@ test "general client/server API coverage" { |
| 524 | 539 | return s.listen_address.in.getPort(); |
| 525 | 540 | } |
| 526 | 541 | }); |
| 527 | defer { | |
| 528 | global.handle_new_requests = false; | |
| 529 | test_server.destroy(); | |
| 530 | } | |
| 542 | defer test_server.destroy(); | |
| 531 | 543 | |
| 532 | 544 | const log = std.log.scoped(.client); |
| 533 | 545 | |
| 534 | 546 | const gpa = std.testing.allocator; |
| 535 | 547 | var client: http.Client = .{ .allocator = gpa }; |
| 536 | errdefer client.deinit(); | |
| 537 | // defer client.deinit(); handled below | |
| 548 | defer client.deinit(); | |
| 538 | 549 | |
| 539 | 550 | const port = test_server.port(); |
| 540 | 551 | |
| ... | ... | @@ -544,20 +555,19 @@ test "general client/server API coverage" { |
| 544 | 555 | const uri = try std.Uri.parse(location); |
| 545 | 556 | |
| 546 | 557 | log.info("{s}", .{location}); |
| 547 | var server_header_buffer: [1024]u8 = undefined; | |
| 548 | var req = try client.open(.GET, uri, .{ | |
| 549 | .server_header_buffer = &server_header_buffer, | |
| 550 | }); | |
| 558 | var redirect_buffer: [1024]u8 = undefined; | |
| 559 | var req = try client.request(.GET, uri, .{}); | |
| 551 | 560 | defer req.deinit(); |
| 552 | 561 | |
| 553 | try req.send(); | |
| 554 | try req.wait(); | |
| 562 | try req.sendBodiless(); | |
| 563 | var response = try req.receiveHead(&redirect_buffer); | |
| 564 | ||
| 565 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 555 | 566 | |
| 556 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 567 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 557 | 568 | defer gpa.free(body); |
| 558 | 569 | |
| 559 | 570 | try expectEqualStrings("Hello, World!\n", body); |
| 560 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 561 | 571 | } |
| 562 | 572 | |
| 563 | 573 | // connection has been kept alive |
| ... | ... | @@ -569,16 +579,14 @@ test "general client/server API coverage" { |
| 569 | 579 | const uri = try std.Uri.parse(location); |
| 570 | 580 | |
| 571 | 581 | log.info("{s}", .{location}); |
| 572 | var server_header_buffer: [1024]u8 = undefined; | |
| 573 | var req = try client.open(.GET, uri, .{ | |
| 574 | .server_header_buffer = &server_header_buffer, | |
| 575 | }); | |
| 582 | var redirect_buffer: [1024]u8 = undefined; | |
| 583 | var req = try client.request(.GET, uri, .{}); | |
| 576 | 584 | defer req.deinit(); |
| 577 | 585 | |
| 578 | try req.send(); | |
| 579 | try req.wait(); | |
| 586 | try req.sendBodiless(); | |
| 587 | var response = try req.receiveHead(&redirect_buffer); | |
| 580 | 588 | |
| 581 | const body = try req.reader().readAllAlloc(gpa, 8192 * 1024); | |
| 589 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 582 | 590 | defer gpa.free(body); |
| 583 | 591 | |
| 584 | 592 | try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); |
| ... | ... | @@ -593,21 +601,20 @@ test "general client/server API coverage" { |
| 593 | 601 | const uri = try std.Uri.parse(location); |
| 594 | 602 | |
| 595 | 603 | log.info("{s}", .{location}); |
| 596 | var server_header_buffer: [1024]u8 = undefined; | |
| 597 | var req = try client.open(.HEAD, uri, .{ | |
| 598 | .server_header_buffer = &server_header_buffer, | |
| 599 | }); | |
| 604 | var redirect_buffer: [1024]u8 = undefined; | |
| 605 | var req = try client.request(.HEAD, uri, .{}); | |
| 600 | 606 | defer req.deinit(); |
| 601 | 607 | |
| 602 | try req.send(); | |
| 603 | try req.wait(); | |
| 608 | try req.sendBodiless(); | |
| 609 | var response = try req.receiveHead(&redirect_buffer); | |
| 610 | ||
| 611 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 612 | try expectEqual(14, response.head.content_length.?); | |
| 604 | 613 | |
| 605 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 614 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 606 | 615 | defer gpa.free(body); |
| 607 | 616 | |
| 608 | 617 | try expectEqualStrings("", body); |
| 609 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 610 | try expectEqual(14, req.response.content_length.?); | |
| 611 | 618 | } |
| 612 | 619 | |
| 613 | 620 | // connection has been kept alive |
| ... | ... | @@ -619,20 +626,19 @@ test "general client/server API coverage" { |
| 619 | 626 | const uri = try std.Uri.parse(location); |
| 620 | 627 | |
| 621 | 628 | log.info("{s}", .{location}); |
| 622 | var server_header_buffer: [1024]u8 = undefined; | |
| 623 | var req = try client.open(.GET, uri, .{ | |
| 624 | .server_header_buffer = &server_header_buffer, | |
| 625 | }); | |
| 629 | var redirect_buffer: [1024]u8 = undefined; | |
| 630 | var req = try client.request(.GET, uri, .{}); | |
| 626 | 631 | defer req.deinit(); |
| 627 | 632 | |
| 628 | try req.send(); | |
| 629 | try req.wait(); | |
| 633 | try req.sendBodiless(); | |
| 634 | var response = try req.receiveHead(&redirect_buffer); | |
| 635 | ||
| 636 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 630 | 637 | |
| 631 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 638 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 632 | 639 | defer gpa.free(body); |
| 633 | 640 | |
| 634 | 641 | try expectEqualStrings("Hello, World!\n", body); |
| 635 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 636 | 642 | } |
| 637 | 643 | |
| 638 | 644 | // connection has been kept alive |
| ... | ... | @@ -644,21 +650,20 @@ test "general client/server API coverage" { |
| 644 | 650 | const uri = try std.Uri.parse(location); |
| 645 | 651 | |
| 646 | 652 | log.info("{s}", .{location}); |
| 647 | var server_header_buffer: [1024]u8 = undefined; | |
| 648 | var req = try client.open(.HEAD, uri, .{ | |
| 649 | .server_header_buffer = &server_header_buffer, | |
| 650 | }); | |
| 653 | var redirect_buffer: [1024]u8 = undefined; | |
| 654 | var req = try client.request(.HEAD, uri, .{}); | |
| 651 | 655 | defer req.deinit(); |
| 652 | 656 | |
| 653 | try req.send(); | |
| 654 | try req.wait(); | |
| 657 | try req.sendBodiless(); | |
| 658 | var response = try req.receiveHead(&redirect_buffer); | |
| 655 | 659 | |
| 656 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 660 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 661 | try expect(response.head.transfer_encoding == .chunked); | |
| 662 | ||
| 663 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 657 | 664 | defer gpa.free(body); |
| 658 | 665 | |
| 659 | 666 | try expectEqualStrings("", body); |
| 660 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 661 | try expect(req.response.transfer_encoding == .chunked); | |
| 662 | 667 | } |
| 663 | 668 | |
| 664 | 669 | // connection has been kept alive |
| ... | ... | @@ -670,21 +675,21 @@ test "general client/server API coverage" { |
| 670 | 675 | const uri = try std.Uri.parse(location); |
| 671 | 676 | |
| 672 | 677 | log.info("{s}", .{location}); |
| 673 | var server_header_buffer: [1024]u8 = undefined; | |
| 674 | var req = try client.open(.GET, uri, .{ | |
| 675 | .server_header_buffer = &server_header_buffer, | |
| 678 | var redirect_buffer: [1024]u8 = undefined; | |
| 679 | var req = try client.request(.GET, uri, .{ | |
| 676 | 680 | .keep_alive = false, |
| 677 | 681 | }); |
| 678 | 682 | defer req.deinit(); |
| 679 | 683 | |
| 680 | try req.send(); | |
| 681 | try req.wait(); | |
| 684 | try req.sendBodiless(); | |
| 685 | var response = try req.receiveHead(&redirect_buffer); | |
| 686 | ||
| 687 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 682 | 688 | |
| 683 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 689 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 684 | 690 | defer gpa.free(body); |
| 685 | 691 | |
| 686 | 692 | try expectEqualStrings("Hello, World!\n", body); |
| 687 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 688 | 693 | } |
| 689 | 694 | |
| 690 | 695 | // connection has been closed |
| ... | ... | @@ -696,32 +701,32 @@ test "general client/server API coverage" { |
| 696 | 701 | const uri = try std.Uri.parse(location); |
| 697 | 702 | |
| 698 | 703 | log.info("{s}", .{location}); |
| 699 | var server_header_buffer: [1024]u8 = undefined; | |
| 700 | var req = try client.open(.GET, uri, .{ | |
| 701 | .server_header_buffer = &server_header_buffer, | |
| 704 | var redirect_buffer: [1024]u8 = undefined; | |
| 705 | var req = try client.request(.GET, uri, .{ | |
| 702 | 706 | .extra_headers = &.{ |
| 703 | 707 | .{ .name = "empty", .value = "" }, |
| 704 | 708 | }, |
| 705 | 709 | }); |
| 706 | 710 | defer req.deinit(); |
| 707 | 711 | |
| 708 | try req.send(); | |
| 709 | try req.wait(); | |
| 712 | try req.sendBodiless(); | |
| 713 | var response = try req.receiveHead(&redirect_buffer); | |
| 710 | 714 | |
| 711 | try std.testing.expectEqual(.ok, req.response.status); | |
| 712 | ||
| 713 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 714 | defer gpa.free(body); | |
| 715 | try std.testing.expectEqual(.ok, response.head.status); | |
| 715 | 716 | |
| 716 | try expectEqualStrings("", body); | |
| 717 | ||
| 718 | var it = req.response.iterateHeaders(); | |
| 717 | var it = response.head.iterateHeaders(); | |
| 719 | 718 | { |
| 720 | 719 | const header = it.next().?; |
| 721 | 720 | try expect(!it.is_trailer); |
| 722 | 721 | try expectEqualStrings("content-length", header.name); |
| 723 | 722 | try expectEqualStrings("0", header.value); |
| 724 | 723 | } |
| 724 | ||
| 725 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 726 | defer gpa.free(body); | |
| 727 | ||
| 728 | try expectEqualStrings("", body); | |
| 729 | ||
| 725 | 730 | { |
| 726 | 731 | const header = it.next().?; |
| 727 | 732 | try expect(!it.is_trailer); |
| ... | ... | @@ -740,16 +745,14 @@ test "general client/server API coverage" { |
| 740 | 745 | const uri = try std.Uri.parse(location); |
| 741 | 746 | |
| 742 | 747 | log.info("{s}", .{location}); |
| 743 | var server_header_buffer: [1024]u8 = undefined; | |
| 744 | var req = try client.open(.GET, uri, .{ | |
| 745 | .server_header_buffer = &server_header_buffer, | |
| 746 | }); | |
| 748 | var redirect_buffer: [1024]u8 = undefined; | |
| 749 | var req = try client.request(.GET, uri, .{}); | |
| 747 | 750 | defer req.deinit(); |
| 748 | 751 | |
| 749 | try req.send(); | |
| 750 | try req.wait(); | |
| 752 | try req.sendBodiless(); | |
| 753 | var response = try req.receiveHead(&redirect_buffer); | |
| 751 | 754 | |
| 752 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 755 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 753 | 756 | defer gpa.free(body); |
| 754 | 757 | |
| 755 | 758 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -764,16 +767,14 @@ test "general client/server API coverage" { |
| 764 | 767 | const uri = try std.Uri.parse(location); |
| 765 | 768 | |
| 766 | 769 | log.info("{s}", .{location}); |
| 767 | var server_header_buffer: [1024]u8 = undefined; | |
| 768 | var req = try client.open(.GET, uri, .{ | |
| 769 | .server_header_buffer = &server_header_buffer, | |
| 770 | }); | |
| 770 | var redirect_buffer: [1024]u8 = undefined; | |
| 771 | var req = try client.request(.GET, uri, .{}); | |
| 771 | 772 | defer req.deinit(); |
| 772 | 773 | |
| 773 | try req.send(); | |
| 774 | try req.wait(); | |
| 774 | try req.sendBodiless(); | |
| 775 | var response = try req.receiveHead(&redirect_buffer); | |
| 775 | 776 | |
| 776 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 777 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 777 | 778 | defer gpa.free(body); |
| 778 | 779 | |
| 779 | 780 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -788,16 +789,14 @@ test "general client/server API coverage" { |
| 788 | 789 | const uri = try std.Uri.parse(location); |
| 789 | 790 | |
| 790 | 791 | log.info("{s}", .{location}); |
| 791 | var server_header_buffer: [1024]u8 = undefined; | |
| 792 | var req = try client.open(.GET, uri, .{ | |
| 793 | .server_header_buffer = &server_header_buffer, | |
| 794 | }); | |
| 792 | var redirect_buffer: [1024]u8 = undefined; | |
| 793 | var req = try client.request(.GET, uri, .{}); | |
| 795 | 794 | defer req.deinit(); |
| 796 | 795 | |
| 797 | try req.send(); | |
| 798 | try req.wait(); | |
| 796 | try req.sendBodiless(); | |
| 797 | var response = try req.receiveHead(&redirect_buffer); | |
| 799 | 798 | |
| 800 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 799 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 801 | 800 | defer gpa.free(body); |
| 802 | 801 | |
| 803 | 802 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -812,17 +811,17 @@ test "general client/server API coverage" { |
| 812 | 811 | const uri = try std.Uri.parse(location); |
| 813 | 812 | |
| 814 | 813 | log.info("{s}", .{location}); |
| 815 | var server_header_buffer: [1024]u8 = undefined; | |
| 816 | var req = try client.open(.GET, uri, .{ | |
| 817 | .server_header_buffer = &server_header_buffer, | |
| 818 | }); | |
| 814 | var redirect_buffer: [1024]u8 = undefined; | |
| 815 | var req = try client.request(.GET, uri, .{}); | |
| 819 | 816 | defer req.deinit(); |
| 820 | 817 | |
| 821 | try req.send(); | |
| 822 | req.wait() catch |err| switch (err) { | |
| 818 | try req.sendBodiless(); | |
| 819 | if (req.receiveHead(&redirect_buffer)) |_| { | |
| 820 | return error.TestFailed; | |
| 821 | } else |err| switch (err) { | |
| 823 | 822 | error.TooManyHttpRedirects => {}, |
| 824 | 823 | else => return err, |
| 825 | }; | |
| 824 | } | |
| 826 | 825 | } |
| 827 | 826 | |
| 828 | 827 | { // redirect to encoded url |
| ... | ... | @@ -831,16 +830,14 @@ test "general client/server API coverage" { |
| 831 | 830 | const uri = try std.Uri.parse(location); |
| 832 | 831 | |
| 833 | 832 | log.info("{s}", .{location}); |
| 834 | var server_header_buffer: [1024]u8 = undefined; | |
| 835 | var req = try client.open(.GET, uri, .{ | |
| 836 | .server_header_buffer = &server_header_buffer, | |
| 837 | }); | |
| 833 | var redirect_buffer: [1024]u8 = undefined; | |
| 834 | var req = try client.request(.GET, uri, .{}); | |
| 838 | 835 | defer req.deinit(); |
| 839 | 836 | |
| 840 | try req.send(); | |
| 841 | try req.wait(); | |
| 837 | try req.sendBodiless(); | |
| 838 | var response = try req.receiveHead(&redirect_buffer); | |
| 842 | 839 | |
| 843 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 840 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 844 | 841 | defer gpa.free(body); |
| 845 | 842 | |
| 846 | 843 | try expectEqualStrings("Encoded redirect successful!\n", body); |
| ... | ... | @@ -855,14 +852,12 @@ test "general client/server API coverage" { |
| 855 | 852 | const uri = try std.Uri.parse(location); |
| 856 | 853 | |
| 857 | 854 | log.info("{s}", .{location}); |
| 858 | var server_header_buffer: [1024]u8 = undefined; | |
| 859 | var req = try client.open(.GET, uri, .{ | |
| 860 | .server_header_buffer = &server_header_buffer, | |
| 861 | }); | |
| 855 | var redirect_buffer: [1024]u8 = undefined; | |
| 856 | var req = try client.request(.GET, uri, .{}); | |
| 862 | 857 | defer req.deinit(); |
| 863 | 858 | |
| 864 | try req.send(); | |
| 865 | const result = req.wait(); | |
| 859 | try req.sendBodiless(); | |
| 860 | const result = req.receiveHead(&redirect_buffer); | |
| 866 | 861 | |
| 867 | 862 | // a proxy without an upstream is likely to return a 5xx status. |
| 868 | 863 | if (client.http_proxy == null) { |
| ... | ... | @@ -872,77 +867,40 @@ test "general client/server API coverage" { |
| 872 | 867 | |
| 873 | 868 | // connection has been kept alive |
| 874 | 869 | try expect(client.http_proxy != null or client.connection_pool.free_len == 1); |
| 875 | ||
| 876 | { // issue 16282 *** This test leaves the client in an invalid state, it must be last *** | |
| 877 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/get", .{port}); | |
| 878 | defer gpa.free(location); | |
| 879 | const uri = try std.Uri.parse(location); | |
| 880 | ||
| 881 | const total_connections = client.connection_pool.free_size + 64; | |
| 882 | var requests = try gpa.alloc(http.Client.Request, total_connections); | |
| 883 | defer gpa.free(requests); | |
| 884 | ||
| 885 | var header_bufs = std.ArrayList([]u8).init(gpa); | |
| 886 | defer header_bufs.deinit(); | |
| 887 | defer for (header_bufs.items) |item| gpa.free(item); | |
| 888 | ||
| 889 | for (0..total_connections) |i| { | |
| 890 | const headers_buf = try gpa.alloc(u8, 1024); | |
| 891 | try header_bufs.append(headers_buf); | |
| 892 | var req = try client.open(.GET, uri, .{ | |
| 893 | .server_header_buffer = headers_buf, | |
| 894 | }); | |
| 895 | req.response.parser.done = true; | |
| 896 | req.connection.?.closing = false; | |
| 897 | requests[i] = req; | |
| 898 | } | |
| 899 | ||
| 900 | for (0..total_connections) |i| { | |
| 901 | requests[i].deinit(); | |
| 902 | } | |
| 903 | ||
| 904 | // free connections should be full now | |
| 905 | try expect(client.connection_pool.free_len == client.connection_pool.free_size); | |
| 906 | } | |
| 907 | ||
| 908 | client.deinit(); | |
| 909 | ||
| 910 | { | |
| 911 | global.handle_new_requests = false; | |
| 912 | ||
| 913 | const conn = try std.net.tcpConnectToAddress(test_server.net_server.listen_address); | |
| 914 | conn.close(); | |
| 915 | } | |
| 916 | 870 | } |
| 917 | 871 | |
| 918 | 872 | test "Server streams both reading and writing" { |
| 919 | 873 | const test_server = try createTestServer(struct { |
| 920 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 921 | var header_buffer: [1024]u8 = undefined; | |
| 922 | const conn = try net_server.accept(); | |
| 923 | defer conn.stream.close(); | |
| 874 | fn run(test_server: *TestServer) anyerror!void { | |
| 875 | const net_server = &test_server.net_server; | |
| 876 | var recv_buffer: [1024]u8 = undefined; | |
| 877 | var send_buffer: [777]u8 = undefined; | |
| 924 | 878 | |
| 925 | var server = http.Server.init(conn, &header_buffer); | |
| 926 | var request = try server.receiveHead(); | |
| 927 | const reader = try request.reader(); | |
| 879 | const connection = try net_server.accept(); | |
| 880 | defer connection.stream.close(); | |
| 928 | 881 | |
| 929 | var send_buffer: [777]u8 = undefined; | |
| 930 | var response = request.respondStreaming(.{ | |
| 931 | .send_buffer = &send_buffer, | |
| 882 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 883 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 884 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 885 | var request = try server.receiveHead(); | |
| 886 | var read_buffer: [100]u8 = undefined; | |
| 887 | var br = try request.readerExpectContinue(&read_buffer); | |
| 888 | var response = try request.respondStreaming(&.{}, .{ | |
| 932 | 889 | .respond_options = .{ |
| 933 | 890 | .transfer_encoding = .none, // Causes keep_alive=false |
| 934 | 891 | }, |
| 935 | 892 | }); |
| 936 | const writer = response.writer(); | |
| 893 | const w = &response.writer; | |
| 937 | 894 | |
| 938 | 895 | while (true) { |
| 939 | 896 | try response.flush(); |
| 940 | var buf: [100]u8 = undefined; | |
| 941 | const n = try reader.read(&buf); | |
| 942 | if (n == 0) break; | |
| 943 | const sub_buf = buf[0..n]; | |
| 944 | for (sub_buf) |*b| b.* = std.ascii.toUpper(b.*); | |
| 945 | try writer.writeAll(sub_buf); | |
| 897 | const buf = br.peekGreedy(1) catch |err| switch (err) { | |
| 898 | error.EndOfStream => break, | |
| 899 | error.ReadFailed => return error.ReadFailed, | |
| 900 | }; | |
| 901 | br.toss(buf.len); | |
| 902 | for (buf) |*b| b.* = std.ascii.toUpper(b.*); | |
| 903 | try w.writeAll(buf); | |
| 946 | 904 | } |
| 947 | 905 | try response.end(); |
| 948 | 906 | } |
| ... | ... | @@ -952,27 +910,24 @@ test "Server streams both reading and writing" { |
| 952 | 910 | var client: http.Client = .{ .allocator = std.testing.allocator }; |
| 953 | 911 | defer client.deinit(); |
| 954 | 912 | |
| 955 | var server_header_buffer: [555]u8 = undefined; | |
| 956 | var req = try client.open(.POST, .{ | |
| 913 | var redirect_buffer: [555]u8 = undefined; | |
| 914 | var req = try client.request(.POST, .{ | |
| 957 | 915 | .scheme = "http", |
| 958 | 916 | .host = .{ .raw = "127.0.0.1" }, |
| 959 | 917 | .port = test_server.port(), |
| 960 | 918 | .path = .{ .percent_encoded = "/" }, |
| 961 | }, .{ | |
| 962 | .server_header_buffer = &server_header_buffer, | |
| 963 | }); | |
| 919 | }, .{}); | |
| 964 | 920 | defer req.deinit(); |
| 965 | 921 | |
| 966 | 922 | req.transfer_encoding = .chunked; |
| 967 | try req.send(); | |
| 968 | try req.wait(); | |
| 969 | ||
| 970 | try req.writeAll("one "); | |
| 971 | try req.writeAll("fish"); | |
| 923 | var body_writer = try req.sendBody(&.{}); | |
| 924 | var response = try req.receiveHead(&redirect_buffer); | |
| 972 | 925 | |
| 973 | try req.finish(); | |
| 926 | try body_writer.writer.writeAll("one "); | |
| 927 | try body_writer.writer.writeAll("fish"); | |
| 928 | try body_writer.end(); | |
| 974 | 929 | |
| 975 | const body = try req.reader().readAllAlloc(std.testing.allocator, 8192); | |
| 930 | const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .unlimited); | |
| 976 | 931 | defer std.testing.allocator.free(body); |
| 977 | 932 | |
| 978 | 933 | try expectEqualStrings("ONE FISH", body); |
| ... | ... | @@ -987,9 +942,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 987 | 942 | defer gpa.free(location); |
| 988 | 943 | const uri = try std.Uri.parse(location); |
| 989 | 944 | |
| 990 | var server_header_buffer: [1024]u8 = undefined; | |
| 991 | var req = try client.open(.POST, uri, .{ | |
| 992 | .server_header_buffer = &server_header_buffer, | |
| 945 | var redirect_buffer: [1024]u8 = undefined; | |
| 946 | var req = try client.request(.POST, uri, .{ | |
| 993 | 947 | .extra_headers = &.{ |
| 994 | 948 | .{ .name = "content-type", .value = "text/plain" }, |
| 995 | 949 | }, |
| ... | ... | @@ -998,14 +952,14 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 998 | 952 | |
| 999 | 953 | req.transfer_encoding = .{ .content_length = 14 }; |
| 1000 | 954 | |
| 1001 | try req.send(); | |
| 1002 | try req.writeAll("Hello, "); | |
| 1003 | try req.writeAll("World!\n"); | |
| 1004 | try req.finish(); | |
| 955 | var body_writer = try req.sendBody(&.{}); | |
| 956 | try body_writer.writer.writeAll("Hello, "); | |
| 957 | try body_writer.writer.writeAll("World!\n"); | |
| 958 | try body_writer.end(); | |
| 1005 | 959 | |
| 1006 | try req.wait(); | |
| 960 | var response = try req.receiveHead(&redirect_buffer); | |
| 1007 | 961 | |
| 1008 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 962 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 1009 | 963 | defer gpa.free(body); |
| 1010 | 964 | |
| 1011 | 965 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1021,9 +975,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1021 | 975 | .{port}, |
| 1022 | 976 | )); |
| 1023 | 977 | |
| 1024 | var server_header_buffer: [1024]u8 = undefined; | |
| 1025 | var req = try client.open(.POST, uri, .{ | |
| 1026 | .server_header_buffer = &server_header_buffer, | |
| 978 | var redirect_buffer: [1024]u8 = undefined; | |
| 979 | var req = try client.request(.POST, uri, .{ | |
| 1027 | 980 | .extra_headers = &.{ |
| 1028 | 981 | .{ .name = "content-type", .value = "text/plain" }, |
| 1029 | 982 | }, |
| ... | ... | @@ -1032,14 +985,14 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1032 | 985 | |
| 1033 | 986 | req.transfer_encoding = .chunked; |
| 1034 | 987 | |
| 1035 | try req.send(); | |
| 1036 | try req.writeAll("Hello, "); | |
| 1037 | try req.writeAll("World!\n"); | |
| 1038 | try req.finish(); | |
| 988 | var body_writer = try req.sendBody(&.{}); | |
| 989 | try body_writer.writer.writeAll("Hello, "); | |
| 990 | try body_writer.writer.writeAll("World!\n"); | |
| 991 | try body_writer.end(); | |
| 1039 | 992 | |
| 1040 | try req.wait(); | |
| 993 | var response = try req.receiveHead(&redirect_buffer); | |
| 1041 | 994 | |
| 1042 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 995 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 1043 | 996 | defer gpa.free(body); |
| 1044 | 997 | |
| 1045 | 998 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1053,8 +1006,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1053 | 1006 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port}); |
| 1054 | 1007 | defer gpa.free(location); |
| 1055 | 1008 | |
| 1056 | var body = std.ArrayList(u8).init(gpa); | |
| 1057 | defer body.deinit(); | |
| 1009 | var body: std.ArrayListUnmanaged(u8) = .empty; | |
| 1010 | defer body.deinit(gpa); | |
| 1058 | 1011 | |
| 1059 | 1012 | const res = try client.fetch(.{ |
| 1060 | 1013 | .location = .{ .url = location }, |
| ... | ... | @@ -1063,7 +1016,7 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1063 | 1016 | .extra_headers = &.{ |
| 1064 | 1017 | .{ .name = "content-type", .value = "text/plain" }, |
| 1065 | 1018 | }, |
| 1066 | .response_storage = .{ .dynamic = &body }, | |
| 1019 | .response_storage = .{ .allocator = gpa, .list = &body }, | |
| 1067 | 1020 | }); |
| 1068 | 1021 | try expectEqual(.ok, res.status); |
| 1069 | 1022 | try expectEqualStrings("Hello, World!\n", body.items); |
| ... | ... | @@ -1074,9 +1027,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1074 | 1027 | defer gpa.free(location); |
| 1075 | 1028 | const uri = try std.Uri.parse(location); |
| 1076 | 1029 | |
| 1077 | var server_header_buffer: [1024]u8 = undefined; | |
| 1078 | var req = try client.open(.POST, uri, .{ | |
| 1079 | .server_header_buffer = &server_header_buffer, | |
| 1030 | var redirect_buffer: [1024]u8 = undefined; | |
| 1031 | var req = try client.request(.POST, uri, .{ | |
| 1080 | 1032 | .extra_headers = &.{ |
| 1081 | 1033 | .{ .name = "expect", .value = "100-continue" }, |
| 1082 | 1034 | .{ .name = "content-type", .value = "text/plain" }, |
| ... | ... | @@ -1086,15 +1038,15 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1086 | 1038 | |
| 1087 | 1039 | req.transfer_encoding = .chunked; |
| 1088 | 1040 | |
| 1089 | try req.send(); | |
| 1090 | try req.writeAll("Hello, "); | |
| 1091 | try req.writeAll("World!\n"); | |
| 1092 | try req.finish(); | |
| 1041 | var body_writer = try req.sendBody(&.{}); | |
| 1042 | try body_writer.writer.writeAll("Hello, "); | |
| 1043 | try body_writer.writer.writeAll("World!\n"); | |
| 1044 | try body_writer.end(); | |
| 1093 | 1045 | |
| 1094 | try req.wait(); | |
| 1095 | try expectEqual(.ok, req.response.status); | |
| 1046 | var response = try req.receiveHead(&redirect_buffer); | |
| 1047 | try expectEqual(.ok, response.head.status); | |
| 1096 | 1048 | |
| 1097 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 1049 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | |
| 1098 | 1050 | defer gpa.free(body); |
| 1099 | 1051 | |
| 1100 | 1052 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1105,9 +1057,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1105 | 1057 | defer gpa.free(location); |
| 1106 | 1058 | const uri = try std.Uri.parse(location); |
| 1107 | 1059 | |
| 1108 | var server_header_buffer: [1024]u8 = undefined; | |
| 1109 | var req = try client.open(.POST, uri, .{ | |
| 1110 | .server_header_buffer = &server_header_buffer, | |
| 1060 | var redirect_buffer: [1024]u8 = undefined; | |
| 1061 | var req = try client.request(.POST, uri, .{ | |
| 1111 | 1062 | .extra_headers = &.{ |
| 1112 | 1063 | .{ .name = "content-type", .value = "text/plain" }, |
| 1113 | 1064 | .{ .name = "expect", .value = "garbage" }, |
| ... | ... | @@ -1117,23 +1068,24 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1117 | 1068 | |
| 1118 | 1069 | req.transfer_encoding = .chunked; |
| 1119 | 1070 | |
| 1120 | try req.send(); | |
| 1121 | try req.wait(); | |
| 1122 | try expectEqual(.expectation_failed, req.response.status); | |
| 1071 | var body_writer = try req.sendBody(&.{}); | |
| 1072 | try body_writer.flush(); | |
| 1073 | var response = try req.receiveHead(&redirect_buffer); | |
| 1074 | try expectEqual(.expectation_failed, response.head.status); | |
| 1075 | _ = try response.reader(&.{}).discardRemaining(); | |
| 1123 | 1076 | } |
| 1124 | ||
| 1125 | _ = try client.fetch(.{ | |
| 1126 | .location = .{ | |
| 1127 | .url = try std.fmt.bufPrint(&location_buffer, "http://127.0.0.1:{d}/end", .{port}), | |
| 1128 | }, | |
| 1129 | }); | |
| 1130 | 1077 | } |
| 1131 | 1078 | |
| 1132 | 1079 | const TestServer = struct { |
| 1080 | shutting_down: bool, | |
| 1133 | 1081 | server_thread: std.Thread, |
| 1134 | 1082 | net_server: std.net.Server, |
| 1135 | 1083 | |
| 1136 | 1084 | fn destroy(self: *@This()) void { |
| 1085 | self.shutting_down = true; | |
| 1086 | const conn = std.net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); | |
| 1087 | conn.close(); | |
| 1088 | ||
| 1137 | 1089 | self.server_thread.join(); |
| 1138 | 1090 | self.net_server.deinit(); |
| 1139 | 1091 | std.testing.allocator.destroy(self); |
| ... | ... | @@ -1153,20 +1105,27 @@ fn createTestServer(S: type) !*TestServer { |
| 1153 | 1105 | |
| 1154 | 1106 | const address = try std.net.Address.parseIp("127.0.0.1", 0); |
| 1155 | 1107 | const test_server = try std.testing.allocator.create(TestServer); |
| 1156 | test_server.net_server = try address.listen(.{ .reuse_address = true }); | |
| 1157 | test_server.server_thread = try std.Thread.spawn(.{}, S.run, .{&test_server.net_server}); | |
| 1108 | test_server.* = .{ | |
| 1109 | .net_server = try address.listen(.{ .reuse_address = true }), | |
| 1110 | .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}), | |
| 1111 | .shutting_down = false, | |
| 1112 | }; | |
| 1158 | 1113 | return test_server; |
| 1159 | 1114 | } |
| 1160 | 1115 | |
| 1161 | 1116 | test "redirect to different connection" { |
| 1162 | 1117 | const test_server_new = try createTestServer(struct { |
| 1163 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 1164 | var header_buffer: [888]u8 = undefined; | |
| 1118 | fn run(test_server: *TestServer) anyerror!void { | |
| 1119 | const net_server = &test_server.net_server; | |
| 1120 | var recv_buffer: [888]u8 = undefined; | |
| 1121 | var send_buffer: [777]u8 = undefined; | |
| 1165 | 1122 | |
| 1166 | const conn = try net_server.accept(); | |
| 1167 | defer conn.stream.close(); | |
| 1123 | const connection = try net_server.accept(); | |
| 1124 | defer connection.stream.close(); | |
| 1168 | 1125 | |
| 1169 | var server = http.Server.init(conn, &header_buffer); | |
| 1126 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 1127 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 1128 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 1170 | 1129 | var request = try server.receiveHead(); |
| 1171 | 1130 | try expectEqualStrings(request.head.target, "/ok"); |
| 1172 | 1131 | try request.respond("good job, you pass", .{}); |
| ... | ... | @@ -1180,18 +1139,22 @@ test "redirect to different connection" { |
| 1180 | 1139 | global.other_port = test_server_new.port(); |
| 1181 | 1140 | |
| 1182 | 1141 | const test_server_orig = try createTestServer(struct { |
| 1183 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 1184 | var header_buffer: [999]u8 = undefined; | |
| 1142 | fn run(test_server: *TestServer) anyerror!void { | |
| 1143 | const net_server = &test_server.net_server; | |
| 1144 | var recv_buffer: [999]u8 = undefined; | |
| 1185 | 1145 | var send_buffer: [100]u8 = undefined; |
| 1186 | 1146 | |
| 1187 | const conn = try net_server.accept(); | |
| 1188 | defer conn.stream.close(); | |
| 1147 | const connection = try net_server.accept(); | |
| 1148 | defer connection.stream.close(); | |
| 1189 | 1149 | |
| 1190 | const new_loc = try std.fmt.bufPrint(&send_buffer, "http://127.0.0.1:{d}/ok", .{ | |
| 1150 | var loc_buf: [50]u8 = undefined; | |
| 1151 | const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{ | |
| 1191 | 1152 | global.other_port.?, |
| 1192 | 1153 | }); |
| 1193 | 1154 | |
| 1194 | var server = http.Server.init(conn, &header_buffer); | |
| 1155 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 1156 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 1157 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 1195 | 1158 | var request = try server.receiveHead(); |
| 1196 | 1159 | try expectEqualStrings(request.head.target, "/help"); |
| 1197 | 1160 | try request.respond("", .{ |
| ... | ... | @@ -1216,16 +1179,15 @@ test "redirect to different connection" { |
| 1216 | 1179 | const uri = try std.Uri.parse(location); |
| 1217 | 1180 | |
| 1218 | 1181 | { |
| 1219 | var server_header_buffer: [666]u8 = undefined; | |
| 1220 | var req = try client.open(.GET, uri, .{ | |
| 1221 | .server_header_buffer = &server_header_buffer, | |
| 1222 | }); | |
| 1182 | var redirect_buffer: [666]u8 = undefined; | |
| 1183 | var req = try client.request(.GET, uri, .{}); | |
| 1223 | 1184 | defer req.deinit(); |
| 1224 | 1185 | |
| 1225 | try req.send(); | |
| 1226 | try req.wait(); | |
| 1186 | try req.sendBodiless(); | |
| 1187 | var response = try req.receiveHead(&redirect_buffer); | |
| 1188 | var reader = response.reader(&.{}); | |
| 1227 | 1189 | |
| 1228 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 1190 | const body = try reader.allocRemaining(gpa, .unlimited); | |
| 1229 | 1191 | defer gpa.free(body); |
| 1230 | 1192 | |
| 1231 | 1193 | try expectEqualStrings("good job, you pass", body); |
lib/std/net.zig+1-1| ... | ... | @@ -1944,7 +1944,7 @@ pub const Stream = struct { |
| 1944 | 1944 | pub const Error = ReadError; |
| 1945 | 1945 | |
| 1946 | 1946 | pub fn getStream(r: *const Reader) Stream { |
| 1947 | return r.stream; | |
| 1947 | return r.net_stream; | |
| 1948 | 1948 | } |
| 1949 | 1949 | |
| 1950 | 1950 | pub fn getError(r: *const Reader) ?Error { |
lib/std/std.zig-1| ... | ... | @@ -57,7 +57,6 @@ pub const debug = @import("debug.zig"); |
| 57 | 57 | pub const dwarf = @import("dwarf.zig"); |
| 58 | 58 | pub const elf = @import("elf.zig"); |
| 59 | 59 | pub const enums = @import("enums.zig"); |
| 60 | pub const fifo = @import("fifo.zig"); | |
| 61 | 60 | pub const fmt = @import("fmt.zig"); |
| 62 | 61 | pub const fs = @import("fs.zig"); |
| 63 | 62 | pub const gpu = @import("gpu.zig"); |
src/Package/Fetch.zig+148-193| ... | ... | @@ -385,21 +385,23 @@ pub fn run(f: *Fetch) RunError!void { |
| 385 | 385 | var resource: Resource = .{ .dir = dir }; |
| 386 | 386 | return f.runResource(path_or_url, &resource, null); |
| 387 | 387 | } else |dir_err| { |
| 388 | var server_header_buffer: [init_resource_buffer_size]u8 = undefined; | |
| 389 | ||
| 388 | 390 | const file_err = if (dir_err == error.NotDir) e: { |
| 389 | 391 | if (fs.cwd().openFile(path_or_url, .{})) |file| { |
| 390 | var resource: Resource = .{ .file = file }; | |
| 392 | var resource: Resource = .{ .file = file.reader(&server_header_buffer) }; | |
| 391 | 393 | return f.runResource(path_or_url, &resource, null); |
| 392 | 394 | } else |err| break :e err; |
| 393 | 395 | } else dir_err; |
| 394 | 396 | |
| 395 | 397 | const uri = std.Uri.parse(path_or_url) catch |uri_err| { |
| 396 | 398 | return f.fail(0, try eb.printString( |
| 397 | "'{s}' could not be recognized as a file path ({s}) or an URL ({s})", | |
| 398 | .{ path_or_url, @errorName(file_err), @errorName(uri_err) }, | |
| 399 | "'{s}' could not be recognized as a file path ({t}) or an URL ({t})", | |
| 400 | .{ path_or_url, file_err, uri_err }, | |
| 399 | 401 | )); |
| 400 | 402 | }; |
| 401 | var server_header_buffer: [header_buffer_size]u8 = undefined; | |
| 402 | var resource = try f.initResource(uri, &server_header_buffer); | |
| 403 | var resource: Resource = undefined; | |
| 404 | try f.initResource(uri, &resource, &server_header_buffer); | |
| 403 | 405 | return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null); |
| 404 | 406 | } |
| 405 | 407 | }, |
| ... | ... | @@ -464,8 +466,9 @@ pub fn run(f: *Fetch) RunError!void { |
| 464 | 466 | f.location_tok, |
| 465 | 467 | try eb.printString("invalid URI: {s}", .{@errorName(err)}), |
| 466 | 468 | ); |
| 467 | var server_header_buffer: [header_buffer_size]u8 = undefined; | |
| 468 | var resource = try f.initResource(uri, &server_header_buffer); | |
| 469 | var buffer: [init_resource_buffer_size]u8 = undefined; | |
| 470 | var resource: Resource = undefined; | |
| 471 | try f.initResource(uri, &resource, &buffer); | |
| 469 | 472 | return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash); |
| 470 | 473 | } |
| 471 | 474 | |
| ... | ... | @@ -866,8 +869,8 @@ fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError { |
| 866 | 869 | } |
| 867 | 870 | |
| 868 | 871 | const Resource = union(enum) { |
| 869 | file: fs.File, | |
| 870 | http_request: std.http.Client.Request, | |
| 872 | file: fs.File.Reader, | |
| 873 | http_request: HttpRequest, | |
| 871 | 874 | git: Git, |
| 872 | 875 | dir: fs.Dir, |
| 873 | 876 | |
| ... | ... | @@ -877,10 +880,16 @@ const Resource = union(enum) { |
| 877 | 880 | want_oid: git.Oid, |
| 878 | 881 | }; |
| 879 | 882 | |
| 883 | const HttpRequest = struct { | |
| 884 | request: std.http.Client.Request, | |
| 885 | response: std.http.Client.Response, | |
| 886 | buffer: []u8, | |
| 887 | }; | |
| 888 | ||
| 880 | 889 | fn deinit(resource: *Resource) void { |
| 881 | 890 | switch (resource.*) { |
| 882 | .file => |*file| file.close(), | |
| 883 | .http_request => |*req| req.deinit(), | |
| 891 | .file => |*file_reader| file_reader.file.close(), | |
| 892 | .http_request => |*http_request| http_request.request.deinit(), | |
| 884 | 893 | .git => |*git_resource| { |
| 885 | 894 | git_resource.fetch_stream.deinit(); |
| 886 | 895 | git_resource.session.deinit(); |
| ... | ... | @@ -890,21 +899,13 @@ const Resource = union(enum) { |
| 890 | 899 | resource.* = undefined; |
| 891 | 900 | } |
| 892 | 901 | |
| 893 | fn reader(resource: *Resource) std.io.AnyReader { | |
| 894 | return .{ | |
| 895 | .context = resource, | |
| 896 | .readFn = read, | |
| 897 | }; | |
| 898 | } | |
| 899 | ||
| 900 | fn read(context: *const anyopaque, buffer: []u8) anyerror!usize { | |
| 901 | const resource: *Resource = @ptrCast(@alignCast(@constCast(context))); | |
| 902 | switch (resource.*) { | |
| 903 | .file => |*f| return f.read(buffer), | |
| 904 | .http_request => |*r| return r.read(buffer), | |
| 905 | .git => |*g| return g.fetch_stream.read(buffer), | |
| 902 | fn reader(resource: *Resource) *std.Io.Reader { | |
| 903 | return switch (resource.*) { | |
| 904 | .file => |*file_reader| return &file_reader.interface, | |
| 905 | .http_request => |*http_request| return http_request.response.reader(http_request.buffer), | |
| 906 | .git => |*g| return &g.fetch_stream.reader, | |
| 906 | 907 | .dir => unreachable, |
| 907 | } | |
| 908 | }; | |
| 908 | 909 | } |
| 909 | 910 | }; |
| 910 | 911 | |
| ... | ... | @@ -967,20 +968,22 @@ const FileType = enum { |
| 967 | 968 | } |
| 968 | 969 | }; |
| 969 | 970 | |
| 970 | const header_buffer_size = 16 * 1024; | |
| 971 | const init_resource_buffer_size = git.Packet.max_data_length; | |
| 971 | 972 | |
| 972 | fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource { | |
| 973 | fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) RunError!void { | |
| 973 | 974 | const gpa = f.arena.child_allocator; |
| 974 | 975 | const arena = f.arena.allocator(); |
| 975 | 976 | const eb = &f.error_bundle; |
| 976 | 977 | |
| 977 | 978 | if (ascii.eqlIgnoreCase(uri.scheme, "file")) { |
| 978 | 979 | const path = try uri.path.toRawMaybeAlloc(arena); |
| 979 | return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| { | |
| 980 | return f.fail(f.location_tok, try eb.printString("unable to open '{f}{s}': {s}", .{ | |
| 981 | f.parent_package_root, path, @errorName(err), | |
| 980 | const file = f.parent_package_root.openFile(path, .{}) catch |err| { | |
| 981 | return f.fail(f.location_tok, try eb.printString("unable to open '{f}{s}': {t}", .{ | |
| 982 | f.parent_package_root, path, err, | |
| 982 | 983 | })); |
| 983 | } }; | |
| 984 | }; | |
| 985 | resource.* = .{ .file = file.reader(reader_buffer) }; | |
| 986 | return; | |
| 984 | 987 | } |
| 985 | 988 | |
| 986 | 989 | const http_client = f.job_queue.http_client; |
| ... | ... | @@ -988,37 +991,35 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re |
| 988 | 991 | if (ascii.eqlIgnoreCase(uri.scheme, "http") or |
| 989 | 992 | ascii.eqlIgnoreCase(uri.scheme, "https")) |
| 990 | 993 | { |
| 991 | var req = http_client.open(.GET, uri, .{ | |
| 992 | .server_header_buffer = server_header_buffer, | |
| 993 | }) catch |err| { | |
| 994 | return f.fail(f.location_tok, try eb.printString( | |
| 995 | "unable to connect to server: {s}", | |
| 996 | .{@errorName(err)}, | |
| 997 | )); | |
| 998 | }; | |
| 999 | errdefer req.deinit(); // releases more than memory | |
| 1000 | ||
| 1001 | req.send() catch |err| { | |
| 1002 | return f.fail(f.location_tok, try eb.printString( | |
| 1003 | "HTTP request failed: {s}", | |
| 1004 | .{@errorName(err)}, | |
| 1005 | )); | |
| 1006 | }; | |
| 1007 | req.wait() catch |err| { | |
| 1008 | return f.fail(f.location_tok, try eb.printString( | |
| 1009 | "invalid HTTP response: {s}", | |
| 1010 | .{@errorName(err)}, | |
| 1011 | )); | |
| 994 | resource.* = .{ .http_request = .{ | |
| 995 | .request = http_client.request(.GET, uri, .{}) catch |err| | |
| 996 | return f.fail(f.location_tok, try eb.printString("unable to connect to server: {t}", .{err})), | |
| 997 | .response = undefined, | |
| 998 | .buffer = reader_buffer, | |
| 999 | } }; | |
| 1000 | const request = &resource.http_request.request; | |
| 1001 | errdefer request.deinit(); | |
| 1002 | ||
| 1003 | request.sendBodiless() catch |err| | |
| 1004 | return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err})); | |
| 1005 | ||
| 1006 | var redirect_buffer: [1024]u8 = undefined; | |
| 1007 | const response = &resource.http_request.response; | |
| 1008 | response.* = request.receiveHead(&redirect_buffer) catch |err| switch (err) { | |
| 1009 | error.ReadFailed => { | |
| 1010 | return f.fail(f.location_tok, try eb.printString("HTTP response read failure: {t}", .{ | |
| 1011 | request.connection.?.getReadError().?, | |
| 1012 | })); | |
| 1013 | }, | |
| 1014 | else => |e| return f.fail(f.location_tok, try eb.printString("invalid HTTP response: {t}", .{e})), | |
| 1012 | 1015 | }; |
| 1013 | 1016 | |
| 1014 | if (req.response.status != .ok) { | |
| 1015 | return f.fail(f.location_tok, try eb.printString( | |
| 1016 | "bad HTTP response code: '{d} {s}'", | |
| 1017 | .{ @intFromEnum(req.response.status), req.response.status.phrase() orelse "" }, | |
| 1018 | )); | |
| 1019 | } | |
| 1017 | if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString( | |
| 1018 | "bad HTTP response code: '{d} {s}'", | |
| 1019 | .{ response.head.status, response.head.status.phrase() orelse "" }, | |
| 1020 | )); | |
| 1020 | 1021 | |
| 1021 | return .{ .http_request = req }; | |
| 1022 | return; | |
| 1022 | 1023 | } |
| 1023 | 1024 | |
| 1024 | 1025 | if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or |
| ... | ... | @@ -1026,7 +1027,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re |
| 1026 | 1027 | { |
| 1027 | 1028 | var transport_uri = uri; |
| 1028 | 1029 | transport_uri.scheme = uri.scheme["git+".len..]; |
| 1029 | var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| { | |
| 1030 | var session = git.Session.init(gpa, http_client, transport_uri, reader_buffer) catch |err| { | |
| 1030 | 1031 | return f.fail(f.location_tok, try eb.printString( |
| 1031 | 1032 | "unable to discover remote git server capabilities: {s}", |
| 1032 | 1033 | .{@errorName(err)}, |
| ... | ... | @@ -1042,16 +1043,12 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re |
| 1042 | 1043 | const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref}); |
| 1043 | 1044 | const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref}); |
| 1044 | 1045 | |
| 1045 | var ref_iterator = session.listRefs(.{ | |
| 1046 | var ref_iterator: git.Session.RefIterator = undefined; | |
| 1047 | session.listRefs(&ref_iterator, .{ | |
| 1046 | 1048 | .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag }, |
| 1047 | 1049 | .include_peeled = true, |
| 1048 | .server_header_buffer = server_header_buffer, | |
| 1049 | }) catch |err| { | |
| 1050 | return f.fail(f.location_tok, try eb.printString( | |
| 1051 | "unable to list refs: {s}", | |
| 1052 | .{@errorName(err)}, | |
| 1053 | )); | |
| 1054 | }; | |
| 1050 | .buffer = reader_buffer, | |
| 1051 | }) catch |err| return f.fail(f.location_tok, try eb.printString("unable to list refs: {t}", .{err})); | |
| 1055 | 1052 | defer ref_iterator.deinit(); |
| 1056 | 1053 | while (ref_iterator.next() catch |err| { |
| 1057 | 1054 | return f.fail(f.location_tok, try eb.printString( |
| ... | ... | @@ -1089,25 +1086,21 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re |
| 1089 | 1086 | |
| 1090 | 1087 | var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined; |
| 1091 | 1088 | _ = std.fmt.bufPrint(&want_oid_buf, "{f}", .{want_oid}) catch unreachable; |
| 1092 | var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| { | |
| 1093 | return f.fail(f.location_tok, try eb.printString( | |
| 1094 | "unable to create fetch stream: {s}", | |
| 1095 | .{@errorName(err)}, | |
| 1096 | )); | |
| 1089 | var fetch_stream: git.Session.FetchStream = undefined; | |
| 1090 | session.fetch(&fetch_stream, &.{&want_oid_buf}, reader_buffer) catch |err| { | |
| 1091 | return f.fail(f.location_tok, try eb.printString("unable to create fetch stream: {t}", .{err})); | |
| 1097 | 1092 | }; |
| 1098 | 1093 | errdefer fetch_stream.deinit(); |
| 1099 | 1094 | |
| 1100 | return .{ .git = .{ | |
| 1095 | resource.* = .{ .git = .{ | |
| 1101 | 1096 | .session = session, |
| 1102 | 1097 | .fetch_stream = fetch_stream, |
| 1103 | 1098 | .want_oid = want_oid, |
| 1104 | 1099 | } }; |
| 1100 | return; | |
| 1105 | 1101 | } |
| 1106 | 1102 | |
| 1107 | return f.fail(f.location_tok, try eb.printString( | |
| 1108 | "unsupported URL scheme: {s}", | |
| 1109 | .{uri.scheme}, | |
| 1110 | )); | |
| 1103 | return f.fail(f.location_tok, try eb.printString("unsupported URL scheme: {s}", .{uri.scheme})); | |
| 1111 | 1104 | } |
| 1112 | 1105 | |
| 1113 | 1106 | fn unpackResource( |
| ... | ... | @@ -1121,9 +1114,11 @@ fn unpackResource( |
| 1121 | 1114 | .file => FileType.fromPath(uri_path) orelse |
| 1122 | 1115 | return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})), |
| 1123 | 1116 | |
| 1124 | .http_request => |req| ft: { | |
| 1117 | .http_request => |*http_request| ft: { | |
| 1118 | const head = &http_request.response.head; | |
| 1119 | ||
| 1125 | 1120 | // Content-Type takes first precedence. |
| 1126 | const content_type = req.response.content_type orelse | |
| 1121 | const content_type = head.content_type orelse | |
| 1127 | 1122 | return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header")); |
| 1128 | 1123 | |
| 1129 | 1124 | // Extract the MIME type, ignoring charset and boundary directives |
| ... | ... | @@ -1165,7 +1160,7 @@ fn unpackResource( |
| 1165 | 1160 | } |
| 1166 | 1161 | |
| 1167 | 1162 | // Next, the filename from 'content-disposition: attachment' takes precedence. |
| 1168 | if (req.response.content_disposition) |cd_header| { | |
| 1163 | if (head.content_disposition) |cd_header| { | |
| 1169 | 1164 | break :ft FileType.fromContentDisposition(cd_header) orelse { |
| 1170 | 1165 | return f.fail(f.location_tok, try eb.printString( |
| 1171 | 1166 | "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream", |
| ... | ... | @@ -1176,10 +1171,7 @@ fn unpackResource( |
| 1176 | 1171 | |
| 1177 | 1172 | // Finally, the path from the URI is used. |
| 1178 | 1173 | break :ft FileType.fromPath(uri_path) orelse { |
| 1179 | return f.fail(f.location_tok, try eb.printString( | |
| 1180 | "unknown file type: '{s}'", | |
| 1181 | .{uri_path}, | |
| 1182 | )); | |
| 1174 | return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})); | |
| 1183 | 1175 | }; |
| 1184 | 1176 | }, |
| 1185 | 1177 | |
| ... | ... | @@ -1187,10 +1179,9 @@ fn unpackResource( |
| 1187 | 1179 | |
| 1188 | 1180 | .dir => |dir| { |
| 1189 | 1181 | f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| { |
| 1190 | return f.fail(f.location_tok, try eb.printString( | |
| 1191 | "unable to copy directory '{s}': {s}", | |
| 1192 | .{ uri_path, @errorName(err) }, | |
| 1193 | )); | |
| 1182 | return f.fail(f.location_tok, try eb.printString("unable to copy directory '{s}': {t}", .{ | |
| 1183 | uri_path, err, | |
| 1184 | })); | |
| 1194 | 1185 | }; |
| 1195 | 1186 | return .{}; |
| 1196 | 1187 | }, |
| ... | ... | @@ -1198,27 +1189,17 @@ fn unpackResource( |
| 1198 | 1189 | |
| 1199 | 1190 | switch (file_type) { |
| 1200 | 1191 | .tar => { |
| 1201 | var adapter_buffer: [1024]u8 = undefined; | |
| 1202 | var adapter = resource.reader().adaptToNewApi(&adapter_buffer); | |
| 1203 | return unpackTarball(f, tmp_directory.handle, &adapter.new_interface); | |
| 1192 | return unpackTarball(f, tmp_directory.handle, resource.reader()); | |
| 1204 | 1193 | }, |
| 1205 | 1194 | .@"tar.gz" => { |
| 1206 | var adapter_buffer: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined; | |
| 1207 | var adapter = resource.reader().adaptToNewApi(&adapter_buffer); | |
| 1208 | 1195 | var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined; |
| 1209 | var decompress: std.compress.flate.Decompress = .init(&adapter.new_interface, .gzip, &flate_buffer); | |
| 1196 | var decompress: std.compress.flate.Decompress = .init(resource.reader(), .gzip, &flate_buffer); | |
| 1210 | 1197 | return try unpackTarball(f, tmp_directory.handle, &decompress.reader); |
| 1211 | 1198 | }, |
| 1212 | 1199 | .@"tar.xz" => { |
| 1213 | 1200 | const gpa = f.arena.child_allocator; |
| 1214 | const reader = resource.reader(); | |
| 1215 | var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader); | |
| 1216 | var dcp = std.compress.xz.decompress(gpa, br.reader()) catch |err| { | |
| 1217 | return f.fail(f.location_tok, try eb.printString( | |
| 1218 | "unable to decompress tarball: {s}", | |
| 1219 | .{@errorName(err)}, | |
| 1220 | )); | |
| 1221 | }; | |
| 1201 | var dcp = std.compress.xz.decompress(gpa, resource.reader().adaptToOldInterface()) catch |err| | |
| 1202 | return f.fail(f.location_tok, try eb.printString("unable to decompress tarball: {t}", .{err})); | |
| 1222 | 1203 | defer dcp.deinit(); |
| 1223 | 1204 | var adapter_buffer: [1024]u8 = undefined; |
| 1224 | 1205 | var adapter = dcp.reader().adaptToNewApi(&adapter_buffer); |
| ... | ... | @@ -1227,9 +1208,7 @@ fn unpackResource( |
| 1227 | 1208 | .@"tar.zst" => { |
| 1228 | 1209 | const window_size = std.compress.zstd.default_window_len; |
| 1229 | 1210 | const window_buffer = try f.arena.allocator().create([window_size]u8); |
| 1230 | var adapter_buffer: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined; | |
| 1231 | var adapter = resource.reader().adaptToNewApi(&adapter_buffer); | |
| 1232 | var decompress: std.compress.zstd.Decompress = .init(&adapter.new_interface, window_buffer, .{ | |
| 1211 | var decompress: std.compress.zstd.Decompress = .init(resource.reader(), window_buffer, .{ | |
| 1233 | 1212 | .verify_checksum = false, |
| 1234 | 1213 | }); |
| 1235 | 1214 | return try unpackTarball(f, tmp_directory.handle, &decompress.reader); |
| ... | ... | @@ -1237,12 +1216,15 @@ fn unpackResource( |
| 1237 | 1216 | .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) { |
| 1238 | 1217 | error.FetchFailed => return error.FetchFailed, |
| 1239 | 1218 | error.OutOfMemory => return error.OutOfMemory, |
| 1240 | else => |e| return f.fail(f.location_tok, try eb.printString( | |
| 1241 | "unable to unpack git files: {s}", | |
| 1242 | .{@errorName(e)}, | |
| 1219 | else => |e| return f.fail(f.location_tok, try eb.printString("unable to unpack git files: {t}", .{e})), | |
| 1220 | }, | |
| 1221 | .zip => return unzip(f, tmp_directory.handle, resource.reader()) catch |err| switch (err) { | |
| 1222 | error.ReadFailed => return f.fail(f.location_tok, try eb.printString( | |
| 1223 | "failed reading resource: {t}", | |
| 1224 | .{err}, | |
| 1243 | 1225 | )), |
| 1226 | else => |e| return e, | |
| 1244 | 1227 | }, |
| 1245 | .zip => return try unzip(f, tmp_directory.handle, resource.reader()), | |
| 1246 | 1228 | } |
| 1247 | 1229 | } |
| 1248 | 1230 | |
| ... | ... | @@ -1277,99 +1259,69 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!Un |
| 1277 | 1259 | return res; |
| 1278 | 1260 | } |
| 1279 | 1261 | |
| 1280 | fn unzip(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult { | |
| 1262 | fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult { | |
| 1281 | 1263 | // We write the entire contents to a file first because zip files |
| 1282 | 1264 | // must be processed back to front and they could be too large to |
| 1283 | 1265 | // load into memory. |
| 1284 | 1266 | |
| 1285 | 1267 | const cache_root = f.job_queue.global_cache; |
| 1286 | ||
| 1287 | // TODO: the downside of this solution is if we get a failure/crash/oom/power out | |
| 1288 | // during this process, we leave behind a zip file that would be | |
| 1289 | // difficult to know if/when it can be cleaned up. | |
| 1290 | // Might be worth it to use a mechanism that enables other processes | |
| 1291 | // to see if the owning process of a file is still alive (on linux this | |
| 1292 | // can be done with file locks). | |
| 1293 | // Coupled with this mechansism, we could also use slots (i.e. zig-cache/tmp/0, | |
| 1294 | // zig-cache/tmp/1, etc) which would mean that subsequent runs would | |
| 1295 | // automatically clean up old dead files. | |
| 1296 | // This could all be done with a simple TmpFile abstraction. | |
| 1297 | 1268 | const prefix = "tmp/"; |
| 1298 | 1269 | const suffix = ".zip"; |
| 1299 | ||
| 1300 | const random_bytes_count = 20; | |
| 1301 | const random_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count); | |
| 1302 | var zip_path: [prefix.len + random_path_len + suffix.len]u8 = undefined; | |
| 1303 | @memcpy(zip_path[0..prefix.len], prefix); | |
| 1304 | @memcpy(zip_path[prefix.len + random_path_len ..], suffix); | |
| 1305 | { | |
| 1306 | var random_bytes: [random_bytes_count]u8 = undefined; | |
| 1307 | std.crypto.random.bytes(&random_bytes); | |
| 1308 | _ = std.fs.base64_encoder.encode( | |
| 1309 | zip_path[prefix.len..][0..random_path_len], | |
| 1310 | &random_bytes, | |
| 1311 | ); | |
| 1312 | } | |
| 1313 | ||
| 1314 | defer cache_root.handle.deleteFile(&zip_path) catch {}; | |
| 1315 | ||
| 1316 | 1270 | const eb = &f.error_bundle; |
| 1317 | ||
| 1318 | { | |
| 1319 | var zip_file = cache_root.handle.createFile( | |
| 1320 | &zip_path, | |
| 1321 | .{}, | |
| 1322 | ) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1323 | "failed to create tmp zip file: {s}", | |
| 1324 | .{@errorName(err)}, | |
| 1325 | )); | |
| 1326 | defer zip_file.close(); | |
| 1327 | var buf: [4096]u8 = undefined; | |
| 1328 | while (true) { | |
| 1329 | const len = reader.readAll(&buf) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1330 | "read zip stream failed: {s}", | |
| 1331 | .{@errorName(err)}, | |
| 1332 | )); | |
| 1333 | if (len == 0) break; | |
| 1334 | zip_file.deprecatedWriter().writeAll(buf[0..len]) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1335 | "write temporary zip file failed: {s}", | |
| 1336 | .{@errorName(err)}, | |
| 1337 | )); | |
| 1338 | } | |
| 1339 | } | |
| 1271 | const random_len = @sizeOf(u64) * 2; | |
| 1272 | ||
| 1273 | var zip_path: [prefix.len + random_len + suffix.len]u8 = undefined; | |
| 1274 | zip_path[0..prefix.len].* = prefix.*; | |
| 1275 | zip_path[prefix.len + random_len ..].* = suffix.*; | |
| 1276 | ||
| 1277 | var zip_file = while (true) { | |
| 1278 | const random_integer = std.crypto.random.int(u64); | |
| 1279 | zip_path[prefix.len..][0..random_len].* = std.fmt.hex(random_integer); | |
| 1280 | ||
| 1281 | break cache_root.handle.createFile(&zip_path, .{ | |
| 1282 | .exclusive = true, | |
| 1283 | .read = true, | |
| 1284 | }) catch |err| switch (err) { | |
| 1285 | error.PathAlreadyExists => continue, | |
| 1286 | else => |e| return f.fail( | |
| 1287 | f.location_tok, | |
| 1288 | try eb.printString("failed to create temporary zip file: {t}", .{e}), | |
| 1289 | ), | |
| 1290 | }; | |
| 1291 | }; | |
| 1292 | defer zip_file.close(); | |
| 1293 | var zip_file_buffer: [4096]u8 = undefined; | |
| 1294 | var zip_file_reader = b: { | |
| 1295 | var zip_file_writer = zip_file.writer(&zip_file_buffer); | |
| 1296 | ||
| 1297 | _ = reader.streamRemaining(&zip_file_writer.interface) catch |err| switch (err) { | |
| 1298 | error.ReadFailed => return error.ReadFailed, | |
| 1299 | error.WriteFailed => return f.fail( | |
| 1300 | f.location_tok, | |
| 1301 | try eb.printString("failed writing temporary zip file: {t}", .{err}), | |
| 1302 | ), | |
| 1303 | }; | |
| 1304 | zip_file_writer.interface.flush() catch |err| return f.fail( | |
| 1305 | f.location_tok, | |
| 1306 | try eb.printString("failed writing temporary zip file: {t}", .{err}), | |
| 1307 | ); | |
| 1308 | break :b zip_file_writer.moveToReader(); | |
| 1309 | }; | |
| 1340 | 1310 | |
| 1341 | 1311 | var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() }; |
| 1342 | 1312 | // no need to deinit since we are using an arena allocator |
| 1343 | 1313 | |
| 1344 | { | |
| 1345 | var zip_file = cache_root.handle.openFile( | |
| 1346 | &zip_path, | |
| 1347 | .{}, | |
| 1348 | ) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1349 | "failed to open temporary zip file: {s}", | |
| 1350 | .{@errorName(err)}, | |
| 1351 | )); | |
| 1352 | defer zip_file.close(); | |
| 1353 | ||
| 1354 | var zip_file_buffer: [1024]u8 = undefined; | |
| 1355 | var zip_file_reader = zip_file.reader(&zip_file_buffer); | |
| 1356 | ||
| 1357 | std.zip.extract(out_dir, &zip_file_reader, .{ | |
| 1358 | .allow_backslashes = true, | |
| 1359 | .diagnostics = &diagnostics, | |
| 1360 | }) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1361 | "zip extract failed: {s}", | |
| 1362 | .{@errorName(err)}, | |
| 1363 | )); | |
| 1364 | } | |
| 1314 | zip_file_reader.seekTo(0) catch |err| | |
| 1315 | return f.fail(f.location_tok, try eb.printString("failed to seek temporary zip file: {t}", .{err})); | |
| 1316 | std.zip.extract(out_dir, &zip_file_reader, .{ | |
| 1317 | .allow_backslashes = true, | |
| 1318 | .diagnostics = &diagnostics, | |
| 1319 | }) catch |err| return f.fail(f.location_tok, try eb.printString("zip extract failed: {t}", .{err})); | |
| 1365 | 1320 | |
| 1366 | cache_root.handle.deleteFile(&zip_path) catch |err| return f.fail(f.location_tok, try eb.printString( | |
| 1367 | "delete temporary zip failed: {s}", | |
| 1368 | .{@errorName(err)}, | |
| 1369 | )); | |
| 1321 | cache_root.handle.deleteFile(&zip_path) catch |err| | |
| 1322 | return f.fail(f.location_tok, try eb.printString("delete temporary zip failed: {t}", .{err})); | |
| 1370 | 1323 | |
| 1371 | const res: UnpackResult = .{ .root_dir = diagnostics.root_dir }; | |
| 1372 | return res; | |
| 1324 | return .{ .root_dir = diagnostics.root_dir }; | |
| 1373 | 1325 | } |
| 1374 | 1326 | |
| 1375 | 1327 | fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult { |
| ... | ... | @@ -1387,10 +1339,13 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U |
| 1387 | 1339 | var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true }); |
| 1388 | 1340 | defer pack_file.close(); |
| 1389 | 1341 | var pack_file_buffer: [4096]u8 = undefined; |
| 1390 | var fifo = std.fifo.LinearFifo(u8, .{ .Slice = {} }).init(&pack_file_buffer); | |
| 1391 | try fifo.pump(resource.fetch_stream.reader(), pack_file.deprecatedWriter()); | |
| 1392 | ||
| 1393 | var pack_file_reader = pack_file.reader(&pack_file_buffer); | |
| 1342 | var pack_file_reader = b: { | |
| 1343 | var pack_file_writer = pack_file.writer(&pack_file_buffer); | |
| 1344 | const fetch_reader = &resource.fetch_stream.reader; | |
| 1345 | _ = try fetch_reader.streamRemaining(&pack_file_writer.interface); | |
| 1346 | try pack_file_writer.interface.flush(); | |
| 1347 | break :b pack_file_writer.moveToReader(); | |
| 1348 | }; | |
| 1394 | 1349 | |
| 1395 | 1350 | var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true }); |
| 1396 | 1351 | defer index_file.close(); |
src/Package/Fetch/git.zig+156-131| ... | ... | @@ -585,17 +585,17 @@ const ObjectCache = struct { |
| 585 | 585 | /// [protocol-common](https://git-scm.com/docs/protocol-common). The special |
| 586 | 586 | /// meanings of the delimiter and response-end packets are documented in |
| 587 | 587 | /// [protocol-v2](https://git-scm.com/docs/protocol-v2). |
| 588 | const Packet = union(enum) { | |
| 588 | pub const Packet = union(enum) { | |
| 589 | 589 | flush, |
| 590 | 590 | delimiter, |
| 591 | 591 | response_end, |
| 592 | 592 | data: []const u8, |
| 593 | 593 | |
| 594 | const max_data_length = 65516; | |
| 594 | pub const max_data_length = 65516; | |
| 595 | 595 | |
| 596 | 596 | /// Reads a packet in pkt-line format. |
| 597 | fn read(reader: anytype, buf: *[max_data_length]u8) !Packet { | |
| 598 | const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket; | |
| 597 | fn read(reader: *std.Io.Reader) !Packet { | |
| 598 | const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket; | |
| 599 | 599 | switch (length) { |
| 600 | 600 | 0 => return .flush, |
| 601 | 601 | 1 => return .delimiter, |
| ... | ... | @@ -603,13 +603,11 @@ const Packet = union(enum) { |
| 603 | 603 | 3 => return error.InvalidPacket, |
| 604 | 604 | else => if (length - 4 > max_data_length) return error.InvalidPacket, |
| 605 | 605 | } |
| 606 | const data = buf[0 .. length - 4]; | |
| 607 | try reader.readNoEof(data); | |
| 608 | return .{ .data = data }; | |
| 606 | return .{ .data = try reader.take(length - 4) }; | |
| 609 | 607 | } |
| 610 | 608 | |
| 611 | 609 | /// Writes a packet in pkt-line format. |
| 612 | fn write(packet: Packet, writer: anytype) !void { | |
| 610 | fn write(packet: Packet, writer: *std.Io.Writer) !void { | |
| 613 | 611 | switch (packet) { |
| 614 | 612 | .flush => try writer.writeAll("0000"), |
| 615 | 613 | .delimiter => try writer.writeAll("0001"), |
| ... | ... | @@ -657,8 +655,10 @@ pub const Session = struct { |
| 657 | 655 | allocator: Allocator, |
| 658 | 656 | transport: *std.http.Client, |
| 659 | 657 | uri: std.Uri, |
| 660 | http_headers_buffer: []u8, | |
| 658 | /// Asserted to be at least `Packet.max_data_length` | |
| 659 | response_buffer: []u8, | |
| 661 | 660 | ) !Session { |
| 661 | assert(response_buffer.len >= Packet.max_data_length); | |
| 662 | 662 | var session: Session = .{ |
| 663 | 663 | .transport = transport, |
| 664 | 664 | .location = try .init(allocator, uri), |
| ... | ... | @@ -668,7 +668,8 @@ pub const Session = struct { |
| 668 | 668 | .allocator = allocator, |
| 669 | 669 | }; |
| 670 | 670 | errdefer session.deinit(); |
| 671 | var capability_iterator = try session.getCapabilities(http_headers_buffer); | |
| 671 | var capability_iterator: CapabilityIterator = undefined; | |
| 672 | try session.getCapabilities(&capability_iterator, response_buffer); | |
| 672 | 673 | defer capability_iterator.deinit(); |
| 673 | 674 | while (try capability_iterator.next()) |capability| { |
| 674 | 675 | if (mem.eql(u8, capability.key, "agent")) { |
| ... | ... | @@ -743,7 +744,8 @@ pub const Session = struct { |
| 743 | 744 | /// |
| 744 | 745 | /// The `session.location` is updated if the server returns a redirect, so |
| 745 | 746 | /// that subsequent session functions do not need to handle redirects. |
| 746 | fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator { | |
| 747 | fn getCapabilities(session: *Session, it: *CapabilityIterator, response_buffer: []u8) !void { | |
| 748 | assert(response_buffer.len >= Packet.max_data_length); | |
| 747 | 749 | var info_refs_uri = session.location.uri; |
| 748 | 750 | { |
| 749 | 751 | const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{ |
| ... | ... | @@ -757,19 +759,22 @@ pub const Session = struct { |
| 757 | 759 | info_refs_uri.fragment = null; |
| 758 | 760 | |
| 759 | 761 | const max_redirects = 3; |
| 760 | var request = try session.transport.open(.GET, info_refs_uri, .{ | |
| 761 | .redirect_behavior = @enumFromInt(max_redirects), | |
| 762 | .server_header_buffer = http_headers_buffer, | |
| 763 | .extra_headers = &.{ | |
| 764 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 765 | }, | |
| 766 | }); | |
| 767 | errdefer request.deinit(); | |
| 768 | try request.send(); | |
| 769 | try request.finish(); | |
| 762 | it.* = .{ | |
| 763 | .request = try session.transport.request(.GET, info_refs_uri, .{ | |
| 764 | .redirect_behavior = .init(max_redirects), | |
| 765 | .extra_headers = &.{ | |
| 766 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 767 | }, | |
| 768 | }), | |
| 769 | .reader = undefined, | |
| 770 | }; | |
| 771 | errdefer it.deinit(); | |
| 772 | const request = &it.request; | |
| 773 | try request.sendBodiless(); | |
| 770 | 774 | |
| 771 | try request.wait(); | |
| 772 | if (request.response.status != .ok) return error.ProtocolError; | |
| 775 | var redirect_buffer: [1024]u8 = undefined; | |
| 776 | var response = try request.receiveHead(&redirect_buffer); | |
| 777 | if (response.head.status != .ok) return error.ProtocolError; | |
| 773 | 778 | const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects; |
| 774 | 779 | if (any_redirects_occurred) { |
| 775 | 780 | const request_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{ |
| ... | ... | @@ -784,8 +789,7 @@ pub const Session = struct { |
| 784 | 789 | session.location = new_location; |
| 785 | 790 | } |
| 786 | 791 | |
| 787 | const reader = request.reader(); | |
| 788 | var buf: [Packet.max_data_length]u8 = undefined; | |
| 792 | it.reader = response.reader(response_buffer); | |
| 789 | 793 | var state: enum { response_start, response_content } = .response_start; |
| 790 | 794 | while (true) { |
| 791 | 795 | // Some Git servers (at least GitHub) include an additional |
| ... | ... | @@ -795,15 +799,15 @@ pub const Session = struct { |
| 795 | 799 | // Thus, we need to skip any such useless additional responses |
| 796 | 800 | // before we get the one we're actually looking for. The responses |
| 797 | 801 | // will be delimited by flush packets. |
| 798 | const packet = Packet.read(reader, &buf) catch |e| switch (e) { | |
| 802 | const packet = Packet.read(it.reader) catch |err| switch (err) { | |
| 799 | 803 | error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found |
| 800 | else => |other| return other, | |
| 804 | else => |e| return e, | |
| 801 | 805 | }; |
| 802 | 806 | switch (packet) { |
| 803 | 807 | .flush => state = .response_start, |
| 804 | 808 | .data => |data| switch (state) { |
| 805 | 809 | .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) { |
| 806 | return .{ .request = request }; | |
| 810 | return; | |
| 807 | 811 | } else { |
| 808 | 812 | state = .response_content; |
| 809 | 813 | }, |
| ... | ... | @@ -816,7 +820,7 @@ pub const Session = struct { |
| 816 | 820 | |
| 817 | 821 | const CapabilityIterator = struct { |
| 818 | 822 | request: std.http.Client.Request, |
| 819 | buf: [Packet.max_data_length]u8 = undefined, | |
| 823 | reader: *std.Io.Reader, | |
| 820 | 824 | |
| 821 | 825 | const Capability = struct { |
| 822 | 826 | key: []const u8, |
| ... | ... | @@ -830,13 +834,13 @@ pub const Session = struct { |
| 830 | 834 | } |
| 831 | 835 | }; |
| 832 | 836 | |
| 833 | fn deinit(iterator: *CapabilityIterator) void { | |
| 834 | iterator.request.deinit(); | |
| 835 | iterator.* = undefined; | |
| 837 | fn deinit(it: *CapabilityIterator) void { | |
| 838 | it.request.deinit(); | |
| 839 | it.* = undefined; | |
| 836 | 840 | } |
| 837 | 841 | |
| 838 | fn next(iterator: *CapabilityIterator) !?Capability { | |
| 839 | switch (try Packet.read(iterator.request.reader(), &iterator.buf)) { | |
| 842 | fn next(it: *CapabilityIterator) !?Capability { | |
| 843 | switch (try Packet.read(it.reader)) { | |
| 840 | 844 | .flush => return null, |
| 841 | 845 | .data => |data| return Capability.parse(Packet.normalizeText(data)), |
| 842 | 846 | else => return error.UnexpectedPacket, |
| ... | ... | @@ -854,11 +858,13 @@ pub const Session = struct { |
| 854 | 858 | include_symrefs: bool = false, |
| 855 | 859 | /// Whether to include the peeled object ID for returned tag refs. |
| 856 | 860 | include_peeled: bool = false, |
| 857 | server_header_buffer: []u8, | |
| 861 | /// Asserted to be at least `Packet.max_data_length`. | |
| 862 | buffer: []u8, | |
| 858 | 863 | }; |
| 859 | 864 | |
| 860 | 865 | /// Returns an iterator over refs known to the server. |
| 861 | pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator { | |
| 866 | pub fn listRefs(session: Session, it: *RefIterator, options: ListRefsOptions) !void { | |
| 867 | assert(options.buffer.len >= Packet.max_data_length); | |
| 862 | 868 | var upload_pack_uri = session.location.uri; |
| 863 | 869 | { |
| 864 | 870 | const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{ |
| ... | ... | @@ -871,59 +877,56 @@ pub const Session = struct { |
| 871 | 877 | upload_pack_uri.query = null; |
| 872 | 878 | upload_pack_uri.fragment = null; |
| 873 | 879 | |
| 874 | var body: std.ArrayListUnmanaged(u8) = .empty; | |
| 875 | defer body.deinit(session.allocator); | |
| 876 | const body_writer = body.writer(session.allocator); | |
| 877 | try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer); | |
| 880 | var body: std.Io.Writer = .fixed(options.buffer); | |
| 881 | try Packet.write(.{ .data = "command=ls-refs\n" }, &body); | |
| 878 | 882 | if (session.supports_agent) { |
| 879 | try Packet.write(.{ .data = agent_capability }, body_writer); | |
| 883 | try Packet.write(.{ .data = agent_capability }, &body); | |
| 880 | 884 | } |
| 881 | 885 | { |
| 882 | const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)}); | |
| 886 | const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={t}\n", .{ | |
| 887 | session.object_format, | |
| 888 | }); | |
| 883 | 889 | defer session.allocator.free(object_format_packet); |
| 884 | try Packet.write(.{ .data = object_format_packet }, body_writer); | |
| 890 | try Packet.write(.{ .data = object_format_packet }, &body); | |
| 885 | 891 | } |
| 886 | try Packet.write(.delimiter, body_writer); | |
| 892 | try Packet.write(.delimiter, &body); | |
| 887 | 893 | for (options.ref_prefixes) |ref_prefix| { |
| 888 | 894 | const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix}); |
| 889 | 895 | defer session.allocator.free(ref_prefix_packet); |
| 890 | try Packet.write(.{ .data = ref_prefix_packet }, body_writer); | |
| 896 | try Packet.write(.{ .data = ref_prefix_packet }, &body); | |
| 891 | 897 | } |
| 892 | 898 | if (options.include_symrefs) { |
| 893 | try Packet.write(.{ .data = "symrefs\n" }, body_writer); | |
| 899 | try Packet.write(.{ .data = "symrefs\n" }, &body); | |
| 894 | 900 | } |
| 895 | 901 | if (options.include_peeled) { |
| 896 | try Packet.write(.{ .data = "peel\n" }, body_writer); | |
| 902 | try Packet.write(.{ .data = "peel\n" }, &body); | |
| 897 | 903 | } |
| 898 | try Packet.write(.flush, body_writer); | |
| 899 | ||
| 900 | var request = try session.transport.open(.POST, upload_pack_uri, .{ | |
| 901 | .redirect_behavior = .unhandled, | |
| 902 | .server_header_buffer = options.server_header_buffer, | |
| 903 | .extra_headers = &.{ | |
| 904 | .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" }, | |
| 905 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 906 | }, | |
| 907 | }); | |
| 908 | errdefer request.deinit(); | |
| 909 | request.transfer_encoding = .{ .content_length = body.items.len }; | |
| 910 | try request.send(); | |
| 911 | try request.writeAll(body.items); | |
| 912 | try request.finish(); | |
| 913 | ||
| 914 | try request.wait(); | |
| 915 | if (request.response.status != .ok) return error.ProtocolError; | |
| 916 | ||
| 917 | return .{ | |
| 904 | try Packet.write(.flush, &body); | |
| 905 | ||
| 906 | it.* = .{ | |
| 907 | .request = try session.transport.request(.POST, upload_pack_uri, .{ | |
| 908 | .redirect_behavior = .unhandled, | |
| 909 | .extra_headers = &.{ | |
| 910 | .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" }, | |
| 911 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 912 | }, | |
| 913 | }), | |
| 914 | .reader = undefined, | |
| 918 | 915 | .format = session.object_format, |
| 919 | .request = request, | |
| 920 | 916 | }; |
| 917 | const request = &it.request; | |
| 918 | errdefer request.deinit(); | |
| 919 | try request.sendBodyComplete(body.buffered()); | |
| 920 | ||
| 921 | var response = try request.receiveHead(options.buffer); | |
| 922 | if (response.head.status != .ok) return error.ProtocolError; | |
| 923 | it.reader = response.reader(options.buffer); | |
| 921 | 924 | } |
| 922 | 925 | |
| 923 | 926 | pub const RefIterator = struct { |
| 924 | 927 | format: Oid.Format, |
| 925 | 928 | request: std.http.Client.Request, |
| 926 | buf: [Packet.max_data_length]u8 = undefined, | |
| 929 | reader: *std.Io.Reader, | |
| 927 | 930 | |
| 928 | 931 | pub const Ref = struct { |
| 929 | 932 | oid: Oid, |
| ... | ... | @@ -937,13 +940,13 @@ pub const Session = struct { |
| 937 | 940 | iterator.* = undefined; |
| 938 | 941 | } |
| 939 | 942 | |
| 940 | pub fn next(iterator: *RefIterator) !?Ref { | |
| 941 | switch (try Packet.read(iterator.request.reader(), &iterator.buf)) { | |
| 943 | pub fn next(it: *RefIterator) !?Ref { | |
| 944 | switch (try Packet.read(it.reader)) { | |
| 942 | 945 | .flush => return null, |
| 943 | 946 | .data => |data| { |
| 944 | 947 | const ref_data = Packet.normalizeText(data); |
| 945 | 948 | const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket; |
| 946 | const oid = Oid.parse(iterator.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket; | |
| 949 | const oid = Oid.parse(it.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket; | |
| 947 | 950 | |
| 948 | 951 | const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len; |
| 949 | 952 | const name = ref_data[oid_sep_pos + 1 .. name_sep_pos]; |
| ... | ... | @@ -957,7 +960,7 @@ pub const Session = struct { |
| 957 | 960 | if (mem.startsWith(u8, attribute, "symref-target:")) { |
| 958 | 961 | symref_target = attribute["symref-target:".len..]; |
| 959 | 962 | } else if (mem.startsWith(u8, attribute, "peeled:")) { |
| 960 | peeled = Oid.parse(iterator.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket; | |
| 963 | peeled = Oid.parse(it.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket; | |
| 961 | 964 | } |
| 962 | 965 | last_sep_pos = next_sep_pos; |
| 963 | 966 | } |
| ... | ... | @@ -973,9 +976,12 @@ pub const Session = struct { |
| 973 | 976 | /// performed if the server supports it. |
| 974 | 977 | pub fn fetch( |
| 975 | 978 | session: Session, |
| 979 | fs: *FetchStream, | |
| 976 | 980 | wants: []const []const u8, |
| 977 | http_headers_buffer: []u8, | |
| 978 | ) !FetchStream { | |
| 981 | /// Asserted to be at least `Packet.max_data_length`. | |
| 982 | response_buffer: []u8, | |
| 983 | ) !void { | |
| 984 | assert(response_buffer.len >= Packet.max_data_length); | |
| 979 | 985 | var upload_pack_uri = session.location.uri; |
| 980 | 986 | { |
| 981 | 987 | const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{ |
| ... | ... | @@ -988,63 +994,71 @@ pub const Session = struct { |
| 988 | 994 | upload_pack_uri.query = null; |
| 989 | 995 | upload_pack_uri.fragment = null; |
| 990 | 996 | |
| 991 | var body: std.ArrayListUnmanaged(u8) = .empty; | |
| 992 | defer body.deinit(session.allocator); | |
| 993 | const body_writer = body.writer(session.allocator); | |
| 994 | try Packet.write(.{ .data = "command=fetch\n" }, body_writer); | |
| 997 | var body: std.Io.Writer = .fixed(response_buffer); | |
| 998 | try Packet.write(.{ .data = "command=fetch\n" }, &body); | |
| 995 | 999 | if (session.supports_agent) { |
| 996 | try Packet.write(.{ .data = agent_capability }, body_writer); | |
| 1000 | try Packet.write(.{ .data = agent_capability }, &body); | |
| 997 | 1001 | } |
| 998 | 1002 | { |
| 999 | 1003 | const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)}); |
| 1000 | 1004 | defer session.allocator.free(object_format_packet); |
| 1001 | try Packet.write(.{ .data = object_format_packet }, body_writer); | |
| 1005 | try Packet.write(.{ .data = object_format_packet }, &body); | |
| 1002 | 1006 | } |
| 1003 | try Packet.write(.delimiter, body_writer); | |
| 1007 | try Packet.write(.delimiter, &body); | |
| 1004 | 1008 | // Our packfile parser supports the OFS_DELTA object type |
| 1005 | try Packet.write(.{ .data = "ofs-delta\n" }, body_writer); | |
| 1009 | try Packet.write(.{ .data = "ofs-delta\n" }, &body); | |
| 1006 | 1010 | // We do not currently convey server progress information to the user |
| 1007 | try Packet.write(.{ .data = "no-progress\n" }, body_writer); | |
| 1011 | try Packet.write(.{ .data = "no-progress\n" }, &body); | |
| 1008 | 1012 | if (session.supports_shallow) { |
| 1009 | try Packet.write(.{ .data = "deepen 1\n" }, body_writer); | |
| 1013 | try Packet.write(.{ .data = "deepen 1\n" }, &body); | |
| 1010 | 1014 | } |
| 1011 | 1015 | for (wants) |want| { |
| 1012 | 1016 | var buf: [Packet.max_data_length]u8 = undefined; |
| 1013 | 1017 | const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable; |
| 1014 | try Packet.write(.{ .data = arg }, body_writer); | |
| 1018 | try Packet.write(.{ .data = arg }, &body); | |
| 1015 | 1019 | } |
| 1016 | try Packet.write(.{ .data = "done\n" }, body_writer); | |
| 1017 | try Packet.write(.flush, body_writer); | |
| 1018 | ||
| 1019 | var request = try session.transport.open(.POST, upload_pack_uri, .{ | |
| 1020 | .redirect_behavior = .not_allowed, | |
| 1021 | .server_header_buffer = http_headers_buffer, | |
| 1022 | .extra_headers = &.{ | |
| 1023 | .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" }, | |
| 1024 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 1025 | }, | |
| 1026 | }); | |
| 1020 | try Packet.write(.{ .data = "done\n" }, &body); | |
| 1021 | try Packet.write(.flush, &body); | |
| 1022 | ||
| 1023 | fs.* = .{ | |
| 1024 | .request = try session.transport.request(.POST, upload_pack_uri, .{ | |
| 1025 | .redirect_behavior = .not_allowed, | |
| 1026 | .extra_headers = &.{ | |
| 1027 | .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" }, | |
| 1028 | .{ .name = "Git-Protocol", .value = "version=2" }, | |
| 1029 | }, | |
| 1030 | }), | |
| 1031 | .input = undefined, | |
| 1032 | .reader = undefined, | |
| 1033 | .remaining_len = undefined, | |
| 1034 | }; | |
| 1035 | const request = &fs.request; | |
| 1027 | 1036 | errdefer request.deinit(); |
| 1028 | request.transfer_encoding = .{ .content_length = body.items.len }; | |
| 1029 | try request.send(); | |
| 1030 | try request.writeAll(body.items); | |
| 1031 | try request.finish(); | |
| 1032 | 1037 | |
| 1033 | try request.wait(); | |
| 1034 | if (request.response.status != .ok) return error.ProtocolError; | |
| 1038 | try request.sendBodyComplete(body.buffered()); | |
| 1039 | ||
| 1040 | var response = try request.receiveHead(&.{}); | |
| 1041 | if (response.head.status != .ok) return error.ProtocolError; | |
| 1035 | 1042 | |
| 1036 | const reader = request.reader(); | |
| 1043 | const reader = response.reader(response_buffer); | |
| 1037 | 1044 | // We are not interested in any of the sections of the returned fetch |
| 1038 | 1045 | // data other than the packfile section, since we aren't doing anything |
| 1039 | 1046 | // complex like ref negotiation (this is a fresh clone). |
| 1040 | 1047 | var state: enum { section_start, section_content } = .section_start; |
| 1041 | 1048 | while (true) { |
| 1042 | var buf: [Packet.max_data_length]u8 = undefined; | |
| 1043 | const packet = try Packet.read(reader, &buf); | |
| 1049 | const packet = try Packet.read(reader); | |
| 1044 | 1050 | switch (state) { |
| 1045 | 1051 | .section_start => switch (packet) { |
| 1046 | 1052 | .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) { |
| 1047 | return .{ .request = request }; | |
| 1053 | fs.input = reader; | |
| 1054 | fs.reader = .{ | |
| 1055 | .buffer = &.{}, | |
| 1056 | .vtable = &.{ .stream = FetchStream.stream }, | |
| 1057 | .seek = 0, | |
| 1058 | .end = 0, | |
| 1059 | }; | |
| 1060 | fs.remaining_len = 0; | |
| 1061 | return; | |
| 1048 | 1062 | } else { |
| 1049 | 1063 | state = .section_content; |
| 1050 | 1064 | }, |
| ... | ... | @@ -1061,20 +1075,23 @@ pub const Session = struct { |
| 1061 | 1075 | |
| 1062 | 1076 | pub const FetchStream = struct { |
| 1063 | 1077 | request: std.http.Client.Request, |
| 1064 | buf: [Packet.max_data_length]u8 = undefined, | |
| 1065 | pos: usize = 0, | |
| 1066 | len: usize = 0, | |
| 1078 | input: *std.Io.Reader, | |
| 1079 | reader: std.Io.Reader, | |
| 1080 | err: ?Error = null, | |
| 1081 | remaining_len: usize, | |
| 1067 | 1082 | |
| 1068 | pub fn deinit(stream: *FetchStream) void { | |
| 1069 | stream.request.deinit(); | |
| 1083 | pub fn deinit(fs: *FetchStream) void { | |
| 1084 | fs.request.deinit(); | |
| 1070 | 1085 | } |
| 1071 | 1086 | |
| 1072 | pub const ReadError = std.http.Client.Request.ReadError || error{ | |
| 1087 | pub const Error = error{ | |
| 1073 | 1088 | InvalidPacket, |
| 1074 | 1089 | ProtocolError, |
| 1075 | 1090 | UnexpectedPacket, |
| 1091 | WriteFailed, | |
| 1092 | ReadFailed, | |
| 1093 | EndOfStream, | |
| 1076 | 1094 | }; |
| 1077 | pub const Reader = std.io.GenericReader(*FetchStream, ReadError, read); | |
| 1078 | 1095 | |
| 1079 | 1096 | const StreamCode = enum(u8) { |
| 1080 | 1097 | pack_data = 1, |
| ... | ... | @@ -1083,33 +1100,41 @@ pub const Session = struct { |
| 1083 | 1100 | _, |
| 1084 | 1101 | }; |
| 1085 | 1102 | |
| 1086 | pub fn reader(stream: *FetchStream) Reader { | |
| 1087 | return .{ .context = stream }; | |
| 1088 | } | |
| 1089 | ||
| 1090 | pub fn read(stream: *FetchStream, buf: []u8) !usize { | |
| 1091 | if (stream.pos == stream.len) { | |
| 1103 | pub fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { | |
| 1104 | const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r)); | |
| 1105 | const input = fs.input; | |
| 1106 | if (fs.remaining_len == 0) { | |
| 1092 | 1107 | while (true) { |
| 1093 | switch (try Packet.read(stream.request.reader(), &stream.buf)) { | |
| 1094 | .flush => return 0, | |
| 1108 | switch (Packet.read(input) catch |err| { | |
| 1109 | fs.err = err; | |
| 1110 | return error.ReadFailed; | |
| 1111 | }) { | |
| 1112 | .flush => return error.EndOfStream, | |
| 1095 | 1113 | .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) { |
| 1096 | 1114 | .pack_data => { |
| 1097 | stream.pos = 1; | |
| 1098 | stream.len = data.len; | |
| 1115 | input.toss(1); | |
| 1116 | fs.remaining_len = data.len; | |
| 1099 | 1117 | break; |
| 1100 | 1118 | }, |
| 1101 | .fatal_error => return error.ProtocolError, | |
| 1119 | .fatal_error => { | |
| 1120 | fs.err = error.ProtocolError; | |
| 1121 | return error.ReadFailed; | |
| 1122 | }, | |
| 1102 | 1123 | else => {}, |
| 1103 | 1124 | }, |
| 1104 | else => return error.UnexpectedPacket, | |
| 1125 | else => { | |
| 1126 | fs.err = error.UnexpectedPacket; | |
| 1127 | return error.ReadFailed; | |
| 1128 | }, | |
| 1105 | 1129 | } |
| 1106 | 1130 | } |
| 1107 | 1131 | } |
| 1108 | ||
| 1109 | const size = @min(buf.len, stream.len - stream.pos); | |
| 1110 | @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]); | |
| 1111 | stream.pos += size; | |
| 1112 | return size; | |
| 1132 | const buf = limit.slice(try w.writableSliceGreedy(1)); | |
| 1133 | const n = @min(buf.len, fs.remaining_len); | |
| 1134 | @memcpy(buf[0..n], input.buffered()[0..n]); | |
| 1135 | input.toss(n); | |
| 1136 | fs.remaining_len -= n; | |
| 1137 | return n; | |
| 1113 | 1138 | } |
| 1114 | 1139 | }; |
| 1115 | 1140 | }; |