authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-06 22:39:26-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-07 10:04:52-07:00
log46b34949c3b40d286233c98c97bd2e0c221c1518
tree4bb81a384f60ee13db3bfb1013ad5d1c6040d22e
parent172d31b0e2d9c60129dfd453d2b0582d55d60720

TLS, HTTP, and package fetching fixes

* TLS: add missing assert for output buffer length requirement * TLS: add missing flushes * TLS: add flush implementation * TLS: finish drain implementation * HTTP: correct buffer sizes for TLS * HTTP: expose a getReadError method on Connection * HTTP: add missing flush on sendBodyComplete * Fetch: remove unwanted deinit * Fetch: improve error reporting

3 files changed, 84 insertions(+), 28 deletions(-)

lib/std/crypto/tls/Client.zig+46-16
...@@ -8,8 +8,8 @@ const mem = std.mem;...@@ -8,8 +8,8 @@ const mem = std.mem;
8const crypto = std.crypto;8const crypto = std.crypto;
9const assert = std.debug.assert;9const assert = std.debug.assert;
10const Certificate = std.crypto.Certificate;10const Certificate = std.crypto.Certificate;
11const Reader = std.io.Reader;11const Reader = std.Io.Reader;
12const Writer = std.io.Writer;12const Writer = std.Io.Writer;
1313
14const max_ciphertext_len = tls.max_ciphertext_len;14const max_ciphertext_len = tls.max_ciphertext_len;
15const hmacExpandLabel = tls.hmacExpandLabel;15const hmacExpandLabel = tls.hmacExpandLabel;
...@@ -27,6 +27,8 @@ reader: Reader,...@@ -27,6 +27,8 @@ reader: Reader,
2727
28/// The encrypted stream from the client to the server. Bytes are pushed here28/// The encrypted stream from the client to the server. Bytes are pushed here
29/// via `writer`.29/// via `writer`.
30///
31/// The buffer is asserted to have capacity at least `min_buffer_len`.
30output: *Writer,32output: *Writer,
31/// The plaintext stream from the client to the server.33/// The plaintext stream from the client to the server.
32writer: Writer,34writer: Writer,
...@@ -122,7 +124,6 @@ pub const Options = struct {...@@ -122,7 +124,6 @@ pub const Options = struct {
122 /// the amount of data expected, such as HTTP with the Content-Length header.124 /// the amount of data expected, such as HTTP with the Content-Length header.
123 allow_truncation_attacks: bool = false,125 allow_truncation_attacks: bool = false,
124 write_buffer: []u8,126 write_buffer: []u8,
125 /// Asserted to have capacity at least `min_buffer_len`.
126 read_buffer: []u8,127 read_buffer: []u8,
127 /// Populated when `error.TlsAlert` is returned from `init`.128 /// Populated when `error.TlsAlert` is returned from `init`.
128 alert: ?*tls.Alert = null,129 alert: ?*tls.Alert = null,
...@@ -185,6 +186,7 @@ const InitError = error{...@@ -185,6 +186,7 @@ const InitError = error{
185/// `input` is asserted to have buffer capacity at least `min_buffer_len`.186/// `input` is asserted to have buffer capacity at least `min_buffer_len`.
186pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client {187pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client {
187 assert(input.buffer.len >= min_buffer_len);188 assert(input.buffer.len >= min_buffer_len);
189 assert(output.buffer.len >= min_buffer_len);
188 const host = switch (options.host) {190 const host = switch (options.host) {
189 .no_verification => "",191 .no_verification => "",
190 .explicit => |host| host,192 .explicit => |host| host,
...@@ -278,6 +280,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -278,6 +280,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
278 {280 {
279 var iovecs: [2][]const u8 = .{ cleartext_header, host };281 var iovecs: [2][]const u8 = .{ cleartext_header, host };
280 try output.writeVecAll(iovecs[0..if (host.len == 0) 1 else 2]);282 try output.writeVecAll(iovecs[0..if (host.len == 0) 1 else 2]);
283 try output.flush();
281 }284 }
282285
283 var tls_version: tls.ProtocolVersion = undefined;286 var tls_version: tls.ProtocolVersion = undefined;
...@@ -763,6 +766,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -763,6 +766,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
763 &client_verify_msg,766 &client_verify_msg,
764 };767 };
765 try output.writeVecAll(&all_msgs_vec);768 try output.writeVecAll(&all_msgs_vec);
769 try output.flush();
766 },770 },
767 }771 }
768 write_seq += 1;772 write_seq += 1;
...@@ -828,6 +832,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -828,6 +832,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
828 &finished_msg,832 &finished_msg,
829 };833 };
830 try output.writeVecAll(&all_msgs_vec);834 try output.writeVecAll(&all_msgs_vec);
835 try output.flush();
831836
832 const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);837 const client_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "c ap traffic", &handshake_hash, P.Hash.digest_length);
833 const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length);838 const server_secret = hkdfExpandLabel(P.Hkdf, pv.master_secret, "s ap traffic", &handshake_hash, P.Hash.digest_length);
...@@ -877,7 +882,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -877,7 +882,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
877 .buffer = options.write_buffer,882 .buffer = options.write_buffer,
878 .vtable = &.{883 .vtable = &.{
879 .drain = drain,884 .drain = drain,
880 .sendFile = Writer.unimplementedSendFile,885 .flush = flush,
881 },886 },
882 },887 },
883 .tls_version = tls_version,888 .tls_version = tls_version,
...@@ -911,31 +916,56 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -911,31 +916,56 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
911916
912fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {917fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
913 const c: *Client = @alignCast(@fieldParentPtr("writer", w));918 const c: *Client = @alignCast(@fieldParentPtr("writer", w));
914 if (true) @panic("update to use the buffer and flush");
915 const sliced_data = if (splat == 0) data[0..data.len -| 1] else data;
916 const output = c.output;919 const output = c.output;
917 const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len);920 const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len);
918 var total_clear: usize = 0;
919 var ciphertext_end: usize = 0;921 var ciphertext_end: usize = 0;
920 for (sliced_data) |buf| {922 var total_clear: usize = 0;
921 const prepared = prepareCiphertextRecord(c, ciphertext_buf[ciphertext_end..], buf, .application_data);923 done: {
922 total_clear += prepared.cleartext_len;924 {
923 ciphertext_end += prepared.ciphertext_end;925 const buf = w.buffered();
924 if (total_clear < buf.len) break;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 }
925 }946 }
926 output.advance(ciphertext_end);947 output.advance(ciphertext_end);
927 return total_clear;948 return w.consume(total_clear);
949}
950
951fn 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;
928}958}
929959
930/// Sends a `close_notify` alert, which is necessary for the server to960/// Sends a `close_notify` alert, which is necessary for the server to
931/// distinguish between a properly finished TLS session, or a truncation961/// distinguish between a properly finished TLS session, or a truncation
932/// attack.962/// attack.
933pub fn end(c: *Client) Writer.Error!void {963pub fn end(c: *Client) Writer.Error!void {
964 try flush(&c.writer);
934 const output = c.output;965 const output = c.output;
935 const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len);966 const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len);
936 const prepared = prepareCiphertextRecord(c, ciphertext_buf, &tls.close_notify_alert, .alert);967 const prepared = prepareCiphertextRecord(c, ciphertext_buf, &tls.close_notify_alert, .alert);
937 output.advance(prepared.cleartext_len);968 output.advance(prepared.ciphertext_end);
938 return prepared.ciphertext_end;
939}969}
940970
941fn prepareCiphertextRecord(971fn prepareCiphertextRecord(
...@@ -1045,7 +1075,7 @@ pub fn eof(c: Client) bool {...@@ -1045,7 +1075,7 @@ pub fn eof(c: Client) bool {
1045 return c.received_close_notify;1075 return c.received_close_notify;
1046}1076}
10471077
1048fn stream(r: *Reader, w: *Writer, limit: std.io.Limit) Reader.StreamError!usize {1078fn stream(r: *Reader, w: *Writer, limit: std.Io.Limit) Reader.StreamError!usize {
1049 const c: *Client = @alignCast(@fieldParentPtr("reader", r));1079 const c: *Client = @alignCast(@fieldParentPtr("reader", r));
1050 if (c.eof()) return error.EndOfStream;1080 if (c.eof()) return error.EndOfStream;
1051 const input = c.input;1081 const input = c.input;
lib/std/http/Client.zig+29-9
...@@ -42,7 +42,7 @@ connection_pool: ConnectionPool = .{},...@@ -42,7 +42,7 @@ connection_pool: ConnectionPool = .{},
42///42///
43/// If the entire HTTP header cannot fit in this amount of bytes,43/// If the entire HTTP header cannot fit in this amount of bytes,
44/// `error.HttpHeadersOversize` will be returned from `Request.wait`.44/// `error.HttpHeadersOversize` will be returned from `Request.wait`.
45read_buffer_size: usize = 4096,45read_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.46/// Each `Connection` allocates this amount for the writer buffer.
47write_buffer_size: usize = 1024,47write_buffer_size: usize = 1024,
4848
...@@ -304,15 +304,16 @@ pub const Connection = struct {...@@ -304,15 +304,16 @@ pub const Connection = struct {
304 const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len];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];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];306 const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size];
307 const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];307 const write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size];
308 assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len);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);
309 @memcpy(host_buffer, remote_host);310 @memcpy(host_buffer, remote_host);
310 const tls: *Tls = @ptrCast(base);311 const tls: *Tls = @ptrCast(base);
311 tls.* = .{312 tls.* = .{
312 .connection = .{313 .connection = .{
313 .client = client,314 .client = client,
314 .stream_writer = stream.writer(socket_write_buffer),315 .stream_writer = stream.writer(tls_write_buffer),
315 .stream_reader = stream.reader(&.{}),316 .stream_reader = stream.reader(tls_read_buffer),
316 .pool_node = .{},317 .pool_node = .{},
317 .port = port,318 .port = port,
318 .host_len = @intCast(remote_host.len),319 .host_len = @intCast(remote_host.len),
...@@ -328,8 +329,8 @@ pub const Connection = struct {...@@ -328,8 +329,8 @@ pub const Connection = struct {
328 .host = .{ .explicit = remote_host },329 .host = .{ .explicit = remote_host },
329 .ca = .{ .bundle = client.ca_bundle },330 .ca = .{ .bundle = client.ca_bundle },
330 .ssl_key_log = client.ssl_key_log,331 .ssl_key_log = client.ssl_key_log,
331 .read_buffer = tls_read_buffer,332 .read_buffer = read_buffer,
332 .write_buffer = tls_write_buffer,333 .write_buffer = write_buffer,
333 // This is appropriate for HTTPS because the HTTP headers contain334 // This is appropriate for HTTPS because the HTTP headers contain
334 // the content length which is used to detect truncation attacks.335 // the content length which is used to detect truncation attacks.
335 .allow_truncation_attacks = true,336 .allow_truncation_attacks = true,
...@@ -347,7 +348,8 @@ pub const Connection = struct {...@@ -347,7 +348,8 @@ pub const Connection = struct {
347 }348 }
348349
349 fn allocLen(client: *Client, host_len: usize) usize {350 fn allocLen(client: *Client, host_len: usize) usize {
350 return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size + client.write_buffer_size;351 return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size +
352 client.write_buffer_size + client.read_buffer_size;
351 }353 }
352354
353 fn host(tls: *Tls) []u8 {355 fn host(tls: *Tls) []u8 {
...@@ -356,6 +358,21 @@ pub const Connection = struct {...@@ -356,6 +358,21 @@ pub const Connection = struct {
356 }358 }
357 };359 };
358360
361 pub const ReadError = std.crypto.tls.Client.ReadError || std.net.Stream.ReadError;
362
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 },
373 };
374 }
375
359 fn getStream(c: *Connection) net.Stream {376 fn getStream(c: *Connection) net.Stream {
360 return c.stream_reader.getStream();377 return c.stream_reader.getStream();
361 }378 }
...@@ -434,7 +451,6 @@ pub const Connection = struct {...@@ -434,7 +451,6 @@ pub const Connection = struct {
434 if (disable_tls) unreachable;451 if (disable_tls) unreachable;
435 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));452 const tls: *Tls = @alignCast(@fieldParentPtr("connection", c));
436 try tls.client.end();453 try tls.client.end();
437 try tls.client.writer.flush();
438 }454 }
439 try c.stream_writer.interface.flush();455 try c.stream_writer.interface.flush();
440 }456 }
...@@ -874,6 +890,7 @@ pub const Request = struct {...@@ -874,6 +890,7 @@ pub const Request = struct {
874 var bw = try sendBodyUnflushed(r, body);890 var bw = try sendBodyUnflushed(r, body);
875 bw.writer.end = body.len;891 bw.writer.end = body.len;
876 try bw.end();892 try bw.end();
893 try r.connection.?.flush();
877 }894 }
878895
879 /// Transfers the HTTP head over the connection, which is not flushed until896 /// Transfers the HTTP head over the connection, which is not flushed until
...@@ -1063,6 +1080,9 @@ pub const Request = struct {...@@ -1063,6 +1080,9 @@ pub const Request = struct {
1063 /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize`1080 /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize`
1064 /// is returned instead. This buffer may be empty if no redirects are to be1081 /// is returned instead. This buffer may be empty if no redirects are to be
1065 /// handled.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.
1066 pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response {1086 pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response {
1067 var aux_buf = redirect_buffer;1087 var aux_buf = redirect_buffer;
1068 while (true) {1088 while (true) {
src/Package/Fetch.zig+9-3
...@@ -998,15 +998,21 @@ fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u...@@ -998,15 +998,21 @@ fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u
998 .buffer = reader_buffer,998 .buffer = reader_buffer,
999 } };999 } };
1000 const request = &resource.http_request.request;1000 const request = &resource.http_request.request;
1001 defer request.deinit();1001 errdefer request.deinit();
10021002
1003 request.sendBodiless() catch |err|1003 request.sendBodiless() catch |err|
1004 return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err}));1004 return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err}));
10051005
1006 var redirect_buffer: [1024]u8 = undefined;1006 var redirect_buffer: [1024]u8 = undefined;
1007 const response = &resource.http_request.response;1007 const response = &resource.http_request.response;
1008 response.* = request.receiveHead(&redirect_buffer) catch |err|1008 response.* = request.receiveHead(&redirect_buffer) catch |err| switch (err) {
1009 return f.fail(f.location_tok, try eb.printString("invalid HTTP response: {t}", .{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})),
1015 };
10101016
1011 if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString(1017 if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString(
1012 "bad HTTP response code: '{d} {s}'",1018 "bad HTTP response code: '{d} {s}'",