| author | |
| committer | |
| log | 02908a2d8c0376fa2f9b793ac22d648632fde735 |
| tree | 2f8d0a8ed2350ad53f5eca5c901e21d28ac8d353 |
| parent | 8843631f7ecfe0a7756a2f1f0bc99a24b57c2fcc |
7 files changed, 2423 insertions(+), 2808 deletions(-)
lib/std/http.zig+766-15| ... | ... | @@ -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", |
| ... | ... | @@ -42,7 +42,7 @@ pub const Method = enum(u64) { |
| 42 | 42 | return x; |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | pub fn format(self: Method, w: *std.io.Writer) std.io.Writer.Error!void { | |
| 45 | pub fn format(self: Method, w: *Writer) Writer.Error!void { | |
| 46 | 46 | const bytes: []const u8 = @ptrCast(&@intFromEnum(self)); |
| 47 | 47 | const str = std.mem.sliceTo(bytes, 0); |
| 48 | 48 | try w.writeAll(str); |
| ... | ... | @@ -296,13 +296,24 @@ pub const TransferEncoding = enum { |
| 296 | 296 | }; |
| 297 | 297 | |
| 298 | 298 | pub const ContentEncoding = enum { |
| 299 | identity, | |
| 300 | compress, | |
| 301 | @"x-compress", | |
| 302 | deflate, | |
| 303 | gzip, | |
| 304 | @"x-gzip", | |
| 305 | 299 | zstd, |
| 300 | gzip, | |
| 301 | deflate, | |
| 302 | compress, | |
| 303 | identity, | |
| 304 | ||
| 305 | pub fn fromString(s: []const u8) ?ContentEncoding { | |
| 306 | const map = std.StaticStringMap(ContentEncoding).initComptime(.{ | |
| 307 | .{ "zstd", .zstd }, | |
| 308 | .{ "gzip", .gzip }, | |
| 309 | .{ "x-gzip", .gzip }, | |
| 310 | .{ "deflate", .deflate }, | |
| 311 | .{ "compress", .compress }, | |
| 312 | .{ "x-compress", .compress }, | |
| 313 | .{ "identity", .identity }, | |
| 314 | }); | |
| 315 | return map.get(s); | |
| 316 | } | |
| 306 | 317 | }; |
| 307 | 318 | |
| 308 | 319 | pub const Connection = enum { |
| ... | ... | @@ -315,15 +326,755 @@ pub const Header = struct { |
| 315 | 326 | value: []const u8, |
| 316 | 327 | }; |
| 317 | 328 | |
| 329 | pub const Reader = struct { | |
| 330 | in: *std.Io.Reader, | |
| 331 | /// This is preallocated memory that might be used by `bodyReader`. That | |
| 332 | /// function might return a pointer to this field, or a different | |
| 333 | /// `*std.Io.Reader`. Advisable to not access this field directly. | |
| 334 | interface: std.Io.Reader, | |
| 335 | /// Keeps track of whether the stream is ready to accept a new request, | |
| 336 | /// making invalid API usage cause assertion failures rather than HTTP | |
| 337 | /// protocol violations. | |
| 338 | state: State, | |
| 339 | /// HTTP trailer bytes. These are at the end of a transfer-encoding: | |
| 340 | /// chunked message. This data is available only after calling one of the | |
| 341 | /// "end" functions and points to data inside the buffer of `in`, and is | |
| 342 | /// therefore invalidated on the next call to `receiveHead`, or any other | |
| 343 | /// read from `in`. | |
| 344 | trailers: []const u8 = &.{}, | |
| 345 | body_err: ?BodyError = null, | |
| 346 | /// Stolen from `in`. | |
| 347 | head_buffer: []u8 = &.{}, | |
| 348 | ||
| 349 | pub const max_chunk_header_len = 22; | |
| 350 | ||
| 351 | pub const RemainingChunkLen = enum(u64) { | |
| 352 | head = 0, | |
| 353 | n = 1, | |
| 354 | rn = 2, | |
| 355 | _, | |
| 356 | ||
| 357 | pub fn init(integer: u64) RemainingChunkLen { | |
| 358 | return @enumFromInt(integer); | |
| 359 | } | |
| 360 | ||
| 361 | pub fn int(rcl: RemainingChunkLen) u64 { | |
| 362 | return @intFromEnum(rcl); | |
| 363 | } | |
| 364 | }; | |
| 365 | ||
| 366 | pub const State = union(enum) { | |
| 367 | /// The stream is available to be used for the first time, or reused. | |
| 368 | ready, | |
| 369 | received_head, | |
| 370 | /// The stream goes until the connection is closed. | |
| 371 | body_none, | |
| 372 | body_remaining_content_length: u64, | |
| 373 | body_remaining_chunk_len: RemainingChunkLen, | |
| 374 | /// The stream would be eligible for another HTTP request, however the | |
| 375 | /// client and server did not negotiate a persistent connection. | |
| 376 | closing, | |
| 377 | }; | |
| 378 | ||
| 379 | pub const BodyError = error{ | |
| 380 | HttpChunkInvalid, | |
| 381 | HttpChunkTruncated, | |
| 382 | HttpHeadersOversize, | |
| 383 | }; | |
| 384 | ||
| 385 | pub const HeadError = error{ | |
| 386 | /// Too many bytes of HTTP headers. | |
| 387 | /// | |
| 388 | /// The HTTP specification suggests to respond with a 431 status code | |
| 389 | /// before closing the connection. | |
| 390 | HttpHeadersOversize, | |
| 391 | /// Partial HTTP request was received but the connection was closed | |
| 392 | /// before fully receiving the headers. | |
| 393 | HttpRequestTruncated, | |
| 394 | /// The client sent 0 bytes of headers before closing the stream. This | |
| 395 | /// happens when a keep-alive connection is finally closed. | |
| 396 | HttpConnectionClosing, | |
| 397 | /// Transitive error occurred reading from `in`. | |
| 398 | ReadFailed, | |
| 399 | }; | |
| 400 | ||
| 401 | pub fn restituteHeadBuffer(reader: *Reader) void { | |
| 402 | reader.in.restitute(reader.head_buffer.len); | |
| 403 | reader.head_buffer.len = 0; | |
| 404 | } | |
| 405 | ||
| 406 | /// Buffers the entire head into `head_buffer`, invalidating the previous | |
| 407 | /// `head_buffer`, if any. | |
| 408 | pub fn receiveHead(reader: *Reader) HeadError!void { | |
| 409 | reader.trailers = &.{}; | |
| 410 | const in = reader.in; | |
| 411 | in.restitute(reader.head_buffer.len); | |
| 412 | reader.head_buffer.len = 0; | |
| 413 | in.rebase(); | |
| 414 | var hp: HeadParser = .{}; | |
| 415 | var head_end: usize = 0; | |
| 416 | while (true) { | |
| 417 | if (head_end >= in.buffer.len) return error.HttpHeadersOversize; | |
| 418 | in.fillMore() catch |err| switch (err) { | |
| 419 | error.EndOfStream => switch (head_end) { | |
| 420 | 0 => return error.HttpConnectionClosing, | |
| 421 | else => return error.HttpRequestTruncated, | |
| 422 | }, | |
| 423 | error.ReadFailed => return error.ReadFailed, | |
| 424 | }; | |
| 425 | head_end += hp.feed(in.buffered()[head_end..]); | |
| 426 | if (hp.state == .finished) { | |
| 427 | reader.head_buffer = in.steal(head_end); | |
| 428 | reader.state = .received_head; | |
| 429 | return; | |
| 430 | } | |
| 431 | } | |
| 432 | } | |
| 433 | ||
| 434 | /// If compressed body has been negotiated this will return compressed bytes. | |
| 435 | /// | |
| 436 | /// Asserts only called once and after `receiveHead`. | |
| 437 | /// | |
| 438 | /// See also: | |
| 439 | /// * `interfaceDecompressing` | |
| 440 | pub fn bodyReader( | |
| 441 | reader: *Reader, | |
| 442 | buffer: []u8, | |
| 443 | transfer_encoding: TransferEncoding, | |
| 444 | content_length: ?u64, | |
| 445 | ) *std.Io.Reader { | |
| 446 | assert(reader.state == .received_head); | |
| 447 | switch (transfer_encoding) { | |
| 448 | .chunked => { | |
| 449 | reader.state = .{ .body_remaining_chunk_len = .head }; | |
| 450 | reader.interface = .{ | |
| 451 | .buffer = buffer, | |
| 452 | .seek = 0, | |
| 453 | .end = 0, | |
| 454 | .vtable = &.{ | |
| 455 | .stream = chunkedStream, | |
| 456 | .discard = chunkedDiscard, | |
| 457 | }, | |
| 458 | }; | |
| 459 | return &reader.interface; | |
| 460 | }, | |
| 461 | .none => { | |
| 462 | if (content_length) |len| { | |
| 463 | reader.state = .{ .body_remaining_content_length = len }; | |
| 464 | reader.interface = .{ | |
| 465 | .buffer = buffer, | |
| 466 | .seek = 0, | |
| 467 | .end = 0, | |
| 468 | .vtable = &.{ | |
| 469 | .stream = contentLengthStream, | |
| 470 | .discard = contentLengthDiscard, | |
| 471 | }, | |
| 472 | }; | |
| 473 | return &reader.interface; | |
| 474 | } else { | |
| 475 | reader.state = .body_none; | |
| 476 | return reader.in; | |
| 477 | } | |
| 478 | }, | |
| 479 | } | |
| 480 | } | |
| 481 | ||
| 482 | /// If compressed body has been negotiated this will return decompressed bytes. | |
| 483 | /// | |
| 484 | /// Asserts only called once and after `receiveHead`. | |
| 485 | /// | |
| 486 | /// See also: | |
| 487 | /// * `interface` | |
| 488 | pub fn bodyReaderDecompressing( | |
| 489 | reader: *Reader, | |
| 490 | transfer_encoding: TransferEncoding, | |
| 491 | content_length: ?u64, | |
| 492 | content_encoding: ContentEncoding, | |
| 493 | decompressor: *Decompressor, | |
| 494 | decompression_buffer: []u8, | |
| 495 | ) *std.Io.Reader { | |
| 496 | if (transfer_encoding == .none and content_length == null) { | |
| 497 | assert(reader.state == .received_head); | |
| 498 | reader.state = .body_none; | |
| 499 | switch (content_encoding) { | |
| 500 | .identity => { | |
| 501 | return reader.in; | |
| 502 | }, | |
| 503 | .deflate => { | |
| 504 | decompressor.* = .{ .flate = .init(reader.in, .raw, decompression_buffer) }; | |
| 505 | return &decompressor.flate.reader; | |
| 506 | }, | |
| 507 | .gzip => { | |
| 508 | decompressor.* = .{ .flate = .init(reader.in, .gzip, decompression_buffer) }; | |
| 509 | return &decompressor.flate.reader; | |
| 510 | }, | |
| 511 | .zstd => { | |
| 512 | decompressor.* = .{ .zstd = .init(reader.in, decompression_buffer, .{ .verify_checksum = false }) }; | |
| 513 | return &decompressor.zstd.reader; | |
| 514 | }, | |
| 515 | .compress => unreachable, | |
| 516 | } | |
| 517 | } | |
| 518 | const transfer_reader = bodyReader(reader, &.{}, transfer_encoding, content_length); | |
| 519 | return decompressor.init(transfer_reader, decompression_buffer, content_encoding); | |
| 520 | } | |
| 521 | ||
| 522 | fn contentLengthStream( | |
| 523 | io_r: *std.Io.Reader, | |
| 524 | w: *Writer, | |
| 525 | limit: std.Io.Limit, | |
| 526 | ) std.Io.Reader.StreamError!usize { | |
| 527 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 528 | const remaining_content_length = &reader.state.body_remaining_content_length; | |
| 529 | const remaining = remaining_content_length.*; | |
| 530 | if (remaining == 0) { | |
| 531 | reader.state = .ready; | |
| 532 | return error.EndOfStream; | |
| 533 | } | |
| 534 | const n = try reader.in.stream(w, limit.min(.limited(remaining))); | |
| 535 | remaining_content_length.* = remaining - n; | |
| 536 | return n; | |
| 537 | } | |
| 538 | ||
| 539 | fn contentLengthDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { | |
| 540 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 541 | const remaining_content_length = &reader.state.body_remaining_content_length; | |
| 542 | const remaining = remaining_content_length.*; | |
| 543 | if (remaining == 0) { | |
| 544 | reader.state = .ready; | |
| 545 | return error.EndOfStream; | |
| 546 | } | |
| 547 | const n = try reader.in.discard(limit.min(.limited(remaining))); | |
| 548 | remaining_content_length.* = remaining - n; | |
| 549 | return n; | |
| 550 | } | |
| 551 | ||
| 552 | fn chunkedStream(io_r: *std.Io.Reader, w: *Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { | |
| 553 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 554 | const chunk_len_ptr = switch (reader.state) { | |
| 555 | .ready => return error.EndOfStream, | |
| 556 | .body_remaining_chunk_len => |*x| x, | |
| 557 | else => unreachable, | |
| 558 | }; | |
| 559 | return chunkedReadEndless(reader, w, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 560 | error.ReadFailed => return error.ReadFailed, | |
| 561 | error.WriteFailed => return error.WriteFailed, | |
| 562 | error.EndOfStream => { | |
| 563 | reader.body_err = error.HttpChunkTruncated; | |
| 564 | return error.ReadFailed; | |
| 565 | }, | |
| 566 | else => |e| { | |
| 567 | reader.body_err = e; | |
| 568 | return error.ReadFailed; | |
| 569 | }, | |
| 570 | }; | |
| 571 | } | |
| 572 | ||
| 573 | fn chunkedReadEndless( | |
| 574 | reader: *Reader, | |
| 575 | w: *Writer, | |
| 576 | limit: std.Io.Limit, | |
| 577 | chunk_len_ptr: *RemainingChunkLen, | |
| 578 | ) (BodyError || std.Io.Reader.StreamError)!usize { | |
| 579 | const in = reader.in; | |
| 580 | len: switch (chunk_len_ptr.*) { | |
| 581 | .head => { | |
| 582 | var cp: ChunkParser = .init; | |
| 583 | while (true) { | |
| 584 | const i = cp.feed(in.buffered()); | |
| 585 | switch (cp.state) { | |
| 586 | .invalid => return error.HttpChunkInvalid, | |
| 587 | .data => { | |
| 588 | in.toss(i); | |
| 589 | break; | |
| 590 | }, | |
| 591 | else => { | |
| 592 | in.toss(i); | |
| 593 | try in.fillMore(); | |
| 594 | continue; | |
| 595 | }, | |
| 596 | } | |
| 597 | } | |
| 598 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | |
| 599 | const n = try in.stream(w, limit.min(.limited(cp.chunk_len))); | |
| 600 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | |
| 601 | return n; | |
| 602 | }, | |
| 603 | .n => { | |
| 604 | if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; | |
| 605 | in.toss(1); | |
| 606 | continue :len .head; | |
| 607 | }, | |
| 608 | .rn => { | |
| 609 | const rn = try in.peekArray(2); | |
| 610 | if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; | |
| 611 | in.toss(2); | |
| 612 | continue :len .head; | |
| 613 | }, | |
| 614 | else => |remaining_chunk_len| { | |
| 615 | const n = try in.stream(w, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); | |
| 616 | chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); | |
| 617 | return n; | |
| 618 | }, | |
| 619 | } | |
| 620 | } | |
| 621 | ||
| 622 | fn chunkedDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { | |
| 623 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 624 | const chunk_len_ptr = switch (reader.state) { | |
| 625 | .ready => return error.EndOfStream, | |
| 626 | .body_remaining_chunk_len => |*x| x, | |
| 627 | else => unreachable, | |
| 628 | }; | |
| 629 | return chunkedDiscardEndless(reader, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 630 | error.ReadFailed => return error.ReadFailed, | |
| 631 | error.EndOfStream => { | |
| 632 | reader.body_err = error.HttpChunkTruncated; | |
| 633 | return error.ReadFailed; | |
| 634 | }, | |
| 635 | else => |e| { | |
| 636 | reader.body_err = e; | |
| 637 | return error.ReadFailed; | |
| 638 | }, | |
| 639 | }; | |
| 640 | } | |
| 641 | ||
| 642 | fn chunkedDiscardEndless( | |
| 643 | reader: *Reader, | |
| 644 | limit: std.Io.Limit, | |
| 645 | chunk_len_ptr: *RemainingChunkLen, | |
| 646 | ) (BodyError || std.Io.Reader.Error)!usize { | |
| 647 | const in = reader.in; | |
| 648 | len: switch (chunk_len_ptr.*) { | |
| 649 | .head => { | |
| 650 | var cp: ChunkParser = .init; | |
| 651 | while (true) { | |
| 652 | const i = cp.feed(in.buffered()); | |
| 653 | switch (cp.state) { | |
| 654 | .invalid => return error.HttpChunkInvalid, | |
| 655 | .data => { | |
| 656 | in.toss(i); | |
| 657 | break; | |
| 658 | }, | |
| 659 | else => { | |
| 660 | in.toss(i); | |
| 661 | try in.fillMore(); | |
| 662 | continue; | |
| 663 | }, | |
| 664 | } | |
| 665 | } | |
| 666 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); | |
| 667 | const n = try in.discard(limit.min(.limited(cp.chunk_len))); | |
| 668 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); | |
| 669 | return n; | |
| 670 | }, | |
| 671 | .n => { | |
| 672 | if ((try in.peekByte()) != '\n') return error.HttpChunkInvalid; | |
| 673 | in.toss(1); | |
| 674 | continue :len .head; | |
| 675 | }, | |
| 676 | .rn => { | |
| 677 | const rn = try in.peekArray(2); | |
| 678 | if (rn[0] != '\r' or rn[1] != '\n') return error.HttpChunkInvalid; | |
| 679 | in.toss(2); | |
| 680 | continue :len .head; | |
| 681 | }, | |
| 682 | else => |remaining_chunk_len| { | |
| 683 | const n = try in.discard(limit.min(.limited(remaining_chunk_len.int() - 2))); | |
| 684 | chunk_len_ptr.* = .init(remaining_chunk_len.int() - n); | |
| 685 | return n; | |
| 686 | }, | |
| 687 | } | |
| 688 | } | |
| 689 | ||
| 690 | /// Called when next bytes in the stream are trailers, or "\r\n" to indicate | |
| 691 | /// end of chunked body. | |
| 692 | fn parseTrailers(reader: *Reader, amt_read: usize) (BodyError || std.Io.Reader.Error)!usize { | |
| 693 | const in = reader.in; | |
| 694 | const rn = try in.peekArray(2); | |
| 695 | if (rn[0] == '\r' and rn[1] == '\n') { | |
| 696 | in.toss(2); | |
| 697 | reader.state = .ready; | |
| 698 | assert(reader.trailers.len == 0); | |
| 699 | return amt_read; | |
| 700 | } | |
| 701 | var hp: HeadParser = .{ .state = .seen_rn }; | |
| 702 | var trailers_len: usize = 2; | |
| 703 | while (true) { | |
| 704 | if (in.buffer.len - trailers_len == 0) return error.HttpHeadersOversize; | |
| 705 | const remaining = in.buffered()[trailers_len..]; | |
| 706 | if (remaining.len == 0) { | |
| 707 | try in.fillMore(); | |
| 708 | continue; | |
| 709 | } | |
| 710 | trailers_len += hp.feed(remaining); | |
| 711 | if (hp.state == .finished) { | |
| 712 | reader.state = .ready; | |
| 713 | reader.trailers = in.buffered()[0..trailers_len]; | |
| 714 | in.toss(trailers_len); | |
| 715 | return amt_read; | |
| 716 | } | |
| 717 | } | |
| 718 | } | |
| 719 | }; | |
| 720 | ||
| 721 | pub const Decompressor = union(enum) { | |
| 722 | flate: std.compress.flate.Decompress, | |
| 723 | zstd: std.compress.zstd.Decompress, | |
| 724 | none: *std.Io.Reader, | |
| 725 | ||
| 726 | pub fn init( | |
| 727 | decompressor: *Decompressor, | |
| 728 | transfer_reader: *std.Io.Reader, | |
| 729 | buffer: []u8, | |
| 730 | content_encoding: ContentEncoding, | |
| 731 | ) *std.Io.Reader { | |
| 732 | switch (content_encoding) { | |
| 733 | .identity => { | |
| 734 | decompressor.* = .{ .none = transfer_reader }; | |
| 735 | return transfer_reader; | |
| 736 | }, | |
| 737 | .deflate => { | |
| 738 | decompressor.* = .{ .flate = .init(transfer_reader, .raw, buffer) }; | |
| 739 | return &decompressor.flate.reader; | |
| 740 | }, | |
| 741 | .gzip => { | |
| 742 | decompressor.* = .{ .flate = .init(transfer_reader, .gzip, buffer) }; | |
| 743 | return &decompressor.flate.reader; | |
| 744 | }, | |
| 745 | .zstd => { | |
| 746 | decompressor.* = .{ .zstd = .init(transfer_reader, buffer, .{ .verify_checksum = false }) }; | |
| 747 | return &decompressor.zstd.reader; | |
| 748 | }, | |
| 749 | .compress => unreachable, | |
| 750 | } | |
| 751 | } | |
| 752 | }; | |
| 753 | ||
| 754 | /// Request or response body. | |
| 755 | pub const BodyWriter = struct { | |
| 756 | /// Until the lifetime of `BodyWriter` ends, it is illegal to modify the | |
| 757 | /// state of this other than via methods of `BodyWriter`. | |
| 758 | http_protocol_output: *Writer, | |
| 759 | state: State, | |
| 760 | writer: Writer, | |
| 761 | ||
| 762 | pub const Error = Writer.Error; | |
| 763 | ||
| 764 | /// How many zeroes to reserve for hex-encoded chunk length. | |
| 765 | const chunk_len_digits = 8; | |
| 766 | const max_chunk_len: usize = std.math.pow(usize, 16, chunk_len_digits) - 1; | |
| 767 | const chunk_header_template = ("0" ** chunk_len_digits) ++ "\r\n"; | |
| 768 | ||
| 769 | comptime { | |
| 770 | assert(max_chunk_len == std.math.maxInt(u32)); | |
| 771 | } | |
| 772 | ||
| 773 | pub const State = union(enum) { | |
| 774 | /// End of connection signals the end of the stream. | |
| 775 | none, | |
| 776 | /// As a debugging utility, counts down to zero as bytes are written. | |
| 777 | content_length: u64, | |
| 778 | /// Each chunk is wrapped in a header and trailer. | |
| 779 | chunked: Chunked, | |
| 780 | /// Cleanly finished stream; connection can be reused. | |
| 781 | end, | |
| 782 | ||
| 783 | pub const Chunked = union(enum) { | |
| 784 | /// Index to the start of the hex-encoded chunk length in the chunk | |
| 785 | /// header within the buffer of `BodyWriter.http_protocol_output`. | |
| 786 | /// Buffered chunk data starts here plus length of `chunk_header_template`. | |
| 787 | offset: usize, | |
| 788 | /// We are in the middle of a chunk and this is how many bytes are | |
| 789 | /// left until the next header. This includes +2 for "\r"\n", and | |
| 790 | /// is zero for the beginning of the stream. | |
| 791 | chunk_len: usize, | |
| 792 | ||
| 793 | pub const init: Chunked = .{ .chunk_len = 0 }; | |
| 794 | }; | |
| 795 | }; | |
| 796 | ||
| 797 | pub fn isEliding(w: *const BodyWriter) bool { | |
| 798 | return w.writer.vtable.drain == Writer.discardingDrain; | |
| 799 | } | |
| 800 | ||
| 801 | /// Sends all buffered data across `BodyWriter.http_protocol_output`. | |
| 802 | pub fn flush(w: *BodyWriter) Error!void { | |
| 803 | const out = w.http_protocol_output; | |
| 804 | switch (w.state) { | |
| 805 | .end, .none, .content_length => return out.flush(), | |
| 806 | .chunked => |*chunked| switch (chunked.*) { | |
| 807 | .offset => |offset| { | |
| 808 | const chunk_len = out.end - offset - chunk_header_template.len; | |
| 809 | if (chunk_len > 0) { | |
| 810 | writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 811 | chunked.* = .{ .chunk_len = 2 }; | |
| 812 | } else { | |
| 813 | out.end = offset; | |
| 814 | chunked.* = .{ .chunk_len = 0 }; | |
| 815 | } | |
| 816 | try out.flush(); | |
| 817 | }, | |
| 818 | .chunk_len => return out.flush(), | |
| 819 | }, | |
| 820 | } | |
| 821 | } | |
| 822 | ||
| 823 | /// When using content-length, asserts that the amount of data sent matches | |
| 824 | /// the value sent in the header, then flushes. | |
| 825 | /// | |
| 826 | /// When using transfer-encoding: chunked, writes the end-of-stream message | |
| 827 | /// with empty trailers, then flushes the stream to the system. Asserts any | |
| 828 | /// started chunk has been completely finished. | |
| 829 | /// | |
| 830 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 831 | /// | |
| 832 | /// See also: | |
| 833 | /// * `endUnflushed` | |
| 834 | /// * `endChunked` | |
| 835 | pub fn end(w: *BodyWriter) Error!void { | |
| 836 | try endUnflushed(w); | |
| 837 | try w.http_protocol_output.flush(); | |
| 838 | } | |
| 839 | ||
| 840 | /// When using content-length, asserts that the amount of data sent matches | |
| 841 | /// the value sent in the header. | |
| 842 | /// | |
| 843 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the | |
| 844 | /// end-of-stream message with empty trailers. | |
| 845 | /// | |
| 846 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 847 | /// | |
| 848 | /// See also: | |
| 849 | /// * `end` | |
| 850 | /// * `endChunked` | |
| 851 | pub fn endUnflushed(w: *BodyWriter) Error!void { | |
| 852 | switch (w.state) { | |
| 853 | .end => unreachable, | |
| 854 | .content_length => |len| { | |
| 855 | assert(len == 0); // Trips when end() called before all bytes written. | |
| 856 | w.state = .end; | |
| 857 | }, | |
| 858 | .none => {}, | |
| 859 | .chunked => return endChunkedUnflushed(w, .{}), | |
| 860 | } | |
| 861 | } | |
| 862 | ||
| 863 | pub const EndChunkedOptions = struct { | |
| 864 | trailers: []const Header = &.{}, | |
| 865 | }; | |
| 866 | ||
| 867 | /// Writes the end-of-stream message and any optional trailers, flushing | |
| 868 | /// the underlying stream. | |
| 869 | /// | |
| 870 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. | |
| 871 | /// | |
| 872 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 873 | /// | |
| 874 | /// See also: | |
| 875 | /// * `endChunkedUnflushed` | |
| 876 | /// * `end` | |
| 877 | pub fn endChunked(w: *BodyWriter, options: EndChunkedOptions) Error!void { | |
| 878 | try endChunkedUnflushed(w, options); | |
| 879 | try w.http_protocol_output.flush(); | |
| 880 | } | |
| 881 | ||
| 882 | /// Writes the end-of-stream message and any optional trailers. | |
| 883 | /// | |
| 884 | /// Does not flush. | |
| 885 | /// | |
| 886 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. | |
| 887 | /// | |
| 888 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 889 | /// | |
| 890 | /// See also: | |
| 891 | /// * `endChunked` | |
| 892 | /// * `endUnflushed` | |
| 893 | /// * `end` | |
| 894 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { | |
| 895 | const chunked = &w.state.chunked; | |
| 896 | if (w.isEliding()) { | |
| 897 | w.state = .end; | |
| 898 | return; | |
| 899 | } | |
| 900 | const bw = w.http_protocol_output; | |
| 901 | switch (chunked.*) { | |
| 902 | .offset => |offset| { | |
| 903 | const chunk_len = bw.end - offset - chunk_header_template.len; | |
| 904 | writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 905 | try bw.writeAll("\r\n"); | |
| 906 | }, | |
| 907 | .chunk_len => |chunk_len| switch (chunk_len) { | |
| 908 | 0 => {}, | |
| 909 | 1 => try bw.writeByte('\n'), | |
| 910 | 2 => try bw.writeAll("\r\n"), | |
| 911 | else => unreachable, // An earlier write call indicated more data would follow. | |
| 912 | }, | |
| 913 | } | |
| 914 | try bw.writeAll("0\r\n"); | |
| 915 | for (options.trailers) |trailer| { | |
| 916 | try bw.writeAll(trailer.name); | |
| 917 | try bw.writeAll(": "); | |
| 918 | try bw.writeAll(trailer.value); | |
| 919 | try bw.writeAll("\r\n"); | |
| 920 | } | |
| 921 | try bw.writeAll("\r\n"); | |
| 922 | w.state = .end; | |
| 923 | } | |
| 924 | ||
| 925 | pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 926 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 927 | assert(!bw.isEliding()); | |
| 928 | const out = bw.http_protocol_output; | |
| 929 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 930 | bw.state.content_length -= n; | |
| 931 | return w.consume(n); | |
| 932 | } | |
| 933 | ||
| 934 | pub fn noneDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 935 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 936 | assert(!bw.isEliding()); | |
| 937 | const out = bw.http_protocol_output; | |
| 938 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 939 | return w.consume(n); | |
| 940 | } | |
| 941 | ||
| 942 | /// Returns `null` if size cannot be computed without making any syscalls. | |
| 943 | pub fn noneSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 944 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 945 | assert(!bw.isEliding()); | |
| 946 | const out = bw.http_protocol_output; | |
| 947 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 948 | return w.consume(n); | |
| 949 | } | |
| 950 | ||
| 951 | pub fn contentLengthSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 952 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 953 | assert(!bw.isEliding()); | |
| 954 | const out = bw.http_protocol_output; | |
| 955 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 956 | bw.state.content_length -= n; | |
| 957 | return w.consume(n); | |
| 958 | } | |
| 959 | ||
| 960 | pub fn chunkedSendFile(w: *Writer, file_reader: *File.Reader, limit: std.Io.Limit) Writer.FileError!usize { | |
| 961 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 962 | assert(!bw.isEliding()); | |
| 963 | const data_len = Writer.countSendFileLowerBound(w.end, file_reader, limit) orelse { | |
| 964 | // If the file size is unknown, we cannot lower to a `sendFile` since we would | |
| 965 | // have to flush the chunk header before knowing the chunk length. | |
| 966 | return error.Unimplemented; | |
| 967 | }; | |
| 968 | const out = bw.http_protocol_output; | |
| 969 | const chunked = &bw.state.chunked; | |
| 970 | state: switch (chunked.*) { | |
| 971 | .offset => |off| { | |
| 972 | // TODO: is it better perf to read small files into the buffer? | |
| 973 | const buffered_len = out.end - off - chunk_header_template.len; | |
| 974 | const chunk_len = data_len + buffered_len; | |
| 975 | writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len); | |
| 976 | const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | |
| 977 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | |
| 978 | return w.consume(n); | |
| 979 | }, | |
| 980 | .chunk_len => |chunk_len| l: switch (chunk_len) { | |
| 981 | 0 => { | |
| 982 | const off = out.end; | |
| 983 | const header_buf = try out.writableArray(chunk_header_template.len); | |
| 984 | @memcpy(header_buf, chunk_header_template); | |
| 985 | chunked.* = .{ .offset = off }; | |
| 986 | continue :state .{ .offset = off }; | |
| 987 | }, | |
| 988 | 1 => { | |
| 989 | try out.writeByte('\n'); | |
| 990 | chunked.chunk_len = 0; | |
| 991 | continue :l 0; | |
| 992 | }, | |
| 993 | 2 => { | |
| 994 | try out.writeByte('\r'); | |
| 995 | chunked.chunk_len = 1; | |
| 996 | continue :l 1; | |
| 997 | }, | |
| 998 | else => { | |
| 999 | const new_limit = limit.min(.limited(chunk_len - 2)); | |
| 1000 | const n = try out.sendFileHeader(w.buffered(), file_reader, new_limit); | |
| 1001 | chunked.chunk_len = chunk_len - n; | |
| 1002 | return w.consume(n); | |
| 1003 | }, | |
| 1004 | }, | |
| 1005 | } | |
| 1006 | } | |
| 1007 | ||
| 1008 | pub fn chunkedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | |
| 1009 | const bw: *BodyWriter = @fieldParentPtr("writer", w); | |
| 1010 | assert(!bw.isEliding()); | |
| 1011 | const out = bw.http_protocol_output; | |
| 1012 | const data_len = w.end + Writer.countSplat(data, splat); | |
| 1013 | const chunked = &bw.state.chunked; | |
| 1014 | state: switch (chunked.*) { | |
| 1015 | .offset => |offset| { | |
| 1016 | if (out.unusedCapacityLen() >= data_len) { | |
| 1017 | return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable); | |
| 1018 | } | |
| 1019 | const buffered_len = out.end - offset - chunk_header_template.len; | |
| 1020 | const chunk_len = data_len + buffered_len; | |
| 1021 | writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | |
| 1022 | const n = try out.writeSplatHeader(w.buffered(), data, splat); | |
| 1023 | chunked.* = .{ .chunk_len = data_len + 2 - n }; | |
| 1024 | return w.consume(n); | |
| 1025 | }, | |
| 1026 | .chunk_len => |chunk_len| l: switch (chunk_len) { | |
| 1027 | 0 => { | |
| 1028 | const offset = out.end; | |
| 1029 | const header_buf = try out.writableArray(chunk_header_template.len); | |
| 1030 | @memcpy(header_buf, chunk_header_template); | |
| 1031 | chunked.* = .{ .offset = offset }; | |
| 1032 | continue :state .{ .offset = offset }; | |
| 1033 | }, | |
| 1034 | 1 => { | |
| 1035 | try out.writeByte('\n'); | |
| 1036 | chunked.chunk_len = 0; | |
| 1037 | continue :l 0; | |
| 1038 | }, | |
| 1039 | 2 => { | |
| 1040 | try out.writeByte('\r'); | |
| 1041 | chunked.chunk_len = 1; | |
| 1042 | continue :l 1; | |
| 1043 | }, | |
| 1044 | else => { | |
| 1045 | const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2)); | |
| 1046 | chunked.chunk_len = chunk_len - n; | |
| 1047 | return w.consume(n); | |
| 1048 | }, | |
| 1049 | }, | |
| 1050 | } | |
| 1051 | } | |
| 1052 | ||
| 1053 | /// Writes an integer as base 16 to `buf`, right-aligned, assuming the | |
| 1054 | /// buffer has already been filled with zeroes. | |
| 1055 | fn writeHex(buf: []u8, x: usize) void { | |
| 1056 | assert(std.mem.allEqual(u8, buf, '0')); | |
| 1057 | const base = 16; | |
| 1058 | var index: usize = buf.len; | |
| 1059 | var a = x; | |
| 1060 | while (a > 0) { | |
| 1061 | const digit = a % base; | |
| 1062 | index -= 1; | |
| 1063 | buf[index] = std.fmt.digitToChar(@intCast(digit), .lower); | |
| 1064 | a /= base; | |
| 1065 | } | |
| 1066 | } | |
| 1067 | }; | |
| 1068 | ||
| 318 | 1069 | test { |
| 1070 | _ = Server; | |
| 1071 | _ = Status; | |
| 1072 | _ = Method; | |
| 1073 | _ = ChunkParser; | |
| 1074 | _ = HeadParser; | |
| 1075 | ||
| 319 | 1076 | if (builtin.os.tag != .wasi) { |
| 320 | 1077 | _ = Client; |
| 321 | _ = Method; | |
| 322 | _ = Server; | |
| 323 | _ = Status; | |
| 324 | _ = HeadParser; | |
| 325 | _ = ChunkParser; | |
| 326 | _ = WebSocket; | |
| 327 | 1078 | _ = @import("http/test.zig"); |
| 328 | 1079 | } |
| 329 | 1080 | } |
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+997-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, | |
| 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,11 +69,13 @@ 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(); |
| ... | ... | @@ -71,7 +87,7 @@ pub const ConnectionPool = struct { |
| 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,25 @@ 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 | /// `allocator` must be the same one used to create `connection`. | |
| 119 | /// | |
| 120 | /// Threadsafe. | |
| 121 | pub fn release(pool: *ConnectionPool, connection: *Connection) void { | |
| 105 | 122 | pool.mutex.lock(); |
| 106 | 123 | defer pool.mutex.unlock(); |
| 107 | 124 | |
| 108 | 125 | pool.used.remove(&connection.pool_node); |
| 109 | 126 | |
| 110 | if (connection.closing or pool.free_size == 0) { | |
| 111 | connection.close(allocator); | |
| 112 | return allocator.destroy(connection); | |
| 113 | } | |
| 127 | if (connection.closing or pool.free_size == 0) return connection.destroy(); | |
| 114 | 128 | |
| 115 | 129 | if (pool.free_len >= pool.free_size) { |
| 116 | 130 | const popped: *Connection = @fieldParentPtr("pool_node", pool.free.popFirst().?); |
| 117 | 131 | pool.free_len -= 1; |
| 118 | 132 | |
| 119 | popped.close(allocator); | |
| 120 | allocator.destroy(popped); | |
| 133 | popped.destroy(); | |
| 121 | 134 | } |
| 122 | 135 | |
| 123 | 136 | if (connection.proxied) { |
| ... | ... | @@ -138,9 +151,11 @@ pub const ConnectionPool = struct { |
| 138 | 151 | pool.used.append(&connection.pool_node); |
| 139 | 152 | } |
| 140 | 153 | |
| 141 | /// Resizes the connection pool. This function is threadsafe. | |
| 154 | /// Resizes the connection pool. | |
| 142 | 155 | /// |
| 143 | 156 | /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. |
| 157 | /// | |
| 158 | /// Threadsafe. | |
| 144 | 159 | pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { |
| 145 | 160 | pool.mutex.lock(); |
| 146 | 161 | defer pool.mutex.unlock(); |
| ... | ... | @@ -158,538 +173,586 @@ pub const ConnectionPool = struct { |
| 158 | 173 | pool.free_size = new_size; |
| 159 | 174 | } |
| 160 | 175 | |
| 161 | /// Frees the connection pool and closes all connections within. This function is threadsafe. | |
| 176 | /// Frees the connection pool and closes all connections within. | |
| 162 | 177 | /// |
| 163 | 178 | /// All future operations on the connection pool will deadlock. |
| 164 | pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void { | |
| 179 | /// | |
| 180 | /// Threadsafe. | |
| 181 | pub fn deinit(pool: *ConnectionPool) void { | |
| 165 | 182 | pool.mutex.lock(); |
| 166 | 183 | |
| 167 | 184 | var next = pool.free.first; |
| 168 | 185 | while (next) |node| { |
| 169 | 186 | const connection: *Connection = @fieldParentPtr("pool_node", node); |
| 170 | 187 | next = node.next; |
| 171 | connection.close(allocator); | |
| 172 | allocator.destroy(connection); | |
| 188 | connection.destroy(); | |
| 173 | 189 | } |
| 174 | 190 | |
| 175 | 191 | next = pool.used.first; |
| 176 | 192 | while (next) |node| { |
| 177 | 193 | const connection: *Connection = @fieldParentPtr("pool_node", node); |
| 178 | 194 | next = node.next; |
| 179 | connection.close(allocator); | |
| 180 | allocator.destroy(node); | |
| 195 | connection.destroy(); | |
| 181 | 196 | } |
| 182 | 197 | |
| 183 | 198 | pool.* = undefined; |
| 184 | 199 | } |
| 185 | 200 | }; |
| 186 | 201 | |
| 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, | |
| 201 | ||
| 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 | } | |
| 233 | }; | |
| 234 | } | |
| 235 | ||
| 236 | pub fn readvDirect(conn: *Connection, buffers: []std.posix.iovec) ReadError!usize { | |
| 237 | if (conn.protocol == .tls) { | |
| 238 | if (disable_tls) unreachable; | |
| 202 | pub const Protocol = enum { | |
| 203 | plain, | |
| 204 | tls, | |
| 239 | 205 | |
| 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, | |
| 206 | fn port(protocol: Protocol) u16 { | |
| 207 | return switch (protocol) { | |
| 208 | .plain => 80, | |
| 209 | .tls => 443, | |
| 247 | 210 | }; |
| 248 | 211 | } |
| 249 | 212 | |
| 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); | |
| 213 | pub fn fromScheme(scheme: []const u8) ?Protocol { | |
| 214 | const protocol_map = std.StaticStringMap(Protocol).initComptime(.{ | |
| 215 | .{ "http", .plain }, | |
| 216 | .{ "ws", .plain }, | |
| 217 | .{ "https", .tls }, | |
| 218 | .{ "wss", .tls }, | |
| 219 | }); | |
| 220 | return protocol_map.get(scheme); | |
| 261 | 221 | } |
| 262 | 222 | |
| 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]; | |
| 223 | pub fn fromUri(uri: Uri) ?Protocol { | |
| 224 | return fromScheme(uri.scheme); | |
| 266 | 225 | } |
| 226 | }; | |
| 267 | 227 | |
| 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 | } | |
| 228 | pub const Connection = struct { | |
| 229 | client: *Client, | |
| 230 | stream_writer: net.Stream.Writer, | |
| 231 | stream_reader: net.Stream.Reader, | |
| 232 | /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. | |
| 233 | pool_node: std.DoublyLinkedList.Node, | |
| 234 | port: u16, | |
| 235 | host_len: u8, | |
| 236 | proxied: bool, | |
| 237 | closing: bool, | |
| 238 | protocol: Protocol, | |
| 272 | 239 | |
| 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; | |
| 240 | const Plain = struct { | |
| 241 | connection: Connection, | |
| 242 | ||
| 243 | fn create( | |
| 244 | client: *Client, | |
| 245 | remote_host: []const u8, | |
| 246 | port: u16, | |
| 247 | stream: net.Stream, | |
| 248 | ) error{OutOfMemory}!*Plain { | |
| 249 | const gpa = client.allocator; | |
| 250 | const alloc_len = allocLen(client, remote_host.len); | |
| 251 | const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); | |
| 252 | errdefer gpa.free(base); | |
| 253 | const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; | |
| 254 | const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; | |
| 255 | const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; | |
| 256 | assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); | |
| 257 | @memcpy(host_buffer, remote_host); | |
| 258 | const plain: *Plain = @ptrCast(base); | |
| 259 | plain.* = .{ | |
| 260 | .connection = .{ | |
| 261 | .client = client, | |
| 262 | .stream_writer = stream.writer(socket_write_buffer), | |
| 263 | .stream_reader = stream.reader(socket_read_buffer), | |
| 264 | .pool_node = .{}, | |
| 265 | .port = port, | |
| 266 | .host_len = @intCast(remote_host.len), | |
| 267 | .proxied = false, | |
| 268 | .closing = false, | |
| 269 | .protocol = .plain, | |
| 270 | }, | |
| 271 | }; | |
| 272 | return plain; | |
| 273 | } | |
| 277 | 274 | |
| 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); | |
| 275 | fn destroy(plain: *Plain) void { | |
| 276 | const c = &plain.connection; | |
| 277 | const gpa = c.client.allocator; | |
| 278 | const base: [*]align(@alignOf(Plain)) u8 = @ptrCast(plain); | |
| 279 | gpa.free(base[0..allocLen(c.client, c.host_len)]); | |
| 280 | } | |
| 281 | 281 | |
| 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; | |
| 282 | fn allocLen(client: *Client, host_len: usize) usize { | |
| 283 | return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; | |
| 284 | } | |
| 286 | 285 | |
| 287 | return available_read; | |
| 286 | fn host(plain: *Plain) []u8 { | |
| 287 | const base: [*]u8 = @ptrCast(plain); | |
| 288 | return base[@sizeOf(Plain)..][0..plain.connection.host_len]; | |
| 288 | 289 | } |
| 290 | }; | |
| 289 | 291 | |
| 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); | |
| 292 | const Tls = struct { | |
| 293 | client: std.crypto.tls.Client, | |
| 294 | connection: Connection, | |
| 295 | ||
| 296 | fn create( | |
| 297 | client: *Client, | |
| 298 | remote_host: []const u8, | |
| 299 | port: u16, | |
| 300 | stream: net.Stream, | |
| 301 | ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { | |
| 302 | const gpa = client.allocator; | |
| 303 | const alloc_len = allocLen(client, remote_host.len); | |
| 304 | const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); | |
| 305 | errdefer gpa.free(base); | |
| 306 | const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; | |
| 307 | const tls_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.tls_buffer_size]; | |
| 308 | const tls_write_buffer = tls_read_buffer.ptr[tls_read_buffer.len..][0..client.tls_buffer_size]; | |
| 309 | const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; | |
| 310 | assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); | |
| 311 | @memcpy(host_buffer, remote_host); | |
| 312 | const tls: *Tls = @ptrCast(base); | |
| 313 | tls.* = .{ | |
| 314 | .connection = .{ | |
| 315 | .client = client, | |
| 316 | .stream_writer = stream.writer(socket_write_buffer), | |
| 317 | .stream_reader = stream.reader(&.{}), | |
| 318 | .pool_node = .{}, | |
| 319 | .port = port, | |
| 320 | .host_len = @intCast(remote_host.len), | |
| 321 | .proxied = false, | |
| 322 | .closing = false, | |
| 323 | .protocol = .tls, | |
| 324 | }, | |
| 325 | // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true | |
| 326 | .client = std.crypto.tls.Client.init( | |
| 327 | tls.connection.stream_reader.interface(), | |
| 328 | &tls.connection.stream_writer.interface, | |
| 329 | .{ | |
| 330 | .host = .{ .explicit = remote_host }, | |
| 331 | .ca = .{ .bundle = client.ca_bundle }, | |
| 332 | .ssl_key_log = client.ssl_key_log, | |
| 333 | .read_buffer = tls_read_buffer, | |
| 334 | .write_buffer = tls_write_buffer, | |
| 335 | // This is appropriate for HTTPS because the HTTP headers contain | |
| 336 | // the content length which is used to detect truncation attacks. | |
| 337 | .allow_truncation_attacks = true, | |
| 338 | }, | |
| 339 | ) catch return error.TlsInitializationFailed, | |
| 340 | }; | |
| 341 | return tls; | |
| 342 | } | |
| 295 | 343 | |
| 296 | if (nread > buffer.len) { | |
| 297 | conn.read_start = 0; | |
| 298 | conn.read_end = @intCast(nread - buffer.len); | |
| 299 | return buffer.len; | |
| 344 | fn destroy(tls: *Tls) void { | |
| 345 | const c = &tls.connection; | |
| 346 | const gpa = c.client.allocator; | |
| 347 | const base: [*]align(@alignOf(Tls)) u8 = @ptrCast(tls); | |
| 348 | gpa.free(base[0..allocLen(c.client, c.host_len)]); | |
| 300 | 349 | } |
| 301 | 350 | |
| 302 | return nread; | |
| 303 | } | |
| 351 | fn allocLen(client: *Client, host_len: usize) usize { | |
| 352 | return @sizeOf(Tls) + host_len + client.tls_buffer_size + client.tls_buffer_size + client.write_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 }; | |
| 361 | fn getStream(c: *Connection) net.Stream { | |
| 362 | return c.stream_reader.getStream(); | |
| 318 | 363 | } |
| 319 | 364 | |
| 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, | |
| 324 | }; | |
| 325 | } | |
| 326 | ||
| 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 | } | |
| 333 | ||
| 334 | return conn.stream.writeAll(buffer) catch |err| switch (err) { | |
| 335 | error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, | |
| 336 | else => return error.UnexpectedWriteFailure, | |
| 365 | fn host(c: *Connection) []u8 { | |
| 366 | return switch (c.protocol) { | |
| 367 | .tls => { | |
| 368 | if (disable_tls) unreachable; | |
| 369 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 370 | return tls.host(); | |
| 371 | }, | |
| 372 | .plain => { | |
| 373 | const plain: *Plain = @fieldParentPtr("connection", c); | |
| 374 | return plain.host(); | |
| 375 | }, | |
| 337 | 376 | }; |
| 338 | 377 | } |
| 339 | 378 | |
| 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 | } | |
| 379 | /// If this is called without calling `flush` or `end`, data will be | |
| 380 | /// dropped unsent. | |
| 381 | pub fn destroy(c: *Connection) void { | |
| 382 | c.getStream().close(); | |
| 383 | switch (c.protocol) { | |
| 384 | .tls => { | |
| 385 | if (disable_tls) unreachable; | |
| 386 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 387 | tls.destroy(); | |
| 388 | }, | |
| 389 | .plain => { | |
| 390 | const plain: *Plain = @fieldParentPtr("connection", c); | |
| 391 | plain.destroy(); | |
| 392 | }, | |
| 349 | 393 | } |
| 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 | 394 | } |
| 356 | 395 | |
| 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]; | |
| 396 | /// HTTP protocol from client to server. | |
| 397 | /// This either goes directly to `stream_writer`, or to a TLS client. | |
| 398 | pub fn writer(c: *Connection) *Writer { | |
| 399 | return switch (c.protocol) { | |
| 400 | .tls => { | |
| 401 | if (disable_tls) unreachable; | |
| 402 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 403 | return &tls.client.writer; | |
| 404 | }, | |
| 405 | .plain => &c.stream_writer.interface, | |
| 406 | }; | |
| 362 | 407 | } |
| 363 | 408 | |
| 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; | |
| 409 | /// HTTP protocol from server to client. | |
| 410 | /// This either comes directly from `stream_reader`, or from a TLS client. | |
| 411 | pub fn reader(c: *Connection) *Reader { | |
| 412 | return switch (c.protocol) { | |
| 413 | .tls => { | |
| 414 | if (disable_tls) unreachable; | |
| 415 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 416 | return &tls.client.reader; | |
| 417 | }, | |
| 418 | .plain => c.stream_reader.interface(), | |
| 419 | }; | |
| 370 | 420 | } |
| 371 | 421 | |
| 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 }; | |
| 422 | pub fn flush(c: *Connection) Writer.Error!void { | |
| 423 | if (c.protocol == .tls) { | |
| 424 | if (disable_tls) unreachable; | |
| 425 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 426 | try tls.client.writer.flush(); | |
| 427 | } | |
| 428 | try c.stream_writer.interface.flush(); | |
| 381 | 429 | } |
| 382 | 430 | |
| 383 | /// Closes the connection. | |
| 384 | pub fn close(conn: *Connection, allocator: Allocator) void { | |
| 385 | if (conn.protocol == .tls) { | |
| 431 | /// If the connection is a TLS connection, sends the close_notify alert. | |
| 432 | /// | |
| 433 | /// Flushes all buffers. | |
| 434 | pub fn end(c: *Connection) Writer.Error!void { | |
| 435 | if (c.protocol == .tls) { | |
| 386 | 436 | 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); | |
| 437 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 438 | try tls.client.end(); | |
| 439 | try tls.client.writer.flush(); | |
| 392 | 440 | } |
| 393 | ||
| 394 | conn.stream.close(); | |
| 395 | allocator.free(conn.host); | |
| 441 | try c.stream_writer.interface.flush(); | |
| 396 | 442 | } |
| 397 | 443 | }; |
| 398 | 444 | |
| 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 | 445 | pub const Response = struct { |
| 417 | version: http.Version, | |
| 418 | status: http.Status, | |
| 419 | reason: []const u8, | |
| 420 | ||
| 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, | |
| 427 | ||
| 428 | keep_alive: bool, | |
| 429 | ||
| 430 | /// If present, the number of bytes in the response body. | |
| 431 | content_length: ?u64 = null, | |
| 446 | request: *Request, | |
| 447 | /// Pointers in this struct are invalidated with the next call to | |
| 448 | /// `receiveHead`. | |
| 449 | head: Head, | |
| 450 | ||
| 451 | pub const Head = struct { | |
| 452 | bytes: []const u8, | |
| 453 | version: http.Version, | |
| 454 | status: http.Status, | |
| 455 | reason: []const u8, | |
| 456 | location: ?[]const u8 = null, | |
| 457 | content_type: ?[]const u8 = null, | |
| 458 | content_disposition: ?[]const u8 = null, | |
| 459 | ||
| 460 | keep_alive: bool, | |
| 461 | ||
| 462 | /// If present, the number of bytes in the response body. | |
| 463 | content_length: ?u64 = null, | |
| 464 | ||
| 465 | transfer_encoding: http.TransferEncoding = .none, | |
| 466 | content_encoding: http.ContentEncoding = .identity, | |
| 467 | ||
| 468 | pub const ParseError = error{ | |
| 469 | HttpConnectionHeaderUnsupported, | |
| 470 | HttpContentEncodingUnsupported, | |
| 471 | HttpHeaderContinuationsUnsupported, | |
| 472 | HttpHeadersInvalid, | |
| 473 | HttpTransferEncodingUnsupported, | |
| 474 | InvalidContentLength, | |
| 475 | }; | |
| 432 | 476 | |
| 433 | /// If present, the transfer encoding of the response body, otherwise none. | |
| 434 | transfer_encoding: http.TransferEncoding = .none, | |
| 477 | pub fn parse(bytes: []const u8) ParseError!Head { | |
| 478 | var res: Head = .{ | |
| 479 | .bytes = bytes, | |
| 480 | .status = undefined, | |
| 481 | .reason = undefined, | |
| 482 | .version = undefined, | |
| 483 | .keep_alive = false, | |
| 484 | }; | |
| 485 | var it = mem.splitSequence(u8, bytes, "\r\n"); | |
| 435 | 486 | |
| 436 | /// If present, the compression of the response body, otherwise identity (no compression). | |
| 437 | transfer_compression: http.ContentEncoding = .identity, | |
| 487 | const first_line = it.next().?; | |
| 488 | if (first_line.len < 12) { | |
| 489 | return error.HttpHeadersInvalid; | |
| 490 | } | |
| 438 | 491 | |
| 439 | parser: proto.HeadersParser, | |
| 440 | compression: Compression = .none, | |
| 492 | const version: http.Version = switch (int64(first_line[0..8])) { | |
| 493 | int64("HTTP/1.0") => .@"HTTP/1.0", | |
| 494 | int64("HTTP/1.1") => .@"HTTP/1.1", | |
| 495 | else => return error.HttpHeadersInvalid, | |
| 496 | }; | |
| 497 | if (first_line[8] != ' ') return error.HttpHeadersInvalid; | |
| 498 | const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); | |
| 499 | const reason = mem.trimLeft(u8, first_line[12..], " "); | |
| 500 | ||
| 501 | res.version = version; | |
| 502 | res.status = status; | |
| 503 | res.reason = reason; | |
| 504 | res.keep_alive = switch (version) { | |
| 505 | .@"HTTP/1.0" => false, | |
| 506 | .@"HTTP/1.1" => true, | |
| 507 | }; | |
| 441 | 508 | |
| 442 | /// Whether the response body should be skipped. Any data read from the | |
| 443 | /// response body will be discarded. | |
| 444 | skip: bool = false, | |
| 509 | while (it.next()) |line| { | |
| 510 | if (line.len == 0) return res; | |
| 511 | switch (line[0]) { | |
| 512 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 513 | else => {}, | |
| 514 | } | |
| 445 | 515 | |
| 446 | pub const ParseError = error{ | |
| 447 | HttpHeadersInvalid, | |
| 448 | HttpHeaderContinuationsUnsupported, | |
| 449 | HttpTransferEncodingUnsupported, | |
| 450 | HttpConnectionHeaderUnsupported, | |
| 451 | InvalidContentLength, | |
| 452 | CompressionUnsupported, | |
| 453 | }; | |
| 516 | var line_it = mem.splitScalar(u8, line, ':'); | |
| 517 | const header_name = line_it.next().?; | |
| 518 | const header_value = mem.trim(u8, line_it.rest(), " \t"); | |
| 519 | if (header_name.len == 0) return error.HttpHeadersInvalid; | |
| 520 | ||
| 521 | if (std.ascii.eqlIgnoreCase(header_name, "connection")) { | |
| 522 | res.keep_alive = !std.ascii.eqlIgnoreCase(header_value, "close"); | |
| 523 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-type")) { | |
| 524 | res.content_type = header_value; | |
| 525 | } else if (std.ascii.eqlIgnoreCase(header_name, "location")) { | |
| 526 | res.location = header_value; | |
| 527 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-disposition")) { | |
| 528 | res.content_disposition = header_value; | |
| 529 | } else if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { | |
| 530 | // Transfer-Encoding: second, first | |
| 531 | // Transfer-Encoding: deflate, chunked | |
| 532 | var iter = mem.splitBackwardsScalar(u8, header_value, ','); | |
| 533 | ||
| 534 | const first = iter.first(); | |
| 535 | const trimmed_first = mem.trim(u8, first, " "); | |
| 536 | ||
| 537 | var next: ?[]const u8 = first; | |
| 538 | if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { | |
| 539 | if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding | |
| 540 | res.transfer_encoding = transfer; | |
| 541 | ||
| 542 | next = iter.next(); | |
| 543 | } | |
| 454 | 544 | |
| 455 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { | |
| 456 | var it = mem.splitSequence(u8, bytes, "\r\n"); | |
| 545 | if (next) |second| { | |
| 546 | const trimmed_second = mem.trim(u8, second, " "); | |
| 457 | 547 | |
| 458 | const first_line = it.next().?; | |
| 459 | if (first_line.len < 12) { | |
| 460 | return error.HttpHeadersInvalid; | |
| 461 | } | |
| 548 | if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { | |
| 549 | if (res.content_encoding != .identity) return error.HttpHeadersInvalid; // double compression is not supported | |
| 550 | res.content_encoding = transfer; | |
| 551 | } else { | |
| 552 | return error.HttpTransferEncodingUnsupported; | |
| 553 | } | |
| 554 | } | |
| 462 | 555 | |
| 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 | }; | |
| 556 | if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; | |
| 557 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | |
| 558 | const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | |
| 479 | 559 | |
| 480 | while (it.next()) |line| { | |
| 481 | if (line.len == 0) return; | |
| 482 | switch (line[0]) { | |
| 483 | ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, | |
| 484 | else => {}, | |
| 485 | } | |
| 560 | if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; | |
| 486 | 561 | |
| 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 | } | |
| 562 | res.content_length = content_length; | |
| 563 | } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { | |
| 564 | if (res.content_encoding != .identity) return error.HttpHeadersInvalid; | |
| 515 | 565 | |
| 516 | if (next) |second| { | |
| 517 | const trimmed_second = mem.trim(u8, second, " "); | |
| 566 | const trimmed = mem.trim(u8, header_value, " "); | |
| 518 | 567 | |
| 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; | |
| 568 | if (http.ContentEncoding.fromString(trimmed)) |ce| { | |
| 569 | res.content_encoding = ce; | |
| 522 | 570 | } else { |
| 523 | return error.HttpTransferEncodingUnsupported; | |
| 571 | return error.HttpContentEncodingUnsupported; | |
| 524 | 572 | } |
| 525 | 573 | } |
| 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 | 574 | } |
| 575 | return error.HttpHeadersInvalid; // missing empty line | |
| 545 | 576 | } |
| 546 | return error.HttpHeadersInvalid; // missing empty line | |
| 547 | } | |
| 548 | ||
| 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 | }; | |
| 566 | 577 | |
| 567 | @memcpy(header_buffer[0..response_bytes.len], response_bytes); | |
| 568 | res.parser.header_bytes_len = response_bytes.len; | |
| 578 | test parse { | |
| 579 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 580 | "LOcation:url\r\n" ++ | |
| 581 | "content-tYpe: text/plain\r\n" ++ | |
| 582 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 583 | "content-Length:10\r\n" ++ | |
| 584 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 585 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 586 | ||
| 587 | const head = try Head.parse(response_bytes); | |
| 588 | ||
| 589 | try testing.expectEqual(.@"HTTP/1.1", head.version); | |
| 590 | try testing.expectEqualStrings("OK", head.reason); | |
| 591 | try testing.expectEqual(.ok, head.status); | |
| 592 | ||
| 593 | try testing.expectEqualStrings("url", head.location.?); | |
| 594 | try testing.expectEqualStrings("text/plain", head.content_type.?); | |
| 595 | try testing.expectEqualStrings("attachment; filename=example.txt", head.content_disposition.?); | |
| 596 | ||
| 597 | try testing.expectEqual(true, head.keep_alive); | |
| 598 | try testing.expectEqual(10, head.content_length.?); | |
| 599 | try testing.expectEqual(.chunked, head.transfer_encoding); | |
| 600 | try testing.expectEqual(.deflate, head.content_encoding); | |
| 601 | } | |
| 569 | 602 | |
| 570 | try res.parse(response_bytes); | |
| 603 | pub fn iterateHeaders(h: Head) http.HeaderIterator { | |
| 604 | return .init(h.bytes); | |
| 605 | } | |
| 571 | 606 | |
| 572 | try testing.expectEqual(.@"HTTP/1.1", res.version); | |
| 573 | try testing.expectEqualStrings("OK", res.reason); | |
| 574 | try testing.expectEqual(.ok, res.status); | |
| 607 | test iterateHeaders { | |
| 608 | const response_bytes = "HTTP/1.1 200 OK\r\n" ++ | |
| 609 | "LOcation:url\r\n" ++ | |
| 610 | "content-tYpe: text/plain\r\n" ++ | |
| 611 | "content-disposition:attachment; filename=example.txt \r\n" ++ | |
| 612 | "content-Length:10\r\n" ++ | |
| 613 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ | |
| 614 | "connectioN:\t keep-alive \r\n\r\n"; | |
| 615 | ||
| 616 | const head = try Head.parse(response_bytes); | |
| 617 | var it = head.iterateHeaders(); | |
| 618 | { | |
| 619 | const header = it.next().?; | |
| 620 | try testing.expectEqualStrings("LOcation", header.name); | |
| 621 | try testing.expectEqualStrings("url", header.value); | |
| 622 | try testing.expect(!it.is_trailer); | |
| 623 | } | |
| 624 | { | |
| 625 | const header = it.next().?; | |
| 626 | try testing.expectEqualStrings("content-tYpe", header.name); | |
| 627 | try testing.expectEqualStrings("text/plain", header.value); | |
| 628 | try testing.expect(!it.is_trailer); | |
| 629 | } | |
| 630 | { | |
| 631 | const header = it.next().?; | |
| 632 | try testing.expectEqualStrings("content-disposition", header.name); | |
| 633 | try testing.expectEqualStrings("attachment; filename=example.txt", header.value); | |
| 634 | try testing.expect(!it.is_trailer); | |
| 635 | } | |
| 636 | { | |
| 637 | const header = it.next().?; | |
| 638 | try testing.expectEqualStrings("content-Length", header.name); | |
| 639 | try testing.expectEqualStrings("10", header.value); | |
| 640 | try testing.expect(!it.is_trailer); | |
| 641 | } | |
| 642 | { | |
| 643 | const header = it.next().?; | |
| 644 | try testing.expectEqualStrings("TRansfer-encoding", header.name); | |
| 645 | try testing.expectEqualStrings("deflate, chunked", header.value); | |
| 646 | try testing.expect(!it.is_trailer); | |
| 647 | } | |
| 648 | { | |
| 649 | const header = it.next().?; | |
| 650 | try testing.expectEqualStrings("connectioN", header.name); | |
| 651 | try testing.expectEqualStrings("keep-alive", header.value); | |
| 652 | try testing.expect(!it.is_trailer); | |
| 653 | } | |
| 654 | try testing.expectEqual(null, it.next()); | |
| 655 | } | |
| 575 | 656 | |
| 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.?); | |
| 657 | inline fn int64(array: *const [8]u8) u64 { | |
| 658 | return @bitCast(array.*); | |
| 659 | } | |
| 579 | 660 | |
| 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 | } | |
| 661 | fn parseInt3(text: *const [3]u8) u10 { | |
| 662 | const nnn: @Vector(3, u8) = text.*; | |
| 663 | const zero: @Vector(3, u8) = .{ '0', '0', '0' }; | |
| 664 | const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; | |
| 665 | return @reduce(.Add, (nnn -% zero) *% mmm); | |
| 666 | } | |
| 585 | 667 | |
| 586 | inline fn int64(array: *const [8]u8) u64 { | |
| 587 | return @bitCast(array.*); | |
| 588 | } | |
| 668 | test parseInt3 { | |
| 669 | const expectEqual = testing.expectEqual; | |
| 670 | try expectEqual(@as(u10, 0), parseInt3("000")); | |
| 671 | try expectEqual(@as(u10, 418), parseInt3("418")); | |
| 672 | try expectEqual(@as(u10, 999), parseInt3("999")); | |
| 673 | } | |
| 674 | }; | |
| 589 | 675 | |
| 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); | |
| 676 | /// If compressed body has been negotiated this will return compressed bytes. | |
| 677 | /// | |
| 678 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 679 | /// available via `bodyErr`. | |
| 680 | /// | |
| 681 | /// Asserts that this function is only called once. | |
| 682 | /// | |
| 683 | /// See also: | |
| 684 | /// * `readerDecompressing` | |
| 685 | pub fn reader(response: *Response, buffer: []u8) *Reader { | |
| 686 | const req = response.request; | |
| 687 | if (!req.method.responseHasBody()) return .ending; | |
| 688 | const head = &response.head; | |
| 689 | return req.reader.bodyReader(buffer, head.transfer_encoding, head.content_length); | |
| 595 | 690 | } |
| 596 | 691 | |
| 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")); | |
| 692 | /// If compressed body has been negotiated this will return decompressed bytes. | |
| 693 | /// | |
| 694 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 695 | /// available via `bodyErr`. | |
| 696 | /// | |
| 697 | /// Asserts that this function is only called once. | |
| 698 | /// | |
| 699 | /// See also: | |
| 700 | /// * `reader` | |
| 701 | pub fn readerDecompressing( | |
| 702 | response: *Response, | |
| 703 | decompressor: *http.Decompressor, | |
| 704 | decompression_buffer: []u8, | |
| 705 | ) *Reader { | |
| 706 | const head = &response.head; | |
| 707 | return response.request.reader.bodyReaderDecompressing( | |
| 708 | head.transfer_encoding, | |
| 709 | head.content_length, | |
| 710 | head.content_encoding, | |
| 711 | decompressor, | |
| 712 | decompression_buffer, | |
| 713 | ); | |
| 602 | 714 | } |
| 603 | 715 | |
| 604 | pub fn iterateHeaders(r: Response) http.HeaderIterator { | |
| 605 | return .init(r.parser.get()); | |
| 716 | /// After receiving `error.ReadFailed` from the `Reader` returned by | |
| 717 | /// `reader` or `readerDecompressing`, this function accesses the | |
| 718 | /// more specific error code. | |
| 719 | pub fn bodyErr(response: *const Response) ?http.Reader.BodyError { | |
| 720 | return response.request.reader.body_err; | |
| 606 | 721 | } |
| 607 | 722 | |
| 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), | |
| 723 | pub fn iterateTrailers(response: *const Response) http.HeaderIterator { | |
| 724 | const r = &response.request.reader; | |
| 725 | assert(r.state == .ready); | |
| 726 | return .{ | |
| 727 | .bytes = r.trailers, | |
| 728 | .index = 0, | |
| 729 | .is_trailer = true, | |
| 624 | 730 | }; |
| 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 | 731 | } |
| 668 | 732 | }; |
| 669 | 733 | |
| 670 | /// A HTTP request that has been sent. | |
| 671 | /// | |
| 672 | /// Order of operations: open -> send[ -> write -> finish] -> wait -> read | |
| 673 | 734 | pub const Request = struct { |
| 735 | /// This field is provided so that clients can observe redirected URIs. | |
| 736 | /// | |
| 737 | /// Its backing memory is externally provided by API users when creating a | |
| 738 | /// request, and then again provided externally via `redirect_buffer` to | |
| 739 | /// `receiveHead`. | |
| 674 | 740 | uri: Uri, |
| 675 | 741 | client: *Client, |
| 676 | 742 | /// This is null when the connection is released. |
| 677 | 743 | connection: ?*Connection, |
| 744 | reader: http.Reader, | |
| 678 | 745 | keep_alive: bool, |
| 679 | 746 | |
| 680 | 747 | method: http.Method, |
| 681 | 748 | version: http.Version = .@"HTTP/1.1", |
| 682 | transfer_encoding: RequestTransfer, | |
| 749 | transfer_encoding: TransferEncoding, | |
| 683 | 750 | redirect_behavior: RedirectBehavior, |
| 751 | accept_encoding: @TypeOf(default_accept_encoding) = default_accept_encoding, | |
| 684 | 752 | |
| 685 | 753 | /// Whether the request should handle a 100-continue response before sending the request body. |
| 686 | 754 | handle_continue: bool, |
| 687 | 755 | |
| 688 | /// The response associated with this request. | |
| 689 | /// | |
| 690 | /// This field is undefined until `wait` is called. | |
| 691 | response: Response, | |
| 692 | ||
| 693 | 756 | /// Standard headers that have default, but overridable, behavior. |
| 694 | 757 | headers: Headers, |
| 695 | 758 | |
| ... | ... | @@ -703,6 +766,20 @@ pub const Request = struct { |
| 703 | 766 | /// Externally-owned; must outlive the Request. |
| 704 | 767 | privileged_headers: []const http.Header, |
| 705 | 768 | |
| 769 | pub const default_accept_encoding: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = b: { | |
| 770 | var result: [@typeInfo(http.ContentEncoding).@"enum".fields.len]bool = @splat(false); | |
| 771 | result[@intFromEnum(http.ContentEncoding.gzip)] = true; | |
| 772 | result[@intFromEnum(http.ContentEncoding.deflate)] = true; | |
| 773 | result[@intFromEnum(http.ContentEncoding.identity)] = true; | |
| 774 | break :b result; | |
| 775 | }; | |
| 776 | ||
| 777 | pub const TransferEncoding = union(enum) { | |
| 778 | content_length: u64, | |
| 779 | chunked: void, | |
| 780 | none: void, | |
| 781 | }; | |
| 782 | ||
| 706 | 783 | pub const Headers = struct { |
| 707 | 784 | host: Value = .default, |
| 708 | 785 | authorization: Value = .default, |
| ... | ... | @@ -742,98 +819,102 @@ pub const Request = struct { |
| 742 | 819 | } |
| 743 | 820 | }; |
| 744 | 821 | |
| 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); | |
| 822 | /// Returns the request's `Connection` back to the pool of the `Client`. | |
| 823 | pub fn deinit(r: *Request) void { | |
| 824 | r.reader.restituteHeadBuffer(); | |
| 825 | if (r.connection) |connection| { | |
| 826 | connection.closing = connection.closing or switch (r.reader.state) { | |
| 827 | .ready => false, | |
| 828 | .received_head => r.method.requestHasBody(), | |
| 829 | else => true, | |
| 830 | }; | |
| 831 | r.client.connection_pool.release(connection); | |
| 753 | 832 | } |
| 754 | req.* = undefined; | |
| 833 | r.* = undefined; | |
| 755 | 834 | } |
| 756 | 835 | |
| 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 | } | |
| 798 | ||
| 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 | }; | |
| 836 | /// Sends and flushes a complete request as only HTTP head, no body. | |
| 837 | pub fn sendBodiless(r: *Request) Writer.Error!void { | |
| 838 | try sendBodilessUnflushed(r); | |
| 839 | try r.connection.?.flush(); | |
| 811 | 840 | } |
| 812 | 841 | |
| 813 | pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; | |
| 842 | /// Sends but does not flush a complete request as only HTTP head, no body. | |
| 843 | pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { | |
| 844 | assert(r.transfer_encoding == .none); | |
| 845 | assert(!r.method.requestHasBody()); | |
| 846 | try sendHead(r); | |
| 847 | } | |
| 814 | 848 | |
| 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; | |
| 849 | /// Transfers the HTTP head over the connection and flushes. | |
| 850 | /// | |
| 851 | /// See also: | |
| 852 | /// * `sendBodyUnflushed` | |
| 853 | pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 854 | const result = try sendBodyUnflushed(r, buffer); | |
| 855 | try r.connection.?.flush(); | |
| 856 | return result; | |
| 857 | } | |
| 819 | 858 | |
| 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, | |
| 859 | /// Transfers the HTTP head over the connection, which is not flushed until | |
| 860 | /// `BodyWriter.flush` or `BodyWriter.end` is called. | |
| 861 | /// | |
| 862 | /// See also: | |
| 863 | /// * `sendBody` | |
| 864 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 865 | assert(r.method.requestHasBody()); | |
| 866 | try sendHead(r); | |
| 867 | const http_protocol_output = r.connection.?.writer(); | |
| 868 | return switch (r.transfer_encoding) { | |
| 869 | .chunked => .{ | |
| 870 | .http_protocol_output = http_protocol_output, | |
| 871 | .state = .{ .chunked = .init }, | |
| 872 | .writer = .{ | |
| 873 | .buffer = buffer, | |
| 874 | .vtable = &.{ | |
| 875 | .drain = http.BodyWriter.chunkedDrain, | |
| 876 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 877 | }, | |
| 878 | }, | |
| 879 | }, | |
| 880 | .content_length => |len| .{ | |
| 881 | .http_protocol_output = http_protocol_output, | |
| 882 | .state = .{ .content_length = len }, | |
| 883 | .writer = .{ | |
| 884 | .buffer = buffer, | |
| 885 | .vtable = &.{ | |
| 886 | .drain = http.BodyWriter.contentLengthDrain, | |
| 887 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 888 | }, | |
| 889 | }, | |
| 890 | }, | |
| 891 | .none => .{ | |
| 892 | .http_protocol_output = http_protocol_output, | |
| 893 | .state = .none, | |
| 894 | .writer = .{ | |
| 895 | .buffer = buffer, | |
| 896 | .vtable = &.{ | |
| 897 | .drain = http.BodyWriter.noneDrain, | |
| 898 | .sendFile = http.BodyWriter.noneSendFile, | |
| 899 | }, | |
| 900 | }, | |
| 901 | }, | |
| 826 | 902 | }; |
| 827 | 903 | } |
| 828 | 904 | |
| 829 | fn sendAdapted(req: *Request, connection: *Connection, w: *std.io.Writer) !void { | |
| 830 | try req.method.format(w); | |
| 905 | /// Sends HTTP headers without flushing. | |
| 906 | fn sendHead(r: *Request) Writer.Error!void { | |
| 907 | const uri = r.uri; | |
| 908 | const connection = r.connection.?; | |
| 909 | const w = connection.writer(); | |
| 910 | ||
| 911 | try r.method.write(w); | |
| 831 | 912 | try w.writeByte(' '); |
| 832 | 913 | |
| 833 | if (req.method == .CONNECT) { | |
| 834 | try req.uri.writeToStream(w, .{ .authority = true }); | |
| 914 | if (r.method == .CONNECT) { | |
| 915 | try uri.writeToStream(.{ .authority = true }, w); | |
| 835 | 916 | } else { |
| 836 | try req.uri.writeToStream(w, .{ | |
| 917 | try uri.writeToStream(.{ | |
| 837 | 918 | .scheme = connection.proxied, |
| 838 | 919 | .authentication = connection.proxied, |
| 839 | 920 | .authority = connection.proxied, |
| ... | ... | @@ -842,58 +923,64 @@ pub const Request = struct { |
| 842 | 923 | }); |
| 843 | 924 | } |
| 844 | 925 | try w.writeByte(' '); |
| 845 | try w.writeAll(@tagName(req.version)); | |
| 926 | try w.writeAll(@tagName(r.version)); | |
| 846 | 927 | try w.writeAll("\r\n"); |
| 847 | 928 | |
| 848 | if (try emitOverridableHeader("host: ", req.headers.host, w)) { | |
| 929 | if (try emitOverridableHeader("host: ", r.headers.host, w)) { | |
| 849 | 930 | try w.writeAll("host: "); |
| 850 | try req.uri.writeToStream(w, .{ .authority = true }); | |
| 931 | try uri.writeToStream(.{ .authority = true }, w); | |
| 851 | 932 | try w.writeAll("\r\n"); |
| 852 | 933 | } |
| 853 | 934 | |
| 854 | if (try emitOverridableHeader("authorization: ", req.headers.authorization, w)) { | |
| 855 | if (req.uri.user != null or req.uri.password != null) { | |
| 935 | if (try emitOverridableHeader("authorization: ", r.headers.authorization, w)) { | |
| 936 | if (uri.user != null or uri.password != null) { | |
| 856 | 937 | 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); | |
| 938 | try basic_authorization.write(uri, w); | |
| 861 | 939 | try w.writeAll("\r\n"); |
| 862 | 940 | } |
| 863 | 941 | } |
| 864 | 942 | |
| 865 | if (try emitOverridableHeader("user-agent: ", req.headers.user_agent, w)) { | |
| 943 | if (try emitOverridableHeader("user-agent: ", r.headers.user_agent, w)) { | |
| 866 | 944 | try w.writeAll("user-agent: zig/"); |
| 867 | 945 | try w.writeAll(builtin.zig_version_string); |
| 868 | 946 | try w.writeAll(" (std.http)\r\n"); |
| 869 | 947 | } |
| 870 | 948 | |
| 871 | if (try emitOverridableHeader("connection: ", req.headers.connection, w)) { | |
| 872 | if (req.keep_alive) { | |
| 949 | if (try emitOverridableHeader("connection: ", r.headers.connection, w)) { | |
| 950 | if (r.keep_alive) { | |
| 873 | 951 | try w.writeAll("connection: keep-alive\r\n"); |
| 874 | 952 | } else { |
| 875 | 953 | try w.writeAll("connection: close\r\n"); |
| 876 | 954 | } |
| 877 | 955 | } |
| 878 | 956 | |
| 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"); | |
| 957 | if (try emitOverridableHeader("accept-encoding: ", r.headers.accept_encoding, w)) { | |
| 958 | try w.writeAll("accept-encoding: "); | |
| 959 | for (r.accept_encoding, 0..) |enabled, i| { | |
| 960 | if (!enabled) continue; | |
| 961 | const tag: http.ContentEncoding = @enumFromInt(i); | |
| 962 | if (tag == .identity) continue; | |
| 963 | const tag_name = @tagName(tag); | |
| 964 | try w.ensureUnusedCapacity(tag_name.len + 2); | |
| 965 | try w.writeAll(tag_name); | |
| 966 | try w.writeAll(", "); | |
| 967 | } | |
| 968 | w.undo(2); | |
| 969 | try w.writeAll("\r\n"); | |
| 883 | 970 | } |
| 884 | 971 | |
| 885 | switch (req.transfer_encoding) { | |
| 972 | switch (r.transfer_encoding) { | |
| 886 | 973 | .chunked => try w.writeAll("transfer-encoding: chunked\r\n"), |
| 887 | 974 | .content_length => |len| try w.print("content-length: {d}\r\n", .{len}), |
| 888 | 975 | .none => {}, |
| 889 | 976 | } |
| 890 | 977 | |
| 891 | if (try emitOverridableHeader("content-type: ", req.headers.content_type, w)) { | |
| 978 | if (try emitOverridableHeader("content-type: ", r.headers.content_type, w)) { | |
| 892 | 979 | // The default is to omit content-type if not provided because |
| 893 | 980 | // "application/octet-stream" is redundant. |
| 894 | 981 | } |
| 895 | 982 | |
| 896 | for (req.extra_headers) |header| { | |
| 983 | for (r.extra_headers) |header| { | |
| 897 | 984 | assert(header.name.len != 0); |
| 898 | 985 | |
| 899 | 986 | try w.writeAll(header.name); |
| ... | ... | @@ -904,8 +991,8 @@ pub const Request = struct { |
| 904 | 991 | |
| 905 | 992 | if (connection.proxied) proxy: { |
| 906 | 993 | const proxy = switch (connection.protocol) { |
| 907 | .plain => req.client.http_proxy, | |
| 908 | .tls => req.client.https_proxy, | |
| 994 | .plain => r.client.http_proxy, | |
| 995 | .tls => r.client.https_proxy, | |
| 909 | 996 | } orelse break :proxy; |
| 910 | 997 | |
| 911 | 998 | const authorization = proxy.authorization orelse break :proxy; |
| ... | ... | @@ -915,282 +1002,198 @@ pub const Request = struct { |
| 915 | 1002 | } |
| 916 | 1003 | |
| 917 | 1004 | try w.writeAll("\r\n"); |
| 918 | ||
| 919 | try connection.flush(); | |
| 920 | 1005 | } |
| 921 | 1006 | |
| 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 | } | |
| 936 | ||
| 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 | } | |
| 1007 | pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{ | |
| 1008 | /// Server sent headers that did not conform to the HTTP protocol. | |
| 1009 | /// | |
| 1010 | /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be | |
| 1011 | /// passed directly to `Request.Head.parse`. | |
| 1012 | HttpHeadersInvalid, | |
| 1013 | TooManyHttpRedirects, | |
| 1014 | /// This can be avoided by calling `receiveHead` before sending the | |
| 1015 | /// request body. | |
| 1016 | RedirectRequiresResend, | |
| 1017 | HttpRedirectLocationMissing, | |
| 1018 | HttpRedirectLocationOversize, | |
| 1019 | HttpRedirectLocationInvalid, | |
| 1020 | HttpContentEncodingUnsupported, | |
| 1021 | HttpChunkInvalid, | |
| 1022 | HttpChunkTruncated, | |
| 1023 | HttpHeadersOversize, | |
| 1024 | UnsupportedUriScheme, | |
| 957 | 1025 | |
| 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 | }; | |
| 1026 | /// Sending the request failed. Error code can be found on the | |
| 1027 | /// `Connection` object. | |
| 1028 | WriteFailed, | |
| 1029 | }; | |
| 968 | 1030 | |
| 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 | 1031 | /// 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. | |
| 1032 | /// function will automatically follow redirects. | |
| 976 | 1033 | /// |
| 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 { | |
| 1034 | /// If a request payload is present, then this function will error with | |
| 1035 | /// `error.RedirectRequiresResend`. | |
| 1036 | /// | |
| 1037 | /// This function takes an auxiliary buffer to store the arbitrarily large | |
| 1038 | /// URI which may need to be merged with the previous URI, and that data | |
| 1039 | /// needs to survive across different connections, which is where the input | |
| 1040 | /// buffer lives. | |
| 1041 | /// | |
| 1042 | /// `redirect_buffer` must outlive accesses to `Request.uri`. If this | |
| 1043 | /// buffer capacity would be exceeded, `error.HttpRedirectLocationOversize` | |
| 1044 | /// is returned instead. This buffer may be empty if no redirects are to be | |
| 1045 | /// handled. | |
| 1046 | pub fn receiveHead(r: *Request, redirect_buffer: []u8) ReceiveHeadError!Response { | |
| 1047 | var aux_buf = redirect_buffer; | |
| 980 | 1048 | 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)); | |
| 1049 | try r.reader.receiveHead(); | |
| 1050 | const response: Response = .{ | |
| 1051 | .request = r, | |
| 1052 | .head = Response.Head.parse(r.reader.head_buffer) catch return error.HttpHeadersInvalid, | |
| 1053 | }; | |
| 1054 | const head = &response.head; | |
| 991 | 1055 | |
| 992 | if (req.response.parser.state.isContent()) break; | |
| 1056 | if (head.status == .@"continue") { | |
| 1057 | if (r.handle_continue) continue; | |
| 1058 | return response; // we're not handling the 100-continue | |
| 993 | 1059 | } |
| 994 | 1060 | |
| 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 | } | |
| 1061 | // This while loop is for handling redirects, which means the request's | |
| 1062 | // connection may be different than the previous iteration. However, it | |
| 1063 | // is still guaranteed to be non-null with each iteration of this loop. | |
| 1064 | const connection = r.connection.?; | |
| 1008 | 1065 | |
| 1009 | // we're switching protocols, so this connection is no longer doing http | |
| 1010 | if (req.method == .CONNECT and req.response.status.class() == .success) { | |
| 1066 | if (r.method == .CONNECT and head.status.class() == .success) { | |
| 1067 | // This connection is no longer doing HTTP. | |
| 1011 | 1068 | connection.closing = false; |
| 1012 | req.response.parser.done = true; | |
| 1013 | return; // the connection is not HTTP past this point | |
| 1069 | return response; | |
| 1014 | 1070 | } |
| 1015 | 1071 | |
| 1016 | connection.closing = !req.response.keep_alive or !req.keep_alive; | |
| 1072 | connection.closing = !head.keep_alive or !r.keep_alive; | |
| 1017 | 1073 | |
| 1018 | 1074 | // Any response to a HEAD request and any response with a 1xx |
| 1019 | 1075 | // (Informational), 204 (No Content), or 304 (Not Modified) status |
| 1020 | 1076 | // code is always terminated by the first empty line after the |
| 1021 | 1077 | // header fields, regardless of the header fields present in the |
| 1022 | 1078 | // 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) | |
| 1079 | if (r.method == .HEAD or head.status.class() == .informational or | |
| 1080 | head.status == .no_content or head.status == .not_modified) | |
| 1025 | 1081 | { |
| 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 | }, | |
| 1082 | return response; | |
| 1045 | 1083 | } |
| 1046 | 1084 | |
| 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 | } | |
| 1085 | if (head.status.class() == .redirect and r.redirect_behavior != .unhandled) { | |
| 1086 | if (r.redirect_behavior == .not_allowed) { | |
| 1087 | // Connection can still be reused by skipping the body. | |
| 1088 | const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); | |
| 1089 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1090 | error.ReadFailed => connection.closing = true, | |
| 1091 | }; | |
| 1092 | return error.TooManyHttpRedirects; | |
| 1087 | 1093 | } |
| 1088 | ||
| 1089 | break; | |
| 1094 | try r.redirect(head, &aux_buf); | |
| 1095 | try r.sendBodiless(); | |
| 1096 | continue; | |
| 1090 | 1097 | } |
| 1091 | } | |
| 1092 | } | |
| 1093 | ||
| 1094 | pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || | |
| 1095 | error{ DecompressionFailure, InvalidTrailers }; | |
| 1096 | 1098 | |
| 1097 | pub const Reader = std.io.GenericReader(*Request, ReadError, read); | |
| 1099 | if (!r.accept_encoding[@intFromEnum(head.content_encoding)]) | |
| 1100 | return error.HttpContentEncodingUnsupported; | |
| 1098 | 1101 | |
| 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)); | |
| 1102 | return response; | |
| 1120 | 1103 | } |
| 1121 | ||
| 1122 | return 0; | |
| 1123 | 1104 | } |
| 1124 | 1105 | |
| 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; | |
| 1106 | /// This function takes an auxiliary buffer to store the arbitrarily large | |
| 1107 | /// URI which may need to be merged with the previous URI, and that data | |
| 1108 | /// needs to survive across different connections, which is where the input | |
| 1109 | /// buffer lives. | |
| 1110 | /// | |
| 1111 | /// `aux_buf` must outlive accesses to `Request.uri`. | |
| 1112 | fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { | |
| 1113 | const new_location = head.location orelse return error.HttpRedirectLocationMissing; | |
| 1114 | if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; | |
| 1115 | const location = aux_buf.*[0..new_location.len]; | |
| 1116 | @memcpy(location, new_location); | |
| 1117 | { | |
| 1118 | // Skip the body of the redirect response to leave the connection in | |
| 1119 | // the correct state. This causes `new_location` to be invalidated. | |
| 1120 | const reader = r.reader.bodyReader(&.{}, head.transfer_encoding, head.content_length); | |
| 1121 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1122 | error.ReadFailed => return r.reader.body_err.?, | |
| 1123 | }; | |
| 1124 | r.reader.restituteHeadBuffer(); | |
| 1132 | 1125 | } |
| 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); | |
| 1126 | const new_uri = r.uri.resolveInPlace(location.len, aux_buf) catch |err| switch (err) { | |
| 1127 | error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, | |
| 1128 | error.InvalidFormat => return error.HttpRedirectLocationInvalid, | |
| 1129 | error.InvalidPort => return error.HttpRedirectLocationInvalid, | |
| 1130 | error.NoSpaceLeft => return error.HttpRedirectLocationOversize, | |
| 1131 | }; | |
| 1139 | 1132 | |
| 1140 | pub fn writer(req: *Request) Writer { | |
| 1141 | return .{ .context = req }; | |
| 1142 | } | |
| 1133 | const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; | |
| 1134 | const old_connection = r.connection.?; | |
| 1135 | const old_host = old_connection.host(); | |
| 1136 | var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; | |
| 1137 | const new_host = try new_uri.getHost(&new_host_name_buffer); | |
| 1138 | const keep_privileged_headers = | |
| 1139 | std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and | |
| 1140 | sameParentDomain(old_host, new_host); | |
| 1143 | 1141 | |
| 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 | } | |
| 1142 | r.client.connection_pool.release(old_connection); | |
| 1143 | r.connection = null; | |
| 1154 | 1144 | |
| 1155 | return bytes.len; | |
| 1156 | }, | |
| 1157 | .content_length => |*len| { | |
| 1158 | if (len.* < bytes.len) return error.MessageTooLong; | |
| 1145 | if (!keep_privileged_headers) { | |
| 1146 | // When redirecting to a different domain, strip privileged headers. | |
| 1147 | r.privileged_headers = &.{}; | |
| 1148 | } | |
| 1159 | 1149 | |
| 1160 | const amt = try req.connection.?.write(bytes); | |
| 1161 | len.* -= amt; | |
| 1162 | return amt; | |
| 1163 | }, | |
| 1164 | .none => return error.NotWriteable, | |
| 1150 | if (switch (head.status) { | |
| 1151 | .see_other => true, | |
| 1152 | .moved_permanently, .found => r.method == .POST, | |
| 1153 | else => false, | |
| 1154 | }) { | |
| 1155 | // A redirect to a GET must change the method and remove the body. | |
| 1156 | r.method = .GET; | |
| 1157 | r.transfer_encoding = .none; | |
| 1158 | r.headers.content_type = .omit; | |
| 1165 | 1159 | } |
| 1166 | } | |
| 1167 | 1160 | |
| 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..]); | |
| 1161 | if (r.transfer_encoding != .none) { | |
| 1162 | // The request body has already been sent. The request is | |
| 1163 | // still in a valid state, but the redirect must be handled | |
| 1164 | // manually. | |
| 1165 | return error.RedirectRequiresResend; | |
| 1174 | 1166 | } |
| 1175 | } | |
| 1176 | 1167 | |
| 1177 | pub const FinishError = WriteError || error{MessageNotCompleted}; | |
| 1168 | const new_connection = try r.client.connect(new_host, uriPort(new_uri, protocol), protocol); | |
| 1169 | r.uri = new_uri; | |
| 1170 | r.connection = new_connection; | |
| 1171 | r.reader = .{ | |
| 1172 | .in = new_connection.reader(), | |
| 1173 | .state = .ready, | |
| 1174 | // Populated when `http.Reader.bodyReader` is called. | |
| 1175 | .interface = undefined, | |
| 1176 | }; | |
| 1177 | r.redirect_behavior.subtractOne(); | |
| 1178 | } | |
| 1178 | 1179 | |
| 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 => {}, | |
| 1180 | /// Returns true if the default behavior is required, otherwise handles | |
| 1181 | /// writing (or not writing) the header. | |
| 1182 | fn emitOverridableHeader(prefix: []const u8, v: Headers.Value, bw: *Writer) Writer.Error!bool { | |
| 1183 | switch (v) { | |
| 1184 | .default => return true, | |
| 1185 | .omit => return false, | |
| 1186 | .override => |x| { | |
| 1187 | var vecs: [3][]const u8 = .{ prefix, x, "\r\n" }; | |
| 1188 | try bw.writeVecAll(&vecs); | |
| 1189 | return false; | |
| 1190 | }, | |
| 1186 | 1191 | } |
| 1187 | ||
| 1188 | try req.connection.?.flush(); | |
| 1189 | 1192 | } |
| 1190 | 1193 | }; |
| 1191 | 1194 | |
| 1192 | 1195 | pub const Proxy = struct { |
| 1193 | protocol: Connection.Protocol, | |
| 1196 | protocol: Protocol, | |
| 1194 | 1197 | host: []const u8, |
| 1195 | 1198 | authorization: ?[]const u8, |
| 1196 | 1199 | port: u16, |
| ... | ... | @@ -1204,10 +1207,8 @@ pub const Proxy = struct { |
| 1204 | 1207 | pub fn deinit(client: *Client) void { |
| 1205 | 1208 | assert(client.connection_pool.used.first == null); // There are still active requests. |
| 1206 | 1209 | |
| 1207 | client.connection_pool.deinit(client.allocator); | |
| 1208 | ||
| 1209 | if (!disable_tls) | |
| 1210 | client.ca_bundle.deinit(client.allocator); | |
| 1210 | client.connection_pool.deinit(); | |
| 1211 | if (!disable_tls) client.ca_bundle.deinit(client.allocator); | |
| 1211 | 1212 | |
| 1212 | 1213 | client.* = undefined; |
| 1213 | 1214 | } |
| ... | ... | @@ -1249,24 +1250,21 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !? |
| 1249 | 1250 | } else return null; |
| 1250 | 1251 | |
| 1251 | 1252 | 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 | }; | |
| 1253 | const protocol = Protocol.fromUri(uri) orelse return null; | |
| 1254 | const raw_host = try uri.getHostAlloc(arena); | |
| 1257 | 1255 | |
| 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); | |
| 1256 | const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: { | |
| 1257 | const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri)); | |
| 1258 | assert(basic_authorization.value(uri, authorization).len == authorization.len); | |
| 1261 | 1259 | break :a authorization; |
| 1262 | 1260 | } else null; |
| 1263 | 1261 | |
| 1264 | 1262 | const proxy = try arena.create(Proxy); |
| 1265 | 1263 | proxy.* = .{ |
| 1266 | 1264 | .protocol = protocol, |
| 1267 | .host = valid_uri.host.?.raw, | |
| 1265 | .host = raw_host, | |
| 1268 | 1266 | .authorization = authorization, |
| 1269 | .port = uriPort(valid_uri, protocol), | |
| 1267 | .port = uriPort(uri, protocol), | |
| 1270 | 1268 | .supports_connect = true, |
| 1271 | 1269 | }; |
| 1272 | 1270 | return proxy; |
| ... | ... | @@ -1277,10 +1275,8 @@ pub const basic_authorization = struct { |
| 1277 | 1275 | pub const max_password_len = 255; |
| 1278 | 1276 | pub const max_value_len = valueLength(max_user_len, max_password_len); |
| 1279 | 1277 | |
| 1280 | const prefix = "Basic "; | |
| 1281 | ||
| 1282 | 1278 | pub fn valueLength(user_len: usize, password_len: usize) usize { |
| 1283 | return prefix.len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); | |
| 1279 | return "Basic ".len + std.base64.standard.Encoder.calcSize(user_len + 1 + password_len); | |
| 1284 | 1280 | } |
| 1285 | 1281 | |
| 1286 | 1282 | pub fn valueLengthFromUri(uri: Uri) usize { |
| ... | ... | @@ -1300,37 +1296,69 @@ pub const basic_authorization = struct { |
| 1300 | 1296 | } |
| 1301 | 1297 | |
| 1302 | 1298 | 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; | |
| 1299 | var bw: Writer = .fixed(out); | |
| 1300 | write(uri, &bw) catch unreachable; | |
| 1301 | return bw.getWritten(); | |
| 1302 | } | |
| 1305 | 1303 | |
| 1304 | pub fn write(uri: Uri, out: *Writer) Writer.Error!void { | |
| 1306 | 1305 | 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 | |
| 1310 | ||
| 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]; | |
| 1306 | var w: Writer = .fixed(&buf); | |
| 1307 | w.print("{fuser}:{fpassword}", .{ | |
| 1308 | uri.user orelse Uri.Component.empty, | |
| 1309 | uri.password orelse Uri.Component.empty, | |
| 1310 | }) catch unreachable; | |
| 1311 | try out.print("Basic {b64}", .{w.buffered()}); | |
| 1314 | 1312 | } |
| 1315 | 1313 | }; |
| 1316 | 1314 | |
| 1317 | pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; | |
| 1315 | pub const ConnectTcpError = Allocator.Error || error{ | |
| 1316 | ConnectionRefused, | |
| 1317 | NetworkUnreachable, | |
| 1318 | ConnectionTimedOut, | |
| 1319 | ConnectionResetByPeer, | |
| 1320 | TemporaryNameServerFailure, | |
| 1321 | NameServerFailure, | |
| 1322 | UnknownHostName, | |
| 1323 | HostLacksNetworkAddresses, | |
| 1324 | UnexpectedConnectFailure, | |
| 1325 | TlsInitializationFailed, | |
| 1326 | }; | |
| 1318 | 1327 | |
| 1319 | /// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. | |
| 1328 | /// Reuses a `Connection` if one matching `host` and `port` is already open. | |
| 1320 | 1329 | /// |
| 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; | |
| 1330 | /// Threadsafe. | |
| 1331 | pub fn connectTcp( | |
| 1332 | client: *Client, | |
| 1333 | host: []const u8, | |
| 1334 | port: u16, | |
| 1335 | protocol: Protocol, | |
| 1336 | ) ConnectTcpError!*Connection { | |
| 1337 | return connectTcpOptions(client, .{ .host = host, .port = port, .protocol = protocol }); | |
| 1338 | } | |
| 1339 | ||
| 1340 | pub const ConnectTcpOptions = struct { | |
| 1341 | host: []const u8, | |
| 1342 | port: u16, | |
| 1343 | protocol: Protocol, | |
| 1328 | 1344 | |
| 1329 | if (disable_tls and protocol == .tls) | |
| 1330 | return error.TlsInitializationFailed; | |
| 1345 | proxied_host: ?[]const u8 = null, | |
| 1346 | proxied_port: ?u16 = null, | |
| 1347 | }; | |
| 1331 | 1348 | |
| 1332 | const conn = try client.allocator.create(Connection); | |
| 1333 | errdefer client.allocator.destroy(conn); | |
| 1349 | pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { | |
| 1350 | const host = options.host; | |
| 1351 | const port = options.port; | |
| 1352 | const protocol = options.protocol; | |
| 1353 | ||
| 1354 | const proxied_host = options.proxied_host orelse host; | |
| 1355 | const proxied_port = options.proxied_port orelse port; | |
| 1356 | ||
| 1357 | if (client.connection_pool.findConnection(.{ | |
| 1358 | .host = proxied_host, | |
| 1359 | .port = proxied_port, | |
| 1360 | .protocol = protocol, | |
| 1361 | })) |conn| return conn; | |
| 1334 | 1362 | |
| 1335 | 1363 | const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { |
| 1336 | 1364 | error.ConnectionRefused => return error.ConnectionRefused, |
| ... | ... | @@ -1345,53 +1373,19 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec |
| 1345 | 1373 | }; |
| 1346 | 1374 | errdefer stream.close(); |
| 1347 | 1375 | |
| 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; | |
| 1376 | switch (protocol) { | |
| 1377 | .tls => { | |
| 1378 | if (disable_tls) return error.TlsInitializationFailed; | |
| 1379 | const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); | |
| 1380 | client.connection_pool.addUsed(&tc.connection); | |
| 1381 | return &tc.connection; | |
| 1382 | }, | |
| 1383 | .plain => { | |
| 1384 | const pc = try Connection.Plain.create(client, proxied_host, proxied_port, stream); | |
| 1385 | client.connection_pool.addUsed(&pc.connection); | |
| 1386 | return &pc.connection; | |
| 1387 | }, | |
| 1390 | 1388 | } |
| 1391 | ||
| 1392 | client.connection_pool.addUsed(conn); | |
| 1393 | ||
| 1394 | return conn; | |
| 1395 | 1389 | } |
| 1396 | 1390 | |
| 1397 | 1391 | pub const ConnectUnixError = Allocator.Error || std.posix.SocketError || error{NameTooLong} || std.posix.ConnectError; |
| ... | ... | @@ -1429,69 +1423,67 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti |
| 1429 | 1423 | return &conn.data; |
| 1430 | 1424 | } |
| 1431 | 1425 | |
| 1432 | /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP | |
| 1426 | /// Connect to `proxied_host:proxied_port` using the specified proxy with HTTP | |
| 1433 | 1427 | /// CONNECT. This will reuse a connection if one is already open. |
| 1434 | 1428 | /// |
| 1435 | 1429 | /// This function is threadsafe. |
| 1436 | pub fn connectTunnel( | |
| 1430 | pub fn connectProxied( | |
| 1437 | 1431 | client: *Client, |
| 1438 | 1432 | proxy: *Proxy, |
| 1439 | tunnel_host: []const u8, | |
| 1440 | tunnel_port: u16, | |
| 1433 | proxied_host: []const u8, | |
| 1434 | proxied_port: u16, | |
| 1441 | 1435 | ) !*Connection { |
| 1442 | 1436 | if (!proxy.supports_connect) return error.TunnelNotSupported; |
| 1443 | 1437 | |
| 1444 | 1438 | if (client.connection_pool.findConnection(.{ |
| 1445 | .host = tunnel_host, | |
| 1446 | .port = tunnel_port, | |
| 1439 | .host = proxied_host, | |
| 1440 | .port = proxied_port, | |
| 1447 | 1441 | .protocol = proxy.protocol, |
| 1448 | })) |node| | |
| 1449 | return node; | |
| 1442 | })) |node| return node; | |
| 1450 | 1443 | |
| 1451 | 1444 | var maybe_valid = false; |
| 1452 | 1445 | (tunnel: { |
| 1453 | const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); | |
| 1446 | const connection = try client.connectTcpOptions(.{ | |
| 1447 | .host = proxy.host, | |
| 1448 | .port = proxy.port, | |
| 1449 | .protocol = proxy.protocol, | |
| 1450 | .proxied_host = proxied_host, | |
| 1451 | .proxied_port = proxied_port, | |
| 1452 | }); | |
| 1454 | 1453 | errdefer { |
| 1455 | conn.closing = true; | |
| 1456 | client.connection_pool.release(client.allocator, conn); | |
| 1454 | connection.closing = true; | |
| 1455 | client.connection_pool.release(connection); | |
| 1457 | 1456 | } |
| 1458 | 1457 | |
| 1459 | var buffer: [8096]u8 = undefined; | |
| 1460 | var req = client.open(.CONNECT, .{ | |
| 1458 | var req = client.request(.CONNECT, .{ | |
| 1461 | 1459 | .scheme = "http", |
| 1462 | .host = .{ .raw = tunnel_host }, | |
| 1463 | .port = tunnel_port, | |
| 1460 | .host = .{ .raw = proxied_host }, | |
| 1461 | .port = proxied_port, | |
| 1464 | 1462 | }, .{ |
| 1465 | 1463 | .redirect_behavior = .unhandled, |
| 1466 | .connection = conn, | |
| 1467 | .server_header_buffer = &buffer, | |
| 1464 | .connection = connection, | |
| 1468 | 1465 | }) catch |err| { |
| 1469 | std.log.debug("err {}", .{err}); | |
| 1470 | 1466 | break :tunnel err; |
| 1471 | 1467 | }; |
| 1472 | 1468 | defer req.deinit(); |
| 1473 | 1469 | |
| 1474 | req.send() catch |err| break :tunnel err; | |
| 1475 | req.wait() catch |err| break :tunnel err; | |
| 1470 | req.sendBodiless() catch |err| break :tunnel err; | |
| 1471 | const response = req.receiveHead(&.{}) catch |err| break :tunnel err; | |
| 1476 | 1472 | |
| 1477 | if (req.response.status.class() == .server_error) { | |
| 1473 | if (response.head.status.class() == .server_error) { | |
| 1478 | 1474 | maybe_valid = true; |
| 1479 | 1475 | break :tunnel error.ServerError; |
| 1480 | 1476 | } |
| 1481 | 1477 | |
| 1482 | if (req.response.status != .ok) break :tunnel error.ConnectionRefused; | |
| 1478 | if (response.head.status != .ok) break :tunnel error.ConnectionRefused; | |
| 1483 | 1479 | |
| 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. | |
| 1480 | // this connection is now a tunnel, so we can't use it for anything | |
| 1481 | // else, it will only be released when the client is de-initialized. | |
| 1485 | 1482 | req.connection = null; |
| 1486 | 1483 | |
| 1487 | client.allocator.free(conn.host); | |
| 1488 | conn.host = try client.allocator.dupe(u8, tunnel_host); | |
| 1489 | errdefer client.allocator.free(conn.host); | |
| 1484 | connection.closing = false; | |
| 1490 | 1485 | |
| 1491 | conn.port = tunnel_port; | |
| 1492 | conn.closing = false; | |
| 1493 | ||
| 1494 | return conn; | |
| 1486 | return connection; | |
| 1495 | 1487 | }) catch { |
| 1496 | 1488 | // something went wrong with the tunnel |
| 1497 | 1489 | proxy.supports_connect = maybe_valid; |
| ... | ... | @@ -1499,12 +1491,11 @@ pub fn connectTunnel( |
| 1499 | 1491 | }; |
| 1500 | 1492 | } |
| 1501 | 1493 | |
| 1502 | // Prevents a dependency loop in open() | |
| 1503 | const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUriScheme, ConnectionRefused }; | |
| 1504 | pub const ConnectError = ConnectErrorPartial || RequestError; | |
| 1494 | pub const ConnectError = ConnectTcpError || RequestError; | |
| 1505 | 1495 | |
| 1506 | 1496 | /// Connect to `host:port` using the specified protocol. This will reuse a |
| 1507 | 1497 | /// connection if one is already open. |
| 1498 | /// | |
| 1508 | 1499 | /// If a proxy is configured for the client, then the proxy will be used to |
| 1509 | 1500 | /// connect to the host. |
| 1510 | 1501 | /// |
| ... | ... | @@ -1513,7 +1504,7 @@ pub fn connect( |
| 1513 | 1504 | client: *Client, |
| 1514 | 1505 | host: []const u8, |
| 1515 | 1506 | port: u16, |
| 1516 | protocol: Connection.Protocol, | |
| 1507 | protocol: Protocol, | |
| 1517 | 1508 | ) ConnectError!*Connection { |
| 1518 | 1509 | const proxy = switch (protocol) { |
| 1519 | 1510 | .plain => client.http_proxy, |
| ... | ... | @@ -1528,32 +1519,24 @@ pub fn connect( |
| 1528 | 1519 | } |
| 1529 | 1520 | |
| 1530 | 1521 | if (proxy.supports_connect) tunnel: { |
| 1531 | return connectTunnel(client, proxy, host, port) catch |err| switch (err) { | |
| 1522 | return connectProxied(client, proxy, host, port) catch |err| switch (err) { | |
| 1532 | 1523 | error.TunnelNotSupported => break :tunnel, |
| 1533 | 1524 | else => |e| return e, |
| 1534 | 1525 | }; |
| 1535 | 1526 | } |
| 1536 | 1527 | |
| 1537 | 1528 | // 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; | |
| 1529 | const connection = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); | |
| 1530 | connection.proxied = true; | |
| 1531 | return connection; | |
| 1546 | 1532 | } |
| 1547 | 1533 | |
| 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 | }; | |
| 1534 | pub const RequestError = ConnectTcpError || error{ | |
| 1535 | UnsupportedUriScheme, | |
| 1536 | UriMissingHost, | |
| 1537 | UriHostTooLong, | |
| 1538 | CertificateBundleLoadFailure, | |
| 1539 | }; | |
| 1557 | 1540 | |
| 1558 | 1541 | pub const RequestOptions = struct { |
| 1559 | 1542 | version: http.Version = .@"HTTP/1.1", |
| ... | ... | @@ -1578,11 +1561,6 @@ pub const RequestOptions = struct { |
| 1578 | 1561 | /// payload or the server has acknowledged the payload). |
| 1579 | 1562 | redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), |
| 1580 | 1563 | |
| 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 | 1564 | /// Must be an already acquired connection. |
| 1587 | 1565 | connection: ?*Connection = null, |
| 1588 | 1566 | |
| ... | ... | @@ -1598,38 +1576,17 @@ pub const RequestOptions = struct { |
| 1598 | 1576 | privileged_headers: []const http.Header = &.{}, |
| 1599 | 1577 | }; |
| 1600 | 1578 | |
| 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 | }; | |
| 1579 | fn uriPort(uri: Uri, protocol: Protocol) u16 { | |
| 1580 | return uri.port orelse protocol.port(); | |
| 1622 | 1581 | } |
| 1623 | 1582 | |
| 1624 | 1583 | /// Open a connection to the host specified by `uri` and prepare to send a HTTP request. |
| 1625 | 1584 | /// |
| 1626 | /// `uri` must remain alive during the entire request. | |
| 1627 | /// | |
| 1628 | 1585 | /// The caller is responsible for calling `deinit()` on the `Request`. |
| 1629 | 1586 | /// This function is threadsafe. |
| 1630 | 1587 | /// |
| 1631 | 1588 | /// Asserts that "\r\n" does not occur in any header name or value. |
| 1632 | pub fn open( | |
| 1589 | pub fn request( | |
| 1633 | 1590 | client: *Client, |
| 1634 | 1591 | method: http.Method, |
| 1635 | 1592 | uri: Uri, |
| ... | ... | @@ -1649,59 +1606,58 @@ pub fn open( |
| 1649 | 1606 | } |
| 1650 | 1607 | } |
| 1651 | 1608 | |
| 1652 | var server_header: std.heap.FixedBufferAllocator = .init(options.server_header_buffer); | |
| 1653 | const protocol, const valid_uri = try validateUri(uri, server_header.allocator()); | |
| 1609 | const protocol = Protocol.fromUri(uri) orelse return error.UnsupportedUriScheme; | |
| 1654 | 1610 | |
| 1655 | if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { | |
| 1611 | if (protocol == .tls) { | |
| 1656 | 1612 | 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); | |
| 1613 | if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { | |
| 1614 | client.ca_bundle_mutex.lock(); | |
| 1615 | defer client.ca_bundle_mutex.unlock(); | |
| 1616 | ||
| 1617 | if (client.next_https_rescan_certs) { | |
| 1618 | client.ca_bundle.rescan(client.allocator) catch | |
| 1619 | return error.CertificateBundleLoadFailure; | |
| 1620 | @atomicStore(bool, &client.next_https_rescan_certs, false, .release); | |
| 1621 | } | |
| 1665 | 1622 | } |
| 1666 | 1623 | } |
| 1667 | 1624 | |
| 1668 | const conn = options.connection orelse | |
| 1669 | try client.connect(valid_uri.host.?.raw, uriPort(valid_uri, protocol), protocol); | |
| 1625 | const connection = options.connection orelse c: { | |
| 1626 | var host_name_buffer: [Uri.host_name_max]u8 = undefined; | |
| 1627 | const host_name = try uri.getHost(&host_name_buffer); | |
| 1628 | break :c try client.connect(host_name, uriPort(uri, protocol), protocol); | |
| 1629 | }; | |
| 1670 | 1630 | |
| 1671 | var req: Request = .{ | |
| 1672 | .uri = valid_uri, | |
| 1631 | return .{ | |
| 1632 | .uri = uri, | |
| 1673 | 1633 | .client = client, |
| 1674 | .connection = conn, | |
| 1634 | .connection = connection, | |
| 1635 | .reader = .{ | |
| 1636 | .in = connection.reader(), | |
| 1637 | .state = .ready, | |
| 1638 | // Populated when `http.Reader.bodyReader` is called. | |
| 1639 | .interface = undefined, | |
| 1640 | }, | |
| 1675 | 1641 | .keep_alive = options.keep_alive, |
| 1676 | 1642 | .method = method, |
| 1677 | 1643 | .version = options.version, |
| 1678 | 1644 | .transfer_encoding = .none, |
| 1679 | 1645 | .redirect_behavior = options.redirect_behavior, |
| 1680 | 1646 | .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 | 1647 | .headers = options.headers, |
| 1689 | 1648 | .extra_headers = options.extra_headers, |
| 1690 | 1649 | .privileged_headers = options.privileged_headers, |
| 1691 | 1650 | }; |
| 1692 | errdefer req.deinit(); | |
| 1693 | ||
| 1694 | return req; | |
| 1695 | 1651 | } |
| 1696 | 1652 | |
| 1697 | 1653 | pub const FetchOptions = struct { |
| 1698 | server_header_buffer: ?[]u8 = null, | |
| 1654 | /// `null` means it will be heap-allocated. | |
| 1655 | redirect_buffer: ?[]u8 = null, | |
| 1656 | /// `null` means it will be heap-allocated. | |
| 1657 | decompress_buffer: ?[]u8 = null, | |
| 1699 | 1658 | 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, | |
| 1659 | /// If the server sends a body, it will be stored here. | |
| 1660 | response_storage: ?ResponseStorage = null, | |
| 1705 | 1661 | |
| 1706 | 1662 | location: Location, |
| 1707 | 1663 | method: ?http.Method = null, |
| ... | ... | @@ -1725,11 +1681,11 @@ pub const FetchOptions = struct { |
| 1725 | 1681 | uri: Uri, |
| 1726 | 1682 | }; |
| 1727 | 1683 | |
| 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), | |
| 1684 | pub const ResponseStorage = struct { | |
| 1685 | list: *std.ArrayListUnmanaged(u8), | |
| 1686 | /// If null then only the existing capacity will be used. | |
| 1687 | allocator: ?Allocator = null, | |
| 1688 | append_limit: std.io.Limit = .unlimited, | |
| 1733 | 1689 | }; |
| 1734 | 1690 | }; |
| 1735 | 1691 | |
| ... | ... | @@ -1737,23 +1693,28 @@ pub const FetchResult = struct { |
| 1737 | 1693 | status: http.Status, |
| 1738 | 1694 | }; |
| 1739 | 1695 | |
| 1696 | pub const FetchError = Uri.ParseError || RequestError || Request.ReceiveHeadError || error{ | |
| 1697 | StreamTooLong, | |
| 1698 | /// TODO provide optional diagnostics when this occurs or break into more error codes | |
| 1699 | WriteFailed, | |
| 1700 | }; | |
| 1701 | ||
| 1740 | 1702 | /// Perform a one-shot HTTP request with the provided options. |
| 1741 | 1703 | /// |
| 1742 | 1704 | /// This function is threadsafe. |
| 1743 | pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { | |
| 1705 | pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { | |
| 1744 | 1706 | const uri = switch (options.location) { |
| 1745 | 1707 | .url => |u| try Uri.parse(u), |
| 1746 | 1708 | .uri => |u| u, |
| 1747 | 1709 | }; |
| 1748 | var server_header_buffer: [16 * 1024]u8 = undefined; | |
| 1749 | ||
| 1750 | 1710 | const method: http.Method = options.method orelse |
| 1751 | 1711 | if (options.payload != null) .POST else .GET; |
| 1752 | 1712 | |
| 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, | |
| 1713 | const redirect_behavior: Request.RedirectBehavior = options.redirect_behavior orelse | |
| 1714 | if (options.payload == null) @enumFromInt(3) else .unhandled; | |
| 1715 | ||
| 1716 | var req = try request(client, method, uri, .{ | |
| 1717 | .redirect_behavior = redirect_behavior, | |
| 1757 | 1718 | .headers = options.headers, |
| 1758 | 1719 | .extra_headers = options.extra_headers, |
| 1759 | 1720 | .privileged_headers = options.privileged_headers, |
| ... | ... | @@ -1761,44 +1722,69 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult { |
| 1761 | 1722 | }); |
| 1762 | 1723 | defer req.deinit(); |
| 1763 | 1724 | |
| 1764 | if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len }; | |
| 1725 | if (options.payload) |payload| { | |
| 1726 | req.transfer_encoding = .{ .content_length = payload.len }; | |
| 1727 | var body = try req.sendBody(&.{}); | |
| 1728 | try body.writer.writeAll(payload); | |
| 1729 | try body.end(); | |
| 1730 | } else { | |
| 1731 | try req.sendBodiless(); | |
| 1732 | } | |
| 1765 | 1733 | |
| 1766 | try req.send(); | |
| 1734 | const redirect_buffer: []u8 = if (redirect_behavior == .unhandled) &.{} else options.redirect_buffer orelse | |
| 1735 | try client.allocator.alloc(u8, 8 * 1024); | |
| 1736 | defer if (options.redirect_buffer == null) client.allocator.free(redirect_buffer); | |
| 1767 | 1737 | |
| 1768 | if (options.payload) |payload| try req.writeAll(payload); | |
| 1738 | var response = try req.receiveHead(redirect_buffer); | |
| 1769 | 1739 | |
| 1770 | try req.finish(); | |
| 1771 | try req.wait(); | |
| 1740 | const storage = options.response_storage orelse { | |
| 1741 | const reader = response.reader(&.{}); | |
| 1742 | _ = reader.discardRemaining() catch |err| switch (err) { | |
| 1743 | error.ReadFailed => return response.bodyErr().?, | |
| 1744 | }; | |
| 1745 | return .{ .status = response.head.status }; | |
| 1746 | }; | |
| 1772 | 1747 | |
| 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 | }, | |
| 1748 | const decompress_buffer: []u8 = switch (response.head.content_encoding) { | |
| 1749 | .identity => &.{}, | |
| 1750 | .zstd => options.decompress_buffer orelse try client.allocator.alloc(u8, std.compress.zstd.default_window_len), | |
| 1751 | else => options.decompress_buffer orelse try client.allocator.alloc(u8, 8 * 1024), | |
| 1752 | }; | |
| 1753 | defer if (options.decompress_buffer == null) client.allocator.free(decompress_buffer); | |
| 1754 | ||
| 1755 | var decompressor: http.Decompressor = undefined; | |
| 1756 | const reader = response.readerDecompressing(&decompressor, decompress_buffer); | |
| 1757 | const list = storage.list; | |
| 1758 | ||
| 1759 | if (storage.allocator) |allocator| { | |
| 1760 | reader.appendRemaining(allocator, null, list, storage.append_limit) catch |err| switch (err) { | |
| 1761 | error.ReadFailed => return response.bodyErr().?, | |
| 1762 | else => |e| return e, | |
| 1763 | }; | |
| 1764 | } else { | |
| 1765 | const buf = storage.append_limit.slice(list.unusedCapacitySlice()); | |
| 1766 | list.items.len += reader.readSliceShort(buf) catch |err| switch (err) { | |
| 1767 | error.ReadFailed => return response.bodyErr().?, | |
| 1768 | }; | |
| 1794 | 1769 | } |
| 1795 | 1770 | |
| 1796 | return .{ | |
| 1797 | .status = req.response.status, | |
| 1798 | }; | |
| 1771 | return .{ .status = response.head.status }; | |
| 1772 | } | |
| 1773 | ||
| 1774 | pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { | |
| 1775 | if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; | |
| 1776 | if (child_host.len == parent_host.len) return true; | |
| 1777 | if (parent_host.len > child_host.len) return false; | |
| 1778 | return child_host[child_host.len - parent_host.len - 1] == '.'; | |
| 1779 | } | |
| 1780 | ||
| 1781 | test sameParentDomain { | |
| 1782 | try testing.expect(!sameParentDomain("foo.com", "bar.com")); | |
| 1783 | try testing.expect(sameParentDomain("foo.com", "foo.com")); | |
| 1784 | try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); | |
| 1785 | try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); | |
| 1799 | 1786 | } |
| 1800 | 1787 | |
| 1801 | 1788 | test { |
| 1802 | 1789 | _ = Response; |
| 1803 | _ = &initDefaultProxies; | |
| 1804 | 1790 | } |
lib/std/http/Server.zig+385-747| ... | ... | @@ -1,139 +1,70 @@ |
| 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 | ||
| 11 | const Server = @This(); | |
| 12 | ||
| 13 | /// Data from the HTTP server to the HTTP client. | |
| 14 | out: *Writer, | |
| 15 | reader: http.Reader, | |
| 31 | 16 | |
| 32 | 17 | /// Initialize an HTTP server that can respond to multiple requests on the same |
| 33 | 18 | /// connection. |
| 19 | /// | |
| 20 | /// The buffer of `in` must be large enough to store the client's entire HTTP | |
| 21 | /// header, otherwise `receiveHead` returns `error.HttpHeadersOversize`. | |
| 22 | /// | |
| 34 | 23 | /// The returned `Server` is ready for `receiveHead` to be called. |
| 35 | pub fn init(connection: net.Server.Connection, read_buffer: []u8) Server { | |
| 24 | pub fn init(in: *std.io.Reader, out: *Writer) Server { | |
| 36 | 25 | return .{ |
| 37 | .connection = connection, | |
| 38 | .state = .ready, | |
| 39 | .read_buffer = read_buffer, | |
| 40 | .read_buffer_len = 0, | |
| 41 | .next_request_start = 0, | |
| 26 | .reader = .{ | |
| 27 | .in = in, | |
| 28 | .state = .ready, | |
| 29 | // Populated when `http.Reader.bodyReader` is called. | |
| 30 | .interface = undefined, | |
| 31 | }, | |
| 32 | .out = out, | |
| 42 | 33 | }; |
| 43 | 34 | } |
| 44 | 35 | |
| 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, | |
| 36 | pub fn deinit(s: *Server) void { | |
| 37 | s.reader.restituteHeadBuffer(); | |
| 38 | } | |
| 39 | ||
| 40 | pub const ReceiveHeadError = http.Reader.HeadError || error{ | |
| 50 | 41 | /// Client sent headers that did not conform to the HTTP protocol. |
| 42 | /// | |
| 43 | /// To find out more detailed diagnostics, `http.Reader.head_buffer` can be | |
| 44 | /// passed directly to `Request.Head.parse`. | |
| 51 | 45 | 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 | 46 | }; |
| 61 | 47 | |
| 62 | /// The header bytes reference the read buffer that Server was initialized with | |
| 63 | /// and remain alive until the next call to receiveHead. | |
| 64 | 48 | 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 { | |
| 49 | try s.reader.receiveHead(); | |
| 110 | 50 | return .{ |
| 111 | 51 | .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, | |
| 52 | // No need to track the returned error here since users can repeat the | |
| 53 | // parse with the header buffer to get detailed diagnostics. | |
| 54 | .head = Request.Head.parse(s.reader.head_buffer) catch return error.HttpHeadersInvalid, | |
| 116 | 55 | }; |
| 117 | 56 | } |
| 118 | 57 | |
| 119 | 58 | pub const Request = struct { |
| 120 | 59 | server: *Server, |
| 121 | /// Index into Server's read_buffer. | |
| 122 | head_end: usize, | |
| 60 | /// Pointers in this struct are invalidated with the next call to | |
| 61 | /// `receiveHead`. | |
| 123 | 62 | 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, | |
| 63 | respond_err: ?RespondError = null, | |
| 64 | ||
| 65 | pub const RespondError = error{ | |
| 66 | /// The request contained an `expect` header with an unrecognized value. | |
| 67 | HttpExpectationFailed, | |
| 137 | 68 | }; |
| 138 | 69 | |
| 139 | 70 | pub const Head = struct { |
| ... | ... | @@ -146,7 +77,6 @@ pub const Request = struct { |
| 146 | 77 | transfer_encoding: http.TransferEncoding, |
| 147 | 78 | transfer_compression: http.ContentEncoding, |
| 148 | 79 | keep_alive: bool, |
| 149 | compression: Compression, | |
| 150 | 80 | |
| 151 | 81 | pub const ParseError = error{ |
| 152 | 82 | UnknownHttpMethod, |
| ... | ... | @@ -200,7 +130,6 @@ pub const Request = struct { |
| 200 | 130 | .@"HTTP/1.0" => false, |
| 201 | 131 | .@"HTTP/1.1" => true, |
| 202 | 132 | }, |
| 203 | .compression = .none, | |
| 204 | 133 | }; |
| 205 | 134 | |
| 206 | 135 | while (it.next()) |line| { |
| ... | ... | @@ -230,7 +159,7 @@ pub const Request = struct { |
| 230 | 159 | |
| 231 | 160 | const trimmed = mem.trim(u8, header_value, " "); |
| 232 | 161 | |
| 233 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { | |
| 162 | if (http.ContentEncoding.fromString(trimmed)) |ce| { | |
| 234 | 163 | head.transfer_compression = ce; |
| 235 | 164 | } else { |
| 236 | 165 | return error.HttpTransferEncodingUnsupported; |
| ... | ... | @@ -255,7 +184,7 @@ pub const Request = struct { |
| 255 | 184 | if (next) |second| { |
| 256 | 185 | const trimmed_second = mem.trim(u8, second, " "); |
| 257 | 186 | |
| 258 | if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { | |
| 187 | if (http.ContentEncoding.fromString(trimmed_second)) |transfer| { | |
| 259 | 188 | if (head.transfer_compression != .identity) |
| 260 | 189 | return error.HttpHeadersInvalid; // double compression is not supported |
| 261 | 190 | head.transfer_compression = transfer; |
| ... | ... | @@ -299,7 +228,8 @@ pub const Request = struct { |
| 299 | 228 | }; |
| 300 | 229 | |
| 301 | 230 | pub fn iterateHeaders(r: *Request) http.HeaderIterator { |
| 302 | return http.HeaderIterator.init(r.server.read_buffer[0..r.head_end]); | |
| 231 | assert(r.server.reader.state == .received_head); | |
| 232 | return http.HeaderIterator.init(r.server.reader.head_buffer); | |
| 303 | 233 | } |
| 304 | 234 | |
| 305 | 235 | test iterateHeaders { |
| ... | ... | @@ -310,22 +240,19 @@ pub const Request = struct { |
| 310 | 240 | "TRansfer-encoding:\tdeflate, chunked \r\n" ++ |
| 311 | 241 | "connectioN:\t keep-alive \r\n\r\n"; |
| 312 | 242 | |
| 313 | var read_buffer: [500]u8 = undefined; | |
| 314 | @memcpy(read_buffer[0..request_bytes.len], request_bytes); | |
| 315 | ||
| 316 | 243 | 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, | |
| 244 | .reader = .{ | |
| 245 | .in = undefined, | |
| 246 | .state = .received_head, | |
| 247 | .head_buffer = @constCast(request_bytes), | |
| 248 | .interface = undefined, | |
| 249 | }, | |
| 250 | .out = undefined, | |
| 322 | 251 | }; |
| 323 | 252 | |
| 324 | 253 | var request: Request = .{ |
| 325 | 254 | .server = &server, |
| 326 | .head_end = request_bytes.len, | |
| 327 | 255 | .head = undefined, |
| 328 | .reader_state = undefined, | |
| 329 | 256 | }; |
| 330 | 257 | |
| 331 | 258 | var it = request.iterateHeaders(); |
| ... | ... | @@ -384,16 +311,22 @@ pub const Request = struct { |
| 384 | 311 | /// no error is surfaced. |
| 385 | 312 | /// |
| 386 | 313 | /// Asserts status is not `continue`. |
| 387 | /// Asserts there are at most 25 extra_headers. | |
| 388 | 314 | /// Asserts that "\r\n" does not occur in any header name or value. |
| 389 | 315 | pub fn respond( |
| 390 | 316 | request: *Request, |
| 391 | 317 | content: []const u8, |
| 392 | 318 | options: RespondOptions, |
| 393 | ) Response.WriteError!void { | |
| 394 | const max_extra_headers = 25; | |
| 319 | ) ExpectContinueError!void { | |
| 320 | try respondUnflushed(request, content, options); | |
| 321 | try request.server.out.flush(); | |
| 322 | } | |
| 323 | ||
| 324 | pub fn respondUnflushed( | |
| 325 | request: *Request, | |
| 326 | content: []const u8, | |
| 327 | options: RespondOptions, | |
| 328 | ) ExpectContinueError!void { | |
| 395 | 329 | assert(options.status != .@"continue"); |
| 396 | assert(options.extra_headers.len <= max_extra_headers); | |
| 397 | 330 | if (std.debug.runtime_safety) { |
| 398 | 331 | for (options.extra_headers) |header| { |
| 399 | 332 | assert(header.name.len != 0); |
| ... | ... | @@ -402,6 +335,7 @@ pub const Request = struct { |
| 402 | 335 | assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null); |
| 403 | 336 | } |
| 404 | 337 | } |
| 338 | try writeExpectContinue(request); | |
| 405 | 339 | |
| 406 | 340 | const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none; |
| 407 | 341 | const server_keep_alive = !transfer_encoding_none and options.keep_alive; |
| ... | ... | @@ -409,130 +343,42 @@ pub const Request = struct { |
| 409 | 343 | |
| 410 | 344 | const phrase = options.reason orelse options.status.phrase() orelse ""; |
| 411 | 345 | |
| 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", .{ | |
| 346 | const out = request.server.out; | |
| 347 | try out.print("{s} {d} {s}\r\n", .{ | |
| 424 | 348 | @tagName(options.version), @intFromEnum(options.status), phrase, |
| 425 | }) catch unreachable; | |
| 349 | }); | |
| 426 | 350 | |
| 427 | 351 | 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"), | |
| 352 | .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), | |
| 353 | .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), | |
| 430 | 354 | } |
| 431 | 355 | |
| 432 | 356 | if (options.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { |
| 433 | 357 | .none => {}, |
| 434 | .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"), | |
| 358 | .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), | |
| 435 | 359 | } else { |
| 436 | h.fixedWriter().print("content-length: {d}\r\n", .{content.len}) catch unreachable; | |
| 360 | try out.print("content-length: {d}\r\n", .{content.len}); | |
| 437 | 361 | } |
| 438 | 362 | |
| 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 | 363 | 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; | |
| 364 | var vecs: [4][]const u8 = .{ header.name, ": ", header.value, "\r\n" }; | |
| 365 | try out.writeVecAll(&vecs); | |
| 475 | 366 | } |
| 476 | 367 | |
| 477 | iovecs[iovecs_len] = .{ | |
| 478 | .base = "\r\n", | |
| 479 | .len = 2, | |
| 480 | }; | |
| 481 | iovecs_len += 1; | |
| 368 | try out.writeAll("\r\n"); | |
| 482 | 369 | |
| 483 | 370 | if (request.head.method != .HEAD) { |
| 484 | 371 | const is_chunked = (options.transfer_encoding orelse .none) == .chunked; |
| 485 | 372 | 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; | |
| 373 | if (content.len > 0) try out.print("{x}\r\n{s}\r\n", .{ content.len, content }); | |
| 374 | try out.writeAll("0\r\n\r\n"); | |
| 517 | 375 | } else if (content.len > 0) { |
| 518 | iovecs[iovecs_len] = .{ | |
| 519 | .base = content.ptr, | |
| 520 | .len = content.len, | |
| 521 | }; | |
| 522 | iovecs_len += 1; | |
| 376 | try out.writeAll(content); | |
| 523 | 377 | } |
| 524 | 378 | } |
| 525 | ||
| 526 | try request.server.connection.stream.writevAll(iovecs[0..iovecs_len]); | |
| 527 | 379 | } |
| 528 | 380 | |
| 529 | 381 | 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 | 382 | /// If provided, the response will use the content-length header; |
| 537 | 383 | /// otherwise it will use transfer-encoding: chunked. |
| 538 | 384 | content_length: ?u64 = null, |
| ... | ... | @@ -540,254 +386,221 @@ pub const Request = struct { |
| 540 | 386 | respond_options: RespondOptions = .{}, |
| 541 | 387 | }; |
| 542 | 388 | |
| 543 | /// The header is buffered but not sent until Response.flush is called. | |
| 389 | /// The header is not guaranteed to be sent until `BodyWriter.flush` or | |
| 390 | /// `BodyWriter.end` is called. | |
| 544 | 391 | /// |
| 545 | 392 | /// If the request contains a body and the connection is to be reused, |
| 546 | 393 | /// discards the request body, leaving the Server in the `ready` state. If |
| 547 | 394 | /// this discarding fails, the connection is marked as not to be reused and |
| 548 | 395 | /// no error is surfaced. |
| 549 | 396 | /// |
| 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 | |
| 397 | /// HEAD requests are handled transparently by setting the | |
| 398 | /// `BodyWriter.elide` flag on the returned `BodyWriter`, causing | |
| 399 | /// the response stream to omit the body. However, it may be worth noticing | |
| 552 | 400 | /// that flag and skipping any expensive work that would otherwise need to |
| 553 | 401 | /// be done to satisfy the request. |
| 554 | 402 | /// |
| 555 | /// Asserts `send_buffer` is large enough to store the entire response header. | |
| 556 | 403 | /// Asserts status is not `continue`. |
| 557 | pub fn respondStreaming(request: *Request, options: RespondStreamingOptions) Response { | |
| 404 | pub fn respondStreaming( | |
| 405 | request: *Request, | |
| 406 | buffer: []u8, | |
| 407 | options: RespondStreamingOptions, | |
| 408 | ) ExpectContinueError!http.BodyWriter { | |
| 409 | try writeExpectContinue(request); | |
| 558 | 410 | const o = options.respond_options; |
| 559 | 411 | assert(o.status != .@"continue"); |
| 560 | 412 | const transfer_encoding_none = (o.transfer_encoding orelse .chunked) == .none; |
| 561 | 413 | const server_keep_alive = !transfer_encoding_none and o.keep_alive; |
| 562 | 414 | const keep_alive = request.discardBody(server_keep_alive); |
| 563 | 415 | const phrase = o.reason orelse o.status.phrase() orelse ""; |
| 416 | const out = request.server.out; | |
| 564 | 417 | |
| 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 | } | |
| 418 | try out.print("{s} {d} {s}\r\n", .{ | |
| 419 | @tagName(o.version), @intFromEnum(o.status), phrase, | |
| 420 | }); | |
| 583 | 421 | |
| 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 | } | |
| 422 | switch (o.version) { | |
| 423 | .@"HTTP/1.0" => if (keep_alive) try out.writeAll("connection: keep-alive\r\n"), | |
| 424 | .@"HTTP/1.1" => if (!keep_alive) try out.writeAll("connection: close\r\n"), | |
| 425 | } | |
| 592 | 426 | |
| 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 | } | |
| 427 | if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) { | |
| 428 | .chunked => try out.writeAll("transfer-encoding: chunked\r\n"), | |
| 429 | .none => {}, | |
| 430 | } else if (options.content_length) |len| { | |
| 431 | try out.print("content-length: {d}\r\n", .{len}); | |
| 432 | } else { | |
| 433 | try out.writeAll("transfer-encoding: chunked\r\n"); | |
| 434 | } | |
| 600 | 435 | |
| 601 | h.appendSliceAssumeCapacity("\r\n"); | |
| 602 | break :eb request.head.method == .HEAD; | |
| 603 | }; | |
| 436 | for (o.extra_headers) |header| { | |
| 437 | assert(header.name.len != 0); | |
| 438 | try out.writeAll(header.name); | |
| 439 | try out.writeAll(": "); | |
| 440 | try out.writeAll(header.value); | |
| 441 | try out.writeAll("\r\n"); | |
| 442 | } | |
| 604 | 443 | |
| 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, | |
| 444 | try out.writeAll("\r\n"); | |
| 445 | const elide_body = request.head.method == .HEAD; | |
| 446 | const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) { | |
| 447 | .chunked => .{ .chunked = .init }, | |
| 448 | .none => .none, | |
| 449 | } else if (options.content_length) |len| .{ | |
| 450 | .content_length = len, | |
| 451 | } else .{ .chunked = .init }; | |
| 452 | ||
| 453 | return if (elide_body) .{ | |
| 454 | .http_protocol_output = request.server.out, | |
| 455 | .state = state, | |
| 456 | .writer = .discarding(buffer), | |
| 457 | } else .{ | |
| 458 | .http_protocol_output = request.server.out, | |
| 459 | .state = state, | |
| 460 | .writer = .{ | |
| 461 | .buffer = buffer, | |
| 462 | .vtable = switch (state) { | |
| 463 | .none => &.{ | |
| 464 | .drain = http.BodyWriter.noneDrain, | |
| 465 | .sendFile = http.BodyWriter.noneSendFile, | |
| 466 | }, | |
| 467 | .content_length => &.{ | |
| 468 | .drain = http.BodyWriter.contentLengthDrain, | |
| 469 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 470 | }, | |
| 471 | .chunked => &.{ | |
| 472 | .drain = http.BodyWriter.chunkedDrain, | |
| 473 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 474 | }, | |
| 475 | .end => unreachable, | |
| 476 | }, | |
| 477 | }, | |
| 618 | 478 | }; |
| 619 | 479 | } |
| 620 | 480 | |
| 621 | pub const ReadError = net.Stream.ReadError || error{ | |
| 622 | HttpChunkInvalid, | |
| 623 | HttpHeadersOversize, | |
| 481 | pub const UpgradeRequest = union(enum) { | |
| 482 | websocket: ?[]const u8, | |
| 483 | other: []const u8, | |
| 484 | none, | |
| 624 | 485 | }; |
| 625 | 486 | |
| 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; | |
| 487 | pub fn upgradeRequested(request: *const Request) UpgradeRequest { | |
| 488 | switch (request.head.version) { | |
| 489 | .@"HTTP/1.0" => return null, | |
| 490 | .@"HTTP/1.1" => if (request.head.method != .GET) return null, | |
| 634 | 491 | } |
| 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 | 492 | |
| 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 | ||
| 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 | }, | |
| 493 | var sec_websocket_key: ?[]const u8 = null; | |
| 494 | var upgrade_name: ?[]const u8 = null; | |
| 495 | var it = request.iterateHeaders(); | |
| 496 | while (it.next()) |header| { | |
| 497 | if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { | |
| 498 | sec_websocket_key = header.value; | |
| 499 | } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) { | |
| 500 | upgrade_name = header.value; | |
| 737 | 501 | } |
| 738 | 502 | } |
| 739 | return out_end; | |
| 503 | ||
| 504 | const name = upgrade_name orelse return .none; | |
| 505 | if (std.ascii.eqlIgnoreCase(name, "websocket")) return .{ .websocket = sec_websocket_key }; | |
| 506 | return .{ .other = name }; | |
| 740 | 507 | } |
| 741 | 508 | |
| 742 | pub const ReaderError = Response.WriteError || error{ | |
| 743 | /// The client sent an expect HTTP header value other than | |
| 744 | /// "100-continue". | |
| 745 | HttpExpectationFailed, | |
| 509 | pub const WebSocketOptions = struct { | |
| 510 | /// The value from `UpgradeRequest.websocket` (sec-websocket-key header value). | |
| 511 | key: []const u8, | |
| 512 | reason: ?[]const u8 = null, | |
| 513 | extra_headers: []const http.Header = &.{}, | |
| 746 | 514 | }; |
| 747 | 515 | |
| 516 | /// The header is not guaranteed to be sent until `WebSocket.flush` is | |
| 517 | /// called on the returned struct. | |
| 518 | pub fn respondWebSocket(request: *Request, options: WebSocketOptions) Writer.Error!WebSocket { | |
| 519 | if (request.head.expect != null) return error.HttpExpectationFailed; | |
| 520 | ||
| 521 | const out = request.server.out; | |
| 522 | const version: http.Version = .@"HTTP/1.1"; | |
| 523 | const status: http.Status = .switching_protocols; | |
| 524 | const phrase = options.reason orelse status.phrase() orelse ""; | |
| 525 | ||
| 526 | assert(request.head.version == version); | |
| 527 | assert(request.head.method == .GET); | |
| 528 | ||
| 529 | var sha1 = std.crypto.hash.Sha1.init(.{}); | |
| 530 | sha1.update(options.key); | |
| 531 | sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); | |
| 532 | var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined; | |
| 533 | sha1.final(&digest); | |
| 534 | try out.print("{s} {d} {s}\r\n", .{ @tagName(version), @intFromEnum(status), phrase }); | |
| 535 | try out.writeAll("connection: upgrade\r\nupgrade: websocket\r\nsec-websocket-accept: "); | |
| 536 | const base64_digest = try out.writableArray(28); | |
| 537 | assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len); | |
| 538 | out.advance(base64_digest.len); | |
| 539 | try out.writeAll("\r\n"); | |
| 540 | ||
| 541 | for (options.extra_headers) |header| { | |
| 542 | assert(header.name.len != 0); | |
| 543 | try out.writeAll(header.name); | |
| 544 | try out.writeAll(": "); | |
| 545 | try out.writeAll(header.value); | |
| 546 | try out.writeAll("\r\n"); | |
| 547 | } | |
| 548 | ||
| 549 | try out.writeAll("\r\n"); | |
| 550 | ||
| 551 | return .{ | |
| 552 | .input = request.server.reader.in, | |
| 553 | .output = request.server.out, | |
| 554 | .key = options.key, | |
| 555 | }; | |
| 556 | } | |
| 557 | ||
| 748 | 558 | /// In the case that the request contains "expect: 100-continue", this |
| 749 | 559 | /// function writes the continuation header, which means it can fail with a |
| 750 | 560 | /// write error. After sending the continuation header, it sets the |
| 751 | 561 | /// request's expect field to `null`. |
| 752 | 562 | /// |
| 753 | 563 | /// 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 | } | |
| 564 | /// | |
| 565 | /// See `readerExpectNone` for an infallible alternative that cannot write | |
| 566 | /// to the server output stream. | |
| 567 | pub fn readerExpectContinue(request: *Request, buffer: []u8) ExpectContinueError!*std.io.Reader { | |
| 568 | const flush = request.head.expect != null; | |
| 569 | try writeExpectContinue(request); | |
| 570 | if (flush) try request.server.out.flush(); | |
| 571 | return readerExpectNone(request, buffer); | |
| 572 | } | |
| 768 | 573 | |
| 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 | } | |
| 574 | /// Asserts the expect header is `null`. The caller must handle the | |
| 575 | /// expectation manually and then set the value to `null` prior to calling | |
| 576 | /// this function. | |
| 577 | /// | |
| 578 | /// Asserts that this function is only called once. | |
| 579 | pub fn readerExpectNone(request: *Request, buffer: []u8) *std.io.Reader { | |
| 580 | assert(request.server.reader.state == .received_head); | |
| 581 | assert(request.head.expect == null); | |
| 582 | if (!request.head.method.requestHasBody()) return .ending; | |
| 583 | return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length); | |
| 584 | } | |
| 585 | ||
| 586 | pub const ExpectContinueError = error{ | |
| 587 | /// Failed to write "HTTP/1.1 100 Continue\r\n\r\n" to the stream. | |
| 588 | WriteFailed, | |
| 589 | /// The client sent an expect HTTP header value other than | |
| 590 | /// "100-continue". | |
| 591 | HttpExpectationFailed, | |
| 592 | }; | |
| 593 | ||
| 594 | pub fn writeExpectContinue(request: *Request) ExpectContinueError!void { | |
| 595 | const expect = request.head.expect orelse return; | |
| 596 | if (!mem.eql(u8, expect, "100-continue")) return error.HttpExpectationFailed; | |
| 597 | try request.server.out.writeAll("HTTP/1.1 100 Continue\r\n\r\n"); | |
| 598 | request.head.expect = null; | |
| 787 | 599 | } |
| 788 | 600 | |
| 789 | 601 | /// Returns whether the connection should remain persistent. |
| 790 | /// If it would fail, it instead sets the Server state to `receiving_body` | |
| 602 | /// | |
| 603 | /// If it would fail, it instead sets the Server state to receiving body | |
| 791 | 604 | /// and returns false. |
| 792 | 605 | fn discardBody(request: *Request, keep_alive: bool) bool { |
| 793 | 606 | // Prepare to receive another request on the same connection. |
| ... | ... | @@ -798,350 +611,175 @@ pub const Request = struct { |
| 798 | 611 | // or the request body. |
| 799 | 612 | // If the connection won't be kept alive, then none of this matters |
| 800 | 613 | // 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) { | |
| 614 | const r = &request.server.reader; | |
| 615 | if (keep_alive and request.head.keep_alive) switch (r.state) { | |
| 803 | 616 | .received_head => { |
| 804 | const r = request.reader() catch return false; | |
| 805 | _ = r.discard() catch return false; | |
| 806 | assert(s.state == .ready); | |
| 617 | if (request.head.method.requestHasBody()) { | |
| 618 | assert(request.head.transfer_encoding != .none or request.head.content_length != null); | |
| 619 | const reader_interface = request.readerExpectContinue(&.{}) catch return false; | |
| 620 | _ = reader_interface.discardRemaining() catch return false; | |
| 621 | assert(r.state == .ready); | |
| 622 | } else { | |
| 623 | r.state = .ready; | |
| 624 | } | |
| 807 | 625 | return true; |
| 808 | 626 | }, |
| 809 | .receiving_body, .ready => return true, | |
| 627 | .body_remaining_content_length, .body_remaining_chunk_len, .body_none, .ready => return true, | |
| 810 | 628 | else => unreachable, |
| 811 | 629 | }; |
| 812 | 630 | |
| 813 | 631 | // Avoid clobbering the state in case a reading stream already exists. |
| 814 | switch (s.state) { | |
| 815 | .received_head => s.state = .closing, | |
| 632 | switch (r.state) { | |
| 633 | .received_head => r.state = .closing, | |
| 816 | 634 | else => {}, |
| 817 | 635 | } |
| 818 | 636 | return false; |
| 819 | 637 | } |
| 820 | 638 | }; |
| 821 | 639 | |
| 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, | |
| 640 | /// See https://tools.ietf.org/html/rfc6455 | |
| 641 | pub const WebSocket = struct { | |
| 642 | key: []const u8, | |
| 643 | input: *std.io.Reader, | |
| 644 | output: *Writer, | |
| 645 | ||
| 646 | pub const Header0 = packed struct(u8) { | |
| 647 | opcode: Opcode, | |
| 648 | rsv3: u1 = 0, | |
| 649 | rsv2: u1 = 0, | |
| 650 | rsv1: u1 = 0, | |
| 651 | fin: bool, | |
| 845 | 652 | }; |
| 846 | 653 | |
| 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 = &.{}, | |
| 654 | pub const Header1 = packed struct(u8) { | |
| 655 | payload_len: enum(u7) { | |
| 656 | len16 = 126, | |
| 657 | len64 = 127, | |
| 658 | _, | |
| 659 | }, | |
| 660 | mask: bool, | |
| 872 | 661 | }; |
| 873 | 662 | |
| 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))); | |
| 663 | pub const Opcode = enum(u4) { | |
| 664 | continuation = 0, | |
| 665 | text = 1, | |
| 666 | binary = 2, | |
| 667 | connection_close = 8, | |
| 668 | ping = 9, | |
| 669 | /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional | |
| 670 | /// heartbeat. A response to an unsolicited Pong frame is not expected." | |
| 671 | pong = 10, | |
| 672 | _, | |
| 673 | }; | |
| 898 | 674 | |
| 899 | var trash: u64 = std.math.maxInt(u64); | |
| 900 | const len = switch (r.transfer_encoding) { | |
| 901 | .content_length => |*len| len, | |
| 902 | else => &trash, | |
| 903 | }; | |
| 675 | pub const ReadSmallTextMessageError = error{ | |
| 676 | ConnectionClose, | |
| 677 | UnexpectedOpCode, | |
| 678 | MessageTooBig, | |
| 679 | MissingMaskBit, | |
| 680 | }; | |
| 904 | 681 | |
| 905 | if (r.elide_body) { | |
| 906 | len.* -= bytes.len; | |
| 907 | return bytes.len; | |
| 908 | } | |
| 682 | pub const SmallMessage = struct { | |
| 683 | /// Can be text, binary, or ping. | |
| 684 | opcode: Opcode, | |
| 685 | data: []u8, | |
| 686 | }; | |
| 909 | 687 | |
| 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; | |
| 688 | /// Reads the next message from the WebSocket stream, failing if the | |
| 689 | /// message does not fit into the input buffer. The returned memory points | |
| 690 | /// into the input buffer and is invalidated on the next read. | |
| 691 | pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage { | |
| 692 | const in = ws.input; | |
| 693 | while (true) { | |
| 694 | const h0 = in.takeStruct(Header0); | |
| 695 | const h1 = in.takeStruct(Header1); | |
| 696 | ||
| 697 | switch (h0.opcode) { | |
| 698 | .text, .binary, .pong, .ping => {}, | |
| 699 | .connection_close => return error.ConnectionClose, | |
| 700 | .continuation => return error.UnexpectedOpCode, | |
| 701 | _ => return error.UnexpectedOpCode, | |
| 931 | 702 | } |
| 932 | 703 | |
| 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 | } | |
| 704 | if (!h0.fin) return error.MessageTooBig; | |
| 705 | if (!h1.mask) return error.MissingMaskBit; | |
| 945 | 706 | |
| 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 | }, | |
| 707 | const len: usize = switch (h1.payload_len) { | |
| 708 | .len16 => try in.takeInt(u16, .big), | |
| 709 | .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageTooBig, | |
| 710 | else => @intFromEnum(h1.payload_len), | |
| 711 | }; | |
| 712 | if (len > in.buffer.len) return error.MessageTooBig; | |
| 713 | const mask: u32 = @bitCast((try in.takeArray(4)).*); | |
| 714 | const payload = try in.take(len); | |
| 715 | ||
| 716 | // Skip pongs. | |
| 717 | if (h0.opcode == .pong) continue; | |
| 718 | ||
| 719 | // The last item may contain a partial word of unused data. | |
| 720 | const floored_len = (payload.len / 4) * 4; | |
| 721 | const u32_payload: []align(1) u32 = @ptrCast(payload[0..floored_len]); | |
| 722 | for (u32_payload) |*elem| elem.* ^= mask; | |
| 723 | const mask_bytes: []const u8 = @ptrCast(&mask); | |
| 724 | for (payload[floored_len..], mask_bytes[0 .. payload.len - floored_len]) |*leftover, m| | |
| 725 | leftover.* ^= m; | |
| 726 | ||
| 727 | return .{ | |
| 728 | .opcode = h0.opcode, | |
| 729 | .data = payload, | |
| 980 | 730 | }; |
| 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 | 731 | } |
| 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 | 732 | } |
| 996 | 733 | |
| 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 | } | |
| 734 | pub fn writeMessage(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void { | |
| 735 | try writeMessageVecUnflushed(ws, &.{data}, op); | |
| 736 | try ws.output.flush(); | |
| 1004 | 737 | } |
| 1005 | 738 | |
| 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 | } | |
| 739 | pub fn writeMessageUnflushed(ws: *WebSocket, data: []const u8, op: Opcode) Writer.Error!void { | |
| 740 | try writeMessageVecUnflushed(ws, &.{data}, op); | |
| 1014 | 741 | } |
| 1015 | 742 | |
| 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; | |
| 743 | pub fn writeMessageVec(ws: *WebSocket, data: []const []const u8, op: Opcode) Writer.Error!void { | |
| 744 | try writeMessageVecUnflushed(ws, data, op); | |
| 745 | try ws.output.flush(); | |
| 1020 | 746 | } |
| 1021 | 747 | |
| 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, | |
| 748 | pub fn writeMessageVecUnflushed(ws: *WebSocket, data: []const []const u8, op: Opcode) Writer.Error!void { | |
| 749 | const total_len = l: { | |
| 750 | var total_len: u64 = 0; | |
| 751 | for (data) |iovec| total_len += iovec.len; | |
| 752 | break :l total_len; | |
| 1046 | 753 | }; |
| 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; | |
| 754 | const out = ws.output; | |
| 755 | try out.writeStruct(@as(Header0, .{ | |
| 756 | .opcode = op, | |
| 757 | .fin = true, | |
| 758 | })); | |
| 759 | switch (total_len) { | |
| 760 | 0...125 => try out.writeStruct(@as(Header1, .{ | |
| 761 | .payload_len = @enumFromInt(total_len), | |
| 762 | .mask = false, | |
| 763 | })), | |
| 764 | 126...0xffff => { | |
| 765 | try out.writeStruct(@as(Header1, .{ | |
| 766 | .payload_len = .len16, | |
| 767 | .mask = false, | |
| 768 | })); | |
| 769 | try out.writeInt(u16, @intCast(total_len), .big); | |
| 770 | }, | |
| 771 | else => { | |
| 772 | try out.writeStruct(@as(Header1, .{ | |
| 773 | .payload_len = .len64, | |
| 774 | .mask = false, | |
| 775 | })); | |
| 776 | try out.writeInt(u64, total_len, .big); | |
| 777 | }, | |
| 1109 | 778 | } |
| 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; | |
| 779 | try out.writeVecAll(data); | |
| 1115 | 780 | } |
| 1116 | 781 | |
| 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 | }; | |
| 782 | pub fn flush(ws: *WebSocket) Writer.Error!void { | |
| 783 | try ws.output.flush(); | |
| 1125 | 784 | } |
| 1126 | 785 | }; |
| 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+272-322| ... | ... | @@ -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,33 @@ 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(); | |
| 65 | try req.sendBodiless(); | |
| 66 | var response = try req.receiveHead(&.{}); | |
| 69 | 67 | |
| 70 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 68 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 71 | 69 | defer gpa.free(body); |
| 72 | 70 | |
| 73 | 71 | try expectEqualStrings("Hello, World!\n", body); |
| 74 | 72 | |
| 75 | var it = req.response.iterateHeaders(); | |
| 76 | 73 | { |
| 74 | var it = response.head.iterateHeaders(); | |
| 77 | 75 | const header = it.next().?; |
| 78 | 76 | try expect(!it.is_trailer); |
| 79 | 77 | try expectEqualStrings("transfer-encoding", header.name); |
| 80 | 78 | try expectEqualStrings("chunked", header.value); |
| 79 | try expectEqual(null, it.next()); | |
| 81 | 80 | } |
| 82 | 81 | { |
| 82 | var it = response.iterateTrailers(); | |
| 83 | 83 | const header = it.next().?; |
| 84 | 84 | try expect(it.is_trailer); |
| 85 | 85 | try expectEqualStrings("X-Checksum", header.name); |
| 86 | 86 | try expectEqualStrings("aaaa", header.value); |
| 87 | try expectEqual(null, it.next()); | |
| 87 | 88 | } |
| 88 | try expectEqual(null, it.next()); | |
| 89 | 89 | } |
| 90 | 90 | |
| 91 | 91 | // connection has been kept alive |
| ... | ... | @@ -94,19 +94,24 @@ test "trailers" { |
| 94 | 94 | |
| 95 | 95 | test "HTTP server handles a chunked transfer coding request" { |
| 96 | 96 | 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); | |
| 97 | fn run(test_server: *TestServer) anyerror!void { | |
| 98 | const net_server = &test_server.net_server; | |
| 99 | var recv_buffer: [8192]u8 = undefined; | |
| 100 | var send_buffer: [500]u8 = undefined; | |
| 101 | const connection = try net_server.accept(); | |
| 102 | defer connection.stream.close(); | |
| 103 | ||
| 104 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 105 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 106 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 103 | 107 | var request = try server.receiveHead(); |
| 104 | 108 | |
| 105 | 109 | try expect(request.head.transfer_encoding == .chunked); |
| 106 | 110 | |
| 107 | 111 | var buf: [128]u8 = undefined; |
| 108 | const n = try (try request.reader()).readAll(&buf); | |
| 109 | try expect(mem.eql(u8, buf[0..n], "ABCD")); | |
| 112 | var br = try request.readerExpectContinue(&.{}); | |
| 113 | const n = try br.readSliceShort(&buf); | |
| 114 | try expectEqualStrings("ABCD", buf[0..n]); | |
| 110 | 115 | |
| 111 | 116 | try request.respond("message from server!\n", .{ |
| 112 | 117 | .extra_headers = &.{ |
| ... | ... | @@ -154,16 +159,20 @@ test "HTTP server handles a chunked transfer coding request" { |
| 154 | 159 | |
| 155 | 160 | test "echo content server" { |
| 156 | 161 | const test_server = try createTestServer(struct { |
| 157 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 158 | var read_buffer: [1024]u8 = undefined; | |
| 162 | fn run(test_server: *TestServer) anyerror!void { | |
| 163 | const net_server = &test_server.net_server; | |
| 164 | var recv_buffer: [1024]u8 = undefined; | |
| 165 | var send_buffer: [100]u8 = undefined; | |
| 159 | 166 | |
| 160 | accept: while (true) { | |
| 161 | const conn = try net_server.accept(); | |
| 162 | defer conn.stream.close(); | |
| 167 | accept: while (!test_server.shutting_down) { | |
| 168 | const connection = try net_server.accept(); | |
| 169 | defer connection.stream.close(); | |
| 163 | 170 | |
| 164 | var http_server = http.Server.init(conn, &read_buffer); | |
| 171 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 172 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 173 | var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 165 | 174 | |
| 166 | while (http_server.state == .ready) { | |
| 175 | while (http_server.reader.state == .ready) { | |
| 167 | 176 | var request = http_server.receiveHead() catch |err| switch (err) { |
| 168 | 177 | error.HttpConnectionClosing => continue :accept, |
| 169 | 178 | else => |e| return e, |
| ... | ... | @@ -173,7 +182,7 @@ test "echo content server" { |
| 173 | 182 | } |
| 174 | 183 | if (request.head.expect) |expect_header_value| { |
| 175 | 184 | if (mem.eql(u8, expect_header_value, "garbage")) { |
| 176 | try expectError(error.HttpExpectationFailed, request.reader()); | |
| 185 | try expectError(error.HttpExpectationFailed, request.readerExpectContinue(&.{})); | |
| 177 | 186 | try request.respond("", .{ .keep_alive = false }); |
| 178 | 187 | continue; |
| 179 | 188 | } |
| ... | ... | @@ -195,16 +204,14 @@ test "echo content server" { |
| 195 | 204 | // request.head.target, |
| 196 | 205 | //}); |
| 197 | 206 | |
| 198 | const body = try (try request.reader()).readAllAlloc(std.testing.allocator, 8192); | |
| 207 | const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .limited(8192)); | |
| 199 | 208 | defer std.testing.allocator.free(body); |
| 200 | 209 | |
| 201 | 210 | try expect(mem.startsWith(u8, request.head.target, "/echo-content")); |
| 202 | 211 | try expectEqualStrings("Hello, World!\n", body); |
| 203 | 212 | try expectEqualStrings("text/plain", request.head.content_type.?); |
| 204 | 213 | |
| 205 | var send_buffer: [100]u8 = undefined; | |
| 206 | var response = request.respondStreaming(.{ | |
| 207 | .send_buffer = &send_buffer, | |
| 214 | var response = try request.respondStreaming(&.{}, .{ | |
| 208 | 215 | .content_length = switch (request.head.transfer_encoding) { |
| 209 | 216 | .chunked => null, |
| 210 | 217 | .none => len: { |
| ... | ... | @@ -213,9 +220,8 @@ test "echo content server" { |
| 213 | 220 | }, |
| 214 | 221 | }, |
| 215 | 222 | }); |
| 216 | ||
| 217 | 223 | try response.flush(); // Test an early flush to send the HTTP headers before the body. |
| 218 | const w = response.writer(); | |
| 224 | const w = &response.writer; | |
| 219 | 225 | try w.writeAll("Hello, "); |
| 220 | 226 | try w.writeAll("World!\n"); |
| 221 | 227 | try response.end(); |
| ... | ... | @@ -241,35 +247,36 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { |
| 241 | 247 | // In this case, the response is expected to stream until the connection is |
| 242 | 248 | // closed, indicating the end of the body. |
| 243 | 249 | const test_server = try createTestServer(struct { |
| 244 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 245 | var header_buffer: [1000]u8 = undefined; | |
| 250 | fn run(test_server: *TestServer) anyerror!void { | |
| 251 | const net_server = &test_server.net_server; | |
| 252 | var recv_buffer: [1000]u8 = undefined; | |
| 253 | var send_buffer: [500]u8 = undefined; | |
| 246 | 254 | var remaining: usize = 1; |
| 247 | 255 | while (remaining != 0) : (remaining -= 1) { |
| 248 | const conn = try net_server.accept(); | |
| 249 | defer conn.stream.close(); | |
| 256 | const connection = try net_server.accept(); | |
| 257 | defer connection.stream.close(); | |
| 250 | 258 | |
| 251 | var server = http.Server.init(conn, &header_buffer); | |
| 259 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 260 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 261 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 252 | 262 | |
| 253 | try expectEqual(.ready, server.state); | |
| 263 | try expectEqual(.ready, server.reader.state); | |
| 254 | 264 | var request = try server.receiveHead(); |
| 255 | 265 | try expectEqualStrings(request.head.target, "/foo"); |
| 256 | var send_buffer: [500]u8 = undefined; | |
| 257 | var response = request.respondStreaming(.{ | |
| 258 | .send_buffer = &send_buffer, | |
| 266 | var buf: [30]u8 = undefined; | |
| 267 | var response = try request.respondStreaming(&buf, .{ | |
| 259 | 268 | .respond_options = .{ |
| 260 | 269 | .transfer_encoding = .none, |
| 261 | 270 | }, |
| 262 | 271 | }); |
| 263 | var total: usize = 0; | |
| 272 | const w = &response.writer; | |
| 264 | 273 | 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; | |
| 274 | try w.print("{d}, ah ha ha!\n", .{i}); | |
| 269 | 275 | } |
| 270 | try expectEqual(7390, total); | |
| 276 | try expectEqual(7390, w.count); | |
| 277 | try w.flush(); | |
| 271 | 278 | try response.end(); |
| 272 | try expectEqual(.closing, server.state); | |
| 279 | try expectEqual(.closing, server.reader.state); | |
| 273 | 280 | } |
| 274 | 281 | } |
| 275 | 282 | }); |
| ... | ... | @@ -308,15 +315,20 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { |
| 308 | 315 | |
| 309 | 316 | test "receiving arbitrary http headers from the client" { |
| 310 | 317 | const test_server = try createTestServer(struct { |
| 311 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 312 | var read_buffer: [666]u8 = undefined; | |
| 318 | fn run(test_server: *TestServer) anyerror!void { | |
| 319 | const net_server = &test_server.net_server; | |
| 320 | var recv_buffer: [666]u8 = undefined; | |
| 321 | var send_buffer: [777]u8 = undefined; | |
| 313 | 322 | var remaining: usize = 1; |
| 314 | 323 | while (remaining != 0) : (remaining -= 1) { |
| 315 | const conn = try net_server.accept(); | |
| 316 | defer conn.stream.close(); | |
| 324 | const connection = try net_server.accept(); | |
| 325 | defer connection.stream.close(); | |
| 326 | ||
| 327 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 328 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 329 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 317 | 330 | |
| 318 | var server = http.Server.init(conn, &read_buffer); | |
| 319 | try expectEqual(.ready, server.state); | |
| 331 | try expectEqual(.ready, server.reader.state); | |
| 320 | 332 | var request = try server.receiveHead(); |
| 321 | 333 | try expectEqualStrings("/bar", request.head.target); |
| 322 | 334 | var it = request.iterateHeaders(); |
| ... | ... | @@ -368,19 +380,21 @@ test "general client/server API coverage" { |
| 368 | 380 | return error.SkipZigTest; |
| 369 | 381 | } |
| 370 | 382 | |
| 371 | const global = struct { | |
| 372 | var handle_new_requests = true; | |
| 373 | }; | |
| 374 | 383 | 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) { | |
| 384 | fn run(test_server: *TestServer) anyerror!void { | |
| 385 | const net_server = &test_server.net_server; | |
| 386 | var recv_buffer: [1024]u8 = undefined; | |
| 387 | var send_buffer: [100]u8 = undefined; | |
| 388 | ||
| 389 | outer: while (!test_server.shutting_down) { | |
| 378 | 390 | var connection = try net_server.accept(); |
| 379 | 391 | defer connection.stream.close(); |
| 380 | 392 | |
| 381 | var http_server = http.Server.init(connection, &client_header_buffer); | |
| 393 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 394 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 395 | var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 382 | 396 | |
| 383 | while (http_server.state == .ready) { | |
| 397 | while (http_server.reader.state == .ready) { | |
| 384 | 398 | var request = http_server.receiveHead() catch |err| switch (err) { |
| 385 | 399 | error.HttpConnectionClosing => continue :outer, |
| 386 | 400 | else => |e| return e, |
| ... | ... | @@ -399,14 +413,11 @@ test "general client/server API coverage" { |
| 399 | 413 | }); |
| 400 | 414 | |
| 401 | 415 | const gpa = std.testing.allocator; |
| 402 | const body = try (try request.reader()).readAllAlloc(gpa, 8192); | |
| 416 | const body = try (try request.readerExpectContinue(&.{})).allocRemaining(gpa, .limited(8192)); | |
| 403 | 417 | defer gpa.free(body); |
| 404 | 418 | |
| 405 | var send_buffer: [100]u8 = undefined; | |
| 406 | ||
| 407 | 419 | if (mem.startsWith(u8, request.head.target, "/get")) { |
| 408 | var response = request.respondStreaming(.{ | |
| 409 | .send_buffer = &send_buffer, | |
| 420 | var response = try request.respondStreaming(&.{}, .{ | |
| 410 | 421 | .content_length = if (mem.indexOf(u8, request.head.target, "?chunked") == null) |
| 411 | 422 | 14 |
| 412 | 423 | else |
| ... | ... | @@ -417,20 +428,19 @@ test "general client/server API coverage" { |
| 417 | 428 | }, |
| 418 | 429 | }, |
| 419 | 430 | }); |
| 420 | const w = response.writer(); | |
| 431 | const w = &response.writer; | |
| 421 | 432 | try w.writeAll("Hello, "); |
| 422 | 433 | try w.writeAll("World!\n"); |
| 423 | 434 | try response.end(); |
| 424 | 435 | // Writing again would cause an assertion failure. |
| 425 | 436 | } else if (mem.startsWith(u8, request.head.target, "/large")) { |
| 426 | var response = request.respondStreaming(.{ | |
| 427 | .send_buffer = &send_buffer, | |
| 437 | var response = try request.respondStreaming(&.{}, .{ | |
| 428 | 438 | .content_length = 14 * 1024 + 14 * 10, |
| 429 | 439 | }); |
| 430 | 440 | |
| 431 | 441 | try response.flush(); // Test an early flush to send the HTTP headers before the body. |
| 432 | 442 | |
| 433 | const w = response.writer(); | |
| 443 | const w = &response.writer; | |
| 434 | 444 | |
| 435 | 445 | var i: u32 = 0; |
| 436 | 446 | while (i < 5) : (i += 1) { |
| ... | ... | @@ -446,8 +456,7 @@ test "general client/server API coverage" { |
| 446 | 456 | |
| 447 | 457 | try response.end(); |
| 448 | 458 | } else if (mem.eql(u8, request.head.target, "/redirect/1")) { |
| 449 | var response = request.respondStreaming(.{ | |
| 450 | .send_buffer = &send_buffer, | |
| 459 | var response = try request.respondStreaming(&.{}, .{ | |
| 451 | 460 | .respond_options = .{ |
| 452 | 461 | .status = .found, |
| 453 | 462 | .extra_headers = &.{ |
| ... | ... | @@ -456,7 +465,7 @@ test "general client/server API coverage" { |
| 456 | 465 | }, |
| 457 | 466 | }); |
| 458 | 467 | |
| 459 | const w = response.writer(); | |
| 468 | const w = &response.writer; | |
| 460 | 469 | try w.writeAll("Hello, "); |
| 461 | 470 | try w.writeAll("Redirected!\n"); |
| 462 | 471 | try response.end(); |
| ... | ... | @@ -524,17 +533,13 @@ test "general client/server API coverage" { |
| 524 | 533 | return s.listen_address.in.getPort(); |
| 525 | 534 | } |
| 526 | 535 | }); |
| 527 | defer { | |
| 528 | global.handle_new_requests = false; | |
| 529 | test_server.destroy(); | |
| 530 | } | |
| 536 | defer test_server.destroy(); | |
| 531 | 537 | |
| 532 | 538 | const log = std.log.scoped(.client); |
| 533 | 539 | |
| 534 | 540 | const gpa = std.testing.allocator; |
| 535 | 541 | var client: http.Client = .{ .allocator = gpa }; |
| 536 | errdefer client.deinit(); | |
| 537 | // defer client.deinit(); handled below | |
| 542 | defer client.deinit(); | |
| 538 | 543 | |
| 539 | 544 | const port = test_server.port(); |
| 540 | 545 | |
| ... | ... | @@ -544,20 +549,18 @@ test "general client/server API coverage" { |
| 544 | 549 | const uri = try std.Uri.parse(location); |
| 545 | 550 | |
| 546 | 551 | 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 | }); | |
| 552 | var redirect_buffer: [1024]u8 = undefined; | |
| 553 | var req = try client.request(.GET, uri, .{}); | |
| 551 | 554 | defer req.deinit(); |
| 552 | 555 | |
| 553 | try req.send(); | |
| 554 | try req.wait(); | |
| 556 | try req.sendBodiless(); | |
| 557 | var response = try req.receiveHead(&redirect_buffer); | |
| 555 | 558 | |
| 556 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 559 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 557 | 560 | defer gpa.free(body); |
| 558 | 561 | |
| 559 | 562 | try expectEqualStrings("Hello, World!\n", body); |
| 560 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 563 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 561 | 564 | } |
| 562 | 565 | |
| 563 | 566 | // connection has been kept alive |
| ... | ... | @@ -569,16 +572,14 @@ test "general client/server API coverage" { |
| 569 | 572 | const uri = try std.Uri.parse(location); |
| 570 | 573 | |
| 571 | 574 | 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 | }); | |
| 575 | var redirect_buffer: [1024]u8 = undefined; | |
| 576 | var req = try client.request(.GET, uri, .{}); | |
| 576 | 577 | defer req.deinit(); |
| 577 | 578 | |
| 578 | try req.send(); | |
| 579 | try req.wait(); | |
| 579 | try req.sendBodiless(); | |
| 580 | var response = try req.receiveHead(&redirect_buffer); | |
| 580 | 581 | |
| 581 | const body = try req.reader().readAllAlloc(gpa, 8192 * 1024); | |
| 582 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192 * 1024)); | |
| 582 | 583 | defer gpa.free(body); |
| 583 | 584 | |
| 584 | 585 | try expectEqual(@as(usize, 14 * 1024 + 14 * 10), body.len); |
| ... | ... | @@ -593,21 +594,19 @@ test "general client/server API coverage" { |
| 593 | 594 | const uri = try std.Uri.parse(location); |
| 594 | 595 | |
| 595 | 596 | 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 | }); | |
| 597 | var redirect_buffer: [1024]u8 = undefined; | |
| 598 | var req = try client.request(.HEAD, uri, .{}); | |
| 600 | 599 | defer req.deinit(); |
| 601 | 600 | |
| 602 | try req.send(); | |
| 603 | try req.wait(); | |
| 601 | try req.sendBodiless(); | |
| 602 | var response = try req.receiveHead(&redirect_buffer); | |
| 604 | 603 | |
| 605 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 604 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 606 | 605 | defer gpa.free(body); |
| 607 | 606 | |
| 608 | 607 | try expectEqualStrings("", body); |
| 609 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 610 | try expectEqual(14, req.response.content_length.?); | |
| 608 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 609 | try expectEqual(14, response.head.content_length.?); | |
| 611 | 610 | } |
| 612 | 611 | |
| 613 | 612 | // connection has been kept alive |
| ... | ... | @@ -619,20 +618,18 @@ test "general client/server API coverage" { |
| 619 | 618 | const uri = try std.Uri.parse(location); |
| 620 | 619 | |
| 621 | 620 | 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 | }); | |
| 621 | var redirect_buffer: [1024]u8 = undefined; | |
| 622 | var req = try client.request(.GET, uri, .{}); | |
| 626 | 623 | defer req.deinit(); |
| 627 | 624 | |
| 628 | try req.send(); | |
| 629 | try req.wait(); | |
| 625 | try req.sendBodiless(); | |
| 626 | var response = try req.receiveHead(&redirect_buffer); | |
| 630 | 627 | |
| 631 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 628 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 632 | 629 | defer gpa.free(body); |
| 633 | 630 | |
| 634 | 631 | try expectEqualStrings("Hello, World!\n", body); |
| 635 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 632 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 636 | 633 | } |
| 637 | 634 | |
| 638 | 635 | // connection has been kept alive |
| ... | ... | @@ -644,21 +641,19 @@ test "general client/server API coverage" { |
| 644 | 641 | const uri = try std.Uri.parse(location); |
| 645 | 642 | |
| 646 | 643 | 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 | }); | |
| 644 | var redirect_buffer: [1024]u8 = undefined; | |
| 645 | var req = try client.request(.HEAD, uri, .{}); | |
| 651 | 646 | defer req.deinit(); |
| 652 | 647 | |
| 653 | try req.send(); | |
| 654 | try req.wait(); | |
| 648 | try req.sendBodiless(); | |
| 649 | var response = try req.receiveHead(&redirect_buffer); | |
| 655 | 650 | |
| 656 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 651 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 657 | 652 | defer gpa.free(body); |
| 658 | 653 | |
| 659 | 654 | try expectEqualStrings("", body); |
| 660 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 661 | try expect(req.response.transfer_encoding == .chunked); | |
| 655 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 656 | try expect(response.head.transfer_encoding == .chunked); | |
| 662 | 657 | } |
| 663 | 658 | |
| 664 | 659 | // connection has been kept alive |
| ... | ... | @@ -670,21 +665,20 @@ test "general client/server API coverage" { |
| 670 | 665 | const uri = try std.Uri.parse(location); |
| 671 | 666 | |
| 672 | 667 | 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, | |
| 668 | var redirect_buffer: [1024]u8 = undefined; | |
| 669 | var req = try client.request(.GET, uri, .{ | |
| 676 | 670 | .keep_alive = false, |
| 677 | 671 | }); |
| 678 | 672 | defer req.deinit(); |
| 679 | 673 | |
| 680 | try req.send(); | |
| 681 | try req.wait(); | |
| 674 | try req.sendBodiless(); | |
| 675 | var response = try req.receiveHead(&redirect_buffer); | |
| 682 | 676 | |
| 683 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 677 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 684 | 678 | defer gpa.free(body); |
| 685 | 679 | |
| 686 | 680 | try expectEqualStrings("Hello, World!\n", body); |
| 687 | try expectEqualStrings("text/plain", req.response.content_type.?); | |
| 681 | try expectEqualStrings("text/plain", response.head.content_type.?); | |
| 688 | 682 | } |
| 689 | 683 | |
| 690 | 684 | // connection has been closed |
| ... | ... | @@ -696,26 +690,25 @@ test "general client/server API coverage" { |
| 696 | 690 | const uri = try std.Uri.parse(location); |
| 697 | 691 | |
| 698 | 692 | 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, | |
| 693 | var redirect_buffer: [1024]u8 = undefined; | |
| 694 | var req = try client.request(.GET, uri, .{ | |
| 702 | 695 | .extra_headers = &.{ |
| 703 | 696 | .{ .name = "empty", .value = "" }, |
| 704 | 697 | }, |
| 705 | 698 | }); |
| 706 | 699 | defer req.deinit(); |
| 707 | 700 | |
| 708 | try req.send(); | |
| 709 | try req.wait(); | |
| 701 | try req.sendBodiless(); | |
| 702 | var response = try req.receiveHead(&redirect_buffer); | |
| 710 | 703 | |
| 711 | try std.testing.expectEqual(.ok, req.response.status); | |
| 704 | try std.testing.expectEqual(.ok, response.head.status); | |
| 712 | 705 | |
| 713 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 706 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 714 | 707 | defer gpa.free(body); |
| 715 | 708 | |
| 716 | 709 | try expectEqualStrings("", body); |
| 717 | 710 | |
| 718 | var it = req.response.iterateHeaders(); | |
| 711 | var it = response.head.iterateHeaders(); | |
| 719 | 712 | { |
| 720 | 713 | const header = it.next().?; |
| 721 | 714 | try expect(!it.is_trailer); |
| ... | ... | @@ -740,16 +733,14 @@ test "general client/server API coverage" { |
| 740 | 733 | const uri = try std.Uri.parse(location); |
| 741 | 734 | |
| 742 | 735 | 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 | }); | |
| 736 | var redirect_buffer: [1024]u8 = undefined; | |
| 737 | var req = try client.request(.GET, uri, .{}); | |
| 747 | 738 | defer req.deinit(); |
| 748 | 739 | |
| 749 | try req.send(); | |
| 750 | try req.wait(); | |
| 740 | try req.sendBodiless(); | |
| 741 | var response = try req.receiveHead(&redirect_buffer); | |
| 751 | 742 | |
| 752 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 743 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 753 | 744 | defer gpa.free(body); |
| 754 | 745 | |
| 755 | 746 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -764,16 +755,14 @@ test "general client/server API coverage" { |
| 764 | 755 | const uri = try std.Uri.parse(location); |
| 765 | 756 | |
| 766 | 757 | 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 | }); | |
| 758 | var redirect_buffer: [1024]u8 = undefined; | |
| 759 | var req = try client.request(.GET, uri, .{}); | |
| 771 | 760 | defer req.deinit(); |
| 772 | 761 | |
| 773 | try req.send(); | |
| 774 | try req.wait(); | |
| 762 | try req.sendBodiless(); | |
| 763 | var response = try req.receiveHead(&redirect_buffer); | |
| 775 | 764 | |
| 776 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 765 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 777 | 766 | defer gpa.free(body); |
| 778 | 767 | |
| 779 | 768 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -788,16 +777,14 @@ test "general client/server API coverage" { |
| 788 | 777 | const uri = try std.Uri.parse(location); |
| 789 | 778 | |
| 790 | 779 | 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 | }); | |
| 780 | var redirect_buffer: [1024]u8 = undefined; | |
| 781 | var req = try client.request(.GET, uri, .{}); | |
| 795 | 782 | defer req.deinit(); |
| 796 | 783 | |
| 797 | try req.send(); | |
| 798 | try req.wait(); | |
| 784 | try req.sendBodiless(); | |
| 785 | var response = try req.receiveHead(&redirect_buffer); | |
| 799 | 786 | |
| 800 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 787 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 801 | 788 | defer gpa.free(body); |
| 802 | 789 | |
| 803 | 790 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -812,17 +799,17 @@ test "general client/server API coverage" { |
| 812 | 799 | const uri = try std.Uri.parse(location); |
| 813 | 800 | |
| 814 | 801 | 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 | }); | |
| 802 | var redirect_buffer: [1024]u8 = undefined; | |
| 803 | var req = try client.request(.GET, uri, .{}); | |
| 819 | 804 | defer req.deinit(); |
| 820 | 805 | |
| 821 | try req.send(); | |
| 822 | req.wait() catch |err| switch (err) { | |
| 806 | try req.sendBodiless(); | |
| 807 | if (req.receiveHead(&redirect_buffer)) |_| { | |
| 808 | return error.TestFailed; | |
| 809 | } else |err| switch (err) { | |
| 823 | 810 | error.TooManyHttpRedirects => {}, |
| 824 | 811 | else => return err, |
| 825 | }; | |
| 812 | } | |
| 826 | 813 | } |
| 827 | 814 | |
| 828 | 815 | { // redirect to encoded url |
| ... | ... | @@ -831,16 +818,14 @@ test "general client/server API coverage" { |
| 831 | 818 | const uri = try std.Uri.parse(location); |
| 832 | 819 | |
| 833 | 820 | 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 | }); | |
| 821 | var redirect_buffer: [1024]u8 = undefined; | |
| 822 | var req = try client.request(.GET, uri, .{}); | |
| 838 | 823 | defer req.deinit(); |
| 839 | 824 | |
| 840 | try req.send(); | |
| 841 | try req.wait(); | |
| 825 | try req.sendBodiless(); | |
| 826 | var response = try req.receiveHead(&redirect_buffer); | |
| 842 | 827 | |
| 843 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 828 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 844 | 829 | defer gpa.free(body); |
| 845 | 830 | |
| 846 | 831 | try expectEqualStrings("Encoded redirect successful!\n", body); |
| ... | ... | @@ -855,14 +840,12 @@ test "general client/server API coverage" { |
| 855 | 840 | const uri = try std.Uri.parse(location); |
| 856 | 841 | |
| 857 | 842 | 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 | }); | |
| 843 | var redirect_buffer: [1024]u8 = undefined; | |
| 844 | var req = try client.request(.GET, uri, .{}); | |
| 862 | 845 | defer req.deinit(); |
| 863 | 846 | |
| 864 | try req.send(); | |
| 865 | const result = req.wait(); | |
| 847 | try req.sendBodiless(); | |
| 848 | const result = req.receiveHead(&redirect_buffer); | |
| 866 | 849 | |
| 867 | 850 | // a proxy without an upstream is likely to return a 5xx status. |
| 868 | 851 | if (client.http_proxy == null) { |
| ... | ... | @@ -872,77 +855,40 @@ test "general client/server API coverage" { |
| 872 | 855 | |
| 873 | 856 | // connection has been kept alive |
| 874 | 857 | 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 | 858 | } |
| 917 | 859 | |
| 918 | 860 | test "Server streams both reading and writing" { |
| 919 | 861 | 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(); | |
| 862 | fn run(test_server: *TestServer) anyerror!void { | |
| 863 | const net_server = &test_server.net_server; | |
| 864 | var recv_buffer: [1024]u8 = undefined; | |
| 865 | var send_buffer: [777]u8 = undefined; | |
| 924 | 866 | |
| 925 | var server = http.Server.init(conn, &header_buffer); | |
| 926 | var request = try server.receiveHead(); | |
| 927 | const reader = try request.reader(); | |
| 867 | const connection = try net_server.accept(); | |
| 868 | defer connection.stream.close(); | |
| 928 | 869 | |
| 929 | var send_buffer: [777]u8 = undefined; | |
| 930 | var response = request.respondStreaming(.{ | |
| 931 | .send_buffer = &send_buffer, | |
| 870 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 871 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 872 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 873 | var request = try server.receiveHead(); | |
| 874 | var read_buffer: [100]u8 = undefined; | |
| 875 | var br = try request.readerExpectContinue(&read_buffer); | |
| 876 | var response = try request.respondStreaming(&.{}, .{ | |
| 932 | 877 | .respond_options = .{ |
| 933 | 878 | .transfer_encoding = .none, // Causes keep_alive=false |
| 934 | 879 | }, |
| 935 | 880 | }); |
| 936 | const writer = response.writer(); | |
| 881 | const w = &response.writer; | |
| 937 | 882 | |
| 938 | 883 | while (true) { |
| 939 | 884 | 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); | |
| 885 | const buf = br.peekGreedy(1) catch |err| switch (err) { | |
| 886 | error.EndOfStream => break, | |
| 887 | error.ReadFailed => return error.ReadFailed, | |
| 888 | }; | |
| 889 | br.toss(buf.len); | |
| 890 | for (buf) |*b| b.* = std.ascii.toUpper(b.*); | |
| 891 | try w.writeAll(buf); | |
| 946 | 892 | } |
| 947 | 893 | try response.end(); |
| 948 | 894 | } |
| ... | ... | @@ -952,27 +898,24 @@ test "Server streams both reading and writing" { |
| 952 | 898 | var client: http.Client = .{ .allocator = std.testing.allocator }; |
| 953 | 899 | defer client.deinit(); |
| 954 | 900 | |
| 955 | var server_header_buffer: [555]u8 = undefined; | |
| 956 | var req = try client.open(.POST, .{ | |
| 901 | var redirect_buffer: [555]u8 = undefined; | |
| 902 | var req = try client.request(.POST, .{ | |
| 957 | 903 | .scheme = "http", |
| 958 | 904 | .host = .{ .raw = "127.0.0.1" }, |
| 959 | 905 | .port = test_server.port(), |
| 960 | 906 | .path = .{ .percent_encoded = "/" }, |
| 961 | }, .{ | |
| 962 | .server_header_buffer = &server_header_buffer, | |
| 963 | }); | |
| 907 | }, .{}); | |
| 964 | 908 | defer req.deinit(); |
| 965 | 909 | |
| 966 | 910 | req.transfer_encoding = .chunked; |
| 967 | try req.send(); | |
| 968 | try req.wait(); | |
| 911 | var body_writer = try req.sendBody(&.{}); | |
| 912 | var response = try req.receiveHead(&redirect_buffer); | |
| 969 | 913 | |
| 970 | try req.writeAll("one "); | |
| 971 | try req.writeAll("fish"); | |
| 914 | try body_writer.writer.writeAll("one "); | |
| 915 | try body_writer.writer.writeAll("fish"); | |
| 916 | try body_writer.end(); | |
| 972 | 917 | |
| 973 | try req.finish(); | |
| 974 | ||
| 975 | const body = try req.reader().readAllAlloc(std.testing.allocator, 8192); | |
| 918 | const body = try response.reader(&.{}).allocRemaining(std.testing.allocator, .limited(8192)); | |
| 976 | 919 | defer std.testing.allocator.free(body); |
| 977 | 920 | |
| 978 | 921 | try expectEqualStrings("ONE FISH", body); |
| ... | ... | @@ -987,9 +930,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 987 | 930 | defer gpa.free(location); |
| 988 | 931 | const uri = try std.Uri.parse(location); |
| 989 | 932 | |
| 990 | var server_header_buffer: [1024]u8 = undefined; | |
| 991 | var req = try client.open(.POST, uri, .{ | |
| 992 | .server_header_buffer = &server_header_buffer, | |
| 933 | var redirect_buffer: [1024]u8 = undefined; | |
| 934 | var req = try client.request(.POST, uri, .{ | |
| 993 | 935 | .extra_headers = &.{ |
| 994 | 936 | .{ .name = "content-type", .value = "text/plain" }, |
| 995 | 937 | }, |
| ... | ... | @@ -998,14 +940,14 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 998 | 940 | |
| 999 | 941 | req.transfer_encoding = .{ .content_length = 14 }; |
| 1000 | 942 | |
| 1001 | try req.send(); | |
| 1002 | try req.writeAll("Hello, "); | |
| 1003 | try req.writeAll("World!\n"); | |
| 1004 | try req.finish(); | |
| 943 | var body_writer = try req.sendBody(&.{}); | |
| 944 | try body_writer.writer.writeAll("Hello, "); | |
| 945 | try body_writer.writer.writeAll("World!\n"); | |
| 946 | try body_writer.end(); | |
| 1005 | 947 | |
| 1006 | try req.wait(); | |
| 948 | var response = try req.receiveHead(&redirect_buffer); | |
| 1007 | 949 | |
| 1008 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 950 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 1009 | 951 | defer gpa.free(body); |
| 1010 | 952 | |
| 1011 | 953 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1021,9 +963,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1021 | 963 | .{port}, |
| 1022 | 964 | )); |
| 1023 | 965 | |
| 1024 | var server_header_buffer: [1024]u8 = undefined; | |
| 1025 | var req = try client.open(.POST, uri, .{ | |
| 1026 | .server_header_buffer = &server_header_buffer, | |
| 966 | var redirect_buffer: [1024]u8 = undefined; | |
| 967 | var req = try client.request(.POST, uri, .{ | |
| 1027 | 968 | .extra_headers = &.{ |
| 1028 | 969 | .{ .name = "content-type", .value = "text/plain" }, |
| 1029 | 970 | }, |
| ... | ... | @@ -1032,14 +973,14 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1032 | 973 | |
| 1033 | 974 | req.transfer_encoding = .chunked; |
| 1034 | 975 | |
| 1035 | try req.send(); | |
| 1036 | try req.writeAll("Hello, "); | |
| 1037 | try req.writeAll("World!\n"); | |
| 1038 | try req.finish(); | |
| 976 | var body_writer = try req.sendBody(&.{}); | |
| 977 | try body_writer.writer.writeAll("Hello, "); | |
| 978 | try body_writer.writer.writeAll("World!\n"); | |
| 979 | try body_writer.end(); | |
| 1039 | 980 | |
| 1040 | try req.wait(); | |
| 981 | var response = try req.receiveHead(&redirect_buffer); | |
| 1041 | 982 | |
| 1042 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 983 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 1043 | 984 | defer gpa.free(body); |
| 1044 | 985 | |
| 1045 | 986 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1053,8 +994,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1053 | 994 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#fetch", .{port}); |
| 1054 | 995 | defer gpa.free(location); |
| 1055 | 996 | |
| 1056 | var body = std.ArrayList(u8).init(gpa); | |
| 1057 | defer body.deinit(); | |
| 997 | var body: std.ArrayListUnmanaged(u8) = .empty; | |
| 998 | defer body.deinit(gpa); | |
| 1058 | 999 | |
| 1059 | 1000 | const res = try client.fetch(.{ |
| 1060 | 1001 | .location = .{ .url = location }, |
| ... | ... | @@ -1063,7 +1004,7 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1063 | 1004 | .extra_headers = &.{ |
| 1064 | 1005 | .{ .name = "content-type", .value = "text/plain" }, |
| 1065 | 1006 | }, |
| 1066 | .response_storage = .{ .dynamic = &body }, | |
| 1007 | .response_storage = .{ .allocator = gpa, .list = &body }, | |
| 1067 | 1008 | }); |
| 1068 | 1009 | try expectEqual(.ok, res.status); |
| 1069 | 1010 | try expectEqualStrings("Hello, World!\n", body.items); |
| ... | ... | @@ -1074,9 +1015,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1074 | 1015 | defer gpa.free(location); |
| 1075 | 1016 | const uri = try std.Uri.parse(location); |
| 1076 | 1017 | |
| 1077 | var server_header_buffer: [1024]u8 = undefined; | |
| 1078 | var req = try client.open(.POST, uri, .{ | |
| 1079 | .server_header_buffer = &server_header_buffer, | |
| 1018 | var redirect_buffer: [1024]u8 = undefined; | |
| 1019 | var req = try client.request(.POST, uri, .{ | |
| 1080 | 1020 | .extra_headers = &.{ |
| 1081 | 1021 | .{ .name = "expect", .value = "100-continue" }, |
| 1082 | 1022 | .{ .name = "content-type", .value = "text/plain" }, |
| ... | ... | @@ -1086,15 +1026,15 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1086 | 1026 | |
| 1087 | 1027 | req.transfer_encoding = .chunked; |
| 1088 | 1028 | |
| 1089 | try req.send(); | |
| 1090 | try req.writeAll("Hello, "); | |
| 1091 | try req.writeAll("World!\n"); | |
| 1092 | try req.finish(); | |
| 1029 | var body_writer = try req.sendBody(&.{}); | |
| 1030 | try body_writer.writer.writeAll("Hello, "); | |
| 1031 | try body_writer.writer.writeAll("World!\n"); | |
| 1032 | try body_writer.end(); | |
| 1093 | 1033 | |
| 1094 | try req.wait(); | |
| 1095 | try expectEqual(.ok, req.response.status); | |
| 1034 | var response = try req.receiveHead(&redirect_buffer); | |
| 1035 | try expectEqual(.ok, response.head.status); | |
| 1096 | 1036 | |
| 1097 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 1037 | const body = try response.reader(&.{}).allocRemaining(gpa, .limited(8192)); | |
| 1098 | 1038 | defer gpa.free(body); |
| 1099 | 1039 | |
| 1100 | 1040 | try expectEqualStrings("Hello, World!\n", body); |
| ... | ... | @@ -1105,9 +1045,8 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1105 | 1045 | defer gpa.free(location); |
| 1106 | 1046 | const uri = try std.Uri.parse(location); |
| 1107 | 1047 | |
| 1108 | var server_header_buffer: [1024]u8 = undefined; | |
| 1109 | var req = try client.open(.POST, uri, .{ | |
| 1110 | .server_header_buffer = &server_header_buffer, | |
| 1048 | var redirect_buffer: [1024]u8 = undefined; | |
| 1049 | var req = try client.request(.POST, uri, .{ | |
| 1111 | 1050 | .extra_headers = &.{ |
| 1112 | 1051 | .{ .name = "content-type", .value = "text/plain" }, |
| 1113 | 1052 | .{ .name = "expect", .value = "garbage" }, |
| ... | ... | @@ -1117,23 +1056,24 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1117 | 1056 | |
| 1118 | 1057 | req.transfer_encoding = .chunked; |
| 1119 | 1058 | |
| 1120 | try req.send(); | |
| 1121 | try req.wait(); | |
| 1122 | try expectEqual(.expectation_failed, req.response.status); | |
| 1059 | var body_writer = try req.sendBody(&.{}); | |
| 1060 | try body_writer.flush(); | |
| 1061 | var response = try req.receiveHead(&redirect_buffer); | |
| 1062 | try expectEqual(.expectation_failed, response.head.status); | |
| 1063 | _ = try response.reader(&.{}).discardRemaining(); | |
| 1123 | 1064 | } |
| 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 | 1065 | } |
| 1131 | 1066 | |
| 1132 | 1067 | const TestServer = struct { |
| 1068 | shutting_down: bool, | |
| 1133 | 1069 | server_thread: std.Thread, |
| 1134 | 1070 | net_server: std.net.Server, |
| 1135 | 1071 | |
| 1136 | 1072 | fn destroy(self: *@This()) void { |
| 1073 | self.shutting_down = true; | |
| 1074 | const conn = std.net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); | |
| 1075 | conn.close(); | |
| 1076 | ||
| 1137 | 1077 | self.server_thread.join(); |
| 1138 | 1078 | self.net_server.deinit(); |
| 1139 | 1079 | std.testing.allocator.destroy(self); |
| ... | ... | @@ -1153,20 +1093,27 @@ fn createTestServer(S: type) !*TestServer { |
| 1153 | 1093 | |
| 1154 | 1094 | const address = try std.net.Address.parseIp("127.0.0.1", 0); |
| 1155 | 1095 | 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}); | |
| 1096 | test_server.* = .{ | |
| 1097 | .net_server = try address.listen(.{ .reuse_address = true }), | |
| 1098 | .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}), | |
| 1099 | .shutting_down = false, | |
| 1100 | }; | |
| 1158 | 1101 | return test_server; |
| 1159 | 1102 | } |
| 1160 | 1103 | |
| 1161 | 1104 | test "redirect to different connection" { |
| 1162 | 1105 | const test_server_new = try createTestServer(struct { |
| 1163 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 1164 | var header_buffer: [888]u8 = undefined; | |
| 1106 | fn run(test_server: *TestServer) anyerror!void { | |
| 1107 | const net_server = &test_server.net_server; | |
| 1108 | var recv_buffer: [888]u8 = undefined; | |
| 1109 | var send_buffer: [777]u8 = undefined; | |
| 1165 | 1110 | |
| 1166 | const conn = try net_server.accept(); | |
| 1167 | defer conn.stream.close(); | |
| 1111 | const connection = try net_server.accept(); | |
| 1112 | defer connection.stream.close(); | |
| 1168 | 1113 | |
| 1169 | var server = http.Server.init(conn, &header_buffer); | |
| 1114 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 1115 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 1116 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 1170 | 1117 | var request = try server.receiveHead(); |
| 1171 | 1118 | try expectEqualStrings(request.head.target, "/ok"); |
| 1172 | 1119 | try request.respond("good job, you pass", .{}); |
| ... | ... | @@ -1180,18 +1127,22 @@ test "redirect to different connection" { |
| 1180 | 1127 | global.other_port = test_server_new.port(); |
| 1181 | 1128 | |
| 1182 | 1129 | const test_server_orig = try createTestServer(struct { |
| 1183 | fn run(net_server: *std.net.Server) anyerror!void { | |
| 1184 | var header_buffer: [999]u8 = undefined; | |
| 1130 | fn run(test_server: *TestServer) anyerror!void { | |
| 1131 | const net_server = &test_server.net_server; | |
| 1132 | var recv_buffer: [999]u8 = undefined; | |
| 1185 | 1133 | var send_buffer: [100]u8 = undefined; |
| 1186 | 1134 | |
| 1187 | const conn = try net_server.accept(); | |
| 1188 | defer conn.stream.close(); | |
| 1135 | const connection = try net_server.accept(); | |
| 1136 | defer connection.stream.close(); | |
| 1189 | 1137 | |
| 1190 | const new_loc = try std.fmt.bufPrint(&send_buffer, "http://127.0.0.1:{d}/ok", .{ | |
| 1138 | var loc_buf: [50]u8 = undefined; | |
| 1139 | const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{ | |
| 1191 | 1140 | global.other_port.?, |
| 1192 | 1141 | }); |
| 1193 | 1142 | |
| 1194 | var server = http.Server.init(conn, &header_buffer); | |
| 1143 | var connection_br = connection.stream.reader(&recv_buffer); | |
| 1144 | var connection_bw = connection.stream.writer(&send_buffer); | |
| 1145 | var server = http.Server.init(connection_br.interface(), &connection_bw.interface); | |
| 1195 | 1146 | var request = try server.receiveHead(); |
| 1196 | 1147 | try expectEqualStrings(request.head.target, "/help"); |
| 1197 | 1148 | try request.respond("", .{ |
| ... | ... | @@ -1216,16 +1167,15 @@ test "redirect to different connection" { |
| 1216 | 1167 | const uri = try std.Uri.parse(location); |
| 1217 | 1168 | |
| 1218 | 1169 | { |
| 1219 | var server_header_buffer: [666]u8 = undefined; | |
| 1220 | var req = try client.open(.GET, uri, .{ | |
| 1221 | .server_header_buffer = &server_header_buffer, | |
| 1222 | }); | |
| 1170 | var redirect_buffer: [666]u8 = undefined; | |
| 1171 | var req = try client.request(.GET, uri, .{}); | |
| 1223 | 1172 | defer req.deinit(); |
| 1224 | 1173 | |
| 1225 | try req.send(); | |
| 1226 | try req.wait(); | |
| 1174 | try req.sendBodiless(); | |
| 1175 | var response = try req.receiveHead(&redirect_buffer); | |
| 1176 | var reader = response.reader(&.{}); | |
| 1227 | 1177 | |
| 1228 | const body = try req.reader().readAllAlloc(gpa, 8192); | |
| 1178 | const body = try reader.allocRemaining(gpa, .limited(8192)); | |
| 1229 | 1179 | defer gpa.free(body); |
| 1230 | 1180 | |
| 1231 | 1181 | try expectEqualStrings("good job, you pass", body); |