| ... | ... | @@ -19,12 +19,55 @@ pub const connection_pool_size = std.options.http_connection_pool_size; |
| 19 | 19 | /// managed buffer is not provided. |
| 20 | 20 | allocator: Allocator, |
| 21 | 21 | ca_bundle: std.crypto.Certificate.Bundle = .{}, |
| 22 | ca_bundle_mutex: std.Thread.Mutex = .{}, |
| 22 | 23 | /// When this is `true`, the next time this client performs an HTTPS request, |
| 23 | 24 | /// it will first rescan the system for root certificates. |
| 24 | 25 | next_https_rescan_certs: bool = true, |
| 25 | 26 | |
| 26 | 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 | 71 | pub const ConnectionPool = struct { |
| 29 | 72 | pub const Criteria = struct { |
| 30 | 73 | host: []const u8, |
| ... | ... | @@ -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 | 192 | pub const Connection = struct { |
| 154 | 193 | stream: net.Stream, |
| 155 | 194 | /// undefined unless protocol is tls. |
| ... | ... | @@ -312,6 +351,10 @@ pub const RequestTransfer = union(enum) { |
| 312 | 351 | }; |
| 313 | 352 | |
| 314 | 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 | 358 | deflate: DeflateDecompressor, |
| 316 | 359 | gzip: GzipDecompressor, |
| 317 | 360 | zstd: ZstdDecompressor, |
| ... | ... | @@ -336,10 +379,11 @@ pub const Response = struct { |
| 336 | 379 | HttpHeaderContinuationsUnsupported, |
| 337 | 380 | HttpTransferEncodingUnsupported, |
| 338 | 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 | 387 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| 344 | 388 | |
| 345 | 389 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| ... | ... | @@ -374,7 +418,7 @@ pub const Response = struct { |
| 374 | 418 | headers.location = header_value; |
| 375 | 419 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 376 | 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 | 422 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { |
| 379 | 423 | // Transfer-Encoding: second, first |
| 380 | 424 | // Transfer-Encoding: deflate, chunked |
| ... | ... | @@ -457,6 +501,14 @@ pub const Response = struct { |
| 457 | 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 | 512 | pub const Request = struct { |
| 461 | 513 | pub const Headers = struct { |
| 462 | 514 | version: http.Version = .@"HTTP/1.1", |
| ... | ... | @@ -506,7 +558,67 @@ pub const Request = struct { |
| 506 | 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 | 623 | pub const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); |
| 512 | 624 | |
| ... | ... | @@ -519,7 +631,10 @@ pub const Request = struct { |
| 519 | 631 | |
| 520 | 632 | var index: usize = 0; |
| 521 | 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 | 638 | if (amt == 0 and req.response.parser.isComplete()) break; |
| 524 | 639 | index += amt; |
| 525 | 640 | } |
| ... | ... | @@ -527,78 +642,60 @@ pub const Request = struct { |
| 527 | 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}; |
| 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 | | } |
| 645 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.Headers.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed }; |
| 554 | 646 | |
| 555 | | if (req.response.headers.transfer_encoding) |te| { |
| 556 | | switch (te) { |
| 557 | | .chunked => { |
| 558 | | req.response.parser.next_chunk_length = 0; |
| 559 | | req.response.parser.state = .chunk_head_size; |
| 560 | | }, |
| 647 | /// Waits for a response from the server and parses any headers that are sent. |
| 648 | /// This function will block until the final response is received. |
| 649 | /// |
| 650 | /// If `handle_redirects` is true, then this function will automatically follow |
| 651 | /// redirects. |
| 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) { |
| 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 | | } |
| 666 | req.response.headers = try Response.Headers.parse(req.response.parser.header_bytes.items); |
| 584 | 667 | |
| 585 | | if (req.response.headers.status.class() == .redirect and req.handle_redirects) req.response.skip = true; |
| 586 | | } |
| 668 | if (req.response.headers.status == .switching_protocols) { |
| 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 { |
| 593 | | return .{ .context = req }; |
| 594 | | } |
| 689 | if (cl == 0) req.response.parser.done = true; |
| 690 | } else { |
| 691 | req.response.parser.done = true; |
| 692 | } |
| 595 | 693 | |
| 596 | | pub fn read(req: *Request, buffer: []u8) ReadError!usize { |
| 597 | | while (true) { |
| 598 | | if (!req.response.parser.state.isContent()) try req.waitForCompleteHead(); |
| 694 | if (req.response.headers.status.class() == .redirect and req.handle_redirects) { |
| 695 | req.response.skip = true; |
| 599 | 696 | |
| 600 | | if (req.handle_redirects and req.response.headers.status.class() == .redirect) { |
| 601 | | assert(try req.transferRead(buffer) == 0); |
| 697 | const empty = @as([*]u8, undefined)[0..0]; |
| 698 | assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary |
| 602 | 699 | |
| 603 | 700 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; |
| 604 | 701 | |
| ... | ... | @@ -624,29 +721,80 @@ pub const Request = struct { |
| 624 | 721 | req.deinit(); |
| 625 | 722 | req.* = new_req; |
| 626 | 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 | 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 | 763 | return switch (req.response.compression) { |
| 632 | | .deflate => |*deflate| try deflate.read(buffer), |
| 633 | | .gzip => |*gzip| try gzip.read(buffer), |
| 634 | | .zstd => |*zstd| try zstd.read(buffer), |
| 764 | .deflate => |*deflate| deflate.read(buffer) catch |err| { |
| 765 | req.client.last_error = .{ .decompress = err }; |
| 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 | 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 | 784 | pub fn readAll(req: *Request, buffer: []u8) !usize { |
| 640 | 785 | var index: usize = 0; |
| 641 | 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 | 791 | if (amt == 0) break; |
| 644 | 792 | index += amt; |
| 645 | 793 | } |
| 646 | 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 | 799 | pub const Writer = std.io.Writer(*Request, WriteError, write); |
| 652 | 800 | |
| ... | ... | @@ -658,16 +806,28 @@ pub const Request = struct { |
| 658 | 806 | pub fn write(req: *Request, bytes: []const u8) WriteError!usize { |
| 659 | 807 | switch (req.headers.transfer_encoding) { |
| 660 | 808 | .chunked => { |
| 661 | | try req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}); |
| 662 | | try req.connection.data.conn.writeAll(bytes); |
| 663 | | try req.connection.data.conn.writeAll("\r\n"); |
| 809 | req.connection.data.conn.writer().print("{x}\r\n", .{bytes.len}) catch |err| { |
| 810 | req.client.last_error = .{ .write = err }; |
| 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 | 822 | return bytes.len; |
| 666 | 823 | }, |
| 667 | 824 | .content_length => |*len| { |
| 668 | 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 | 831 | len.* -= amt; |
| 672 | 832 | return amt; |
| 673 | 833 | }, |
| ... | ... | @@ -678,7 +838,10 @@ pub const Request = struct { |
| 678 | 838 | /// Finish the body of a request. This notifies the server that you have no more data to send. |
| 679 | 839 | pub fn finish(req: *Request) !void { |
| 680 | 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 | 845 | .content_length => |len| if (len != 0) return error.MessageNotCompleted, |
| 683 | 846 | .none => {}, |
| 684 | 847 | } |
| ... | ... | @@ -692,7 +855,7 @@ pub fn deinit(client: *Client) void { |
| 692 | 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 | 860 | pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*ConnectionPool.Node { |
| 698 | 861 | if (client.connection_pool.findConnection(.{ |
| ... | ... | @@ -706,7 +869,11 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 706 | 869 | errdefer client.allocator.destroy(conn); |
| 707 | 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 | 878 | conn.data = .{ |
| 712 | 879 | .buffered = .{ .conn = .{ |
| ... | ... | @@ -717,12 +884,18 @@ pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connectio |
| 717 | 884 | .host = try client.allocator.dupe(u8, host), |
| 718 | 885 | .port = port, |
| 719 | 886 | }; |
| 887 | errdefer client.allocator.free(conn.data.host); |
| 720 | 888 | |
| 721 | 889 | switch (protocol) { |
| 722 | 890 | .plain => {}, |
| 723 | 891 | .tls => { |
| 724 | 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 | 899 | // This is appropriate for HTTPS because the HTTP headers contain |
| 727 | 900 | // the content length which is used to detect truncation attacks. |
| 728 | 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 | 907 | return conn; |
| 735 | 908 | } |
| 736 | 909 | |
| 737 | | pub const RequestError = ConnectError || BufferedConnection.WriteError || error{ |
| 910 | pub const RequestError = ConnectError || error{ |
| 738 | 911 | UnsupportedUrlScheme, |
| 739 | 912 | UriMissingHost, |
| 740 | 913 | |
| 741 | | CertificateAuthorityBundleTooBig, |
| 742 | | InvalidPadding, |
| 743 | | MissingEndCertificateMarker, |
| 744 | | Unseekable, |
| 745 | | EndOfStream, |
| 914 | CertificateAuthorityBundleFailed, |
| 915 | WriteFailed, |
| 746 | 916 | }; |
| 747 | 917 | |
| 748 | 918 | 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 | 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")) |
| 769 | | .plain |
| 770 | | else if (mem.eql(u8, uri.scheme, "https")) |
| 771 | | .tls |
| 772 | | else |
| 773 | | return error.UnsupportedUrlScheme; |
| 945 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| 774 | 946 | |
| 775 | 947 | const port: u16 = uri.port orelse switch (protocol) { |
| 776 | 948 | .plain => 80, |
| ... | ... | @@ -779,13 +951,16 @@ pub fn request(client: *Client, uri: Uri, headers: Request.Headers, options: Opt |
| 779 | 951 | |
| 780 | 952 | const host = uri.host orelse return error.UriMissingHost; |
| 781 | 953 | |
| 782 | | if (client.next_https_rescan_certs and protocol == .tls) { |
| 783 | | client.connection_pool.mutex.lock(); // TODO: this could be so much better than reusing the connection pool mutex. |
| 784 | | defer client.connection_pool.mutex.unlock(); |
| 954 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { |
| 955 | client.ca_bundle_mutex.lock(); |
| 956 | defer client.ca_bundle_mutex.unlock(); |
| 785 | 957 | |
| 786 | 958 | if (client.next_https_rescan_certs) { |
| 787 | | try client.ca_bundle.rescan(client.allocator); |
| 788 | | client.next_https_rescan_certs = false; |
| 959 | client.ca_bundle.rescan(client.allocator) catch |err| { |
| 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 | 979 | }, |
| 805 | 980 | .arena = undefined, |
| 806 | 981 | }; |
| 982 | errdefer req.deinit(); |
| 807 | 983 | |
| 808 | 984 | req.arena = std.heap.ArenaAllocator.init(client.allocator); |
| 809 | 985 | |
| 810 | | { |
| 811 | | var buffered = std.io.bufferedWriter(req.connection.data.buffered.writer()); |
| 812 | | const writer = buffered.writer(); |
| 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); |
| 986 | req.start(uri, headers) catch |err| { |
| 987 | if (err == error.OutOfMemory) return error.OutOfMemory; |
| 988 | const err_casted = @errSetCast(BufferedConnection.WriteError, err); |
| 819 | 989 | |
| 820 | | const escaped_fragment = if (uri.fragment) |f| try Uri.escapeQuery(client.allocator, f) else null; |
| 821 | | defer if (escaped_fragment) |f| client.allocator.free(f); |
| 822 | | |
| 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 | | } |
| 990 | client.last_error = .{ .write = err_casted }; |
| 991 | return error.WriteFailed; |
| 992 | }; |
| 869 | 993 | |
| 870 | 994 | return req; |
| 871 | 995 | } |
| ... | ... | @@ -880,5 +1004,5 @@ test { |
| 880 | 1004 | |
| 881 | 1005 | if (builtin.os.tag == .wasi) return error.SkipZigTest; |
| 882 | 1006 | |
| 883 | | _ = Request; |
| 1007 | std.testing.refAllDecls(@This()); |
| 884 | 1008 | } |