| ... | @@ -19,12 +19,55 @@ pub const connection_pool_size = std.options.http_connection_pool_size; | ... | @@ -19,12 +19,55 @@ pub const connection_pool_size = std.options.http_connection_pool_size; |
| 19 | /// managed buffer is not provided. | 19 | /// managed buffer is not provided. |
| 20 | allocator: Allocator, | 20 | allocator: Allocator, |
| 21 | ca_bundle: std.crypto.Certificate.Bundle = .{}, | 21 | ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| | 22 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| 22 | /// When this is `true`, the next time this client performs an HTTPS request, | 23 | /// When this is `true`, the next time this client performs an HTTPS request, |
| 23 | /// it will first rescan the system for root certificates. | 24 | /// it will first rescan the system for root certificates. |
| 24 | next_https_rescan_certs: bool = true, | 25 | next_https_rescan_certs: bool = true, |
| 25 | | 26 | |
| 26 | connection_pool: ConnectionPool = .{}, | 27 | connection_pool: ConnectionPool = .{}, |
| 27 | | 28 | |
| | 29 | last_error: ?ExtraError = null, |
| | 30 | |
| | 31 | pub const ExtraError = union(enum) { |
| | 32 | fn impliedErrorSet(comptime f: anytype) type { |
| | 33 | const set = @typeInfo(@typeInfo(@TypeOf(f)).Fn.return_type.?).ErrorUnion.error_set; |
| | 34 | if (@typeName(set)[0] != '@') @compileError(@typeName(f) ++ " doesn't have an implied error set any more."); |
| | 35 | return set; |
| | 36 | } |
| | 37 | |
| | 38 | // There's apparently a dependency loop with using Client.DeflateDecompressor. |
| | 39 | const FakeTransferError = proto.HeadersParser.ReadError || error{ReadFailed}; |
| | 40 | const FakeTransferReader = std.io.Reader(void, FakeTransferError, fakeRead); |
| | 41 | fn fakeRead(ctx: void, buf: []u8) FakeTransferError!usize { |
| | 42 | _ = .{ buf, ctx }; |
| | 43 | return 0; |
| | 44 | } |
| | 45 | |
| | 46 | const FakeDeflateDecompressor = std.compress.zlib.ZlibStream(FakeTransferReader); |
| | 47 | const FakeGzipDecompressor = std.compress.gzip.Decompress(FakeTransferReader); |
| | 48 | const FakeZstdDecompressor = std.compress.zstd.DecompressStream(FakeTransferReader, .{}); |
| | 49 | |
| | 50 | pub const TcpConnectError = std.net.TcpConnectToHostError; |
| | 51 | pub const TlsError = std.crypto.tls.Client.InitError(net.Stream); |
| | 52 | pub const WriteError = BufferedConnection.WriteError; |
| | 53 | pub const ReadError = BufferedConnection.ReadError || error{HttpChunkInvalid}; |
| | 54 | pub const CaBundleError = impliedErrorSet(std.crypto.Certificate.Bundle.rescan); |
| | 55 | |
| | 56 | pub const ZlibInitError = error{ BadHeader, InvalidCompression, InvalidWindowSize, Unsupported, EndOfStream, OutOfMemory } || Request.TransferReadError; |
| | 57 | pub const GzipInitError = error{ BadHeader, InvalidCompression, OutOfMemory, WrongChecksum, EndOfStream, StreamTooLong } || Request.TransferReadError; |
| | 58 | // pub const DecompressError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error; |
| | 59 | pub const DecompressError = FakeDeflateDecompressor.Error || FakeGzipDecompressor.Error || FakeZstdDecompressor.Error; |
| | 60 | |
| | 61 | zlib_init: ZlibInitError, // error.CompressionInitializationFailed |
| | 62 | gzip_init: GzipInitError, // error.CompressionInitializationFailed |
| | 63 | connect: TcpConnectError, // error.ConnectionFailed |
| | 64 | ca_bundle: CaBundleError, // error.CertificateAuthorityBundleFailed |
| | 65 | tls: TlsError, // error.TlsInitializationFailed |
| | 66 | write: WriteError, // error.WriteFailed |
| | 67 | read: ReadError, // error.ReadFailed |
| | 68 | decompress: DecompressError, // error.ReadFailed |
| | 69 | }; |
| | 70 | |
| 28 | pub const ConnectionPool = struct { | 71 | pub const ConnectionPool = struct { |
| 29 | pub const Criteria = struct { | 72 | pub const Criteria = struct { |
| 30 | host: []const u8, | 73 | host: []const u8, |
| ... | @@ -146,10 +189,6 @@ pub const ConnectionPool = struct { | ... | @@ -146,10 +189,6 @@ pub const ConnectionPool = struct { |
| 146 | } | 189 | } |
| 147 | }; | 190 | }; |
| 148 | | 191 | |
| 149 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader); | | |
| 150 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); | | |
| 151 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{}); | | |
| 152 | | | |
| 153 | pub const Connection = struct { | 192 | pub const Connection = struct { |
| 154 | stream: net.Stream, | 193 | stream: net.Stream, |
| 155 | /// undefined unless protocol is tls. | 194 | /// undefined unless protocol is tls. |
| ... | @@ -312,6 +351,10 @@ pub const RequestTransfer = union(enum) { | ... | @@ -312,6 +351,10 @@ pub const RequestTransfer = union(enum) { |
| 312 | }; | 351 | }; |
| 313 | | 352 | |
| 314 | pub const Compression = union(enum) { | 353 | pub const Compression = union(enum) { |
| | 354 | pub const DeflateDecompressor = std.compress.zlib.ZlibStream(Request.TransferReader); |
| | 355 | pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); |
| | 356 | pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{}); |
| | 357 | |
| 315 | deflate: DeflateDecompressor, | 358 | deflate: DeflateDecompressor, |
| 316 | gzip: GzipDecompressor, | 359 | gzip: GzipDecompressor, |
| 317 | zstd: ZstdDecompressor, | 360 | zstd: ZstdDecompressor, |
| ... | @@ -336,10 +379,11 @@ pub const Response = struct { | ... | @@ -336,10 +379,11 @@ pub const Response = struct { |
| 336 | HttpHeaderContinuationsUnsupported, | 379 | HttpHeaderContinuationsUnsupported, |
| 337 | HttpTransferEncodingUnsupported, | 380 | HttpTransferEncodingUnsupported, |
| 338 | HttpConnectionHeaderUnsupported, | 381 | HttpConnectionHeaderUnsupported, |
| 339 | InvalidCharacter, | 382 | InvalidContentLength, |
| | 383 | CompressionNotSupported, |
| 340 | }; | 384 | }; |
| 341 | | 385 | |
| 342 | pub fn parse(bytes: []const u8) !Headers { | 386 | pub fn parse(bytes: []const u8) ParseError!Headers { |
| 343 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | 387 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| 344 | | 388 | |
| 345 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | 389 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| ... | @@ -374,7 +418,7 @@ pub const Response = struct { | ... | @@ -374,7 +418,7 @@ pub const Response = struct { |
| 374 | headers.location = header_value; | 418 | headers.location = header_value; |
| 375 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | 419 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 376 | if (headers.content_length != null) return error.HttpHeadersInvalid; | 420 | if (headers.content_length != null) return error.HttpHeadersInvalid; |
| 377 | headers.content_length = try std.fmt.parseInt(u64, header_value, 10); | 421 | headers.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; |
| 378 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | 422 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { |
| 379 | // Transfer-Encoding: second, first | 423 | // Transfer-Encoding: second, first |
| 380 | // Transfer-Encoding: deflate, chunked | 424 | // Transfer-Encoding: deflate, chunked |
| ... | @@ -457,6 +501,14 @@ pub const Response = struct { | ... | @@ -457,6 +501,14 @@ pub const Response = struct { |
| 457 | skip: bool = false, | 501 | skip: bool = false, |
| 458 | }; | 502 | }; |
| 459 | | 503 | |
| | 504 | /// A HTTP request. |
| | 505 | /// |
| | 506 | /// Order of operations: |
| | 507 | /// - request |
| | 508 | /// - write |
| | 509 | /// - finish |
| | 510 | /// - do |
| | 511 | /// - read |
| 460 | pub const Request = struct { | 512 | pub const Request = struct { |
| 461 | pub const Headers = struct { | 513 | pub const Headers = struct { |
| 462 | version: http.Version = .@"HTTP/1.1", | 514 | version: http.Version = .@"HTTP/1.1", |
| ... | @@ -506,7 +558,67 @@ pub const Request = struct { | ... | @@ -506,7 +558,67 @@ pub const Request = struct { |
| 506 | req.* = undefined; | 558 | req.* = undefined; |
| 507 | } | 559 | } |
| 508 | | 560 | |
| 509 | pub const TransferReadError = BufferedConnection.ReadError || proto.HeadersParser.ReadError; | 561 | pub fn start(req: *Request, uri: Uri, headers: Headers) !void { |
| | 562 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| | 563 | const w = buffered.writer(); |
| | 564 | |
| | 565 | const escaped_path = try Uri.escapePath(req.client.allocator, uri.path); |
| | 566 | defer req.client.allocator.free(escaped_path); |
| | 567 | |
| | 568 | const escaped_query = if (uri.query) |q| try Uri.escapeQuery(req.client.allocator, q) else null; |
| | 569 | defer if (escaped_query) |q| req.client.allocator.free(q); |
| | 570 | |
| | 571 | const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(req.client.allocator, f) else null; |
| | 572 | defer if (escaped_fragment) |f| req.client.allocator.free(f); |
| | 573 | |
| | 574 | try w.writeAll(@tagName(headers.method)); |
| | 575 | try w.writeByte(' '); |
| | 576 | if (escaped_path.len == 0) { |
| | 577 | try w.writeByte('/'); |
| | 578 | } else { |
| | 579 | try w.writeAll(escaped_path); |
| | 580 | } |
| | 581 | if (escaped_query) |q| { |
| | 582 | try w.writeByte('?'); |
| | 583 | try w.writeAll(q); |
| | 584 | } |
| | 585 | if (escaped_fragment) |f| { |
| | 586 | try w.writeByte('#'); |
| | 587 | try w.writeAll(f); |
| | 588 | } |
| | 589 | try w.writeByte(' '); |
| | 590 | try w.writeAll(@tagName(headers.version)); |
| | 591 | try w.writeAll("\r\nHost: "); |
| | 592 | try w.writeAll(uri.host.?); |
| | 593 | try w.writeAll("\r\nUser-Agent: "); |
| | 594 | try w.writeAll(headers.user_agent); |
| | 595 | if (headers.connection == .close) { |
| | 596 | try w.writeAll("\r\nConnection: close"); |
| | 597 | } else { |
| | 598 | try w.writeAll("\r\nConnection: keep-alive"); |
| | 599 | } |
| | 600 | try w.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); |
| | 601 | try w.writeAll("\r\nTE: trailers, gzip, deflate"); |
| | 602 | |
| | 603 | switch (headers.transfer_encoding) { |
| | 604 | .chunked => try w.writeAll("\r\nTransfer-Encoding: chunked"), |
| | 605 | .content_length => |content_length| try w.print("\r\nContent-Length: {d}", .{content_length}), |
| | 606 | .none => {}, |
| | 607 | } |
| | 608 | |
| | 609 | for (headers.custom) |header| { |
| | 610 | try w.writeAll("\r\n"); |
| | 611 | try w.writeAll(header.name); |
| | 612 | try w.writeAll(": "); |
| | 613 | try w.writeAll(header.value); |
| | 614 | } |
| | 615 | |
| | 616 | try w.writeAll("\r\n\r\n"); |
| | 617 | |
| | 618 | try buffered.flush(); |
| | 619 | } |
| | 620 | |
| | 621 | pub const TransferReadError = proto.HeadersParser.ReadError || error{ReadFailed}; |
| 510 | | 622 | |
| 511 | pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); | 623 | pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); |
| 512 | | 624 | |
| ... | @@ -519,7 +631,10 @@ pub const Request = struct { | ... | @@ -519,7 +631,10 @@ pub const Request = struct { |
| 519 | | 631 | |
| 520 | var index: usize = 0; | 632 | var index: usize = 0; |
| 521 | while (index == 0) { | 633 | while (index == 0) { |
| 522 | const amt = try req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip); | 634 | const amt = req.response.parser.read(&req.connection.data.buffered, buf[index..], req.response.skip) catch |err| { |
| | 635 | req.client.last_error = .{ .read = err }; |
| | 636 | return error.ReadFailed; |
| | 637 | }; |
| 523 | if (amt == 0 and req.response.parser.isComplete()) break; | 638 | if (amt == 0 and req.response.parser.isComplete()) break; |
| 524 | index += amt; | 639 | index += amt; |
| 525 | } | 640 | } |
| ... | @@ -527,78 +642,60 @@ pub const Request = struct { | ... | @@ -527,78 +642,60 @@ pub const Request = struct { |
| 527 | return index; | 642 | return index; |
| 528 | } | 643 | } |
| 529 | | 644 | |
| 530 | pub const WaitForCompleteHeadError = BufferedConnection.ReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize } || error{CompressionNotSupported}; | 645 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; |
| 531 | | | |
| 532 | pub fn waitForCompleteHead(req: *Request) !void { | | |
| 533 | while (true) { | | |
| 534 | try req.connection.data.buffered.fill(); | | |
| 535 | | | |
| 536 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek()); | | |
| 537 | req.connection.data.buffered.clear(@intCast(u16, nchecked)); | | |
| 538 | | | |
| 539 | if (req.response.parser.state.isContent()) break; | | |
| 540 | } | | |
| 541 | | | |
| 542 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); | | |
| 543 | | | |
| 544 | if (req.response.headers.status == .switching_protocols) { | | |
| 545 | req.connection.data.closing = false; | | |
| 546 | req.response.parser.done = true; | | |
| 547 | } | | |
| 548 | | | |
| 549 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { | | |
| 550 | req.connection.data.closing = false; | | |
| 551 | } else { | | |
| 552 | req.connection.data.closing = true; | | |
| 553 | } | | |
| 554 | | 646 | |
| 555 | if (req.response.headers.transfer_encoding) |te| { | 647 | /// Waits for a response from the server and parses any headers that are sent. |
| 556 | switch (te) { | 648 | /// This function will block until the final response is received. |
| 557 | .chunked => { | 649 | /// |
| 558 | req.response.parser.next_chunk_length = 0; | 650 | /// If `handle_redirects` is true, then this function will automatically follow |
| 559 | req.response.parser.state = .chunk_head_size; | 651 | /// redirects. |
| 560 | }, | 652 | pub fn do(req: *Request) DoError!void { |
| | 653 | while (true) { // handle redirects |
| | 654 | while (true) { // read headers |
| | 655 | req.connection.data.buffered.fill() catch |err| { |
| | 656 | req.client.last_error = .{ .read = err }; |
| | 657 | return error.ReadFailed; |
| | 658 | }; |
| | 659 | |
| | 660 | const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.data.buffered.peek()); |
| | 661 | req.connection.data.buffered.clear(@intCast(u16, nchecked)); |
| | 662 | |
| | 663 | if (req.response.parser.state.isContent()) break; |
| 561 | } | 664 | } |
| 562 | } else if (req.response.headers.content_length) |cl| { | | |
| 563 | req.response.parser.next_chunk_length = cl; | | |
| 564 | | | |
| 565 | if (cl == 0) req.response.parser.done = true; | | |
| 566 | } else { | | |
| 567 | req.response.parser.done = true; | | |
| 568 | } | | |
| 569 | | 665 | |
| 570 | if (!req.response.parser.done) { | 666 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); |
| 571 | if (req.response.headers.transfer_compression) |tc| switch (tc) { | | |
| 572 | .compress => return error.CompressionNotSupported, | | |
| 573 | .deflate => req.response.compression = .{ | | |
| 574 | .deflate = try std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()), | | |
| 575 | }, | | |
| 576 | .gzip => req.response.compression = .{ | | |
| 577 | .gzip = try std.compress.gzip.decompress(req.client.allocator, req.transferReader()), | | |
| 578 | }, | | |
| 579 | .zstd => req.response.compression = .{ | | |
| 580 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), | | |
| 581 | }, | | |
| 582 | }; | | |
| 583 | } | | |
| 584 | | 667 | |
| 585 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) req.response.skip = true; | 668 | if (req.response.headers.status == .switching_protocols) { |
| 586 | } | 669 | req.connection.data.closing = false; |
| | 670 | req.response.parser.done = true; |
| | 671 | } |
| 587 | | 672 | |
| 588 | pub const ReadError = RequestError || Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, InvalidFormat, InvalidPort, UnexpectedCharacter }; | 673 | if (req.headers.connection == .keep_alive and req.response.headers.connection == .keep_alive) { |
| | 674 | req.connection.data.closing = false; |
| | 675 | } else { |
| | 676 | req.connection.data.closing = true; |
| | 677 | } |
| 589 | | 678 | |
| 590 | pub const Reader = std.io.Reader(*Request, ReadError, read); | 679 | if (req.response.headers.transfer_encoding) |te| { |
| | 680 | switch (te) { |
| | 681 | .chunked => { |
| | 682 | req.response.parser.next_chunk_length = 0; |
| | 683 | req.response.parser.state = .chunk_head_size; |
| | 684 | }, |
| | 685 | } |
| | 686 | } else if (req.response.headers.content_length) |cl| { |
| | 687 | req.response.parser.next_chunk_length = cl; |
| 591 | | 688 | |
| 592 | pub fn reader(req: *Request) Reader { | 689 | if (cl == 0) req.response.parser.done = true; |
| 593 | return .{ .context = req }; | 690 | } else { |
| 594 | } | 691 | req.response.parser.done = true; |
| | 692 | } |
| 595 | | 693 | |
| 596 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { | 694 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) { |
| 597 | while (true) { | 695 | req.response.skip = true; |
| 598 | if (!req.response.parser.state.isContent()) try req.waitForCompleteHead(); | | |
| 599 | | 696 | |
| 600 | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { | 697 | const empty = @as([*]u8, undefined)[0..0]; |
| 601 | assert(try req.transferRead(buffer) == 0); | 698 | assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary |
| 602 | | 699 | |
| 603 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | 700 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 604 | | 701 | |
| ... | @@ -624,29 +721,80 @@ pub const Request = struct { | ... | @@ -624,29 +721,80 @@ pub const Request = struct { |
| 624 | req.deinit(); | 721 | req.deinit(); |
| 625 | req.* = new_req; | 722 | req.* = new_req; |
| 626 | } else { | 723 | } else { |
| | 724 | req.response.skip = false; |
| | 725 | if (!req.response.parser.done) { |
| | 726 | if (req.response.headers.transfer_compression) |tc| switch (tc) { |
| | 727 | .compress => return error.CompressionNotSupported, |
| | 728 | .deflate => req.response.compression = .{ |
| | 729 | .deflate = std.compress.zlib.zlibStream(req.client.allocator, req.transferReader()) catch |err| { |
| | 730 | req.client.last_error = .{ .zlib_init = err }; |
| | 731 | return error.CompressionInitializationFailed; |
| | 732 | }, |
| | 733 | }, |
| | 734 | .gzip => req.response.compression = .{ |
| | 735 | .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch |err| { |
| | 736 | req.client.last_error = .{ .gzip_init = err }; |
| | 737 | return error.CompressionInitializationFailed; |
| | 738 | }, |
| | 739 | }, |
| | 740 | .zstd => req.response.compression = .{ |
| | 741 | .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), |
| | 742 | }, |
| | 743 | }; |
| | 744 | } |
| | 745 | |
| 627 | break; | 746 | break; |
| 628 | } | 747 | } |
| 629 | } | 748 | } |
| | 749 | } |
| | 750 | |
| | 751 | pub const ReadError = TransferReadError; |
| | 752 | |
| | 753 | pub const Reader = std.io.Reader(*Request, ReadError, read); |
| | 754 | |
| | 755 | pub fn reader(req: *Request) Reader { |
| | 756 | return .{ .context = req }; |
| | 757 | } |
| | 758 | |
| | 759 | /// Reads data from the response body. Must be called after `do`. |
| | 760 | pub fn read(req: *Request, buffer: []u8) ReadError!usize { |
| | 761 | assert(req.response.parser.state.isContent()); |
| 630 | | 762 | |
| 631 | return switch (req.response.compression) { | 763 | return switch (req.response.compression) { |
| 632 | .deflate => |*deflate| try deflate.read(buffer), | 764 | .deflate => |*deflate| deflate.read(buffer) catch |err| { |
| 633 | .gzip => |*gzip| try gzip.read(buffer), | 765 | req.client.last_error = .{ .decompress = err }; |
| 634 | .zstd => |*zstd| try zstd.read(buffer), | 766 | err catch {}; |
| | 767 | return error.ReadFailed; |
| | 768 | }, |
| | 769 | .gzip => |*gzip| gzip.read(buffer) catch |err| { |
| | 770 | req.client.last_error = .{ .decompress = err }; |
| | 771 | err catch {}; |
| | 772 | return error.ReadFailed; |
| | 773 | }, |
| | 774 | .zstd => |*zstd| zstd.read(buffer) catch |err| { |
| | 775 | req.client.last_error = .{ .decompress = err }; |
| | 776 | err catch {}; |
| | 777 | return error.ReadFailed; |
| | 778 | }, |
| 635 | else => try req.transferRead(buffer), | 779 | else => try req.transferRead(buffer), |
| 636 | }; | 780 | }; |
| 637 | } | 781 | } |
| 638 | | 782 | |
| | 783 | /// Reads data from the response body. Must be called after `do`. |
| 639 | pub fn readAll(req: *Request, buffer: []u8) !usize { | 784 | pub fn readAll(req: *Request, buffer: []u8) !usize { |
| 640 | var index: usize = 0; | 785 | var index: usize = 0; |
| 641 | while (index < buffer.len) { | 786 | while (index < buffer.len) { |
| 642 | const amt = try read(req, buffer[index..]); | 787 | const amt = read(req, buffer[index..]) catch |err| { |
| | 788 | req.client.last_error = .{ .read = err }; |
| | 789 | return error.ReadFailed; |
| | 790 | }; |
| 643 | if (amt == 0) break; | 791 | if (amt == 0) break; |
| 644 | index += amt; | 792 | index += amt; |
| 645 | } | 793 | } |
| 646 | return index; | 794 | return index; |
| 647 | } | 795 | } |
| 648 | | 796 | |
| 649 | pub const WriteError = BufferedConnection.WriteError || error{ NotWriteable, MessageTooLong }; | 797 | pub const WriteError = error{ WriteFailed, NotWriteable, MessageTooLong }; |
| 650 | | 798 | |
| 651 | pub const Writer = std.io.Writer(*Request, WriteError, write); | 799 | pub const Writer = std.io.Writer(*Request, WriteError, write); |
| 652 | | 800 | |
| ... | @@ -658,16 +806,28 @@ pub const Request = struct { | ... | @@ -658,16 +806,28 @@ pub const Request = struct { |
| 658 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { | 806 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { |
| 659 | switch (req.headers.transfer_encoding) { | 807 | switch (req.headers.transfer_encoding) { |
| 660 | .chunked => { | 808 | .chunked => { |
| 661 | try req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}); | 809 | req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}) catch |err| { |
| 662 | try req.connection.data.conn.writeAll(bytes); | 810 | req.client.last_error = .{ .write = err }; |
| 663 | try req.connection.data.conn.writeAll("\r\n"); | 811 | return error.WriteFailed; |
| | 812 | }; |
| | 813 | req.connection.data.conn.writeAll(bytes) catch |err| { |
| | 814 | req.client.last_error = .{ .write = err }; |
| | 815 | return error.WriteFailed; |
| | 816 | }; |
| | 817 | req.connection.data.conn.writeAll("\r\n") catch |err| { |
| | 818 | req.client.last_error = .{ .write = err }; |
| | 819 | return error.WriteFailed; |
| | 820 | }; |
| 664 | | 821 | |
| 665 | return bytes.len; | 822 | return bytes.len; |
| 666 | }, | 823 | }, |
| 667 | .content_length => |*len| { | 824 | .content_length => |*len| { |
| 668 | if (len.* < bytes.len) return error.MessageTooLong; | 825 | if (len.* < bytes.len) return error.MessageTooLong; |
| 669 | | 826 | |
| 670 | const amt = try req.connection.data.conn.write(bytes); | 827 | const amt = req.connection.data.conn.write(bytes) catch |err| { |
| | 828 | req.client.last_error = .{ .write = err }; |
| | 829 | return error.WriteFailed; |
| | 830 | }; |
| 671 | len.* -= amt; | 831 | len.* -= amt; |
| 672 | return amt; | 832 | return amt; |
| 673 | }, | 833 | }, |
| ... | @@ -678,7 +838,10 @@ pub const Request = struct { | ... | @@ -678,7 +838,10 @@ pub const Request = struct { |
| 678 | /// Finish the body of a request. This notifies the server that you have no more data to send. | 838 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 679 | pub fn finish(req: *Request) !void { | 839 | pub fn finish(req: *Request) !void { |
| 680 | switch (req.headers.transfer_encoding) { | 840 | switch (req.headers.transfer_encoding) { |
| 681 | .chunked => try req.connection.data.conn.writeAll("0\r\n"), | 841 | .chunked => req.connection.data.conn.writeAll("0\r\n") catch |err| { |
| | 842 | req.client.last_error = .{ .write = err }; |
| | 843 | return error.WriteFailed; |
| | 844 | }, |
| 682 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, | 845 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 683 | .none => {}, | 846 | .none => {}, |
| 684 | } | 847 | } |
| ... | @@ -692,7 +855,7 @@ pub fn deinit(client: *Client) void { | ... | @@ -692,7 +855,7 @@ pub fn deinit(client: *Client) void { |
| 692 | client.* = undefined; | 855 | client.* = undefined; |
| 693 | } | 856 | } |
| 694 | | 857 | |
| 695 | pub const ConnectError = Allocator.Error || net.TcpConnectToHostError || std.crypto.tls.Client.InitError(net.Stream); | 858 | pub const ConnectError = Allocator.Error || error{ ConnectionFailed, TlsInitializationFailed }; |
| 696 | | 859 | |
| 697 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { | 860 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 698 | if (client.connection_pool.findConnection(.{ | 861 | if (client.connection_pool.findConnection(.{ |
| ... | @@ -706,7 +869,11 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio | ... | @@ -706,7 +869,11 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 706 | errdefer client.allocator.destroy(conn); | 869 | errdefer client.allocator.destroy(conn); |
| 707 | conn.* = .{ .data = undefined }; | 870 | conn.* = .{ .data = undefined }; |
| 708 | | 871 | |
| 709 | const stream = try net.tcpConnectToHost(client.allocator, host, port); | 872 | const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| { |
| | 873 | client.last_error = .{ .connect = err }; |
| | 874 | return error.ConnectionFailed; |
| | 875 | }; |
| | 876 | errdefer stream.close(); |
| 710 | | 877 | |
| 711 | conn.data = .{ | 878 | conn.data = .{ |
| 712 | .buffered = .{ .conn = .{ | 879 | .buffered = .{ .conn = .{ |
| ... | @@ -717,12 +884,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio | ... | @@ -717,12 +884,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 717 | .host = try client.allocator.dupe(u8, host), | 884 | .host = try client.allocator.dupe(u8, host), |
| 718 | .port = port, | 885 | .port = port, |
| 719 | }; | 886 | }; |
| | 887 | errdefer client.allocator.free(conn.data.host); |
| 720 | | 888 | |
| 721 | switch (protocol) { | 889 | switch (protocol) { |
| 722 | .plain => {}, | 890 | .plain => {}, |
| 723 | .tls => { | 891 | .tls => { |
| 724 | conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client); | 892 | conn.data.buffered.conn.tls_client = try client.allocator.create(std.crypto.tls.Client); |
| 725 | conn.data.buffered.conn.tls_client.* = try std.crypto.tls.Client.init(stream, client.ca_bundle, host); | 893 | errdefer client.allocator.destroy(conn.data.buffered.conn.tls_client); |
| | 894 | |
| | 895 | conn.data.buffered.conn.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch |err| { |
| | 896 | client.last_error = .{ .tls = err }; |
| | 897 | return error.TlsInitializationFailed; |
| | 898 | }; |
| 726 | // This is appropriate for HTTPS because the HTTP headers contain | 899 | // This is appropriate for HTTPS because the HTTP headers contain |
| 727 | // the content length which is used to detect truncation attacks. | 900 | // the content length which is used to detect truncation attacks. |
| 728 | conn.data.buffered.conn.tls_client.allow_truncation_attacks = true; | 901 | conn.data.buffered.conn.tls_client.allow_truncation_attacks = true; |
| ... | @@ -734,15 +907,12 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio | ... | @@ -734,15 +907,12 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 734 | return conn; | 907 | return conn; |
| 735 | } | 908 | } |
| 736 | | 909 | |
| 737 | pub const RequestError = ConnectError || BufferedConnection.WriteError || error{ | 910 | pub const RequestError = ConnectError || error{ |
| 738 | UnsupportedUrlScheme, | 911 | UnsupportedUrlScheme, |
| 739 | UriMissingHost, | 912 | UriMissingHost, |
| 740 | | 913 | |
| 741 | CertificateAuthorityBundleTooBig, | 914 | CertificateAuthorityBundleFailed, |
| 742 | InvalidPadding, | 915 | WriteFailed, |
| 743 | MissingEndCertificateMarker, | | |
| 744 | Unseekable, | | |
| 745 | EndOfStream, | | |
| 746 | }; | 916 | }; |
| 747 | | 917 | |
| 748 | pub const Options = struct { | 918 | pub const Options = struct { |
| ... | @@ -764,13 +934,15 @@ pub const Options = struct { | ... | @@ -764,13 +934,15 @@ pub const Options = struct { |
| 764 | }; | 934 | }; |
| 765 | }; | 935 | }; |
| 766 | | 936 | |
| | 937 | pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ |
| | 938 | .{ "http", .plain }, |
| | 939 | .{ "ws", .plain }, |
| | 940 | .{ "https", .tls }, |
| | 941 | .{ "wss", .tls }, |
| | 942 | }); |
| | 943 | |
| 767 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { | 944 | pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Options) RequestError!Request { |
| 768 | const protocol: Connection.Protocol = if (mem.eql(u8, uri.scheme, "http")) | 945 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 769 | .plain | | |
| 770 | else if (mem.eql(u8, uri.scheme, "https")) | | |
| 771 | .tls | | |
| 772 | else | | |
| 773 | return error.UnsupportedUrlScheme; | | |
| 774 | | 946 | |
| 775 | const port: u16 = uri.port orelse switch (protocol) { | 947 | const port: u16 = uri.port orelse switch (protocol) { |
| 776 | .plain => 80, | 948 | .plain => 80, |
| ... | @@ -779,13 +951,16 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt | ... | @@ -779,13 +951,16 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 779 | | 951 | |
| 780 | const host = uri.host orelse return error.UriMissingHost; | 952 | const host = uri.host orelse return error.UriMissingHost; |
| 781 | | 953 | |
| 782 | if (client.next_https_rescan_certs and protocol == .tls) { | 954 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { |
| 783 | client.connection_pool.mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex. | 955 | client.ca_bundle_mutex.lock(); |
| 784 | defer client.connection_pool.mutex.unlock(); | 956 | defer client.ca_bundle_mutex.unlock(); |
| 785 | | 957 | |
| 786 | if (client.next_https_rescan_certs) { | 958 | if (client.next_https_rescan_certs) { |
| 787 | try client.ca_bundle.rescan(client.allocator); | 959 | client.ca_bundle.rescan(client.allocator) catch |err| { |
| 788 | client.next_https_rescan_certs = false; | 960 | client.last_error = .{ .ca_bundle = err }; |
| | 961 | return error.CertificateAuthorityBundleFailed; |
| | 962 | }; |
| | 963 | @atomicStore(bool, &client.next_https_rescan_certs, false, .Release); |
| 789 | } | 964 | } |
| 790 | } | 965 | } |
| 791 | | 966 | |
| ... | @@ -804,68 +979,17 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt | ... | @@ -804,68 +979,17 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 804 | }, | 979 | }, |
| 805 | .arena = undefined, | 980 | .arena = undefined, |
| 806 | }; | 981 | }; |
| | 982 | errdefer req.deinit(); |
| 807 | | 983 | |
| 808 | req.arena = std.heap.ArenaAllocator.init(client.allocator); | 984 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 809 | | 985 | |
| 810 | { | 986 | req.start(uri, headers) catch |err| { |
| 811 | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); | 987 | if (err == error.OutOfMemory) return error.OutOfMemory; |
| 812 | const writer = buffered.writer(); | 988 | const err_casted = @errSetCast(BufferedConnection.WriteError, err); |
| 813 | | | |
| 814 | const escaped_path = try Uri.escapePath(client.allocator, uri.path); | | |
| 815 | defer client.allocator.free(escaped_path); | | |
| 816 | | | |
| 817 | const escaped_query = if (uri.query) |q| try Uri.escapeQuery(client.allocator, q) else null; | | |
| 818 | defer if (escaped_query) |q| client.allocator.free(q); | | |
| 819 | | 989 | |
| 820 | const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(client.allocator, f) else null; | 990 | client.last_error = .{ .write = err_casted }; |
| 821 | defer if (escaped_fragment) |f| client.allocator.free(f); | 991 | return error.WriteFailed; |
| 822 | | 992 | }; |
| 823 | try writer.writeAll(@tagName(headers.method)); | | |
| 824 | try writer.writeByte(' '); | | |
| 825 | if (escaped_path.len == 0) { | | |
| 826 | try writer.writeByte('/'); | | |
| 827 | } else { | | |
| 828 | try writer.writeAll(escaped_path); | | |
| 829 | } | | |
| 830 | if (escaped_query) |q| { | | |
| 831 | try writer.writeByte('?'); | | |
| 832 | try writer.writeAll(q); | | |
| 833 | } | | |
| 834 | if (escaped_fragment) |f| { | | |
| 835 | try writer.writeByte('#'); | | |
| 836 | try writer.writeAll(f); | | |
| 837 | } | | |
| 838 | try writer.writeByte(' '); | | |
| 839 | try writer.writeAll(@tagName(headers.version)); | | |
| 840 | try writer.writeAll("\r\nHost: "); | | |
| 841 | try writer.writeAll(host); | | |
| 842 | try writer.writeAll("\r\nUser-Agent: "); | | |
| 843 | try writer.writeAll(headers.user_agent); | | |
| 844 | if (headers.connection == .close) { | | |
| 845 | try writer.writeAll("\r\nConnection: close"); | | |
| 846 | } else { | | |
| 847 | try writer.writeAll("\r\nConnection: keep-alive"); | | |
| 848 | } | | |
| 849 | try writer.writeAll("\r\nAccept-Encoding: gzip, deflate, zstd"); | | |
| 850 | try writer.writeAll("\r\nTE: trailers, gzip, deflate"); | | |
| 851 | | | |
| 852 | switch (headers.transfer_encoding) { | | |
| 853 | .chunked => try writer.writeAll("\r\nTransfer-Encoding: chunked"), | | |
| 854 | .content_length => |content_length| try writer.print("\r\nContent-Length: {d}", .{content_length}), | | |
| 855 | .none => {}, | | |
| 856 | } | | |
| 857 | | | |
| 858 | for (headers.custom) |header| { | | |
| 859 | try writer.writeAll("\r\n"); | | |
| 860 | try writer.writeAll(header.name); | | |
| 861 | try writer.writeAll(": "); | | |
| 862 | try writer.writeAll(header.value); | | |
| 863 | } | | |
| 864 | | | |
| 865 | try writer.writeAll("\r\n\r\n"); | | |
| 866 | | | |
| 867 | try buffered.flush(); | | |
| 868 | } | | |
| 869 | | 993 | |
| 870 | return req; | 994 | return req; |
| 871 | } | 995 | } |
| ... | @@ -880,5 +1004,5 @@ test { | ... | @@ -880,5 +1004,5 @@ test { |
| 880 | | 1004 | |
| 881 | if (builtin.os.tag == .wasi) return error.SkipZigTest; | 1005 | if (builtin.os.tag == .wasi) return error.SkipZigTest; |
| 882 | | 1006 | |
| 883 | _ = Request; | 1007 | std.testing.refAllDecls(@This()); |
| 884 | } | 1008 | } |