pax_global_header00006660000000000000000000000064151103053350014506gustar00rootroot0000000000000052 comment=4182eed4c07f8670e1505f7df128f21aaeaf49eb algesten-ureq-proto-4182eed/000077500000000000000000000000001511030533500160415ustar00rootroot00000000000000algesten-ureq-proto-4182eed/.github/000077500000000000000000000000001511030533500174015ustar00rootroot00000000000000algesten-ureq-proto-4182eed/.github/workflows/000077500000000000000000000000001511030533500214365ustar00rootroot00000000000000algesten-ureq-proto-4182eed/.github/workflows/test.yml000066400000000000000000000044711511030533500231460ustar00rootroot00000000000000on: [push, pull_request] name: CI concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: name: Lint runs-on: ubuntu-latest env: RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Run Rustfmt run: cargo fmt -- --check - name: Run Clippy run: cargo clippy doc: name: Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: dtolnay/rust-toolchain@stable - name: Docs env: RUSTDOCFLAGS: -Dwarnings run: cargo doc --no-deps --all-features --document-private-items build_versions: strategy: matrix: rust: [stable, beta, 1.71.1] runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v2 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v1 - run: cargo build build_and_test: name: Test runs-on: ubuntu-latest env: RUST_BACKTRACE: "1" RUSTFLAGS: "-D dead_code -D unused-variables -D unused" steps: - uses: actions/checkout@master - uses: dtolnay/rust-toolchain@stable - name: Test run: cargo test # Disable cargo-deny until this is sorted: https://github.com/EmbarkStudios/cargo-deny/issues/804 # # cargo-deny: # name: cargo-deny # # TODO: remove this matrix when https://github.com/EmbarkStudios/cargo-deny/issues/324 is resolved # strategy: # fail-fast: false # matrix: # platform: # - aarch64-apple-ios # - aarch64-linux-android # - i686-pc-windows-gnu # - i686-pc-windows-msvc # - i686-unknown-linux-gnu # - wasm32-unknown-unknown # - x86_64-apple-darwin # - x86_64-apple-ios # - x86_64-pc-windows-gnu # - x86_64-pc-windows-msvc # - x86_64-unknown-linux-gnu # - x86_64-unknown-redox # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v3 # - uses: EmbarkStudios/cargo-deny-action@v1 # with: # command: check # log-level: error # arguments: --all-features --target ${{ matrix.platform }} algesten-ureq-proto-4182eed/.gitignore000066400000000000000000000000071511030533500200260ustar00rootroot00000000000000target algesten-ureq-proto-4182eed/.vscode/000077500000000000000000000000001511030533500174025ustar00rootroot00000000000000algesten-ureq-proto-4182eed/.vscode/settings.json000066400000000000000000000000731511030533500221350ustar00rootroot00000000000000{ "rust-analyzer.showUnlinkedFileNotification": false }algesten-ureq-proto-4182eed/Cargo.lock000066400000000000000000000030601511030533500177450ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bytes" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "http" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "httparse" version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "ureq-proto" version = "0.5.3" dependencies = [ "base64", "http", "httparse", "log", ] algesten-ureq-proto-4182eed/Cargo.toml000066400000000000000000000012661511030533500177760ustar00rootroot00000000000000[package] name = "ureq-proto" version = "0.5.3" edition = "2021" authors = ["Martin Algesten "] description = "ureq support crate" keywords = ["http", "server", "web"] license = "MIT OR Apache-2.0" repository = "https://github.com/algesten/ureq-proto" exclude = ["/cargo_deny.sh", "/deny.toml", "/doc/", "/run-fuzz.sh", "/test.sh"] # MSRV rust-version = "1.71.1" [features] default = ["client", "server"] client = [] server = [] [dependencies] base64 = { version = "0.22.1", default-features = false, features = ["std"] } http = { version = "1.1.0", default-features = false, features = ["std"] } httparse = { version = "1.8.0", default-features = false } log = "0.4.22" algesten-ureq-proto-4182eed/LICENSE-MIT.txt000066400000000000000000000020371511030533500203150ustar00rootroot00000000000000Copyright 2022 Martin Algesten Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. algesten-ureq-proto-4182eed/README.md000066400000000000000000000002331511030533500173160ustar00rootroot00000000000000# ureq-proto Supporting crate for [ureq](https://crates.io/crates/ureq). This crate contains types used to implement ureq. License: MIT OR Apache-2.0 algesten-ureq-proto-4182eed/cargo_deny.sh000077500000000000000000000025431511030533500205160ustar00rootroot00000000000000#!/usr/bin/env bash # # https://github.com/EmbarkStudios/cargo-deny # # cargo-deny checks our dependency tree for copy-left licenses, # duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories). # # Install: `cargo install cargo-deny` # # This scripts checks the dependency tree for all targets. # cargo-deny is configured in `deny.toml`. set -eu script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cd "$script_path" set -x # cargo install cargo-deny cargo deny --all-features --log-level error --target aarch64-apple-darwin check cargo deny --all-features --log-level error --target i686-pc-windows-gnu check cargo deny --all-features --log-level error --target i686-pc-windows-msvc check cargo deny --all-features --log-level error --target i686-unknown-linux-gnu check cargo deny --all-features --log-level error --target wasm32-unknown-unknown check cargo deny --all-features --log-level error --target x86_64-apple-darwin check cargo deny --all-features --log-level error --target x86_64-pc-windows-gnu check cargo deny --all-features --log-level error --target x86_64-pc-windows-msvc check cargo deny --all-features --log-level error --target x86_64-unknown-linux-gnu check cargo deny --all-features --log-level error --target x86_64-unknown-linux-musl check cargo deny --all-features --log-level error --target x86_64-unknown-redox check algesten-ureq-proto-4182eed/deny.toml000066400000000000000000000053661511030533500177070ustar00rootroot00000000000000# https://github.com/EmbarkStudios/cargo-deny # # cargo-deny checks our dependency tree for copy-left licenses, # duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories). # # Install: `cargo install cargo-deny` # Check: `cargo deny check` or run `cargo_deny.sh`. # Note: running just `cargo deny check` without a `--target` can result in # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 targets = [ { triple = "aarch64-apple-darwin" }, { triple = "i686-pc-windows-gnu" }, { triple = "i686-pc-windows-msvc" }, { triple = "i686-unknown-linux-gnu" }, { triple = "wasm32-unknown-unknown" }, { triple = "x86_64-apple-darwin" }, { triple = "x86_64-pc-windows-gnu" }, { triple = "x86_64-pc-windows-msvc" }, { triple = "x86_64-unknown-linux-gnu" }, { triple = "x86_64-unknown-linux-musl" }, { triple = "x86_64-unknown-redox" }, ] [advisories] yanked = "deny" ignore = [] [bans] multiple-versions = "deny" wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed deny = [] skip = [] skip-tree = [] [licenses] private = { ignore = true } confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text allow = [ "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ "ISC", # https://tldrlegal.com/license/-isc-license "LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321 "MIT-0", # https://choosealicense.com/licenses/mit-0/ "MIT", # https://tldrlegal.com/license/mit-license "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Unicode-3.0", # https://spdx.org/licenses/Unicode-3.0.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] algesten-ureq-proto-4182eed/doc/000077500000000000000000000000001511030533500166065ustar00rootroot00000000000000algesten-ureq-proto-4182eed/doc/client-states.monopic000066400000000000000000000045251511030533500227610ustar00rootroot00000000000000MONOPIC]s8W|O˹uS=ǩCl6v$[vP! e$=ާϨ|_4utS^~MRyռn<_Fw͢eW1Sh5?|4f(Mt)B~̀")ˢG;yUsA3E+-%m[FT~D%8/M>8Ξ#͐k/7\qDЌEzT{T$QH-ȶcڵKm!c4Ng!ml'{m i=>g,=IB-Թ+خ ,bo!YYqYT >_fj΋aEl]Y.x7og)>U\<^e "{s̛޽E.Ä4 iι Q,e^j,;.u-"[?傩(R1gOeɅo Ͽ˭I V}7Ed8CQolt2r5z?N@}h{=s'*,lc& ldn.[^ ^qkGG#E{QբYUie3kl}oWMڄ&mRU 7,HP5I s%)* NjGq*v,\.z.[j%eySjɫf#QXtyV0| N 6s/H.d`/2 k@1`[Xz(svxɑoYg$\xdLhQdqQlcCAF SK/b/y)ZA蒅@B xB1 -ODd 3r4H H'&4bi `@ A85AxW|=>A 'OR:\"!)m.lvF\-mzP[-oض҆mxx1pdB9߲%^]rjgCa^,ҵ:!m]Gב kq4SIwMd& 4e)CMjPp\&λ.32@ X.>v(aTmKOvJ[]ف=fSF,%%a0A@p(*J,p+8A[ RoO6)7bg&y}~OT}b0Xԡ@AVu>Bq̂[%*m](iFc P@GZ6J6`:*icĀ n4á0\}| =x7Rf$8Lf̂/;a>#a<=D EhI-@K~ 4(h6cƾO7h8'Kk8|Uݯq%6t5O5?k7#59 }Mpl*ct]?Wvr' #zxr1 nN%-S,-Bnc Thm 5h-DeC44kwZk7;4 '9\ۅXkFBBre\Zhx7 N Yz qPk_!yXG$pߎ ?t3l=u˷rjwL-#r̂ė(DGSg"ް9salgesten-ureq-proto-4182eed/fuzz/000077500000000000000000000000001511030533500170375ustar00rootroot00000000000000algesten-ureq-proto-4182eed/fuzz/.gitignore000066400000000000000000000000411511030533500210220ustar00rootroot00000000000000target corpus artifacts coverage algesten-ureq-proto-4182eed/fuzz/Cargo.lock000066400000000000000000000056241511030533500207530ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libfuzzer-sys" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", ] [[package]] name = "log" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "ureq-proto" version = "0.3.5" dependencies = [ "base64", "http", "httparse", "log", ] [[package]] name = "ureq-proto-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", "ureq-proto", ] algesten-ureq-proto-4182eed/fuzz/Cargo.toml000066400000000000000000000004511511030533500207670ustar00rootroot00000000000000[package] name = "ureq-proto-fuzz" version = "0.0.0" publish = false edition = "2021" [package.metadata] cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" [dependencies.ureq-proto] path = ".." [[bin]] name = "client" path = "fuzz_targets/client.rs" test = false doc = false bench = false algesten-ureq-proto-4182eed/fuzz/fuzz_targets/000077500000000000000000000000001511030533500215665ustar00rootroot00000000000000algesten-ureq-proto-4182eed/fuzz/fuzz_targets/client.rs000066400000000000000000000244621511030533500234220ustar00rootroot00000000000000#![no_main] use libfuzzer_sys::fuzz_target; use ureq_proto::client::{ Await100Result, Call, RecvBodyResult, RecvResponseResult, SendRequestResult, }; use ureq_proto::http::{Method, Request, Version}; // List of HTTP methods to randomly choose from const METHODS: &[&str] = &["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"]; // List of relevant request headers that drive the Call API logic const RELEVANT_REQUEST_HEADERS: &[(&str, &[&str])] = &[ // Header name, possible values ("content-length", &["10", "100", "1000"]), ("transfer-encoding", &["chunked"]), ("expect", &["100-continue"]), ("connection", &["close", "keep-alive"]), ("host", &["example.com", "test.org", "localhost"]), ("authorization", &["Basic dXNlcjpwYXNz", "Bearer token123"]), ]; // List of relevant response headers that drive the Call API logic const RELEVANT_RESPONSE_HEADERS: &[(&str, &[&str])] = &[ // Header name, possible values ("content-length", &["5", "10", "100"]), ("transfer-encoding", &["chunked"]), ("connection", &["close", "keep-alive"]), ( "location", &[ "http://example.com/redirect", "/relative/path", "../parent/path", ], ), ("set-cookie", &["session=123", "user=test"]), ( "content-type", &["text/plain", "application/json", "text/html"], ), ]; // List of status codes to randomly choose from const STATUS_CODES: &[u16] = &[ 200, 201, 204, // Success 301, 302, 303, 307, 308, // Redirects 400, 401, 403, 404, // Client errors 500, 502, 503, // Server errors ]; fuzz_target!(|data: &[u8]| { // Ensure we have enough data to work with if data.len() < 8 { return; } // Use the first byte to select a method let method_idx = (data[0] as usize) % METHODS.len(); let method_str = METHODS[method_idx]; let method = Method::from_bytes(method_str.as_bytes()).unwrap(); // Create a basic request let mut request_builder = Request::builder() .method(method) .uri("http://example.com/test") .version(Version::HTTP_11); // Use the second byte to determine how many headers to add let header_count = ((data[1] as usize) % 5) + 1; // 1 to 5 headers // Add random headers from the relevant headers list for i in 0..header_count { if i + 2 >= data.len() { break; } let header_idx = (data[i + 2] as usize) % RELEVANT_REQUEST_HEADERS.len(); let (header_name, header_values) = RELEVANT_REQUEST_HEADERS[header_idx]; let value_idx = (data[i + 3] as usize) % header_values.len(); let header_value = header_values[value_idx]; request_builder = request_builder.header(header_name, header_value); } // Build the request let request = match request_builder.body(()) { Ok(req) => req, Err(_) => return, // Skip invalid requests }; // Create a Call from the request let call = match Call::new(request) { Ok(call) => call, Err(_) => return, // Skip if Call creation fails }; // Proceed to SendRequest state let mut call = call.proceed(); // Create a buffer for writing the request let mut output = vec![0u8; 4096]; // Write the request headers match call.write(&mut output) { Ok(_) => {} Err(_) => return, // Skip if writing fails } // Check if we can proceed if !call.can_proceed() { return; // Skip if we can't proceed } // Proceed to the next state let next_call = match call.proceed() { Ok(Some(next)) => next, _ => return, // Skip if proceeding fails }; // Handle the different possible next states match next_call { SendRequestResult::Await100(call) => { // Simulate a 100 Continue response handle_await_100(call, &mut output, data); } SendRequestResult::SendBody(call) => { // Send a body handle_send_body(call, &mut output, data); } SendRequestResult::RecvResponse(call) => { // Simulate a response handle_recv_response(call, &mut output, data); } } }); // Helper function to handle the Await100 state with randomized responses fn handle_await_100( mut call: Call, output: &mut [u8], data: &[u8], ) { // Use a byte from the fuzz data to decide whether to send a 100 Continue response let send_100 = data.len() > 6 && (data[6] % 2 == 0); if send_100 { // Create a randomized 100 Continue response let mut response = String::from("HTTP/1.1 100 Continue\r\n"); // Randomize whether to include headers (which is technically invalid but good for fuzzing) let include_headers = data.len() > 7 && (data[7] % 5 == 0); // 20% chance if include_headers { // Add a random header (100 Continue shouldn't have headers, but we're fuzzing) let header_idx = (data[7] as usize) % RELEVANT_RESPONSE_HEADERS.len(); let (header_name, header_values) = RELEVANT_RESPONSE_HEADERS[header_idx]; let value_idx = (data[7] as usize) % header_values.len(); let header_value = header_values[value_idx]; response.push_str(&format!("{}: {}\r\n", header_name, header_value)); } response.push_str("\r\n"); // Try to read the 100 Continue response match call.try_read_100(response.as_bytes()) { Ok(_) => {} Err(_) => return, // Skip if reading fails } } // Proceed to the next state match call.proceed() { Ok(Await100Result::SendBody(call)) => { // Handle the send body state handle_send_body(call, output, data); } Ok(Await100Result::RecvResponse(call)) => { // Handle the response directly handle_recv_response(call, output, data); } Err(_) => return, // Skip if proceeding fails } } // Helper function to handle the SendBody state fn handle_send_body( mut call: Call, output: &mut [u8], data: &[u8], ) { // Use some of the fuzz data as the body let body_data = if data.len() > 10 { &data[5..10] } else { b"test" }; // Write the body match call.write(body_data, output) { Ok(_) => {} Err(_) => return, // Skip if writing fails } // Write an empty chunk to signal the end of the body match call.write(&[], &mut output[body_data.len()..]) { Ok(_) => {} Err(_) => return, // Skip if writing fails } // Check if we can proceed if !call.can_proceed() { return; // Skip if we can't proceed } // Proceed to RecvResponse match call.proceed() { Some(call) => { // Handle the response handle_recv_response(call, output, data); } None => return, // Skip if proceeding fails } } // Helper function to handle the RecvResponse state with randomized responses fn handle_recv_response( mut call: Call, output: &mut [u8], data: &[u8], ) { // Randomize the status code let status_code = if data.len() > 11 { let idx = (data[11] as usize) % STATUS_CODES.len(); STATUS_CODES[idx] } else { 200 }; // Start building the response let mut response = format!("HTTP/1.1 {} OK\r\n", status_code); // Randomize the number of headers let header_count = if data.len() > 12 { (data[12] as usize) % 5 // 0 to 4 headers } else { 1 // Default to 1 header }; // Add random headers for i in 0..header_count { if data.len() <= 13 + i { break; } let header_idx = (data[13 + i] as usize) % RELEVANT_RESPONSE_HEADERS.len(); let (header_name, header_values) = RELEVANT_RESPONSE_HEADERS[header_idx]; let value_idx = if data.len() > 14 + i { (data[14 + i] as usize) % header_values.len() } else { 0 }; let header_value = header_values[value_idx]; response.push_str(&format!("{}: {}\r\n", header_name, header_value)); } // End headers section response.push_str("\r\n"); // Try to parse the response match call.try_response(response.as_bytes(), false) { Ok(_) => {} Err(_) => return, // Skip if parsing fails } // Proceed to the next state let next_call = match call.proceed() { Some(next) => next, None => return, // Skip if proceeding fails }; // Handle the different possible next states match next_call { RecvResponseResult::RecvBody(mut call) => { // Create a response body as a Vec to avoid type mismatches let body = if response.contains("content-length: 5") || response.contains("Content-Length: 5") { b"hello".to_vec() } else { // For other content lengths or chunked encoding, use a generic body b"hello world this is a test body".to_vec() }; // Read the response body match call.read(&body, output) { Ok(_) => {} Err(_) => return, // Skip if reading fails } // Proceed to the next state let next_call = match call.proceed() { Some(RecvBodyResult::Cleanup(call)) => call, Some(RecvBodyResult::Redirect(_)) => { // We don't want to follow redirects, so we're done return; } None => return, // Skip if proceeding fails }; // Check if we need to close the connection let _must_close = next_call.must_close_connection(); // In a real client, we would close the connection if must_close is true } RecvResponseResult::Redirect(_) => { // We don't want to follow redirects, so we're done return; } RecvResponseResult::Cleanup(call) => { // Check if we need to close the connection let _must_close = call.must_close_connection(); // In a real client, we would close the connection if must_close is true } } } algesten-ureq-proto-4182eed/run-fuzz.sh000077500000000000000000000004561511030533500202050ustar00rootroot00000000000000#!/bin/sh # This is for macOS. # For linux use: NUM_CPUS=$(awk '/^processor/ {++n} END {print n+1}' /proc/cpuinfo) NUM_CPUS=$(sysctl -n hw.ncpu) if [ "$1" == "" ]; then cargo +nightly fuzz list else cargo +nightly fuzz run $1 --jobs $NUM_CPUS -- --stop-after-first-failure -max_len=200000 fi algesten-ureq-proto-4182eed/src/000077500000000000000000000000001511030533500166305ustar00rootroot00000000000000algesten-ureq-proto-4182eed/src/body.rs000066400000000000000000000415311511030533500201370ustar00rootroot00000000000000use std::fmt; use std::io::Write; use http::{header, HeaderName, HeaderValue, Method}; use crate::chunk::Dechunker; use crate::util::{compare_lowercase_ascii, log_data, Writer}; use crate::Error; #[derive(Debug, Clone, Copy, Default)] pub(crate) struct BodyWriter { mode: SenderMode, ended: bool, } #[derive(Debug, Clone, Copy, Default)] enum SenderMode { #[default] None, Sized(u64), Chunked, } // This is 0x2800 in hex. pub(crate) const DEFAULT_CHUNK_SIZE: usize = 10 * 1024; // 4 is 0x2800 and the other + 4 is for the \r\n\r\n overhead. pub(crate) const DEFAULT_CHUNK_OVERHEAD: usize = 4 + 4; pub(crate) const DEFAULT_CHUNK_AND_OVERHEAD: usize = DEFAULT_CHUNK_SIZE + DEFAULT_CHUNK_OVERHEAD; impl BodyWriter { pub fn new_none() -> Self { BodyWriter { mode: SenderMode::None, ended: true, } } pub fn new_chunked() -> Self { BodyWriter { mode: SenderMode::Chunked, ended: false, } } pub fn new_sized(size: u64) -> Self { BodyWriter { mode: SenderMode::Sized(size), ended: false, } } #[cfg(feature = "server")] pub fn body_mode(&self) -> BodyMode { match self.mode { SenderMode::None => BodyMode::NoBody, SenderMode::Sized(n) => BodyMode::LengthDelimited(n), SenderMode::Chunked => BodyMode::Chunked, } } pub fn has_body(&self) -> bool { match self.mode { SenderMode::Sized(n) => n > 0, SenderMode::Chunked => true, SenderMode::None => false, } } pub fn is_chunked(&self) -> bool { matches!(self.mode, SenderMode::Chunked) } pub fn write(&mut self, input: &[u8], w: &mut Writer) -> usize { match &mut self.mode { SenderMode::None => unreachable!(), SenderMode::Sized(left) => { let left_usize = (*left).min(usize::MAX as u64) as usize; let to_write = w.available().min(input.len()).min(left_usize); let success = w.try_write(|w| w.write_all(&input[..to_write])); assert!(success); *left -= to_write as u64; if *left == 0 { self.ended = true; } to_write } SenderMode::Chunked => { let mut input_used = 0; if input.is_empty() { self.finish(w); self.ended = true; } else { // The chunk size might be smaller than the entire input, in which case // we continue to send chunks frome the same input. while write_chunk( // &input[input_used..], &mut input_used, w, DEFAULT_CHUNK_SIZE, ) {} } input_used } } } fn finish(&self, w: &mut Writer) -> bool { if self.is_chunked() { let success = w.try_write(|w| w.write_all(b"0\r\n\r\n")); if !success { return false; } } true } pub(crate) fn body_header(&self) -> (HeaderName, HeaderValue) { match self.mode { SenderMode::None => unreachable!(), SenderMode::Sized(size) => ( header::CONTENT_LENGTH, // TODO(martin): avoid allocation here HeaderValue::from_str(&size.to_string()).unwrap(), ), SenderMode::Chunked => ( header::TRANSFER_ENCODING, HeaderValue::from_static("chunked"), ), } } pub(crate) fn is_ended(&self) -> bool { self.ended } pub(crate) fn left_to_send(&self) -> Option { match self.mode { SenderMode::Sized(v) => Some(v), _ => None, } } pub(crate) fn consume_direct_write(&mut self, amount: usize) { match &mut self.mode { SenderMode::None => unreachable!(), SenderMode::Sized(left) => { *left -= amount as u64; if *left == 0 { self.ended = true; } } SenderMode::Chunked => unreachable!(), } } } #[allow(unused)] pub(crate) fn calculate_chunk_overhead(output_len: usize) -> usize { // The + 1 and floor() is to make even powers of 16 right. // The + 4 is for the \r\n overhead. // // A chunk is with length is: // \r\n // \r\n // // And an end/0-sized chunk is: // 0\r\n // \r\n ((output_len as f64).log(16.0) + 1.0).floor() as usize + 4 } pub(crate) fn calculate_max_input(output_len: usize) -> usize { let chunks = output_len / DEFAULT_CHUNK_AND_OVERHEAD; let remaining = output_len % DEFAULT_CHUNK_AND_OVERHEAD; // We can safely assume remaining is < DEFAULT_CHUNK_AND_OVERHEAD which requires // DEFAULT_CHUNK_HEX number of chars to write. Thus whatever the remaining length is, // it will fit into DEFAULT_CHUNK_HEX + 4 (for the \r\n overhead) let tail = remaining.saturating_sub(DEFAULT_CHUNK_OVERHEAD); chunks * DEFAULT_CHUNK_SIZE + tail } fn write_chunk(input: &[u8], input_used: &mut usize, w: &mut Writer, max_chunk: usize) -> bool { // TODO(martin): Redo this to try and calculate a perfect fit of the // input into the output. // 5 is the smallest possible overhead let available = w.available().saturating_sub(5); let to_write = input.len().min(max_chunk).min(available); let success = w.try_write(|w| { // chunk length write!(w, "{:0x?}\r\n", to_write)?; // chunk w.write_all(&input[..to_write])?; // chunk end write!(w, "\r\n") }); if success { *input_used += to_write; } // write another chunk? success && input.len() > to_write } #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum BodyReader { /// No body is expected either due to the status or method. NoBody, /// Delimited by content-length. /// The value is what's left to receive. LengthDelimited(u64), /// Chunked transfer encoding Chunked(Dechunker), /// Expect remote to close at end of body. #[cfg(feature = "client")] CloseDelimited, } /// Kind of body #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BodyMode { /// No body is expected either due to the status or method. NoBody, /// Delimited by content-length. /// The value is what's left to receive. LengthDelimited(u64), /// Chunked transfer encoding Chunked, /// Expect remote to close at end of body. CloseDelimited, } impl BodyReader { pub fn body_mode(&self) -> BodyMode { match self { BodyReader::NoBody => BodyMode::NoBody, // TODO(martin): if we read body_mode at the wrong time, this v is // not the total length, but the the remaining. BodyReader::LengthDelimited(v) => BodyMode::LengthDelimited(*v), BodyReader::Chunked(_) => BodyMode::Chunked, #[cfg(feature = "client")] BodyReader::CloseDelimited => BodyMode::CloseDelimited, } } #[cfg(feature = "server")] pub fn has_body(&self) -> bool { match self { BodyReader::NoBody => false, BodyReader::LengthDelimited(v) if *v == 0 => false, _ => true, } } #[cfg(feature = "server")] pub fn for_request<'a>( http10: bool, method: &Method, force_send: bool, header_lookup: &'a dyn Fn(http::HeaderName) -> Option<&'a str>, ) -> Result { use crate::ext::MethodExt; if !method.allow_request_body() && !force_send { return Ok(Self::NoBody); } let ret = Self::header_defined(http10, header_lookup)?.unwrap_or_else(|| { if !http10 && method.need_request_body() { Self::Chunked(Dechunker::new()) } else { Self::NoBody } }); Ok(ret) } #[cfg(feature = "client")] // https://datatracker.ietf.org/doc/html/rfc2616#section-4.3 pub fn for_response<'a>( http10: bool, method: &Method, status_code: u16, force_recv: bool, header_lookup: &'a dyn Fn(HeaderName) -> Option<&'a str>, ) -> Result { let header_defined = Self::header_defined(http10, header_lookup)?.unwrap_or(Self::CloseDelimited); // Is body mode being defined in headers? let body_mode_defined = header_defined.body_mode() != BodyMode::CloseDelimited; // Are we allowed to receive a body? let body_allowed = response_body_allowed(method, status_code, header_defined.body_mode()); if body_mode_defined && (body_allowed || force_recv) { // Response contains a body header (even Content-Length: 0) // and expects a body or is forcing a body. Ok(header_defined) } else if !body_mode_defined && body_allowed { // Response has no body header but a body might follow. // Assume close-delimited body. Ok(header_defined) } else { Ok(Self::NoBody) } } fn header_defined<'a>( http10: bool, header_lookup: &'a dyn Fn(HeaderName) -> Option<&'a str>, ) -> Result, Error> { let mut content_length: Option = None; let mut chunked = false; // for head in headers { if let Some(value) = header_lookup(header::CONTENT_LENGTH) { let v = value .parse::() .map_err(|_| Error::BadContentLengthHeader)?; if content_length.is_some() { return Err(Error::TooManyContentLengthHeaders); } content_length = Some(v); } if let Some(value) = header_lookup(header::TRANSFER_ENCODING) { // Header can repeat, stop looking if we found "chunked" chunked = value .split(',') .map(|v| v.trim()) .any(|v| compare_lowercase_ascii(v, "chunked")); } if chunked && !http10 { // https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 // Messages MUST NOT include both a Content-Length header field and a // non-identity transfer-coding. If the message does include a non- // identity transfer-coding, the Content-Length MUST be ignored. return Ok(Some(Self::Chunked(Dechunker::new()))); } if let Some(len) = content_length { return Ok(Some(Self::LengthDelimited(len))); } Ok(None) } /// A request is allowed to have a body based solely upon it's method. A response, /// however, requires a number of factors to be examined. This function checks these /// factors. pub fn read( &mut self, src: &[u8], dst: &mut [u8], stop_on_chunk_boundary: bool, ) -> Result<(usize, usize), Error> { // unwrap is ok because we can't be in state RECV_BODY without setting it. let part = match self { BodyReader::LengthDelimited(_) => self.read_limit(src, dst), BodyReader::Chunked(_) => self.read_chunked(src, dst, stop_on_chunk_boundary), BodyReader::NoBody => return Ok((0, 0)), #[cfg(feature = "client")] BodyReader::CloseDelimited => self.read_unlimit(src, dst), }?; log_data(&src[..part.0]); Ok(part) } fn read_limit(&mut self, src: &[u8], dst: &mut [u8]) -> Result<(usize, usize), Error> { let left = match self { BodyReader::LengthDelimited(v) => v, _ => unreachable!(), }; let left_usize = (*left).min(usize::MAX as u64) as usize; let to_read = src.len().min(dst.len()).min(left_usize); dst[..to_read].copy_from_slice(&src[..to_read]); *left -= to_read as u64; Ok((to_read, to_read)) } fn read_chunked( &mut self, src: &[u8], dst: &mut [u8], stop_on_chunk_boundary: bool, ) -> Result<(usize, usize), Error> { let dechunker = match self { BodyReader::Chunked(v) => v, _ => unreachable!(), }; let mut input_used = 0; let mut output_used = 0; loop { let (i, o) = dechunker.parse_input(&src[input_used..], &mut dst[output_used..])?; input_used += i; output_used += o; if i == 0 || input_used == src.len() || output_used == dst.len() { break; } if dechunker.is_ended() { break; } if stop_on_chunk_boundary && dechunker.is_on_chunk_boundary() { break; } } Ok((input_used, output_used)) } #[cfg(feature = "client")] fn read_unlimit(&mut self, src: &[u8], dst: &mut [u8]) -> Result<(usize, usize), Error> { let to_read = src.len().min(dst.len()); dst[..to_read].copy_from_slice(&src[..to_read]); Ok((to_read, to_read)) } pub fn is_ended(&self) -> bool { match self { BodyReader::NoBody => true, BodyReader::LengthDelimited(v) => *v == 0, BodyReader::Chunked(v) => v.is_ended(), #[cfg(feature = "client")] BodyReader::CloseDelimited => false, } } #[cfg(feature = "client")] pub fn is_ended_chunked(&self) -> bool { match self { BodyReader::Chunked(v) => v.is_ending() || v.is_ended(), _ => false, } } pub(crate) fn is_on_chunk_boundary(&self) -> bool { match self { BodyReader::NoBody => false, BodyReader::LengthDelimited(_) => false, BodyReader::Chunked(v) => v.is_on_chunk_boundary(), #[cfg(feature = "client")] BodyReader::CloseDelimited => false, } } } /// A request is allowed to have a body based solely upon it's method. A response, /// however, requires a number of factors to be examined. This function checks these /// factors. pub fn response_body_allowed(method: &Method, status_code: u16, body_mode: BodyMode) -> bool { let is_success = (200..=299).contains(&status_code); let is_informational = (100..=199).contains(&status_code); let is_redirect = (300..=399).contains(&status_code) && status_code != 304; // Implicitly we know that CloseDelimited means no header indicated that // there was a body. let has_body_header = body_mode != BodyMode::CloseDelimited; // https://datatracker.ietf.org/doc/html/rfc2616#section-4.3 // All responses to the HEAD request method // MUST NOT include a message-body, even though the presence of entity- // header fields might lead one to believe they do. let body_not_allowed = method == Method::HEAD || // A client MUST ignore any Content-Length or Transfer-Encoding // header fields received in a successful response to CONNECT. is_success && method == Method::CONNECT || // All 1xx (informational), 204 (no content), and 304 (not modified) responses // MUST NOT include a message-body. is_informational || matches!(status_code, 204 | 304) || // Surprisingly, redirects may have a body. Whether they do we need to // check the existence of content-length or transfer-encoding headers. is_redirect && !has_body_header; !body_not_allowed } impl fmt::Debug for BodyReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NoBody => write!(f, "NoBody"), Self::LengthDelimited(arg0) => f.debug_tuple("LengthDelimited").field(arg0).finish(), Self::Chunked(_) => write!(f, "Chunked"), #[cfg(feature = "client")] Self::CloseDelimited => write!(f, "CloseDelimited"), } } } #[cfg(test)] mod test { use super::*; #[test] fn test_calculate_max_input() { assert_eq!(calculate_max_input(0), 0); assert_eq!(calculate_max_input(1), 0); assert_eq!(calculate_max_input(2), 0); assert_eq!(calculate_max_input(9), 1); assert_eq!(calculate_max_input(10), 2); assert_eq!(calculate_max_input(11), 3); assert_eq!(calculate_max_input(10247), 10239); assert_eq!(calculate_max_input(10248), 10240); assert_eq!(calculate_max_input(10249), 10240); assert_eq!(calculate_max_input(10250), 10240); assert_eq!(calculate_max_input(10257), 10241); assert_eq!(calculate_max_input(10258), 10242); assert_eq!(calculate_max_input(10259), 10243); } } algesten-ureq-proto-4182eed/src/chunk.rs000066400000000000000000000150641511030533500203140ustar00rootroot00000000000000use core::str; use crate::util::find_crlf; use crate::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Dechunker { Size, Chunk(usize), CrLf, Ending, Trailer, Ended, } #[derive(Debug)] struct Pos { index_in: usize, index_out: usize, } impl Dechunker { pub fn new() -> Self { Dechunker::Size } pub fn parse_input(&mut self, src: &[u8], dst: &mut [u8]) -> Result<(usize, usize), Error> { let mut pos = Pos { index_in: 0, index_out: 0, }; loop { let more = match self { Dechunker::Size => self.read_size(src, &mut pos)?, Dechunker::Chunk(_) => self.read_data(src, dst, &mut pos)?, Dechunker::CrLf => self.expect_crlf(src, &mut pos)?, Dechunker::Ending => self.trailer_or_ended(src, &mut pos)?, Dechunker::Trailer => self.trailer(src, &mut pos)?, Dechunker::Ended => false, }; if !more { break; } } Ok((pos.index_in, pos.index_out)) } pub fn is_on_chunk_boundary(&self) -> bool { *self == Self::Size } #[cfg(test)] fn left(&self) -> usize { if let Self::Chunk(l) = self { *l } else { 0 } } pub fn is_ended(&self) -> bool { matches!(self, Self::Ended) } #[cfg(feature = "client")] pub fn is_ending(&self) -> bool { matches!(self, Self::Ending) } fn read_size(&mut self, src: &[u8], pos: &mut Pos) -> Result { let src = &src[pos.index_in..]; let i = match find_crlf(src) { Some(v) => v, None => return Ok(false), }; const SANITY_CHECK: usize = 20; // Some sanity check for how long the chunk length is if i > SANITY_CHECK { return Err(Error::ChunkExpectedCrLf); } let maybe_meta = src.iter().take(100).position(|c| *c == b';'); let len_end = maybe_meta.unwrap_or(SANITY_CHECK + 1).min(i); let len_str = str::from_utf8(&src[..len_end]) .map_err(|_| Error::ChunkLenNotAscii)? .trim(); let len = usize::from_str_radix(len_str, 16).map_err(|_| Error::ChunkLenNotANumber)?; pos.index_in += i + 2; *self = if len == 0 { Self::Ending } else { Self::Chunk(len) }; Ok(true) } fn read_data(&mut self, src: &[u8], dst: &mut [u8], pos: &mut Pos) -> Result { let src = &src[pos.index_in..]; let dst = &mut dst[pos.index_out..]; let left = match self { Self::Chunk(v) => v, _ => unreachable!(), }; // Read the smallest amount of input/output or length left of chunk. let to_read = src.len().min(dst.len()).min(*left); dst[..to_read].copy_from_slice(&src[..to_read]); pos.index_in += to_read; pos.index_out += to_read; *left -= to_read; if *left == 0 { *self = Self::CrLf; } Ok(to_read > 0) } fn expect_crlf(&mut self, src: &[u8], pos: &mut Pos) -> Result { let src = &src[pos.index_in..]; let i = match find_crlf(src) { Some(v) => v, None => return Ok(false), }; if i > 0 { return Err(Error::ChunkExpectedCrLf); } pos.index_in += 2; *self = Self::Size; Ok(false) } fn trailer_or_ended(&mut self, src: &[u8], pos: &mut Pos) -> Result { let src = &src[pos.index_in..]; let i = match find_crlf(src) { Some(v) => v, None => return Ok(false), }; if i == 0 { pos.index_in += 2; *self = Self::Ended; } else { // Non-crlf before *self = Self::Trailer; } Ok(true) } fn trailer(&mut self, src: &[u8], pos: &mut Pos) -> Result { let src = &src[pos.index_in..]; let i = match find_crlf(src) { Some(v) => v, None => return Ok(false), }; assert!(i > 0); // advance the trailer, and 2 for the crlf. pos.index_in += i + 2; *self = Self::Ending; Ok(true) } } #[cfg(test)] mod test { use super::*; #[test] fn test_dechunk_size() -> Result<(), Error> { let mut d = Dechunker::new(); let mut b = [0; 1024]; assert_eq!(d.parse_input(b"", &mut b)?, (0, 0)); assert_eq!(d.parse_input(b"2", &mut b)?, (0, 0)); assert_eq!(d.parse_input(b"2\r", &mut b)?, (0, 0)); assert_eq!(d.left(), 0); assert_eq!(d.parse_input(b"2\r\n", &mut b)?, (3, 0)); assert_eq!(d.left(), 2); Ok(()) } #[test] fn test_dechunk_size_meta() -> Result<(), Error> { let mut d = Dechunker::new(); let mut b = [0; 1024]; assert_eq!(d.parse_input(b"2;meta\r", &mut b)?, (0, 0)); assert_eq!(d.parse_input(b"2;meta\r\n", &mut b)?, (8, 0)); Ok(()) } #[test] fn test_dechunk_size_not_meta() -> Result<(), Error> { let mut d = Dechunker::new(); let mut b = [0; 1024]; assert_eq!(d.parse_input(b"9\r\nnot meta;\r\n", &mut b)?, (14, 9)); assert_eq!(String::from_utf8_lossy(&b[..9]), "not meta;"); Ok(()) } #[test] #[cfg(feature = "client")] fn test_dechunk_data() -> Result<(), Error> { let mut d = Dechunker::new(); let mut b = [0; 1024]; assert_eq!(d.parse_input(b"2\r\nOK", &mut b)?, (5, 2)); assert_eq!(&b[..2], b"OK"); assert_eq!(d.left(), 0); assert_eq!(d.parse_input(b"\r\n", &mut b)?, (2, 0)); assert_eq!(d.left(), 0); assert!(!d.is_ended()); assert!(!d.is_ending()); assert_eq!(d.parse_input(b"0\r\n", &mut b)?, (3, 0)); assert!(!d.is_ended()); assert!(d.is_ending()); assert_eq!(d.parse_input(b"\r\n", &mut b)?, (2, 0)); assert!(d.is_ended()); assert!(!d.is_ending()); Ok(()) } #[test] fn test_dechunk_one_chunk_at_a_time() -> Result<(), Error> { let mut d = Dechunker::new(); let mut b = [0; 1024]; const DATA: &[u8] = b"4\r\ndata\r\n4\r\nmoar\r\n"; assert_eq!(d.parse_input(DATA, &mut b)?, (9, 4)); // Stop reading on every chunk boundary. assert!(d.is_on_chunk_boundary()); assert_eq!(String::from_utf8_lossy(&b[..4]), "data"); Ok(()) } } algesten-ureq-proto-4182eed/src/client/000077500000000000000000000000001511030533500201065ustar00rootroot00000000000000algesten-ureq-proto-4182eed/src/client/amended.rs000066400000000000000000000233111511030533500220510ustar00rootroot00000000000000use std::{fmt, mem}; use http::uri::PathAndQuery; use http::{header, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, Version}; use crate::body::BodyWriter; use crate::ext::MethodExt; use crate::util::compare_lowercase_ascii; use crate::Error; /// `Request` with amends. /// /// The user provides the `Request<()>`, which we consider an immutable object. /// When executing a request there are a couple of changes/overrides required to /// that immutable object. The `AmendedRequest` encapsulates the original request /// and the amends. /// /// The expected amends are: /// /// 1. Cookie headers. Cookie jar functionality is out of scope, but the /// `headers from such a jar should be possible to add. /// 2. `Host` header. Taken from the Request URI unless already set. /// 3. `Content-Type` header. The actual request body handling is out of scope, /// but an implementation must be able to autodetect the content type for a given body /// and provide that on the request. /// 4. `Content-Length` header. When sending non chunked transfer bodies (and not HTTP/1.0 /// which closes the connection). /// 5. `Transfer-Encoding: chunked` header when the content length for a body is unknown. /// 6. `Content-Encoding` header to indicate on-the-wire compression. The compression itself /// is out of scope, but the user must be able to set it. /// 7. `User-Agent` header. /// 8. `Accept` header. /// pub(crate) struct AmendedRequest { request: Request<()>, headers: Vec<(HeaderName, HeaderValue)>, } impl AmendedRequest { pub fn new(request: Request<()>) -> Self { AmendedRequest { request, headers: vec![], } } pub fn take_request(&mut self) -> Request<()> { let empty = http::Request::new(()); mem::replace(&mut self.request, empty) } pub fn uri(&self) -> &Uri { self.request.uri() } pub fn prelude(&self) -> (&Method, &str, Version) { let r = &self.request; let target = if r.method() == Method::CONNECT { // unwrap allowed as previous code returns error if no authority exists for CONNECT request self.uri().authority().unwrap().as_str() } else { self.uri() .path_and_query() .map(|p| p.as_str()) .unwrap_or("/") }; (r.method(), target, r.version()) } pub fn set_header(&mut self, name: K, value: V) -> Result<(), Error> where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { let name = >::try_from(name) .map_err(Into::into) .map_err(|e| Error::BadHeader(e.to_string()))?; let value = >::try_from(value) .map_err(Into::into) .map_err(|e| Error::BadHeader(e.to_string()))?; self.headers.push((name, value)); Ok(()) } pub fn original_request_headers(&self) -> &HeaderMap { self.request.headers() } pub fn headers(&self) -> impl Iterator { self.headers .iter() .map(|v| (&v.0, &v.1)) .chain(self.request.headers().iter()) } fn headers_get_all(&self, key: HeaderName) -> impl Iterator { self.headers() .filter(move |(k, _)| *k == key) .map(|(_, v)| v) } fn headers_get(&self, key: HeaderName) -> Option<&HeaderValue> { self.headers_get_all(key).next() } pub fn headers_len(&self) -> usize { self.headers().count() } #[cfg(test)] pub fn headers_vec(&self) -> Vec<(&str, &str)> { self.headers() // unwrap here is ok because the tests using this method should // only use header values representable as utf-8. // If we want to test non-utf8 header values, use .headers() // iterator instead. .map(|(k, v)| (k.as_str(), v.to_str().unwrap())) .collect() } pub fn method(&self) -> &Method { self.request.method() } pub(crate) fn version(&self) -> Version { self.request.version() } pub fn new_uri_from_location(&self, location: &str) -> Result { let base = self.uri().clone(); join(base, location) } pub fn analyze( &self, wanted_mode: BodyWriter, allow_non_standard_methods: bool, ) -> Result { let v = self.request.version(); let m = self.method(); if !allow_non_standard_methods { m.verify_version(v)?; } let count_host = self.headers_get_all(header::HOST).count(); if count_host > 1 { return Err(Error::TooManyHostHeaders); } let count_len = self.headers_get_all(header::CONTENT_LENGTH).count(); if count_len > 1 { return Err(Error::TooManyContentLengthHeaders); } let mut req_host_header = false; if let Some(h) = self.headers_get(header::HOST) { h.to_str().map_err(|_| Error::BadHostHeader)?; req_host_header = true; } let mut req_auth_header = false; if let Some(h) = self.headers_get(header::AUTHORIZATION) { h.to_str().map_err(|_| Error::BadAuthorizationHeader)?; req_auth_header = true; } let mut content_length: Option = None; if let Some(h) = self.headers_get(header::CONTENT_LENGTH) { let n = h .to_str() .ok() .and_then(|s| s.parse::().ok()) .ok_or(Error::BadContentLengthHeader)?; content_length = Some(n); } let has_chunked = self .headers_get_all(header::TRANSFER_ENCODING) .filter_map(|v| v.to_str().ok()) .any(|v| compare_lowercase_ascii(v, "chunked")); let req_body_header = has_chunked || content_length.is_some(); // https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 // Messages MUST NOT include both a Content-Length header field and a // non-identity transfer-coding. If the message does include a non- // identity transfer-coding, the Content-Length MUST be ignored. let body_mode = if has_chunked { // chunked "wins" BodyWriter::new_chunked() } else if let Some(n) = content_length { // user provided content-length second BodyWriter::new_sized(n) } else { wanted_mode }; Ok(RequestInfo { body_mode, req_host_header, req_auth_header, req_body_header, }) } } fn join(base: Uri, location: &str) -> Result { let mut parts = base.into_parts(); let maybe = location.parse::(); let has_scheme = maybe .as_ref() .ok() .map(|u| u.scheme().is_some()) .unwrap_or(false); if has_scheme { // Location is a complete Uri. // unwrap is ok beause has_scheme cannot be true if parsing failed. return Ok(maybe.unwrap()); } if location.starts_with("/") { // Location is root-relative, i.e. we keep the // authority of the base uri but replace the path let pq: PathAndQuery = location .parse() .map_err(|_| Error::BadLocationHeader(location.to_string()))?; parts.path_and_query = Some(pq); } else { // Location is relative, i.e. y/foo.html, ../ or ./ which means // we should interpret it against the base uri path. let base_path = parts .path_and_query .as_ref() .map(|p| p.path()) .unwrap_or("/"); let total_path = join_relative(base_path, location)?; let pq: PathAndQuery = total_path .parse() .map_err(|_| Error::BadLocationHeader(location.to_string()))?; parts.path_and_query = Some(pq); } let uri = Uri::from_parts(parts).map_err(|_| Error::BadLocationHeader(location.to_string()))?; Ok(uri) } fn join_relative(base_path: &str, location: &str) -> Result { // base_path should be at least "/". assert!(!base_path.is_empty()); // we should not attempt to join relative if location starts with "/" assert!(!location.starts_with("/")); let mut joiner: Vec<&str> = base_path.split('/').collect(); // "" => [""] // "/" => ["", ""] // "/foo" => ["", "foo"] // "/foo/" => ["", "foo", ""] if joiner.len() > 1 { joiner.pop(); } for segment in location.split('/') { if segment == "." { // do nothing } else if segment == ".." { if joiner.len() == 1 { trace!("Location is relative above root"); return Err(Error::BadLocationHeader(location.to_string())); } joiner.pop(); } else { joiner.push(segment); } } Ok(joiner.join("/")) } pub(crate) struct RequestInfo { pub body_mode: BodyWriter, pub req_host_header: bool, pub req_auth_header: bool, pub req_body_header: bool, } impl fmt::Debug for AmendedRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AmendedRequest") .field("method", &self.request.method()) .field("headers", &self.headers) .finish() } } #[cfg(test)] mod test { use super::*; #[test] fn join_things() { let uri: Uri = "foo.html".parse().unwrap(); println!("{:?}", uri.into_parts()); } } algesten-ureq-proto-4182eed/src/client/await100.rs000066400000000000000000000105531511030533500220060ustar00rootroot00000000000000use http::StatusCode; use crate::body::BodyWriter; use crate::parser::try_parse_response; use crate::Error; use super::state::Await100; use super::{Await100Result, Call}; use crate::CloseReason; impl Call { /// Attempt to read a 100-continue response. /// /// Tries to interpret bytes sent by the server as a 100-continue response. The expect-100 mechanic /// means we hope the server will give us an indication on whether to upload a potentially big /// request body, before we start doing it. /// /// * If the server supports expect-100, it will respond `HTTP/1.1 100 Continue\r\n\r\n`, or /// some other response code (such as 403) if we are not allowed to post the body. /// * If the server does not support expect-100, it will not respond at all, in which case /// we will proceed to sending the request body after some timeout. /// /// The results are: /// /// * `Ok(0)` - not enough data yet, continue waiting (or `proceed()` if you think we waited enough) /// * `Ok(n)` - `n` number of input bytes were consumed. Call `proceed()` next /// * `Err(e)` - some error that is not recoverable pub fn try_read_100(&mut self, input: &[u8]) -> Result { // Try parsing a status line without any headers. The line we are looking for is: // // HTTP/1.1 100 Continue\r\n\r\n // // There should be no headers. match try_parse_response::<0>(input) { Ok(v) => match v { Some((input_used, response)) => { self.inner.await_100_continue = false; if response.status() == StatusCode::CONTINUE { // should_send_body ought to be true since initialization. assert!(self.inner.state.writer.has_body()); Ok(input_used) } else { // We encountered a status line, without headers, but it wasn't 100, // so we should not continue to send the body. Furthermore we mustn't // reuse the connection. // https://curl.se/mail/lib-2004-08/0002.html self.inner.close_reason.push(CloseReason::Not100Continue); self.inner.state.writer = BodyWriter::new_none(); Ok(0) } } // Not enough input yet. None => Ok(0), }, Err(e) => { self.inner.await_100_continue = false; if e == Error::HttpParseTooManyHeaders { // We encountered headers after the status line. That means the server did // not send 100-continue, and also continued to produce an answer before we // sent the body. Regardless of what the answer is, we must not send the body. // A 200-answer would be nonsensical given we haven't yet sent the body. // // We do however want to receive the response to be able to provide // the Response<()> to the user. Hence this is not considered an error. self.inner.close_reason.push(CloseReason::Not100Continue); self.inner.state.writer = BodyWriter::new_none(); Ok(0) } else { Err(e) } } } } /// Tell if there is any point in waiting for more data from the server. /// /// Becomes `false` as soon as `try_read_100()` got enough data to determine what to do next. /// This might become `false` even if `try_read_100` returns `Ok(0)`. /// /// If this returns `false`, the user should continue with `proceed()`. pub fn can_keep_await_100(&self) -> bool { self.inner.await_100_continue } /// Proceed to the next state. pub fn proceed(self) -> Result { // We can always proceed out of Await100 if self.inner.state.writer.has_body() { // TODO(martin): do i need this? // call.inner.call.analyze_request()?; let call = Call::wrap(self.inner); Ok(Await100Result::SendBody(call)) } else { Ok(Await100Result::RecvResponse(Call::wrap(self.inner))) } } } algesten-ureq-proto-4182eed/src/client/mod.rs000066400000000000000000001300001511030533500212250ustar00rootroot00000000000000//! HTTP/1.1 client protocol //! //! Sans-IO protocol impl, which means "writing" and "reading" are made via buffers //! rather than the Write/Read std traits. //! //! The [`Call`] object attempts to encode correct HTTP/1.1 handling using //! state variables, for example `Call<'a, SendRequest>` to represent the //! lifecycle stage where we are to send the request. //! //! The states are: //! //! * **Prepare** - Preparing a request means 1) adding headers such as //! cookies. 2) acquiring the connection from a pool or opening a new //! socket (potentially wrappping in TLS) //! * **SendRequest** - Send the first row, which is the method, path //! and version as well as the request headers //! * **SendBody** - Send the request body //! * **Await100** - If there is an `Expect: 100-continue` header, the //! client should pause before sending the body //! * **RecvResponse** - Receive the response, meaning the status and //! version and the response headers //! * **RecvBody** - Receive the response body //! * **Redirect** - Handle redirects, potentially spawning new requests //! * **Cleanup** - Return the connection to the pool or close it //! //! //! ```text //! ┌──────────────────┐ //! ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶│ Prepare │ //! └──────────────────┘ //! │ │ //! ▼ //! │ ┌──────────────────┐ //! ┌──│ SendRequest │──────────────┐ //! │ │ └──────────────────┘ │ //! │ │ │ //! │ │ ▼ ▼ //! │ ┌──────────────────┐ ┌──────────────────┐ //! │ │ │ SendBody │◀───│ Await100 │ //! │ └──────────────────┘ └──────────────────┘ //! │ │ │ │ //! │ ▼ │ //! │ └─▶┌──────────────────┐◀─────────────┘ //! ┌─────────────│ RecvResponse │──┐ //! │ │ └──────────────────┘ │ //! │ │ │ //! │ ▼ ▼ │ //! ┌──────────────────┐ ┌──────────────────┐ │ //! └ ─│ Redirect │◀───│ RecvBody │ │ //! └──────────────────┘ └──────────────────┘ │ //! │ │ │ //! │ ▼ │ //! │ ┌──────────────────┐ │ //! └────────────▶│ Cleanup │◀─┘ //! └──────────────────┘ //! ``` //! //! # Example //! //! ``` //! use ureq_proto::client::*; //! use ureq_proto::http::Request; //! //! let request = Request::put("https://example.test/my-path") //! .header("Expect", "100-continue") //! .header("x-foo", "bar") //! .body(()) //! .unwrap(); //! //! // ********************************** Prepare //! //! let mut call = Call::new(request).unwrap(); //! //! // Prepare with state from cookie jar. The uri //! // is used to key the cookies. //! let uri = call.uri(); //! //! // call.header("Cookie", "my_cookie1=value1"); //! // call.header("Cookie", "my_cookie2=value2"); //! //! // Obtain a connection for the uri, either a //! // pooled connection from a previous http/1.1 //! // keep-alive, or open a new. The connection //! // must be TLS wrapped if the scheme so indicate. //! // let connection = todo!(); //! //! // Sans-IO means it does not use any //! // Write trait or similar. Requests and request //! // bodies are written to a buffer that in turn //! // should be sent via the connection. //! let mut output = vec![0_u8; 1024]; //! //! // ********************************** SendRequest //! //! // Proceed to the next state writing the request. //! let mut call = call.proceed(); //! //! let output_used = call.write(&mut output).unwrap(); //! assert_eq!(output_used, 107); //! //! assert_eq!(&output[..output_used], b"\ //! PUT /my-path HTTP/1.1\r\n\ //! host: example.test\r\n\ //! transfer-encoding: chunked\r\n\ //! expect: 100-continue\r\n\ //! x-foo: bar\r\n\ //! \r\n"); //! //! // Check we can continue to send the body //! assert!(call.can_proceed()); //! //! // ********************************** Await100 //! //! // In this example, we know the next state is Await100. //! // A real client needs to match on the variants. //! let mut call = match call.proceed() { //! Ok(Some(SendRequestResult::Await100(v))) => v, //! _ => panic!(), //! }; //! //! // When awaiting 100, the client should run a timer and //! // proceed to sending the body either when the server //! // indicates it can receive the body, or the timer runs out. //! //! // This boolean can be checked whether there's any point //! // in keeping waiting for the timer to run out. //! assert!(call.can_keep_await_100()); //! //! let input = b"HTTP/1.1 100 Continue\r\n\r\n"; //! let input_used = call.try_read_100(input).unwrap(); //! //! assert_eq!(input_used, 25); //! assert!(!call.can_keep_await_100()); //! //! // ********************************** SendBody //! //! // Proceeding is possible regardless of whether the //! // can_keep_await_100() is true or false. //! // A real client needs to match on the variants. //! let mut call = match call.proceed() { //! Ok(Await100Result::SendBody(v)) => v, //! _ => panic!(), //! }; //! //! let (input_used, o1) = //! call.write(b"hello", &mut output).unwrap(); //! //! assert_eq!(input_used, 5); //! //! // When doing transfer-encoding: chunked, //! // the end of body must be signaled with //! // an empty input. This is also valid for //! // regular content-length body. //! assert!(!call.can_proceed()); //! //! let (_, o2) = call.write(&[], &mut output[o1..]).unwrap(); //! //! let output_used = o1 + o2; //! assert_eq!(output_used, 15); //! //! assert_eq!(&output[..output_used], b"\ //! 5\r\n\ //! hello\ //! \r\n\ //! 0\r\n\ //! \r\n"); //! //! assert!(call.can_proceed()); //! //! // ********************************** RecvRequest //! //! // Proceed to read the request. //! let mut call = call.proceed().unwrap(); //! //! let part = b"HTTP/1.1 200 OK\r\nContent-Len"; //! let full = b"HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\n"; //! //! // try_response can be used repeatedly until we //! // get enough content including all headers. //! let (input_used, maybe_response) = //! call.try_response(part, false).unwrap(); //! //! assert_eq!(input_used, 0); //! assert!(maybe_response.is_none()); //! //! let (input_used, maybe_response) = //! call.try_response(full, false).unwrap(); //! //! assert_eq!(input_used, 38); //! let response = maybe_response.unwrap(); //! //! // ********************************** RecvBody //! //! // It's not possible to proceed until we //! // have read a response. //! let mut call = match call.proceed() { //! Some(RecvResponseResult::RecvBody(v)) => v, //! _ => panic!(), //! }; //! //! let(input_used, output_used) = //! call.read(b"hi there!", &mut output).unwrap(); //! //! assert_eq!(input_used, 9); //! assert_eq!(output_used, 9); //! //! assert_eq!(&output[..output_used], b"hi there!"); //! //! // ********************************** Cleanup //! //! let call = match call.proceed() { //! Some(RecvBodyResult::Cleanup(v)) => v, //! _ => panic!(), //! }; //! //! if call.must_close_connection() { //! // connection.close(); //! } else { //! // connection.return_to_pool(); //! } //! //! ``` use std::fmt; use std::marker::PhantomData; use http::{HeaderValue, StatusCode}; use crate::body::{BodyReader, BodyWriter}; use crate::util::ArrayVec; use crate::CloseReason; use amended::AmendedRequest; mod amended; // mod holder; #[cfg(test)] mod test; /// Max number of headers to parse from an HTTP response pub const MAX_RESPONSE_HEADERS: usize = 128; /// State types for the Call state machine. /// /// These types are used as type parameters to `Call` to represent /// the current state of the HTTP request/response state machine. pub mod state { pub(crate) trait Named { fn name() -> &'static str; } macro_rules! call_state { ($n:tt) => { #[doc(hidden)] pub struct $n(()); impl Named for $n { fn name() -> &'static str { stringify!($n) } } }; } call_state!(Prepare); call_state!(SendRequest); call_state!(Await100); call_state!(SendBody); call_state!(RecvResponse); call_state!(RecvBody); call_state!(Redirect); call_state!(Cleanup); } use self::state::*; /// A state machine for an HTTP request/response cycle. /// /// This type represents a state machine that transitions through various /// states during the lifecycle of an HTTP request/response. /// /// The type parameters are: /// - `State`: The current state of the state machine (e.g., `Prepare`, `SendRequest`, etc.) /// /// See the [state graph][crate::client] in the client module documentation for a /// visual representation of the state transitions. pub struct Call { inner: Inner, _ph: PhantomData, } /// Internal state of a Call. /// /// This struct contains the actual state data for a Call, independent of the /// state type parameter. It's exposed as pub(crate) to allow tests to inspect /// the state. #[derive(Debug)] pub(crate) struct Inner { pub request: AmendedRequest, pub analyzed: bool, pub state: BodyState, pub close_reason: ArrayVec, pub force_recv_body: bool, pub force_send_body: bool, pub await_100_continue: bool, pub status: Option, pub location: Option, } impl Inner { fn is_redirect(&self) -> bool { match self.status { // 304 is a redirect code, but it has no location header and // thus we don't consider it a redirection. Some(v) => v.is_redirection() && v != StatusCode::NOT_MODIFIED, None => false, } } } /// State of the request/response body. /// /// This struct tracks the current phase of the request/response cycle /// and manages the body writers and readers. #[derive(Debug, Default)] pub(crate) struct BodyState { phase: RequestPhase, writer: BodyWriter, reader: Option, allow_non_standard_methods: bool, stop_on_chunk_boundary: bool, } impl BodyState { fn need_response_body(&self) -> bool { !matches!( self.reader, Some(BodyReader::NoBody) | Some(BodyReader::LengthDelimited(0)) ) } } /// Phases of sending an HTTP request. /// /// This enum represents the different phases of sending an HTTP request: /// - `SendLine`: Sending the request line (method, path, version) /// - `SendHeaders`: Sending the request headers /// - `SendBody`: Sending the request body #[derive(Clone, Copy, PartialEq, Eq, Default)] enum RequestPhase { #[default] Line, Headers(usize), Body, } impl RequestPhase { fn is_prelude(&self) -> bool { matches!(self, RequestPhase::Line | RequestPhase::Headers(_)) } fn is_body(&self) -> bool { matches!(self, RequestPhase::Body) } } impl Call { fn wrap(inner: Inner) -> Call where S: Named, { let wrapped = Call { inner, _ph: PhantomData, }; debug!("{:?}", wrapped); wrapped } #[cfg(test)] pub(crate) fn inner(&self) -> &Inner { &self.inner } } // //////////////////////////////////////////////////////////////////////////////////////////// PREPARE mod prepare; // //////////////////////////////////////////////////////////////////////////////////////////// SEND REQUEST mod sendreq; /// Possible state transitions after sending a request. /// /// After sending the request headers, the call can transition to one of three states: /// - `Await100`: If the request included an `Expect: 100-continue` header /// - `SendBody`: If the request has a body to send /// - `RecvResponse`: If the request has no body (e.g., GET, HEAD) /// /// See the [state graph][crate::client] for a visual representation. pub enum SendRequestResult { /// Expect-100/Continue mechanic. Await100(Call), /// Send the request body. SendBody(Call), /// Receive the response. RecvResponse(Call), } // //////////////////////////////////////////////////////////////////////////////////////////// AWAIT 100 mod await100; /// Possible state transitions after awaiting a 100 Continue response. /// /// After awaiting a 100 Continue response, the call can transition to one of two states: /// - `SendBody`: If the server sent a 100 Continue response or the timeout expired /// - `RecvResponse`: If the server sent a different response /// /// See the [state graph][crate::client] for a visual representation. pub enum Await100Result { /// Send the request body. SendBody(Call), /// Receive server response. RecvResponse(Call), } // //////////////////////////////////////////////////////////////////////////////////////////// SEND BODY mod sendbody; // //////////////////////////////////////////////////////////////////////////////////////////// RECV RESPONSE mod recvresp; /// Possible state transitions after receiving a response. /// /// After receiving a response (status and headers), the call can transition to one of three states: /// - `RecvBody`: If the response has a body to receive /// - `Redirect`: If the response is a redirect /// - `Cleanup`: If the response has no body and is not a redirect /// /// See the [state graph][crate::client] for a visual representation. pub enum RecvResponseResult { /// Receive a response body. RecvBody(Call), /// Follow a redirect. Redirect(Call), /// Run cleanup. Cleanup(Call), } // //////////////////////////////////////////////////////////////////////////////////////////// RECV BODY mod recvbody; /// Possible state transitions after receiving a response body. /// /// After receiving a response body, the call can transition to one of two states: /// - `Redirect`: If the response is a redirect /// - `Cleanup`: If the response is not a redirect /// /// See the [state graph][crate::client] for a visual representation. pub enum RecvBodyResult { /// Follow a redirect Redirect(Call), /// Go to cleanup Cleanup(Call), } // //////////////////////////////////////////////////////////////////////////////////////////// REDIRECT mod redirect; /// Strategy for preserving authorization headers during redirects. /// /// This enum defines how authorization headers should be handled when following /// redirects: /// /// * `Never`: Never preserve the `authorization` header in redirects. This is the default. /// * `SameHost`: Preserve the `authorization` header when the redirect is to the same host /// and uses the same scheme (or switches to a more secure one, i.e., from HTTP to HTTPS, /// but not the reverse). #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum RedirectAuthHeaders { /// Never preserve the `authorization` header on redirect. This is the default. Never, /// Preserve the `authorization` header when the redirect is to the same host. Both hosts must use /// the same scheme (or switch to a more secure one, i.e we can redirect from `http` to `https`, /// but not the reverse). SameHost, } // //////////////////////////////////////////////////////////////////////////////////////////// CLEANUP impl Call { /// Tell if we must close the connection. pub fn must_close_connection(&self) -> bool { self.close_reason().is_some() } /// If we are closing the connection, give a reason. pub fn close_reason(&self) -> Option<&'static str> { self.inner.close_reason.first().map(|s| s.explain()) } } // //////////////////////////////////////////////////////////////////////////////////////////// impl fmt::Debug for Call { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Call<{}>", State::name()) } } impl fmt::Debug for RequestPhase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Line => write!(f, "SendLine"), Self::Headers(_) => write!(f, "SendHeaders"), Self::Body => write!(f, "SendBody"), } } } #[cfg(test)] mod tests { use super::*; use crate::client::amended::AmendedRequest; use crate::client::state::SendRequest; use crate::client::Inner; use crate::Error; use http::{header, Method, Request, Version}; use std::str; #[test] fn get_simple() { let req = Request::get("http://foo.test/page") .header("content-length", "0") .body(()) .unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "GET /page HTTP/1.1\r\n\ host: foo.test\r\n\ content-length: 0\r\n\ \r\n" ); // Since GET has no body (explicitly defined by content-length too) we should go straight to RecvResponse let Ok(Some(SendRequestResult::RecvResponse(_call))) = call.proceed() else { panic!("Exepcted `RecvResponse`") }; } #[test] fn head_simple() { let req = Request::head("http://foo.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "HEAD /page HTTP/1.1\r\nhost: foo.test\r\n\r\n"); } #[test] fn head_without_body() { let req = Request::head("http://foo.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to the next state let call = call.proceed().unwrap().unwrap(); // For a HEAD request, we should get a RecvResponse result let SendRequestResult::RecvResponse(_) = call else { panic!("Expected RecvResponse") }; } #[test] fn head_with_body_despite_method() { let req = Request::head("http://foo.fest/page") .header(header::TRANSFER_ENCODING, "chunked") .body(()) .unwrap(); let mut call = Call::new(req).unwrap(); // Force sending a body despite the method call.force_send_body(); let mut call = call.proceed(); let mut output = vec![0; 1024]; call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to the next state let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; // Write an empty body let (i, n) = call.write(&[], &mut output).unwrap(); assert_eq!(i, 0); assert_eq!(n, 5); // "0\r\n\r\n" for chunked encoding // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } #[test] fn post_simple() { let req = Request::post("http://f.test/page") .header("content-length", 5) .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n1 = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to the next state let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; // Write the body let (i1, n2) = call.write(b"hallo", &mut output[n1..]).unwrap(); assert_eq!(i1, 5); let s = str::from_utf8(&output[..n1 + n2]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ncontent-length: 5\r\n\r\nhallo" ); } #[test] fn post_small_output() { let req = Request::post("http://f.test/page") .header("content-length", 5) .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); let mut output = vec![0; 1024]; let body = b"hallo"; // Write the request headers in multiple steps with small output buffers { let n = call.write(&mut output[..25]).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "POST /page HTTP/1.1\r\n"); assert!(!call.can_proceed()); } { let n = call.write(&mut output[..20]).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "host: f.test\r\n"); assert!(!call.can_proceed()); } { let n = call.write(&mut output[..21]).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "content-length: 5\r\n\r\n"); assert!(call.can_proceed()); } // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; // Write the body { let (i, n) = call.write(body, &mut output[..25]).unwrap(); assert_eq!(n, 5); assert_eq!(i, 5); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "hallo"); // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } } #[test] fn post_with_short_content_length() { let req = Request::post("http://f.test/page") .header("content-length", 2) .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n1 = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; // Write the body (first write should fail because it's larger than content-length) let body = b"hallo"; let r = call.write(body, &mut output[n1..]); assert_eq!(r.unwrap_err(), Error::BodyLargerThanContentLength); // Write a smaller body that fits within content-length let body = b"ha"; let r = call.write(body, &mut output[n1..]); assert!(r.is_ok()); // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } #[test] fn post_with_short_body_input() { let req = Request::post("http://f.test/page") .header("content-length", 5) .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n1 = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; // Write the first part of the body let (i1, n2) = call.write(b"ha", &mut output[n1..]).unwrap(); assert_eq!(i1, 2); // Write the second part of the body let (i2, n3) = call.write(b"ha", &mut output[n1 + n2..]).unwrap(); assert_eq!(i2, 2); let s = str::from_utf8(&output[..n1 + n2 + n3]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ncontent-length: 5\r\n\r\nhaha" ); // Check if we can proceed (body is not fully sent yet) assert!(!call.can_proceed()); // Write the third part of the body (should fail because it's larger than remaining content length) let err = call.write(b"llo", &mut output[n1 + n2 + n3..]).unwrap_err(); assert_eq!(err, Error::BodyLargerThanContentLength); // Write the last byte to complete the content length let (i3, n4) = call.write(b"l", &mut output[n1 + n2 + n3..]).unwrap(); assert_eq!(i3, 1); let s = str::from_utf8(&output[..n1 + n2 + n3 + n4]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ncontent-length: 5\r\n\r\nhahal" ); // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } #[test] fn post_with_chunked() { let req = Request::post("http://f.test/page") .header("transfer-encoding", "chunked") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n1 = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody") }; let body = b"hallo"; // Write the first chunk of the body let (i1, n2) = call.write(body, &mut output[n1..]).unwrap(); assert_eq!(i1, 5); // Write the second chunk of the body let (i2, n3) = call.write(body, &mut output[n1 + n2..]).unwrap(); assert_eq!(i2, 5); // Indicate the end of the body let (i3, n4) = call.write(&[], &mut output[n1 + n2 + n3..]).unwrap(); assert_eq!(i3, 0); let s = str::from_utf8(&output[..n1 + n2 + n3 + n4]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ntransfer-encoding: chunked\r\n\r\n5\r\nhallo\r\n5\r\nhallo\r\n0\r\n\r\n" ); // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } #[test] fn post_without_body() { let req = Request::post("http://foo.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to the next state let call = call.proceed().unwrap().unwrap(); // For a POST request, we should get a SendBody result let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody"); }; // Check that we can't proceed without writing a body assert!(!call.can_proceed()); // Write an empty body let (i, n) = call.write(&[], &mut output).unwrap(); assert_eq!(i, 0); assert_eq!(n, 5); // "0\r\n\r\n" for chunked encoding // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); } #[test] fn post_streaming() { let req = Request::post("http://f.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n1 = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody"); }; // Write the first chunk of the body (using i2, n2 to match original test) let (i2, n2) = call.write(b"hallo", &mut output[n1..]).unwrap(); // Send end (using i3, n3 to match original test) let (i3, n3) = call.write(&[], &mut output[n1 + n2..]).unwrap(); // Use i1 = 0 to match original test (in Call API, i1 is not used for headers) let i1 = 0; // Verify the results with the same assertions as the original test assert_eq!(i1, 0); assert_eq!(i2, 5); assert_eq!(n1, 65); assert_eq!(n2, 10); assert_eq!(i3, 0); assert_eq!(n3, 5); let s = str::from_utf8(&output[..(n1 + n2 + n3)]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ntransfer-encoding: chunked\r\n\r\n5\r\nhallo\r\n0\r\n\r\n" ); } #[test] fn post_streaming_with_size() { let req = Request::post("http://f.test/page") .header("content-length", "5") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let headers_n = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody"); }; // Write the body (first call) let (i1, n1) = call.write(b"hallo", &mut output[headers_n..]).unwrap(); // Verify the results assert_eq!(i1, 5); // In Call API, i1 is the number of bytes consumed from the input assert_eq!(n1, 5); // In Call API, n1 is the number of bytes written to the output // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); // Try to write more data after the body is fully sent (should fail) let err = call .write(b"hallo", &mut output[headers_n + n1..]) .unwrap_err(); assert_eq!(err, Error::BodyContentAfterFinish); let s = str::from_utf8(&output[..headers_n + n1]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ncontent-length: 5\r\n\r\nhallo" ); } #[test] fn post_streaming_after_end() { let req = Request::post("http://f.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let headers_n = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody"); }; // Write the body let (_, n1) = call.write(b"hallo", &mut output[headers_n..]).unwrap(); // Send end let (_, n2) = call.write(&[], &mut output[headers_n + n1..]).unwrap(); // Try to write after end let err = call.write(b"after end", &mut output[headers_n + n1 + n2..]); assert_eq!(err, Err(Error::BodyContentAfterFinish)); } #[test] fn post_streaming_too_much() { let req = Request::post("http://f.test/page") .header("content-length", "5") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let headers_n = call.write(&mut output).unwrap(); // Check if we can proceed assert!(call.can_proceed()); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(mut call) = call else { panic!("Expected SendBody"); }; // Write the body (first call) let (i1, n1) = call.write(b"hallo", &mut output[headers_n..]).unwrap(); // Verify the results assert_eq!(i1, 5); // In Call API, i1 is the number of bytes consumed from the input assert_eq!(n1, 5); // In Call API, n1 is the number of bytes written to the output // Check if we can proceed (body is fully sent) assert!(call.can_proceed()); // Try to write more data after the body is fully sent (should fail with BodyContentAfterFinish) let err = call .write(b"hallo", &mut output[headers_n + n1..]) .unwrap_err(); assert_eq!(err, Error::BodyContentAfterFinish); let s = str::from_utf8(&output[..headers_n + n1]).unwrap(); assert_eq!( s, "POST /page HTTP/1.1\r\nhost: f.test\r\ncontent-length: 5\r\n\r\nhallo" ); } #[test] fn username_password_uri() { let req = Request::get("http://martin:secret@f.test/page") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "GET /page HTTP/1.1\r\nhost: f.test\r\n\ authorization: Basic bWFydGluOnNlY3JldA==\r\n\r\n" ); } #[test] fn username_uri() { let req = Request::get("http://martin@f.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "GET /page HTTP/1.1\r\nhost: f.test\r\n\ authorization: Basic bWFydGluOg==\r\n\r\n" ); } #[test] fn password_uri() { let req = Request::get("http://:secret@f.test/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "GET /page HTTP/1.1\r\nhost: f.test\r\n\ authorization: Basic OnNlY3JldA==\r\n\r\n" ); } #[test] fn override_auth_header() { let req = Request::get("http://martin:secret@f.test/page") // This should override the auth from the URI .header("authorization", "meh meh meh") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "GET /page HTTP/1.1\r\nhost: f.test\r\n\ authorization: meh meh meh\r\n\r\n" ); } #[test] fn non_standard_port() { let req = Request::get("http://f.test:8080/page").body(()).unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "GET /page HTTP/1.1\r\nhost: f.test:8080\r\n\r\n"); } #[test] fn non_standard_method_not_allowed() { let m = Method::from_bytes(b"FNORD").unwrap(); let req = Request::builder() .method(m.clone()) .uri("http://f.test:8080/page") .body(()) .unwrap(); let call = Call::new(req).unwrap(); // Proceed to SendRequest let mut call = call.proceed(); // Try to write the request headers let mut output = vec![0; 1024]; let err = call.write(&mut output).unwrap_err(); assert_eq!(err, Error::MethodVersionMismatch(m, Version::HTTP_11)); } #[test] fn non_standard_method_when_allowed() { let m = Method::from_bytes(b"FNORD").unwrap(); let req = Request::builder() .method(m.clone()) .uri("http://f.test:8080/page") .body(()) .unwrap(); let mut call = Call::new(req).unwrap(); // Allow non-standard methods call.allow_non_standard_methods(true); // Proceed to SendRequest let mut call = call.proceed(); // Write the request headers let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "FNORD /page HTTP/1.1\r\nhost: f.test:8080\r\n\r\n"); } #[test] fn ensure_reasonable_stack_sizes() { macro_rules! ensure { ($type:ty, $size:tt) => { let sz = std::mem::size_of::<$type>(); assert!( sz <= $size, "Stack size of {} is too big {} > {}", stringify!($type), sz, $size ); }; } ensure!(http::Request<()>, 300); // 224 ensure!(AmendedRequest, 400); // 368 ensure!(Inner, 600); // 512 ensure!(Call, 600); // 512 } #[test] fn connect() { let req = Request::builder() .method(Method::CONNECT) .uri("http://example.com:80") .body(()) .unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); // CONNECT request should have request target in authority form let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "CONNECT example.com:80 HTTP/1.1\r\nhost: example.com:80\r\n\r\n" ); // Should go to RecvResponse state (content-length/transfer-encoding is ignored with CONNECT) let SendRequestResult::RecvResponse(mut call) = call.proceed().unwrap().unwrap() else { panic!("Expected RecvResponse state") }; let input = b"HTTP/1.1 200 OK\r\n\r\n"; let (n, res) = call.try_response(input, false).unwrap(); assert_eq!(n, 19); let Some(res) = res else { panic!("`try_response()` should return a response"); }; assert!(res.headers().is_empty()); // should go to Cleanup state (content-length/transfer-encoding is ignored with CONNECT) let RecvResponseResult::Cleanup(_call) = call.proceed().unwrap() else { panic!("Expected Cleanup state") }; } #[test] fn connect_read_body() { let req = Request::builder() .method(Method::CONNECT) .uri("http://example.com:80") .body(()) .unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); // CONNECT request should have request target in authority form let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "CONNECT example.com:80 HTTP/1.1\r\nhost: example.com:80\r\n\r\n" ); // Should go to RecvResponse state (content-length/transfer-encoding is ignored with CONNECT) let SendRequestResult::RecvResponse(mut call) = call.proceed().unwrap().unwrap() else { panic!("Expected RecvResponse state") }; call.force_recv_body(); let input = b"HTTP/1.1 200 OK\r\ncontent-length: 1024\r\n\r\n"; let (_n, res) = call.try_response(input, false).unwrap(); let Some(_res) = res else { panic!("`try_response()` should return a response"); }; let RecvResponseResult::RecvBody(_call) = call.proceed().unwrap() else { panic!("Expect RecvBody state"); }; } #[test] fn connect_send_body_fails() { let req = Request::builder() .method(Method::CONNECT) .uri("http://example.com:80") .header("content-length", 1024) .body(()) .unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; call.write(&mut output) .expect_err("no body allowed on CONNECT request"); } #[test] fn connect_send_body_with_footgun() { let req = Request::builder() .method(Method::CONNECT) .uri("http://example.com:80") .header("content-length", 1024) .body(()) .unwrap(); let mut call = Call::new(req).unwrap(); call.force_send_body(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); // CONNECT request should have request target in authority form let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "CONNECT example.com:80 HTTP/1.1\r\nhost: example.com:80\r\ncontent-length: 1024\r\n\r\n" ); let SendRequestResult::SendBody(_call) = call.proceed().unwrap().unwrap() else { panic!("Expected SendBody state") }; } #[test] fn query_no_slash() { let req = Request::get("http://foo.test?query=foo").body(()).unwrap(); let call = Call::new(req).unwrap(); let mut call = call.proceed(); let mut output = vec![0; 1024]; let n = call.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); // The added / here is to be compliant with RFC 9112 §3.2.1 assert_eq!( s, "GET /?query=foo HTTP/1.1\r\n\ host: foo.test\r\n\ \r\n" ); } } algesten-ureq-proto-4182eed/src/client/prepare.rs000066400000000000000000000065621511030533500221230ustar00rootroot00000000000000use http::{header, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, Version}; use crate::body::BodyWriter; use crate::client::amended::AmendedRequest; use crate::ext::{HeaderIterExt, MethodExt}; use crate::{ArrayVec, Error}; use super::state::{Prepare, SendRequest}; use super::{BodyState, Call, CloseReason, Inner}; impl Call { /// Create a new Call instance from an HTTP request. /// /// This initializes a new Call state machine in the Prepare state, /// setting up the necessary internal state based on the request properties. pub fn new(request: Request<()>) -> Result { let mut close_reason = ArrayVec::from_fn(|_| CloseReason::ClientConnectionClose); if request.version() == Version::HTTP_10 { // request.analyze() in CallHolder::new() ensures the only versions are HTTP 1.0 and 1.1 close_reason.push(CloseReason::CloseDelimitedBody) } if request.headers().iter().has(header::CONNECTION, "close") { close_reason.push(CloseReason::ClientConnectionClose); } let need_request_body = request.method().need_request_body(); let await_100_continue = request.headers().iter().has_expect_100(); let request = AmendedRequest::new(request); let default_body_mode = if need_request_body { BodyWriter::new_chunked() } else { BodyWriter::new_none() }; let inner = Inner { request, analyzed: false, state: BodyState { writer: default_body_mode, ..Default::default() }, close_reason, force_send_body: false, force_recv_body: false, await_100_continue, status: None, location: None, }; Ok(Call::wrap(inner)) } /// Inspect call method pub fn method(&self) -> &Method { self.inner.request.method() } /// Inspect call URI pub fn uri(&self) -> &Uri { self.inner.request.uri() } /// Inspect call HTTP version pub fn version(&self) -> Version { self.inner.request.version() } /// Inspect call headers pub fn headers(&self) -> &HeaderMap { self.inner.request.original_request_headers() } /// Set whether to allow non-standard HTTP methods. /// /// By default the methods are limited by the HTTP version. pub fn allow_non_standard_methods(&mut self, v: bool) { self.inner.state.allow_non_standard_methods = v; } /// Add more headers to the call pub fn header(&mut self, key: K, value: V) -> Result<(), Error> where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { self.inner.request.set_header(key, value) } /// Convert the state to send body despite method. /// /// Methods like GET, HEAD and DELETE should not have a request body. /// Some broken APIs use bodies anyway, and this is an escape hatch to /// interoperate with such services. pub fn force_send_body(&mut self) { self.inner.force_send_body = true; } /// Continue to the next call state. pub fn proceed(self) -> Call { Call::wrap(self.inner) } } algesten-ureq-proto-4182eed/src/client/recvbody.rs000066400000000000000000000065501511030533500222770ustar00rootroot00000000000000use crate::body::BodyReader; use crate::{BodyMode, Error}; use super::state::RecvBody; use super::{Call, RecvBodyResult}; impl Call { /// Read the response body from `input` to `output`. /// /// Depending on response headers, we can be in `transfer-encoding: chunked` or not. If we are, /// there will be less `output` bytes than `input`. /// /// The result `(usize, usize)` is `(input consumed, output buffer used)`. pub fn read(&mut self, input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> { let rbm = self.inner.state.reader.as_mut().unwrap(); if rbm.is_ended() { return Ok((0, 0)); } rbm.read(input, output, self.inner.state.stop_on_chunk_boundary) } /// Set if we are stopping on chunk boundaries. /// /// If `false`, we try to fill entire `output` on each read() call. /// Has no meaning unless the response in chunked. /// /// Defaults to `false` pub fn stop_on_chunk_boundary(&mut self, enabled: bool) { self.inner.state.stop_on_chunk_boundary = enabled; } /// Tell if the reading is on a chunk boundary. /// /// Used when we want to read exactly chunk-by-chunk. /// /// Only releveant if we first enabled `stop_on_chunk_boundary()`. pub fn is_on_chunk_boundary(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); rbm.is_on_chunk_boundary() } /// Tell which kind of mode the response body is. pub fn body_mode(&self) -> BodyMode { self.inner.state.reader.as_ref().unwrap().body_mode() } /// Check if the response body has been fully received. pub fn can_proceed(&self) -> bool { self.is_ended() || self.is_close_delimited() } /// Tell if the response is over fn is_ended(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); rbm.is_ended() } /// Tell if we got an end chunk when reading chunked. /// /// A normal chunk ending is: /// /// ```text /// 0\r\n /// \r\n /// ``` /// /// However there are cases where the server abruptly does a `FIN` after sending `0\r\n`. /// This means we still got the entire response body, and could use it, but not reuse the connection. /// /// This returns true as soon as we got the `0\r\n`. pub fn is_ended_chunked(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); rbm.is_ended_chunked() } /// Tell if response body is closed delimited /// /// HTTP/1.0 does not have `content-length` to serialize many requests over the same /// socket. Instead it uses socket close to determine the body is finished. fn is_close_delimited(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); matches!(rbm, BodyReader::CloseDelimited) } /// Proceed to the next state. /// /// Returns `None` if we are not fully received the body. It is guaranteed that if `can_proceed()` /// returns `true`, this will return `Some`. pub fn proceed(self) -> Option { if !self.can_proceed() { return None; } Some(if self.inner.is_redirect() { RecvBodyResult::Redirect(Call::wrap(self.inner)) } else { RecvBodyResult::Cleanup(Call::wrap(self.inner)) }) } } algesten-ureq-proto-4182eed/src/client/recvresp.rs000066400000000000000000000174301511030533500223120ustar00rootroot00000000000000use http::{header, HeaderName, HeaderValue, Response, StatusCode, Version}; use crate::body::BodyReader; use crate::ext::HeaderIterExt; use crate::parser::{try_parse_partial_response, try_parse_response}; use crate::util::log_data; use crate::Error; use super::state::RecvResponse; use super::MAX_RESPONSE_HEADERS; use super::{Call, CloseReason, RecvResponseResult}; impl Call { /// Try reading a response from the input. /// /// * `allow_partial_redirect` - if `true`, we can accept to find the `Location` header /// and proceed without reading the entire header. This is useful for broken servers that /// don't send an entire \r\n at the end of the preamble. /// /// The `(usize, Option)` is `(input amount consumed, response`). /// /// Notice that it's possible that we get an `input amount consumed` despite not returning /// a `Some(Response)`. This can happen if the server returned a 100-continue, and due to /// timing reasons we did not receive it while we were in the `Await100` call state. This /// "spurios" 100 will be discarded before we parse the actual response. pub fn try_response( &mut self, input: &[u8], allow_partial_redirect: bool, ) -> Result<(usize, Option>), Error> { let maybe_response = self.do_try_response(input, allow_partial_redirect)?; let (input_used, response) = match maybe_response { Some(v) => v, // Not enough input for a full response yet None => return Ok((0, None)), }; if response.status() == StatusCode::CONTINUE { // We have received a 100-continue response. This can happen in two scenarios: // 1. A "delayed" 100-continue when we used Expect: 100-continue but the server // did not produce the response in time while we were in the Await100 state. // 2. An unsolicited 100-continue from a server that sends it regardless of // whether the client requested it (e.g., on a regular GET request). // // According to RFC 7231, 100-continue is an informational (1xx) response and // should always be consumed by clients, regardless of whether it was solicited. self.inner.await_100_continue = false; // We consume the response and wait for the actual final response. return Ok((input_used, None)); } self.inner.status = Some(response.status()); // We want the last Location header. self.inner.location = response .headers() .get_all(header::LOCATION) .into_iter() .next_back() .cloned(); if response.headers().iter().has(header::CONNECTION, "close") { self.inner .close_reason .push(CloseReason::ServerConnectionClose); } Ok((input_used, Some(response))) } /// Try reading response headers /// /// A response is only possible once the `input` holds all the HTTP response /// headers. Before that this returns `None`. When the response is succesfully read, /// the return value `(usize, Response<()>)` contains how many bytes were consumed /// of the `input`. fn do_try_response( &mut self, input: &[u8], allow_partial_redirect: bool, ) -> Result)>, Error> { // ~3k for 100 headers let (input_used, response) = match try_parse_response::(input)? { Some(v) => v, None => { // The caller decides whether to allow a partial parse. if !allow_partial_redirect { return Ok(None); } // TODO(martin): I don't like this code. The mission is to be correct HTTP/1.1 // and this is a hack to allow for broken servers. // // As a special case, to handle broken servers that does a redirect without // the final trailing \r\n, we try parsing the response as partial, and // if it is a redirect, we can allow the request to continue. let Some(mut r) = try_parse_partial_response::(input)? else { return Ok(None); }; // A redirection must have a location header. let is_complete_redirection = r.status().is_redirection() && r.headers().contains_key(header::LOCATION); if !is_complete_redirection { return Ok(None); } // Insert a synthetic connection: close, since the connection is // not valid after using a partial request. debug!("Partial redirection response, insert fake connection: close"); r.headers_mut() .insert(header::CONNECTION, HeaderValue::from_static("close")); (input.len(), r) } }; log_data(&input[..input_used]); let http10 = response.version() == Version::HTTP_10; let status = response.status().as_u16(); if status == StatusCode::CONTINUE { // There should be no headers for this response. if !response.headers().is_empty() { return Err(Error::HeadersWith100); } return Ok(Some((input_used, response))); } let header_lookup = |name: HeaderName| { if let Some(header) = response.headers().get(name) { return header.to_str().ok(); } None }; let force_recv = self.inner.force_recv_body; let recv_body_mode = BodyReader::for_response( http10, self.inner.request.method(), status, force_recv, &header_lookup, )?; self.inner.state.reader = Some(recv_body_mode); Ok(Some((input_used, response))) } /// Tell if we have finished receiving the response. pub fn can_proceed(&self) -> bool { self.inner.state.reader.is_some() } /// Tell if response body is closed delimited /// /// HTTP/1.0 does not have `content-length` to serialize many requests over the same /// socket. Instead it uses socket close to determine the body is finished. fn is_close_delimited(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); matches!(rbm, BodyReader::CloseDelimited) } /// Proceed to the next state. /// /// This returns `None` if we have not finished receiving the response. It is guaranteed that if /// `can_proceed()` returns true, this will return `Some`. pub fn proceed(mut self) -> Option { if !self.can_proceed() { return None; } let has_response_body = self.inner.state.need_response_body(); if has_response_body { if self.is_close_delimited() { self.inner .close_reason .push(CloseReason::CloseDelimitedBody); } Some(RecvResponseResult::RecvBody(Call::wrap(self.inner))) } else { Some(if self.inner.is_redirect() { RecvResponseResult::Redirect(Call::wrap(self.inner)) } else { RecvResponseResult::Cleanup(Call::wrap(self.inner)) }) } } /// Convert the state to receive a body despite method. /// /// Methods like HEAD and CONNECT should not have attached bodies. /// Some broken APIs use bodies anyway and this is an escape hatch to /// interoperate with such services. pub fn force_recv_body(&mut self) { self.inner.force_recv_body = true; } } algesten-ureq-proto-4182eed/src/client/redirect.rs000066400000000000000000000104571511030533500222640ustar00rootroot00000000000000use http::uri::Scheme; use http::{header, Method, StatusCode, Uri}; use crate::ext::{MethodExt, StatusExt}; use crate::Error; use super::state::{Cleanup, Prepare, Redirect}; use super::{Call, RedirectAuthHeaders}; impl Call { /// Construct a new `Call` by following the redirect. /// /// There are some rules when following a redirect. /// /// * For 307/308 /// * POST/PUT results in `None`, since we do not allow redirecting a request body /// * DELETE is intentionally excluded: /// * All other methods retain the method in the redirect /// * Other redirect (301, 302, etc) /// * HEAD results in HEAD in the redirect /// * All other methods becomes GET pub fn as_new_call( &mut self, redirect_auth_headers: RedirectAuthHeaders, ) -> Result>, Error> { let header = match &self.inner.location { Some(v) => v, None => return Err(Error::NoLocationHeader), }; let location = match header.to_str() { Ok(v) => v, Err(_) => { return Err(Error::BadLocationHeader( String::from_utf8_lossy(header.as_bytes()).to_string(), )) } }; // Previous request let previous = &mut self.inner.request; // Unwrap is OK, because we can't be here without having read a response. let status = self.inner.status.unwrap(); let method = previous.method(); // A new uri by combining the base from the previous request and the new location. let uri = previous.new_uri_from_location(location)?; // Perform the redirect method differently depending on 3xx code. let new_method = if status.is_redirect_retaining_status() { if method.need_request_body() { // only resend the request if it cannot have a body return Ok(None); } else if method == Method::DELETE { // NOTE: DELETE is intentionally excluded: https://stackoverflow.com/questions/299628 return Ok(None); } else { method.clone() } } else { // this is to follow how curl does it. POST, PUT etc change // to GET on a redirect. if matches!(*method, Method::GET | Method::HEAD) { method.clone() } else { Method::GET } }; let mut request = previous.take_request(); // The calculated redirect method *request.method_mut() = new_method; let keep_auth_header = match redirect_auth_headers { RedirectAuthHeaders::Never => false, RedirectAuthHeaders::SameHost => can_redirect_auth_header(request.uri(), &uri), }; // The redirect URI *request.uri_mut() = uri; // Mutate the original request to remove headers we cannot keep in the redirect. let headers = request.headers_mut(); if !keep_auth_header { headers.remove(header::AUTHORIZATION); } headers.remove(header::COOKIE); headers.remove(header::CONTENT_LENGTH); // Next state let next = Call::new(request)?; Ok(Some(next)) } /// The redirect status code. pub fn status(&self) -> StatusCode { self.inner.status.unwrap() } /// Whether we must close the connection corresponding to the current call. /// /// This is used to inform connection pooling. pub fn must_close_connection(&self) -> bool { self.close_reason().is_some() } /// If we are closing the connection, give a reason why. pub fn close_reason(&self) -> Option<&'static str> { self.inner.close_reason.first().map(|s| s.explain()) } /// Proceed to the cleanup state. pub fn proceed(self) -> Call { Call::wrap(self.inner) } } fn can_redirect_auth_header(prev: &Uri, next: &Uri) -> bool { let host_prev = prev.authority().map(|a| a.host()); let host_next = next.authority().map(|a| a.host()); let scheme_prev = prev.scheme(); let scheme_next = next.scheme(); host_prev == host_next && (scheme_prev == scheme_next || scheme_next == Some(&Scheme::HTTPS)) } algesten-ureq-proto-4182eed/src/client/sendbody.rs000066400000000000000000000073661511030533500222770ustar00rootroot00000000000000use crate::body::calculate_max_input; use crate::util::Writer; use crate::Error; use super::state::{RecvResponse, SendBody}; use super::Call; impl Call { /// Write request body from `input` to `output`. /// /// This is called repeatedly until the entire body has been sent. The output buffer is filled /// as much as possible for each call. /// /// Depending on request headers, the output might be `transfer-encoding: chunked`. Chunking means /// the output is slightly larger than the input due to the extra length headers per chunk. /// When not doing chunked, the input/output will be the same per call. /// /// The result `(usize, usize)` is `(input consumed, output used)`. /// /// **Important** /// /// To indicate that the body is fully sent, you call write with an `input` parameter set to `&[]`. /// This ends the `transfer-encoding: chunked` and ensures the state is correct to proceed. pub fn write(&mut self, input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> { let mut w = Writer::new(output); if !input.is_empty() && self.inner.state.writer.is_ended() { return Err(Error::BodyContentAfterFinish); } if let Some(left) = self.inner.state.writer.left_to_send() { if input.len() as u64 > left { return Err(Error::BodyLargerThanContentLength); } } let input_used = self.inner.state.writer.write(input, &mut w); let output_used = w.len(); Ok((input_used, output_used)) } /// Helper to avoid copying memory. /// /// When the transfer is _NOT_ chunked, `write()` just copies the `input` to the `output`. /// This memcopy might be possible to avoid if the user can use the `input` buffer directly /// against the transport. /// /// This function is used to "report" how much of the input that has been used. It's effectively /// the same as the first `usize` in the pair returned by `write()`. pub fn consume_direct_write(&mut self, amount: usize) -> Result<(), Error> { if let Some(left) = self.inner.state.writer.left_to_send() { if amount as u64 > left { return Err(Error::BodyLargerThanContentLength); } } else { return Err(Error::BodyIsChunked); } self.inner.state.writer.consume_direct_write(amount); Ok(()) } /// Calculate the max amount of input we can transfer to fill the `output_len`. /// /// For chunked transfer, the input is less than the output. pub fn calculate_max_input(&self, output_len: usize) -> usize { // For non-chunked, the entire output can be used. if !self.is_chunked() { return output_len; } calculate_max_input(output_len) } /// Test if call is chunked. /// /// This might need some processing, hence the &mut and pub fn is_chunked(&self) -> bool { self.inner.state.writer.is_chunked() } /// Check whether the request body is fully sent. /// /// For requests with a `content-length` header set, this will only become `true` once the /// number of bytes communicated have been sent. For chunked transfer, this becomes `true` /// after calling `write()` with an input of `&[]`. pub fn can_proceed(&self) -> bool { self.inner.state.writer.is_ended() } /// Proceed to the next state. /// /// Returns `None` if it's not possible to proceed. It's guaranteed that if `can_proceed()` returns /// `true`, this will result in `Some`. pub fn proceed(self) -> Option> { if !self.can_proceed() { return None; } Some(Call::wrap(self.inner)) } } algesten-ureq-proto-4182eed/src/client/sendreq.rs000066400000000000000000000211011511030533500221100ustar00rootroot00000000000000use std::io::Write; use base64::prelude::BASE64_STANDARD; use base64::Engine; use http::uri::Scheme; use http::{header, HeaderMap, HeaderName, HeaderValue, Method, Uri, Version}; use crate::client::amended::AmendedRequest; use crate::ext::{AuthorityExt, MethodExt, SchemeExt}; use crate::util::Writer; use crate::Error; use super::state::SendRequest; use super::{BodyState, Call, RequestPhase, SendRequestResult}; impl Call { /// Write the request to the buffer. /// /// Writes incrementally, it can be called repeatedly in situations where the output /// buffer is small. /// /// This includes the first row, i.e. `GET / HTTP/1.1` and all headers. /// The output buffer needs to be large enough for the longest row. /// /// Example: /// /// ```text /// POST /bar HTTP/1.1\r\n /// Host: my.server.test\r\n /// User-Agent: myspecialthing\r\n /// \r\n /// /// ``` /// /// The buffer would need to be at least 28 bytes big, since the `User-Agent` row is /// 28 bytes long. /// /// If the output is too small for the longest line, the result is an `OutputOverflow` error. /// /// The `Ok(usize)` is the number of bytes of the `output` buffer that was used. pub fn write(&mut self, output: &mut [u8]) -> Result { self.maybe_analyze_request()?; let mut w = Writer::new(output); try_write_prelude(&self.inner.request, &mut self.inner.state, &mut w)?; let output_used = w.len(); Ok(output_used) } /// The configured method. pub fn method(&self) -> &Method { self.inner.request.method() } /// The uri being requested. pub fn uri(&self) -> &Uri { self.inner.request.uri() } /// Version of the request. /// /// This can only be 1.0 or 1.1. pub fn version(&self) -> Version { self.inner.request.version() } /// The configured headers. pub fn headers_map(&mut self) -> Result { self.maybe_analyze_request()?; let mut map = HeaderMap::new(); for (k, v) in self.inner.request.headers() { map.insert(k, v.clone()); } Ok(map) } /// Check whether the entire request has been sent. /// /// This is useful when the output buffer is small and we need to repeatedly /// call `write()` to send the entire request. pub fn can_proceed(&self) -> bool { !self.inner.state.phase.is_prelude() } /// Attempt to proceed from this state to the next. /// /// Returns `None` if the entire request has not been sent. It is guaranteed that if /// `can_proceed()` returns `true`, this will return `Some`. pub fn proceed(mut self) -> Result, Error> { if !self.can_proceed() { return Ok(None); } if self.inner.state.writer.has_body() { if self.inner.await_100_continue { Ok(Some(SendRequestResult::Await100(Call::wrap(self.inner)))) } else { // TODO(martin): is this needed? self.maybe_analyze_request()?; let call = Call::wrap(self.inner); Ok(Some(SendRequestResult::SendBody(call))) } } else { let call = Call::wrap(self.inner); Ok(Some(SendRequestResult::RecvResponse(call))) } } pub(crate) fn maybe_analyze_request(&mut self) -> Result<(), Error> { if self.inner.analyzed { return Ok(()); } let info = self.inner.request.analyze( self.inner.state.writer, self.inner.state.allow_non_standard_methods, )?; let method = self.inner.request.method(); let send_body = (method.allow_request_body() || self.inner.force_send_body) && info.body_mode.has_body(); if !send_body && info.body_mode.has_body() { return Err(Error::BodyNotAllowed); } if !info.req_host_header && method != Method::CONNECT { if let Some(host) = self.inner.request.uri().host() { // User did not set a host header, and there is one in uri, we set that. // We need an owned value to set the host header. // This might append the port if it differs from the scheme default. let value = maybe_with_port(host, self.inner.request.uri())?; self.inner.request.set_header(header::HOST, value)?; } } if let Some(auth) = self.inner.request.uri().authority() { if self.inner.request.method() != Method::CONNECT { if auth.userinfo().is_some() && !info.req_auth_header { let user = auth.username().unwrap_or_default(); let pass = auth.password().unwrap_or_default(); let creds = BASE64_STANDARD.encode(format!("{}:{}", user, pass)); let auth = format!("Basic {}", creds); self.inner.request.set_header(header::AUTHORIZATION, auth)?; } } else if !info.req_host_header { self.inner .request .set_header(header::HOST, auth.clone().as_str())?; } } if !info.req_body_header && info.body_mode.has_body() { // User did not set a body header, we set one. let header = info.body_mode.body_header(); self.inner.request.set_header(header.0, header.1)?; } self.inner.state.writer = info.body_mode; self.inner.analyzed = true; Ok(()) } } fn maybe_with_port(host: &str, uri: &Uri) -> Result { fn from_str(src: &str) -> Result { HeaderValue::from_str(src).map_err(|e| Error::BadHeader(e.to_string())) } if let Some(port) = uri.port() { let scheme = uri.scheme().unwrap_or(&Scheme::HTTP); if let Some(scheme_default) = scheme.default_port() { if port != scheme_default { // This allocates, so we only do it if we absolutely have to. let host_port = format!("{}:{}", host, port); return from_str(&host_port); } } } // Fall back on no port (without allocating). from_str(host) } fn try_write_prelude( request: &AmendedRequest, state: &mut BodyState, w: &mut Writer, ) -> Result<(), Error> { let at_start = w.len(); loop { if try_write_prelude_part(request, state, w) { continue; } let written = w.len() - at_start; if written > 0 || state.phase.is_body() { return Ok(()); } else { return Err(Error::OutputOverflow); } } } fn try_write_prelude_part(request: &AmendedRequest, state: &mut BodyState, w: &mut Writer) -> bool { match &mut state.phase { RequestPhase::Line => { let success = do_write_send_line(request.prelude(), w); if success { state.phase = RequestPhase::Headers(0); } success } RequestPhase::Headers(index) => { let header_count = request.headers_len(); let all = request.headers(); let skipped = all.skip(*index); if header_count > 0 { do_write_headers(skipped, index, header_count - 1, w); } if *index == header_count { state.phase = RequestPhase::Body; } false } // We're past the header. _ => false, } } fn do_write_send_line(line: (&Method, &str, Version), w: &mut Writer) -> bool { // Ensure origin-form request-target starts with "/" when only a query is present // per RFC 9112 §3.2.1 (@https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.1). let need_initial_slash = line.1.starts_with('?'); let slash = if need_initial_slash { "/" } else { "" }; w.try_write(|w| write!(w, "{} {}{} {:?}\r\n", line.0, slash, line.1, line.2)) } fn do_write_headers<'a, I>(headers: I, index: &mut usize, last_index: usize, w: &mut Writer) where I: Iterator, { for h in headers { let success = w.try_write(|w| { write!(w, "{}: ", h.0)?; w.write_all(h.1.as_bytes())?; write!(w, "\r\n")?; if *index == last_index { write!(w, "\r\n")?; } Ok(()) }); if success { *index += 1; } else { break; } } } algesten-ureq-proto-4182eed/src/client/test/000077500000000000000000000000001511030533500210655ustar00rootroot00000000000000algesten-ureq-proto-4182eed/src/client/test/mod.rs000066400000000000000000000005461511030533500222170ustar00rootroot00000000000000mod scenario; mod state_prepare; mod state_send_request; mod state_send_body; mod state_recv_response; mod state_await_100; mod state_recv_body; mod state_redirect; mod state_cleanup; trait TestSliceExt { fn as_str(&self) -> &str; } impl TestSliceExt for [u8] { fn as_str(&self) -> &str { std::str::from_utf8(self).unwrap() } } algesten-ureq-proto-4182eed/src/client/test/scenario.rs000066400000000000000000000242551511030533500232460ustar00rootroot00000000000000use std::io::Write; use std::marker::PhantomData; use http::{Method, Request, Response, StatusCode}; use crate::client::state::{ Await100, Cleanup, Prepare, RecvBody, RecvResponse, Redirect, SendBody, SendRequest, }; use crate::client::{Await100Result, Call, SendRequestResult}; use crate::client::{RecvBodyResult, RecvResponseResult}; pub struct Scenario { request: Request<()>, headers_amend: Vec<(String, String)>, send_body: Vec, response: Response<()>, recv_body: Vec, } impl Scenario { pub fn builder() -> ScenarioBuilder<()> { ScenarioBuilder::new() } } impl Scenario { pub fn to_prepare(&self) -> Call { // The unwraps here are ok because the user is not supposed to // construct tests that test the Scenario builder itself. let mut call = Call::new(self.request.clone()).unwrap(); for (key, value) in &self.headers_amend { call.header(key, value).unwrap(); } call } pub fn to_send_request(&self) -> Call { let call = self.to_prepare(); call.proceed() } pub fn to_send_body(&self) -> Call { let mut call = self.to_send_request(); // Write the prelude and discard call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::SendBody(v))) => v, _ => unreachable!("Incorrect scenario not leading to_send_body()"), } } pub fn to_await_100(&self) -> Call { let mut call = self.to_send_request(); // Write the prelude and discard call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::Await100(v))) => v, _ => unreachable!("Incorrect scenario not leading to_await_100()"), } } pub fn to_recv_response(&self) -> Call { let mut call = self.to_send_request(); // Write the prelude and discard call.write(&mut vec![0; 1024]).unwrap(); if call.inner().state.writer.has_body() { let mut call = if call.inner().await_100_continue { // Go via Await100 let call = match call.proceed() { Ok(Some(SendRequestResult::Await100(v))) => v, _ => unreachable!(), }; // Proceed straight out of Await100 match call.proceed() { Ok(Await100Result::SendBody(v)) => v, _ => unreachable!(), } } else { match call.proceed() { Ok(Some(SendRequestResult::SendBody(v))) => v, _ => unreachable!(), } }; let mut input = &self.send_body[..]; let mut output = vec![0; 1024]; while !input.is_empty() { let (input_used, _) = call.write(input, &mut output).unwrap(); input = &input[input_used..]; } call.write(&[], &mut output).unwrap(); call.proceed().unwrap() } else { match call.proceed() { Ok(Some(SendRequestResult::RecvResponse(v))) => v, _ => unreachable!(), } } } pub fn to_recv_body(&self) -> Call { let mut call = self.to_recv_response(); let input = write_response(&self.response); // use crate::client::test::TestSliceExt; // println!("{:?}", input.as_slice().as_str()); call.try_response(&input, true).unwrap(); match call.proceed() { Some(RecvResponseResult::RecvBody(v)) => v, _ => unreachable!("Incorrect scenario not leading to_recv_body()"), } } pub fn to_redirect(&self) -> Call { let mut call = self.to_recv_response(); let input = write_response(&self.response); call.try_response(&input, true).unwrap(); match call.proceed().unwrap() { RecvResponseResult::Redirect(v) => v, RecvResponseResult::RecvBody(mut state) => { let mut output = vec![0; 1024]; state.read(&self.recv_body, &mut output).unwrap(); match state.proceed() { Some(RecvBodyResult::Redirect(v)) => v, _ => unreachable!("Incorrect scenario not leading to_redirect()"), } } _ => unreachable!("Incorrect scenario not leading to_redirect()"), } } pub fn to_cleanup(&self) -> Call { let mut call = self.to_recv_response(); let input = write_response(&self.response); call.try_response(&input, true).unwrap(); match call.proceed().unwrap() { RecvResponseResult::Redirect(v) => v.proceed(), RecvResponseResult::RecvBody(mut call) => { let mut output = vec![0; 1024]; call.read(&self.recv_body, &mut output).unwrap(); match call.proceed() { Some(RecvBodyResult::Redirect(v)) => v.proceed(), Some(RecvBodyResult::Cleanup(v)) => v, _ => unreachable!("Incorrect scenario not leading to_redirect()"), } } RecvResponseResult::Cleanup(v) => v, } } } pub fn write_response(r: &Response<()>) -> Vec { let mut input = Vec::::new(); let s = r.status(); write!( &mut input, "{:?} {} {}\r\n", r.version(), s.as_u16(), s.canonical_reason().unwrap() ) .unwrap(); for (k, v) in r.headers().iter() { write!(&mut input, "{}: {}\r\n", k.as_str(), v.to_str().unwrap()).unwrap(); } write!(&mut input, "\r\n").unwrap(); input } #[derive(Default)] pub struct ScenarioBuilder { request: Request<()>, headers_amend: Vec<(String, String)>, send_body: Vec, response: Response<()>, recv_body: Vec, _ph: PhantomData, } pub struct WithReq(()); pub struct WithRes(()); #[allow(unused)] impl ScenarioBuilder<()> { pub fn new() -> Self { Default::default() } pub fn request(self, request: Request<()>) -> ScenarioBuilder { ScenarioBuilder { request, headers_amend: self.headers_amend, send_body: vec![], response: Response::default(), recv_body: vec![], _ph: PhantomData, } } pub fn method(self, method: Method, uri: &str) -> ScenarioBuilder { self.request(Request::builder().method(method).uri(uri).body(()).unwrap()) } pub fn get(self, uri: &str) -> ScenarioBuilder { self.request(Request::get(uri).body(()).unwrap()) } pub fn head(self, uri: &str) -> ScenarioBuilder { self.request(Request::head(uri).body(()).unwrap()) } pub fn post(self, uri: &str) -> ScenarioBuilder { self.request(Request::post(uri).body(()).unwrap()) } pub fn put(self, uri: &str) -> ScenarioBuilder { self.request(Request::put(uri).body(()).unwrap()) } pub fn options(self, uri: &str) -> ScenarioBuilder { self.request(Request::options(uri).body(()).unwrap()) } pub fn delete(self, uri: &str) -> ScenarioBuilder { self.request(Request::delete(uri).body(()).unwrap()) } pub fn trace(self, uri: &str) -> ScenarioBuilder { self.request(Request::trace(uri).body(()).unwrap()) } pub fn connect(self, uri: &str) -> ScenarioBuilder { self.request(Request::connect(uri).body(()).unwrap()) } pub fn patch(self, uri: &str) -> ScenarioBuilder { self.request(Request::patch(uri).body(()).unwrap()) } } #[allow(unused)] impl ScenarioBuilder { pub fn header(mut self, key: &'static str, value: impl ToString) -> Self { self.request .headers_mut() .append(key, value.to_string().try_into().unwrap()); self } pub fn send_body>(mut self, body: B, chunked: bool) -> Self { let body = body.as_ref().to_vec(); let len = body.len(); self.send_body = body; let (k, v) = if chunked { ("transfer-encoding".to_string(), "chunked".to_string()) } else { ("content-length".to_string(), len.to_string()) }; self.headers_amend.push((k, v)); self } pub fn redirect(self, status: StatusCode, location: &str) -> ScenarioBuilder { let r = Response::builder() .status(status) .header("location", location) .body(()) .unwrap(); self.response(r) } pub fn response(mut self, response: Response<()>) -> ScenarioBuilder { let ScenarioBuilder { request, headers_amend, send_body, recv_body, .. } = self; ScenarioBuilder { request, headers_amend, send_body, response, recv_body, _ph: PhantomData, } } pub fn build(self) -> Scenario { Scenario { request: self.request, send_body: self.send_body, headers_amend: self.headers_amend, response: self.response, recv_body: self.recv_body, } } } impl ScenarioBuilder { pub fn recv_body>(mut self, body: B, chunked: bool) -> Self { let body = body.as_ref().to_vec(); let len = body.len(); self.recv_body = body; let (k, v) = if chunked { ("transfer-encoding", "chunked".to_string()) } else { ("content-length", len.to_string()) }; self.response.headers_mut().append(k, v.try_into().unwrap()); self } pub fn build(self) -> Scenario { Scenario { request: self.request, send_body: self.send_body, headers_amend: self.headers_amend, response: self.response, recv_body: self.recv_body, } } } algesten-ureq-proto-4182eed/src/client/test/state_await_100.rs000066400000000000000000000062251511030533500243250ustar00rootroot00000000000000use crate::client::Await100Result; use super::scenario::Scenario; #[test] fn proceed_without_100_continue() { let scenario = Scenario::builder() .put("https://q.test") .header("expect", "100-continue") .build(); let call = scenario.to_await_100(); assert!(call.can_keep_await_100()); let inner = call.inner(); assert!(inner.state.writer.has_body()); assert!(inner.close_reason.is_empty()); match call.proceed() { Ok(Await100Result::SendBody(_)) => {} _ => panic!("proceed without 100-continue should go to SendBody"), } } #[test] fn proceed_after_100_continue() { let scenario = Scenario::builder() .put("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_await_100(); let input = b"HTTP/1.1 100 Continue\r\n\r\n"; let n = call.try_read_100(input).unwrap(); assert_eq!(n, 25); assert!(!call.can_keep_await_100()); let inner = call.inner(); assert!(inner.state.writer.has_body()); assert!(inner.close_reason.is_empty()); match call.proceed() { Ok(Await100Result::SendBody(_)) => {} _ => panic!("proceed after 100-continue should go to SendBody"), } } #[test] fn proceed_after_403() { let scenario = Scenario::builder() .put("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_await_100(); let input = b"HTTP/1.1 403 Forbidden\r\n\r\n"; let n = call.try_read_100(input).unwrap(); assert_eq!(n, 0); assert!(!call.can_keep_await_100()); let inner = call.inner(); assert!(!inner.state.writer.has_body()); assert!(!inner.close_reason.is_empty()); match call.proceed() { Ok(Await100Result::RecvResponse(_)) => {} _ => panic!("proceed after 403 should go to RecvResponse"), } } #[test] fn proceed_after_200() { let scenario = Scenario::builder() .put("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_await_100(); let input = b"HTTP/1.1 200 Ok\r\n\r\n"; let n = call.try_read_100(input).unwrap(); assert_eq!(n, 0); assert!(!call.can_keep_await_100()); let inner = call.inner(); assert!(!inner.state.writer.has_body()); assert!(!inner.close_reason.is_empty()); match call.proceed() { Ok(Await100Result::RecvResponse(_)) => {} _ => panic!("proceed after 200 should go to RecvResponse"), } } #[test] fn proceed_after_403_with_headers() { let scenario = Scenario::builder() .put("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_await_100(); let input = b"HTTP/1.1 403 Forbidden\r\nContent-Length: 100\r\n"; let n = call.try_read_100(input).unwrap(); assert_eq!(n, 0); assert!(!call.can_keep_await_100()); let inner = call.inner(); assert!(!inner.state.writer.has_body()); assert!(!inner.close_reason.is_empty()); match call.proceed() { Ok(Await100Result::RecvResponse(_)) => {} _ => panic!("proceed after 403 should go to RecvResponse"), } } algesten-ureq-proto-4182eed/src/client/test/state_cleanup.rs000066400000000000000000000071371511030533500242720ustar00rootroot00000000000000use http::{Request, Response, StatusCode, Version}; use crate::client::test::scenario::write_response; use crate::CloseReason; use super::scenario::Scenario; #[test] fn reuse_without_send_body() { let scenario = Scenario::builder() .get("https://a.test") .response(Response::new(())) .recv_body("hello", false) .build(); let call = scenario.to_cleanup(); assert!(!call.must_close_connection()); } #[test] fn reuse_with_send_body() { let scenario = Scenario::builder() .post("https://a.test") .send_body("hi", false) .response(Response::new(())) .recv_body("hello", false) .build(); let call = scenario.to_cleanup(); assert!(!call.must_close_connection()); } #[test] fn reuse_without_recv_body() { let scenario = Scenario::builder() .head("https://a.test") .response(Response::new(())) .build(); let call = scenario.to_cleanup(); assert!(!call.must_close_connection()); } #[test] fn reuse_after_redirect() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "https://b.test") .build(); let call = scenario.to_cleanup(); assert!(!call.must_close_connection()); } #[test] fn close_due_to_http10() { let scenario = Scenario::builder() .request( Request::get("https://a.test") .version(Version::HTTP_10) .body(()) .unwrap(), ) .build(); let call = scenario.to_cleanup(); let inner = call.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::CloseDelimitedBody ); assert!(call.must_close_connection()); } #[test] fn close_due_to_client_connection_close() { let scenario = Scenario::builder() .get("https://a.test") .header("connection", "close") .build(); let call = scenario.to_cleanup(); let inner = call.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::ClientConnectionClose ); assert!(call.must_close_connection()); } #[test] fn close_due_to_server_connection_close() { let scenario = Scenario::builder() .get("https://a.test") .response( Response::builder() .header("connection", "close") .body(()) .unwrap(), ) .build(); let call = scenario.to_cleanup(); let inner = call.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::ServerConnectionClose ); assert!(call.must_close_connection()); } #[test] fn close_due_to_not_100_continue() { let scenario = Scenario::builder() .post("https://q.test") .header("expect", "100-continue") .send_body("hi", false) .build(); let mut call = scenario.to_await_100(); let input = write_response( &Response::builder() .status(StatusCode::FORBIDDEN) .body(()) .unwrap(), ); call.try_read_100(&input).unwrap(); let inner = call.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::Not100Continue ); } #[test] fn close_due_to_close_delimited_body() { // no content-length or transfer-encoding let scenario = Scenario::builder().get("https://a.test").build(); let call = scenario.to_cleanup(); let inner = call.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::CloseDelimitedBody ); assert!(call.must_close_connection()); } algesten-ureq-proto-4182eed/src/client/test/state_prepare.rs000066400000000000000000000014731511030533500242760ustar00rootroot00000000000000use super::scenario::Scenario; #[test] fn proceed_without_amended_headers() { let scenario = Scenario::builder().get("https://q.test").build(); let call = scenario.to_prepare(); let inner = call.inner(); let request = &inner.request; assert_eq!(request.headers_vec(), []); call.proceed(); } #[test] fn proceed_with_amended_headers() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_prepare(); call.header("Cookie", "name=bar").unwrap(); call.header("Cookie", "name2=baz").unwrap(); let inner = call.inner(); let request = &inner.request; assert_eq!( request.headers_vec(), [ // ("cookie", "name=bar"), ("cookie", "name2=baz"), ] ); call.proceed(); } algesten-ureq-proto-4182eed/src/client/test/state_recv_body.rs000066400000000000000000000106611511030533500246130ustar00rootroot00000000000000use http::Response; use crate::client::test::TestSliceExt; use crate::CloseReason; use super::scenario::Scenario; #[test] fn recv_body_close_delimited() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_recv_body(); let mut output = vec![0; 1024]; assert!(call.can_proceed()); let (input_used, output_used) = call.read(b"hello", &mut output).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 5); let inner = call.inner(); let reason = *inner.close_reason.first().unwrap(); assert_eq!(reason, CloseReason::CloseDelimitedBody); assert_eq!(output[..output_used].as_str(), "hello"); assert!(call.can_proceed()); } #[test] fn recv_body_chunked_partial() { let scenario = Scenario::builder() .get("https://q.test") .response( Response::builder() .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build(); let mut call = scenario.to_recv_body(); let mut output = vec![0; 1024]; let (input_used, output_used) = call.read(b"5\r", &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 0); assert!(!call.can_proceed()); let (input_used, output_used) = call.read(b"5\r\nhel", &mut output).unwrap(); assert_eq!(input_used, 6); assert_eq!(output_used, 3); assert!(!call.can_proceed()); let (input_used, output_used) = call.read(b"lo", &mut output).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 2); assert!(!call.can_proceed()); let (input_used, output_used) = call.read(b"\r\n", &mut output).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 0); assert!(!call.can_proceed()); let (input_used, output_used) = call.read(b"0\r\n\r\n", &mut output).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 0); assert!(call.can_proceed()); } #[test] fn recv_body_chunked_full() { let scenario = Scenario::builder() .get("https://q.test") .response( Response::builder() .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build(); let mut call = scenario.to_recv_body(); let mut output = vec![0; 1024]; // this is the default // call.stop_on_chunk_boundary(false); let (input_used, output_used) = call.read(b"5\r\nhello\r\n0\r\n\r\n", &mut output).unwrap(); assert_eq!(input_used, 15); assert_eq!(output_used, 5); assert_eq!(output[..output_used].as_str(), "hello"); assert!(call.can_proceed()); } #[test] fn recv_body_chunked_stop_boundary() { let scenario = Scenario::builder() .get("https://q.test") .response( Response::builder() .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build(); let mut call = scenario.to_recv_body(); let mut output = vec![0; 1024]; call.stop_on_chunk_boundary(true); // chunk reading starts on boundary. assert!(call.is_on_chunk_boundary()); let (input_used, output_used) = call.read(b"5\r\nhello\r\n0\r\n\r\n", &mut output).unwrap(); assert_eq!(input_used, 10); assert_eq!(output_used, 5); assert_eq!(output[..output_used].as_str(), "hello"); // chunk reading stops on chunk boundary. assert!(call.is_on_chunk_boundary()); let (input_used, output_used) = call.read(b"0\r\n\r\n", &mut output).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 0); assert!(call.can_proceed()); } #[test] fn recv_body_content_length() { let scenario = Scenario::builder() .get("https://q.test") .response( Response::builder() .header("content-length", "5") .body(()) .unwrap(), ) .build(); let mut call = scenario.to_recv_body(); let mut output = vec![0; 1024]; let (input_used, output_used) = call.read(b"hel", &mut output).unwrap(); assert_eq!(input_used, 3); assert_eq!(output_used, 3); assert_eq!(output[..output_used].as_str(), "hel"); assert!(!call.can_proceed()); let (input_used, output_used) = call.read(b"lo", &mut output).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 2); assert_eq!(output[..output_used].as_str(), "lo"); assert!(call.can_proceed()); } algesten-ureq-proto-4182eed/src/client/test/state_recv_response.rs000066400000000000000000000102431511030533500255100ustar00rootroot00000000000000use http::{header, StatusCode, Version}; use crate::client::test::scenario::Scenario; use crate::ext::HeaderIterExt; // This is a complete response. const RESPONSE: &[u8] = b"\ HTTP/1.1 200 OK\r\n\ Content-Length: 123\r\n\ Content-Type: text/plain\r\n\ \r\n"; #[test] fn receive_incomplete_response() { // -1 to never reach the end for i in 14..RESPONSE.len() - 1 { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_recv_response(); let (input_used, maybe_response) = call.try_response(&RESPONSE[..i], true).unwrap(); assert_eq!(input_used, 0); assert!(maybe_response.is_none()); assert!(!call.can_proceed()); } } #[test] fn receive_complete_response() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_recv_response(); let (input_used, maybe_response) = call.try_response(RESPONSE, true).unwrap(); assert_eq!(input_used, 66); assert!(maybe_response.is_some()); let response = maybe_response.unwrap(); assert_eq!(response.version(), Version::HTTP_11); assert_eq!(response.status(), StatusCode::OK); assert_eq!( response.headers().get(header::CONTENT_LENGTH).unwrap(), "123" ); assert!(response .headers() .iter() .has(header::CONTENT_TYPE, "text/plain")); assert!(call.can_proceed()); } #[test] fn prepended_100_continue() { // In the case of expect-100-continue, there's a chance the 100-continue // arrives after we started sending the request body, in which case // we receive it before the actual response. let scenario = Scenario::builder() .post("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_recv_response(); // incomplete 100-continue should be ignored. let (input_used, maybe_response) = call .try_response(b"HTTP/1.1 100 Continue\r\n", true) .unwrap(); assert_eq!(input_used, 0); assert!(maybe_response.is_none()); assert!(!call.can_proceed()); // complete 100-continue should be consumed without producing a request let (input_used, maybe_response) = call .try_response(b"HTTP/1.1 100 Continue\r\n\r\n", true) .unwrap(); assert_eq!(input_used, 25); assert!(maybe_response.is_none()); assert!(!call.can_proceed()); // full response after prepended 100-continue let (input_used, maybe_response) = call.try_response(RESPONSE, true).unwrap(); assert_eq!(input_used, 66); assert!(maybe_response.is_some()); assert!(call.can_proceed()); } #[test] fn expect_100_without_100_continue() { // In the case of expect-100-continue let scenario = Scenario::builder() .post("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_recv_response(); // full response and no 100-continue let (input_used, maybe_response) = call.try_response(RESPONSE, true).unwrap(); assert_eq!(input_used, 66); assert!(maybe_response.is_some()); assert!(call.can_proceed()); } #[test] fn unsolicited_100_continue_on_get() { // Bug: server sends 100-continue on a regular GET request // without the client sending Expect: 100-continue header. // This reproduces the issue from https://github.com/algesten/ureq/issues/1137 let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_recv_response(); // Server sends unsolicited 100-continue let (input_used, maybe_response) = call .try_response(b"HTTP/1.1 100 Continue\r\n\r\n", true) .unwrap(); assert_eq!(input_used, 25); assert!( maybe_response.is_none(), "100-continue should be consumed, not returned" ); assert!(!call.can_proceed()); // Server then sends the actual response let (input_used, maybe_response) = call.try_response(RESPONSE, true).unwrap(); assert_eq!(input_used, 66); assert!(maybe_response.is_some()); assert!(call.can_proceed()); let response = maybe_response.unwrap(); assert_eq!(response.status(), StatusCode::OK); } algesten-ureq-proto-4182eed/src/client/test/state_redirect.rs000066400000000000000000000264431511030533500244450ustar00rootroot00000000000000use http::{header, Method, Response, StatusCode}; use crate::client::test::TestSliceExt; use crate::client::RedirectAuthHeaders; use crate::Error; use super::scenario::Scenario; #[test] fn without_recv_body() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "https://b.test") .build(); scenario.to_redirect(); } #[test] fn with_recv_body() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "https://b.test") .recv_body(b"hi there", false) .build(); scenario.to_redirect(); } #[test] #[should_panic] fn with_recv_body_0() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "https://b.test") .recv_body(b"", false) .build(); scenario.to_recv_body(); } #[test] fn absolute_url() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "https://b.test") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://b.test/"); } #[test] fn relative_url_absolute_path() { let scenario = Scenario::builder() .get("https://a.test") .redirect(StatusCode::FOUND, "/foo.html") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://a.test/foo.html"); } #[test] fn relative_url_relative_path() { let scenario = Scenario::builder() .get("https://a.test/x/foo.html") .redirect(StatusCode::FOUND, "y/bar.html") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://a.test/x/y/bar.html"); } #[test] fn relative_url_parent_relative() { let scenario = Scenario::builder() .get("https://a.test/x/foo.html") .redirect(StatusCode::FOUND, "../bar.html") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://a.test/bar.html"); } #[test] fn relative_url_dot_relative() { let scenario = Scenario::builder() .get("https://a.test/x/foo.html") .redirect(StatusCode::FOUND, "./bar.html") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://a.test/x/bar.html"); } #[test] fn relative_url_dot_dotdot_relative() { let scenario = Scenario::builder() .get("https://a.test/x/foo.html") .redirect(StatusCode::FOUND, "./../bar.html") .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://a.test/bar.html"); } #[test] fn relative_url_parent_overflow_relative() { let scenario = Scenario::builder() .get("https://a.test/x/foo.html") .redirect(StatusCode::FOUND, "../../bar.html") .build(); let error = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap_err(); assert_eq!( error, Error::BadLocationHeader("../../bar.html".to_string()) ); } #[test] fn last_location_header() { let scenario = Scenario::builder() .get("https://a.test") .response( Response::builder() .status(StatusCode::MOVED_PERMANENTLY) .header("location", "https://b.test") .header("location", "https://c.test") .header("location", "https://d.test") .header("location", "https://e.test") .body(()) .unwrap(), ) .build(); let call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); assert_eq!(&call.uri().to_string(), "https://e.test/"); } #[test] fn change_redirect_methods() { const METHOD_CHANGES: &[(StatusCode, &[(Method, Option)])] = &[ ( StatusCode::FOUND, &[ (Method::GET, Some(Method::GET)), (Method::HEAD, Some(Method::HEAD)), (Method::POST, Some(Method::GET)), (Method::PUT, Some(Method::GET)), (Method::PATCH, Some(Method::GET)), (Method::DELETE, Some(Method::GET)), (Method::OPTIONS, Some(Method::GET)), (Method::CONNECT, Some(Method::GET)), (Method::TRACE, Some(Method::GET)), ], ), ( StatusCode::MOVED_PERMANENTLY, &[ (Method::GET, Some(Method::GET)), (Method::HEAD, Some(Method::HEAD)), (Method::POST, Some(Method::GET)), (Method::PUT, Some(Method::GET)), (Method::PATCH, Some(Method::GET)), (Method::DELETE, Some(Method::GET)), (Method::OPTIONS, Some(Method::GET)), (Method::CONNECT, Some(Method::GET)), (Method::TRACE, Some(Method::GET)), ], ), ( StatusCode::TEMPORARY_REDIRECT, &[ (Method::GET, Some(Method::GET)), (Method::HEAD, Some(Method::HEAD)), (Method::POST, None), (Method::PUT, None), (Method::PATCH, None), (Method::DELETE, None), (Method::OPTIONS, Some(Method::OPTIONS)), (Method::CONNECT, Some(Method::CONNECT)), (Method::TRACE, Some(Method::TRACE)), ], ), ( StatusCode::PERMANENT_REDIRECT, &[ (Method::GET, Some(Method::GET)), (Method::HEAD, Some(Method::HEAD)), (Method::POST, None), (Method::PUT, None), (Method::PATCH, None), (Method::DELETE, None), (Method::OPTIONS, Some(Method::OPTIONS)), (Method::CONNECT, Some(Method::CONNECT)), (Method::TRACE, Some(Method::TRACE)), ], ), ]; for (status, methods) in METHOD_CHANGES { for (method_from, method_to) in methods.iter() { let scenario = Scenario::builder() .method(method_from.clone(), "https://a.test") .redirect(*status, "https://b.test") .build(); let maybe_state = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap(); if let Some(state) = maybe_state { let inner = state.inner(); let method = inner.request.method(); assert_eq!( method, method_to.clone().unwrap(), "{} {} -> {:?}", status, method_from, method_to ); } else { assert!(method_to.is_none()); } } } } #[test] fn keep_auth_header_never() { let scenario = Scenario::builder() .get("https://a.test/foo") .header("authorization", "some secret") .redirect(StatusCode::FOUND, "https://a.test/bar") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap() .proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET /bar HTTP/1.1\r\n\ host: a.test\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } #[test] fn keep_auth_header_same_host() { let scenario = Scenario::builder() .get("https://a.test:123/foo") .header("authorization", "some secret") .redirect(StatusCode::FOUND, "https://a.test:234/bar") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::SameHost) .unwrap() .unwrap() .proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET /bar HTTP/1.1\r\n\ host: a.test:234\r\n\ authorization: some secret\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } #[test] fn dont_keep_auth_header_different_host() { let scenario = Scenario::builder() .get("https://a.test/foo") .header("authorization", "some secret") .redirect(StatusCode::FOUND, "https://b.test/bar") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::SameHost) .unwrap() .unwrap() .proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET /bar HTTP/1.1\r\n\ host: b.test\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } #[test] fn dont_keep_cookie_header() { let scenario = Scenario::builder() .get("https://a.test/foo") .header("x-my", "ya") .header("cookie", "secret=value") .redirect(StatusCode::FOUND, "https://b.test/bar") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::SameHost) .unwrap() .unwrap() .proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET /bar HTTP/1.1\r\n\ host: b.test\r\n\ x-my: ya\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } #[test] fn dont_keep_content_length() { let scenario = Scenario::builder() .post("https://a.test/foo") .header("x-my", "ya") .send_body("123", false) .redirect(StatusCode::FOUND, "https://b.test/bar") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::SameHost) .unwrap() .unwrap() .proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET /bar HTTP/1.1\r\n\ host: b.test\r\n\ x-my: ya\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } #[test] fn can_set_cookie_on_redirected_request() { let scenario = Scenario::builder() .get("https://a.test") .header("cookie", "not redirected") .redirect(StatusCode::FOUND, "https://b.test") .build(); let mut call = scenario .to_redirect() .as_new_call(RedirectAuthHeaders::Never) .unwrap() .unwrap(); call.header(header::COOKIE, "hello").unwrap(); let mut call = call.proceed(); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); let cmp = "\ GET / HTTP/1.1\r\n\ cookie: hello\r\n\ host: b.test\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); } algesten-ureq-proto-4182eed/src/client/test/state_send_body.rs000066400000000000000000000077111511030533500246070ustar00rootroot00000000000000use crate::client::SendRequestResult; use super::scenario::Scenario; use super::TestSliceExt; #[test] fn write_with_content_length() { let input = b"hello".as_slice(); let scenario = Scenario::builder() .post("https://q.test") .header("content-length", input.len()) .build(); let mut call = scenario.to_send_body(); // deliberately short buffer to require multiple writes let mut output = vec![0; 3]; let overhead = call.calculate_max_input(output.len()); assert_eq!(overhead, 3); // not chunked. entire output can be used as input. assert!(!call.can_proceed()); // 1st write let (input_used, output_used) = call.write(input, &mut output).unwrap(); assert_eq!(input_used, 3); assert_eq!(output_used, 3); assert_eq!(output[..output_used].as_str(), "hel"); let input = &input[input_used..]; assert!(!call.can_proceed()); // 2nd write let (input_used, output_used) = call.write(input, &mut output).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 2); assert_eq!(output[..output_used].as_str(), "lo"); assert!(call.can_proceed()); } #[test] fn write_with_content_length_empty_slices() { let input = b"hello".as_slice(); let scenario = Scenario::builder() .post("https://q.test") .header("content-length", input.len()) .build(); let mut call = scenario.to_send_body(); let mut output = vec![0; 1024]; // Useless write let (input_used, output_used) = call.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 0); assert!(!call.can_proceed()); // Proper write let (input_used, output_used) = call.write(input, &mut output).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 5); assert!(call.can_proceed()); // More useless writes let (input_used, output_used) = call.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 0); let (input_used, output_used) = call.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 0); let (input_used, output_used) = call.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 0); assert!(call.can_proceed()); } #[test] fn write_with_chunked() { let input = b"hello".as_slice(); let scenario = Scenario::builder().post("https://q.test").build(); let mut call = scenario.to_send_body(); let mut output = vec![0; 21 * 1024 + 74]; let overhead = call.calculate_max_input(output.len()); assert_eq!(overhead, 21554); assert!(!call.can_proceed()); // 1st write let (input_used, output_used) = call.write(&input[..3], &mut output).unwrap(); assert_eq!(input_used, 3); assert_eq!(output_used, 8); assert_eq!(output[..output_used].as_str(), "3\r\nhel\r\n"); assert!(!call.can_proceed()); // 2nd write let (input_used, output_used) = call.write(&input[3..], &mut output).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 7); assert_eq!(output[..output_used].as_str(), "2\r\nlo\r\n"); assert!(!call.can_proceed()); // write end let (input_used, output_used) = call.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert_eq!(output_used, 5); assert_eq!(output[..output_used].as_str(), "0\r\n\r\n"); assert!(call.can_proceed()); } #[test] fn send_body_despite_method() { let scenario = Scenario::builder() .delete("https://q.test") .send_body("DELETE should not have a body", true) .build(); let mut call = scenario.to_prepare(); call.force_send_body(); let mut call = call.proceed(); // Write the prelude and discard call.write(&mut vec![0; 1024]).unwrap(); let result = call.proceed().unwrap().unwrap(); // We should be able to get to this state without errors. assert!(matches!(result, SendRequestResult::SendBody(_))); } algesten-ureq-proto-4182eed/src/client/test/state_send_request.rs000066400000000000000000000051631511030533500253410ustar00rootroot00000000000000use crate::client::SendRequestResult; use crate::Error; use super::scenario::Scenario; use super::TestSliceExt; #[test] fn write_request() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_send_request(); assert!(!call.can_proceed()); let mut o = vec![0; 1024]; let n = call.write(&mut o).unwrap(); assert_eq!(n, 32); let cmp = "\ GET / HTTP/1.1\r\n\ host: q.test\r\n\ \r\n"; assert_eq!(o[..n].as_str(), cmp); assert!(call.can_proceed()); } #[test] fn short_buffer() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_send_request(); assert!(!call.can_proceed()); // Buffer too short to hold entire request let mut output = vec![0; 10]; let r = call.write(&mut output); assert_eq!(r.unwrap_err(), Error::OutputOverflow); assert!(!call.can_proceed()); } #[test] fn proceed_without_body() { let scenario = Scenario::builder().get("https://q.test").build(); let mut call = scenario.to_send_request(); call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::RecvResponse(_))) => {} _ => panic!("Mehod without body should result in RecvResponse"), } } #[test] fn proceed_with_body() { let scenario = Scenario::builder().post("https://q.test").build(); let mut call = scenario.to_send_request(); call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::SendBody(_))) => {} _ => panic!("Method with body should result in SendBody"), } } #[test] fn proceed_with_await_100() { let scenario = Scenario::builder() .post("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_send_request(); call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::Await100(_))) => {} _ => panic!("Method with body and Expect: 100-continue should result in Await100"), } } #[test] fn proceed_without_body_and_expect_100() { let scenario = Scenario::builder() // GET should not result in Await100 since // there is no body to send. .get("https://q.test") .header("expect", "100-continue") .build(); let mut call = scenario.to_send_request(); call.write(&mut vec![0; 1024]).unwrap(); match call.proceed() { Ok(Some(SendRequestResult::RecvResponse(_))) => {} _ => panic!("Method without body and Expect: 100-continue should result in RecvResponse"), } } algesten-ureq-proto-4182eed/src/close_reason.rs000066400000000000000000000026371511030533500216620ustar00rootroot00000000000000/// Reasons for closing an HTTP connection. /// /// This enum represents the various reasons why an HTTP connection might need /// to be closed after a request/response cycle is complete. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CloseReason { /// Client sent `connection: close`. ClientConnectionClose, /// Server sent `connection: close`. ServerConnectionClose, /// When doing expect-100 the server sent _some other response_. /// /// For expect-100, the only options for a server response are: /// /// * 100 continue, in which case we continue to send the body. /// * do nothing, in which case we continue to send the body after a timeout. /// * a 4xx or 5xx response indicating the server cannot receive the body. Not100Continue, /// Response body is close delimited. /// /// We do not know how much body data to receive. The socket will be closed /// when it's done. This is HTTP/1.0 semantics. CloseDelimitedBody, } impl CloseReason { pub(crate) fn explain(&self) -> &'static str { match self { CloseReason::ClientConnectionClose => "client sent Connection: close", CloseReason::ServerConnectionClose => "server sent Connection: close", CloseReason::Not100Continue => "non-100 response before body", CloseReason::CloseDelimitedBody => "response body is close delimited", } } } algesten-ureq-proto-4182eed/src/error.rs000066400000000000000000000410431511030533500203310ustar00rootroot00000000000000use std::fmt; use http::{Method, StatusCode, Version}; /// Error type for ureq-proto #[derive(Debug, PartialEq, Eq)] #[allow(missing_docs)] #[non_exhaustive] pub enum Error { BadHeader(String), UnsupportedVersion, MethodVersionMismatch(Method, Version), TooManyHostHeaders, TooManyContentLengthHeaders, BadHostHeader, BadAuthorizationHeader, BadContentLengthHeader, OutputOverflow, ChunkLenNotAscii, ChunkLenNotANumber, ChunkExpectedCrLf, BodyContentAfterFinish, BodyLargerThanContentLength, BodyNotAllowed, HttpParseFail(String), HttpParseTooManyHeaders, NoLocationHeader, BadLocationHeader(String), HeadersWith100, BodyIsChunked, BadReject100Status(StatusCode), } impl From for Error { fn from(value: httparse::Error) -> Self { Error::HttpParseFail(value.to_string()) } } impl std::error::Error for Error {} impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::BadHeader(v) => write!(f, "bad header: {}", v), Error::UnsupportedVersion => write!(f, "unsupported http version"), Error::MethodVersionMismatch(m, v) => { write!(f, "{} not valid for HTTP version {:?}", m, v) } Error::TooManyHostHeaders => write!(f, "more than one host header"), Error::TooManyContentLengthHeaders => write!(f, "more than one content-length header"), Error::BadHostHeader => write!(f, "host header is not a string"), Error::BadAuthorizationHeader => write!(f, "authorization header is not a string"), Error::BadContentLengthHeader => write!(f, "content-length header not a number"), Error::OutputOverflow => write!(f, "output too small to write output"), Error::ChunkLenNotAscii => write!(f, "chunk length is not ascii"), Error::ChunkLenNotANumber => write!(f, "chunk length cannot be read as a number"), Error::ChunkExpectedCrLf => write!(f, "chunk expected crlf as next character"), Error::BodyContentAfterFinish => { write!(f, "attempt to stream body after sending finish (&[])") } Error::BodyLargerThanContentLength => { write!(f, "attempt to write larger body than content-length") } Error::BodyNotAllowed => { write!(f, "body not allowed by method") } Error::HttpParseFail(v) => write!(f, "http parse fail: {}", v), Error::HttpParseTooManyHeaders => write!(f, "http parse resulted in too many headers"), Error::NoLocationHeader => write!(f, "missing a location header"), Error::BadLocationHeader(v) => write!(f, "location header is malformed: {}", v), Error::HeadersWith100 => write!(f, "received headers with 100-continue response"), Error::BodyIsChunked => write!(f, "body is chunked"), Error::BadReject100Status(v) => { write!(f, "expect-100 must be rejected with 4xx or 5xx: {}", v) } } } } #[cfg(test)] #[cfg(feature = "client")] mod tests_client { use super::*; use crate::client::{ state::{RecvResponse, Redirect, SendBody, SendRequest}, Call, RecvResponseResult, RedirectAuthHeaders, SendRequestResult, }; use http::{HeaderValue, Method, Request, Version}; // Helper methods to reduce test verbosity /// Creates a Call object from a request and proceeds to the initial state fn setup_call(req: Request<()>) -> (Call, Vec) { let call = Call::new(req).unwrap(); let call = call.proceed(); let output = vec![0; 1024]; (call, output) } /// Creates a Call object and proceeds to the RecvResponse state fn setup_recv_response_call() -> (Call, Vec) { let req = Request::get("http://example.com").body(()).unwrap(); let (mut call, mut output) = setup_call(req); // Write the request headers call.write(&mut output).unwrap(); // Proceed to RecvResponse let call = call.proceed().unwrap().unwrap(); let SendRequestResult::RecvResponse(call) = call else { panic!("Expected SendRequestResult::RecvResponse"); }; (call, output) } /// Creates a Call object and proceeds to the SendBody state fn setup_send_body_call(req: Request<()>) -> (Call, Vec, usize) { let (mut call, mut output) = setup_call(req); // Write the request headers let n = call.write(&mut output).unwrap(); // Proceed to SendBody let call = call.proceed().unwrap().unwrap(); let SendRequestResult::SendBody(call) = call else { panic!("Expected SendBody"); }; (call, output, n) } /// Creates a Call object and proceeds to the Redirect state with the given response fn setup_redirect_call(response: &[u8]) -> Result<(Call, Vec), Error> { let (mut call, output) = setup_recv_response_call(); // Try to parse the response let (_, _) = call.try_response(response, false)?; // Proceed to Redirect state let RecvResponseResult::Redirect(call) = call.proceed().unwrap() else { panic!("Expected RecvResponseResult::Redirect"); }; Ok((call, output)) } // BadHeader #[test] fn test_bad_header() { // Create a request let req = Request::get("http://example.com").body(()).unwrap(); let mut call = Call::new(req).unwrap(); // Try to set an invalid header using the Call API let result = call.header("Invalid\0Header", "value"); // Verify that it returns a BadHeader error assert!(result.is_err()); let err = result.unwrap_err(); assert!(matches!(err, Error::BadHeader(_))); } // UnsupportedVersion #[test] fn test_unsupported_version() { // Create a request with HTTP/2.0 which is not supported by the Call API let req = Request::builder() .uri("http://example.com") .version(Version::HTTP_2) .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); assert!(matches!(err, Error::UnsupportedVersion)); } // MethodVersionMismatch #[test] fn test_method_version_mismatch() { // TRACE method is not valid for HTTP/1.0 let m = Method::from_bytes(b"TRACE").unwrap(); let req = Request::builder() .method(m.clone()) .uri("http://example.com") .version(Version::HTTP_10) .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); assert!(matches!(err, Error::MethodVersionMismatch(_, _))); if let Error::MethodVersionMismatch(method, version) = err { assert_eq!(method, m); assert_eq!(version, Version::HTTP_10); } } // TooManyHostHeaders #[test] fn test_too_many_host_headers() { // Create a request with two Host headers let req = Request::builder() .uri("http://example.com") .header("Host", "example.com") .header("Host", "another-example.com") .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); // Verify that it returns a TooManyHostHeaders error assert!(matches!(err, Error::TooManyHostHeaders)); } // TooManyContentLengthHeaders #[test] fn test_too_many_content_length_headers() { // Create a request with two Content-Length headers let req = Request::builder() .uri("http://example.com") .header("Content-Length", "10") .header("Content-Length", "20") .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); // Verify that it returns a TooManyContentLengthHeaders error assert!(matches!(err, Error::TooManyContentLengthHeaders)); } // BadHostHeader #[test] fn test_bad_host_header() { // Create a request with an invalid Host header value (non-UTF8 bytes) let req = Request::builder() .uri("http://example.com") .header("Host", HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap()) .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); // Verify that it returns a BadHostHeader error assert!(matches!(err, Error::BadHostHeader)); } // BadAuthorizationHeader #[test] fn test_bad_authorization_header() { // Create a request with an invalid Authorization header value (non-UTF8 bytes) let req = Request::builder() .uri("http://example.com") .header( "Authorization", HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap(), ) .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); // Verify that it returns a BadAuthorizationHeader error assert!(matches!(err, Error::BadAuthorizationHeader)); } // BadContentLengthHeader #[test] fn test_bad_content_length_header() { // Create a request with an invalid Content-Length header value (not a number) let req = Request::builder() .uri("http://example.com") .header("Content-Length", "not-a-number") .body(()) .unwrap(); let (mut call, mut output) = setup_call(req); // Try to write the request headers let err = call.write(&mut output).unwrap_err(); // Verify that it returns a BadContentLengthHeader error assert!(matches!(err, Error::BadContentLengthHeader)); } // OutputOverflow #[test] fn test_output_overflow() { // Create a request with a long header let req = Request::builder() .uri("http://example.com") .header("x-long-header", "a".repeat(100)) .body(()) .unwrap(); let (mut call, _) = setup_call(req); // Try to write to a very small output buffer let mut tiny_output = vec![0; 10]; let err = call.write(&mut tiny_output).unwrap_err(); assert!(matches!(err, Error::OutputOverflow)); } /// Tests a chunked encoding error with the given chunk data fn test_chunk_error(chunk_data: &[u8]) -> Error { let (mut call, mut output) = setup_recv_response_call(); const RES_PREFIX: &[u8] = b"HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n"; call.try_response(RES_PREFIX, false).unwrap(); let RecvResponseResult::RecvBody(mut call) = call.proceed().unwrap() else { panic!("Expected RecvResponseResult::RecvBody"); }; call.read(chunk_data, &mut output).unwrap_err() } // ChunkLenNotAscii #[test] fn test_chunk_len_not_ascii() { let err = test_chunk_error(b"\xFF\r\ndata\r\n"); assert!(matches!(err, Error::ChunkLenNotAscii)); } // ChunkLenNotANumber #[test] fn test_chunk_len_not_a_number() { let err = test_chunk_error(b"xyz\r\ndata\r\n"); assert!(matches!(err, Error::ChunkLenNotANumber)); } // ChunkExpectedCrLf #[test] fn test_chunk_expected_crlf() { let err = test_chunk_error(b"5abcdefghijabcdefghijabcdefghij\r\n"); assert!(matches!(err, Error::ChunkExpectedCrLf)); } // BodyContentAfterFinish #[test] fn test_body_content_after_finish() { // Create a POST request let req = Request::post("http://example.com").body(()).unwrap(); let (mut call, mut output, n) = setup_send_body_call(req); // Write some data let (_, n2) = call.write(b"data", &mut output[n..]).unwrap(); // Finish the body let (_, n3) = call.write(&[], &mut output[n + n2..]).unwrap(); // Try to write more data after finishing let err = call .write(b"more data", &mut output[n + n2 + n3..]) .unwrap_err(); assert!(matches!(err, Error::BodyContentAfterFinish)); } // BodyLargerThanContentLength #[test] fn test_body_larger_than_content_length() { // Create a request with a content-length header let req = Request::post("http://example.com") .header("content-length", "5") .body(()) .unwrap(); let (mut call, mut output, n) = setup_send_body_call(req); // Try to write more data than specified in content-length let err = call.write(b"too much data", &mut output[n..]).unwrap_err(); assert!(matches!(err, Error::BodyLargerThanContentLength)); } // HttpParseFail #[test] fn test_http_parse_fail() { let (mut call, _) = setup_recv_response_call(); // Invalid HTTP response (missing space after HTTP/1.1) const RES: &[u8] = b"HTTP/1.1200 OK\r\n\r\n"; let err = call.try_response(RES, false).unwrap_err(); assert!(matches!(err, Error::HttpParseFail(_))); } // HttpParseTooManyHeaders #[test] fn test_http_parse_too_many_headers() { let (mut call, _) = setup_recv_response_call(); // Create a response with many headers (more than the parser can handle) let mut res = String::from("HTTP/1.1 200 OK\r\n"); for i in 0..1000 { res.push_str(&format!("X-Header-{}: value\r\n", i)); } res.push_str("\r\n"); let err = call.try_response(res.as_bytes(), false).unwrap_err(); assert!(matches!(err, Error::HttpParseTooManyHeaders)); } // NoLocationHeader #[test] fn test_no_location_header() { // Redirect response without a Location header const RES: &[u8] = b"HTTP/1.1 302 Found\r\n\ \r\n"; let (mut call, _) = setup_redirect_call(RES).unwrap(); // Try to create a new Call, which should fail due to missing Location header let err = call.as_new_call(RedirectAuthHeaders::Never).unwrap_err(); assert!(matches!(err, Error::NoLocationHeader)); } // BadLocationHeader #[test] fn test_bad_location_header() { // Redirect response with a malformed Location header const RES: &[u8] = b"HTTP/1.1 302 Found\r\n\ Location: \xFF\r\n\ \r\n"; let (mut call, _) = setup_redirect_call(RES).unwrap(); // Try to create a new Call, which should fail due to malformed Location header let err = call.as_new_call(RedirectAuthHeaders::Never).unwrap_err(); assert!(matches!(err, Error::BadLocationHeader(_))); } // HeadersWith100 #[test] fn test_headers_with_100() { let (mut call, _) = setup_recv_response_call(); // 100 Continue response with headers const RES: &[u8] = b"HTTP/1.1 100 Continue\r\n\ Content-Type: text/plain\r\n\ \r\n"; let err = call.try_response(RES, false).unwrap_err(); assert!(matches!(err, Error::HeadersWith100)); } // BodyIsChunked #[test] fn test_body_is_chunked() { // Create a request with chunked transfer encoding let req = Request::post("http://example.com") .header("transfer-encoding", "chunked") .body(()) .unwrap(); let (mut call, _, _) = setup_send_body_call(req); // Try to use consume_direct_write which doesn't work with chunked encoding let err = call.consume_direct_write(5).unwrap_err(); assert!(matches!(err, Error::BodyIsChunked)); } // Test the From implementation #[test] fn test_from_httparse_error() { let httparse_error = httparse::Error::HeaderName; let error: Error = httparse_error.into(); assert!(matches!(error, Error::HttpParseFail(_))); let Error::HttpParseFail(_) = error else { panic!("Not Error::HttpParseFail"); }; } } algesten-ureq-proto-4182eed/src/ext.rs000066400000000000000000000103471511030533500200030ustar00rootroot00000000000000use http::{header, HeaderName, HeaderValue, Method, StatusCode}; #[cfg(feature = "server")] pub(crate) trait StatusCodeExt { /// Check if the status code requires a body according to HTTP spec. /// /// According to the HTTP specification, the following status codes must not include a message body: /// - 1xx (Informational): 100, 101, etc. /// - 204 (No Content) /// - 304 (Not Modified) /// /// All other status codes can include a message body. fn body_allowed(&self) -> bool; } #[cfg(feature = "server")] impl StatusCodeExt for StatusCode { fn body_allowed(&self) -> bool { !self.is_informational() && *self != StatusCode::NO_CONTENT && *self != StatusCode::NOT_MODIFIED } } pub(crate) trait MethodExt { #[cfg(feature = "client")] fn is_http10(&self) -> bool; #[cfg(feature = "client")] fn is_http11(&self) -> bool; fn need_request_body(&self) -> bool; fn allow_request_body(&self) -> bool; #[cfg(feature = "client")] fn verify_version(&self, version: http::Version) -> Result<(), crate::Error>; } impl MethodExt for Method { #[cfg(feature = "client")] fn is_http10(&self) -> bool { self == Method::GET || self == Method::HEAD || self == Method::POST } #[cfg(feature = "client")] fn is_http11(&self) -> bool { self == Method::PUT || self == Method::DELETE || self == Method::CONNECT || self == Method::OPTIONS || self == Method::TRACE || self == Method::PATCH } fn need_request_body(&self) -> bool { self == Method::POST || self == Method::PUT || self == Method::PATCH } fn allow_request_body(&self) -> bool { self != Method::HEAD && self != Method::CONNECT } #[cfg(feature = "client")] fn verify_version(&self, v: http::Version) -> Result<(), crate::Error> { use crate::Error; use http::Version; if v != Version::HTTP_10 && v != Version::HTTP_11 { return Err(Error::UnsupportedVersion); } let method_ok = self.is_http10() || v == Version::HTTP_11 && self.is_http11(); if !method_ok { return Err(Error::MethodVersionMismatch(self.clone(), v)); } Ok(()) } } pub(crate) trait HeaderIterExt { fn has(self, key: HeaderName, value: &str) -> bool; fn has_expect_100(self) -> bool; } impl<'a, I: Iterator> HeaderIterExt for I { fn has(self, key: HeaderName, value: &str) -> bool { self.filter(|i| i.0 == key).any(|i| i.1 == value) } fn has_expect_100(self) -> bool { self.has(header::EXPECT, "100-continue") } } #[cfg(feature = "client")] pub(crate) trait StatusExt { /// Detect 307/308 redirect fn is_redirect_retaining_status(&self) -> bool; } #[cfg(feature = "client")] impl StatusExt for StatusCode { fn is_redirect_retaining_status(&self) -> bool { *self == StatusCode::TEMPORARY_REDIRECT || *self == StatusCode::PERMANENT_REDIRECT } } #[cfg(feature = "client")] pub trait SchemeExt { fn default_port(&self) -> Option; } #[cfg(feature = "client")] impl SchemeExt for http::uri::Scheme { fn default_port(&self) -> Option { use http::uri::Scheme; if *self == Scheme::HTTPS { Some(443) } else if *self == Scheme::HTTP { Some(80) } else { debug!("Unknown scheme: {}", self); None } } } #[cfg(feature = "client")] pub(crate) trait AuthorityExt { fn userinfo(&self) -> Option<&str>; fn username(&self) -> Option<&str>; fn password(&self) -> Option<&str>; } // NB: Treating &str with direct indexes is OK, since Uri parsed the Authority, // and ensured it's all ASCII (or %-encoded). #[cfg(feature = "client")] impl AuthorityExt for http::uri::Authority { fn userinfo(&self) -> Option<&str> { let s = self.as_str(); s.rfind('@').map(|i| &s[..i]) } fn username(&self) -> Option<&str> { self.userinfo() .map(|a| a.rfind(':').map(|i| &a[..i]).unwrap_or(a)) } fn password(&self) -> Option<&str> { self.userinfo() .and_then(|a| a.rfind(':').map(|i| &a[i + 1..])) } } algesten-ureq-proto-4182eed/src/lib.rs000066400000000000000000000024301511030533500177430ustar00rootroot00000000000000//! Supporting crate for [ureq](https://crates.io/crates/ureq). //! //! This crate contains types used to implement ureq. //! //! # In scope: //! //! * First class HTTP/1.1 protocol implementation //! * Indication of connection states (such as when a connection must be closed) //! * transfer-encoding: chunked //! * 100-continue handling //! //! # Out of scope: //! //! * Opening/closing sockets //! * TLS (https) //! * Request routing //! * Body data transformations (charset, compression etc) //! //! # The http crate //! //! Based on the [http crate](https://crates.io/crates/http) - a unified HTTP API for Rust. #![forbid(unsafe_code)] #![warn(clippy::all)] #![allow(clippy::uninlined_format_args)] #![deny(missing_docs)] // I don't think elided lifetimes help in understanding the code. #![allow(clippy::needless_lifetimes)] #[macro_use] extern crate log; // Re-export the basis for this library. pub use http; mod error; pub use error::Error; mod chunk; mod ext; mod util; mod body; pub use body::BodyMode; #[cfg(feature = "client")] pub mod client; #[cfg(feature = "server")] pub mod server; mod close_reason; pub use close_reason::CloseReason; /// Low level HTTP parser /// /// This is to bridge `httparse` crate to `http` crate. pub mod parser; #[doc(hidden)] pub use util::ArrayVec; algesten-ureq-proto-4182eed/src/parser.rs000066400000000000000000000155141511030533500205000ustar00rootroot00000000000000use http::{Method, Request, Response, StatusCode, Version}; use httparse::Status; use crate::Error; /// Parse bytes into a complete response. /// /// Complete means that the last HTTP header is followed by an `\r\n`. /// /// If the result is `None`, the bytes did not contain a full response. That /// typically means you need to read more bytes and append to the in input buffer /// before trying again. /// /// The first `usize` in the resulting pair, is the number of bytes required from /// the input buffer to form the response. /// /// The const `N` is the number of headers to max expect. If the input has more /// headers than `N` you get an error [`Error::HttpParseTooManyHeaders`]. pub fn try_parse_response( input: &[u8], ) -> Result)>, Error> { let mut headers = [httparse::EMPTY_HEADER; N]; // 100 headers ~3kb let mut res = httparse::Response::new(&mut headers); let maybe_input_used = match res.parse(input) { Ok(v) => v, Err(e) => { return Err(if e == httparse::Error::TooManyHeaders { // For expect-100 we use this value to detect that the server // sent a regular response instead of a 100-continue. Error::HttpParseTooManyHeaders } else { e.into() }); } }; let input_used = match maybe_input_used { Status::Complete(v) => v, Status::Partial => return Ok(None), }; let version = { // unwrap: Looking at the impl of parse(), version cannot be None. let v = res.version.unwrap_or(1); match v { 0 => Version::HTTP_10, 1 => Version::HTTP_11, _ => return Err(Error::UnsupportedVersion), } }; let status = { // unwrap: Looking at the impl of parse(), code cannot be None. let v = res.code.unwrap_or(000); // unwrap: Looking at the impl of parse(), the status is 3 digits and cant fail. StatusCode::from_u16(v).unwrap() }; let mut builder = Response::builder().version(version).status(status); for h in res.headers { builder = builder.header(h.name, h.value); } let response = builder.body(()).expect("a valid response"); Ok(Some((input_used, response))) } /// Try parsing as much as possible of a response. /// /// To get a result we need at least the complete initial status row, /// but we don't need complete headers. /// /// The const `N` is the number of headers to max expect. If the input has more /// headers than `N` you get an error [`Error::HttpParseTooManyHeaders`]. pub fn try_parse_partial_response( input: &[u8], ) -> Result>, Error> { let mut headers = vec![httparse::EMPTY_HEADER; N]; // 100 headers ~3kb let mut res = httparse::Response::new(&mut headers); match res.parse(input) { Ok(_) => {} Err(e) => { return Err(if e == httparse::Error::TooManyHeaders { // For expect-100 we use this value to detect that the server // sent a regular response instead of a 100-continue. Error::HttpParseTooManyHeaders } else { e.into() }); } }; let version = { match res.version { Some(0) => Version::HTTP_10, Some(1) => Version::HTTP_11, _ => return Ok(None), } }; let status = { let v = match res.code { Some(v) => v, None => return Ok(None), }; // unwrap: Looking at the impl of parse(), the status is 3 digits and cant fail. StatusCode::from_u16(v).unwrap_or_default() }; let mut builder = Response::builder().version(version).status(status); for h in res.headers { if h.name.is_empty() || h.value.is_empty() { break; } builder = builder.header(h.name, h.value); } let response = builder.body(()).expect("a valid response"); Ok(Some(response)) } /// Parse bytes into a complete request. /// /// Complete means that the last HTTP header is followed by an `\r\n`. /// /// If the result is `None`, the bytes did not contain a full request. That /// typically means you need to read more bytes and append to the in input buffer /// before trying again. /// /// The first `usize` in the resulting pair, is the number of bytes required from /// the input buffer to form the request. /// /// The const `N` is the number of headers to max expect. If the input has more /// headers than `N` you get an error [`Error::HttpParseTooManyHeaders`]. pub fn try_parse_request( input: &[u8], ) -> Result)>, Error> { let mut headers = [httparse::EMPTY_HEADER; N]; // 100 headers ~3kb let mut req = httparse::Request::new(&mut headers); let maybe_input_used = match req.parse(input) { Ok(v) => v, Err(e) => { return Err(if e == httparse::Error::TooManyHeaders { // For expect-100 we use this value to detect that the server // sent a regular response instead of a 100-continue. Error::HttpParseTooManyHeaders } else { e.into() }); } }; let input_used = match maybe_input_used { Status::Complete(v) => v, Status::Partial => return Ok(None), }; let version = { // unwrap: Looking at the impl of parse(), version cannot be None. let v = req.version.unwrap_or(1); match v { 0 => Version::HTTP_10, 1 => Version::HTTP_11, _ => return Err(Error::UnsupportedVersion), } }; let method = { // unwrap: Looking at the impl of parse(), method cannot be None. let v = req.method.unwrap_or("GET"); // unwrap: Looking at the impl of parse(), method will be something. Method::from_bytes(v.as_bytes()).unwrap_or_default() }; let uri = req.path.unwrap_or("/"); let mut builder = Request::builder().uri(uri).version(version).method(method); for h in req.headers { builder = builder.header(h.name, h.value); } let request = builder .body(()) .map_err(|e| Error::HttpParseFail(e.to_string()))?; Ok(Some((input_used, request))) } #[cfg(test)] mod test { use crate::parser::{try_parse_request, try_parse_response}; #[test] fn ensure_no_half_response() { let bytes = "HTTP/1.1 200 OK\r\n\ Content-Type: text/plain\r\n\ Content-Length: 100\r\n\r\n"; try_parse_response::<0>(bytes.as_bytes()).expect_err("too many headers"); } #[test] fn error_on_invalid_authority() { let bytes = "GET example\".com HTTP/1.1\r\n\r\n"; try_parse_request::<0>(bytes.as_bytes()).expect_err("invalid URI character"); } } algesten-ureq-proto-4182eed/src/server/000077500000000000000000000000001511030533500201365ustar00rootroot00000000000000algesten-ureq-proto-4182eed/src/server/amended.rs000066400000000000000000000073651511030533500221140ustar00rootroot00000000000000use std::fmt; use http::{header, HeaderName, HeaderValue, Response, StatusCode, Version}; use crate::body::BodyWriter; use crate::util::compare_lowercase_ascii; use crate::Error; pub(crate) struct AmendedResponse { response: Response<()>, headers: Vec<(HeaderName, HeaderValue)>, } impl AmendedResponse { pub fn new(response: Response<()>) -> Self { AmendedResponse { response, headers: vec![], } } pub fn prelude(&self) -> (Version, StatusCode) { let r = &self.response; (r.version(), r.status()) } pub fn set_header(&mut self, name: K, value: V) -> Result<(), Error> where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { let name = >::try_from(name) .map_err(Into::into) .map_err(|e| Error::BadHeader(e.to_string()))?; let value = >::try_from(value) .map_err(Into::into) .map_err(|e| Error::BadHeader(e.to_string()))?; self.headers.push((name, value)); Ok(()) } pub fn headers(&self) -> impl Iterator { self.headers .iter() .map(|v| (&v.0, &v.1)) .chain(self.response.headers().iter()) } fn headers_get_all(&self, key: HeaderName) -> impl Iterator { self.headers() .filter(move |(k, _)| *k == key) .map(|(_, v)| v) } fn headers_get(&self, key: HeaderName) -> Option<&HeaderValue> { self.headers_get_all(key).next() } pub fn headers_len(&self) -> usize { self.headers().count() } pub fn analyze(&self, wanted_mode: BodyWriter) -> Result { let count_len = self.headers_get_all(header::CONTENT_LENGTH).count(); if count_len > 1 { return Err(Error::TooManyContentLengthHeaders); } let mut content_length: Option = None; if let Some(h) = self.headers_get(header::CONTENT_LENGTH) { let n = h .to_str() .ok() .and_then(|s| s.parse::().ok()) .ok_or(Error::BadContentLengthHeader)?; content_length = Some(n); } let has_chunked = self .headers_get_all(header::TRANSFER_ENCODING) .filter_map(|v| v.to_str().ok()) .any(|v| compare_lowercase_ascii(v, "chunked")); let mut res_body_header = false; // https://datatracker.ietf.org/doc/html/rfc2616#section-4.4 // Messages MUST NOT include both a Content-Length header field and a // non-identity transfer-coding. If the message does include a non- // identity transfer-coding, the Content-Length MUST be ignored. let body_mode = if has_chunked { // chunked "wins" res_body_header = true; BodyWriter::new_chunked() } else if let Some(n) = content_length { // user provided content-length second res_body_header = true; BodyWriter::new_sized(n) } else { wanted_mode }; Ok(ResponseInfo { body_mode, res_body_header, }) } } pub(crate) struct ResponseInfo { pub body_mode: BodyWriter, pub res_body_header: bool, } impl fmt::Debug for AmendedResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AmendedResponse") .field("status", &self.response.status()) .field("headers", &self.headers) .finish() } } algesten-ureq-proto-4182eed/src/server/mod.rs000066400000000000000000001034001511030533500212610ustar00rootroot00000000000000//! HTTP/1.1 server protocol //! //! Sans-IO protocol impl, which means "writing" and "reading" are made via buffers //! rather than the Write/Read std traits. //! //! The [`Reply`] object attempts to encode correct HTTP/1.1 handling using //! state variables, for example `Reply` to represent the //! lifecycle stage where we are to receive a request. //! //! The states are: //! //! * **RecvRequest** - Receive the request, which is the method, path, //! version and the request headers //! * **Send100** - If there is an `Expect: 100-continue` header, the //! server should send a 100 Continue response before receiving the body //! * **RecvBody** - Receive the request body //! * **ProvideResponse** - Prepare a response to the request //! * **SendResponse** - Send the response status and headers //! * **SendBody** - Send the response body //! * **Cleanup** - Close the connection or prepare for the next request //! //! ```text //! ┌──────────────────┐ //! ┌──│ RecvRequest │───────────────┐ //! │ └──────────────────┘ │ //! │ │ │ //! │ │ │ //! │ ▼ ▼ //! │ ┌──────────────────┐ ┌──────────────────┐ //! │ │ RecvBody │◀────│ Send100 │ //! │ └──────────────────┘ └──────────────────┘ //! │ │ │ //! │ │ │ //! │ ▼ │ //! └─▶┌──────────────────┐ │ //! │ ProvideResponse │ reject //! └──────────────────┘ │ //! │ │ //! │ │ //! ▼ │ //! ┌──────────────────┐◀──────────────┘ //! │ SendResponse │──┐ //! └──────────────────┘ │ //! │ │ //! │ │ //! ▼ │ //! ┌──────────────────┐ │ //! │ SendBody │ │ //! └──────────────────┘ │ //! │ │ //! │ │ //! ▼ │ //! ┌──────────────────┐ │ //! │ Cleanup │◀─┘ //! └──────────────────┘ //! ``` //! //! # Example //! //! ``` //! use ureq_proto::server::*; //! use http::{Response, StatusCode, Version}; //! //! // ********************************** RecvRequest //! //! // Create a new Reply in the RecvRequest state //! let mut reply = Reply::new().unwrap(); //! //! // Receive a request from the client //! let input = b"POST /my-path HTTP/1.1\r\n\ //! host: example.test\r\n\ //! transfer-encoding: chunked\r\n\ //! expect: 100-continue\r\n\ //! \r\n"; //! let (input_used, request) = reply.try_request(input).unwrap(); //! //! assert_eq!(input_used, 96); //! let request = request.unwrap(); //! assert_eq!(request.uri().path(), "/my-path"); //! assert_eq!(request.method(), "POST"); //! //! // Check if we can proceed to the next state //! // In a real server, you would implement this method //! // let can_proceed = reply.can_proceed(); //! //! // Proceed to the next state //! let reply = reply.proceed().unwrap(); //! //! // ********************************** Send100 //! //! // In this example, we know the next state is Send100 because //! // the request included an "Expect: 100-continue" header. //! // A real server needs to match on the variants. //! let reply = match reply { //! RecvRequestResult::Send100(v) => v, //! _ => panic!(), //! }; //! //! // We can either accept or reject the 100-continue request //! // Here we accept it and proceed to receiving the body //! let mut output = vec![0_u8; 1024]; //! let (output_used, reply) = reply.accept(&mut output).unwrap(); //! //! assert_eq!(output_used, 25); //! assert_eq!(&output[..output_used], b"HTTP/1.1 100 Continue\r\n\r\n"); //! //! // ********************************** RecvBody //! //! // Now we can receive the request body //! let mut reply = reply; //! //! // Receive the body in chunks (chunked encoding format) //! let input = b"5\r\nhello\r\n0\r\n\r\n"; //! let mut body_buffer = vec![0_u8; 1024]; //! let (input_used, output_used) = reply.read(input, &mut body_buffer).unwrap(); //! //! assert_eq!(input_used, 15); //! assert_eq!(output_used, 5); //! assert_eq!(&body_buffer[..output_used], b"hello"); //! //! // Check if the body is fully received //! // In this example, we'll assume it is //! assert!(reply.is_ended()); //! //! // Proceed to providing a response //! let reply = reply.proceed().unwrap(); //! //! // ********************************** ProvideResponse //! //! // Create a response //! let response = Response::builder() //! .status(StatusCode::OK) //! .header("content-type", "text/plain") //! .body(()) //! .unwrap(); //! //! // Provide the response and proceed to sending it //! let mut reply = reply.provide(response).unwrap(); //! //! // ********************************** SendResponse //! //! // Send the response headers //! let output_used = reply.write(&mut output).unwrap(); //! //! assert_eq!(&output[..output_used], b"\ //! HTTP/1.1 200 OK\r\n\ //! transfer-encoding: chunked\r\n\ //! content-type: text/plain\r\n\ //! \r\n"); //! //! // Check if the response headers are fully sent //! assert!(reply.is_finished()); //! //! // Proceed to sending the response body //! let SendResponseResult::SendBody(mut reply) = reply.proceed() else { //! panic!("Expected SendBody"); //! }; //! //! // ********************************** SendBody //! //! // Send the response body //! let (input_used, output_used) = reply.write(b"hello world", &mut output).unwrap(); //! //! assert_eq!(input_used, 11); //! assert_eq!(&output[..output_used], b"b\r\nhello world\r\n"); //! //! // Indicate the end of the body with an empty input //! let (input_used, output_used) = reply.write(&[], &mut output).unwrap(); //! //! assert_eq!(input_used, 0); //! assert_eq!(&output[..output_used], b"0\r\n\r\n"); //! //! // Check if the body is fully sent //! assert!(reply.is_finished()); //! //! // ********************************** Cleanup //! //! // Proceed to cleanup //! let reply = reply.proceed(); //! //! // Check if we need to close the connection //! if reply.must_close_connection() { //! // connection.close(); //! } else { //! // Prepare for the next request //! // let new_reply = Reply::new().unwrap(); //! } //! ``` use std::fmt; use std::io::Write; use std::marker::PhantomData; use amended::AmendedResponse; use http::{Method, Response, StatusCode, Version}; use crate::body::{BodyReader, BodyWriter}; use crate::ext::{MethodExt, StatusCodeExt}; use crate::util::Writer; use crate::{ArrayVec, CloseReason}; mod amended; #[cfg(test)] mod test; /// Maximum number of headers to parse from an HTTP request. /// /// This constant defines the upper limit on the number of headers that can be /// parsed from an incoming HTTP request. Requests with more headers than this /// will be rejected. pub const MAX_REQUEST_HEADERS: usize = 128; /// A state machine for an HTTP request/response cycle. /// /// This type represents a state machine that transitions through various /// states during the lifecycle of an HTTP request/response. /// /// The type parameters are: /// - `State`: The current state of the state machine (e.g., `RecvRequest`, `SendResponse`, etc.) /// - `B`: The type of the response body (defaults to `()`) /// /// See the [state graph][crate::server] in the server module documentation for a /// visual representation of the state transitions. pub struct Reply { inner: Inner, _ph: PhantomData, } // pub(crate) for tests to inspect state #[derive(Debug)] pub(crate) struct Inner { pub phase: ResponsePhase, pub state: BodyState, pub response: Option, pub close_reason: ArrayVec, pub force_recv_body: bool, pub force_send_body: bool, pub method: Option, pub expect_100: bool, pub expect_100_reject: bool, } #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum ResponsePhase { Status, Headers(usize), Body, } impl ResponsePhase { fn is_prelude(&self) -> bool { matches!(self, ResponsePhase::Status | ResponsePhase::Headers(_)) } fn is_body(&self) -> bool { matches!(self, ResponsePhase::Body) } } #[derive(Debug, Default)] pub(crate) struct BodyState { reader: Option, writer: Option, stop_on_chunk_boundary: bool, } #[doc(hidden)] pub mod state { pub(crate) trait Named { fn name() -> &'static str; } macro_rules! reply_state { ($n:tt) => { #[doc(hidden)] pub struct $n(()); impl Named for $n { fn name() -> &'static str { stringify!($n) } } }; } reply_state!(RecvRequest); reply_state!(Send100); reply_state!(RecvBody); reply_state!(ProvideResponse); reply_state!(SendResponse); reply_state!(SendBody); reply_state!(Cleanup); } use self::state::*; impl Reply { fn wrap(inner: Inner) -> Reply where S: Named, { let wrapped = Reply { inner, _ph: PhantomData, }; debug!("{:?}", wrapped); wrapped } #[cfg(test)] pub(crate) fn inner(&self) -> &Inner { &self.inner } } // //////////////////////////////////////////////////////////////////////////////////////////// RECV REQUEST mod recvreq; /// The possible states after receiving a request. /// /// See [state graph][crate::server] pub enum RecvRequestResult { /// Client is expecting a 100-continue response. Send100(Reply), /// Receive a request body. RecvBody(Reply), /// Client did not send a body. ProvideResponse(Reply), } // //////////////////////////////////////////////////////////////////////////////////////////// SEND 100 mod send100; /// Internal function to append a response to an existing inner state. /// /// This function is used when transitioning from a state that has received a request /// to a state that will send a response. fn append_request(inner: Inner, response: Response<()>) -> Inner { // unwrap is ok because method is set early. let method_allows_body = inner.method.as_ref().unwrap().allow_request_body(); let status_allows_body = response.status().body_allowed(); let default_body_mode = if method_allows_body && status_allows_body { BodyWriter::new_chunked() } else { BodyWriter::new_none() }; Inner { phase: inner.phase, state: BodyState { writer: Some(default_body_mode), ..inner.state }, response: Some(AmendedResponse::new(response)), force_recv_body: inner.force_recv_body, force_send_body: inner.force_send_body, close_reason: inner.close_reason, method: inner.method, expect_100: inner.expect_100, expect_100_reject: inner.expect_100_reject, } } /// Internal function to write a status line to a writer. /// /// This function is used when sending a response status line. fn do_write_send_line(line: (Version, StatusCode), w: &mut Writer, end_head: bool) -> bool { w.try_write(|w| { write!( w, "{:?} {} {}\r\n{}", line.0, line.1.as_str(), line.1.canonical_reason().unwrap_or("Unknown"), if end_head { "\r\n" } else { "" } ) }) } // //////////////////////////////////////////////////////////////////////////////////////////// RECV BODY mod provres; // //////////////////////////////////////////////////////////////////////////////////////////// RECV BODY mod recvbody; // //////////////////////////////////////////////////////////////////////////////////////////// SEND RESPONSE mod sendres; /// The possible states after sending a response. /// /// After sending the response headers, the reply can transition to one of two states: /// - `SendBody`: If the response has a body that needs to be sent /// - `Cleanup`: If the response has no body (e.g., HEAD requests, 204 responses) /// /// See the [state graph][crate::server] for a visual representation. pub enum SendResponseResult { /// Send the response body. SendBody(Reply), /// Proceed directly to cleanup without sending a body. Cleanup(Reply), } // //////////////////////////////////////////////////////////////////////////////////////////// SEND RESPONSE mod sendbody; // //////////////////////////////////////////////////////////////////////////////////////////// CLEANUP impl Reply { /// Tell if we must close the connection. pub fn must_close_connection(&self) -> bool { self.close_reason().is_some() } /// If we are closing the connection, give a reason. pub fn close_reason(&self) -> Option<&'static str> { self.inner.close_reason.first().map(|s| s.explain()) } } // //////////////////////////////////////////////////////////////////////////////////////////// impl fmt::Debug for Reply { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Reply<{}>", State::name()) } } impl fmt::Debug for ResponsePhase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ResponsePhase::Status => write!(f, "SendStatus"), ResponsePhase::Headers(_) => write!(f, "SendHeaders"), ResponsePhase::Body => write!(f, "SendBody"), } } } #[cfg(test)] mod tests { use super::*; use http::{Response, StatusCode}; use std::str; #[test] fn get_simple() { // Create a new Reply in RecvRequest state let mut reply = Reply::new().unwrap(); // Simulate receiving a GET request let input = b"GET /page HTTP/1.1\r\n\ host: test.local\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 40); assert_eq!(request.method(), "GET"); assert_eq!(request.uri().path(), "/page"); // Since GET has no body, we should go straight to ProvideResponse let reply = reply.proceed().unwrap(); let RecvRequestResult::ProvideResponse(reply) = reply else { panic!("Expected ProvideResponse state"); }; // Create and provide a response let response = Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain") .body(()) .unwrap(); let mut reply = reply.provide(response).unwrap(); // Write response headers let mut output = vec![0_u8; 1024]; let n = reply.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!( s, "HTTP/1.1 200 OK\r\n\ transfer-encoding: chunked\r\n\ content-type: text/plain\r\n\ \r\n" ); } #[test] fn post_with_100_continue() { // Create a new Reply let mut reply = Reply::new().unwrap(); // Receive POST request with Expect: 100-continue let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ expect: 100-continue\r\n\ transfer-encoding: chunked\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 93); // Verify exact bytes consumed assert_eq!(request.method(), "POST"); assert_eq!(request.uri().path(), "/upload"); assert_eq!(request.headers().get("expect").unwrap(), "100-continue"); // Proceed to Send100 state and handle the state transition let reply = reply.proceed().unwrap(); let reply = match reply { RecvRequestResult::Send100(r) => r, _ => panic!("Expected Send100 state"), }; // Accept the 100-continue request // Accept the 100-continue request let mut output = vec![0_u8; 1024]; let (n, reply) = reply.accept(&mut output).unwrap(); assert_eq!(&output[..n], b"HTTP/1.1 100 Continue\r\n\r\n"); // Receive chunked body let mut reply = reply; let mut body_buf = vec![0_u8; 1024]; // First chunk let input = b"5\r\nhello\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 10); assert_eq!(&body_buf[..output_used], b"hello"); // Final chunk let input = b"0\r\n\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf[5..]).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 0); assert!(reply.is_ended()); } #[test] fn post_with_content_length() { let mut reply = Reply::new().unwrap(); // Receive POST request with Content-Length let input = b"POST /data HTTP/1.1\r\n\ host: test.local\r\n\ content-length: 11\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 61); // Verify exact bytes consumed assert_eq!(request.method(), "POST"); assert_eq!(request.uri().path(), "/data"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Receive fixed-length body let mut body_buf = vec![0_u8; 1024]; let input = b"Hello World"; let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 11); assert_eq!(&body_buf[..output_used], b"Hello World"); assert!(reply.is_ended()); } #[test] fn head_response_with_body_fails() { let mut reply = Reply::new().unwrap(); // Receive HEAD request let input = b"HEAD /status HTTP/1.1\r\n\ host: test.local\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 43); // Verify exact bytes consumed assert_eq!(request.method(), "HEAD"); assert_eq!(request.uri().path(), "/status"); // Go to ProvideResponse let reply = reply.proceed().unwrap(); let RecvRequestResult::ProvideResponse(reply) = reply else { panic!("Expected ProvideResponse state"); }; // Provide a response let response = Response::builder() .status(StatusCode::OK) .header("content-length", "1000") // Even with content-length .body(()) .unwrap(); reply .provide(response) .expect_err("no body allowed on HEAD response"); } #[test] fn head_response_with_body_and_footgun() { let mut reply = Reply::new().unwrap(); // Receive HEAD request let input = b"HEAD /status HTTP/1.1\r\n\ host: test.local\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 43); // Verify exact bytes consumed assert_eq!(request.method(), "HEAD"); assert_eq!(request.uri().path(), "/status"); // Go to ProvideResponse let reply = reply.proceed().unwrap(); let RecvRequestResult::ProvideResponse(mut reply) = reply else { panic!("Expected ProvideResponse state"); }; // Provide a response let response = Response::builder() .status(StatusCode::OK) .header("content-length", "1000") // Even with content-length .body(()) .unwrap(); reply.force_send_body(); let mut reply = reply.provide(response).unwrap(); // Write response headers let mut output = vec![0_u8; 1024]; let n = reply.write(&mut output).unwrap(); // For HEAD requests, we send headers but no body let s = str::from_utf8(&output[..n]).unwrap(); assert!(s.contains("content-length: 1000")); assert!(!s.contains("transfer-encoding")); } #[test] fn post_streaming() { let mut reply = Reply::new().unwrap(); // Receive streaming POST request let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ transfer-encoding: chunked\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 71); assert_eq!(request.method(), "POST"); assert_eq!(request.uri().path(), "/upload"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Receive first chunk let mut body_buf = vec![0_u8; 1024]; let input = b"5\r\nhello\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 10); assert_eq!(output_used, 5); assert_eq!(&body_buf[..output_used], b"hello"); // Receive final chunk let input = b"0\r\n\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf[5..]).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 0); assert!(reply.is_ended()); } #[test] fn post_small_input() { let mut reply = Reply::new().unwrap(); // Receive POST request headers in small chunks let input1 = b"POST /upload"; let (used1, req1) = reply.try_request(input1).unwrap(); assert_eq!(used1, 0); assert!(req1.is_none()); let input2 = b"POST /upload HTTP/1.1\r\n"; let (used2, req2) = reply.try_request(input2).unwrap(); assert_eq!(used2, 0); assert!(req2.is_none()); let input3 = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n"; let (used3, req3) = reply.try_request(input3).unwrap(); assert_eq!(used3, 0); assert!(req3.is_none()); let input4 = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ \r\n"; let (used4, req4) = reply.try_request(input4).unwrap(); assert_eq!(used4, 43); let request = req4.unwrap(); assert_eq!(request.method(), "POST"); assert_eq!(request.uri().path(), "/upload"); } #[test] fn post_with_short_content_length() { let mut reply = Reply::new().unwrap(); // Receive POST request with short content-length let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ content-length: 2\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 62); assert_eq!(request.method(), "POST"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Try to receive more data than content-length let mut body_buf = vec![0_u8; 1024]; let input = b"hello"; let (i1, o1) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(i1, 2); assert_eq!(o1, 2); assert!(reply.is_ended()); } #[test] fn post_streaming_too_much() { let mut reply = Reply::new().unwrap(); // Receive POST request with content-length let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ content-length: 5\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 62); assert_eq!(request.method(), "POST"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Try to receive more data than content-length let mut body_buf = vec![0_u8; 1024]; let input = b"hello world"; // 11 bytes, but content-length is 5 let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 5); } #[test] fn post_streaming_after_end() { let mut reply = Reply::new().unwrap(); // Receive POST request with chunked encoding let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ transfer-encoding: chunked\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 71); assert_eq!(request.method(), "POST"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Receive body chunks let mut body_buf = vec![0_u8; 1024]; // First chunk let input = b"5\r\nhello\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 10); assert_eq!(output_used, 5); // Final chunk let input = b"0\r\n\r\n"; let (input_used, output_used) = reply.read(input, &mut body_buf[5..]).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 0); assert!(reply.is_ended()); // Try to receive more data after end let input = b"more data"; let (i1, o1) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(i1, 0); assert_eq!(o1, 0); } #[test] fn post_with_short_body_input() { let mut reply = Reply::new().unwrap(); // Receive POST request with content-length let input = b"POST /upload HTTP/1.1\r\n\ host: test.local\r\n\ content-length: 11\r\n\ \r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 63); assert_eq!(request.method(), "POST"); // Should go to RecvBody state let reply = reply.proceed().unwrap(); let mut reply = match reply { RecvRequestResult::RecvBody(r) => r, _ => panic!("Expected RecvBody state"), }; // Receive body in small chunks let mut body_buf = vec![0_u8; 1024]; // First chunk let input = b"He"; let (input_used, output_used) = reply.read(input, &mut body_buf).unwrap(); assert_eq!(input_used, 2); assert_eq!(output_used, 2); assert_eq!(&body_buf[..output_used], b"He"); // Second chunk let input = b"llo "; let (input_used, output_used) = reply.read(input, &mut body_buf[2..]).unwrap(); assert_eq!(input_used, 4); assert_eq!(output_used, 4); assert_eq!(&body_buf[..6], b"Hello "); // Final chunk let input = b"World"; let (input_used, output_used) = reply.read(input, &mut body_buf[6..]).unwrap(); assert_eq!(input_used, 5); assert_eq!(output_used, 5); assert_eq!(&body_buf[..11], b"Hello World"); assert!(reply.is_ended()); } #[test] fn non_standard_method_is_ok() { let mut reply = Reply::new().unwrap(); // Try to receive request with non-standard method let input = b"FNORD /page HTTP/1.1\r\n\ host: test.local\r\n\ \r\n"; let result = reply.try_request(input); assert!(result.is_ok()); } #[test] fn ensure_reasonable_stack_sizes() { macro_rules! ensure { ($type:ty, $size:tt) => { let sz = std::mem::size_of::<$type>(); assert!( sz <= $size, "Stack size of {} is too big {} > {}", stringify!($type), sz, $size ); }; } ensure!(http::Response<()>, 300); // ~224 ensure!(AmendedResponse, 400); // ~368 ensure!(Inner, 600); // ~512 ensure!(Reply, 600); // ~512 } #[test] fn connect() { let mut reply = Reply::new().unwrap(); let input = b"CONNECT example.com HTTP/1.1\r\nhost: example.com\r\n\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 51); assert_eq!(request.method(), "CONNECT"); assert_eq!(request.uri().path(), ""); // Should go to ProvideReponse state (content-length/transfer-encoding is ignored with CONNECT) let RecvRequestResult::ProvideResponse(reply) = reply.proceed().unwrap() else { panic!("Expected ProvideResponse state"); }; let response = Response::builder().status(StatusCode::OK).body(()).unwrap(); let mut reply = reply.provide(response).unwrap(); let mut output = vec![0_u8; 1024]; let n = reply.write(&mut output).unwrap(); // Response should ignore provided content-length/transfer-encoding headers let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "HTTP/1.1 200 OK\r\n\r\n"); // should go to Cleanup state (content-length/transfer-encoding is ignored with CONNECT) let SendResponseResult::Cleanup(_reply) = reply.proceed() else { panic!("Expected Cleanup state") }; } #[test] fn connect_read_body() { let mut reply = Reply::new().unwrap(); reply.force_recv_body(); let input = b"CONNECT example.com HTTP/1.1\r\nhost: example.com\r\ncontent-length: 1024\r\n\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 73); assert_eq!(request.method(), "CONNECT"); assert_eq!(request.uri().path(), ""); // Should go to RecvBody state (body reading footgun was enabled) let RecvRequestResult::RecvBody(_reply) = reply.proceed().unwrap() else { panic!("Expected RecvBody state"); }; } #[test] fn connect_send_body_fails() { let mut reply = Reply::new().unwrap(); let input = b"CONNECT example.com HTTP/1.1\r\nhost: example.com\r\ncontent-length: 1024\r\n\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 73); assert_eq!(request.method(), "CONNECT"); assert_eq!(request.uri().path(), ""); // Should go to ProvideReponse state (content-length/transfer-encoding is ignored with CONNECT) let RecvRequestResult::ProvideResponse(reply) = reply.proceed().unwrap() else { panic!("Expected ProvideResponse state"); }; let response = Response::builder() .status(StatusCode::OK) .header("content-length", 1024) .body(()) .unwrap(); reply .provide(response) .expect_err("no body allowed on CONNECT response"); } #[test] fn connect_send_body_with_footgun() { let mut reply = Reply::new().unwrap(); let input = b"CONNECT example.com HTTP/1.1\r\nhost: example.com\r\ncontent-length: 1024\r\n\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); let request = request.unwrap(); assert_eq!(input_used, 73); assert_eq!(request.method(), "CONNECT"); assert_eq!(request.uri().path(), ""); // Should go to ProvideReponse state (content-length/transfer-encoding is ignored with CONNECT) let RecvRequestResult::ProvideResponse(mut reply) = reply.proceed().unwrap() else { panic!("Expected ProvideResponse state"); }; let response = Response::builder() .status(StatusCode::OK) .header("content-length", 1024) .body(()) .unwrap(); reply.force_send_body(); let mut reply = reply.provide(response).unwrap(); let mut output = vec![0_u8; 1024]; let n = reply.write(&mut output).unwrap(); let s = str::from_utf8(&output[..n]).unwrap(); assert_eq!(s, "HTTP/1.1 200 OK\r\ncontent-length: 1024\r\n\r\n"); let SendResponseResult::SendBody(_reply) = reply.proceed() else { panic!("Expected SendBody state") }; } } algesten-ureq-proto-4182eed/src/server/provres.rs000066400000000000000000000047521511030533500222140ustar00rootroot00000000000000use http::{header, Response}; use crate::body::response_body_allowed; use crate::{CloseReason, Error}; use super::state::{ProvideResponse, SendResponse}; use super::{append_request, Reply}; impl Reply { /// Provide a response to the client's request. /// /// Takes a Response object and transitions to the SendResponse state. /// Handles setting appropriate headers for the response body if they weren't already set. pub fn provide(self, response: Response<()>) -> Result, Error> { if self.inner.expect_100_reject && !response.status().is_client_error() && !response.status().is_server_error() { return Err(Error::BadReject100Status(response.status())); } let mut inner = append_request(self.inner, response); // unwrap are correct due to state we should be in when we get here. let response = inner.response.as_mut().unwrap(); if response .headers() .any(|(h, v)| h == header::CONNECTION && v == "close") { inner.close_reason.push(CloseReason::ServerConnectionClose); } let writer = inner.state.writer.take().unwrap(); let info = response.analyze(writer)?; let body_provided = info.body_mode.has_body(); let (_, status) = response.prelude(); let status = status.into(); let method = inner.method.as_ref().unwrap(); let body_allowed = response_body_allowed(method, status, info.body_mode.body_mode()); let force_send = inner.force_send_body; let should_send_body = body_allowed || force_send; if body_provided && !should_send_body { // User set a body header but method does not allow one return Err(Error::BodyNotAllowed); } if body_provided && !info.res_body_header && should_send_body { // User did not set a body header, we set one. let header = info.body_mode.body_header(); response.set_header(header.0, header.1)?; } inner.state.writer = Some(info.body_mode); Ok(Reply::wrap(inner)) } /// Convert the state to send a body despite the method /// /// Methods like HEAD and CONNECT should not have attached bodies. /// Some broken APIs use bodies anyway and this is an escape hatch to /// interoperate with such services. pub fn force_send_body(&mut self) { self.inner.force_send_body = true; } } algesten-ureq-proto-4182eed/src/server/recvbody.rs000066400000000000000000000057121511030533500223260ustar00rootroot00000000000000use crate::{BodyMode, Error}; use super::state::{ProvideResponse, RecvBody}; use super::Reply; impl Reply { /// Read the input as a request body. /// /// This method reads data from the input buffer (the request body from the client) /// and writes it to the output buffer. It handles different transfer encodings /// (chunked, content-length, etc.) automatically. /// /// * `input` - A byte slice containing the input data from the client /// * `output` - A mutable byte slice to write the decoded body data to /// /// Returns a tuple `(usize, usize)` where: /// - The first element is the number of bytes consumed from the input /// - The second element is the number of bytes written to the output pub fn read(&mut self, input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> { let rbm = self.inner.state.reader.as_mut().unwrap(); if rbm.is_ended() { return Ok((0, 0)); } rbm.read(input, output, self.inner.state.stop_on_chunk_boundary) } /// Set whether we are stopping on chunk boundaries. /// /// If `false`, we are trying to fill the entire `output` in each `read()` call. /// /// This is useful when processing chunked transfer encoding and you want to /// handle each chunk separately. /// /// * `enabled` - Whether to stop reading at chunk boundaries /// /// Defaults to `false` (read as much as possible). pub fn stop_on_chunk_boundary(&mut self, enabled: bool) { self.inner.state.stop_on_chunk_boundary = enabled; } /// Tell if we are currently on a chunk boundary. /// /// This method is useful when you've enabled `stop_on_chunk_boundary()` to /// determine if the current position is at a chunk boundary. /// /// Returns `true` if the current position is at a chunk boundary, `false` otherwise. /// /// Only relevant if you are using chunked transfer encoding and have enabled /// `stop_on_chunk_boundary()`. pub fn is_on_chunk_boundary(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); rbm.is_on_chunk_boundary() } /// Tell which kind of mode the response body is. pub fn body_mode(&self) -> BodyMode { self.inner.state.reader.as_ref().unwrap().body_mode() } /// Tell if the request body is over /// /// Returns `true` if the entire request body has been received, `false` otherwise. pub fn is_ended(&self) -> bool { let rbm = self.inner.state.reader.as_ref().unwrap(); rbm.is_ended() } /// Proceed to sending a response. /// /// This is only possible when the request body is fully read. /// /// Returns the Reply in the ProvideResponse state. /// /// Panics if the request body has not been fully read. pub fn proceed(self) -> Result, Error> { assert!(self.is_ended()); Ok(Reply::wrap(self.inner)) } } algesten-ureq-proto-4182eed/src/server/recvreq.rs000066400000000000000000000131021511030533500221500ustar00rootroot00000000000000use http::{header, Request, Version}; use crate::body::BodyReader; use crate::ext::HeaderIterExt; use crate::parser::try_parse_request; use crate::util::log_data; use crate::{ArrayVec, CloseReason, Error}; use super::state::RecvRequest; use super::{Inner, Reply, ResponsePhase}; use super::{RecvRequestResult, MAX_REQUEST_HEADERS}; impl Reply { /// Create a new Reply in the RecvRequest state. /// /// This is the entry point for the server state machine. It creates a new Reply /// in the RecvRequest state, ready to receive an HTTP request from a client. /// /// Returns an error if the Reply cannot be created. pub fn new() -> Result { let close_reason = ArrayVec::from_fn(|_| CloseReason::ClientConnectionClose); let inner = Inner { phase: ResponsePhase::Status, state: super::BodyState::default(), response: None, close_reason, force_recv_body: false, force_send_body: false, method: None, expect_100: false, expect_100_reject: false, }; Ok(Reply::wrap(inner)) } /// Try reading a request from the input. /// /// Attempts to parse an HTTP request from the input buffer. If the input buffer /// doesn't contain a complete request, this method will return `Ok((0, None))`. /// /// Returns a tuple with the number of bytes consumed from the input and /// the parsed request (or None if incomplete). /// /// Returns an error if there's a problem parsing the request. pub fn try_request(&mut self, input: &[u8]) -> Result<(usize, Option>), Error> { let maybe_request = self.do_try_request(input)?; let (input_used, request) = match maybe_request { Some(v) => v, // Not enough input for a full response yet None => return Ok((0, None)), }; self.inner.method = Some(request.method().clone()); self.inner.expect_100 = request.headers().iter().has_expect_100(); let headers = request.headers(); let is_http10 = request.version() == Version::HTTP_10; let is_keep_alive = headers.iter().has(header::CONNECTION, "keep-alive"); let is_conn_close = headers.iter().has(header::CONNECTION, "close"); if is_http10 && !is_keep_alive { self.inner .close_reason .push(CloseReason::CloseDelimitedBody); } if is_conn_close { self.inner .close_reason .push(CloseReason::ClientConnectionClose); } Ok((input_used, Some(request))) } /// Try reading request headers /// /// A request is only possible once the `input` holds all the HTTP request /// headers. Before that this returns `None`. When the request is succesfully read, /// the return value `(usize, Request<()>)` contains how many bytes were consumed /// of the `input`. fn do_try_request(&mut self, input: &[u8]) -> Result)>, Error> { // ~3k for 100 headers let (input_used, request) = match try_parse_request::(input)? { Some(v) => v, None => { return Ok(None); } }; log_data(&input[..input_used]); let http10 = request.version() == Version::HTTP_10; let method = request.method(); let header_lookup = |name: http::HeaderName| { if let Some(header) = request.headers().get(name) { return header.to_str().ok(); } None }; let reader = BodyReader::for_request(http10, method, self.inner.force_recv_body, &header_lookup)?; self.inner.state.reader = Some(reader); Ok(Some((input_used, request))) } /// Check if the Reply can proceed to the next state. /// /// This method is currently not implemented and will panic if called. /// In a real implementation, it would check if the request has been fully received /// and is ready to proceed to the next state. pub fn can_proceed(&self) -> bool { self.inner.state.reader.is_some() } /// Proceed to the next state. /// /// This returns `None` if we have not finished receiving the request. It is guaranteed that if /// `can_proceed()` returns true, this will return `Some`. /// /// Returns one of the following variants of `RecvRequestResult`: /// - `Send100` if the request included an "Expect: 100-continue" header /// - `RecvBody` if the request has a body to receive /// - `ProvideResponse` if the request doesn't have a body pub fn proceed(self) -> Option { if !self.can_proceed() { return None; } let has_request_body = self.inner.state.reader.as_ref().unwrap().has_body(); if has_request_body { if self.inner.expect_100 { Some(RecvRequestResult::Send100(Reply::wrap(self.inner))) } else { Some(RecvRequestResult::RecvBody(Reply::wrap(self.inner))) } } else { Some(RecvRequestResult::ProvideResponse(Reply::wrap(self.inner))) } } /// Convert the state to receive a body despite method. /// /// Methods like HEAD and CONNECT should not have attached bodies. /// Some broken APIs use bodies anyway and this is an escape hatch to /// interoperate with such services. pub fn force_recv_body(&mut self) { self.inner.force_recv_body = true; } } algesten-ureq-proto-4182eed/src/server/send100.rs000066400000000000000000000032631511030533500216620ustar00rootroot00000000000000use http::{StatusCode, Version}; use crate::util::Writer; use crate::Error; use super::state::{ProvideResponse, RecvBody, Send100}; use super::{do_write_send_line, Reply}; impl Reply { /// Sends a 100 Continue response and proceeds to receiving the body. /// /// This method sends an HTTP 100 Continue response to the client, indicating that /// the server is willing to accept the request body. After sending the response, /// it transitions to the RecvBody state to receive the request body. /// /// Returns a tuple with the number of bytes written to the output buffer and /// the Reply in the RecvBody state. /// /// Returns an `Error::OutputOverflow` if the output buffer isn't large enough to /// contain the 100 Continue status line. pub fn accept(self, output: &mut [u8]) -> Result<(usize, Reply), Error> { let mut w = Writer::new(output); let success = do_write_send_line((Version::HTTP_11, StatusCode::CONTINUE), &mut w, true); if !success { return Err(Error::OutputOverflow); } let output_used = w.len(); let flow = Reply::wrap(self.inner); Ok((output_used, flow)) } /// Rejects the 100 Continue request and proceeds to providing a response. /// /// This method rejects the client's "Expect: 100-continue" request and transitions /// to the ProvideResponse state. The server should then provide an error response /// (typically a 4xx or 5xx status code) to indicate why the request was rejected. pub fn reject(mut self) -> Reply { self.inner.expect_100_reject = true; Reply::wrap(self.inner) } } algesten-ureq-proto-4182eed/src/server/sendbody.rs000066400000000000000000000100561511030533500223150ustar00rootroot00000000000000use crate::body::calculate_max_input; use crate::util::Writer; use crate::Error; use super::state::{Cleanup, SendBody}; use super::Reply; impl Reply { /// Write response body from `input` to `output`. /// /// This is called repeatedly until the entire body has been sent. The output buffer is filled /// as much as possible for each call. /// /// Depending on response headers, the output might be `transfer-encoding: chunked`. Chunking means /// the output is slightly larger than the input due to the extra length headers per chunk. /// When not doing chunked, the input/output will be the same per call. /// /// The result `(usize, usize)` is `(input consumed, output used)`. /// /// **Important** /// /// To indicate that the body is fully sent, you call write with an `input` parameter set to `&[]`. /// This ends the `transfer-encoding: chunked` and ensures the state is correct to proceed. pub fn write(&mut self, input: &[u8], output: &mut [u8]) -> Result<(usize, usize), Error> { let mut w = Writer::new(output); // unwrap is ok because we must have called analyze in above write() // to use consume_direct_write() let writer = self.inner.state.writer.as_mut().unwrap(); if !input.is_empty() && writer.is_ended() { return Err(Error::BodyContentAfterFinish); } if let Some(left) = writer.left_to_send() { if input.len() as u64 > left { return Err(Error::BodyLargerThanContentLength); } } let input_used = writer.write(input, &mut w); let output_used = w.len(); Ok((input_used, output_used)) } /// Helper to avoid copying memory. /// /// When the transfer is _NOT_ chunked, `write()` just copies the `input` to the `output`. /// This memcopy might be possible to avoid if the user can use the `input` buffer directly /// against the transport. /// /// This function is used to "report" how much of the input that has been used. It's effectively /// the same as the first `usize` in the pair returned by `write()`. pub fn consume_direct_write(&mut self, amount: usize) -> Result<(), Error> { // unwrap is ok because we must have called analyze in above write() // to use consume_direct_write() let writer = self.inner.state.writer.as_mut().unwrap(); if let Some(left) = writer.left_to_send() { if amount as u64 > left { return Err(Error::BodyLargerThanContentLength); } } else { return Err(Error::BodyIsChunked); } writer.consume_direct_write(amount); Ok(()) } /// Calculate the max amount of input we can transfer to fill the `output_len`. /// /// For chunked transfer, the input is less than the output. pub fn calculate_max_input(&self, output_len: usize) -> usize { // For non-chunked, the entire output can be used. if !self.is_chunked() { return output_len; } calculate_max_input(output_len) } /// Test if the response is using chunked transfer encoding. pub fn is_chunked(&self) -> bool { self.inner.state.writer.as_ref().unwrap().is_chunked() } /// Check whether the response body is fully sent. /// /// For responses with a `content-length` header set, this will only become `true` once the /// number of bytes communicated have been sent. For chunked transfer, this becomes `true` /// after calling `write()` with an input of `&[]`. pub fn is_finished(&self) -> bool { self.inner.state.writer.as_ref().unwrap().is_ended() } /// Proceed to the Cleanup state. /// /// Transitions to the Cleanup state after the response body has been fully sent. /// This is only possible when the response body is fully sent. /// /// Panics if the response body has not been fully sent. pub fn proceed(self) -> Reply { assert!(self.is_finished()); Reply::wrap(self.inner) } } algesten-ureq-proto-4182eed/src/server/sendres.rs000066400000000000000000000077631511030533500221640ustar00rootroot00000000000000use std::io::Write; use http::{HeaderName, HeaderValue}; use crate::util::Writer; use crate::Error; use super::state::SendResponse; use super::{do_write_send_line, Reply, ResponsePhase, SendResponseResult}; impl Reply { /// Write the response headers to the output buffer. /// /// Writes the response status line and headers to the output buffer. /// May need to be called multiple times if the output buffer isn't large enough. /// /// Returns the number of bytes written to the output buffer. pub fn write(&mut self, output: &mut [u8]) -> Result { // unwrap is ok because we are not here without providing it let response = self.inner.response.as_ref().unwrap(); let mut w = Writer::new(output); try_write_prelude(response, &mut self.inner.phase, &mut w)?; let output_used = w.len(); Ok(output_used) } /// Whether the response headers have been fully written. /// /// Returns true if all response headers have been written and the state /// is ready to proceed to sending the response body. pub fn is_finished(&self) -> bool { !self.inner.phase.is_prelude() } /// Proceed to sending a response body or cleanup. /// /// Transitions to either: /// - SendBody state if the response needs a body (based on status code and method) /// - Cleanup state if no response body should be sent (e.g., HEAD requests) /// /// This is only possible when the response headers are fully written. /// /// Panics if the response headers have not been fully written. pub fn proceed(self) -> SendResponseResult { assert!(self.is_finished()); if let Some(writer) = self.inner.state.writer { // Enter SendBody for both chunked and sized bodies, even when size is 0, // to ensure consistent state progression for explicit body headers. // // TODO(martin): Do we actually want this API wise? Isn't it better to go straight to Cleanup? if writer.is_chunked() || writer.left_to_send().is_some() { return SendResponseResult::SendBody(Reply::wrap(self.inner)); } } SendResponseResult::Cleanup(Reply::wrap(self.inner)) } } fn try_write_prelude( response: &super::amended::AmendedResponse, phase: &mut ResponsePhase, w: &mut Writer, ) -> Result<(), Error> { let at_start = w.len(); loop { if try_write_prelude_part(response, phase, w) { continue; } let written = w.len() - at_start; if written > 0 || phase.is_body() { return Ok(()); } else { return Err(Error::OutputOverflow); } } } fn try_write_prelude_part( response: &super::amended::AmendedResponse, phase: &mut ResponsePhase, w: &mut Writer, ) -> bool { match phase { ResponsePhase::Status => { let success = do_write_send_line(response.prelude(), w, false); if success { *phase = ResponsePhase::Headers(0); } success } ResponsePhase::Headers(index) => { let header_count = response.headers_len(); let all = response.headers(); let skipped = all.skip(*index); do_write_headers(skipped, index, w); if *index == header_count && w.try_write(|w| write!(w, "\r\n")) { *phase = ResponsePhase::Body; } false } // We're past the header. _ => false, } } fn do_write_headers<'a, I>(headers: I, index: &mut usize, w: &mut Writer) where I: Iterator, { for h in headers { let success = w.try_write(|w| { write!(w, "{}: ", h.0)?; w.write_all(h.1.as_bytes())?; write!(w, "\r\n")?; Ok(()) }); if success { *index += 1; } else { break; } } } algesten-ureq-proto-4182eed/src/server/test/000077500000000000000000000000001511030533500211155ustar00rootroot00000000000000algesten-ureq-proto-4182eed/src/server/test/mod.rs000066400000000000000000000002551511030533500222440ustar00rootroot00000000000000mod scenario; mod state_cleanup; mod state_provide_response; mod state_recv_body; mod state_recv_request; mod state_send_100; mod state_send_body; mod state_send_response; algesten-ureq-proto-4182eed/src/server/test/scenario.rs000066400000000000000000000262001511030533500232660ustar00rootroot00000000000000use std::io::Write; use std::marker::PhantomData; use http::{Method, Request, Response}; use crate::server::state::{ Cleanup, ProvideResponse, RecvBody, RecvRequest, Send100, SendBody, SendResponse, }; use crate::server::{RecvRequestResult, Reply, SendResponseResult}; pub struct Scenario { request: Request<()>, request_body: Vec, response: Response<()>, response_body: Vec, } impl Scenario { pub fn builder() -> ScenarioBuilder<()> { ScenarioBuilder::new() } } impl Scenario { pub fn to_recv_request(&self) -> Reply { // Create a new Reply in the RecvRequest state Reply::new().unwrap() } pub fn to_send_100(&self) -> Reply { let mut reply = self.to_recv_request(); // Write the request and proceed to Send100 let input = write_request(&self.request); let (_, request) = reply.try_request(&input).unwrap(); assert!(request.is_some()); match reply.proceed().unwrap() { RecvRequestResult::Send100(v) => v, _ => unreachable!("Incorrect scenario not leading to_send_100()"), } } pub fn to_recv_body(&self) -> Reply { // For tests that need a Reply in the RecvBody state, we'll create one directly // This is a simplified version that doesn't go through the full state machine let mut reply = self.to_recv_request(); // Write the request and proceed to RecvBody let input = write_request(&self.request); let (_, request) = reply.try_request(&input).unwrap(); assert!(request.is_some()); // If the request has an Expect: 100-continue header, we need to go through Send100 if self .request .headers() .get("expect") .is_some_and(|v| v == "100-continue") { match reply.proceed().unwrap() { RecvRequestResult::Send100(reply) => { // Accept the 100-continue and proceed to RecvBody let mut output = vec![0; 1024]; let (_, reply) = reply.accept(&mut output).unwrap(); reply } _ => unreachable!("Expect: 100-continue header should lead to Send100"), } } else { // Otherwise, proceed directly to RecvBody match reply.proceed().unwrap() { RecvRequestResult::RecvBody(reply) => reply, _ => unreachable!("Request without body should lead to ProvideResponse"), } } } pub fn to_provide_response(&self) -> Reply { // For tests that need a Reply in the ProvideResponse state, we'll create one directly // This is a simplified version that doesn't go through the full state machine let mut reply = self.to_recv_request(); // Write the request and proceed let input = write_request(&self.request); let (_, request) = reply.try_request(&input).unwrap(); assert!(request.is_some()); // If the request has an Expect: 100-continue header, we need to go through Send100 if self .request .headers() .get("expect") .is_some_and(|v| v == "100-continue") { match reply.proceed().unwrap() { RecvRequestResult::Send100(reply) => { // Accept the 100-continue and proceed to RecvBody let mut output = vec![0; 1024]; let (_, mut reply) = reply.accept(&mut output).unwrap(); // Write the request body and proceed to ProvideResponse let (_, _) = reply.read(&self.request_body, &mut vec![0; 1024]).unwrap(); // If the body is chunked, we need to send the end marker if !self.request_body.is_empty() && !self.request_body.ends_with(b"\r\n0\r\n\r\n") { let end_marker = b"0\r\n\r\n"; let (_, _) = reply.read(end_marker, &mut vec![0; 1024]).unwrap(); } reply.proceed().unwrap() } _ => unreachable!("Expect: 100-continue header should lead to Send100"), } } else if !self.request_body.is_empty() { // If the request has a body, we need to go through RecvBody match reply.proceed().unwrap() { RecvRequestResult::RecvBody(mut reply) => { // Write the request body and proceed to ProvideResponse let (_, _) = reply.read(&self.request_body, &mut vec![0; 1024]).unwrap(); // If the body is chunked, we need to send the end marker if !self.request_body.is_empty() && !self.request_body.ends_with(b"\r\n0\r\n\r\n") { let end_marker = b"0\r\n\r\n"; let (_, _) = reply.read(end_marker, &mut vec![0; 1024]).unwrap(); } reply.proceed().unwrap() } _ => unreachable!("Request with body should lead to RecvBody"), } } else { // Otherwise, proceed directly to ProvideResponse match reply.proceed().unwrap() { RecvRequestResult::ProvideResponse(reply) => reply, _ => unreachable!("Request without body should lead to ProvideResponse"), } } } pub fn to_send_response(&self) -> Reply { // For tests that need a Reply in the SendResponse state, we'll create one directly let reply = self.to_provide_response(); // Provide the response and proceed to SendResponse reply.provide(self.response.clone()).unwrap() } pub fn to_send_body(&self) -> Reply { // For tests that need a Reply in the SendBody state, we'll create one directly let mut reply = self.to_send_response(); // Write the response headers and proceed to SendBody let mut output = vec![0; 1024]; reply.write(&mut output).unwrap(); match reply.proceed() { SendResponseResult::SendBody(reply) => reply, SendResponseResult::Cleanup(_) => { panic!("Expected SendBody variant, got Cleanup. This usually means the response doesn't need a body (e.g., HEAD request or 204 response)") } } } pub fn to_cleanup(&self) -> Reply { // For tests that need a Reply in the Cleanup state, we'll create one directly let mut reply = self.to_send_body(); // Write the response body and proceed to Cleanup let mut output = vec![0; 1024]; if !self.response_body.is_empty() { reply.write(&self.response_body, &mut output).unwrap(); } // Send end marker reply.write(&[], &mut output).unwrap(); reply.proceed() } } pub fn write_request(r: &Request<()>) -> Vec { let mut output = Vec::::new(); write!( &mut output, "{} {} {:?}\r\n", r.method(), r.uri().path(), r.version() ) .unwrap(); for (k, v) in r.headers().iter() { write!(&mut output, "{}: {}\r\n", k.as_str(), v.to_str().unwrap()).unwrap(); } write!(&mut output, "\r\n").unwrap(); output } #[derive(Default)] pub struct ScenarioBuilder { request: Request<()>, request_body: Vec, response: Response<()>, response_body: Vec, _ph: PhantomData, } pub struct WithReq(()); pub struct WithRes(()); #[allow(unused)] impl ScenarioBuilder<()> { pub fn new() -> Self { Default::default() } pub fn request(self, request: Request<()>) -> ScenarioBuilder { ScenarioBuilder { request, request_body: vec![], response: Response::default(), response_body: vec![], _ph: PhantomData, } } pub fn method(self, method: Method, uri: &str) -> ScenarioBuilder { self.request(Request::builder().method(method).uri(uri).body(()).unwrap()) } pub fn get(self, uri: &str) -> ScenarioBuilder { self.request(Request::get(uri).body(()).unwrap()) } pub fn head(self, uri: &str) -> ScenarioBuilder { self.request(Request::head(uri).body(()).unwrap()) } pub fn post(self, uri: &str) -> ScenarioBuilder { self.request(Request::post(uri).body(()).unwrap()) } pub fn put(self, uri: &str) -> ScenarioBuilder { self.request(Request::put(uri).body(()).unwrap()) } pub fn options(self, uri: &str) -> ScenarioBuilder { self.request(Request::options(uri).body(()).unwrap()) } pub fn delete(self, uri: &str) -> ScenarioBuilder { self.request(Request::delete(uri).body(()).unwrap()) } pub fn trace(self, uri: &str) -> ScenarioBuilder { self.request(Request::trace(uri).body(()).unwrap()) } pub fn connect(self, uri: &str) -> ScenarioBuilder { self.request(Request::connect(uri).body(()).unwrap()) } pub fn patch(self, uri: &str) -> ScenarioBuilder { self.request(Request::patch(uri).body(()).unwrap()) } } #[allow(unused)] impl ScenarioBuilder { pub fn header(mut self, key: &'static str, value: impl ToString) -> Self { self.request .headers_mut() .append(key, value.to_string().try_into().unwrap()); self } pub fn request_body>(mut self, body: B, chunked: bool) -> Self { let body = body.as_ref().to_vec(); let len = body.len(); if chunked { // Format the body as chunked encoding let mut chunked_body = Vec::new(); write!(&mut chunked_body, "{:x}\r\n", len).unwrap(); chunked_body.extend_from_slice(&body); write!(&mut chunked_body, "\r\n0\r\n\r\n").unwrap(); self.request_body = chunked_body; self.header("transfer-encoding", "chunked") } else { self.request_body = body; self.header("content-length", len.to_string()) } } pub fn response(mut self, response: Response<()>) -> ScenarioBuilder { let ScenarioBuilder { request, request_body, response_body, .. } = self; ScenarioBuilder { request, request_body, response, response_body, _ph: PhantomData, } } pub fn build(self) -> Scenario { Scenario { request: self.request, request_body: self.request_body, response: Response::default(), response_body: vec![], } } } impl ScenarioBuilder { pub fn build(self) -> Scenario { Scenario { request: self.request, request_body: self.request_body, response: self.response, response_body: self.response_body, } } } algesten-ureq-proto-4182eed/src/server/test/state_cleanup.rs000066400000000000000000000075531511030533500243240ustar00rootroot00000000000000use http::{Request, Response, Version}; use crate::CloseReason; use super::scenario::Scenario; #[test] fn http10_with_keep_alive() { // Create a scenario with an HTTP/1.0 request with "connection: keep-alive" header let scenario = Scenario::builder() .request( Request::get("/path") .version(Version::HTTP_10) .header("connection", "keep-alive") .body(()) .unwrap(), ) .response( Response::builder() .status(200) .header("content-length", "0") .body(()) .unwrap(), ) .build(); // Get a Reply in the Cleanup state let reply = scenario.to_cleanup(); // Verify that we don't need to close the connection assert!(!reply.must_close_connection()); } #[test] fn reuse_connection() { // Create a scenario with a GET request and a response let scenario = Scenario::builder() .get("/path") .response( Response::builder() .status(200) .header("content-length", "0") .body(()) .unwrap(), ) .build(); // Get a Reply in the Cleanup state let reply = scenario.to_cleanup(); // Verify that we don't need to close the connection assert!(!reply.must_close_connection()); } #[test] fn close_due_to_client_connection_close() { // Create a scenario with a GET request with "connection: close" header let scenario = Scenario::builder() .request( Request::get("/path") .header("connection", "close") .body(()) .unwrap(), ) .response( Response::builder() .status(200) .header("content-length", "0") .body(()) .unwrap(), ) .build(); // Get a Reply in the Cleanup state let reply = scenario.to_cleanup(); // Verify that we must close the connection assert!(reply.must_close_connection()); // Verify the close reason let inner = reply.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::ClientConnectionClose ); } #[test] fn close_due_to_server_connection_close() { // Create a scenario with a GET request and a response with "connection: close" header let scenario = Scenario::builder() .get("/path") .response( Response::builder() .status(200) .header("connection", "close") .header("content-length", "0") .body(()) .unwrap(), ) .build(); // Get a Reply in the Cleanup state let reply = scenario.to_cleanup(); // Verify that we must close the connection assert!(reply.must_close_connection()); // Verify the close reason let inner = reply.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::ServerConnectionClose ); } #[test] fn close_due_to_http10() { // Create a scenario with an HTTP/1.0 request let scenario = Scenario::builder() .request( Request::get("/path") .version(Version::HTTP_10) .body(()) .unwrap(), ) .response( Response::builder() .status(200) .header("content-length", "0") .body(()) .unwrap(), ) .build(); // Get a Reply in the Cleanup state let reply = scenario.to_cleanup(); // Verify that we must close the connection assert!(reply.must_close_connection()); // Verify the close reason let inner = reply.inner(); assert_eq!( *inner.close_reason.first().unwrap(), CloseReason::CloseDelimitedBody ); } algesten-ureq-proto-4182eed/src/server/test/state_provide_response.rs000066400000000000000000000130241511030533500262510ustar00rootroot00000000000000use http::{Response, StatusCode}; use crate::Error; use super::scenario::Scenario; #[test] fn provide_successful_response() { // Create a scenario with a GET request let scenario = Scenario::builder().get("/path").build(); // Get a Reply in the ProvideResponse state let reply = scenario.to_provide_response(); // Create a successful response let response = Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain") .body(()) .unwrap(); // Provide the response let reply = reply.provide(response).unwrap(); // Verify the state transition assert!(reply.inner().response.is_some()); assert!(reply.inner().state.writer.is_some()); // Verify the status code let status = reply.inner().response.as_ref().unwrap().prelude().1; assert_eq!(status, StatusCode::OK); } #[test] fn provide_error_response() { // Create a scenario with a GET request let scenario = Scenario::builder().get("/path").build(); // Get a Reply in the ProvideResponse state let reply = scenario.to_provide_response(); // Create an error response let response = Response::builder() .status(StatusCode::NOT_FOUND) .header("content-type", "text/plain") .body(()) .unwrap(); // Provide the response let reply = reply.provide(response).unwrap(); // Verify the state transition assert!(reply.inner().response.is_some()); assert!(reply.inner().state.writer.is_some()); // Verify the status code let status = reply.inner().response.as_ref().unwrap().prelude().1; assert_eq!(status, StatusCode::NOT_FOUND); } #[test] fn provide_response_after_reject_100() { // Create a scenario with a POST request with Expect: 100-continue let scenario = Scenario::builder() .post("/path") .header("expect", "100-continue") .build(); // Get a Reply in the Send100 state let reply = scenario.to_send_100(); // Reject the 100-continue request let reply = reply.reject(); // Create an error response (required after rejecting 100-continue) let response = Response::builder() .status(StatusCode::BAD_REQUEST) .header("content-type", "text/plain") .body(()) .unwrap(); // Provide the response let reply = reply.provide(response).unwrap(); // Verify the state transition assert!(reply.inner().response.is_some()); assert!(reply.inner().state.writer.is_some()); // Verify the status code let status = reply.inner().response.as_ref().unwrap().prelude().1; assert_eq!(status, StatusCode::BAD_REQUEST); } #[test] fn error_when_providing_success_after_reject_100() { // Create a scenario with a POST request with Expect: 100-continue let scenario = Scenario::builder() .post("/path") .header("expect", "100-continue") .build(); // Get a Reply in the Send100 state let reply = scenario.to_send_100(); // Reject the 100-continue request let reply = reply.reject(); // Create a successful response (invalid after rejecting 100-continue) let response = Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain") .body(()) .unwrap(); // Provide the response (should fail) let result = reply.provide(response); // Verify the error assert!(result.is_err()); match result.unwrap_err() { Error::BadReject100Status(status) => { assert_eq!(status, StatusCode::OK); } _ => panic!("Expected BadReject100Status error"), } } #[test] fn provide_response_with_content_length() { // Create a scenario with a GET request let scenario = Scenario::builder().get("/path").build(); // Get a Reply in the ProvideResponse state let reply = scenario.to_provide_response(); // Create a response with content-length let response = Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain") .header("content-length", "11") .body(()) .unwrap(); // Provide the response let reply = reply.provide(response).unwrap(); // Verify the state transition assert!(reply.inner().response.is_some()); assert!(reply.inner().state.writer.is_some()); // Verify the content-length header is present let has_content_length = reply .inner() .response .as_ref() .unwrap() .headers() .any(|(name, value)| name == "content-length" && value == "11"); assert!(has_content_length); } #[test] fn provide_response_with_chunked_encoding() { // Create a scenario with a GET request let scenario = Scenario::builder().get("/path").build(); // Get a Reply in the ProvideResponse state let reply = scenario.to_provide_response(); // Create a response with chunked encoding let response = Response::builder() .status(StatusCode::OK) .header("content-type", "text/plain") .header("transfer-encoding", "chunked") .body(()) .unwrap(); // Provide the response let reply = reply.provide(response).unwrap(); // Verify the state transition assert!(reply.inner().response.is_some()); assert!(reply.inner().state.writer.is_some()); // Verify the transfer-encoding header is present let has_chunked = reply .inner() .response .as_ref() .unwrap() .headers() .any(|(name, value)| name == "transfer-encoding" && value == "chunked"); assert!(has_chunked); } algesten-ureq-proto-4182eed/src/server/test/state_recv_body.rs000066400000000000000000000117611511030533500246450ustar00rootroot00000000000000use super::scenario::Scenario; #[test] fn read_content_length_body() { // Create a scenario with a POST request with content-length let scenario = Scenario::builder() .post("/path") .request_body(b"hello world", false) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Read the request body let mut output = vec![0; 1024]; let (input_used, output_used) = reply.read(b"hello world", &mut output).unwrap(); // Verify the results assert_eq!(input_used, 11); assert_eq!(output_used, 11); assert_eq!(&output[..output_used], b"hello world"); // Verify the body is fully received assert!(reply.is_ended()); } #[test] fn read_chunked_body() { // Create a scenario with a POST request with chunked encoding let scenario = Scenario::builder() .post("/path") .request_body(b"hello world", true) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Read the chunked request body let mut output = vec![0; 1024]; let chunked_input = b"b\r\nhello world\r\n0\r\n\r\n"; let (input_used, output_used) = reply.read(chunked_input, &mut output).unwrap(); // Verify the results assert_eq!(input_used, 21); assert_eq!(output_used, 11); assert_eq!(&output[..output_used], b"hello world"); // Verify the body is fully received assert!(reply.is_ended()); } #[test] fn read_chunked_body_in_parts() { // Create a scenario with a POST request with chunked encoding let scenario = Scenario::builder() .post("/path") .request_body(b"hello world", true) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Enable stopping on chunk boundaries reply.stop_on_chunk_boundary(true); // Read the first chunk let mut output = vec![0; 1024]; let chunk1 = b"5\r\nhello\r\n"; let (input_used1, output_used1) = reply.read(chunk1, &mut output).unwrap(); // Verify the first chunk assert_eq!(input_used1, 10); assert_eq!(output_used1, 5); assert_eq!(&output[..output_used1], b"hello"); assert!(reply.is_on_chunk_boundary()); assert!(!reply.is_ended()); // Read the second chunk let chunk2 = b"6\r\n world\r\n"; let (input_used2, output_used2) = reply.read(chunk2, &mut output[output_used1..]).unwrap(); // Verify the second chunk assert_eq!(input_used2, 11); assert_eq!(output_used2, 6); assert_eq!( &output[output_used1..output_used1 + output_used2], b" world" ); assert!(reply.is_on_chunk_boundary()); assert!(!reply.is_ended()); // Read the end marker let end_marker = b"0\r\n\r\n"; let (input_used3, output_used3) = reply .read(end_marker, &mut output[output_used1 + output_used2..]) .unwrap(); // Verify the end marker assert_eq!(input_used3, 5); assert_eq!(output_used3, 0); assert!(reply.is_ended()); } #[test] fn proceed_to_provide_response() { // Create a scenario with a POST request with content-length let scenario = Scenario::builder() .post("/path") .request_body(b"hello", false) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Read the request body let mut output = vec![0; 1024]; reply.read(b"hello", &mut output).unwrap(); // Verify the body is fully received assert!(reply.is_ended()); // Proceed to ProvideResponse let reply = reply.proceed().unwrap(); // Verify the state transition assert!(reply.inner().state.reader.is_some()); } #[test] #[should_panic] fn proceed_before_body_ended() { // Create a scenario with a POST request with content-length let scenario = Scenario::builder() .post("/path") .request_body(b"hello world", false) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Read only part of the request body let mut output = vec![0; 1024]; reply.read(b"hello", &mut output).unwrap(); // Verify the body is not fully received assert!(!reply.is_ended()); // This should panic because the body is not fully received let _ = reply.proceed(); } #[test] fn read_after_end() { // Create a scenario with a POST request with content-length let scenario = Scenario::builder() .post("/path") .request_body(b"hello", false) .build(); // Get a Reply in the RecvBody state let mut reply = scenario.to_recv_body(); // Read the request body let mut output = vec![0; 1024]; reply.read(b"hello", &mut output).unwrap(); // Verify the body is fully received assert!(reply.is_ended()); // Try to read more after the body is fully received let (input_used, output_used) = reply.read(b"more data", &mut output).unwrap(); // Should not consume any input or produce any output assert_eq!(input_used, 0); assert_eq!(output_used, 0); } algesten-ureq-proto-4182eed/src/server/test/state_recv_request.rs000066400000000000000000000137721511030533500254040ustar00rootroot00000000000000use crate::server::{RecvRequestResult, Reply}; #[test] fn parse_request() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); let input = b"GET /path HTTP/1.1\r\nhost: example.com\r\n\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); assert_eq!(input_used, 41); let request = request.unwrap(); assert_eq!(request.uri().path(), "/path"); assert_eq!(request.method(), "GET"); assert_eq!(request.headers().get("host").unwrap(), "example.com"); } #[test] fn incomplete_request() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // Incomplete request (missing final \r\n) let input = b"GET /path HTTP/1.1\r\nhost: example.com\r\n"; let (input_used, request) = reply.try_request(input).unwrap(); // Should not consume any input and return None assert_eq!(input_used, 0); assert!(request.is_none()); } #[test] fn malformed_request() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // Malformed request (invalid HTTP version) let input = b"GET /path INVALID/1.1\r\nhost: example.com\r\n\r\n"; let result = reply.try_request(input); assert!(result.is_err()); } #[test] fn proceed_to_send_100() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // Request with Expect: 100-continue let input = b"POST /path HTTP/1.1\r\nhost: example.com\r\nexpect: 100-continue\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to Send100 match reply.proceed().unwrap() { RecvRequestResult::Send100(_) => {} _ => panic!("Expected Send100 state"), } } #[test] fn proceed_to_recv_body() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // POST request without Expect: 100-continue let input = b"POST /path HTTP/1.1\r\nhost: example.com\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to RecvBody match reply.proceed().unwrap() { RecvRequestResult::RecvBody(_) => {} _ => panic!("Expected RecvBody state"), } } #[test] fn proceed_to_provide_response() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // GET request (no body) let input = b"GET /path HTTP/1.1\r\nhost: example.com\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to ProvideResponse match reply.proceed().unwrap() { RecvRequestResult::ProvideResponse(_) => {} _ => panic!("Expected ProvideResponse state"), } } #[test] fn cannot_proceed_without_request() { // Create a new Reply directly let reply = Reply::new().unwrap(); // No request received yet assert!(!reply.can_proceed()); assert!(reply.proceed().is_none()); } #[test] fn proceed_to_provide_response_with_zero_length() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // POST request with Content-Length: 0 let input = b"POST /path HTTP/1.1\r\nhost: example.com\r\ncontent-length: 0\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to ProvideResponse since there's no body to receive match reply.proceed().unwrap() { RecvRequestResult::ProvideResponse(_) => {} _ => panic!("Expected ProvideResponse state"), } } #[test] fn proceed_to_recv_body_with_get() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // GET request with Content-Length: 5 let input = b"GET /path HTTP/1.1\r\nhost: example.com\r\ncontent-length: 5\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to RecvBody since Content-Length indicates a body match reply.proceed().unwrap() { RecvRequestResult::RecvBody(_) => {} _ => panic!("Expected RecvBody state"), } } #[test] fn proceed_to_provide_response_with_head() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // HEAD request with Content-Length: 5 (should still go to ProvideResponse) let input = b"HEAD /path HTTP/1.1\r\nhost: example.com\r\ncontent-length: 5\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to ProvideResponse since HEAD requests never have a body match reply.proceed().unwrap() { RecvRequestResult::ProvideResponse(_) => {} _ => panic!("Expected ProvideResponse state"), } } #[test] fn proceed_to_provide_response_with_head_expect() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // HEAD request with Expect: 100-continue (should still go to ProvideResponse) let input = b"HEAD /path HTTP/1.1\r\nhost: example.com\r\nexpect: 100-continue\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to ProvideResponse since HEAD requests never have a body, // regardless of Expect: 100-continue header match reply.proceed().unwrap() { RecvRequestResult::ProvideResponse(_) => {} _ => panic!("Expected ProvideResponse state"), } } #[test] fn proceed_to_send_100_with_get_chunked() { // Create a new Reply directly let mut reply = Reply::new().unwrap(); // GET request with both Transfer-Encoding: chunked and Expect: 100-continue let input = b"GET /path HTTP/1.1\r\nhost: example.com\r\ntransfer-encoding: chunked\r\nexpect: 100-continue\r\n\r\n"; let (_, request) = reply.try_request(input).unwrap(); assert!(request.is_some()); // Should proceed to Send100 since request has both a body (chunked) and expect: 100-continue, // even though it's a GET request match reply.proceed().unwrap() { RecvRequestResult::Send100(_) => {} _ => panic!("Expected Send100 state"), } } algesten-ureq-proto-4182eed/src/server/test/state_send_100.rs000066400000000000000000000034131511030533500241750ustar00rootroot00000000000000use crate::Error; use super::scenario::Scenario; #[test] fn accept_100_continue() { // Create a scenario with a request that includes Expect: 100-continue let scenario = Scenario::builder() .post("/path") .header("expect", "100-continue") .build(); // Get a Reply in the Send100 state let reply = scenario.to_send_100(); // Accept the 100-continue request let mut output = vec![0; 1024]; let (output_used, reply) = reply.accept(&mut output).unwrap(); // Verify the output assert_eq!(output_used, 25); assert_eq!(&output[..output_used], b"HTTP/1.1 100 Continue\r\n\r\n"); // Verify the state transition assert!(reply.inner().state.reader.is_some()); } #[test] fn reject_100_continue() { // Create a scenario with a request that includes Expect: 100-continue let scenario = Scenario::builder() .post("/path") .header("expect", "100-continue") .build(); // Get a Reply in the Send100 state let reply = scenario.to_send_100(); // Reject the 100-continue request let reply = reply.reject(); // Verify the state transition assert!(reply.inner().expect_100_reject); } #[test] fn short_buffer() { // Create a scenario with a request that includes Expect: 100-continue let scenario = Scenario::builder() .post("/path") .header("expect", "100-continue") .build(); // Get a Reply in the Send100 state let reply = scenario.to_send_100(); // Try to accept with a buffer that's too small let mut output = vec![0; 10]; // Too small for "HTTP/1.1 100 Continue\r\n\r\n" let result = reply.accept(&mut output); // Verify the error assert!(result.is_err()); assert_eq!(result.unwrap_err(), Error::OutputOverflow); } algesten-ureq-proto-4182eed/src/server/test/state_send_body.rs000066400000000000000000000207311511030533500246340ustar00rootroot00000000000000use crate::Error; use super::scenario::Scenario; #[test] fn write_response_body() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .status(200) .header("content-length", "13") .body(()) .unwrap(), ) .build() .to_send_body(); // Create a buffer to write the response body to let mut output = [0u8; 1024]; // Write some response body data let body = b"Hello, world!"; let (input_used, output_used) = scenario.write(body, &mut output).unwrap(); // Verify that all input was consumed assert_eq!(input_used, body.len()); // Verify that the output contains the body assert_eq!(&output[..output_used], body); } #[test] fn write_chunked_response_body() { // Create a scenario with a GET request and a chunked response let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .status(200) .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build() .to_send_body(); // Create a buffer to write the response body to let mut output = [0u8; 1024]; // Write some response body data let body = b"Hello, world!"; let (input_used, output_used) = scenario.write(body, &mut output).unwrap(); // Verify that all input was consumed assert_eq!(input_used, body.len()); // Verify that the output contains the chunked body let output_str = std::str::from_utf8(&output[..output_used]).unwrap(); assert!(output_str.starts_with("d\r\n")); // 13 bytes in hex assert!(output_str.contains("Hello, world!")); assert!(output_str.ends_with("\r\n")); // Verify that the response is chunked assert!(scenario.is_chunked()); // Verify that the response is not finished yet assert!(!scenario.is_finished()); // Write an empty chunk to finish the response let (input_used, output_used) = scenario.write(&[], &mut output).unwrap(); assert_eq!(input_used, 0); assert!(output_used > 0); // Verify that the response is now finished assert!(scenario.is_finished()); } #[test] fn calculate_max_input() { // Create a scenario with a GET request and a content-length response let scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .status(200) .header("content-length", "100") .body(()) .unwrap(), ) .build() .to_send_body(); // For non-chunked responses, max input equals output length assert_eq!(scenario.calculate_max_input(100), 100); // Create a scenario with a GET request and a chunked response let scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .status(200) .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build() .to_send_body(); // For chunked responses, max input is less than output length assert!(scenario.calculate_max_input(100) < 100); } #[test] fn proceed_to_cleanup() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "13") .body(()) .unwrap(), ) .build() .to_send_body(); // Create a buffer to write the response body to let mut output = [0u8; 1024]; // Write the response body let body = b"Hello, world!"; let (input_used, _) = scenario.write(body, &mut output).unwrap(); assert_eq!(input_used, body.len()); // Verify that the response is now finished assert!(scenario.is_finished()); // Proceed to Cleanup let _cleanup = scenario.proceed(); // The type system ensures that _cleanup is now Reply } #[test] #[should_panic] fn proceed_before_finished_panics() { // Create a scenario with a GET request and a response with content-length let scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "100") .body(()) .unwrap(), ) .build() .to_send_body(); // Verify that the response is not finished assert!(!scenario.is_finished()); // Proceed to Cleanup (should panic) let _ = scenario.proceed(); } #[test] fn error_writing_after_finished() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "13") .body(()) .unwrap(), ) .build() .to_send_body(); // Create a buffer to write the response body to let mut output = [0u8; 1024]; // Write the response body let body = b"Hello, world!"; let (input_used, _) = scenario.write(body, &mut output).unwrap(); assert_eq!(input_used, body.len()); // Verify that the response is now finished assert!(scenario.is_finished()); // Try to write more data (should fail) let result = scenario.write(b"More data", &mut output); assert!(result.is_err()); match result.unwrap_err() { Error::BodyContentAfterFinish => {} _ => panic!("Expected BodyContentAfterFinish error"), } } #[test] fn error_writing_more_than_content_length() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "10") .body(()) .unwrap(), ) .build() .to_send_body(); // Create a buffer to write the response body to let mut output = [0u8; 1024]; // Try to write more data than the content-length (should fail) let result = scenario.write(b"Hello, world!", &mut output); assert!(result.is_err()); match result.unwrap_err() { Error::BodyLargerThanContentLength => {} _ => panic!("Expected BodyLargerThanContentLength error"), } } #[test] fn consume_direct_write() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "13") .body(()) .unwrap(), ) .build() .to_send_body(); // Consume direct write scenario.consume_direct_write(13).unwrap(); // Verify that the response is now finished assert!(scenario.is_finished()); } #[test] fn error_consuming_more_than_content_length() { // Create a scenario with a GET request and a response with content-length let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("content-length", "10") .body(()) .unwrap(), ) .build() .to_send_body(); // Try to consume more than the content-length (should fail) let result = scenario.consume_direct_write(11); assert!(result.is_err()); match result.unwrap_err() { Error::BodyLargerThanContentLength => {} _ => panic!("Expected BodyLargerThanContentLength error"), } } #[test] fn error_consuming_direct_write_with_chunked() { // Create a scenario with a GET request and a chunked response let mut scenario = Scenario::builder() .get("/path") .response( http::Response::builder() .header("transfer-encoding", "chunked") .body(()) .unwrap(), ) .build() .to_send_body(); // Try to consume direct write with chunked encoding (should fail) let result = scenario.consume_direct_write(10); assert!(result.is_err()); match result.unwrap_err() { Error::BodyIsChunked => {} _ => panic!("Expected BodyIsChunked error"), } } algesten-ureq-proto-4182eed/src/server/test/state_send_response.rs000066400000000000000000000120771511030533500255410ustar00rootroot00000000000000use crate::Error; use super::super::SendResponseResult; use super::scenario::Scenario; #[test] fn write_response() { // Create a scenario with a GET request let scenario = Scenario::builder().get("/path").build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create a buffer to write the response to let mut output = [0u8; 1024]; // Write the response let bytes_written = reply.write(&mut output).unwrap(); // Verify that some bytes were written assert!(bytes_written > 0); // Verify the response contains the expected status line let response_str = std::str::from_utf8(&output[..bytes_written]).unwrap(); assert!(response_str.starts_with("HTTP/1.1 200 OK\r\n")); } #[test] fn short_buffer() { // Create a scenario with a GET request and many headers let scenario = Scenario::builder() .get("/path") .header("header1", "value1") .header("header2", "value2") .header("header3", "value3") .header("header4", "value4") .header("header5", "value5") .build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create a very small buffer to force overflow let mut output = [0u8; 20]; // Write the response (should succeed but not write everything) let bytes_written = reply.write(&mut output).unwrap(); // Verify that some bytes were written assert!(bytes_written > 0); assert!(bytes_written <= 20); // Verify the response is not finished assert!(!reply.is_finished()); // Write more of the response let mut output2 = [0u8; 1024]; let bytes_written2 = reply.write(&mut output2).unwrap(); // Verify that more bytes were written assert!(bytes_written2 > 0); // Continue writing until finished while !reply.is_finished() { let mut output3 = [0u8; 1024]; let _ = reply.write(&mut output3).unwrap(); } // Verify the response is now finished assert!(reply.is_finished()); } #[test] fn proceed_to_send_body() { // Create a scenario with a GET request that should have a body let scenario = Scenario::builder() .get("/path") .header("content-type", "text/plain") .build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create a buffer to write the response to let mut output = [0u8; 1024]; // Write the response until finished while !reply.is_finished() { let _ = reply.write(&mut output).unwrap(); } // Proceed and verify we get SendBody variant match reply.proceed() { SendResponseResult::SendBody(_) => {} SendResponseResult::Cleanup(_) => panic!("Expected SendBody variant"), } } #[test] fn proceed_to_cleanup() { // Create a scenario with a HEAD request (should never have a body) let scenario = Scenario::builder() .head("/path") .header("content-type", "text/plain") .build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create a buffer to write the response to let mut output = [0u8; 1024]; // Write the response until finished while !reply.is_finished() { let _ = reply.write(&mut output).unwrap(); } // Proceed and verify we get Cleanup variant match reply.proceed() { SendResponseResult::Cleanup(_) => {} SendResponseResult::SendBody(_) => panic!("Expected Cleanup variant"), } } #[test] #[should_panic] fn proceed_before_finished_panics() { // Create a scenario with a GET request and many headers let scenario = Scenario::builder() .get("/path") .header("header1", "value1") .header("header2", "value2") .header("header3", "value3") .header("header4", "value4") .header("header5", "value5") .build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create a small buffer to ensure we don't finish writing let mut output = [0u8; 20]; let _ = reply.write(&mut output).unwrap(); // Verify the response is not finished assert!(!reply.is_finished()); // Proceed to SendBody (should panic) let _ = reply.proceed(); } #[test] fn buffer_overflow() { // Create a scenario with a GET request and many headers let scenario = Scenario::builder() .get("/path") .header("header1", "value1") .header("header2", "value2") .header("header3", "value3") .header("header4", "value4") .header("header5", "value5") .build(); // Get a Reply in the SendResponse state let mut reply = scenario.to_send_response(); // Create an empty buffer to force overflow let mut output = [0u8; 0]; // Write the response (should fail with OutputOverflow) let result = reply.write(&mut output); // Verify the error assert!(result.is_err()); match result.unwrap_err() { Error::OutputOverflow => {} _ => panic!("Expected OutputOverflow error"), } } algesten-ureq-proto-4182eed/src/util.rs000066400000000000000000000133001511030533500201500ustar00rootroot00000000000000use std::fmt; use std::io::{self, Cursor}; use std::ops::{Deref, DerefMut}; pub(crate) fn find_crlf(b: &[u8]) -> Option { let cr = b.iter().position(|c| *c == b'\r')?; let maybe_lf = b.get(cr + 1)?; if *maybe_lf == b'\n' { Some(cr) } else { None } } pub(crate) fn compare_lowercase_ascii(a: &str, lowercased: &str) -> bool { if a.len() != lowercased.len() { return false; } for (a, b) in a.chars().zip(lowercased.chars()) { if !a.is_ascii() { return false; } let norm = a.to_ascii_lowercase(); if norm != b { return false; } } true } pub(crate) struct Writer<'a>(pub Cursor<&'a mut [u8]>); impl<'a> Writer<'a> { pub(crate) fn new(output: &'a mut [u8]) -> Writer<'a> { Self(Cursor::new(output)) } pub fn len(&self) -> usize { self.0.position() as usize } pub fn available(&self) -> usize { self.0.get_ref().len() - self.len() } pub(crate) fn try_write(&mut self, block: impl Fn(&mut Self) -> io::Result<()>) -> bool { let pos = self.0.position(); let success = (block)(self).is_ok(); if !success { self.0.set_position(pos); } success } } impl<'a> io::Write for Writer<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { self.0.write(buf) } fn flush(&mut self) -> io::Result<()> { self.0.flush() } } const CHARS_PER_ROW: usize = 16; impl<'a> Drop for Writer<'a> { fn drop(&mut self) { let len = self.len(); log_data(&self.0.get_ref()[..len]); } } pub(crate) fn log_data(data: &[u8]) { for row in data.chunks(CHARS_PER_ROW) { trace!("{:?}", Row(row)) } } struct Row<'a>(&'a [u8]); impl<'a> fmt::Debug for Row<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for i in 0..CHARS_PER_ROW { if let Some(v) = self.0.get(i) { write!(f, "{}", HEX[*v as usize])? } else { write!(f, "--")?; } if i % 2 == 1 { write!(f, " ")?; } } write!(f, " ")?; for i in 0..CHARS_PER_ROW { if let Some(v) = self.0.get(i) { if v.is_ascii_alphanumeric() || v.is_ascii_punctuation() { write!(f, "{}", *v as char)?; } else { write!(f, ".")?; } } else { write!(f, ".")?; } } Ok(()) } } const HEX: [&str; 256] = [ "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff", ]; /// Simple impl of an array behaving like a vec. pub struct ArrayVec { len: usize, arr: [T; N], } impl Deref for ArrayVec { type Target = [T]; fn deref(&self) -> &Self::Target { &self.arr[..self.len] } } impl DerefMut for ArrayVec { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.arr[..self.len] } } impl ArrayVec { /// Construct the array. /// /// The function must produces placeholder elements of the type `T`. pub fn from_fn(cb: impl FnMut(usize) -> T) -> Self { Self { len: 0, arr: std::array::from_fn(cb), } } /// Add a value T. pub fn push(&mut self, value: T) { self.arr[self.len] = value; self.len += 1; } /// Shorten the vec. /// /// This does not drop the elements that are now unused. pub fn truncate(&mut self, len: usize) { assert!(len <= self.len); self.len = len; } } impl fmt::Debug for ArrayVec where T: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ArrayVec") .field("len", &self.len) .field("arr", &&self.arr[..self.len]) .finish() } } impl<'a, T, const N: usize> IntoIterator for &'a ArrayVec { type Item = &'a T; type IntoIter = core::slice::Iter<'a, T>; fn into_iter(self) -> Self::IntoIter { self[..self.len].iter() } } algesten-ureq-proto-4182eed/test.sh000077500000000000000000000000341511030533500173540ustar00rootroot00000000000000#!/bin/sh cargo +1.67 test