pax_global_header00006660000000000000000000000064150572460420014517gustar00rootroot0000000000000052 comment=d58cf1878ee9c95db8b2257e9d20ce2406f130a5 algesten-ureq-d58cf18/000077500000000000000000000000001505724604200147175ustar00rootroot00000000000000algesten-ureq-d58cf18/.github/000077500000000000000000000000001505724604200162575ustar00rootroot00000000000000algesten-ureq-d58cf18/.github/workflows/000077500000000000000000000000001505724604200203145ustar00rootroot00000000000000algesten-ureq-d58cf18/.github/workflows/test.yml000066400000000000000000000102241505724604200220150ustar00rootroot00000000000000on: [push, pull_request] name: CI concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} jobs: lint: name: Lint runs-on: ubuntu-latest env: RUSTFLAGS: -D warnings steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Rust id: toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Run Rustfmt run: cargo +${{steps.toolchain.outputs.name}} fmt --check - name: Run Clippy run: cargo +${{steps.toolchain.outputs.name}} clippy --all-features doc: name: Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Rust id: toolchain uses: dtolnay/rust-toolchain@stable - name: Docs env: RUSTDOCFLAGS: -Dwarnings run: cargo +${{steps.toolchain.outputs.name}} doc --no-deps --all-features --document-private-items snowflake: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Martin's snowflake formatting rules uses: algesten/snowflake@v1.0.4 with: check_diff: true build_versions: strategy: matrix: rust: [stable, beta, 1.71.1] runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v4 with: persist-credentials: false - uses: dtolnay/rust-toolchain@master id: toolchain with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 - name: Build 1 run: cargo +${{steps.toolchain.outputs.name}} build --all-features - name: Build 2 run: cargo +${{steps.toolchain.outputs.name}} build --all-features build_and_test: name: Test runs-on: ubuntu-latest strategy: matrix: feature: - "" - charset - cookies - socks-proxy - gzip - brotli - json - native-tls env: RUST_BACKTRACE: "1" RUSTFLAGS: "-D dead_code -D unused-variables -D unused" steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Rust id: toolchain uses: dtolnay/rust-toolchain@stable - name: Test run: | cargo +${{steps.toolchain.outputs.name}} test \ --no-default-features --features "_test rustls ${{ matrix.feature }}" build_without_rustls: name: Test runs-on: ubuntu-latest strategy: matrix: feature: - "" - charset - cookies - socks-proxy - gzip - brotli - json - native-tls - rustls-no-provider env: RUST_BACKTRACE: "1" RUSTFLAGS: "-D dead_code -D unused-variables -D unused" steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Rust id: toolchain uses: dtolnay/rust-toolchain@stable - name: Test run: | cargo +${{steps.toolchain.outputs.name}} test \ --no-default-features --features "_test ${{ matrix.feature }}" 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@v4 with: persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check log-level: error arguments: --all-features --target ${{ matrix.platform }} algesten-ureq-d58cf18/.gitignore000066400000000000000000000000331505724604200167030ustar00rootroot00000000000000target rls .idea/ .vscode/ algesten-ureq-d58cf18/CHANGELOG.md000066400000000000000000000437051505724604200165410ustar00rootroot00000000000000# Unreleased # 3.1.2 * Fix bug when query is after host "example.com?query" #1115 # 3.1.1 * Fix regression in MSRV (hold back native-tls) #1113 * Fix edge case regression when setting request header Content-Length: 0 #1109 # 3.1.0 DECISION: webpki-roots and webpki-root-certs goes from pre-release (0.26) to stable release (1.0.0). This is potentially a big change for ureq users. We release this as semver minor. * Bump all deps to latest #1104 * Fixes to CONNECT to follow spec #1103 * Send Content-Length for File #1100 * native-tls transport capture and surface underlying errors #1093 * Bump webpki-roots/webpki-root-certs to 1.0.0 #1089 * Bump rustls-platform-verifier to 0.6.0 #1089 * Allow the license CDLA-Permissive-2.0 #1089 # 3.0.12 * Chunked transfer handle abrupt close after 0\r\n #1074 * Fix incorrect DNS resolving when using proxies #1081 * Use + instead of %20 for url encoded form bodies #1071 * Fix problem with double-quotes in cookie values #1068 * Reduce Body size #1065 * Fix featue flag `public_suffix` for CookieStore #1063 * Improve doc for 10MB limit #1061 # 3.0.11 * Fix CONNECT proxy bug #1057 * Fix bug setting cookies in redirects #1055 * Annotated example for making a Transport #1054 * Feature for adding unencoded query parameters #1039 * Fix bug in not encoding ' in query parameters #1039 * Fix bug making timeout settings not working #1051 * Big refactor of ureq-proto #1042 * Extension trait for http::Request allowing agent config #1011 # 3.0.10 * Bump rustls-platform-verifier to 0.5.0 # 3.0.9 * Bump deps #1031 * Allow body for all methods when using http-crate API #1035 * Improved errors and doc for bespoke transports #1032 # 3.0.8 * Fix incorrect parsing bug "missing http version" #1026 # 3.0.7 * Allow non-standard HTTP methods #1018 * Fix not appending port to host header #1017 # 3.0.6 * Avoid percent encoding some query parameter chars #1004 * Fix incorrect LargeResponseHeader #1003 * Stop passing internal state in Config #996 * Support request level TlsConfig #996 # 3.0.5 * Fix incorrect reading of valid utf8 #992 * Fix panic when parsing malformed proxy URI #990 * ureq::Error wrapped as io::Error should pass through body chain #984 * send_json should set content-length header #983 # 3.0.4 * Manually unroll some macros to regular code #978 * Fix bug in `rustls-no-provider` when disabling ring #973 # 3.0.3 * Use the same data in CONNECT and Host header for proxied requests #967 * Set default scheme in proxy uri #966 * Redact URI and Location header on debug level #964 * Downgrade all logging to debug and below #964 # 3.0.2 * Remove dependency on once_cell #959 * Fix bug parsing partial redirects #958 * Expose typestate variables #956 # 3.0.1 * Fix excessive stack sizes #950 * Do not enable **json** (by default breaking, but it was a mistake) #948 # 3.0.0 * Replace RequestBuilder Deref with explicit wrappers #944 * Remove dependency on `url` crate #943 * Feature `Config::save_redirect_history` #939 # 3.0.0-rc5 * `TlsConfig::unversioned_rustls_crypto_provider()` #931 * Feature `rustls-no-provider` to compile without ring #931 * Fix CONNECT proxy Host header #936 * Re-enable CONNECT proxy support #932 * Body::content_length #927 * Handle Authorization: Basic from URI #923 * Remove many uses of Box::new() from Connector chain #919 # 3.0.0-rc4 * Default to `TooManyRedirects` error #916 * Add `ConfigBuilder::max_redirects_will_error()` #916 * Add new `SendBody::into_reader()` #914 * Fix completely broken PEM parsing #912 * Improve ergonomics for `AutoHeaderValue` #896 # 3.0.0-rc3 * Re-export ureq_proto::ArrayVec #891 * Expose typestate variables, but #[doc(hidden)] #889 * Clarify versioning and MSRV policy #887 * Get last used uri via `ResponseExt::get_uri()` #884 * Expose more things for 3rd party Transport impls #886 * Make accessor fn for `Config` and `TlsConfig` #886 * Move `Transport` and `Resolver` traits to `unversioned` #881 * Upgrade deps #885 * MSRV 1.71.1 to follow rustls #885 * Fix bug in chunked overhead calculation #880 * Make it possible to disable all automatic headers #876 * Rename `hoot` -> `ureq_proto`#872 * Fix `disable_verification` for TLS #871 * `vendored` feature flag to get vendored native-tls #866 * Fix incorrect handling of expect-100 #867 # 3.0.0-rc2 * Remove pub-field config structs in favor of builders #848 * BodyBuilder to create a response Body for test/middleware #847 * RequestBuilder::send_empty() convenience fn #846 * Rename BodyWithConfig::into_reader -> reader #845 * Escape hatch to send body for any method #857 * Reintrodice RequestBuilder::query #843 * Reintroduce RequestBuilder::query_pairs #856 * Reintroduce ResponseBuilder::send_form helper #859 * (internal) Use HeaderName for non-sensitive headers #855 * Fix broken build with rustls #832 * Reduce dependency count, platform-verifier feature #833 #818 # 3.0.0-rc1 * Ground up rewrite based on the http crate API. # 2.12.1 * Do not use multi-version deps (>=x.x.x) #907 # 2.12.0 * Bump MSRV 1.67 -> 1.71 because rustls will soon adopt it #905 * Unpin rustls dep (>=0.23.19) #905 # 2.11.0 * Fixes for changes to cargo-deny #882 * Pin rustls dep on 0.23.19 to keep MSRV 1.67 #878 * Bump MSRV 1.63 -> 1.67 due to time crate #878 * Re-export rustls #813 # 2.10.1 * default `ureq` Rustls tls config updated to avoid panic for applications that activate the default Rustls `aws-lc-rs` feature without setting a process-wide crypto provider. `ureq` will now use `*ring*` in this circumstance instead of panicking. # 2.10.0 * Bump MSRV 1.61 -> 1.63 due to rustls #764 * Update deps only patch versions (in Cargo.lock) #763 * Refork frewsxcv/rust-chunked-transfer to fix MIT/Apache2.0 license #761 * Enable http-crate feature for docs #755 * Update Rustls from 0.22 to 0.23 - this may be a breaking change if your application depends on Rustls 0.22 (e.g. to provide a custom `rustls::ClientConfig` to `ureq`). See the [Rustls 0.23.0][rustls-0.23.0] changelog for a list of breaking API changes #753 * Rustls dep to default to ring backend. If your project uses the default `ureq` TLS config, or constructs its own `rustls::ClientConfig` with `rustls::ClientConfig::builder()` you must ensure the Rustls `aws-lc-rs` feature is not activated, or set the process default cryptography provider before constructing any configs. See the Rustls [CryptoProvider][CryptoProvider] docs for more information #753 * Remove direct dep rustls-webpki #752 * Fix doc Rustls does now support IP address certificates #759 #753 [rustls-0.23.0]: https://github.com/rustls/rustls/releases/tag/v%2F0.23.0 [CryptoProvider]: https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#using-the-per-process-default-cryptoprovider # 2.9.7 * Update deps `base64` 0.22, `rustls` to 0.22.4 #747 #748 * Parse URL after middleware to enable changing it #745 * Tidy up code and fix compilation #742, 743 # 2.9.6 ## Fixed * `hootbin` is optional dep. Tests must be run with feature `testdeps` #729 * Exclude script files from cargo package #728 # 2.9.5 ## Fixed * Update deps (`cookie` 0.18, `cookie_store` 0.21, unpin `url`). #722 # 2.9.4 ## Fixed * MSRV 1.61 with CI tests # 2.9.3 ## Fixed * docs.rs docs # 2.9.2 ## Added * Replace dependency on httpbin.org for tests/doc-tests. #703 ## Fixed * Remove Header struct that never should have been exported. #696 * Update deps (rustls 0.22) #690 # 2.9.1 ## Fixed * Unbreak feature `http-interop`. This feature is version locked to http crate 0.2 * New feature `http-crate`. This feature is for http crate 1.0 * New feature `proxy-from-env` to detect proxy settings for global Agent (ureq::get) # 2.9.0 ## Fixed * Broken rustls dep (introduced new function in patch version) #677 * Doc and test fixes #670 #673 #674 ## Added * Upgraded http dep to 1.0 * http_interop to not require utf-8 headers #672 * http_interop implement conversion for `http::request::Parts` #669 # 2.8.0 ## Fixed * Fix regression in IPv6 handling #635 * Read proxy response to \r\n\r\n #620 ## Added * Auto-detect proxy from env vars (turned off by default) #649 * Conversion ureq::Response -> http::Response> #638 * cargo-deny CI action to disallow copy-left and duplicate deps #661 # 2.7.1 ## Fixed * Updated serde_json dependency constraint to be >=1.0.97 #630 # 2.7.0 ## Fixed * Pass User-Agent when connecting to proxy #597 * Proxy: Use CONNECT for HTTPS requests, but not HTTP requests #587 * Cookie headers are now cleared on redirect #608 * HTTP/1.0 responses with Content-Length no longer block until server closes the socket. #625 ## Added * Conversions to and from http::Response and http::request::Builder #591 * Updated to rustls 0.21 and rustls-webpki, which add support for IP address certificates #601 * Response::local_addr #605 # 2.6.2 ## Fixed * Non-empty connection pools were never dropped #583 # 2.6.1 ## Fixed * gzip: examine Content-Length header before removing #578 # 2.6.0 ## Added * Response::remote_addr() #489 * Request::query_pairs() - make query params from an Iterator of pairs #519 ## Fixed * Gzip responses with chunked encoding now work with connection pooling #560 * Don't panic when rustls-native-certs errors #564 * Responses with zero-length body now work with connection pooling #565 # 2.5.0 * Add tcp no_delay option #465 * Rework public TLS traits * Warn if requests aren't sent #490 * Fixes for returning stream to pool #509 * Avoid extra syscalls when content is buffered #508 * Remove dep on sync_wrapper #514 #528 * Error instead of panic on large deadlines #517 * Make ReadWrite trait simpler (used in bespoke TLS impls) #530 * Buffer short response bodies #531 * Update cookie/cookie_store dep # 2.4.0 * Enable `gzip` feature by default #455 * `gzip` and `brotli` feature flags to enable decompression #453 #421 * Middleware function on agent #448 * Agent option to preserve `Authorization` header on redirects #445 * Deprecate re-exported serde types #446 * Widen type of `send_json` to `impl Serializable` #446 * `native-tls` feature provides an alternative TLS backend #449 #391 # 2.3.2 * Re-introduce the `ureq::patch` and `agent::patch` calls. * Fix regression in 2.3.x for feature native-certs #441 # 2.3.1 * Don't panic when given an invalid DNS name #436 * Update to rustls-native-certs v0.6 #432 # 2.3.0 * Upgrade to rustls 0.20 #427 * Make test mocks of Response more accurate by removing newline #423 * Redact sensitive headers when logging prelude #414 # 2.2.0 * Update to latest dependencies * Add SOCKS4 support #410 * Downgrade logging on info level to debug #409 * Bugfix: Clear content-length header on redirect #394 #395 # 2.1.1 * Bugfix: don't reuse conns with bytes pending from server #372. This reduces Transport errors when using an Agent for connection pooling. # 2.1.0 * Bugfix: allow status lines without a reason phrase #316 * Example: "cureq" to easier make ad-hoc command line tests #330 * Override timeout per Request #335 * Bugfix: handle non-utf8 status and headers #347 and better errors #329 * Request inspection (method, url, etc) #310 #350 * Bugfix: stop percent encoding cookies #353 * Enforce cookie RFC naming/value rules #353 * Bugfix: reduce error struct size #356 # 2.0.2 * Bugfix: Apply deadline across redirects. #313 * OrAnyStatus::or_any_status ergonomic helper * Allow header lines to end with only LF #321 # 2.0.1 * Fix handling of 308 redirects (port from 1.5.4 branch) * Return UnexpectedEof instead of InvalidData on short responses. #293 * Implement std::error::Error for error::Transport. #299 # 2.0.0 * Methods that formerly returned Response now return Result. You'll need to change all instances of `.call()` to `.call()?` or handle errors using a `match` statement. * Non-2xx responses are considered Error by default. See [Error documentation] for details on how to get Response bodies for non-2xx. * Rewrite Error type. It's now an enum of two types of error: Status and Transport. Status errors (i.e. non-2xx) can be readily turned into a Response using match statements. * Errors now include the source error (e.g. errors from DNS or I/O) when appropriate, as well as the URL that caused an error. * The "synthetic error" concept is removed. * Move more configuration to Agent. Timeouts, TLS config, and proxy config now require building an Agent. * Create AgentBuilder to separate the process of building an agent from using the resulting agent. Headers can be set on an AgentBuilder, not the resulting Agent. * Agent is cheaply cloneable with an internal Arc. This makes it easy to share a single agent throughout your program. * There is now a default timeout_connect of 30 seconds. Read and write timeouts continue to be unset by default. * Add ureq::request_url and Agent::request_url, to send requests with already-parsed URLs. * Remove native_tls support. * Remove convenience methods `options(url)`, `trace(url)`, and `patch(url)`. To send requests with those verbs use `request(method, url)`. * Remove Request::build. This was a workaround because some of Request's methods took `&mut self` instead of `mut self`, and is no longer needed. You can simply delete any calls to `Request::build`. * Remove Agent::set_cookie. * Remove Header from the public API. The type wasn't used by any public methods. * Remove basic auth support. The API was incomplete. We may add back something better in the future. * Remove into_json_deserialize. Now into_json handles both serde_json::Value and other types that implement serde::Deserialize. If you were using serde_json before, you will probably have to explicitly annotate a type, like: `let v: serde_json::Value = response.into_json();`. * Rewrite README and top-level documentation. [Error documentation]: https://docs.rs/ureq/2.0.0-rc4/ureq/enum.Error.html # 2.0.0-rc4 * Remove error_on_non_2xx. #272 * Do more validation on status line. #266 * (internal) Add history to response objects #275 # 2.0.0-rc3 * Refactor Error to use an enum for easier extraction of status code errors. * (Internal) Use BufRead::read_line when reading headers. # 2.0.0-rc2 * These changes are mostly already listed under 2.0.0. * Remove the "synthetic error" concept. Methods that formerly returned Response now return Result. * Rewrite Error type. Instead of an enum, it's now a struct with an ErrorKind. This allows us to store the source error when appropriate, as well as the URL that caused an error. * Move more configuration to Agent. Timeouts, TLS config, and proxy config now require building an Agent. * Create AgentBuilder to separate the process of building an agent from using the resulting agent. Headers can be set on an AgentBuilder, not the resulting Agent. * Agent is cheaply cloneable with an internal Arc. This makes it easy to share a single agent throughout your program. * There is now a default timeout_connect of 30 seconds. Read and write timeouts continue to be unset by default. * Add ureq::request_url and Agent::request_url, to send requests with already-parsed URLs. * Remove native_tls support. * Remove convenience methods `options(url)`, `trace(url)`, and `patch(url)`. To send requests with those verbs use `request(method, url)`. * Remove Request::build. This was a workaround because some of Request's methods took `&mut self` instead of `mut self`, and is no longer needed. You can simply delete any calls to `Request::build`. * Remove Agent::set_cookie. * Remove Header from the public API. The type wasn't used by any public methods. * Remove basic auth support. The API was incomplete. We may add back something better in the future. * Remove into_json_deserialize. Now into_json handles both serde_json::Value and other types that implement serde::Deserialize. If you were using serde_json before, you will probably have to explicitly annotate a type, like: `let v: serde_json::Value = response.into_json();`. * Rewrite README and top-level documentation. # 1.5.2 * Remove 'static constraint on Request.send(), allowing a wider variety of types to be passed. Also eliminate some copying. #205 * Allow turning a Response into an Error #214 * Update env_logger to 0.8.1 #195 * Remove convenience method for CONNECT verb #177 * Fix bugs in handling of timeout_read #197 and #198 # 1.5.1 * Use cookie_store crate for correct cookie handling #169 * Fix bug in picking wrong host for redirects introduced in 1.5.0 #180 * Allow proxy settings on Agent #178 # 1.5.0 * Add pluggable name resolution. Users can now override the IP addresses for hostnames of their choice #148 * bugfix: Don't re-pool streams on drop. This would occur if the user called `response.into_reader()` and dropped the resulting `Read` before reading all the way to EOF. The result would be a BadStatus error on the next request to the same hostname. This only affected users using an explicit Agent #160 * Automatically set Transfer-Encoding: chunked when using `send` #86 * `into_reader()` now returns `impl Read + Send` instead of `impl Read` #156 * Add support for log crate #170 * Retry broken connections in more cases (should reduce BadStatus errors) #168 # 1.4.1 * Use buffer to avoid byte-by-byte parsing result in multiple syscalls. * Allow pooling multiple connections per host. * Put version in user agent "ureq/1.4.1". # 1.4.0 * Propagate WouldBlock in io:Error for Response::to_json. * Merge multiple cookies into one header to be spec compliant. * Allow setting TLSConnector for native-tls. * Remove brackets against TLS lib when IPv6 addr is used as hostname. * Include proxy in connection pool keys. * Stop assuming localhost for URLs without host part. * Error if body length is less than content-length. * Validate header names. # 1.3.0 * Changelog start algesten-ureq-d58cf18/CONTRIBUTING.md000066400000000000000000000011751505724604200171540ustar00rootroot00000000000000## License Copyright (c) 2019 Martin Algesten Licensed under either of * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed under the Apache License, Version 2.0, and the MIT license, without any additional terms or conditions. See LICENSE-APACHE and LICENSE-MIT for details. algesten-ureq-d58cf18/Cargo.lock000066400000000000000000001340171505724604200166320ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "assert_no_alloc" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ca83137a482d61d916ceb1eba52a684f98004f18e0cafea230fe5579c178a3" [[package]] name = "auto-args" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40797fd8889b7625595cb391d6f8428802b65c22023ada3dc08d67953ac5e5e2" dependencies = [ "auto-args-derive", ] [[package]] name = "auto-args-derive" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d4cbc684c4d4c80b06cd1009b7b78fe5a910187d1daeda1cc04319207b5894f" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", "syn 0.15.44", ] [[package]] name = "aws-lc-rs" version = "1.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" dependencies = [ "aws-lc-sys", "zeroize", ] [[package]] name = "aws-lc-sys" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools", "lazy_static", "lazycell", "log", "prettyplease", "proc-macro2 1.0.94", "quote 1.0.40", "regex", "rustc-hash", "shlex", "syn 2.0.100", "which", ] [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "brotli-decompressor" version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[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 = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "cmake" version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "cookie" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "percent-encoding", "time", "version_check", ] [[package]] name = "cookie_store" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" dependencies = [ "cookie", "document-features", "idna", "indexmap", "log", "serde", "serde_derive", "serde_json", "time", "url", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "pem-rfc7468", "zeroize", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "document-features" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" dependencies = [ "litrs", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[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 = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "jni" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", "windows-sys 0.45.0", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "native-tls" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", "security-framework 2.11.1", "security-framework-sys", "tempfile", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "once_cell" version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "openssl" version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags", "cfg-if", "foreign-types", "libc", "once_cell", "openssl-macros", "openssl-sys", ] [[package]] name = "openssl-macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" version = "300.4.2+3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" dependencies = [ "cc", ] [[package]] name = "openssl-sys" version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", "openssl-src", "pkg-config", "vcpkg", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" dependencies = [ "proc-macro2 1.0.94", "syn 2.0.100", ] [[package]] name = "proc-macro2" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ "unicode-xid", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ "proc-macro2 0.4.30", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2 1.0.94", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", "getrandom", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustls" version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", "security-framework 3.2.0", ] [[package]] name = "rustls-pemfile" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ "rustls-pki-types", ] [[package]] name = "rustls-pki-types" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "zeroize", ] [[package]] name = "rustls-platform-verifier" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" dependencies = [ "core-foundation 0.10.0", "core-foundation-sys", "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", "security-framework 3.2.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", ] [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "schannel" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags", "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "serde_json" version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socks" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" dependencies = [ "byteorder", "libc", "winapi", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ "proc-macro2 0.4.30", "quote 0.6.13", "unicode-xid", ] [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "tempfile" version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] [[package]] name = "time" version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-xid" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" version = "3.1.2" dependencies = [ "assert_no_alloc", "auto-args", "base64", "brotli-decompressor", "cookie_store", "der", "encoding_rs", "env_logger", "flate2", "log", "native-tls", "percent-encoding", "rustls", "rustls-pemfile", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "socks", "ureq-proto", "url", "utf-8", "webpki-root-certs", "webpki-roots", ] [[package]] name = "ureq-proto" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ "base64", "http", "httparse", "log", ] [[package]] name = "url" version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", ] [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "webpki-root-certs" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] [[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", "home", "once_cell", "rustix", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", "synstructure", ] [[package]] name = "zerofrom" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", "synstructure", ] [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", "syn 2.0.100", ] algesten-ureq-d58cf18/Cargo.toml000066400000000000000000000105511505724604200166510ustar00rootroot00000000000000[package] name = "ureq" version = "3.1.2" authors = ["Martin Algesten ", "Jacob Hoffman-Andrews "] description = "Simple, safe HTTP client" license = "MIT OR Apache-2.0" repository = "https://github.com/algesten/ureq" readme = "README.md" keywords = ["web", "request", "https", "http", "client"] categories = ["web-programming::http-client"] edition = "2018" exclude = ["/cargo_deny.sh", "/deny.toml", "/test.sh"] # MSRV rust-version = "1.71.1" [package.metadata.docs.rs] features = ["rustls", "platform-verifier", "native-tls", "socks-proxy", "cookies", "gzip", "brotli", "charset", "json", "_test", "_doc"] [features] default = ["rustls", "gzip"] ######## SUPPORTED FEATURES rustls = ["rustls-no-provider", "_ring"] native-tls = ["dep:native-tls", "dep:der", "_tls", "dep:webpki-root-certs"] platform-verifier = ["dep:rustls-platform-verifier"] socks-proxy = ["dep:socks"] cookies = ["dep:cookie_store", "_url"] gzip = ["dep:flate2"] brotli = ["dep:brotli-decompressor"] charset = ["dep:encoding_rs"] json = ["dep:serde", "dep:serde_json", "cookie_store?/serde_json"] ######## UNSTABLE FEATURES. # Might be removed or changed in a minor version. # Rustls CryptoProviders are not picked up from feature flags alone. They must be # configured on Agent. This feature flag makes it possible to compile ureq with # rustls, but without ring. rustls-no-provider = ["dep:rustls", "_tls", "dep:webpki-roots", "_rustls"] # Supported as long as native-tls supports this. vendored = ["native-tls?/vendored"] ######## INTERNAL FEATURES. DO NOT USE. # Ring has a higher chance of compiling cleanly without additional developer environment. # Supported as long as rustls supports this. _ring = ["rustls?/ring"] _url = ["dep:url"] _tls = ["dep:rustls-pemfile", "dep:rustls-pki-types"] _test = [] _rustls = [] _doc = ["rustls?/aws-lc-rs"] [dependencies] base64 = "0.22.1" ureq-proto = { version = "0.5.2", default-features = false, features = ["client"] } # ureq-proto = { path = "../ureq-proto", default-features = false, features = ["client"] } log = "0.4.25" utf-8 = "0.7.6" percent-encoding = "2.3.1" # These are used regardless of TLS implementation. rustls-pemfile = { version = "2.1.2", optional = true, default-features = false, features = ["std"] } rustls-pki-types = { version = "1.11.0", optional = true, default-features = false, features = ["std"] } rustls-platform-verifier = { version = "0.6.0", optional = true, default-features = false } webpki-roots = { version = "1.0.0", optional = true, default-features = false } webpki-root-certs = { version = "1.0.0", optional = true, default-features = false } rustls = { version = "0.23.22", optional = true, default-features = false, features = ["logging", "std", "tls12"] } # held back on 0.2.12 to avoid double dependency on windows-sys (0.59.0, 0.52.0) # native-tls 0.2.13 requires MSRV 1.82. We keep 0.2.12 until we decide to bunp MSRV native-tls = { version = "0.2.12", optional = true, default-features = false } der = { version = "0.7.9", optional = true, default-features = false, features = ["pem", "std"] } socks = { version = "0.3.4", optional = true } # cookie_store uses Url, while http-crate has its own Uri. # Keep url crate in lockstep with cookie_store. cookie_store = { version = "0.22", optional = true, default-features = false, features = ["preserve_order"] } # ureq-proto forces url=2.5.4. This optional dep documents the situation in cookie_store. url = { version = "2.3.1", optional = true, default-features = false } flate2 = { version = "1.0.30", optional = true } brotli-decompressor = { version = "5.0.0", optional = true } encoding_rs = { version = "0.8.34", optional = true } serde = { version = "1.0.138", optional = true, default-features = false, features = ["std"] } serde_json = { version = "1.0.120", optional = true, default-features = false, features = ["std"] } [dev-dependencies] env_logger = "0.11.7" auto-args = "0.3.0" serde = { version = "1.0.204", features = ["std", "derive"] } assert_no_alloc = "1.1.2" # Enable aws-lc-rs for tests so we can demonstrate using ureq without compiling ring. rustls = { version = "0.23", features = ["aws-lc-rs"] } [[example]] name = "cureq" required-features = ["rustls", "native-tls", "socks-proxy", "cookies", "gzip", "brotli", "charset"] [[example]] name = "mpsc-transport" required-features = ["rustls"] [[example]] name = "proxy" required-features = ["rustls"] algesten-ureq-d58cf18/LICENSE-APACHE000066400000000000000000000261351505724604200166520ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. algesten-ureq-d58cf18/LICENSE-MIT000066400000000000000000000020601505724604200163510ustar00rootroot00000000000000MIT License Copyright (c) 2019 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-d58cf18/MIGRATE-2-to-3.md000066400000000000000000000051541505724604200172550ustar00rootroot00000000000000# Changes ureq 2.x -> 3.x This is not an exhaustive list of changes. Most tweaks to the API are clear by looking at the docs. If anything is unclear, please open a PR and we can clarify further. ## Rewrite ureq 3.x is a ground up complete rewrite of ureq 2.x. The HTTP protocol is re-engineered to a Sans-IO style implementation living in the `ureq-proto` crate. Both protocol and ureq main crate remain `#![forbid(unsafe_code)]`. The goals of the project remain largely the same: A simple, sync, HTTP/1.1 client with a minimum number of dependencies. With Sans-IO the user can now implement their own `Transport` thus providing alternative TLS or non-socket based communication in crates mainitained outside the ureq project. The same goes for `Resolver`. ## HTTP Crate In 2.x ureq implemented it's own `Request` and `Response` structs. In 3.x, we drop our own impl in favor of the [http crate]. The http crate presents a unified HTTP API and can be found as a dependency of a number of big [http-related crates] in the Rust ecosystem. The idea is that presenting a well-known API towards users of ureq will make it easier to use. ## Re-exported crates must be semver 1.x (stable) ureq2.x re-exported a number of semver 0.x crates and thus suffered from that breaking changes in those crates technically were breaking changes in ureq (and thus ought to increase major version). In ureq 3.x we will strive to re-export as few crates as possible. * No re-exported tls config * No re-exported cookie crates * No re-exported json macro Instead we made our own TLS config and Cookie API, and drop the json macro. ## No retry idempotent ureq 2.x did an automatic retry of idempotent methods (GET, HEAD). This was considered confusing, so 3.x has no built-in retries. ## No send body charset For now, ureq 3.x can't change the charset of a send body. It can however still do that for the response body. [http crate]: https://crates.io/crates/http [http-related crates]: https://crates.io/crates/http/reverse_dependencies ## Features - `proxy-from-env` is the default now. CONNECT-proxy needs no extra feature flag, but `socks-proxy` does. - `native-certs` is built-in. In the `TlsConfig`, which you can set on agent or request level, you have three choices via the [`RootCerts`](https://docs.rs/ureq/3.0.6/ureq/tls/enum.RootCerts.html#variant.PlatformVerifier) enum. `Specific` when you want to set the root certs yourself, `PlatformVerifier`, which for rustls delegates to the system, and for native-tls means using the root certs native-tls is picking up (this is what you want), and finally `WebPki`, which uses the root certs bundled with ureq.algesten-ureq-d58cf18/README.md000066400000000000000000000440551505724604200162060ustar00rootroot00000000000000[comment]: # (README.md is autogenerated from src/lib.rs by `cargo readme > README.md`) # ureq A simple, safe HTTP client. Ureq's first priority is being easy for you to use. It's great for anyone who wants a low-overhead HTTP client that just gets the job done. Works very well with HTTP APIs. Its features include cookies, JSON, HTTP proxies, HTTPS, charset decoding, and is based on the API of the `http` crate. Ureq is in pure Rust for safety and ease of understanding. It avoids using `unsafe` directly. It uses blocking I/O instead of async I/O, because that keeps the API simple and keeps dependencies to a minimum. For TLS, ureq uses rustls or native-tls. See the [changelog] for details of recent releases. [changelog]: https://github.com/algesten/ureq/blob/main/CHANGELOG.md ## Usage In its simplest form, ureq looks like this: ```rust let body: String = ureq::get("http://example.com") .header("Example-Header", "header value") .call()? .body_mut() .read_to_string()?; ``` For more involved tasks, you'll want to create an [`Agent`]. An Agent holds a connection pool for reuse, and a cookie store if you use the **cookies** feature. An Agent can be cheaply cloned due to internal [`Arc`] and all clones of an Agent share state among each other. Creating an Agent also allows setting options like the TLS configuration. ```rust use ureq::Agent; use std::time::Duration; let mut config = Agent::config_builder() .timeout_global(Some(Duration::from_secs(5))) .build(); let agent: Agent = config.into(); let body: String = agent.get("http://example.com/page") .call()? .body_mut() .read_to_string()?; // Reuses the connection from previous request. let response: String = agent.put("http://example.com/upload") .header("Authorization", "example-token") .send("some body data")? .body_mut() .read_to_string()?; ``` ### JSON Ureq supports sending and receiving json, if you enable the **json** feature: ```rust use serde::{Serialize, Deserialize}; #[derive(Serialize)] struct MySendBody { thing: String, } #[derive(Deserialize)] struct MyRecvBody { other: String, } let send_body = MySendBody { thing: "yo".to_string() }; // Requires the `json` feature enabled. let recv_body = ureq::post("http://example.com/post/ingest") .header("X-My-Header", "Secret") .send_json(&send_body)? .body_mut() .read_json::()?; ``` ### Error handling ureq returns errors via `Result`. That includes I/O errors, protocol errors. By default, also HTTP status code errors (when the server responded 4xx or 5xx) results in [`Error`]. This behavior can be turned off via [`http_status_as_error()`] ```rust use ureq::Error; match ureq::get("http://mypage.example.com/").call() { Ok(response) => { /* it worked */}, Err(Error::StatusCode(code)) => { /* the server returned an unexpected status code (such as 400, 500 etc) */ } Err(_) => { /* some kind of io/transport/etc error */ } } ``` ## Features To enable a minimal dependency tree, some features are off by default. You can control them when including ureq as a dependency. `ureq = { version = "3", features = ["socks-proxy", "charset"] }` The default enabled features are: **rustls** and **gzip**. * **rustls** enables the rustls TLS implementation. This is the default for the the crate level convenience calls (`ureq::get` etc). It currently uses `ring` as the TLS provider. * **native-tls** enables the native tls backend for TLS. Due to the risk of diamond dependencies accidentally switching on an unwanted TLS implementation, `native-tls` is never picked up as a default or used by the crate level convenience calls (`ureq::get` etc) – it must be configured on the agent * **platform-verifier** enables verifying the server certificates using a method native to the platform ureq is executing on. See [rustls-platform-verifier] crate * **socks-proxy** enables proxy config using the `socks4://`, `socks4a://`, `socks5://` and `socks://` (equal to `socks5://`) prefix * **cookies** enables cookies * **gzip** enables requests of gzip-compressed responses and decompresses them * **brotli** enables requests brotli-compressed responses and decompresses them * **charset** enables interpreting the charset part of the Content-Type header (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the library defaults to Rust's built in `utf-8` * **json** enables JSON sending and receiving via serde_json #### Unstable These features are unstable and might change in a minor version. * **rustls-no-provider** Enables rustls, but does not enable any [`CryptoProvider`] such as `ring`. Providers other than the default (currently `ring`) are never picked up from feature flags alone. It must be configured on the agent. * **vendored** compiles and statically links to a copy of non-Rust vendors (e.g. OpenSSL from `native-tls`) ## TLS (https) ### rustls By default, ureq uses [`rustls` crate] with the `ring` cryptographic provider. As of Sep 2024, the `ring` provider has a higher chance of compiling successfully. If the user installs another process [default provider], that choice is respected. ureq does not guarantee to default to ring indefinitely. `rustls` as a feature flag will always work, but the specific crypto backend might change in a minor version. ```rust // This uses rustls ureq::get("https://www.google.com/").call().unwrap(); ``` #### rustls without ring ureq never changes TLS backend from feature flags alone. It is possible to compile ureq without ring, but it requires specific feature flags and configuring the [`Agent`]. Since rustls is not semver 1.x, this requires non-semver-guaranteed API. I.e. ureq might change this behavior without a major version bump. Read more at [`TlsConfigBuilder::unversioned_rustls_crypto_provider`][crate::tls::TlsConfigBuilder::unversioned_rustls_crypto_provider]. ### native-tls As an alternative, ureq ships with [`native-tls`] as a TLS provider. This must be enabled using the **native-tls** feature. Due to the risk of diamond dependencies accidentally switching on an unwanted TLS implementation, `native-tls` is never picked up as a default or used by the crate level convenience calls (`ureq::get` etc) – it must be configured on the agent. ```rust use ureq::config::Config; use ureq::tls::{TlsConfig, TlsProvider}; let mut config = Config::builder() .tls_config( TlsConfig::builder() // requires the native-tls feature .provider(TlsProvider::NativeTls) .build() ) .build(); let agent = config.new_agent(); agent.get("https://www.google.com/").call().unwrap(); ``` ### Root certificates #### webpki-roots By default, ureq uses Mozilla's root certificates via the [webpki-roots] crate. This is a static bundle of root certificates that do not update automatically. It also circumvents whatever root certificates are installed on the host running ureq, which might be a good or a bad thing depending on your perspective. There is also no mechanism for [SCT], [CRL]s or other revocations. To maintain a "fresh" list of root certs, you need to bump the ureq dependency from time to time. The main reason for chosing this as the default is to minimize the number of dependencies. More details about this decision can be found at [PR 818]. If your use case for ureq is talking to a limited number of servers with high trust, the default setting is likely sufficient. If you use ureq with a high number of servers, or servers you don't trust, we recommend using the platform verifier (see below). #### platform-verifier The [rustls-platform-verifier] crate provides access to natively checking the certificate via your OS. To use this verifier, you need to enable it using feature flag **platform-verifier** as well as configure an agent to use it. ```rust use ureq::Agent; use ureq::tls::{TlsConfig, RootCerts}; let agent = Agent::config_builder() .tls_config( TlsConfig::builder() .root_certs(RootCerts::PlatformVerifier) .build() ) .build() .new_agent(); let response = agent.get("https://httpbin.org/get").call()?; ``` Setting `RootCerts::PlatformVerifier` together with `TlsProvider::NativeTls` means also native-tls will use the OS roots instead of [webpki-roots] crate. Whether that results in a config that has CRLs and revocations is up to whatever native-tls links to. ## JSON By enabling the **json** feature, the library supports serde json. This is enabled by default. * [`request.send_json()`] send body as json. * [`body.read_json()`] transform response to json. ## Sending body data HTTP/1.1 has two ways of transfering body data. Either of a known size with the `Content-Length` HTTP header, or unknown size with the `Transfer-Encoding: chunked` header. ureq supports both and will use the appropriate method depending on which body is being sent. ureq has a [`AsSendBody`] trait that is implemented for many well known types of data that we might want to send. The request body can thus be anything from a `String` to a `File`, see below. ### Content-Length The library will send a `Content-Length` header on requests with bodies of known size, in other words, if the body to send is one of: * `&[u8]` * `&[u8; N]` * `&str` * `String` * `&String` * `Vec` * `&Vec)` * [`SendBody::from_json()`] (implicitly via [`request.send_json()`]) ### Transfer-Encoding: chunked ureq will send a `Transfer-Encoding: chunked` header on requests where the body is of unknown size. The body is automatically converted to an [`std::io::Read`] when the type is one of: * `File` * `&File` * `TcpStream` * `&TcpStream` * `Stdin` * `UnixStream` (not on windows) #### From readers The chunked method also applies for bodies constructed via: * [`SendBody::from_reader()`] * [`SendBody::from_owned_reader()`] ### Proxying a response body As a special case, when ureq sends a [`Body`] from a previous http call, the use of `Content-Length` or `chunked` depends on situation. For input such as gzip decoding (**gzip** feature) or charset transformation (**charset** feature), the output body might not match the input, which means ureq is forced to use the `chunked` method. * `Response` ### Sending form data [`request.send_form()`] provides a way to send `application/x-www-form-urlencoded` encoded data. The key/values provided will be URL encoded. ### Overriding If you set your own Content-Length or Transfer-Encoding header before sending the body, ureq will respect that header by not overriding it, and by encoding the body or not, as indicated by the headers you set. ```rust let resp = ureq::put("https://httpbin.org/put") .header("Transfer-Encoding", "chunked") .send("Hello world")?; ``` ## Character encoding By enabling the **charset** feature, the library supports receiving other character sets than `utf-8`. For [`Body::read_to_string()`] we read the header like: `Content-Type: text/plain; charset=iso-8859-1` and if it contains a charset specification, we try to decode the body using that encoding. In the absence of, or failing to interpret the charset, we fall back on `utf-8`. Currently ureq does not provide a way to encode when sending request bodies. ### Lossy utf-8 When reading text bodies (with a `Content-Type` starting `text/` as in `text/plain`, `text/html`, etc), ureq can ensure the body is possible to read as a `String` also if it contains characters that are not valid for utf-8. Invalid characters are replaced with a question mark `?` (NOT the utf-8 replacement character). For [`Body::read_to_string()`] this is turned on by default, but it can be disabled and conversely for [`Body::as_reader()`] it is not enabled, but can be. To precisely configure the behavior use [`Body::with_config()`]. ## Proxying ureq supports two kinds of proxies, [`HTTP`] ([`CONNECT`]), [`SOCKS4`]/[`SOCKS5`], the former is always available while the latter must be enabled using the feature **socks-proxy**. Proxies settings are configured on an [`Agent`]. All request sent through the agent will be proxied. ### Example using HTTP ```rust use ureq::{Agent, Proxy}; // Configure an http connect proxy. let proxy = Proxy::new("http://user:password@cool.proxy:9090")?; let agent: Agent = Agent::config_builder() .proxy(Some(proxy)) .build() .into(); // This is proxied. let resp = agent.get("http://cool.server").call()?; ``` ### Example using SOCKS5 ```rust use ureq::{Agent, Proxy}; // Configure a SOCKS proxy. let proxy = Proxy::new("socks5://user:password@cool.proxy:9090")?; let agent: Agent = Agent::config_builder() .proxy(Some(proxy)) .build() .into(); // This is proxied. let resp = agent.get("http://cool.server").call()?; ``` ## Log levels ureq uses the log crate. These are the definitions of the log levels, however we do not guarantee anything for dependencies such as `http` and `rustls`. * `ERROR` - nothing * `WARN` - if we detect a user configuration problem. * `INFO` - nothing * `DEBUG` - uri, state changes, transport, resolver and selected request/response headers * `TRACE` - wire level debug. NOT REDACTED! The request/response headers on DEBUG levels are allow-listed to only include headers that are considered safe. The code has the [allow list](https://github.com/algesten/ureq/blob/81127cfc38516903330dc1b9c618122372f8dc29/src/util.rs#L184-L198). ## Versioning ### Semver and `unversioned` ureq follows semver. From ureq 3.x we strive to have a much closer adherence to semver than 2.x. The main mistake in 2.x was to re-export crates that were not yet semver 1.0. In ureq 3.x TLS and cookie configuration is shimmed using our own types. ureq 3.x is trying out two new traits that had no equivalent in 2.x, [`Transport`] and [`Resolver`]. These allow the user write their own bespoke transports and (DNS name) resolver. The API:s for these parts are not yet solidified. They live under the [`unversioned`] module, and do not follow semver. See module doc for more info. ### Breaking changes in dependencies ureq relies on non-semver 1.x crates such as `rustls` and `native-tls`. Some scenarios, such as configuring `rustls` to not use `ring`, a user of ureq might need to interact with these crates directly instead of going via ureq's provided API. Such changes can break when ureq updates dependencies. This is not considered a breaking change for ureq and will not be reflected by a major version bump. We strive to mark ureq's API with the word "unversioned" to identify places where this risk arises. ### Minimum Supported Rust Version (MSRV) From time to time we will need to update our minimum supported Rust version (MSRV). This is not something we do lightly; our ambition is to be as conservative with MSRV as possible. * For some dependencies, we will opt for pinning the version of the dep instead of bumping our MSRV. * For important dependencies, like the TLS libraries, we cannot hold back our MSRV if they change. * We do not consider MSRV changes to be breaking for the purposes of semver. * We will not make MSRV changes in patch releases. * MSRV changes will get their own minor release, and not be co-mingled with other changes. [`HTTP`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling#http_tunneling [`CONNECT`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT [`SOCKS4`]: https://en.wikipedia.org/wiki/SOCKS#SOCKS4 [`SOCKS5`]: https://en.wikipedia.org/wiki/SOCKS#SOCKS5 [`rustls` crate]: https://crates.io/crates/rustls [default provider]: https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#method.install_default [`native-tls`]: https://crates.io/crates/native-tls [rustls-platform-verifier]: https://crates.io/crates/rustls-platform-verifier [webpki-roots]: https://crates.io/crates/webpki-roots [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html [`Agent`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Agent.html [`Error`]: https://docs.rs/ureq/3.0.0-rc4/ureq/enum.Error.html [`http_status_as_error()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/config/struct.ConfigBuilder.html#method.http_status_as_error [SCT]: https://en.wikipedia.org/wiki/Certificate_Transparency [CRL]: https://en.wikipedia.org/wiki/Certificate_revocation_list [PR818]: https://github.com/algesten/ureq/pull/818 [`request.send_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.RequestBuilder.html#method.send_json [`body.read_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.read_json [`AsSendBody`]: https://docs.rs/ureq/3.0.0-rc4/ureq/trait.AsSendBody.html [`SendBody::from_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_json [`std::io::Read`]: https://doc.rust-lang.org/std/io/trait.Read.html [`SendBody::from_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_reader [`SendBody::from_owned_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_owned_reader [`Body`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html [`request.send_form()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.RequestBuilder.html#method.send_form [`Body::read_to_string()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.read_to_string [`Body::as_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.as_reader [`Body::with_config()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.with_config [`Transport`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/transport/trait.Transport.html [`Resolver`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/resolver/trait.Resolver.html [`unversioned`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/index.html [`CryptoProvider`]: https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html algesten-ureq-d58cf18/README.tpl000066400000000000000000000001611505724604200163730ustar00rootroot00000000000000[comment]: # (README.md is autogenerated from src/lib.rs by `cargo readme > README.md`) # {{crate}} {{readme}} algesten-ureq-d58cf18/RELEASE.txt000066400000000000000000000024601505724604200165420ustar00rootroot00000000000000Releasing ureq ============== 1. UPDATE CHANGELOG Ensure the changelog is updated. Use git history to highlight the main changes, especially API changes. Smaller things can be omitted. Make a PR for changelog if there is time. 2. CHECK OUTDATED DEPS Quickly scan whether we can bump some dep. Use `cargo install cargo-outdated` as a helper to find them. cargo update cargo outdated --depth=1 The initial update is just to ensure your checkout is using the latest deps allowed by Cargo.toml already. Outdated deps doesn't _have_ to make the release, use your judgement. Make a PR for outdated deps if there is time. 3. UPDATE Cargo.toml VERSION We follow semver. Bug fixes bump patch version, API changes bump minor version. Cargo bump is a helper to update the version in Cargo.toml. `cargo install cargo-bump` cargo bump patch Git commit Cargo.toml and push straight into master. 3. GIT TAG Each release has a corresponding git tag. For version `1.2.3` there would be a `git tag 1.2.3`. The tag should point to the bump commit pushed in 3. Do the tag and git push --tags. 4. WAIT FOR CI Both the push to master and following git tag will cause Github CI to run. Wait for both runs to complete to ensure we have a "good version". 5. PUBLISH Publish the release to crates.io. cargo publish algesten-ureq-d58cf18/cargo_deny.sh000077500000000000000000000025431505724604200173740ustar00rootroot00000000000000#!/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-d58cf18/deny.toml000066400000000000000000000121271505724604200165560ustar00rootroot00000000000000# 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" }, ] # 2025-03-13: We get double windows-sys crates in our build deps due to aws-lc-rs/bindgen # The dep all other crates use is 0.59.0. Ideally we don't want to ignore this, but # for now it appears to be the only solution. # 2025-08-17: The situation is getting worse, not better. We have 4 different versions # now via various dependencies (previously 2). # # windows-sys v0.45.0 # └── jni v0.21.1 # └── rustls-platform-verifier v0.6.1 # └── ureq v3.0.12 (/Users/martin/dev/ureq) # # windows-sys v0.52.0 # └── ring v0.17.14 # ├── rustls v0.23.31 # │ ├── rustls-platform-verifier v0.6.1 # # windows-sys v0.59.0 # ├── rustix v0.38.44 # │ ├── tempfile v3.15.0 # │ │ ├── native-tls v0.2.12 # │ │ │ └── ureq v3.0.12 (/Users/martin/dev/ureq) # │ │ └── ureq v3.0.12 (/Users/martin/dev/ureq) # 2025-03-17: security-framework differs between native-tls and rustls-platform-verifier. # Hopefully only very few people end up using both rustls and native-tls at # the same time. # 2025-08-17: This is still the case. Different versions, same situation. # # = security-framework v2.11.1 # └── native-tls v0.2.14 # └── ureq v3.0.9 # = security-framework v3.2.0 # └── rustls-platform-verifier v0.5.1 # └── ureq v3.0.9 # # 2025-08-17: getrandom is bumped in other crates, but ring holds it back. # See https://github.com/briansmith/ring/issues/2341 # # ├ getrandom v0.2.16 # └── ring v0.17.14 # ├── rustls v0.23.31 # │ ├── rustls-platform-verifier v0.6.1 # │ │ └── ureq v3.0.12 # ├ getrandom v0.3.3 # └── tempfile v3.20.0 # └── native-tls v0.2.14 # └── ureq v3.0.12 exclude = ["windows-sys", "security-framework", "getrandom"] [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 = [ { name = "bitflags" }, # Unfortunate duplicate dependency due to old version beeing pulled in by `security-framework` ] 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://www.unicode.org/license.txt "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) "CDLA-Permissive-2.0", # https://cdla.dev/permissive-2-0/ ] [[licenses.clarify]] name = "webpki" expression = "ISC" license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] [[licenses.clarify]] name = "ring" expression = "MIT AND ISC AND OpenSSL" license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] algesten-ureq-d58cf18/examples/000077500000000000000000000000001505724604200165355ustar00rootroot00000000000000algesten-ureq-d58cf18/examples/cureq.rs000066400000000000000000000026601505724604200202260ustar00rootroot00000000000000use std::io::{BufRead, BufReader}; use std::process; use std::time::Duration; use auto_args::AutoArgs; use ureq::tls::TlsConfig; use ureq::Agent; #[derive(Debug, AutoArgs)] struct Opt { /// Print headers include: Option, /// Timeout for entire request (seconds) max_time: Option, /// Disable certificate verification insecure: Option, /// URL to request url: String, } fn main() { env_logger::init(); let opt = Opt::from_args(); if let Err(e) = run(&opt) { eprintln!("{} - {}", e, opt.url); process::exit(1); } } fn run(opt: &Opt) -> Result<(), ureq::Error> { let agent: Agent = Agent::config_builder() .timeout_global(opt.max_time.map(|t| Duration::from_secs(t.into()))) .tls_config( TlsConfig::builder() .disable_verification(opt.insecure.unwrap_or(false)) .build(), ) .build() .into(); let mut res = agent.get(&opt.url).call()?; if opt.include.unwrap_or(false) { eprintln!("{:#?}", res.headers()); } const MAX_BODY_SIZE: u64 = 5 * 1024 * 1024; let reader = BufReader::new(res.body_mut().with_config().limit(MAX_BODY_SIZE).reader()); let lines = reader.lines(); for r in lines { let line = match r { Ok(v) => v, Err(e) => return Err(e.into()), }; println!("{}", line); } Ok(()) } algesten-ureq-d58cf18/examples/mpsc-transport.rs000066400000000000000000000157421505724604200221100ustar00rootroot00000000000000use std::sync::{mpsc, Arc, Mutex}; use std::thread; use std::time::Duration; use ureq::config::Config; use ureq::unversioned::resolver::DefaultResolver; use ureq::unversioned::transport::{Buffers, ConnectionDetails, Connector, LazyBuffers}; use ureq::unversioned::transport::{NextTimeout, RustlsConnector, Transport}; use ureq::{Agent, Error}; pub fn main() { // To see some inner workings of ureq, this example can be interesting to run with: // RUST_LOG=trace cargo run --example mpsc-transport env_logger::init(); // Our very own connector. let connector = MpscConnector::default(); // Spawn a fake server in a thread. We take the server_side TxRx to be able to // communicate with the client. let server_side = connector.server_side.clone(); thread::spawn(|| run_server(server_side)); let chain = connector // For educational purposes, we wrap add the RustlsConnector to the chain. // This would mean an URL that starts with https:// will be wrapped in TLS // letting our MpscConnector handle the "underlying" transport // with TLS on top. // // This example does not use an https URL since that would require us // to terminate the TLS in the server side, which is more involved than // this example is trying to show. .chain(RustlsConnector::default()); // Use default config and resolver. let config = Config::default(); let resolver = DefaultResolver::default(); // Construct an agent from the parts let agent = Agent::with_parts(config, chain, resolver); // This request will use the MpscTransport to communicate with the fake server. // If you change this to "https", the RustlsConnector will be used, but the // fake server is not made to handle TLS. let mut res = agent.get("http://example.com").call().unwrap(); println!( "CLIENT got response: {:?}", res.body_mut().read_to_string().unwrap() ); } #[derive(Debug, Default)] pub struct MpscConnector { server_side: Arc>>, } impl Connector for MpscConnector { type Out = MpscTransport; fn connect( &self, details: &ConnectionDetails, _: Option, ) -> Result, Error> { println!( "Making an mpsc connection to {:?} (with addrs: {:?})", details.uri, // The default resolver does resolve this to some IP addresses. &details.addrs[..] ); let (txrx1, txrx2) = TxRx::pair(); let transport = MpscTransport::new(txrx1, 1024, 1024); // This is how we pass the server_side TxRx to the server thread. // A more realistic example would not do this. { let mut server_side = self.server_side.lock().unwrap(); *server_side = Some(txrx2); } Ok(Some(transport)) } } /// A pair of channels for transmitting and receiving data. /// /// These will be connected to another such pair. #[derive(Debug)] pub struct TxRx { tx: mpsc::SyncSender>, // The Mutex here us unfortunate for this example since we are not using rx in // a "Sync way", but we also don't want to make an unsafe impl Sync to risk // having the repo flagged as unsafe by overzealous compliance tools. rx: Mutex>>, alive: bool, } impl TxRx { pub fn pair() -> (TxRx, TxRx) { let (tx1, rx1) = mpsc::sync_channel(10); let (tx2, rx2) = mpsc::sync_channel(10); (TxRx::new(tx1, rx2), TxRx::new(tx2, rx1)) } fn new(tx: mpsc::SyncSender>, rx: mpsc::Receiver>) -> Self { Self { tx, rx: Mutex::new(rx), alive: true, } } pub fn send(&mut self, data: Vec) { if let Err(e) = self.tx.send(data) { println!("Failed to send data: {}", e); self.alive = false; } } pub fn recv(&mut self) -> Option> { let rx = self.rx.lock().unwrap(); match rx.recv() { Ok(data) => Some(data), Err(e) => { println!("Failed to receive data: {}", e); self.alive = false; None } } } pub fn is_alive(&self) -> bool { self.alive } } /// A transport over TxRx channel. #[derive(Debug)] pub struct MpscTransport { buffers: LazyBuffers, txrx: TxRx, } impl MpscTransport { pub fn new(txrx: TxRx, input_buffer_size: usize, output_buffer_size: usize) -> Self { Self { buffers: LazyBuffers::new(input_buffer_size, output_buffer_size), txrx, } } } impl Transport for MpscTransport { fn buffers(&mut self) -> &mut dyn Buffers { &mut self.buffers } fn transmit_output(&mut self, amount: usize, _timeout: NextTimeout) -> Result<(), Error> { // The data to send. Must use the amount to know how much of the buffer // is relevant. let to_send = &self.buffers.output()[..amount]; // Blocking send until the other side receives it. self.txrx.send(to_send.to_vec()); Ok(()) } fn await_input(&mut self, _timeout: NextTimeout) -> Result { let Some(data) = self.txrx.recv() else { return Ok(false); }; // Append the data to the input buffer. let input = self.buffers.input_append_buf(); let len = data.len(); input[..len].copy_from_slice(data.as_slice()); // Report how many bytes appended to the input buffer. self.buffers.input_appended(len); // Return true if we made progress, i.e. if we managed to fill the input buffer with any bytes. Ok(len > 0) } fn is_open(&mut self) -> bool { self.txrx.is_alive() } } // A fake HTTP server that responds with "Hello, world!" fn run_server(server_side: Arc>>) { // Wait until the server side is present. let txrx = loop { // Scope to not hold lock while sleeping let txrx = { let mut lock = server_side.lock().unwrap(); lock.take() }; if let Some(txrx) = txrx { break txrx; } thread::sleep(Duration::from_millis(100)); }; // No contention on this lock. See above why we even need it.e let rx = txrx.rx.lock().unwrap(); let mut incoming = String::new(); // We are not guaranteed to receive the entire request in one go. // Loop until we know we have it. loop { let data = rx.recv().unwrap(); let s = String::from_utf8_lossy(&data); incoming.push_str(&s); if incoming.contains("\r\n\r\n") { break; } } println!("SERVER received request: {:?}", incoming); // A random response. let response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!"; println!("SERVER sending response: {:?}", response); txrx.tx.send(response.as_bytes().to_vec()).unwrap(); } algesten-ureq-d58cf18/examples/proxy.rs000066400000000000000000000014011505724604200202600ustar00rootroot00000000000000use std::error::Error; use ureq::tls::TlsConfig; use ureq::{config::Config, Agent, Proxy}; // Use this example with something like mitmproxy // $ mitmproxy --listen-port 8080 fn main() -> Result<(), Box> { let proxy = Proxy::new("http://localhost:8080")?; let config = Config::builder() .tls_config( TlsConfig::builder() // The mitmproxy uses a certificate authority we // don't know. Do not disable verification in // production use. .disable_verification(true) .build(), ) .proxy(Some(proxy)) .build(); let agent = Agent::new_with_config(config); let _ = agent.get("https://example.com").call()?; Ok(()) } algesten-ureq-d58cf18/src/000077500000000000000000000000001505724604200155065ustar00rootroot00000000000000algesten-ureq-d58cf18/src/agent.rs000066400000000000000000000310631505724604200171550ustar00rootroot00000000000000use std::convert::TryFrom; use std::fmt; use std::sync::Arc; use http::{Method, Request, Response, Uri}; use ureq_proto::BodyMode; use crate::body::Body; use crate::config::typestate::{AgentScope, HttpCrateScope}; use crate::config::{Config, ConfigBuilder, RequestLevelConfig}; use crate::http; use crate::middleware::MiddlewareNext; use crate::pool::ConnectionPool; use crate::request::ForceSendBody; use crate::resolver::{DefaultResolver, Resolver}; use crate::send_body::AsSendBody; use crate::transport::{boxed_connector, Connector, DefaultConnector, Transport}; use crate::unversioned::transport::{ConnectionDetails, RunConnector}; use crate::{Error, RequestBuilder, SendBody}; use crate::{WithBody, WithoutBody}; /// Agents keep state between requests. /// /// By default, no state, such as cookies, is kept between requests. /// But by creating an agent as entry point for the request, we /// can keep a state. /// /// # Example /// /// ```no_run /// let mut agent = ureq::agent(); /// /// agent /// .post("http://example.com/post/login") /// .send(b"my password")?; /// /// let secret = agent /// .get("http://example.com/get/my-protected-page") /// .call()? /// .body_mut() /// .read_to_string()?; /// /// println!("Secret is: {}", secret); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// # About threads and cloning /// /// Agent uses inner [`Arc`]. Cloning an Agent results in an instance /// that shares the same underlying connection pool and other state. /// /// The connection pool contains an inner [`Mutex`][std::sync::Mutex] which is (briefly) /// held when borrowing a pooled connection, or returning a connection to the pool. /// /// All request functions in ureq have a signature similar to this: /// /// ``` /// # use ureq::{http, Body, AsSendBody, Error}; /// fn run(request: http::Request) -> Result, Error> { /// // /// # todo!() /// } /// ``` /// /// It follows that: /// /// * An Agent is borrowed for the duration of: /// 1. Sending the request header ([`http::Request`]) /// 2. Sending the request body ([`SendBody`]) /// 3. Receiving the response header ([`http::Response`]) /// * The [`Body`] of the response is not bound to the lifetime of the Agent. /// /// A response [`Body`] can be streamed (for instance via [`Body::into_reader()`]). The [`Body`] /// implements [`Send`], which means it's possible to read the response body on another thread than /// the one that run the request. Behind the scenes, the [`Body`] retains the connection to the remote /// server and it is returned to the agent's pool, once the Body instance (or reader) is dropped. /// /// There is an asymmetry in that sending a request body will borrow the Agent instance, while receiving /// the response body does not. This inconvenience is somewhat mitigated by that [`Agent::run()`] (or /// going via the methods such as [`Agent::get()`]), borrows `&self`, i.e. not exclusive `mut` borrows. /// /// That cloning the agent shares the connection pool is considered a feature. It is often useful to /// retain a single pool for the entire process, while dispatching requests from different threads. /// And if we want separate pools, we can create multiple agents via one of the constructors /// (such as [`Agent::new_with_config()`]). /// /// Note that both [`Config::clone()`] and [`Agent::clone()`] are "cheap" meaning they should not /// incur any heap allocation. #[derive(Clone)] pub struct Agent { pub(crate) config: Arc, pub(crate) pool: Arc, pub(crate) resolver: Arc, #[cfg(feature = "cookies")] pub(crate) jar: Arc, pub(crate) run_connector: Arc, } impl Agent { /// Creates an agent with defaults. pub fn new_with_defaults() -> Self { Self::with_parts_inner( Config::default(), Box::new(DefaultConnector::default()), DefaultResolver::default(), ) } /// Creates an agent with config. pub fn new_with_config(config: Config) -> Self { Self::with_parts_inner( config, Box::new(DefaultConnector::default()), DefaultResolver::default(), ) } /// Shortcut to reach a [`ConfigBuilder`] /// /// This is the same as doing [`Config::builder()`]. pub fn config_builder() -> ConfigBuilder { Config::builder() } /// Creates an agent with a bespoke transport and resolver. /// /// _This is low level API that isn't for regular use of ureq._ pub fn with_parts(config: Config, connector: impl Connector, resolver: impl Resolver) -> Self { let boxed = boxed_connector(connector); Self::with_parts_inner(config, boxed, resolver) } /// Inner helper to avoid additional boxing of the [`DefaultConnector`]. fn with_parts_inner( config: Config, connector: Box>>, resolver: impl Resolver, ) -> Self { let pool = Arc::new(ConnectionPool::new(connector, &config)); let run_connector = { let pool = pool.clone(); Arc::new(move |details: &ConnectionDetails| pool.run_connector(details)) }; Agent { config: Arc::new(config), pool, resolver: Arc::new(resolver), #[cfg(feature = "cookies")] jar: Arc::new(crate::cookies::SharedCookieJar::new()), run_connector, } } /// Access the shared cookie jar. /// /// Used to persist and manipulate the cookies. The jar is shared between /// all clones of the same [`Agent`], meaning you must drop the CookieJar /// before using the agent, or end up with a deadlock. /// /// ```rust /// # #[cfg(feature = "json")] /// # fn no_run() -> Result<(), ureq::Error> { /// use std::io::Write; /// use std::fs::File; /// /// let agent = ureq::agent(); /// /// // Cookies set by www.google.com are stored in agent. /// agent.get("https://www.google.com/").call()?; /// /// // Saves (persistent) cookies /// let mut file = File::create("cookies.json")?; /// let jar = agent.cookie_jar_lock(); /// /// jar.save_json(&mut file)?; /// /// // Release the cookie jar to use agents again. /// jar.release(); /// /// # Ok(()) /// # } /// ``` #[cfg(feature = "cookies")] pub fn cookie_jar_lock(&self) -> crate::cookies::CookieJar<'_> { self.jar.lock() } /// Run a [`http::Request`]. /// /// Used to execute http crate [`http::Request`] directly on this agent. /// /// # Example /// /// ``` /// use ureq::{http, Agent}; /// /// let agent: Agent = Agent::new_with_defaults(); /// /// let mut request = /// http::Request::get("http://httpbin.org/get") /// .body(())?; /// /// let body = agent.run(request)? /// .body_mut() /// .read_to_string()?; /// # Ok::<(), ureq::Error>(()) /// ``` pub fn run(&self, request: Request) -> Result, Error> { let (parts, mut body) = request.into_parts(); let mut body = body.as_body(); let mut request = Request::from_parts(parts, ()); // When using the http-crate API we cannot enforce the correctness of // Method vs Body combos. This also solves a problem where we can't // determine if a non-standard method is supposed to have a body such // as for WebDAV PROPFIND. let has_body = !matches!(body.body_mode(), Ok(BodyMode::NoBody)); if has_body { request.extensions_mut().insert(ForceSendBody); } self.run_via_middleware(request, body) } pub(crate) fn run_via_middleware( &self, request: Request<()>, body: SendBody, ) -> Result, Error> { let (parts, _) = request.into_parts(); let request = http::Request::from_parts(parts, body); let next = MiddlewareNext::new(self); next.handle(request) } /// Get the config for this agent. pub fn config(&self) -> &Config { &self.config } /// Alter the configuration for an http crate request. /// /// Notice: It's an error to configure a [`http::Request`] using /// one instance of [`Agent`] and run using another instance. The /// library does not currently detect this situation, but it is /// not considered a breaking change if this is enforced in /// the future. pub fn configure_request( &self, mut request: Request, ) -> ConfigBuilder> { let exts = request.extensions_mut(); if exts.get::().is_none() { exts.insert(self.new_request_level_config()); } ConfigBuilder(HttpCrateScope(request)) } pub(crate) fn new_request_level_config(&self) -> RequestLevelConfig { RequestLevelConfig(self.config.as_ref().clone()) } /// Make a GET request using this agent. #[must_use] pub fn get(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::GET, uri) } /// Make a POST request using this agent. #[must_use] pub fn post(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::POST, uri) } /// Make a PUT request using this agent. #[must_use] pub fn put(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::PUT, uri) } /// Make a DELETE request using this agent. #[must_use] pub fn delete(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::DELETE, uri) } /// Make a HEAD request using this agent. #[must_use] pub fn head(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::HEAD, uri) } /// Make an OPTIONS request using this agent. #[must_use] pub fn options(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::OPTIONS, uri) } /// Make a CONNECT request using this agent. #[must_use] pub fn connect(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::CONNECT, uri) } /// Make a PATCH request using this agent. #[must_use] pub fn patch(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::PATCH, uri) } /// Make a TRACE request using this agent. #[must_use] pub fn trace(&self, uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(self.clone(), Method::TRACE, uri) } } impl From for Agent { fn from(value: Config) -> Self { Agent::new_with_config(value) } } impl fmt::Debug for Agent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut dbg = f.debug_struct("Agent"); dbg.field("config", &self.config) .field("pool", &self.pool) .field("resolver", &self.resolver); #[cfg(feature = "cookies")] { dbg.field("jar", &self.jar); } dbg.finish() } } #[cfg(test)] impl Agent { /// Exposed for testing the pool count. pub fn pool_count(&self) -> usize { self.pool.pool_count() } } #[cfg(test)] mod test { use super::*; use assert_no_alloc::*; #[test] fn agent_clone_does_not_allocate() { let a = Agent::new_with_defaults(); assert_no_alloc(|| a.clone()); } } algesten-ureq-d58cf18/src/body/000077500000000000000000000000001505724604200164435ustar00rootroot00000000000000algesten-ureq-d58cf18/src/body/brotli.rs000066400000000000000000000013201505724604200203000ustar00rootroot00000000000000use std::io; use brotli_decompressor::Decompressor; use crate::error::is_wrapped_ureq_error; use crate::Error; pub(crate) struct BrotliDecoder(Decompressor); impl BrotliDecoder { pub fn new(reader: R) -> Self { BrotliDecoder(Decompressor::new(reader, 4096)) } } impl io::Read for BrotliDecoder { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf).map_err(|e| { if is_wrapped_ureq_error(&e) { // If this already is a ureq::Error, like Timeout, pass it along. e } else { Error::Decompress("brotli", e).into_io() } }) } } algesten-ureq-d58cf18/src/body/build.rs000066400000000000000000000065241505724604200201170ustar00rootroot00000000000000use std::io::{self, Cursor}; use std::sync::Arc; use ureq_proto::BodyMode; use super::{Body, BodyDataSource, ContentEncoding, ResponseInfo}; /// Builder for creating a response body. /// /// This is useful for testing, or for [`Middleware`][crate::middleware::Middleware] that /// returns another body than the requested one. /// /// # Example /// /// ``` /// use ureq::Body; /// use ureq::http::Response; /// /// let body = Body::builder() /// .mime_type("text/plain") /// .charset("utf-8") /// .data("Hello world!"); /// /// let mut response = Response::builder() /// .status(200) /// .header("content-type", "text/plain; charset=utf-8") /// .body(body)?; /// /// let text = response /// .body_mut() /// .read_to_string()?; /// /// assert_eq!(text, "Hello world!"); /// # Ok::<_, ureq::Error>(()) /// ``` pub struct BodyBuilder { info: ResponseInfo, limit: Option, } impl BodyBuilder { pub(crate) fn new() -> Self { BodyBuilder { info: ResponseInfo { content_encoding: ContentEncoding::None, mime_type: None, charset: None, body_mode: BodyMode::NoBody, }, limit: None, } } /// Set the mime type of the body. /// /// **This does not set any HTTP headers. Affects Body decoding.** /// /// ``` /// use ureq::Body; /// /// let body = Body::builder() /// .mime_type("text/plain") /// .data("Hello world!"); /// ``` pub fn mime_type(mut self, mime_type: impl Into) -> Self { self.info.mime_type = Some(mime_type.into()); self } /// Set the mime type of the body /// /// **This does not set any HTTP headers. Affects Body decoding.** /// /// ``` /// use ureq::Body; /// /// let body = Body::builder() /// .mime_type("text/plain") /// .charset("utf-8") /// .data("Hello world!"); /// ``` pub fn charset(mut self, charset: impl Into) -> Self { self.info.charset = Some(charset.into()); self } /// Limit how much data is to be released from the body. /// /// **This does not set any HTTP headers. Affects Body decoding.** /// /// ``` /// use ureq::Body; /// /// let body = Body::builder() /// .mime_type("text/plain") /// .charset("utf-8") /// .limit(5) /// // This will be limited to "Hello" /// .data("Hello world!"); /// ``` pub fn limit(mut self, l: u64) -> Self { self.limit = Some(l); self } /// Creates the body data turned into bytes. /// /// Will automatically limit the body reader to the lenth of the data. pub fn data(mut self, data: impl Into>) -> Body { let data: Vec = data.into(); let len = self.limit.unwrap_or(data.len() as u64); self.info.body_mode = BodyMode::LengthDelimited(len); self.reader(Cursor::new(data)) } /// Creates a body from a streaming reader. /// /// The reader can be limited by using `.limit()` or that the reader /// reaches the end. pub fn reader(self, data: impl io::Read + Send + Sync + 'static) -> Body { Body { source: BodyDataSource::Reader(Box::new(data)), info: Arc::new(self.info), } } } algesten-ureq-d58cf18/src/body/charset.rs000066400000000000000000000102741505724604200204460ustar00rootroot00000000000000use encoding_rs::{Decoder, Encoder, Encoding}; use std::fmt; use std::io::{self, BufRead, BufReader}; use crate::util::ConsumeBuf; const MAX_OUTPUT: usize = 4096; /// Charset transcoder pub(crate) struct CharCodec { reader: BufReader, dec: Option, enc: Option, buf: ConsumeBuf, reached_end: bool, } impl CharCodec where R: io::Read, { pub fn new(reader: R, from: &'static Encoding, to: &'static Encoding) -> Self { CharCodec { reader: BufReader::new(reader), dec: Some(from.new_decoder()), enc: if to == encoding_rs::UTF_8 { None } else { Some(to.new_encoder()) }, buf: ConsumeBuf::new(MAX_OUTPUT), reached_end: false, } } } impl io::Read for CharCodec { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.reached_end && self.buf.unconsumed().is_empty() { return Ok(0); } let input = 'read: { if self.buf.unconsumed().len() > MAX_OUTPUT / 4 { // Do not keep filling if we have unused output. break 'read self.reader.buffer(); } let tmp = self.reader.fill_buf()?; let tmp_len = tmp.len(); if tmp_len >= 4 { // We need some minimum input to make progress. break 'read tmp; } let tmp2 = self.reader.fill_buf()?; if tmp2.len() == tmp_len { // Made no progress. That means we reached the end. self.reached_end = true; } tmp2 }; if self.buf.free_mut().len() < 4 { self.buf.add_space(1024); } let output = self.buf.free_mut(); if let Some(dec) = &mut self.dec { let (_, input_used, output_used, _had_errors) = dec.decode_to_utf8(input, output, self.reached_end); self.reader.consume(input_used); self.buf.add_filled(output_used); if self.reached_end { // Can't be used again self.dec = None; } } // guaranteed to be on a char boundary by encoding_rs let bytes = self.buf.unconsumed(); let amount = if let Some(enc) = &mut self.enc { // unwrap is ok because it is on a char boundary, and non-utf8 chars have been replaced let utf8 = std::str::from_utf8(bytes).unwrap(); let (_, input_used, output_used, _) = enc.encode_from_utf8(utf8, buf, self.reached_end); self.buf.consume(input_used); if self.reached_end { // Can't be used again self.enc = None; } output_used } else { // No encoder, we want utf8 let max = bytes.len().min(buf.len()); buf[..max].copy_from_slice(&bytes[..max]); self.buf.consume(max); max }; Ok(amount) } } impl fmt::Debug for CharCodec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "CharCodec {{ from: {}, to: {} }}", self.dec .as_ref() .map(|d| d.encoding().name()) .unwrap_or(encoding_rs::UTF_8.name()), self.enc .as_ref() .map(|e| e.encoding()) .unwrap_or(encoding_rs::UTF_8) .name() ) } } #[cfg(all(test, feature = "_test"))] mod test { use super::*; #[test] fn create_encodings() { assert!(Encoding::for_label(b"utf8").is_some()); assert_eq!(Encoding::for_label(b"utf8"), Encoding::for_label(b"utf-8")); } #[test] #[cfg(feature = "charset")] fn non_ascii_reason() { use crate::test::init_test_log; use crate::Agent; init_test_log(); let agent: Agent = Agent::config_builder().max_redirects(0).build().into(); let res = agent .get("https://my.test/non-ascii-reason") .call() .unwrap(); assert_eq!(res.status(), 302); } } algesten-ureq-d58cf18/src/body/gzip.rs000066400000000000000000000043211505724604200177620ustar00rootroot00000000000000use std::io; use flate2::read::MultiGzDecoder; use crate::error::is_wrapped_ureq_error; use crate::Error; pub(crate) struct GzipDecoder(MultiGzDecoder); impl GzipDecoder { pub fn new(reader: R) -> Self { GzipDecoder(MultiGzDecoder::new(reader)) } } impl io::Read for GzipDecoder { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf).map_err(|e| { if is_wrapped_ureq_error(&e) { // If this already is a ureq::Error, like Timeout, pass it along. e } else { Error::Decompress("gzip", e).into_io() } }) } } #[cfg(all(test, feature = "_test"))] mod test { use crate::test::init_test_log; use crate::transport::set_handler; use crate::Agent; // Test that a stream gets returned to the pool if it is gzip encoded and the gzip // decoder reads the exact amount from a chunked stream, not past the 0. This // happens because gzip has built-in knowledge of the length to read. #[test] fn gz_internal_length() { init_test_log(); let gz_body = vec![ b'E', b'\r', b'\n', // 14 first chunk 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0xCB, 0x48, 0xCD, 0xC9, b'\r', b'\n', // b'E', b'\r', b'\n', // 14 second chunk 0xC9, 0x57, 0x28, 0xCF, 0x2F, 0xCA, 0x49, 0x51, 0xC8, 0x18, 0xBC, 0x6C, 0x00, 0xA5, b'\r', b'\n', // b'7', b'\r', b'\n', // 7 third chunk 0x5C, 0x7C, 0xEF, 0xA7, 0x00, 0x00, 0x00, // b'\r', b'\n', // // end b'0', b'\r', b'\n', // b'\r', b'\n', // ]; let agent = Agent::new_with_defaults(); assert_eq!(agent.pool_count(), 0); set_handler( "/gz_body", 200, &[ ("transfer-encoding", "chunked"), ("content-encoding", "gzip"), ], &gz_body, ); let mut res = agent.get("https://example.test/gz_body").call().unwrap(); res.body_mut().read_to_string().unwrap(); assert_eq!(agent.pool_count(), 1); } } algesten-ureq-d58cf18/src/body/limit.rs000066400000000000000000000033401505724604200201270ustar00rootroot00000000000000use std::io; use crate::Error; pub(crate) struct LimitReader { reader: R, limit: u64, left: u64, } impl LimitReader { pub fn new(reader: R, limit: u64) -> Self { LimitReader { reader, limit, left: limit, } } } impl io::Read for LimitReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.left == 0 { return Err(Error::BodyExceedsLimit(self.limit).into_io()); } // The max buffer size is usize, which may be 32 bit. let max = (self.left.min(usize::MAX as u64) as usize).min(buf.len()); let n = self.reader.read(&mut buf[..max])?; self.left -= n as u64; Ok(n) } } #[cfg(all(test, feature = "_test"))] mod test { use std::io; use crate::test::init_test_log; use crate::transport::set_handler; use crate::Error; #[test] fn short_read() { init_test_log(); set_handler("/get", 200, &[("content-length", "10")], b"hello"); let mut res = crate::get("https://my.test/get").call().unwrap(); let err = res.body_mut().read_to_string().unwrap_err(); let ioe = err.into_io(); assert_eq!(ioe.kind(), io::ErrorKind::UnexpectedEof); } #[test] fn limit_below_size() { init_test_log(); set_handler("/get", 200, &[("content-length", "5")], b"hello"); let mut res = crate::get("https://my.test/get").call().unwrap(); let err = res .body_mut() .with_config() .limit(3) .read_to_string() .unwrap_err(); println!("{:?}", err); assert!(matches!(err, Error::BodyExceedsLimit(3))); } } algesten-ureq-d58cf18/src/body/lossy.rs000066400000000000000000000131061505724604200201630ustar00rootroot00000000000000use std::io; use utf8::DecodeError; use crate::util::ConsumeBuf; const REPLACEMENT_CHAR: u8 = b'?'; const MIN_BUF: usize = 8; pub struct LossyUtf8Reader { reader: R, ended: bool, input: ConsumeBuf, valid_len: usize, } impl LossyUtf8Reader { pub(crate) fn new(reader: R) -> Self { Self { reader, ended: false, input: ConsumeBuf::new(8), valid_len: 0, } } fn process_input(&mut self) -> usize { match utf8::decode(self.input.unconsumed()) { Ok(_) => { // Entire input is valid self.input.unconsumed().len() } Err(e) => match e { DecodeError::Invalid { valid_prefix, invalid_sequence, .. } => { let valid_len = valid_prefix.len(); let invalid_len = invalid_sequence.len(); // Switch out the problem input chars let replace_in = self.input.unconsumed_mut(); for i in 0..invalid_len { replace_in[valid_len + i] = REPLACEMENT_CHAR; } valid_len + invalid_len } DecodeError::Incomplete { valid_prefix, .. } => { let valid_len = valid_prefix.len(); if self.ended { // blank the rest let replace_in = self.input.unconsumed_mut(); let invalid_len = replace_in.len() - valid_len; for i in 0..invalid_len { replace_in[valid_len + i] = REPLACEMENT_CHAR; } valid_len + invalid_len } else { valid_len } } }, } } } impl io::Read for LossyUtf8Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { // Match the input buffer size if !self.ended { let total_len = self.input.unconsumed().len() + self.input.free_mut().len(); let wanted_len = buf.len().max(MIN_BUF); if wanted_len < total_len { self.input.add_space(total_len - wanted_len); } } // Fill up to a point where we definitely will make progress. while !self.ended && self.input.unconsumed().len() < MIN_BUF { let amount = self.reader.read(self.input.free_mut())?; self.input.add_filled(amount); if amount == 0 { self.ended = true; } } if self.ended && self.input.unconsumed().is_empty() { return Ok(0); } if self.valid_len == 0 { self.valid_len = self.process_input(); assert!(self.valid_len > 0); } let src = &self.input.unconsumed()[..self.valid_len]; let max = src.len().min(buf.len()); buf[..max].copy_from_slice(&src[..max]); self.input.consume(max); self.valid_len -= max; Ok(max) } } #[cfg(test)] mod test { use std::io::Read; use super::*; fn do_reader<'a>(bytes: &'a mut [&'a [u8]]) -> String { let mut r = LossyUtf8Reader::new(TestReader(bytes)); let mut buf = String::new(); r.read_to_string(&mut buf).unwrap(); buf } #[test] fn ascii() { assert_eq!(do_reader(&mut [b"abc123"]), "abc123"); } #[test] fn utf8_one_read() { assert_eq!(do_reader(&mut ["åiåaäeö".as_bytes()]), "åiåaäeö"); } #[test] fn utf8_chopped_single_char() { assert_eq!(do_reader(&mut [&[195], &[165]]), "å"); } #[test] fn utf8_chopped_prefix_ascii() { assert_eq!(do_reader(&mut [&[97, 97, 97, 195], &[165]]), "aaaå"); } #[test] fn utf8_chopped_suffix_ascii() { assert_eq!(do_reader(&mut [&[195], &[165, 97, 97, 97]]), "åaaa"); } #[test] fn utf8_broken_single() { assert_eq!(do_reader(&mut [&[195]]), "?"); } #[test] fn utf8_broken_suffix_ascii() { assert_eq!(do_reader(&mut [&[195, 97, 97, 97]]), "?aaa"); } #[test] fn utf8_broken_prefix_ascii() { assert_eq!(do_reader(&mut [&[97, 97, 97, 195]]), "aaa?"); } #[test] fn hiragana() { assert_eq!(do_reader(&mut ["あいうえお".as_bytes()]), "あいうえお"); } #[test] fn emoji() { assert_eq!(do_reader(&mut ["✅✅✅".as_bytes()]), "✅✅✅"); } #[test] fn leftover() { let s = "あ"; assert_eq!(s.as_bytes(), &[227, 129, 130]); let mut buf = [0; 2]; let mut r = LossyUtf8Reader::new(s.as_bytes()); assert_eq!(r.read(&mut buf).unwrap(), 2); assert_eq!(&buf[..], &[227, 129]); assert_eq!(r.read(&mut buf).unwrap(), 1); assert_eq!(&buf[..1], &[130]); assert_eq!(r.read(&mut buf).unwrap(), 0); } struct TestReader<'a>(&'a mut [&'a [u8]]); impl<'a> io::Read for TestReader<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.0.iter().all(|c| c.is_empty()) { return Ok(0); } let pos = self.0.iter().position(|c| !c.is_empty()).unwrap(); let cur = &self.0[pos]; let max = cur.len().min(buf.len()); buf[..max].copy_from_slice(&cur[..max]); self.0[pos] = &cur[max..]; Ok(max) } } } algesten-ureq-d58cf18/src/body/mod.rs000066400000000000000000000733701505724604200176020ustar00rootroot00000000000000use std::fmt; use std::io; use std::sync::Arc; pub use build::BodyBuilder; use ureq_proto::http::header; use ureq_proto::BodyMode; use crate::http; use crate::run::BodyHandler; use crate::Error; use self::limit::LimitReader; use self::lossy::LossyUtf8Reader; mod build; mod limit; mod lossy; #[cfg(feature = "charset")] mod charset; #[cfg(feature = "gzip")] mod gzip; #[cfg(feature = "brotli")] mod brotli; /// Default max body size for read_to_string() and read_to_vec(). const MAX_BODY_SIZE: u64 = 10 * 1024 * 1024; /// A response body returned as [`http::Response`]. /// /// # Default size limit /// /// Methods like `read_to_string()`, `read_to_vec()`, and `read_json()` have a **default 10MB limit** /// to prevent memory exhaustion. To download larger files, use `with_config().limit(new_size)`: /// /// ``` /// // Download a 20MB file /// let bytes = ureq::get("http://httpbin.org/bytes/200000000") /// .call()? /// .body_mut() /// .with_config() /// .limit(20 * 1024 * 1024) // 20MB /// .read_to_vec()?; /// # Ok::<_, ureq::Error>(()) /// ``` /// /// # Body lengths /// /// HTTP/1.1 has two major modes of transfering body data. Either a `Content-Length` /// header defines exactly how many bytes to transfer, or `Transfer-Encoding: chunked` /// facilitates a streaming style when the size is not known up front. /// /// To protect against a problem called [request smuggling], ureq has heuristics for /// how to interpret a server sending both `Transfer-Encoding` and `Content-Length` headers. /// /// 1. `chunked` takes precedence if there both headers are present (not for HTTP/1.0) /// 2. `content-length` is used if there is no chunked /// 3. If there are no headers, fall back on "close delimited" meaning the socket /// must close to end the body /// /// When a `Content-Length` header is used, ureq will ensure the received body is _EXACTLY_ /// as many bytes as declared (it cannot be less). This mechanic is in `ureq-proto` /// and is different to the [`BodyWithConfig::limit()`] below. /// /// # Pool reuse /// /// To return a connection (aka [`Transport`][crate::unversioned::transport::Transport]) /// to the Agent's pool, the body must be read to end. If [`BodyWithConfig::limit()`] is set /// shorter size than the actual response body, the connection will not be reused. /// /// # Example /// /// ``` /// use std::io::Read; /// let mut res = ureq::get("http://httpbin.org/bytes/100") /// .call()?; /// /// assert!(res.headers().contains_key("Content-Length")); /// let len: usize = res.headers().get("Content-Length") /// .unwrap().to_str().unwrap().parse().unwrap(); /// /// let mut bytes: Vec = Vec::with_capacity(len); /// res.body_mut().as_reader() /// .read_to_end(&mut bytes)?; /// /// assert_eq!(bytes.len(), len); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// [request smuggling]: https://en.wikipedia.org/wiki/HTTP_request_smuggling pub struct Body { source: BodyDataSource, info: Arc, } enum BodyDataSource { Handler(Box), Reader(Box), } #[derive(Clone)] pub(crate) struct ResponseInfo { content_encoding: ContentEncoding, mime_type: Option, charset: Option, body_mode: BodyMode, } impl Body { /// Builder for creating a body /// /// This is useful for testing, or for [`Middleware`][crate::middleware::Middleware] that /// returns another body than the requested one. pub fn builder() -> BodyBuilder { BodyBuilder::new() } pub(crate) fn new(handler: BodyHandler, info: ResponseInfo) -> Self { Body { source: BodyDataSource::Handler(Box::new(handler)), info: Arc::new(info), } } /// The mime-type of the `content-type` header. /// /// For the below header, we would get `Some("text/plain")`: /// /// ```text /// Content-Type: text/plain; charset=iso-8859-1 /// ``` /// /// *Caution:* A bad server might set `Content-Type` to one thing and send /// something else. There is no way ureq can verify this. /// /// # Example /// /// ``` /// let res = ureq::get("https://www.google.com/") /// .call()?; /// /// assert_eq!(res.body().mime_type(), Some("text/html")); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn mime_type(&self) -> Option<&str> { self.info.mime_type.as_deref() } /// The charset of the `content-type` header. /// /// For the below header, we would get `Some("iso-8859-1")`: /// /// ```text /// Content-Type: text/plain; charset=iso-8859-1 /// ``` /// /// *Caution:* A bad server might set `Content-Type` to one thing and send /// something else. There is no way ureq can verify this. /// /// # Example /// /// ``` /// let res = ureq::get("https://www.google.com/") /// .call()?; /// /// assert_eq!(res.body().charset(), Some("ISO-8859-1")); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn charset(&self) -> Option<&str> { self.info.charset.as_deref() } /// The content length of the body. /// /// This is the value of the `Content-Length` header, if there is one. For chunked /// responses (`Transfer-Encoding: chunked`) , this will be `None`. Similarly for /// HTTP/1.0 without a `Content-Length` header, the response is close delimited, /// which means the length is unknown. /// /// A bad server might set `Content-Length` to one thing and send something else. /// ureq will double check this, see section on body length heuristics. /// /// # Example /// /// ``` /// let res = ureq::get("https://httpbin.org/bytes/100") /// .call()?; /// /// assert_eq!(res.body().content_length(), Some(100)); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn content_length(&self) -> Option { match self.info.body_mode { BodyMode::NoBody => None, BodyMode::LengthDelimited(v) => Some(v), BodyMode::Chunked => None, BodyMode::CloseDelimited => None, } } /// Handle this body as a shared `impl Read` of the body. /// /// This is the regular API which goes via [`http::Response::body_mut()`] to get a /// mut reference to the `Body`, and then use `as_reader()`. It is also possible to /// get a non-shared, owned reader via [`Body::into_reader()`]. /// /// * Reader is not limited by default. That means a malicious server could /// exhaust all avaliable memory on your client machine. /// To set a limit use [`Body::into_with_config()`]. /// * Reader will error if `Content-Length` is set, but the connection is closed /// before all bytes are received. /// /// # Example /// /// ``` /// use std::io::Read; /// /// let mut res = ureq::get("http://httpbin.org/bytes/100") /// .call()?; /// /// let mut bytes: Vec = Vec::with_capacity(1000); /// res.body_mut().as_reader() /// .read_to_end(&mut bytes)?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn as_reader(&mut self) -> BodyReader { self.with_config().reader() } /// Turn this response into an owned `impl Read` of the body. /// /// Sometimes it might be useful to disconnect the body reader from the body. /// The reader returned by [`Body::as_reader()`] borrows the body, while this /// variant consumes the body and turns it into a reader with lifetime `'static`. /// The reader can for instance be sent to another thread. /// /// * Reader is not limited by default. That means a malicious server could /// exhaust all avaliable memory on your client machine. /// To set a limit use [`Body::into_with_config()`]. /// * Reader will error if `Content-Length` is set, but the connection is closed /// before all bytes are received. /// /// ``` /// use std::io::Read; /// /// let res = ureq::get("http://httpbin.org/bytes/100") /// .call()?; /// /// let (_, body) = res.into_parts(); /// /// let mut bytes: Vec = Vec::with_capacity(1000); /// body.into_reader() /// .read_to_end(&mut bytes)?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn into_reader(self) -> BodyReader<'static> { self.into_with_config().reader() } /// Read the response as a string. /// /// * Response is limited to 10MB /// * Replaces incorrect utf-8 chars to `?` /// /// To change these defaults use [`Body::with_config()`]. /// /// ``` /// let mut res = ureq::get("http://httpbin.org/robots.txt") /// .call()?; /// /// let s = res.body_mut().read_to_string()?; /// assert_eq!(s, "User-agent: *\nDisallow: /deny\n"); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// For larger text files, you must explicitly increase the limit: /// /// ``` /// // Read a large text file (25MB) /// let text = ureq::get("http://httpbin.org/get") /// .call()? /// .body_mut() /// .with_config() /// .limit(25 * 1024 * 1024) // 25MB /// .read_to_string()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn read_to_string(&mut self) -> Result { self.with_config() .limit(MAX_BODY_SIZE) .lossy_utf8(true) .read_to_string() } /// Read the response to a vec. /// /// * Response is limited to 10MB. /// /// To change this default use [`Body::with_config()`]. /// ``` /// let mut res = ureq::get("http://httpbin.org/bytes/100") /// .call()?; /// /// let bytes = res.body_mut().read_to_vec()?; /// assert_eq!(bytes.len(), 100); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// For larger files, you must explicitly increase the limit: /// /// ``` /// // Download a larger file (50MB) /// let bytes = ureq::get("http://httpbin.org/bytes/200000000") /// .call()? /// .body_mut() /// .with_config() /// .limit(50 * 1024 * 1024) // 50MB /// .read_to_vec()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn read_to_vec(&mut self) -> Result, Error> { self.with_config() // .limit(MAX_BODY_SIZE) .read_to_vec() } /// Read the response from JSON. /// /// * Response is limited to 10MB. /// /// To change this default use [`Body::with_config()`]. /// /// The returned value is something that derives [`Deserialize`](serde::Deserialize). /// You might need to be explicit with which type you want. See example below. /// /// ``` /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct BodyType { /// slideshow: BodyTypeInner, /// } /// /// #[derive(Deserialize)] /// struct BodyTypeInner { /// author: String, /// } /// /// let body = ureq::get("https://httpbin.org/json") /// .call()? /// .body_mut() /// .read_json::()?; /// /// assert_eq!(body.slideshow.author, "Yours Truly"); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// For larger JSON files, you must explicitly increase the limit: /// /// ``` /// use serde_json::Value; /// /// // Parse a large JSON file (30MB) /// let json: Value = ureq::get("https://httpbin.org/json") /// .call()? /// .body_mut() /// .with_config() /// .limit(30 * 1024 * 1024) // 30MB /// .read_json()?; /// # Ok::<_, ureq::Error>(()) /// ``` #[cfg(feature = "json")] pub fn read_json(&mut self) -> Result { let reader = self.with_config().limit(MAX_BODY_SIZE).reader(); let value: T = serde_json::from_reader(reader)?; Ok(value) } /// Read the body data with configuration. /// /// This borrows the body which gives easier use with [`http::Response::body_mut()`]. /// To get a non-borrowed reader use [`Body::into_with_config()`]. /// /// # Example /// /// ``` /// let reader = ureq::get("http://httpbin.org/bytes/100") /// .call()? /// .body_mut() /// .with_config() /// // Reader will only read 50 bytes /// .limit(50) /// .reader(); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn with_config(&mut self) -> BodyWithConfig { let handler = (&mut self.source).into(); BodyWithConfig::new(handler, self.info.clone()) } /// Consume self and read the body with configuration. /// /// This consumes self and returns a reader with `'static` lifetime. /// /// # Example /// /// ``` /// // Get the body out of http::Response /// let (_, body) = ureq::get("http://httpbin.org/bytes/100") /// .call()? /// .into_parts(); /// /// let reader = body /// .into_with_config() /// // Reader will only read 50 bytes /// .limit(50) /// .reader(); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// This limit behavior can be used to prevent a malicious server from exhausting /// memory on the client machine. For example, if the machine running /// ureq has 1GB of RAM, you could protect the machine by setting a smaller /// limit such as 128MB. The exact number will vary by your client's download /// needs, available system resources, and system utilization. pub fn into_with_config(self) -> BodyWithConfig<'static> { let handler = self.source.into(); BodyWithConfig::new(handler, self.info.clone()) } } /// Configuration of how to read the body. /// /// Obtained via one of: /// /// * [Body::with_config()] /// * [Body::into_with_config()] /// /// # Handling large responses /// /// The `BodyWithConfig` is the primary way to increase the default 10MB size limit /// when downloading large files to memory: /// /// ``` /// // Download a 50MB file /// let large_data = ureq::get("http://httpbin.org/bytes/200000000") /// .call()? /// .body_mut() /// .with_config() /// .limit(50 * 1024 * 1024) // 50MB /// .read_to_vec()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub struct BodyWithConfig<'a> { handler: BodySourceRef<'a>, info: Arc, limit: u64, lossy_utf8: bool, } impl<'a> BodyWithConfig<'a> { fn new(handler: BodySourceRef<'a>, info: Arc) -> Self { BodyWithConfig { handler, info, limit: u64::MAX, lossy_utf8: false, } } /// Limit the response body. /// /// Controls how many bytes we should read before throwing an error. This is used /// to ensure RAM isn't exhausted by a server sending a very large response body. /// /// The default limit is `u64::MAX` (unlimited). pub fn limit(mut self, value: u64) -> Self { self.limit = value; self } /// Replace invalid utf-8 chars. /// /// `true` means that broken utf-8 characters are replaced by a question mark `?` /// (not utf-8 replacement char). This happens after charset conversion regardless of /// whether the **charset** feature is enabled or not. /// /// The default is `false`. pub fn lossy_utf8(mut self, value: bool) -> Self { self.lossy_utf8 = value; self } fn do_build(self) -> BodyReader<'a> { BodyReader::new( LimitReader::new(self.handler, self.limit), &self.info, self.info.body_mode, self.lossy_utf8, ) } /// Creates a reader. /// /// The reader is either shared or owned, depending on `with_config` or `into_with_config`. /// /// # Example of owned vs shared /// /// ``` /// // Creates an owned reader. /// let reader = ureq::get("https://httpbin.org/get") /// .call()? /// .into_body() /// // takes ownership of Body /// .into_with_config() /// .limit(10) /// .reader(); /// # Ok::<_, ureq::Error>(()) /// ``` /// /// ``` /// // Creates a shared reader. /// let reader = ureq::get("https://httpbin.org/get") /// .call()? /// .body_mut() /// // borrows Body /// .with_config() /// .limit(10) /// .reader(); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn reader(self) -> BodyReader<'a> { self.do_build() } /// Read into string. /// /// *Caution:* without a preceeding [`limit()`][BodyWithConfig::limit], this /// becomes an unbounded sized `String`. A bad server could exhaust your memory. /// /// # Example /// /// ``` /// // Reads max 10k to a String. /// let string = ureq::get("https://httpbin.org/get") /// .call()? /// .body_mut() /// .with_config() /// // Important. Limits body to 10k /// .limit(10_000) /// .read_to_string()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn read_to_string(self) -> Result { use std::io::Read; let mut reader = self.do_build(); let mut buf = String::new(); reader.read_to_string(&mut buf)?; Ok(buf) } /// Read into vector. /// /// *Caution:* without a preceeding [`limit()`][BodyWithConfig::limit], this /// becomes an unbounded sized `Vec`. A bad server could exhaust your memory. /// /// # Example /// /// ``` /// // Reads max 10k to a Vec. /// let myvec = ureq::get("https://httpbin.org/get") /// .call()? /// .body_mut() /// .with_config() /// // Important. Limits body to 10k /// .limit(10_000) /// .read_to_vec()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn read_to_vec(self) -> Result, Error> { use std::io::Read; let mut reader = self.do_build(); let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; Ok(buf) } /// Read JSON body. /// /// *Caution:* without a preceeding [`limit()`][BodyWithConfig::limit], this /// becomes an unbounded sized `String`. A bad server could exhaust your memory. /// /// # Example /// /// ``` /// use serde_json::Value; /// /// // Reads max 10k as a JSON value. /// let json: Value = ureq::get("https://httpbin.org/get") /// .call()? /// .body_mut() /// .with_config() /// // Important. Limits body to 10k /// .limit(10_000) /// .read_json()?; /// # Ok::<_, ureq::Error>(()) /// ``` #[cfg(feature = "json")] pub fn read_json(self) -> Result { let reader = self.do_build(); let value: T = serde_json::from_reader(reader)?; Ok(value) } } #[derive(Debug, Clone, Copy)] enum ContentEncoding { None, Gzip, Brotli, Unknown, } impl ResponseInfo { pub fn new(headers: &http::HeaderMap, body_mode: BodyMode) -> Self { let content_encoding = headers .get(header::CONTENT_ENCODING) .and_then(|v| v.to_str().ok()) .map(ContentEncoding::from) .unwrap_or(ContentEncoding::None); let (mime_type, charset) = headers .get(header::CONTENT_TYPE) .and_then(|v| v.to_str().ok()) .map(split_content_type) .unwrap_or((None, None)); ResponseInfo { content_encoding, mime_type, charset, body_mode, } } /// Whether the mime type indicats text. fn is_text(&self) -> bool { self.mime_type .as_deref() .map(|s| s.starts_with("text/")) .unwrap_or(false) } } fn split_content_type(content_type: &str) -> (Option, Option) { // Content-Type: text/plain; charset=iso-8859-1 let mut split = content_type.split(';'); let Some(mime_type) = split.next() else { return (None, None); }; let mut charset = None; for maybe_charset in split { let maybe_charset = maybe_charset.trim(); if let Some(s) = maybe_charset.strip_prefix("charset=") { charset = Some(s.to_string()); } } (Some(mime_type.to_string()), charset) } /// A reader of the response data. /// /// 1. If `Transfer-Encoding: chunked`, the returned reader will unchunk it /// and any `Content-Length` header is ignored. /// 2. If `Content-Encoding: gzip` (or `br`) and the corresponding feature /// flag is enabled (**gzip** and **brotli**), decompresses the body data. /// 3. Given a header like `Content-Type: text/plain; charset=ISO-8859-1` /// and the **charset** feature enabled, will translate the body to utf-8. /// This mechanic need two components a mime-type starting `text/` and /// a non-utf8 charset indication. /// 4. If `Content-Length` is set, the returned reader is limited to this byte /// length regardless of how many bytes the server sends. /// 5. If no length header, the reader is until server stream end. /// 6. The limit in the body method used to obtain the reader. /// /// Note: The reader is also limited by the [`Body::as_reader`] and /// [`Body::into_reader`] calls. If that limit is set very high, a malicious /// server might return enough bytes to exhaust available memory. If you're /// making requests to untrusted servers, you should use set that /// limit accordingly. /// /// # Example /// /// ``` /// use std::io::Read; /// let mut res = ureq::get("http://httpbin.org/bytes/100") /// .call()?; /// /// assert!(res.headers().contains_key("Content-Length")); /// let len: usize = res.headers().get("Content-Length") /// .unwrap().to_str().unwrap().parse().unwrap(); /// /// let mut bytes: Vec = Vec::with_capacity(len); /// res.body_mut().as_reader() /// .read_to_end(&mut bytes)?; /// /// assert_eq!(bytes.len(), len); /// # Ok::<_, ureq::Error>(()) /// ``` pub struct BodyReader<'a> { reader: MaybeLossyDecoder>>>>, // If this reader is used as SendBody for another request, this // body mode can indiciate the content-length. Gzip, charset etc // would mean input is not same as output. outgoing_body_mode: BodyMode, } impl<'a> BodyReader<'a> { fn new( reader: LimitReader>, info: &ResponseInfo, incoming_body_mode: BodyMode, lossy_utf8: bool, ) -> BodyReader<'a> { // This is outgoing body_mode in case we are using the BodyReader as a send body // in a proxy situation. let mut outgoing_body_mode = incoming_body_mode; let reader = match info.content_encoding { ContentEncoding::None | ContentEncoding::Unknown => ContentDecoder::PassThrough(reader), #[cfg(feature = "gzip")] ContentEncoding::Gzip => { debug!("Decoding gzip"); outgoing_body_mode = BodyMode::Chunked; ContentDecoder::Gzip(Box::new(gzip::GzipDecoder::new(reader))) } #[cfg(not(feature = "gzip"))] ContentEncoding::Gzip => ContentDecoder::PassThrough(reader), #[cfg(feature = "brotli")] ContentEncoding::Brotli => { debug!("Decoding brotli"); outgoing_body_mode = BodyMode::Chunked; ContentDecoder::Brotli(Box::new(brotli::BrotliDecoder::new(reader))) } #[cfg(not(feature = "brotli"))] ContentEncoding::Brotli => ContentDecoder::PassThrough(reader), }; let reader = if info.is_text() { charset_decoder( reader, info.mime_type.as_deref(), info.charset.as_deref(), &mut outgoing_body_mode, ) } else { CharsetDecoder::PassThrough(reader) }; let reader = if info.is_text() && lossy_utf8 { MaybeLossyDecoder::Lossy(LossyUtf8Reader::new(reader)) } else { MaybeLossyDecoder::PassThrough(reader) }; BodyReader { outgoing_body_mode, reader, } } pub(crate) fn body_mode(&self) -> BodyMode { self.outgoing_body_mode } } #[allow(unused)] fn charset_decoder( reader: R, mime_type: Option<&str>, charset: Option<&str>, body_mode: &mut BodyMode, ) -> CharsetDecoder { #[cfg(feature = "charset")] { use encoding_rs::{Encoding, UTF_8}; let from = charset .and_then(|c| Encoding::for_label(c.as_bytes())) .unwrap_or(UTF_8); if from == UTF_8 { // Do nothing CharsetDecoder::PassThrough(reader) } else { debug!("Decoding charset {}", from.name()); *body_mode = BodyMode::Chunked; CharsetDecoder::Decoder(self::charset::CharCodec::new(reader, from, UTF_8)) } } #[cfg(not(feature = "charset"))] { CharsetDecoder::PassThrough(reader) } } enum MaybeLossyDecoder { Lossy(LossyUtf8Reader), PassThrough(R), } impl io::Read for MaybeLossyDecoder { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { MaybeLossyDecoder::Lossy(r) => r.read(buf), MaybeLossyDecoder::PassThrough(r) => r.read(buf), } } } impl<'a> io::Read for BodyReader<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.reader.read(buf) } } enum CharsetDecoder { #[cfg(feature = "charset")] Decoder(charset::CharCodec), PassThrough(R), } impl io::Read for CharsetDecoder { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { #[cfg(feature = "charset")] CharsetDecoder::Decoder(v) => v.read(buf), CharsetDecoder::PassThrough(v) => v.read(buf), } } } enum ContentDecoder { #[cfg(feature = "gzip")] Gzip(Box>), #[cfg(feature = "brotli")] Brotli(Box>), PassThrough(R), } impl io::Read for ContentDecoder { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { #[cfg(feature = "gzip")] ContentDecoder::Gzip(v) => v.read(buf), #[cfg(feature = "brotli")] ContentDecoder::Brotli(v) => v.read(buf), ContentDecoder::PassThrough(v) => v.read(buf), } } } impl fmt::Debug for Body { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Body").finish() } } impl From<&str> for ContentEncoding { fn from(s: &str) -> Self { match s { "gzip" => ContentEncoding::Gzip, "br" => ContentEncoding::Brotli, _ => { debug!("Unknown content-encoding: {}", s); ContentEncoding::Unknown } } } } impl<'a> From<&'a mut BodyDataSource> for BodySourceRef<'a> { fn from(value: &'a mut BodyDataSource) -> Self { match value { BodyDataSource::Handler(v) => Self::HandlerShared(v), BodyDataSource::Reader(v) => Self::ReaderShared(v), } } } impl From for BodySourceRef<'static> { fn from(value: BodyDataSource) -> Self { match value { BodyDataSource::Handler(v) => Self::HandlerOwned(v), BodyDataSource::Reader(v) => Self::ReaderOwned(v), } } } pub(crate) enum BodySourceRef<'a> { HandlerShared(&'a mut BodyHandler), HandlerOwned(Box), ReaderShared(&'a mut (dyn io::Read + Send + Sync)), ReaderOwned(Box), } impl<'a> io::Read for BodySourceRef<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { BodySourceRef::HandlerShared(v) => v.read(buf), BodySourceRef::HandlerOwned(v) => v.read(buf), BodySourceRef::ReaderShared(v) => v.read(buf), BodySourceRef::ReaderOwned(v) => v.read(buf), } } } #[cfg(all(test, feature = "_test"))] mod test { use crate::test::init_test_log; use crate::transport::set_handler; use crate::Error; #[test] fn content_type_without_charset() { init_test_log(); set_handler("/get", 200, &[("content-type", "application/json")], b"{}"); let res = crate::get("https://my.test/get").call().unwrap(); assert_eq!(res.body().mime_type(), Some("application/json")); assert!(res.body().charset().is_none()); } #[test] fn content_type_with_charset() { init_test_log(); set_handler( "/get", 200, &[("content-type", "application/json; charset=iso-8859-4")], b"{}", ); let res = crate::get("https://my.test/get").call().unwrap(); assert_eq!(res.body().mime_type(), Some("application/json")); assert_eq!(res.body().charset(), Some("iso-8859-4")); } #[test] fn chunked_transfer() { init_test_log(); let s = "3\r\n\ hel\r\n\ b\r\n\ lo world!!!\r\n\ 0\r\n\ \r\n"; set_handler( "/get", 200, &[("transfer-encoding", "chunked")], s.as_bytes(), ); let mut res = crate::get("https://my.test/get").call().unwrap(); let b = res.body_mut().read_to_string().unwrap(); assert_eq!(b, "hello world!!!"); } #[test] fn large_response_header() { init_test_log(); set_handler( "/get", 200, &[("content-type", &"b".repeat(64 * 1024))], b"{}", ); let err = crate::get("https://my.test/get").call().unwrap_err(); assert!(matches!(err, Error::LargeResponseHeader(_, _))); } } algesten-ureq-d58cf18/src/config.rs000066400000000000000000000743441505724604200173350ustar00rootroot00000000000000//! Agent configuration use std::fmt; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use http::Uri; use crate::middleware::{Middleware, MiddlewareChain}; use crate::{http, Body, Error}; use crate::{Agent, AsSendBody, Proxy, RequestBuilder}; #[cfg(feature = "_tls")] use crate::tls::TlsConfig; pub use ureq_proto::client::RedirectAuthHeaders; mod private { use super::Config; pub trait ConfigScope { fn config(&mut self) -> &mut Config; } } pub(crate) mod typestate { use super::*; use crate::request_ext::WithAgent; /// Typestate for [`Config`] when configured for an [`Agent`]. pub struct AgentScope(pub(crate) Config); /// Typestate for [`Config`] when configured on the [`RequestBuilder`] level. pub struct RequestScope(pub(crate) RequestBuilder); /// Typestate for for [`Config`] when configured via [`Agent::configure_request`]. pub struct HttpCrateScope(pub(crate) http::Request); /// Typestate for for [`Config`] when configured via [`crate::RequestExt::with_agent`]. pub struct RequestExtScope<'a, S: AsSendBody>(pub(crate) WithAgent<'a, S>); impl private::ConfigScope for AgentScope { fn config(&mut self) -> &mut Config { &mut self.0 } } impl private::ConfigScope for RequestScope { fn config(&mut self) -> &mut Config { self.0.request_level_config() } } impl private::ConfigScope for HttpCrateScope { fn config(&mut self) -> &mut Config { // This unwrap is OK, because we should not construct an // HttpCrateScope without first ensure it is there. let req_level: &mut RequestLevelConfig = self.0.extensions_mut().get_mut().unwrap(); &mut req_level.0 } } impl private::ConfigScope for RequestExtScope<'_, S> { fn config(&mut self) -> &mut Config { self.0.request_level_config() } } } use crate::config::typestate::RequestExtScope; use crate::http::Response; use crate::request_ext::WithAgent; use typestate::AgentScope; use typestate::HttpCrateScope; use typestate::RequestScope; /// Config primarily for the [`Agent`], but also per-request. /// /// Config objects are cheap to clone and should not incur any heap allocations. /// /// # Agent level config /// /// ## Example /// /// ``` /// use ureq::Agent; /// use std::time::Duration; /// /// let config = Agent::config_builder() /// .timeout_global(Some(Duration::from_secs(10))) /// .https_only(true) /// .build(); /// /// let agent = Agent::new_with_config(config); /// ``` /// /// /// # Request level config /// /// The config can also be change per-request. Since every request ultimately executes /// using an [`Agent`] (also the root-level `ureq::get(...)` have an implicit agent), /// a request level config clones the agent level config. /// /// There are two ways of getting a request level config. /// /// ## Request builder example /// /// The first way is via [`RequestBuilder::config()`][crate::RequestBuilder::config]. /// /// ``` /// use ureq::Agent; /// /// let agent: Agent = Agent::config_builder() /// .https_only(false) /// .build() /// .into(); /// /// let response = agent.get("http://httpbin.org/get") /// .config() /// // override agent level setting for this request /// .https_only(true) /// .build() /// .call(); /// ``` /// /// ## HTTP request example /// /// The second way is via [`Agent::configure_request()`][crate::Agent::configure_request]. /// This is used when working with the http crate [`http::Request`] type directly. /// /// ``` /// use ureq::{http, Agent}; /// /// let agent: Agent = Agent::config_builder() /// .https_only(false) /// .build() /// .into(); /// /// let request = http::Request::get("http://httpbin.org/get") /// .body(()).unwrap(); /// /// let request = agent.configure_request(request) /// // override agent level setting for this request /// .https_only(true) /// .build(); /// /// let response = agent.run(request); /// ``` /// #[derive(Clone)] pub struct Config { http_status_as_error: bool, https_only: bool, ip_family: IpFamily, #[cfg(feature = "_tls")] tls_config: TlsConfig, proxy: Option, no_delay: bool, max_redirects: u32, max_redirects_will_error: bool, redirect_auth_headers: RedirectAuthHeaders, save_redirect_history: bool, user_agent: AutoHeaderValue, accept: AutoHeaderValue, accept_encoding: AutoHeaderValue, timeouts: Timeouts, max_response_header_size: usize, input_buffer_size: usize, output_buffer_size: usize, max_idle_connections: usize, max_idle_connections_per_host: usize, max_idle_age: Duration, allow_non_standard_methods: bool, // Chain built for middleware. pub(crate) middleware: MiddlewareChain, } impl Config { /// A builder to make a bespoke configuration. /// /// The default values are already set. pub fn builder() -> ConfigBuilder { ConfigBuilder(AgentScope(Config::default())) } /// Creates a new agent by cloning this config. /// /// Cloning the config does not incur heap allocations. pub fn new_agent(&self) -> Agent { self.clone().into() } pub(crate) fn connect_proxy_uri(&self) -> Option<&Uri> { let proxy = self.proxy.as_ref()?; if !proxy.protocol().is_connect() { return None; } Some(proxy.uri()) } pub(crate) fn max_redirects_do_error(&self) -> bool { self.max_redirects > 0 && self.max_redirects_will_error } pub(crate) fn clone_without_proxy(&self) -> Self { let mut c = self.clone(); c.proxy = None; c } } impl Config { /// Whether to treat 4xx and 5xx HTTP status codes as /// [`Err(Error::StatusCode))`](crate::Error::StatusCode). /// /// Defaults to `true`. pub fn http_status_as_error(&self) -> bool { self.http_status_as_error } /// Whether to limit requests (including redirects) to https only /// /// Defaults to `false`. pub fn https_only(&self) -> bool { self.https_only } /// Configuration of IPv4/IPv6. /// /// This affects the resolver. /// /// Defaults to `IpFamily::Any`. pub fn ip_family(&self) -> IpFamily { self.ip_family } /// Config for TLS. /// /// This config is generic for all TLS connectors. #[cfg(feature = "_tls")] pub fn tls_config(&self) -> &TlsConfig { &self.tls_config } /// Proxy configuration. /// /// Picked up from environment when using [`Config::default()`] or pub fn proxy(&self) -> Option<&Proxy> { self.proxy.as_ref() } /// Disable Nagle's algorithm /// /// Set TCP_NODELAY. It's up to the transport whether this flag is honored. /// /// Defaults to `true`. pub fn no_delay(&self) -> bool { self.no_delay } /// The max number of redirects to follow before giving up. /// /// Whe max redirects are reached, the behavior is controlled by the /// `max_redirects_will_error` setting. Set to `true` (which /// is the default) the result is a `TooManyRedirects` error. Set /// to `false`, the response is returned as is. /// /// If `max_redirects` is 0, no redirects are followed and the response /// is always returned (never a `TooManyRedirects` error). /// /// Defaults to 10 pub fn max_redirects(&self) -> u32 { self.max_redirects } /// If we should error when max redirects are reached. /// /// This has no meaning if `max_redirects` is 0. /// /// Defaults to true pub fn max_redirects_will_error(&self) -> bool { self.max_redirects_will_error } /// How to handle `Authorization` headers when following redirects /// /// * `Never` (the default) means the authorization header is never attached to a redirected call. /// * `SameHost` will keep the header when the redirect is to the same host and under https. /// /// Defaults to `None`. pub fn redirect_auth_headers(&self) -> RedirectAuthHeaders { self.redirect_auth_headers } /// If we should record a history of every redirect location, /// including the request and final locations. /// /// Comes at the cost of allocating/retaining the `Uri` for /// every redirect loop. /// /// See [`ResponseExt::get_redirect_history()`][crate::ResponseExt::get_redirect_history]. /// /// Defaults to `false`. pub fn save_redirect_history(&self) -> bool { self.save_redirect_history } /// Value to use for the `User-Agent` header. /// /// This can be overridden by setting a `user-agent` header on the request /// object. The one difference is that a connection to a HTTP proxy server /// will receive this value, not the request-level one. /// /// Setting a value of `""` on the request or agent level will also not send a header. /// /// Defaults to `Default`, which results in `ureq/` pub fn user_agent(&self) -> &AutoHeaderValue { &self.user_agent } /// Value to use for the `Accept` header. /// /// This agent configured value can be overriden per request by setting the header. // /// Setting a value of `""` on the request or agent level will also not send a header. /// /// Defaults to `Default`, which results in `*/*` pub fn accept(&self) -> &AutoHeaderValue { &self.accept } /// Value to use for the `Accept-Encoding` header. /// /// Defaults to `Default`, which will add `gz` and `brotli` depending on /// the feature flags **gzip** and **brotli** respectively. If neither /// feature is enabled, the header is not added. /// /// This agent configured value can be overriden per request by setting the header. /// /// Setting a value of `""` on the request or agent level will also not send a header. /// /// This communicates capability to the server, however the triggering the /// automatic decompression behavior is not affected since that only looks /// at the `Content-Encoding` response header. pub fn accept_encoding(&self) -> &AutoHeaderValue { &self.accept_encoding } /// All configured timeouts. pub fn timeouts(&self) -> Timeouts { self.timeouts } /// Max size of the HTTP response header. /// /// From the status, including all headers up until the body. /// /// Defaults to 64kb. pub fn max_response_header_size(&self) -> usize { self.max_response_header_size } /// Default size of the input buffer /// /// The default connectors use this setting. /// /// Defaults to 128kb. pub fn input_buffer_size(&self) -> usize { self.input_buffer_size } /// Default size of the output buffer. /// /// The default connectors use this setting. /// /// Defaults to 128kb. pub fn output_buffer_size(&self) -> usize { self.output_buffer_size } /// Max number of idle pooled connections overall. /// /// This setting has no effect when used per-request. /// /// Defaults to 10 pub fn max_idle_connections(&self) -> usize { self.max_idle_connections } /// Max number of idle pooled connections per host/port combo. /// /// This setting has no effect when used per-request. /// /// Defaults to 3 pub fn max_idle_connections_per_host(&self) -> usize { self.max_idle_connections_per_host } /// Max duration to keep an idle connection in the pool /// /// This can also be configured per-request to be shorter than the pool. /// For example: if the pool is configured to 15 seconds and we have a /// connection with an age of 10 seconds, a request setting this config /// property to 3 seconds, would ignore the pooled connection (but still /// leave it in the pool). /// /// Defaults to 15 seconds pub fn max_idle_age(&self) -> Duration { self.max_idle_age } /// Whether to allow non-standard HTTP methods. /// /// By default the methods are limited by the HTTP version. /// /// Defaults to false pub fn allow_non_standard_methods(&self) -> bool { self.allow_non_standard_methods } } /// Builder of [`Config`] pub struct ConfigBuilder(pub(crate) Scope); impl ConfigBuilder { fn config(&mut self) -> &mut Config { self.0.config() } /// Whether to treat 4xx and 5xx HTTP status codes as /// [`Err(Error::StatusCode))`](crate::Error::StatusCode). /// /// Defaults to `true`. pub fn http_status_as_error(mut self, v: bool) -> Self { self.config().http_status_as_error = v; self } /// Whether to limit requests (including redirects) to https only /// /// Defaults to `false`. pub fn https_only(mut self, v: bool) -> Self { self.config().https_only = v; self } /// Configuration of IPv4/IPv6. /// /// This affects the resolver. /// /// Defaults to `IpFamily::Any`. pub fn ip_family(mut self, v: IpFamily) -> Self { self.config().ip_family = v; self } /// Config for TLS. /// /// This config is generic for all TLS connectors. #[cfg(feature = "_tls")] pub fn tls_config(mut self, v: TlsConfig) -> Self { self.config().tls_config = v; self } /// Proxy configuration. /// /// Picked up from environment when using [`Config::default()`] or /// [`Agent::new_with_defaults()`][crate::Agent::new_with_defaults]. pub fn proxy(mut self, v: Option) -> Self { self.config().proxy = v; self } /// Disable Nagle's algorithm /// /// Set TCP_NODELAY. It's up to the transport whether this flag is honored. /// /// Defaults to `true`. pub fn no_delay(mut self, v: bool) -> Self { self.config().no_delay = v; self } /// The max number of redirects to follow before giving up. /// /// Whe max redirects are reached, the behavior is controlled by the /// `max_redirects_will_error` setting. Set to `true` (which /// is the default) the result is a `TooManyRedirects` error. Set /// to `false`, the response is returned as is. /// /// If `max_redirects` is 0, no redirects are followed and the response /// is always returned (never a `TooManyRedirects` error). /// /// Defaults to 10 pub fn max_redirects(mut self, v: u32) -> Self { self.config().max_redirects = v; self } /// If we should error when max redirects are reached. /// /// This has no meaning if `max_redirects` is 0. /// /// Defaults to true pub fn max_redirects_will_error(mut self, v: bool) -> Self { self.config().max_redirects_will_error = v; self } /// How to handle `Authorization` headers when following redirects /// /// * `Never` (the default) means the authorization header is never attached to a redirected call. /// * `SameHost` will keep the header when the redirect is to the same host and under https. /// /// Defaults to `None`. pub fn redirect_auth_headers(mut self, v: RedirectAuthHeaders) -> Self { self.config().redirect_auth_headers = v; self } /// If we should record a history of every redirect location, /// including the request and final locations. /// /// Comes at the cost of allocating/retaining the `Uri` for /// every redirect loop. /// /// See [`ResponseExt::get_redirect_history()`][crate::ResponseExt::get_redirect_history]. /// /// Defaults to `false`. pub fn save_redirect_history(mut self, v: bool) -> Self { self.config().save_redirect_history = v; self } /// Value to use for the `User-Agent` header. /// /// This can be overridden by setting a `user-agent` header on the request /// object. The one difference is that a connection to a HTTP proxy server /// will receive this value, not the request-level one. /// /// Setting a value of `""` on the request or agent level will also not send a header. /// /// Defaults to `Default`, which results in `ureq/` pub fn user_agent(mut self, v: impl Into) -> Self { self.config().user_agent = v.into(); self } /// Value to use for the `Accept` header. /// /// This agent configured value can be overriden per request by setting the header. // /// Setting a value of `""` on the request or agent level will also not send a header. /// /// Defaults to `Default`, which results in `*/*` pub fn accept(mut self, v: impl Into) -> Self { self.config().accept = v.into(); self } /// Value to use for the `Accept-Encoding` header. /// /// Defaults to `Default`, which will add `gz` and `brotli` depending on /// the feature flags **gzip** and **brotli** respectively. If neither /// feature is enabled, the header is not added. /// /// This agent configured value can be overriden per request by setting the header. /// /// Setting a value of `""` on the request or agent level will also not send a header. /// /// This communicates capability to the server, however the triggering the /// automatic decompression behavior is not affected since that only looks /// at the `Content-Encoding` response header. pub fn accept_encoding(mut self, v: impl Into) -> Self { self.config().accept_encoding = v.into(); self } /// Max size of the HTTP response header. /// /// From the status, including all headers up until the body. /// /// Defaults to 64kb. pub fn max_response_header_size(mut self, v: usize) -> Self { self.config().max_response_header_size = v; self } /// Default size of the input buffer /// /// The default connectors use this setting. /// /// Defaults to 128kb. pub fn input_buffer_size(mut self, v: usize) -> Self { self.config().input_buffer_size = v; self } /// Default size of the output buffer. /// /// The default connectors use this setting. /// /// Defaults to 128kb. pub fn output_buffer_size(mut self, v: usize) -> Self { self.config().output_buffer_size = v; self } /// Max number of idle pooled connections overall. /// /// This setting has no effect when used per-request. /// /// Defaults to 10 pub fn max_idle_connections(mut self, v: usize) -> Self { self.config().max_idle_connections = v; self } /// Max number of idle pooled connections per host/port combo. /// /// This setting has no effect when used per-request. /// /// Defaults to 3 pub fn max_idle_connections_per_host(mut self, v: usize) -> Self { self.config().max_idle_connections_per_host = v; self } /// Max duration to keep an idle connection in the pool /// /// This can also be configured per-request to be shorter than the pool. /// For example: if the pool is configured to 15 seconds and we have a /// connection with an age of 10 seconds, a request setting this config /// property to 3 seconds, would ignore the pooled connection (but still /// leave it in the pool). /// /// Defaults to 15 seconds pub fn max_idle_age(mut self, v: Duration) -> Self { self.config().max_idle_age = v; self } /// Whether to allow non-standard HTTP methods. /// /// By default the methods are limited by the HTTP version. /// /// Defaults to false pub fn allow_non_standard_methods(mut self, v: bool) -> Self { self.config().allow_non_standard_methods = v; self } /// Add middleware to use for each request in this agent. /// /// Defaults to no middleware. pub fn middleware(mut self, v: impl Middleware) -> Self { self.config().middleware.add(v); self } /// Timeout for the entire call /// /// This is end-to-end, from DNS lookup to finishing reading the response body. /// Thus it covers all other timeouts. /// /// Defaults to `None`. pub fn timeout_global(mut self, v: Option) -> Self { self.config().timeouts.global = v; self } /// Timeout for call-by-call when following redirects /// /// This covers a single call and the timeout is reset when /// ureq follows a redirections. /// /// Defaults to `None`.. pub fn timeout_per_call(mut self, v: Option) -> Self { self.config().timeouts.per_call = v; self } /// Max duration for doing the DNS lookup when establishing the connection /// /// Because most platforms do not have an async syscall for looking up /// a host name, setting this might force str0m to spawn a thread to handle /// the timeout. /// /// Defaults to `None`. pub fn timeout_resolve(mut self, v: Option) -> Self { self.config().timeouts.resolve = v; self } /// Max duration for establishing the connection /// /// For a TLS connection this includes opening the socket and doing the TLS handshake. /// /// Defaults to `None`. pub fn timeout_connect(mut self, v: Option) -> Self { self.config().timeouts.connect = v; self } /// Max duration for sending the request, but not the request body. /// /// Defaults to `None`. pub fn timeout_send_request(mut self, v: Option) -> Self { self.config().timeouts.send_request = v; self } /// Max duration for awaiting a 100-continue response. /// /// Only used if there is a request body and we sent the `Expect: 100-continue` /// header to indicate we want the server to respond with 100. /// /// This defaults to 1 second. pub fn timeout_await_100(mut self, v: Option) -> Self { self.config().timeouts.await_100 = v; self } /// Max duration for sending a request body (if there is one) /// /// Defaults to `None`. pub fn timeout_send_body(mut self, v: Option) -> Self { self.config().timeouts.send_body = v; self } /// Max duration for receiving the response headers, but not the body /// /// Defaults to `None`. pub fn timeout_recv_response(mut self, v: Option) -> Self { self.config().timeouts.recv_response = v; self } /// Max duration for receving the response body. /// /// Defaults to `None`. pub fn timeout_recv_body(mut self, v: Option) -> Self { self.config().timeouts.recv_body = v; self } } /// Possible config values for headers. /// /// * `None` no automatic header /// * `Default` default behavior. I.e. for user-agent something like `ureq/3.1.2` /// * `Provided` is a user provided header #[derive(Debug, Clone)] pub enum AutoHeaderValue { /// No automatic header. None, /// Default behavior. /// /// I.e. for user-agent something like `ureq/3.1.2`. Default, /// User provided header value. Provided(Arc), } impl Default for AutoHeaderValue { fn default() -> Self { Self::Default } } impl AutoHeaderValue { pub(crate) fn as_str(&self, default: &'static str) -> Option<&str> { let x = match self { AutoHeaderValue::None => "", AutoHeaderValue::Default => default, AutoHeaderValue::Provided(v) => v.as_str(), }; if x.is_empty() { None } else { Some(x) } } } impl> From for AutoHeaderValue { fn from(value: S) -> Self { match value.as_ref() { "" => Self::None, _ => Self::Provided(Arc::new(value.as_ref().to_owned())), } } } impl ConfigBuilder { /// Finalize the config pub fn build(self) -> Config { self.0 .0 } } impl ConfigBuilder> { /// Finalize the config pub fn build(self) -> RequestBuilder { self.0 .0 } } impl ConfigBuilder> { /// Finalize the config pub fn build(self) -> http::Request { self.0 .0 } } impl<'a, S: AsSendBody> ConfigBuilder> { /// Finalize the config pub fn build(self) -> WithAgent<'a, S> { self.0 .0 } /// Run the request with the agent in the ConfigBuilder pub fn run(self) -> Result, Error> { self.0 .0.run() } } /// Request timeout configuration. /// /// This can be configured both on Agent level as well as per request. #[derive(Clone, Copy)] pub struct Timeouts { /// Timeout for the entire call pub global: Option, /// Timeout for call-by-call when following redirects pub per_call: Option, /// Max duration for doing the DNS lookup when establishing the connection pub resolve: Option, /// Max duration for establishing the connection pub connect: Option, /// Max duration for sending the request, but not the request body. pub send_request: Option, /// Max duration for awaiting a 100-continue response. pub await_100: Option, /// Max duration for sending a request body (if there is one) pub send_body: Option, /// Max duration for receiving the response headers, but not the body pub recv_response: Option, /// Max duration for receving the response body. pub recv_body: Option, } #[derive(Debug, Clone)] pub(crate) struct RequestLevelConfig(pub Config); pub(crate) static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); impl Default for Config { fn default() -> Self { Self { http_status_as_error: true, https_only: false, ip_family: IpFamily::Any, #[cfg(feature = "_tls")] tls_config: TlsConfig::default(), proxy: Proxy::try_from_env(), no_delay: true, max_redirects: 10, max_redirects_will_error: true, redirect_auth_headers: RedirectAuthHeaders::Never, save_redirect_history: false, user_agent: AutoHeaderValue::default(), accept: AutoHeaderValue::default(), accept_encoding: AutoHeaderValue::default(), timeouts: Timeouts::default(), max_response_header_size: 64 * 1024, input_buffer_size: 128 * 1024, output_buffer_size: 128 * 1024, max_idle_connections: 10, max_idle_connections_per_host: 3, max_idle_age: Duration::from_secs(15), allow_non_standard_methods: false, middleware: MiddlewareChain::default(), } } } impl Default for Timeouts { fn default() -> Self { Self { global: None, per_call: None, resolve: None, connect: None, send_request: None, await_100: Some(Duration::from_secs(1)), send_body: None, recv_response: None, recv_body: None, } } } /// Configuration of IP family to use. /// /// Used to limit the IP to either IPv4, IPv6 or any. // TODO(martin): make this configurable #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IpFamily { /// Both Ipv4 and Ipv6 Any, /// Just Ipv4 Ipv4Only, /// Just Ipv6 Ipv6Only, } impl IpFamily { /// Filter the socket addresses to the family of IP. pub fn keep_wanted<'a>( &'a self, iter: impl Iterator + 'a, ) -> impl Iterator + 'a { iter.filter(move |a| self.is_wanted(a)) } fn is_wanted(&self, addr: &SocketAddr) -> bool { match self { IpFamily::Any => true, IpFamily::Ipv4Only => addr.is_ipv4(), IpFamily::Ipv6Only => addr.is_ipv6(), } } } impl fmt::Debug for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut dbg = f.debug_struct("Config"); dbg.field("http_status_as_error", &self.http_status_as_error) .field("https_only", &self.https_only) .field("ip_family", &self.ip_family) .field("proxy", &self.proxy) .field("no_delay", &self.no_delay) .field("max_redirects", &self.max_redirects) .field("redirect_auth_headers", &self.redirect_auth_headers) .field("save_redirect_history", &self.save_redirect_history) .field("user_agent", &self.user_agent) .field("timeouts", &self.timeouts) .field("max_response_header_size", &self.max_response_header_size) .field("input_buffer_size", &self.input_buffer_size) .field("output_buffer_size", &self.output_buffer_size) .field("max_idle_connections", &self.max_idle_connections) .field( "max_idle_connections_per_host", &self.max_idle_connections_per_host, ) .field("max_idle_age", &self.max_idle_age) .field("middleware", &self.middleware); #[cfg(feature = "_tls")] { dbg.field("tls_config", &self.tls_config); } dbg.finish() } } impl fmt::Debug for Timeouts { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Timeouts") .field("global", &self.global) .field("per_call", &self.per_call) .field("resolve", &self.resolve) .field("connect", &self.connect) .field("send_request", &self.send_request) .field("await_100", &self.await_100) .field("send_body", &self.send_body) .field("recv_response", &self.recv_response) .field("recv_body", &self.recv_body) .finish() } } #[cfg(test)] mod test { use super::*; use assert_no_alloc::*; #[test] fn default_config_clone_does_not_allocate() { let c = Config::default(); assert_no_alloc(|| c.clone()); } } algesten-ureq-d58cf18/src/cookies.rs000066400000000000000000000245361505724604200175220ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt; use std::iter; use std::sync::{Mutex, MutexGuard}; use cookie_store::CookieStore; use http::Uri; use crate::http; use crate::util::UriExt; use crate::Error; #[cfg(feature = "json")] use std::io; #[derive(Debug)] pub(crate) struct SharedCookieJar { inner: Mutex, } /// Collection of cookies. /// /// The jar is accessed using [`Agent::cookie_jar_lock`][crate::Agent::cookie_jar_lock]. /// It can be saved and loaded. pub struct CookieJar<'a>(MutexGuard<'a, CookieStore>); /// Representation of an HTTP cookie. /// /// Conforms to [IETF RFC6265](https://datatracker.ietf.org/doc/html/rfc6265) /// /// ## Constructing a `Cookie` /// /// To construct a cookie it must be parsed and bound to a uri: /// /// ``` /// use ureq::Cookie; /// use ureq::http::Uri; /// /// let uri = Uri::from_static("https://my.server.com"); /// let cookie = Cookie::parse("name=value", &uri)?; /// assert_eq!(cookie.to_string(), "name=value"); /// # Ok::<_, ureq::Error>(()) /// ``` pub struct Cookie<'a>(CookieInner<'a>); #[allow(clippy::large_enum_variant)] enum CookieInner<'a> { Borrowed(&'a cookie_store::Cookie<'a>), Owned(cookie_store::Cookie<'a>), } impl<'a> CookieInner<'a> { fn into_static(self) -> cookie_store::Cookie<'static> { match self { CookieInner::Borrowed(v) => v.clone().into_owned(), CookieInner::Owned(v) => v.into_owned(), } } } impl<'a> Cookie<'a> { /// Parses a new [`Cookie`] from a string pub fn parse(cookie_str: S, uri: &Uri) -> Result, Error> where S: Into>, { let cookie = cookie_store::Cookie::parse(cookie_str, &uri.try_into_url()?)?; Ok(Cookie(CookieInner::Owned(cookie))) } /// The cookie's name. pub fn name(&self) -> &str { match &self.0 { CookieInner::Borrowed(v) => v.name(), CookieInner::Owned(v) => v.name(), } } /// The cookie's value. pub fn value(&self) -> &str { match &self.0 { CookieInner::Borrowed(v) => v.value(), CookieInner::Owned(v) => v.value(), } } #[cfg(test)] fn as_cookie_store(&self) -> &cookie_store::Cookie<'a> { match &self.0 { CookieInner::Borrowed(v) => v, CookieInner::Owned(v) => v, } } } impl Cookie<'static> { fn into_owned(self) -> cookie_store::Cookie<'static> { match self.0 { CookieInner::Owned(v) => v, _ => unreachable!(), } } } impl<'a> CookieJar<'a> { /// Returns a reference to the __unexpired__ `Cookie` corresponding to the specified `domain`, /// `path`, and `name`. pub fn get(&self, domain: &str, path: &str, name: &str) -> Option> { self.0 .get(domain, path, name) .map(|c| Cookie(CookieInner::Borrowed(c))) } /// Removes a `Cookie` from the jar, returning the `Cookie` if it was in the jar pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option> { self.0 .remove(domain, path, name) .map(|c| Cookie(CookieInner::Owned(c))) } /// Inserts `cookie`, received from `uri`, into the jar, following the rules of the /// [IETF RFC6265 Storage Model](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3). pub fn insert(&mut self, cookie: Cookie<'static>, uri: &Uri) -> Result<(), Error> { let url = uri.try_into_url()?; self.0.insert(cookie.into_owned(), &url)?; Ok(()) } /// Clear the contents of the jar pub fn clear(&mut self) { self.0.clear() } /// An iterator visiting all the __unexpired__ cookies in the jar pub fn iter(&self) -> impl Iterator> { self.0 .iter_unexpired() .map(|c| Cookie(CookieInner::Borrowed(c))) } /// Serialize any __unexpired__ and __persistent__ cookies in the jar to JSON format and /// write them to `writer` #[cfg(feature = "json")] pub fn save_json(&self, writer: &mut W) -> Result<(), Error> { Ok(cookie_store::serde::json::save(&self.0, writer)?) } /// Load JSON-formatted cookies from `reader`, skipping any __expired__ cookies /// /// Replaces all the contents of the current cookie jar. #[cfg(feature = "json")] pub fn load_json(&mut self, reader: R) -> Result<(), Error> { let store = cookie_store::serde::json::load(reader)?; *self.0 = store; Ok(()) } pub(crate) fn store_response_cookies<'b>( &mut self, iter: impl Iterator>, uri: &Uri, ) { let url = uri.try_into_url().expect("uri to be a url"); let raw_cookies = iter.map(|c| c.0.into_static().into()); self.0.store_response_cookies(raw_cookies, &url); } /// Release the cookie jar. pub fn release(self) {} } // CookieStore::new() changes parameters depending on feature flag "public_suffix". // That means if a user enables public_suffix for CookieStore through diamond dependency, // we start having compilation errors un ureq. // // This workaround instantiates a CookieStore in a way that does not change with flags. fn instantiate_cookie_store() -> CookieStore { let i = iter::empty::, &str>>(); CookieStore::from_cookies(i, true).unwrap() } impl SharedCookieJar { pub(crate) fn new() -> Self { SharedCookieJar { inner: Mutex::new(instantiate_cookie_store()), } } pub(crate) fn lock(&self) -> CookieJar<'_> { let lock = self.inner.lock().unwrap(); CookieJar(lock) } pub(crate) fn get_request_cookies(&self, uri: &Uri) -> String { let mut cookies = String::new(); let url = match uri.try_into_url() { Ok(v) => v, Err(e) => { debug!("Bad url for cookie: {:?}", e); return cookies; } }; let store = self.inner.lock().unwrap(); for c in store.matches(&url) { if !is_cookie_rfc_compliant(c) { debug!("Do not send non compliant cookie: {:?}", c.name()); continue; } if !cookies.is_empty() { cookies.push(';'); } cookies.push_str(&c.to_string()); } cookies } } fn is_cookie_rfc_compliant(cookie: &cookie_store::Cookie) -> bool { // https://tools.ietf.org/html/rfc6265#page-9 // set-cookie-header = "Set-Cookie:" SP set-cookie-string // set-cookie-string = cookie-pair *( ";" SP cookie-av ) // cookie-pair = cookie-name "=" cookie-value // cookie-name = token // cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E // ; US-ASCII characters excluding CTLs, // ; whitespace DQUOTE, comma, semicolon, // ; and backslash // token = // https://tools.ietf.org/html/rfc2616#page-17 // CHAR = // ... // CTL = // ... // token = 1* // separators = "(" | ")" | "<" | ">" | "@" // | "," | ";" | ":" | "\" | <"> // | "/" | "[" | "]" | "?" | "=" // | "{" | "}" | SP | HT fn is_valid_name(b: &u8) -> bool { is_tchar(b) } fn is_valid_value(b: &u8) -> bool { b.is_ascii() && !b.is_ascii_control() && !b.is_ascii_whitespace() && *b != b'"' && *b != b',' && *b != b';' && *b != b'\\' } let name = cookie.name().as_bytes(); let valid_name = name.iter().all(is_valid_name); if !valid_name { log::trace!("cookie name is not valid: {:?}", cookie.name()); return false; } let value = cookie.value().as_bytes(); let valid_value = value .strip_prefix(br#"""#) .and_then(|value| value.strip_suffix(br#"""#)) .unwrap_or(value) .iter() .all(is_valid_value); if !valid_value { // NB. Do not log cookie value since it might be secret log::trace!("cookie value is not valid: {:?}", cookie.name()); return false; } true } #[inline] pub(crate) fn is_tchar(b: &u8) -> bool { match b { b'!' | b'#' | b'$' | b'%' | b'&' => true, b'\'' | b'*' | b'+' | b'-' | b'.' => true, b'^' | b'_' | b'`' | b'|' | b'~' => true, b if b.is_ascii_alphanumeric() => true, _ => false, } } impl fmt::Display for Cookie<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { CookieInner::Borrowed(v) => v.fmt(f), CookieInner::Owned(v) => v.fmt(f), } } } #[cfg(test)] mod test { use std::convert::TryFrom; use super::*; fn uri() -> Uri { Uri::try_from("https://example.test").unwrap() } #[test] fn illegal_cookie_name() { let cookie = Cookie::parse("borked/=value", &uri()).unwrap(); assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store())); } #[test] fn illegal_cookie_value() { let cookie = Cookie::parse("name=borked,", &uri()).unwrap(); assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store())); let cookie = Cookie::parse("name=\"borked", &uri()).unwrap(); assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store())); let cookie = Cookie::parse("name=borked\"", &uri()).unwrap(); assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store())); let cookie = Cookie::parse("name=\"\"borked\"", &uri()).unwrap(); assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store())); } #[test] fn legal_cookie_name_value() { let cookie = Cookie::parse("name=value", &uri()).unwrap(); assert!(is_cookie_rfc_compliant(cookie.as_cookie_store())); let cookie = Cookie::parse("name=\"value\"", &uri()).unwrap(); assert!(is_cookie_rfc_compliant(cookie.as_cookie_store())); } } algesten-ureq-d58cf18/src/error.rs000066400000000000000000000277041505724604200172170ustar00rootroot00000000000000use std::{fmt, io}; use crate::http; use crate::Timeout; /// Errors from ureq. #[derive(Debug)] #[non_exhaustive] pub enum Error { /// When [`http_status_as_error()`](crate::config::ConfigBuilder::http_status_as_error) is true, /// 4xx and 5xx response status codes are translated to this error. /// /// This is the default behavior. StatusCode(u16), /// Errors arising from the http-crate. /// /// These errors happen for things like invalid characters in header names. Http(http::Error), /// Error if the URI is missing scheme or host. BadUri(String), /// An HTTP/1.1 protocol error. /// /// This can happen if the remote server ends incorrect HTTP data like /// missing version or invalid chunked transfer. Protocol(ureq_proto::Error), /// Error in io such as the TCP socket. Io(io::Error), /// Error raised if the request hits any configured timeout. /// /// By default no timeouts are set, which means this error can't happen. Timeout(Timeout), /// Error when resolving a hostname fails. HostNotFound, /// A redirect failed. /// /// This happens when ureq encounters a redirect when sending a request body /// such as a POST request, and receives a 307/308 response. ureq refuses to /// redirect the POST body and instead raises this error. RedirectFailed, /// Error when creating proxy settings. InvalidProxyUrl, /// A connection failed. /// /// This is a fallback error when there is no other explanation as to /// why a connector chain didn't produce a connection. The idea is that the connector /// chain would return some other [`Error`] rather than rely om this value. /// /// Typically bespoke connector chains should, as far as possible, map their underlying /// errors to [`Error::Io`] and use the [`io::ErrorKind`] to provide a reason. /// /// A bespoke chain is allowed to map to this value, but that provides very little /// information to the user as to why the connection failed. One way to mitigate that /// would be to rely on the `log` crate to provide additional information. ConnectionFailed, /// A send body (Such as `&str`) is larger than the `content-length` header. BodyExceedsLimit(u64), /// Too many redirects. /// /// The error can be turned off by setting /// [`max_redirects_will_error()`](crate::config::ConfigBuilder::max_redirects_will_error) /// to false. When turned off, the last response will be returned instead of causing /// an error, even if it is a redirect. /// /// The number of redirects is limited to 10 by default. TooManyRedirects, /// Some error with TLS. // #[cfg(feature = "_tls")] This is deliberately not _tls, because we want to let // bespoke TLS transports use this error code. Tls(&'static str), /// Error in reading PEM certificates/private keys. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "_tls")] Pem(rustls_pemfile::Error), /// An error originating in Rustls. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "_rustls")] Rustls(rustls::Error), /// An error originating in Native-TLS. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "native-tls")] NativeTls(native_tls::Error), /// An error providing DER encoded certificates or private keys to Native-TLS. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "native-tls")] Der(der::Error), /// An error with the cookies. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "cookies")] Cookie(cookie_store::CookieError), /// An error parsing a cookie value. #[cfg(feature = "cookies")] CookieValue(&'static str), /// An error in the cookie store. /// /// *Note:* The wrapped error struct is not considered part of ureq API. /// Breaking changes in that struct will not be reflected in ureq /// major versions. #[cfg(feature = "cookies")] CookieJar(cookie_store::Error), /// An unrecognised character set. #[cfg(feature = "charset")] UnknownCharset(String), /// The setting [`https_only`](crate::config::ConfigBuilder::https_only) is true and /// the URI is not https. RequireHttpsOnly(String), /// The response header, from status up until body, is too big. LargeResponseHeader(usize, usize), /// Body decompression failed (gzip or brotli). #[cfg(any(feature = "gzip", feature = "brotli"))] Decompress(&'static str, io::Error), /// Serde JSON error. #[cfg(feature = "json")] Json(serde_json::Error), /// Attempt to connect to a CONNECT proxy failed. ConnectProxyFailed(String), /// The protocol requires TLS (https), but the connector did not /// create a TLS secured transport. /// /// This typically indicates a fault in bespoke `Connector` chains. TlsRequired, /// Some other error occured. /// /// This is an escape hatch for bespoke connector chains having errors that don't naturally /// map to any other error. For connector chains we recommend: /// /// 1. Map to [`Error::Io`] as far as possible. /// 2. Map to other [`Error`] where reasonable. /// 3. Fall back on [`Error::Other`]. /// 4. As a last resort [`Error::ConnectionFailed`]. /// /// ureq does not produce this error using the default connectors. Other(Box), /// ureq-proto made no progress and there is no more input to read. /// /// We should never see this value. #[doc(hidden)] BodyStalled, } impl std::error::Error for Error {} impl Error { /// Convert the error into a [`std::io::Error`]. /// /// If the error is [`Error::Io`], we unpack the error. In othe cases we make /// an `std::io::ErrorKind::Other`. pub fn into_io(self) -> io::Error { if let Self::Io(e) = self { e } else { io::Error::new(io::ErrorKind::Other, self) } } pub(crate) fn disconnected(reason: &'static str) -> Error { trace!("UnexpectedEof reason: {}", reason); io::Error::new(io::ErrorKind::UnexpectedEof, "Peer disconnected").into() } } pub(crate) fn is_wrapped_ureq_error(e: &io::Error) -> bool { e.get_ref().map(|x| x.is::()).unwrap_or(false) } impl From for Error { fn from(e: io::Error) -> Self { if is_wrapped_ureq_error(&e) { // unwraps are ok, see above. let boxed = e.into_inner().unwrap(); let ureq = boxed.downcast::().unwrap(); *ureq } else { Error::Io(e) } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::StatusCode(v) => write!(f, "http status: {}", v), Error::Http(v) => write!(f, "http: {}", v), Error::BadUri(v) => write!(f, "bad uri: {}", v), Error::Protocol(v) => write!(f, "protocol: {}", v), Error::Io(v) => write!(f, "io: {}", v), Error::Timeout(v) => write!(f, "timeout: {}", v), Error::HostNotFound => write!(f, "host not found"), Error::RedirectFailed => write!(f, "redirect failed"), Error::InvalidProxyUrl => write!(f, "invalid proxy url"), Error::ConnectionFailed => write!(f, "connection failed"), Error::BodyExceedsLimit(v) => { write!(f, "the response body is larger than request limit: {}", v) } Error::TooManyRedirects => write!(f, "too many redirects"), Error::Tls(v) => write!(f, "{}", v), #[cfg(feature = "_tls")] Error::Pem(v) => write!(f, "PEM: {:?}", v), #[cfg(feature = "_rustls")] Error::Rustls(v) => write!(f, "rustls: {}", v), #[cfg(feature = "native-tls")] Error::NativeTls(v) => write!(f, "native-tls: {}", v), #[cfg(feature = "native-tls")] Error::Der(v) => write!(f, "der: {}", v), #[cfg(feature = "cookies")] Error::Cookie(v) => write!(f, "cookie: {}", v), #[cfg(feature = "cookies")] Error::CookieValue(v) => write!(f, "{}", v), #[cfg(feature = "cookies")] Error::CookieJar(v) => write!(f, "cookie: {}", v), #[cfg(feature = "charset")] Error::UnknownCharset(v) => write!(f, "unknown character set: {}", v), Error::RequireHttpsOnly(v) => write!(f, "configured for https only: {}", v), Error::LargeResponseHeader(x, y) => { write!(f, "response header is too big: {} > {}", x, y) } #[cfg(any(feature = "gzip", feature = "brotli"))] Error::Decompress(x, y) => write!(f, "{} decompression failed: {}", x, y), #[cfg(feature = "json")] Error::Json(v) => write!(f, "json: {}", v), Error::ConnectProxyFailed(v) => write!(f, "CONNECT proxy failed: {}", v), Error::TlsRequired => write!(f, "TLS required, but transport is unsecured"), Error::Other(v) => write!(f, "other: {}", v), Error::BodyStalled => write!(f, "body data reading stalled"), } } } impl From for Error { fn from(value: http::Error) -> Self { Self::Http(value) } } impl From for Error { fn from(value: ureq_proto::Error) -> Self { Self::Protocol(value) } } #[cfg(feature = "_rustls")] impl From for Error { fn from(value: rustls::Error) -> Self { Self::Rustls(value) } } #[cfg(feature = "native-tls")] impl From for Error { fn from(value: native_tls::Error) -> Self { Self::NativeTls(value) } } #[cfg(feature = "native-tls")] impl From for Error { fn from(value: der::Error) -> Self { Self::Der(value) } } #[cfg(feature = "cookies")] impl From for Error { fn from(value: cookie_store::CookieError) -> Self { Self::Cookie(value) } } #[cfg(feature = "cookies")] impl From for Error { fn from(value: cookie_store::Error) -> Self { Self::CookieJar(value) } } #[cfg(feature = "json")] impl From for Error { fn from(value: serde_json::Error) -> Self { Self::Json(value) } } #[cfg(test)] mod test { use super::*; #[test] #[cfg(feature = "_test")] fn status_code_error_redirect() { use crate::test::init_test_log; use crate::transport::set_handler; init_test_log(); set_handler( "/redirect_a", 302, &[("Location", "http://example.edu/redirect_b")], &[], ); set_handler( "/redirect_b", 302, &[("Location", "http://example.com/status/500")], &[], ); set_handler("/status/500", 500, &[], &[]); let err = crate::get("http://example.org/redirect_a") .call() .unwrap_err(); assert!(matches!(err, Error::StatusCode(500))); } #[test] fn ensure_error_size() { // This is platform dependent, so we can't be too strict or precise. let size = std::mem::size_of::(); assert!(size < 100); // 40 on Macbook M1 } } algesten-ureq-d58cf18/src/lib.rs000066400000000000000000001263441505724604200166340ustar00rootroot00000000000000//!
//! //! //! Crates.io version //! //! //! //! docs.rs docs //! //! //! //! Crates.io downloads //! //!
//! //! A simple, safe HTTP client. //! //! Ureq's first priority is being easy for you to use. It's great for //! anyone who wants a low-overhead HTTP client that just gets the job done. Works //! very well with HTTP APIs. Its features include cookies, JSON, HTTP proxies, //! HTTPS, charset decoding, and is based on the API of the `http` crate. //! //! Ureq is in pure Rust for safety and ease of understanding. It avoids using //! `unsafe` directly. It uses blocking I/O instead of async I/O, because that keeps //! the API simple and keeps dependencies to a minimum. For TLS, ureq uses //! rustls or native-tls. //! //! See the [changelog] for details of recent releases. //! //! [changelog]: https://github.com/algesten/ureq/blob/main/CHANGELOG.md //! //! # Usage //! //! In its simplest form, ureq looks like this: //! //! ```rust //! let body: String = ureq::get("http://example.com") //! .header("Example-Header", "header value") //! .call()? //! .body_mut() //! .read_to_string()?; //! # Ok::<(), ureq::Error>(()) //! ``` //! //! For more involved tasks, you'll want to create an [`Agent`]. An Agent //! holds a connection pool for reuse, and a cookie store if you use the //! **cookies** feature. An Agent can be cheaply cloned due to internal //! [`Arc`] and all clones of an Agent share state among each other. Creating //! an Agent also allows setting options like the TLS configuration. //! //! ```rust //! # fn no_run() -> Result<(), ureq::Error> { //! use ureq::Agent; //! use std::time::Duration; //! //! let mut config = Agent::config_builder() //! .timeout_global(Some(Duration::from_secs(5))) //! .build(); //! //! let agent: Agent = config.into(); //! //! let body: String = agent.get("http://example.com/page") //! .call()? //! .body_mut() //! .read_to_string()?; //! //! // Reuses the connection from previous request. //! let response: String = agent.put("http://example.com/upload") //! .header("Authorization", "example-token") //! .send("some body data")? //! .body_mut() //! .read_to_string()?; //! # Ok(())} //! ``` //! //! ## JSON //! //! Ureq supports sending and receiving json, if you enable the **json** feature: //! //! ```rust //! # #[cfg(feature = "json")] //! # fn no_run() -> Result<(), ureq::Error> { //! use serde::{Serialize, Deserialize}; //! //! #[derive(Serialize)] //! struct MySendBody { //! thing: String, //! } //! //! #[derive(Deserialize)] //! struct MyRecvBody { //! other: String, //! } //! //! let send_body = MySendBody { thing: "yo".to_string() }; //! //! // Requires the `json` feature enabled. //! let recv_body = ureq::post("http://example.com/post/ingest") //! .header("X-My-Header", "Secret") //! .send_json(&send_body)? //! .body_mut() //! .read_json::()?; //! # Ok(())} //! ``` //! //! ## Error handling //! //! ureq returns errors via `Result`. That includes I/O errors, //! protocol errors. By default, also HTTP status code errors (when the //! server responded 4xx or 5xx) results in [`Error`]. //! //! This behavior can be turned off via [`http_status_as_error()`] //! //! ```rust //! use ureq::Error; //! //! # fn no_run() -> Result<(), ureq::Error> { //! match ureq::get("http://mypage.example.com/").call() { //! Ok(response) => { /* it worked */}, //! Err(Error::StatusCode(code)) => { //! /* the server returned an unexpected status //! code (such as 400, 500 etc) */ //! } //! Err(_) => { /* some kind of io/transport/etc error */ } //! } //! # Ok(())} //! ``` //! //! # Features //! //! To enable a minimal dependency tree, some features are off by default. //! You can control them when including ureq as a dependency. //! //! `ureq = { version = "3", features = ["socks-proxy", "charset"] }` //! //! The default enabled features are: **rustls** and **gzip**. //! //! * **rustls** enables the rustls TLS implementation. This is the default for the the crate level //! convenience calls (`ureq::get` etc). It currently uses `ring` as the TLS provider. //! * **native-tls** enables the native tls backend for TLS. Due to the risk of diamond dependencies //! accidentally switching on an unwanted TLS implementation, `native-tls` is never picked up as //! a default or used by the crate level convenience calls (`ureq::get` etc) – it must be configured //! on the agent //! * **platform-verifier** enables verifying the server certificates using a method native to the //! platform ureq is executing on. See [rustls-platform-verifier] crate //! * **socks-proxy** enables proxy config using the `socks4://`, `socks4a://`, `socks5://` //! and `socks://` (equal to `socks5://`) prefix //! * **cookies** enables cookies //! * **gzip** enables requests of gzip-compressed responses and decompresses them //! * **brotli** enables requests brotli-compressed responses and decompresses them //! * **charset** enables interpreting the charset part of the Content-Type header //! (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the //! library defaults to Rust's built in `utf-8` //! * **json** enables JSON sending and receiving via serde_json //! //! ### Unstable //! //! These features are unstable and might change in a minor version. //! //! * **rustls-no-provider** Enables rustls, but does not enable any [`CryptoProvider`] such as `ring`. //! Providers other than the default (currently `ring`) are never picked up from feature flags alone. //! It must be configured on the agent. //! //! * **vendored** compiles and statically links to a copy of non-Rust vendors (e.g. OpenSSL from `native-tls`) //! //! # TLS (https) //! //! ## rustls //! //! By default, ureq uses [`rustls` crate] with the `ring` cryptographic provider. //! As of Sep 2024, the `ring` provider has a higher chance of compiling successfully. If the user //! installs another process [default provider], that choice is respected. //! //! ureq does not guarantee to default to ring indefinitely. `rustls` as a feature flag will always //! work, but the specific crypto backend might change in a minor version. //! //! ``` //! # #[cfg(feature = "rustls")] //! # { //! // This uses rustls //! ureq::get("https://www.google.com/").call().unwrap(); //! # } Ok::<_, ureq::Error>(()) //! ``` //! //! ### rustls without ring //! //! ureq never changes TLS backend from feature flags alone. It is possible to compile ureq //! without ring, but it requires specific feature flags and configuring the [`Agent`]. //! //! Since rustls is not semver 1.x, this requires non-semver-guaranteed API. I.e. ureq might //! change this behavior without a major version bump. //! //! Read more at [`TlsConfigBuilder::unversioned_rustls_crypto_provider`][crate::tls::TlsConfigBuilder::unversioned_rustls_crypto_provider]. //! //! ## native-tls //! //! As an alternative, ureq ships with [`native-tls`] as a TLS provider. This must be //! enabled using the **native-tls** feature. Due to the risk of diamond dependencies //! accidentally switching on an unwanted TLS implementation, `native-tls` is never picked //! up as a default or used by the crate level convenience calls (`ureq::get` etc) – it //! must be configured on the agent. //! //! ``` //! # #[cfg(feature = "native-tls")] //! # { //! use ureq::config::Config; //! use ureq::tls::{TlsConfig, TlsProvider}; //! //! let mut config = Config::builder() //! .tls_config( //! TlsConfig::builder() //! // requires the native-tls feature //! .provider(TlsProvider::NativeTls) //! .build() //! ) //! .build(); //! //! let agent = config.new_agent(); //! //! agent.get("https://www.google.com/").call().unwrap(); //! # } Ok::<_, ureq::Error>(()) //! ``` //! //! ## Root certificates //! //! ### webpki-roots //! //! By default, ureq uses Mozilla's root certificates via the [webpki-roots] crate. This is a static //! bundle of root certificates that do not update automatically. It also circumvents whatever root //! certificates are installed on the host running ureq, which might be a good or a bad thing depending //! on your perspective. There is also no mechanism for [SCT], [CRL]s or other revocations. //! To maintain a "fresh" list of root certs, you need to bump the ureq dependency from time to time. //! //! The main reason for chosing this as the default is to minimize the number of dependencies. More //! details about this decision can be found at [PR 818]. //! //! If your use case for ureq is talking to a limited number of servers with high trust, the //! default setting is likely sufficient. If you use ureq with a high number of servers, or servers //! you don't trust, we recommend using the platform verifier (see below). //! //! ### platform-verifier //! //! The [rustls-platform-verifier] crate provides access to natively checking the certificate via your OS. //! To use this verifier, you need to enable it using feature flag **platform-verifier** as well as //! configure an agent to use it. //! //! ``` //! # #[cfg(all(feature = "rustls", feature="platform-verifier"))] //! # { //! use ureq::Agent; //! use ureq::tls::{TlsConfig, RootCerts}; //! //! let agent = Agent::config_builder() //! .tls_config( //! TlsConfig::builder() //! .root_certs(RootCerts::PlatformVerifier) //! .build() //! ) //! .build() //! .new_agent(); //! //! let response = agent.get("https://httpbin.org/get").call()?; //! # } Ok::<_, ureq::Error>(()) //! ``` //! //! Setting `RootCerts::PlatformVerifier` together with `TlsProvider::NativeTls` means //! also native-tls will use the OS roots instead of [webpki-roots] crate. Whether that //! results in a config that has CRLs and revocations is up to whatever native-tls links to. //! //! # JSON //! //! By enabling the **json** feature, the library supports serde json. //! //! This is enabled by default. //! //! * [`request.send_json()`] send body as json. //! * [`body.read_json()`] transform response to json. //! //! # Sending body data //! //! HTTP/1.1 has two ways of transfering body data. Either of a known size with //! the `Content-Length` HTTP header, or unknown size with the //! `Transfer-Encoding: chunked` header. ureq supports both and will use the //! appropriate method depending on which body is being sent. //! //! ureq has a [`AsSendBody`] trait that is implemented for many well known types //! of data that we might want to send. The request body can thus be anything //! from a `String` to a `File`, see below. //! //! ## Content-Length //! //! The library will send a `Content-Length` header on requests with bodies of //! known size, in other words, if the body to send is one of: //! //! * `&[u8]` //! * `&[u8; N]` //! * `&str` //! * `String` //! * `&String` //! * `Vec` //! * `&Vec)` //! * [`SendBody::from_json()`] (implicitly via [`request.send_json()`]) //! //! ## Transfer-Encoding: chunked //! //! ureq will send a `Transfer-Encoding: chunked` header on requests where the body //! is of unknown size. The body is automatically converted to an [`std::io::Read`] //! when the type is one of: //! //! * `File` //! * `&File` //! * `TcpStream` //! * `&TcpStream` //! * `Stdin` //! * `UnixStream` (not on windows) //! //! ### From readers //! //! The chunked method also applies for bodies constructed via: //! //! * [`SendBody::from_reader()`] //! * [`SendBody::from_owned_reader()`] //! //! ## Proxying a response body //! //! As a special case, when ureq sends a [`Body`] from a previous http call, the //! use of `Content-Length` or `chunked` depends on situation. For input such as //! gzip decoding (**gzip** feature) or charset transformation (**charset** feature), //! the output body might not match the input, which means ureq is forced to use //! the `chunked` method. //! //! * `Response` //! //! ## Sending form data //! //! [`request.send_form()`] provides a way to send `application/x-www-form-urlencoded` //! encoded data. The key/values provided will be URL encoded. //! //! ## Overriding //! //! If you set your own Content-Length or Transfer-Encoding header before //! sending the body, ureq will respect that header by not overriding it, //! and by encoding the body or not, as indicated by the headers you set. //! //! ``` //! let resp = ureq::put("https://httpbin.org/put") //! .header("Transfer-Encoding", "chunked") //! .send("Hello world")?; //! # Ok::<_, ureq::Error>(()) //! ``` //! //! # Character encoding //! //! By enabling the **charset** feature, the library supports receiving other //! character sets than `utf-8`. //! //! For [`Body::read_to_string()`] we read the header like: //! //! `Content-Type: text/plain; charset=iso-8859-1` //! //! and if it contains a charset specification, we try to decode the body using that //! encoding. In the absence of, or failing to interpret the charset, we fall back on `utf-8`. //! //! Currently ureq does not provide a way to encode when sending request bodies. //! //! ## Lossy utf-8 //! //! When reading text bodies (with a `Content-Type` starting `text/` as in `text/plain`, //! `text/html`, etc), ureq can ensure the body is possible to read as a `String` also if //! it contains characters that are not valid for utf-8. Invalid characters are replaced //! with a question mark `?` (NOT the utf-8 replacement character). //! //! For [`Body::read_to_string()`] this is turned on by default, but it can be disabled //! and conversely for [`Body::as_reader()`] it is not enabled, but can be. //! //! To precisely configure the behavior use [`Body::with_config()`]. //! //! # Proxying //! //! ureq supports two kinds of proxies, [`HTTP`] ([`CONNECT`]), [`SOCKS4`]/[`SOCKS5`], //! the former is always available while the latter must be enabled using the feature //! **socks-proxy**. //! //! Proxies settings are configured on an [`Agent`]. All request sent through the agent will be proxied. //! //! ## Example using HTTP //! //! ```rust //! use ureq::{Agent, Proxy}; //! # fn no_run() -> std::result::Result<(), ureq::Error> { //! // Configure an http connect proxy. //! let proxy = Proxy::new("http://user:password@cool.proxy:9090")?; //! let agent: Agent = Agent::config_builder() //! .proxy(Some(proxy)) //! .build() //! .into(); //! //! // This is proxied. //! let resp = agent.get("http://cool.server").call()?; //! # Ok(())} //! # fn main() {} //! ``` //! //! ## Example using SOCKS5 //! //! ```rust //! use ureq::{Agent, Proxy}; //! # #[cfg(feature = "socks-proxy")] //! # fn no_run() -> std::result::Result<(), ureq::Error> { //! // Configure a SOCKS proxy. //! let proxy = Proxy::new("socks5://user:password@cool.proxy:9090")?; //! let agent: Agent = Agent::config_builder() //! .proxy(Some(proxy)) //! .build() //! .into(); //! //! // This is proxied. //! let resp = agent.get("http://cool.server").call()?; //! # Ok(())} //! ``` //! //! # Log levels //! //! ureq uses the log crate. These are the definitions of the log levels, however we //! do not guarantee anything for dependencies such as `http` and `rustls`. //! //! * `ERROR` - nothing //! * `WARN` - if we detect a user configuration problem. //! * `INFO` - nothing //! * `DEBUG` - uri, state changes, transport, resolver and selected request/response headers //! * `TRACE` - wire level debug. NOT REDACTED! //! //! The request/response headers on DEBUG levels are allow-listed to only include headers that //! are considered safe. The code has the [allow list](https://github.com/algesten/ureq/blob/81127cfc38516903330dc1b9c618122372f8dc29/src/util.rs#L184-L198). //! //! # Versioning //! //! ## Semver and `unversioned` //! //! ureq follows semver. From ureq 3.x we strive to have a much closer adherence to semver than 2.x. //! The main mistake in 2.x was to re-export crates that were not yet semver 1.0. In ureq 3.x TLS and //! cookie configuration is shimmed using our own types. //! //! ureq 3.x is trying out two new traits that had no equivalent in 2.x, [`Transport`] and [`Resolver`]. //! These allow the user write their own bespoke transports and (DNS name) resolver. The API:s for //! these parts are not yet solidified. They live under the [`unversioned`] module, and do not //! follow semver. See module doc for more info. //! //! ## Breaking changes in dependencies //! //! ureq relies on non-semver 1.x crates such as `rustls` and `native-tls`. Some scenarios, such //! as configuring `rustls` to not use `ring`, a user of ureq might need to interact with these //! crates directly instead of going via ureq's provided API. //! //! Such changes can break when ureq updates dependencies. This is not considered a breaking change //! for ureq and will not be reflected by a major version bump. //! //! We strive to mark ureq's API with the word "unversioned" to identify places where this risk arises. //! //! ## Minimum Supported Rust Version (MSRV) //! //! From time to time we will need to update our minimum supported Rust version (MSRV). This is not //! something we do lightly; our ambition is to be as conservative with MSRV as possible. //! //! * For some dependencies, we will opt for pinning the version of the dep instead //! of bumping our MSRV. //! * For important dependencies, like the TLS libraries, we cannot hold back our MSRV if they change. //! * We do not consider MSRV changes to be breaking for the purposes of semver. //! * We will not make MSRV changes in patch releases. //! * MSRV changes will get their own minor release, and not be co-mingled with other changes. //! //! [`HTTP`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling#http_tunneling //! [`CONNECT`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT //! [`SOCKS4`]: https://en.wikipedia.org/wiki/SOCKS#SOCKS4 //! [`SOCKS5`]: https://en.wikipedia.org/wiki/SOCKS#SOCKS5 //! [`rustls` crate]: https://crates.io/crates/rustls //! [default provider]: https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#method.install_default //! [`native-tls`]: https://crates.io/crates/native-tls //! [rustls-platform-verifier]: https://crates.io/crates/rustls-platform-verifier //! [webpki-roots]: https://crates.io/crates/webpki-roots //! [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html //! [`Agent`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Agent.html //! [`Error`]: https://docs.rs/ureq/3.0.0-rc4/ureq/enum.Error.html //! [`http_status_as_error()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/config/struct.ConfigBuilder.html#method.http_status_as_error //! [SCT]: https://en.wikipedia.org/wiki/Certificate_Transparency //! [CRL]: https://en.wikipedia.org/wiki/Certificate_revocation_list //! [PR818]: https://github.com/algesten/ureq/pull/818 //! [`request.send_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.RequestBuilder.html#method.send_json //! [`body.read_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.read_json //! [`AsSendBody`]: https://docs.rs/ureq/3.0.0-rc4/ureq/trait.AsSendBody.html //! [`SendBody::from_json()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_json //! [`std::io::Read`]: https://doc.rust-lang.org/std/io/trait.Read.html //! [`SendBody::from_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_reader //! [`SendBody::from_owned_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.SendBody.html#method.from_owned_reader //! [`Body`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html //! [`request.send_form()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.RequestBuilder.html#method.send_form //! [`Body::read_to_string()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.read_to_string //! [`Body::as_reader()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.as_reader //! [`Body::with_config()`]: https://docs.rs/ureq/3.0.0-rc4/ureq/struct.Body.html#method.with_config //! [`Transport`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/transport/trait.Transport.html //! [`Resolver`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/resolver/trait.Resolver.html //! [`unversioned`]: https://docs.rs/ureq/3.0.0-rc4/ureq/unversioned/index.html //! [`CryptoProvider`]: https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html #![forbid(unsafe_code)] #![warn(clippy::all)] #![deny(missing_docs)] // I don't think elided lifetimes help in understanding the code. #![allow(clippy::needless_lifetimes)] // Since you can't use inlined args for all cases, using it means the // code will have a mix of inlined and not inlined. Code should be // uniform, thus this lint is misguided. #![allow(clippy::uninlined_format_args)] #![allow(mismatched_lifetime_syntaxes)] #[macro_use] extern crate log; use std::convert::TryFrom; /// Re-exported http-crate. pub use ureq_proto::http; pub use body::{Body, BodyBuilder, BodyReader, BodyWithConfig}; use http::Method; use http::{Request, Response, Uri}; pub use proxy::{Proxy, ProxyBuilder, ProxyProtocol}; pub use request::RequestBuilder; use request::{WithBody, WithoutBody}; pub use request_ext::RequestExt; pub use response::ResponseExt; pub use send_body::AsSendBody; mod agent; mod body; pub mod config; mod error; mod pool; mod proxy; mod query; mod request; mod response; mod run; mod send_body; mod timings; mod util; pub mod unversioned; use unversioned::resolver; use unversioned::transport; pub mod middleware; #[cfg(feature = "_tls")] pub mod tls; #[cfg(feature = "cookies")] mod cookies; mod request_ext; #[cfg(feature = "cookies")] pub use cookies::{Cookie, CookieJar}; pub use agent::Agent; pub use error::Error; pub use send_body::SendBody; pub use timings::Timeout; /// Typestate variables. pub mod typestate { pub use super::request::WithBody; pub use super::request::WithoutBody; pub use super::config::typestate::AgentScope; pub use super::config::typestate::HttpCrateScope; pub use super::config::typestate::RequestScope; } /// Run a [`http::Request`]. pub fn run(request: Request) -> Result, Error> { let agent = Agent::new_with_defaults(); agent.run(request) } /// A new [Agent] with default configuration /// /// Agents are used to hold configuration and keep state between requests. pub fn agent() -> Agent { Agent::new_with_defaults() } /// Make a GET request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn get(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::GET, uri) } /// Make a POST request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn post(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::POST, uri) } /// Make a PUT request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn put(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::PUT, uri) } /// Make a DELETE request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn delete(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::DELETE, uri) } /// Make a HEAD request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn head(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::HEAD, uri) } /// Make an OPTIONS request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn options(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::OPTIONS, uri) } /// Make a CONNECT request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn connect(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::CONNECT, uri) } /// Make a PATCH request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn patch(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::PATCH, uri) } /// Make a TRACE request. /// /// Run on a use-once [`Agent`]. #[must_use] pub fn trace(uri: T) -> RequestBuilder where Uri: TryFrom, >::Error: Into, { RequestBuilder::::new(Agent::new_with_defaults(), Method::TRACE, uri) } #[cfg(test)] pub(crate) mod test { use std::{io, sync::OnceLock}; use assert_no_alloc::AllocDisabler; use config::{Config, ConfigBuilder}; use typestate::AgentScope; use super::*; #[global_allocator] // Some tests checks that we are not allocating static A: AllocDisabler = AllocDisabler; pub fn init_test_log() { static INIT_LOG: OnceLock<()> = OnceLock::new(); INIT_LOG.get_or_init(env_logger::init); } #[test] fn connect_http_google() { init_test_log(); let agent = Agent::new_with_defaults(); let res = agent.get("http://www.google.com/").call().unwrap(); assert_eq!( "text/html;charset=ISO-8859-1", res.headers() .get("content-type") .unwrap() .to_str() .unwrap() .replace("; ", ";") ); assert_eq!(res.body().mime_type(), Some("text/html")); } #[test] #[cfg(feature = "rustls")] fn connect_https_google_rustls() { init_test_log(); use config::Config; use crate::tls::{TlsConfig, TlsProvider}; let agent: Agent = Config::builder() .tls_config(TlsConfig::builder().provider(TlsProvider::Rustls).build()) .build() .into(); let res = agent.get("https://www.google.com/").call().unwrap(); assert_eq!( "text/html;charset=ISO-8859-1", res.headers() .get("content-type") .unwrap() .to_str() .unwrap() .replace("; ", ";") ); assert_eq!(res.body().mime_type(), Some("text/html")); } #[test] #[cfg(feature = "native-tls")] fn connect_https_google_native_tls_simple() { init_test_log(); use config::Config; use crate::tls::{TlsConfig, TlsProvider}; let agent: Agent = Config::builder() .tls_config( TlsConfig::builder() .provider(TlsProvider::NativeTls) .build(), ) .build() .into(); let mut res = agent.get("https://www.google.com/").call().unwrap(); assert_eq!( "text/html;charset=ISO-8859-1", res.headers() .get("content-type") .unwrap() .to_str() .unwrap() .replace("; ", ";") ); assert_eq!(res.body().mime_type(), Some("text/html")); res.body_mut().read_to_string().unwrap(); } #[test] #[cfg(feature = "rustls")] fn connect_https_google_rustls_webpki() { init_test_log(); use crate::tls::{RootCerts, TlsConfig, TlsProvider}; use config::Config; let agent: Agent = Config::builder() .tls_config( TlsConfig::builder() .provider(TlsProvider::Rustls) .root_certs(RootCerts::WebPki) .build(), ) .build() .into(); agent.get("https://www.google.com/").call().unwrap(); } #[test] #[cfg(feature = "native-tls")] fn connect_https_google_native_tls_webpki() { init_test_log(); use crate::tls::{RootCerts, TlsConfig, TlsProvider}; use config::Config; let agent: Agent = Config::builder() .tls_config( TlsConfig::builder() .provider(TlsProvider::NativeTls) .root_certs(RootCerts::WebPki) .build(), ) .build() .into(); agent.get("https://www.google.com/").call().unwrap(); } #[test] #[cfg(feature = "rustls")] fn connect_https_google_noverif() { init_test_log(); use crate::tls::{TlsConfig, TlsProvider}; let agent: Agent = Config::builder() .tls_config( TlsConfig::builder() .provider(TlsProvider::Rustls) .disable_verification(true) .build(), ) .build() .into(); let res = agent.get("https://www.google.com/").call().unwrap(); assert_eq!( "text/html;charset=ISO-8859-1", res.headers() .get("content-type") .unwrap() .to_str() .unwrap() .replace("; ", ";") ); assert_eq!(res.body().mime_type(), Some("text/html")); } #[test] fn simple_put_content_len() { init_test_log(); let mut res = put("http://httpbin.org/put").send(&[0_u8; 100]).unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn simple_put_chunked() { init_test_log(); let mut res = put("http://httpbin.org/put") // override default behavior .header("transfer-encoding", "chunked") .send(&[0_u8; 100]) .unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn simple_get() { init_test_log(); let mut res = get("http://httpbin.org/get").call().unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn query_no_slash() { init_test_log(); let mut res = get("http://httpbin.org?query=foo").call().unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn simple_head() { init_test_log(); let mut res = head("http://httpbin.org/get").call().unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn redirect_no_follow() { init_test_log(); let agent: Agent = Config::builder().max_redirects(0).build().into(); let mut res = agent .get("http://httpbin.org/redirect-to?url=%2Fget") .call() .unwrap(); let txt = res.body_mut().read_to_string().unwrap(); #[cfg(feature = "_test")] assert_eq!(txt, "You've been redirected"); #[cfg(not(feature = "_test"))] assert_eq!(txt, ""); } #[test] fn redirect_max_with_error() { init_test_log(); let agent: Agent = Config::builder().max_redirects(3).build().into(); let res = agent .get( "http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\ url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D", ) .call(); let err = res.unwrap_err(); assert_eq!(err.to_string(), "too many redirects"); } #[test] fn redirect_max_without_error() { init_test_log(); let agent: Agent = Config::builder() .max_redirects(3) .max_redirects_will_error(false) .build() .into(); let res = agent .get( "http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\ url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D", ) .call() .unwrap(); assert_eq!(res.status(), 302); } #[test] fn redirect_follow() { init_test_log(); let res = get("http://httpbin.org/redirect-to?url=%2Fget") .call() .unwrap(); let response_uri = res.get_uri(); assert_eq!(response_uri.path(), "/get") } #[test] fn redirect_history_none() { init_test_log(); let res = get("http://httpbin.org/redirect-to?url=%2Fget") .call() .unwrap(); let redirect_history = res.get_redirect_history(); assert_eq!(redirect_history, None) } #[test] fn redirect_history_some() { init_test_log(); let agent: Agent = Config::builder() .max_redirects(3) .max_redirects_will_error(false) .save_redirect_history(true) .build() .into(); let res = agent .get("http://httpbin.org/redirect-to?url=%2Fget") .call() .unwrap(); let redirect_history = res.get_redirect_history(); assert_eq!( redirect_history, Some( vec![ "http://httpbin.org/redirect-to?url=%2Fget".parse().unwrap(), "http://httpbin.org/get".parse().unwrap() ] .as_ref() ) ); let res = agent .get( "http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\ url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D", ) .call() .unwrap(); let redirect_history = res.get_redirect_history(); assert_eq!( redirect_history, Some(vec![ "http://httpbin.org/redirect-to?url=%2Fredirect-to%3Furl%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D".parse().unwrap(), "http://httpbin.org/redirect-to?url=/redirect-to?url=%2Fredirect-to%3Furl%3D".parse().unwrap(), "http://httpbin.org/redirect-to?url=/redirect-to?url=".parse().unwrap(), "http://httpbin.org/redirect-to?url=".parse().unwrap(), ].as_ref()) ); let res = agent.get("https://www.google.com/").call().unwrap(); let redirect_history = res.get_redirect_history(); assert_eq!( redirect_history, Some(vec!["https://www.google.com/".parse().unwrap()].as_ref()) ); } #[test] fn connect_https_invalid_name() { let result = get("https://example.com{REQUEST_URI}/").call(); let err = result.unwrap_err(); assert!(matches!(err, Error::Http(_))); assert_eq!(err.to_string(), "http: invalid uri character"); } #[test] fn post_big_body_chunked() { init_test_log(); // https://github.com/algesten/ureq/issues/879 let mut data = io::Cursor::new(vec![42; 153_600]); post("http://httpbin.org/post") .content_type("application/octet-stream") .send(SendBody::from_reader(&mut data)) .expect("to send correctly"); } #[test] #[cfg(not(feature = "_test"))] fn post_array_body_sends_content_length() { init_test_log(); let mut response = post("http://httpbin.org/post") .content_type("application/octet-stream") .send(vec![42; 123]) .expect("to send correctly"); let ret = response.body_mut().read_to_string().unwrap(); assert!(ret.contains("\"Content-Length\": \"123\"")); } #[test] #[cfg(not(feature = "_test"))] fn post_file_sends_file_length() { init_test_log(); let file = std::fs::File::open("LICENSE-MIT").unwrap(); let mut response = post("http://httpbin.org/post") .content_type("application/octet-stream") .send(file) .expect("to send correctly"); let ret = response.body_mut().read_to_string().unwrap(); assert!(ret.contains("\"Content-Length\": \"1072\"")); } #[test] #[cfg(not(feature = "_test"))] fn username_password_from_uri() { init_test_log(); let mut res = get("https://martin:secret@httpbin.org/get").call().unwrap(); let body = res.body_mut().read_to_string().unwrap(); assert!(body.contains("Basic bWFydGluOnNlY3JldA==")); } #[test] #[cfg(all(feature = "cookies", feature = "_test"))] fn store_response_cookies() { let agent = Agent::new_with_defaults(); let _ = agent.get("https://www.google.com").call().unwrap(); let mut all: Vec<_> = agent .cookie_jar_lock() .iter() .map(|c| c.name().to_string()) .collect(); all.sort(); assert_eq!(all, ["AEC", "__Secure-ENID"]) } #[test] #[cfg(all(feature = "cookies", feature = "_test"))] fn send_request_cookies() { init_test_log(); let agent = Agent::new_with_defaults(); let uri = Uri::from_static("http://cookie.test/cookie-test"); let uri2 = Uri::from_static("http://cookie2.test/cookie-test"); let mut jar = agent.cookie_jar_lock(); jar.insert(Cookie::parse("a=1", &uri).unwrap(), &uri) .unwrap(); jar.insert(Cookie::parse("b=2", &uri).unwrap(), &uri) .unwrap(); jar.insert(Cookie::parse("c=3", &uri2).unwrap(), &uri2) .unwrap(); jar.release(); let _ = agent.get("http://cookie.test/cookie-test").call().unwrap(); } #[test] #[cfg(all(feature = "_test", not(feature = "cookies")))] fn partial_redirect_when_following() { init_test_log(); // this should work because we follow the redirect and go to /get get("http://my-host.com/partial-redirect").call().unwrap(); } #[test] #[cfg(feature = "_test")] fn partial_redirect_when_not_following() { init_test_log(); // this should fail because we are not following redirects, and the // response is partial before the server is hanging up get("http://my-host.com/partial-redirect") .config() .max_redirects(0) .build() .call() .unwrap_err(); } #[test] #[cfg(feature = "_test")] fn http_connect_proxy() { init_test_log(); let proxy = Proxy::new("http://my_proxy:1234/connect-proxy").unwrap(); let agent = Agent::config_builder() .proxy(Some(proxy)) .build() .new_agent(); let mut res = agent.get("http://httpbin.org/get").call().unwrap(); res.body_mut().read_to_string().unwrap(); } #[test] fn ensure_reasonable_stack_sizes() { macro_rules! ensure { ($type:ty, $size:tt) => { let sz = std::mem::size_of::<$type>(); // println!("{}: {}", stringify!($type), sz); assert!( sz <= $size, "Stack size of {} is too big {} > {}", stringify!($type), sz, $size ); }; } ensure!(RequestBuilder, 400); // 304 ensure!(Agent, 100); // 32 ensure!(Config, 400); // 320 ensure!(ConfigBuilder, 400); // 320 ensure!(Response, 250); // 136 ensure!(Body, 50); // 24 } #[test] #[cfg(feature = "_test")] fn limit_max_response_header_size() { init_test_log(); let err = get("http://httpbin.org/get") .config() .max_response_header_size(5) .build() .call() .unwrap_err(); assert!(matches!(err, Error::LargeResponseHeader(65, 5))); } #[test] #[cfg(feature = "_test")] fn propfind_with_body() { init_test_log(); // https://github.com/algesten/ureq/issues/1034 let request = http::Request::builder() .method("PROPFIND") .uri("https://www.google.com/") .body("Some really cool body") .unwrap(); let _ = Agent::config_builder() .allow_non_standard_methods(true) .build() .new_agent() .run(request) .unwrap(); } #[test] #[cfg(feature = "_test")] fn non_standard_method() { init_test_log(); let method = Method::from_bytes(b"FNORD").unwrap(); let req = Request::builder() .method(method) .uri("http://httpbin.org/fnord") .body(()) .unwrap(); let agent = Agent::new_with_defaults(); let req = agent .configure_request(req) .allow_non_standard_methods(true) .build(); agent.run(req).unwrap(); } #[test] #[cfg(feature = "_test")] fn chunk_abort() { init_test_log(); let mut res = get("http://my-fine-server/1chunk-abort").call().unwrap(); let body = res.body_mut().read_to_string().unwrap(); assert_eq!(body, "OK"); let mut res = get("http://my-fine-server/2chunk-abort").call().unwrap(); let body = res.body_mut().read_to_string().unwrap(); assert_eq!(body, "OK"); let mut res = get("http://my-fine-server/3chunk-abort").call().unwrap(); let body = res.body_mut().read_to_string().unwrap(); assert_eq!(body, "OK"); let mut res = get("http://my-fine-server/4chunk-abort").call().unwrap(); let body = res.body_mut().read_to_string().unwrap(); assert_eq!(body, "OK"); } // This doesn't need to run, just compile. fn _ensure_send_sync() { fn is_send(_t: impl Send) {} fn is_sync(_t: impl Sync) {} // Agent is_send(Agent::new_with_defaults()); is_sync(Agent::new_with_defaults()); // ResponseBuilder is_send(get("https://example.test")); is_sync(get("https://example.test")); let data = vec![0_u8, 1, 2, 3, 4]; // Response via ResponseBuilder is_send(post("https://example.test").send(&data)); is_sync(post("https://example.test").send(&data)); // Request is_send(Request::post("https://yaz").body(&data).unwrap()); is_sync(Request::post("https://yaz").body(&data).unwrap()); // Response via Agent::run is_send(run(Request::post("https://yaz").body(&data).unwrap())); is_sync(run(Request::post("https://yaz").body(&data).unwrap())); // Response> let mut response = post("https://yaz").send(&data).unwrap(); let shared_reader = response.body_mut().as_reader(); is_send(shared_reader); let shared_reader = response.body_mut().as_reader(); is_sync(shared_reader); // Response> let response = post("https://yaz").send(&data).unwrap(); let owned_reader = response.into_parts().1.into_reader(); is_send(owned_reader); let response = post("https://yaz").send(&data).unwrap(); let owned_reader = response.into_parts().1.into_reader(); is_sync(owned_reader); let err = Error::HostNotFound; is_send(err); let err = Error::HostNotFound; is_sync(err); } } algesten-ureq-d58cf18/src/middleware.rs000066400000000000000000000155131505724604200201760ustar00rootroot00000000000000//! Chained interception to modify the request or response. use std::fmt; use std::sync::Arc; use crate::http; use crate::run::run; use crate::{Agent, Body, Error, SendBody}; /// Chained processing of request (and response). /// /// # Middleware as `fn` /// /// The middleware trait is implemented for all functions that have the signature /// /// `Fn(Request, MiddlewareNext) -> Result` /// /// That means the easiest way to implement middleware is by providing a `fn`, like so /// /// ``` /// use ureq::{Body, SendBody}; /// use ureq::middleware::MiddlewareNext; /// use ureq::http::{Request, Response}; /// /// fn my_middleware(req: Request, next: MiddlewareNext) /// -> Result, ureq::Error> { /// /// // do middleware things to request /// /// // continue the middleware chain /// let res = next.handle(req)?; /// /// // do middleware things to response /// /// Ok(res) /// } /// ``` /// /// # Adding headers /// /// A common use case is to add headers to the outgoing request. Here an example of how. /// /// ```no_run /// use ureq::{Body, SendBody, Agent, config::Config}; /// use ureq::middleware::MiddlewareNext; /// use ureq::http::{Request, Response, header::HeaderValue}; /// /// # #[cfg(feature = "json")] /// # { /// fn my_middleware(mut req: Request, next: MiddlewareNext) /// -> Result, ureq::Error> { /// /// req.headers_mut().insert("X-My-Header", HeaderValue::from_static("value_42")); /// /// // set my bespoke header and continue the chain /// next.handle(req) /// } /// /// let mut config = Config::builder() /// .middleware(my_middleware) /// .build(); /// /// let agent: Agent = config.into(); /// /// let result: serde_json::Value = /// agent.get("http://httpbin.org/headers").call()?.body_mut().read_json()?; /// /// assert_eq!(&result["headers"]["X-My-Header"], "value_42"); /// # } Ok::<_, ureq::Error>(()) /// ``` /// /// # State /// /// To maintain state between middleware invocations, we need to do something more elaborate than /// the simple `fn` and implement the `Middleware` trait directly. /// /// ## Example with mutex lock /// /// In the `examples` directory there is an additional example `count-bytes.rs` which uses /// a mutex lock like shown below. /// /// ``` /// use std::sync::{Arc, Mutex}; /// /// use ureq::{Body, SendBody}; /// use ureq::middleware::{Middleware, MiddlewareNext}; /// use ureq::http::{Request, Response}; /// /// struct MyState { /// // whatever is needed /// } /// /// struct MyMiddleware(Arc>); /// /// impl Middleware for MyMiddleware { /// fn handle(&self, request: Request, next: MiddlewareNext) /// -> Result, ureq::Error> { /// /// // These extra brackets ensures we release the Mutex lock before continuing the /// // chain. There could also be scenarios where we want to maintain the lock through /// // the invocation, which would block other requests from proceeding concurrently /// // through the middleware. /// { /// let mut state = self.0.lock().unwrap(); /// // do stuff with state /// } /// /// // continue middleware chain /// next.handle(request) /// } /// } /// ``` /// /// ## Example with atomic /// /// This example shows how we can increase a counter for each request going /// through the agent. /// /// ``` /// use ureq::{Body, SendBody, Agent, config::Config}; /// use ureq::middleware::{Middleware, MiddlewareNext}; /// use ureq::http::{Request, Response}; /// use std::sync::atomic::{AtomicU64, Ordering}; /// use std::sync::Arc; /// /// // Middleware that stores a counter state. This example uses an AtomicU64 /// // since the middleware is potentially shared by multiple threads running /// // requests at the same time. /// struct MyCounter(Arc); /// /// impl Middleware for MyCounter { /// fn handle(&self, req: Request, next: MiddlewareNext) /// -> Result, ureq::Error> { /// /// // increase the counter for each invocation /// self.0.fetch_add(1, Ordering::Relaxed); /// /// // continue the middleware chain /// next.handle(req) /// } /// } /// /// let shared_counter = Arc::new(AtomicU64::new(0)); /// /// let mut config = Config::builder() /// .middleware(MyCounter(shared_counter.clone())) /// .build(); /// /// let agent: Agent = config.into(); /// /// agent.get("http://httpbin.org/get").call()?; /// agent.get("http://httpbin.org/get").call()?; /// /// // Check we did indeed increase the counter twice. /// assert_eq!(shared_counter.load(Ordering::Relaxed), 2); /// /// # Ok::<_, ureq::Error>(()) /// ``` pub trait Middleware: Send + Sync + 'static { /// Handle of the middleware logic. fn handle( &self, request: http::Request, next: MiddlewareNext, ) -> Result, Error>; } #[derive(Clone, Default)] pub(crate) struct MiddlewareChain { chain: Arc>>, } impl MiddlewareChain { pub(crate) fn add(&mut self, mw: impl Middleware) { let Some(chain) = Arc::get_mut(&mut self.chain) else { panic!("Can't add to a MiddlewareChain that is already cloned") }; chain.push(Box::new(mw)); } } /// Continuation of a [`Middleware`] chain. pub struct MiddlewareNext<'a> { agent: &'a Agent, index: usize, } impl<'a> MiddlewareNext<'a> { pub(crate) fn new(agent: &'a Agent) -> Self { MiddlewareNext { agent, index: 0 } } /// Continue the middleware chain. /// /// The middleware must call this in order to run the request. Not calling /// it is a valid choice for not wanting the request to execute. pub fn handle( mut self, request: http::Request, ) -> Result, Error> { if let Some(mw) = self.agent.config().middleware.chain.get(self.index) { // This middleware exists, run it. self.index += 1; mw.handle(request, self) } else { // When chain is over, call the main run(). let (parts, body) = request.into_parts(); let request = http::Request::from_parts(parts, ()); run(self.agent, request, body) } } } impl Middleware for F where F: Fn(http::Request, MiddlewareNext) -> Result, Error> + Send + Sync + 'static, { fn handle( &self, request: http::Request, next: MiddlewareNext, ) -> Result, Error> { (self)(request, next) } } impl fmt::Debug for MiddlewareChain { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("MiddlewareChain") .field("len", &self.chain.len()) .finish() } } algesten-ureq-d58cf18/src/pool.rs000066400000000000000000000222531505724604200170310ustar00rootroot00000000000000use std::collections::VecDeque; use std::fmt; use std::sync::{Arc, Mutex, Weak}; use http::uri::{Authority, Scheme}; use http::Uri; use crate::config::Config; use crate::http; use crate::proxy::Proxy; use crate::transport::time::{Duration, Instant}; use crate::transport::{Buffers, ConnectionDetails, Connector, NextTimeout, Transport}; use crate::util::DebugAuthority; use crate::Error; pub(crate) struct ConnectionPool { connector: Box>>, pool: Arc>, } impl ConnectionPool { pub fn new(connector: Box>>, config: &Config) -> Self { ConnectionPool { connector, pool: Arc::new(Mutex::new(Pool::new(config))), } } pub fn connect( &self, details: &ConnectionDetails, max_idle_age: Duration, ) -> Result { let key = details.into(); { let mut pool = self.pool.lock().unwrap(); pool.purge(details.now); if let Some(conn) = pool.get(&key, max_idle_age, details.now) { debug!("Use pooled: {:?}", key); return Ok(conn); } } let transport = self.run_connector(details)?; let conn = Connection { transport, key, last_use: details.now, pool: Arc::downgrade(&self.pool), position_per_host: None, }; Ok(conn) } pub fn run_connector(&self, details: &ConnectionDetails) -> Result, Error> { let transport = self .connector .connect(details, None)? .ok_or(Error::ConnectionFailed)?; Ok(transport) } #[cfg(test)] /// Exposed for testing the pool count. pub fn pool_count(&self) -> usize { let lock = self.pool.lock().unwrap(); lock.lru.len() } } pub(crate) struct Connection { transport: Box, key: PoolKey, last_use: Instant, pool: Weak>, /// Used to prune max_idle_connections_by_host. /// /// # Example /// /// If we have a max idle per hosts set to 3, and we have the following LRU: /// /// ```text /// [B, A, A, B, A, B, A] /// ``` /// /// This field is used to enumerate the elements per host reverse: /// /// ```text /// [B2, A3, A2, B1, A1, B0, A0] /// ``` /// /// Once we have that enumeration, we can drop elements from the front where there /// position_per_host >= idle_per_host. position_per_host: Option, } impl Connection { pub fn buffers(&mut self) -> &mut dyn Buffers { self.transport.buffers() } pub fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { self.transport.transmit_output(amount, timeout) } pub fn maybe_await_input(&mut self, timeout: NextTimeout) -> Result { self.transport.maybe_await_input(timeout) } pub fn consume_input(&mut self, amount: usize) { self.transport.buffers().input_consume(amount) } pub fn close(self) { debug!("Close: {:?}", self.key); // Just consume self. } pub fn reuse(mut self, now: Instant) { if !self.transport.is_open() { // The purpose of probing is that is_open() for tcp connector attempts // to read some more bytes. If that succeeds, the connection is considered // _NOT_ open, since that means we either failed to read the previous // body to end, or the server sent bogus data after the body. Either // is a condition where we mustn't reuse the connection. return; } self.last_use = now; let Some(arc) = self.pool.upgrade() else { debug!("Pool gone: {:?}", self.key); return; }; debug!("Return to pool: {:?}", self.key); let mut pool = arc.lock().unwrap(); pool.add(self); pool.purge(now); } pub fn is_tls(&self) -> bool { self.transport.is_tls() } fn age(&self, now: Instant) -> Duration { now.duration_since(now) } fn is_open(&mut self) -> bool { self.transport.is_open() } } /// The pool key is the Scheme, Authority from the uri and the Proxy setting /// /// /// ```notrust /// abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1 /// |-| |-------------------------------||--------| |-------------------| |-----| /// | | | | | /// scheme authority path query fragment /// ``` /// /// It's correct to include username/password since connections with differing such and /// the same host/port must not be mixed up. /// #[derive(Clone, PartialEq, Eq)] struct PoolKey(Arc); impl PoolKey { fn new(uri: &Uri, proxy: Option<&Proxy>) -> Self { let inner = PoolKeyInner( uri.scheme().expect("uri with scheme").clone(), uri.authority().expect("uri with authority").clone(), proxy.cloned(), ); PoolKey(Arc::new(inner)) } } #[derive(PartialEq, Eq)] struct PoolKeyInner(Scheme, Authority, Option); #[derive(Debug)] struct Pool { lru: VecDeque, max_idle_connections: usize, max_idle_connections_per_host: usize, max_idle_age: Duration, } impl Pool { fn new(config: &Config) -> Self { Pool { lru: VecDeque::new(), max_idle_connections: config.max_idle_connections(), max_idle_connections_per_host: config.max_idle_connections_per_host(), max_idle_age: config.max_idle_age().into(), } } fn purge(&mut self, now: Instant) { while self.lru.len() > self.max_idle_connections || self.front_is_too_old(now) { self.lru.pop_front(); } self.update_position_per_host(); let max = self.max_idle_connections_per_host; // unwrap is ok because update_position_per_host() should have set all self.lru.retain(|c| c.position_per_host.unwrap() < max); } fn front_is_too_old(&self, now: Instant) -> bool { self.lru.front().map(|c| c.age(now)) > Some(self.max_idle_age) } fn update_position_per_host(&mut self) { // Reset position counters for c in &mut self.lru { c.position_per_host = None; } loop { let maybe_uncounted = self .lru .iter() .rev() .find(|c| c.position_per_host.is_none()); let Some(uncounted) = maybe_uncounted else { break; // nothing more to count. }; let key_to_count = uncounted.key.clone(); for (position, c) in self .lru .iter_mut() .rev() .filter(|c| c.key == key_to_count) .enumerate() { c.position_per_host = Some(position); } } } fn add(&mut self, conn: Connection) { self.lru.push_back(conn) } fn get(&mut self, key: &PoolKey, max_idle_age: Duration, now: Instant) -> Option { while let Some(i) = self.lru.iter().position(|c| c.key == *key) { let mut conn = self.lru.remove(i).unwrap(); // unwrap ok since we just got the position // Before we release the connection, we probe that it appears to still work. if !conn.is_open() { // This connection is broken. Try find another one. continue; } if conn.age(now) >= max_idle_age { // A max_duration that is shorter in the request than the pool. // This connection survives in the pool, but is not used for this // specific connection. continue; } return Some(conn); } None } } impl fmt::Debug for ConnectionPool { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ConnectionPool") .field("connector", &self.connector) .finish() } } impl fmt::Debug for Connection { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Connection") .field("key", &self.key) .field("conn", &self.transport) .finish() } } impl fmt::Debug for PoolKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PoolKey") .field("scheme", &self.0 .0) .field("authority", &DebugAuthority(&self.0 .1)) .field("proxy", &self.0 .2) .finish() } } impl<'a, 'b> From<&'a ConnectionDetails<'b>> for PoolKey { fn from(details: &'a ConnectionDetails) -> Self { PoolKey::new(details.uri, details.config.proxy()) } } #[cfg(all(test, feature = "_test"))] mod test { use super::*; #[test] fn poolkey_new() { // Test that PoolKey::new() does not panic on unrecognized schemes. PoolKey::new(&Uri::from_static("zzz://example.com"), None); } } algesten-ureq-d58cf18/src/proxy.rs000066400000000000000000000446211505724604200172440ustar00rootroot00000000000000use std::convert::{TryFrom, TryInto}; use std::fmt; use std::sync::Arc; use ureq_proto::http::uri::{PathAndQuery, Scheme}; use http::Uri; use crate::http; use crate::util::{AuthorityExt, DebugUri}; use crate::Error; /// Proxy protocol #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum ProxyProtocol { /// CONNECT proxy over HTTP Http, /// CONNECT proxy over HTTPS Https, /// A SOCKS4 proxy Socks4, /// A SOCKS4a proxy (proxy can resolve domain name) Socks4A, /// SOCKS5 proxy Socks5, } impl ProxyProtocol { pub(crate) fn default_port(&self) -> u16 { match self { ProxyProtocol::Http => 80, ProxyProtocol::Https => 443, ProxyProtocol::Socks4 | ProxyProtocol::Socks4A | ProxyProtocol::Socks5 => 1080, } } pub(crate) fn is_socks(&self) -> bool { matches!(self, Self::Socks4 | Self::Socks4A | Self::Socks5) } pub(crate) fn is_connect(&self) -> bool { matches!(self, Self::Http | Self::Https) } fn default_resolve_target(&self) -> bool { match self { ProxyProtocol::Http => false, ProxyProtocol::Https => false, ProxyProtocol::Socks4 => true, // we must locally resolve before using proxy ProxyProtocol::Socks4A => false, ProxyProtocol::Socks5 => false, } } } /// Proxy server settings /// /// This struct represents a proxy server configuration that can be used to route HTTP/HTTPS /// requests through a proxy server. It supports various proxy protocols including HTTP CONNECT, /// HTTPS CONNECT, SOCKS4, SOCKS4A, and SOCKS5. /// /// # Protocol Support /// /// * `HTTP`: HTTP CONNECT proxy /// * `HTTPS`: HTTPS CONNECT proxy (requires a TLS provider) /// * `SOCKS4`: SOCKS4 proxy (requires **socks-proxy** feature) /// * `SOCKS4A`: SOCKS4A proxy (requires **socks-proxy** feature) /// * `SOCKS5`: SOCKS5 proxy (requires **socks-proxy** feature) /// /// # DNS Resolution /// /// The `resolve_target` setting controls where DNS resolution happens: /// /// * When `true`: DNS resolution happens locally before connecting to the proxy. /// The resolved IP address is sent to the proxy. /// * When `false`: The hostname is sent to the proxy, which performs DNS resolution. /// /// Default behavior: /// * For SOCKS4: `true` (local resolution required) /// * For all other protocols: `false` (proxy performs resolution) /// /// # Examples /// /// ```rust /// use ureq::{Proxy, ProxyProtocol}; /// /// // Create a proxy from a URI string /// let proxy = Proxy::new("http://localhost:8080").unwrap(); /// /// // Create a proxy using the builder pattern /// let proxy = Proxy::builder(ProxyProtocol::Socks5) /// .host("proxy.example.com") /// .port(1080) /// .username("user") /// .password("pass") /// .resolve_target(true) // Force local DNS resolution /// .build() /// .unwrap(); /// /// // Read proxy settings from environment variables /// if let Some(proxy) = Proxy::try_from_env() { /// // Use proxy from environment /// } /// ``` #[derive(Clone, Eq, Hash, PartialEq)] pub struct Proxy { inner: Arc, } #[derive(Eq, Hash, PartialEq)] struct ProxyInner { proto: ProxyProtocol, uri: Uri, from_env: bool, resolve_target: bool, } impl Proxy { /// Create a proxy from a uri. /// /// # Arguments: /// /// * `proxy` - a str of format `://:@:port` . All parts /// except host are optional. /// /// ### Protocols /// /// * `http`: HTTP CONNECT proxy /// * `https`: HTTPS CONNECT proxy (requires a TLS provider) /// * `socks4`: SOCKS4 (requires **socks-proxy** feature) /// * `socks4a`: SOCKS4A (requires **socks-proxy** feature) /// * `socks5` and `socks`: SOCKS5 (requires **socks-proxy** feature) /// /// # Examples proxy formats /// /// * `http://127.0.0.1:8080` /// * `socks5://john:smith@socks.google.com` /// * `john:smith@socks.google.com:8000` /// * `localhost` pub fn new(proxy: &str) -> Result { Self::new_with_flag(proxy, false, None) } /// Creates a proxy config using a builder. pub fn builder(p: ProxyProtocol) -> ProxyBuilder { ProxyBuilder { protocol: p, host: None, port: None, username: None, password: None, resolve_target: p.default_resolve_target(), } } fn new_with_flag( proxy: &str, from_env: bool, resolve_target: Option, ) -> Result { let mut uri = proxy.parse::().or(Err(Error::InvalidProxyUrl))?; // The uri must have an authority part (with the host), or // it is invalid. let _ = uri.authority().ok_or(Error::InvalidProxyUrl)?; let scheme = match uri.scheme_str() { Some(v) => v, None => { // The default protocol is Proto::HTTP, and it is missing in // the uri. Let's put it in place. uri = insert_default_scheme(uri); "http" } }; let proto: ProxyProtocol = scheme.try_into()?; let resolve_target = resolve_target.unwrap_or(proto.default_resolve_target()); let inner = ProxyInner { proto, uri, from_env, resolve_target, }; Ok(Self { inner: Arc::new(inner), }) } /// Read proxy settings from environment variables. /// /// The environment variable is expected to contain a proxy URI. The following /// environment variables are attempted: /// /// * `ALL_PROXY` /// * `HTTPS_PROXY` /// * `HTTP_PROXY` /// /// Returns `None` if no environment variable is set or the URI is invalid. pub fn try_from_env() -> Option { const TRY_ENV: &[&str] = &[ "ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", ]; for attempt in TRY_ENV { if let Ok(env) = std::env::var(attempt) { if let Ok(proxy) = Self::new_with_flag(&env, true, None) { return Some(proxy); } } } None } /// The configured protocol. pub fn protocol(&self) -> ProxyProtocol { self.inner.proto } /// The proxy uri pub fn uri(&self) -> &Uri { &self.inner.uri } /// The host part of the proxy uri pub fn host(&self) -> &str { self.inner .uri .authority() .map(|a| a.host()) .expect("constructor to ensure there is an authority") } /// The port of the proxy uri pub fn port(&self) -> u16 { self.inner .uri .authority() .and_then(|a| a.port_u16()) .unwrap_or_else(|| self.inner.proto.default_port()) } /// The username of the proxy uri pub fn username(&self) -> Option<&str> { self.inner.uri.authority().and_then(|a| a.username()) } /// The password of the proxy uri pub fn password(&self) -> Option<&str> { self.inner.uri.authority().and_then(|a| a.password()) } /// Whether this proxy setting was created manually or from /// environment variables. pub fn is_from_env(&self) -> bool { self.inner.from_env } /// Whether to resolve target locally before calling the proxy. /// /// * `true` - resolve the DNS before calling proxy. /// * `false` - send the target host to the proxy and let it resolve. /// /// Defaults to `false` for all proxies protocols except `SOCKS4`. I.e. the normal /// case is to let the proxy resolve the target host. pub fn resolve_target(&self) -> bool { self.inner.resolve_target } } fn insert_default_scheme(uri: Uri) -> Uri { let mut parts = uri.into_parts(); parts.scheme = Some(Scheme::HTTP); // For some reason uri.into_parts can produce None for // the path, but Uri::from_parts does not accept that. parts.path_and_query = parts .path_and_query .or_else(|| Some(PathAndQuery::from_static("/"))); Uri::from_parts(parts).unwrap() } /// Builder for configuring a proxy. /// /// Obtained via [`Proxy::builder()`]. pub struct ProxyBuilder { protocol: ProxyProtocol, host: Option, port: Option, username: Option, password: Option, resolve_target: bool, } impl ProxyBuilder { /// Set the proxy hostname /// /// Defaults to `localhost`. Invalid hostnames surface in [`ProxyBuilder::build()`]. pub fn host(mut self, host: &str) -> Self { self.host = Some(host.to_string()); self } /// Set the proxy port /// /// Defaults to whatever is default for the chosen [`ProxyProtocol`]. pub fn port(mut self, port: u16) -> Self { self.port = Some(port); self } /// Set the username /// /// Defaults to none. Invalid usernames surface in [`ProxyBuilder::build()`]. pub fn username(mut self, v: &str) -> Self { self.username = Some(v.to_string()); self } /// Set the password /// /// If you want to set only a password, no username, i.e. `https://secret@foo.com`, /// you need to set it as [`ProxyBuilder::username()`]. /// /// Defaults to none. Invalid passwords surface in [`ProxyBuilder::build()`]. pub fn password(mut self, v: &str) -> Self { self.password = Some(v.to_string()); self } /// Whether to resolve the target host locally before calling the proxy. /// /// * `true` - resolve target host locally before calling proxy. /// * `false` - let proxy resolve the host. /// /// For SOCKS4, this defaults to `true`, for all other protocols `false`. I.e. /// in the "normal" case, we let the proxy itself resolve host names. pub fn resolve_target(mut self, do_resolve: bool) -> Self { self.resolve_target = do_resolve; self } /// Construct the [`Proxy`] pub fn build(self) -> Result { let host = self.host.as_deref().unwrap_or("localhost"); let port = self.port.unwrap_or(self.protocol.default_port()); let mut userpass = String::new(); if let Some(username) = self.username { userpass.push_str(&username); if let Some(password) = self.password { userpass.push(':'); userpass.push_str(&password); } userpass.push('@'); } // TODO(martin): This incurs as a somewhat unnecessary allocation, but we get some // validation and normalization in new_with_flag. This could be refactored // in the future. let proxy = format!("{}://{}{}:{}", self.protocol, userpass, host, port); Proxy::new_with_flag(&proxy, false, Some(self.resolve_target)) } } impl TryFrom<&str> for ProxyProtocol { type Error = Error; fn try_from(scheme: &str) -> Result { match scheme.to_ascii_lowercase().as_str() { "http" => Ok(ProxyProtocol::Http), "https" => Ok(ProxyProtocol::Https), "socks4" => Ok(ProxyProtocol::Socks4), "socks4a" => Ok(ProxyProtocol::Socks4A), "socks" => Ok(ProxyProtocol::Socks5), "socks5" => Ok(ProxyProtocol::Socks5), _ => Err(Error::InvalidProxyUrl), } } } impl fmt::Debug for Proxy { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Proxy") .field("proto", &self.inner.proto) .field("uri", &DebugUri(&self.inner.uri)) .field("from_env", &self.inner.from_env) .finish() } } impl fmt::Display for ProxyProtocol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ProxyProtocol::Http => write!(f, "HTTP"), ProxyProtocol::Https => write!(f, "HTTPS"), ProxyProtocol::Socks4 => write!(f, "SOCKS4"), ProxyProtocol::Socks4A => write!(f, "SOCKS4a"), ProxyProtocol::Socks5 => write!(f, "SOCKS5"), } } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_proxy_fakeproto() { assert!(Proxy::new("fakeproto://localhost").is_err()); } #[test] fn parse_proxy_http_user_pass_server_port() { let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Http); } #[test] fn parse_proxy_http_user_pass_server_port_trailing_slash() { let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999/").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Http); } #[test] fn parse_proxy_socks4_user_pass_server_port() { let proxy = Proxy::new("socks4://user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4); } #[test] fn parse_proxy_socks4a_user_pass_server_port() { let proxy = Proxy::new("socks4a://user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4A); } #[test] fn parse_proxy_socks_user_pass_server_port() { let proxy = Proxy::new("socks://user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5); } #[test] fn parse_proxy_socks5_user_pass_server_port() { let proxy = Proxy::new("socks5://user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5); } #[test] fn parse_proxy_user_pass_server_port() { let proxy = Proxy::new("user:p@ssw0rd@localhost:9999").unwrap(); assert_eq!(proxy.username(), Some("user")); assert_eq!(proxy.password(), Some("p@ssw0rd")); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Http); } #[test] fn parse_proxy_server_port() { let proxy = Proxy::new("localhost:9999").unwrap(); assert_eq!(proxy.username(), None); assert_eq!(proxy.password(), None); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 9999); assert_eq!(proxy.inner.proto, ProxyProtocol::Http); } #[test] fn parse_proxy_server() { let proxy = Proxy::new("localhost").unwrap(); assert_eq!(proxy.username(), None); assert_eq!(proxy.password(), None); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 80); assert_eq!(proxy.inner.proto, ProxyProtocol::Http); } } #[cfg(test)] mod test { use super::*; use assert_no_alloc::*; #[test] fn proxy_clone_does_not_allocate() { let c = Proxy::new("socks://1.2.3.4").unwrap(); assert_no_alloc(|| c.clone()); } #[test] fn proxy_new_default_scheme() { let c = Proxy::new("localhost:1234").unwrap(); assert_eq!(c.protocol(), ProxyProtocol::Http); assert_eq!(c.uri(), "http://localhost:1234"); } #[test] fn proxy_empty_env_url() { let result = Proxy::new_with_flag("", false, None); assert!(result.is_err()); } #[test] fn proxy_invalid_env_url() { let result = Proxy::new_with_flag("r32/?//52:**", false, None); assert!(result.is_err()); } #[test] fn proxy_builder() { let proxy = Proxy::builder(ProxyProtocol::Socks4) .host("my-proxy.com") .port(5551) .resolve_target(false) .build() .unwrap(); assert_eq!(proxy.protocol(), ProxyProtocol::Socks4); assert_eq!(proxy.uri(), "SOCKS4://my-proxy.com:5551/"); assert_eq!(proxy.host(), "my-proxy.com"); assert_eq!(proxy.port(), 5551); assert_eq!(proxy.username(), None); assert_eq!(proxy.password(), None); assert_eq!(proxy.is_from_env(), false); assert_eq!(proxy.resolve_target(), false); } #[test] fn proxy_builder_username() { let proxy = Proxy::builder(ProxyProtocol::Https) .username("hemligearne") .build() .unwrap(); assert_eq!(proxy.protocol(), ProxyProtocol::Https); assert_eq!(proxy.uri(), "https://hemligearne@localhost:443/"); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 443); assert_eq!(proxy.username(), Some("hemligearne")); assert_eq!(proxy.password(), None); assert_eq!(proxy.is_from_env(), false); assert_eq!(proxy.resolve_target(), false); } #[test] fn proxy_builder_username_password() { let proxy = Proxy::builder(ProxyProtocol::Https) .username("hemligearne") .password("kulgrej") .build() .unwrap(); assert_eq!(proxy.protocol(), ProxyProtocol::Https); assert_eq!(proxy.uri(), "https://hemligearne:kulgrej@localhost:443/"); assert_eq!(proxy.host(), "localhost"); assert_eq!(proxy.port(), 443); assert_eq!(proxy.username(), Some("hemligearne")); assert_eq!(proxy.password(), Some("kulgrej")); assert_eq!(proxy.is_from_env(), false); assert_eq!(proxy.resolve_target(), false); } } algesten-ureq-d58cf18/src/query.rs000066400000000000000000000160341505724604200172250ustar00rootroot00000000000000use std::borrow::Cow; use std::fmt; use std::iter::Enumerate; use std::ops::Deref; use std::str::Chars; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; /// AsciiSet for characters that need to be percent-encoded in URL query parameters. /// /// This set follows URL specification from pub const ENCODED_IN_QUERY: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') .add(b'#') .add(b'$') .add(b'%') .add(b'&') .add(b'\'') // Single quote should be encoded according to the URL specs .add(b'+') .add(b',') .add(b'/') .add(b':') .add(b';') .add(b'<') .add(b'=') .add(b'>') .add(b'?') .add(b'@') .add(b'[') .add(b'\\') .add(b']') .add(b'^') .add(b'`') .add(b'{') .add(b'|') .add(b'}'); #[derive(Clone)] pub(crate) struct QueryParam<'a> { source: Source<'a>, } #[derive(Clone)] enum Source<'a> { Borrowed(&'a str), Owned(String), } /// Percent-encode a string using the ENCODED_IN_QUERY set. pub fn url_enc(i: &str) -> Cow { utf8_percent_encode(i, ENCODED_IN_QUERY).into() } /// Percent-encode a string using the ENCODED_IN_QUERY set, but replace encoded `%20` with `+`. pub fn form_url_enc(i: &str) -> Cow { let mut iter = utf8_percent_encode(i, ENCODED_IN_QUERY).map(|part| match part { "%20" => "+", _ => part, }); // We try to avoid allocating if we can (returning a Cow). match iter.next() { None => "".into(), Some(first) => match iter.next() { // Case avoids allocation None => first.into(), // Following allocates Some(second) => { let mut string = first.to_owned(); string.push_str(second); string.extend(iter); string.into() } }, } } impl<'a> QueryParam<'a> { /// Create a new key-value pair with both the key and value percent-encoded. pub fn new_key_value(param: &str, value: &str) -> QueryParam<'static> { let s = format!("{}={}", url_enc(param), url_enc(value)); QueryParam { source: Source::Owned(s), } } /// Create a new key-value pair without percent-encoding. /// /// This is used by query_raw() to add parameters that are already encoded /// or that should not be encoded. pub fn new_key_value_raw(param: &str, value: &str) -> QueryParam<'static> { let s = format!("{}={}", param, value); QueryParam { source: Source::Owned(s), } } fn as_str(&self) -> &str { match &self.source { Source::Borrowed(v) => v, Source::Owned(v) => v.as_str(), } } } pub(crate) fn parse_query_params(query_string: &str) -> impl Iterator> { assert!(query_string.is_ascii()); QueryParamIterator(query_string, query_string.chars().enumerate()) } struct QueryParamIterator<'a>(&'a str, Enumerate>); impl<'a> Iterator for QueryParamIterator<'a> { type Item = QueryParam<'a>; fn next(&mut self) -> Option { let mut first = None; let mut value = None; let mut separator = None; for (n, c) in self.1.by_ref() { if first.is_none() { first = Some(n); } if value.is_none() && c == '=' { value = Some(n + 1); } if c == '&' { separator = Some(n); break; } } if let Some(start) = first { let end = separator.unwrap_or(self.0.len()); let chunk = &self.0[start..end]; return Some(QueryParam { source: Source::Borrowed(chunk), }); } None } } impl<'a> fmt::Debug for QueryParam<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("QueryParam").field(&self.as_str()).finish() } } impl<'a> fmt::Display for QueryParam<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.source { Source::Borrowed(v) => write!(f, "{}", v), Source::Owned(v) => write!(f, "{}", v), } } } impl<'a> Deref for QueryParam<'a> { type Target = str; fn deref(&self) -> &Self::Target { self.as_str() } } impl<'a> PartialEq for QueryParam<'a> { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } #[cfg(test)] mod test { use super::*; use crate::http::Uri; #[test] fn query_string_does_not_start_with_question_mark() { let u: Uri = "https://foo.com/qwe?abc=qwe".parse().unwrap(); assert_eq!(u.query(), Some("abc=qwe")); } #[test] fn percent_encoding_is_not_decoded() { let u: Uri = "https://foo.com/qwe?abc=%20123".parse().unwrap(); assert_eq!(u.query(), Some("abc=%20123")); } #[test] fn fragments_are_not_a_thing() { let u: Uri = "https://foo.com/qwe?abc=qwe#yaz".parse().unwrap(); assert_eq!(u.to_string(), "https://foo.com/qwe?abc=qwe"); } fn p(s: &str) -> Vec { parse_query_params(s).map(|q| q.to_string()).collect() } #[test] fn parse_query_string() { assert_eq!(parse_query_params("").next(), None); assert_eq!(p("&"), vec![""]); assert_eq!(p("="), vec!["="]); assert_eq!(p("&="), vec!["", "="]); assert_eq!(p("foo=bar"), vec!["foo=bar"]); assert_eq!(p("foo=bar&"), vec!["foo=bar"]); assert_eq!(p("foo=bar&foo2=bar2"), vec!["foo=bar", "foo2=bar2"]); } #[test] fn do_not_url_encode_some_things() { const NOT_ENCODE: &str = "!()*-._~"; let q = QueryParam::new_key_value("key", NOT_ENCODE); assert_eq!(q.as_str(), format!("key={}", NOT_ENCODE)); } #[test] fn special_encoding_space_for_form() { let value = "value with spaces and 'quotes'"; let form = form_url_enc(value); assert_eq!(form.as_ref(), "value+with+spaces+and+%27quotes%27"); } #[test] fn do_encode_single_quote() { let value = "value'with'quotes"; let q = QueryParam::new_key_value("key", value); assert_eq!(q.as_str(), "key=value%27with%27quotes"); } #[test] fn raw_query_param_no_encoding() { // Use URI-valid characters for the raw param test let value = "value-without-spaces&special='chars'"; let q = QueryParam::new_key_value_raw("key", value); assert_eq!(q.as_str(), format!("key={}", value)); // Verify that symbols like &=+?/ remain unencoded in raw mode // but are encoded in normal mode let special_symbols = "symbols&=+?/'"; let q_raw = QueryParam::new_key_value_raw("raw", special_symbols); let q_encoded = QueryParam::new_key_value("encoded", special_symbols); // Raw should preserve all special chars, encoded should encode them assert_eq!(q_raw.as_str(), "raw=symbols&=+?/'"); assert_ne!(q_raw.as_str(), q_encoded.as_str()); } } algesten-ureq-d58cf18/src/request.rs000066400000000000000000000636431505724604200175600ustar00rootroot00000000000000use std::convert::TryFrom; use std::fmt; use std::marker::PhantomData; use http::{Extensions, HeaderMap, HeaderName, HeaderValue}; use http::{Method, Request, Response, Uri, Version}; use crate::body::Body; use crate::config::typestate::RequestScope; use crate::config::{Config, ConfigBuilder, RequestLevelConfig}; use crate::http; use crate::query::form_url_enc; use crate::query::{parse_query_params, QueryParam}; use crate::send_body::AsSendBody; use crate::util::private::Private; use crate::util::HeaderMapExt; use crate::util::UriExt; use crate::{Agent, Error, SendBody}; /// Transparent wrapper around [`http::request::Builder`]. /// /// The purpose is to provide the [`.call()`][RequestBuilder::call] and [`.send()`][RequestBuilder::send] /// and additional helpers for query parameters like [`.query()`][RequestBuilder::query] functions to /// make an API for sending requests. pub struct RequestBuilder { agent: Agent, builder: http::request::Builder, query_extra: Vec>, // This is only used in case http::request::Builder contains an error // (such as URL parsing error), and the user wants a `.config()`. dummy_config: Option>, _ph: PhantomData, } /// Typestate when [`RequestBuilder`] has no send body. /// /// `RequestBuilder` /// /// Methods: GET, DELETE, HEAD, OPTIONS, CONNECT, TRACE #[derive(Debug)] pub struct WithoutBody(()); impl Private for WithoutBody {} /// Typestate when [`RequestBuilder`] needs to a send body. /// /// `RequestBuilder` /// /// Methods: POST, PUT, PATCH #[derive(Debug)] pub struct WithBody(()); impl Private for WithBody {} impl RequestBuilder { /// Get the HTTP Method for this request. /// /// By default this is `GET`. If builder has error, returns None. /// /// # Examples /// /// ``` /// use ureq::http::Method; /// /// let req = ureq::get("http://httpbin.org/get"); /// assert_eq!(req.method_ref(),Some(&Method::GET)); /// /// let req = ureq::post("http://httpbin.org/post"); /// assert_eq!(req.method_ref(),Some(&Method::POST)); /// ``` pub fn method_ref(&self) -> Option<&Method> { self.builder.method_ref() } /// Appends a header to this request builder. /// /// This function will append the provided key/value as a header to the /// set of headers. It does not replace headers. /// /// # Examples /// /// ``` /// let req = ureq::get("https://httpbin.org/get") /// .header("X-Custom-Foo", "bar"); /// ``` pub fn header(mut self, key: K, value: V) -> Self where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { self.builder = self.builder.header(key, value); self } /// Get header on this request builder. /// /// When builder has error returns `None`. /// /// # Example /// /// ``` /// let req = ureq::get("http://httpbin.org/get") /// .header("Accept", "text/html") /// .header("X-Custom-Foo", "bar"); /// let headers = req.headers_ref().unwrap(); /// assert_eq!( headers["Accept"], "text/html" ); /// assert_eq!( headers["X-Custom-Foo"], "bar" ); /// ``` pub fn headers_ref(&self) -> Option<&HeaderMap> { self.builder.headers_ref() } /// Get headers on this request builder. /// /// When builder has error returns `None`. /// /// # Example /// /// ``` /// # use ureq::http::header::HeaderValue; /// let mut req = ureq::get("http://httpbin.org/get"); /// { /// let headers = req.headers_mut().unwrap(); /// headers.insert("Accept", HeaderValue::from_static("text/html")); /// headers.insert("X-Custom-Foo", HeaderValue::from_static("bar")); /// } /// let headers = req.headers_ref().unwrap(); /// assert_eq!( headers["Accept"], "text/html" ); /// assert_eq!( headers["X-Custom-Foo"], "bar" ); /// ``` pub fn headers_mut(&mut self) -> Option<&mut HeaderMap> { self.builder.headers_mut() } /// Add a query parameter to the URL. /// /// Always appends a new parameter, also when using the name of /// an already existing one. Both key and value are percent-encoded /// according to the URL specification. /// /// # Examples /// /// ``` /// // Creates a URL with an encoded query parameter: /// // https://httpbin.org/get?my_query=with%20value /// let req = ureq::get("https://httpbin.org/get") /// .query("my_query", "with value"); /// ``` pub fn query(mut self, key: K, value: V) -> Self where K: AsRef, V: AsRef, { self.query_extra .push(QueryParam::new_key_value(key.as_ref(), value.as_ref())); self } /// Add a query parameter to the URL without percent-encoding. /// /// Always appends a new parameter, also when using the name of /// an already existing one. Neither key nor value are percent-encoded, /// which allows you to use pre-encoded values or bypass encoding. /// /// **Important note**: When using this method, you must ensure that your /// query parameters don't contain characters that would make the URI invalid, /// such as spaces or control characters. You are responsible for any pre-encoding /// needed for URI validity. If you're unsure, use the regular `query()` method instead. /// /// # Examples /// /// ``` /// // Creates a URL with a raw query parameter: /// // https://httpbin.org/get?my_query=pre-encoded%20value /// let req = ureq::get("https://httpbin.org/get") /// .query_raw("my_query", "pre-encoded%20value"); /// ``` pub fn query_raw(mut self, key: K, value: V) -> Self where K: AsRef, V: AsRef, { self.query_extra .push(QueryParam::new_key_value_raw(key.as_ref(), value.as_ref())); self } /// Set multi query parameters. /// /// Both keys and values are percent-encoded according to the URL specification. /// /// For example, to set `?format=json&dest=%2Flogin` /// /// ``` /// let query = vec![ /// ("format", "json"), /// ("dest", "/login"), /// ]; /// /// let response = ureq::get("http://httpbin.org/get") /// .query_pairs(query) /// .call()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn query_pairs(mut self, iter: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { self.query_extra.extend( iter.into_iter() .map(|(k, v)| QueryParam::new_key_value(k.as_ref(), v.as_ref())), ); self } /// Set multi query parameters without percent-encoding. /// /// Neither keys nor values are percent-encoded, which allows you to use /// pre-encoded values or bypass encoding. /// /// **Important note**: When using this method, you must ensure that your /// query parameters don't contain characters that would make the URI invalid, /// such as spaces or control characters. You are responsible for any pre-encoding /// needed for URI validity. If you're unsure, use the regular `query_pairs()` method instead. /// /// For example, to set `?format=json&dest=/login` without encoding: /// /// ``` /// let query = vec![ /// ("format", "json"), /// ("dest", "/login"), /// ]; /// /// let response = ureq::get("http://httpbin.org/get") /// .query_pairs_raw(query) /// .call()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn query_pairs_raw(mut self, iter: I) -> Self where I: IntoIterator, K: AsRef, V: AsRef, { self.query_extra.extend( iter.into_iter() .map(|(k, v)| QueryParam::new_key_value_raw(k.as_ref(), v.as_ref())), ); self } /// Overrides the URI for this request. /// /// Typically this is set via `ureq::get()` or `Agent::get()`. This /// lets us change it. /// /// # Examples /// /// ``` /// let req = ureq::get("https://www.google.com/") /// .uri("https://httpbin.org/get"); /// ``` pub fn uri(mut self, uri: T) -> Self where Uri: TryFrom, >::Error: Into, { self.builder = self.builder.uri(uri); self } /// Get the URI for this request /// /// By default this is `/`. /// /// # Examples /// /// ``` /// let req = ureq::get("http://httpbin.org/get"); /// assert_eq!(req.uri_ref().unwrap(), "http://httpbin.org/get"); /// ``` pub fn uri_ref(&self) -> Option<&Uri> { self.builder.uri_ref() } /// Set the HTTP version for this request. /// /// By default this is HTTP/1.1. /// ureq only handles HTTP/1.1 and HTTP/1.0. /// /// # Examples /// /// ``` /// use ureq::http::Version; /// /// let req = ureq::get("https://www.google.com/") /// .version(Version::HTTP_10); /// ``` pub fn version(mut self, version: Version) -> Self { self.builder = self.builder.version(version); self } /// Get the HTTP version for this request /// /// By default this is HTTP/1.1. /// /// # Examples /// /// ``` /// use ureq::http::Version; /// /// let req = ureq::get("http://httpbin.org/get"); /// assert_eq!(req.version_ref().unwrap(), &Version::HTTP_11); /// ``` pub fn version_ref(&self) -> Option<&Version> { self.builder.version_ref() } /// Override agent level config on the request level. /// /// The agent config is copied and modified on request level. /// /// # Example /// /// ``` /// use ureq::Agent; /// /// let agent: Agent = Agent::config_builder() /// .https_only(false) /// .build() /// .into(); /// /// let request = agent.get("http://httpbin.org/get") /// .config() /// // override agent default for this request /// .https_only(true) /// .build(); /// /// // Make the request /// let result = request.call(); /// /// // The https_only was set on request level /// assert!(matches!(result.unwrap_err(), ureq::Error::RequireHttpsOnly(_))); /// # Ok::<_, ureq::Error>(()) /// ``` pub fn config(self) -> ConfigBuilder> { ConfigBuilder(RequestScope(self)) } /// Adds an extension to this builder /// /// # Examples /// /// ``` /// let req = ureq::get("http://httpbin.org/get") /// .extension("My Extension"); /// /// assert_eq!(req.extensions_ref().unwrap().get::<&'static str>(), /// Some(&"My Extension")); /// ``` pub fn extension(mut self, extension: T) -> Self where T: Clone + std::any::Any + Send + Sync + 'static, { self.builder = self.builder.extension(extension); self } /// Get a reference to the extensions for this request builder. /// /// If the builder has an error, this returns `None`. /// /// # Example /// /// ``` /// let req = ureq::get("http://httpbin.org/get") /// .extension("My Extension").extension(5u32); /// let extensions = req.extensions_ref().unwrap(); /// assert_eq!(extensions.get::<&'static str>(), Some(&"My Extension")); /// assert_eq!(extensions.get::(), Some(&5u32)); /// ``` pub fn extensions_ref(&self) -> Option<&Extensions> { self.builder.extensions_ref() } /// Get a mutable reference to the extensions for this request builder. /// /// If the builder has an error, this returns `None`. /// /// # Example /// /// ``` /// let mut req = ureq::get("http://httpbin.org/get"); /// let mut extensions = req.extensions_mut().unwrap(); /// extensions.insert(5u32); /// assert_eq!(extensions.get::(), Some(&5u32)); /// ``` pub fn extensions_mut(&mut self) -> Option<&mut Extensions> { self.builder.extensions_mut() } pub(crate) fn request_level_config(&mut self) -> &mut Config { let Some(exts) = self.builder.extensions_mut() else { // This means self.builder has an error such as URL parsing error. // The error will surface on .call() (or .send()) and we fill in // a dummy Config meanwhile. return self .dummy_config .get_or_insert_with(|| Box::new(Config::default())); }; if exts.get::().is_none() { exts.insert(self.agent.new_request_level_config()); } // Unwrap is OK because of above check let req_level: &mut RequestLevelConfig = exts.get_mut().unwrap(); &mut req_level.0 } } impl RequestBuilder { pub(crate) fn new(agent: Agent, method: Method, uri: T) -> Self where Uri: TryFrom, >::Error: Into, { Self { agent, builder: Request::builder().method(method).uri(uri), query_extra: vec![], dummy_config: None, _ph: PhantomData, } } /// Sends the request and blocks the caller until we receive a response. /// /// It sends neither `Content-Length` nor `Transfer-Encoding`. /// /// ``` /// let res = ureq::get("http://httpbin.org/get") /// .call()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn call(self) -> Result, Error> { let request = self.builder.body(())?; do_call(self.agent, request, self.query_extra, SendBody::none()) } /// Force sending a body. /// /// This is an escape hatch to interact with broken services. /// /// According to the spec, methods such as GET, DELETE and TRACE should /// not have a body. Despite that there are broken API services and /// servers that use it. /// /// Example using DELETE while sending a body. /// /// ``` /// let res = ureq::delete("http://httpbin.org/delete") /// // this "unlocks" send() below /// .force_send_body() /// .send("DELETE with body is not correct")?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn force_send_body(mut self) -> RequestBuilder { if let Some(exts) = self.extensions_mut() { exts.insert(ForceSendBody); } RequestBuilder { agent: self.agent, builder: self.builder, query_extra: self.query_extra, dummy_config: None, _ph: PhantomData, } } } #[derive(Debug, Clone)] pub(crate) struct ForceSendBody; impl RequestBuilder { pub(crate) fn new(agent: Agent, method: Method, uri: T) -> Self where Uri: TryFrom, >::Error: Into, { Self { agent, builder: Request::builder().method(method).uri(uri), query_extra: vec![], dummy_config: None, _ph: PhantomData, } } /// Set the content-type header. /// /// ``` /// let res = ureq::post("http://httpbin.org/post") /// .content_type("text/html; charset=utf-8") /// .send("åäö")?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn content_type(mut self, content_type: V) -> Self where HeaderValue: TryFrom, >::Error: Into, { self.builder = self.builder.header("content-type", content_type); self } /// Send body data and blocks the caller until we receive response. /// /// ``` /// let res = ureq::post("http://httpbin.org/post") /// .send(&[0_u8; 1000])?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn send(self, data: impl AsSendBody) -> Result, Error> { let request = self.builder.body(())?; let mut data_ref = data; do_call(self.agent, request, self.query_extra, data_ref.as_body()) } /// Send an empty body. /// /// The method is POST, PUT or PATCH, which normally has a body. Using /// this function makes it explicit you want to send an empty body despite /// the method. /// /// This is equivalent to `.send(&[])`. /// /// ``` /// let res = ureq::post("http://httpbin.org/post") /// .send_empty()?; /// # Ok::<_, ureq::Error>(()) /// ``` pub fn send_empty(self) -> Result, Error> { self.send(&[]) } /// Send form encoded data. /// /// Constructs a [form submission] with the content-type header /// `application/x-www-form-urlencoded`. Keys and values will be URL encoded. /// /// ``` /// let form = [ /// ("name", "martin"), /// ("favorite_bird", "blue-footed booby"), /// ]; /// /// let response = ureq::post("http://httpbin.org/post") /// .send_form(form)?; /// # Ok::<_, ureq::Error>(()) /// ``` /// /// [form submission]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST#url-encoded_form_submission pub fn send_form(self, iter: I) -> Result, Error> where I: IntoIterator, K: AsRef, V: AsRef, { let iter = iter.into_iter(); // TODO(martin): can we calculate a size hint for capacity here? let mut body = String::new(); for (k, v) in iter { if !body.is_empty() { body.push('&'); } body.push_str(&form_url_enc(k.as_ref())); body.push('='); body.push_str(&form_url_enc(v.as_ref())); } let mut request = self.builder.body(())?; if !request.headers().has_content_type() { request.headers_mut().append( http::header::CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"), ); } do_call(self.agent, request, self.query_extra, body.as_body()) } /// Send body data as JSON. /// /// Requires the **json** feature. /// /// The data typically derives [`Serialize`](serde::Serialize) and is converted /// to a string before sending (does allocate). Will set the content-type header /// `application/json`. /// /// ``` /// use serde::Serialize; /// /// #[derive(Serialize)] /// struct MyData { /// thing: String, /// } /// /// let body = MyData { /// thing: "yo".to_string(), /// }; /// /// let res = ureq::post("http://httpbin.org/post") /// .send_json(&body)?; /// # Ok::<_, ureq::Error>(()) /// ``` #[cfg(feature = "json")] pub fn send_json(self, data: impl serde::ser::Serialize) -> Result, Error> { let mut request = self.builder.body(())?; let body = SendBody::from_json(&data)?; if !request.headers().has_content_type() { request.headers_mut().append( http::header::CONTENT_TYPE, HeaderValue::from_static("application/json; charset=utf-8"), ); } do_call(self.agent, request, self.query_extra, body) } } fn do_call( agent: Agent, mut request: Request<()>, query_extra: Vec>, body: SendBody, ) -> Result, Error> { if !query_extra.is_empty() { request.uri().ensure_valid_url()?; request = amend_request_query(request, query_extra.into_iter()); } let response = agent.run_via_middleware(request, body)?; Ok(response) } fn amend_request_query( request: Request<()>, query_extra: impl Iterator>, ) -> Request<()> { let (mut parts, body) = request.into_parts(); let uri = parts.uri; let mut path = uri.path().to_string(); let query_existing = parse_query_params(uri.query().unwrap_or("")); let mut do_first = true; fn append<'a>( path: &mut String, do_first: &mut bool, iter: impl Iterator>, ) { for q in iter { if *do_first { *do_first = false; path.push('?'); } else { path.push('&'); } path.push_str(&q); } } append(&mut path, &mut do_first, query_existing); append(&mut path, &mut do_first, query_extra); // Unwraps are OK, because we had a correct URI to begin with let rebuild = Uri::builder() .scheme(uri.scheme().unwrap().clone()) .authority(uri.authority().unwrap().clone()) .path_and_query(path) .build() .unwrap(); parts.uri = rebuild; Request::from_parts(parts, body) } impl fmt::Debug for RequestBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RequestBuilder") // unwraps are OK because we can't be in this state without having method+uri .field("method", &self.builder.method_ref().unwrap()) .field("uri", &self.builder.uri_ref().unwrap()) .finish() } } impl fmt::Debug for RequestBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RequestBuilder") // unwraps are OK because we can't be in this state without having method+uri .field("method", &self.builder.method_ref().unwrap()) .field("uri", &self.builder.uri_ref().unwrap()) .finish() } } #[cfg(test)] mod test { use std::time::Duration; use crate::get; use crate::test::init_test_log; use super::*; #[test] fn disallow_empty_host() { let err = crate::get("file:///some/path").call().unwrap_err(); assert_eq!(err.to_string(), "http: invalid format"); assert!(matches!(err, Error::Http(_))); } #[test] fn query_with_encoding() { let request = Request::builder() .uri("https://foo.bar/path") .body(()) .unwrap(); // Test that single quotes and spaces are encoded let amended = amend_request_query( request, vec![QueryParam::new_key_value("key", "value with 'quotes'")].into_iter(), ); assert_eq!( amended.uri(), "https://foo.bar/path?key=value%20with%20%27quotes%27" ); } #[test] fn query_raw_without_encoding() { let request = Request::builder() .uri("https://foo.bar/path") .body(()) .unwrap(); // Test that raw values remain unencoded (using URI-valid characters) let amended = amend_request_query( request, vec![QueryParam::new_key_value_raw("key", "value-with-'quotes'")].into_iter(), ); assert_eq!( amended.uri(), "https://foo.bar/path?key=value-with-'quotes'" ); } #[test] fn encoded_and_raw_combined() { let request = Request::builder() .uri("https://foo.bar/path") .body(()) .unwrap(); // Test combination of encoded and unencoded parameters let amended = amend_request_query( request, vec![ QueryParam::new_key_value("encoded", "value with spaces"), QueryParam::new_key_value_raw("raw", "value-without-spaces"), ] .into_iter(), ); assert_eq!( amended.uri(), "https://foo.bar/path?encoded=value%20with%20spaces&raw=value-without-spaces" ); } #[test] fn debug_print_without_body() { let call = crate::get("https://foo/bar"); assert_eq!( format!("{:?}", call), "RequestBuilder { method: GET, uri: https://foo/bar }" ); } #[test] fn debug_print_with_body() { let call = crate::post("https://foo/bar"); assert_eq!( format!("{:?}", call), "RequestBuilder { method: POST, uri: https://foo/bar }" ); } #[test] fn config_after_broken_url() { init_test_log(); get("http://x.y.z/ borked url") .config() .timeout_global(Some(Duration::from_millis(1))) .build(); } #[test] fn add_params_to_request_without_query() { let request = Request::builder() .uri("https://foo.bar/path") .body(()) .unwrap(); let amended = amend_request_query( request, vec![ QueryParam::new_key_value("x", "z"), QueryParam::new_key_value("ab", "cde"), ] .into_iter(), ); assert_eq!(amended.uri(), "https://foo.bar/path?x=z&ab=cde"); } #[test] fn add_params_to_request_with_query() { let request = Request::builder() .uri("https://foo.bar/path?x=z") .body(()) .unwrap(); let amended = amend_request_query( request, vec![QueryParam::new_key_value("ab", "cde")].into_iter(), ); assert_eq!(amended.uri(), "https://foo.bar/path?x=z&ab=cde"); } #[test] fn add_params_that_need_percent_encoding() { let request = Request::builder() .uri("https://foo.bar/path") .body(()) .unwrap(); let amended = amend_request_query( request, vec![QueryParam::new_key_value("å ", "i åa ä e ö")].into_iter(), ); assert_eq!( amended.uri(), "https://foo.bar/path?%C3%A5%20=i%20%C3%A5a%20%C3%A4%20e%20%C3%B6" ); } } algesten-ureq-d58cf18/src/request_ext.rs000066400000000000000000000226041505724604200204300ustar00rootroot00000000000000use crate::config::typestate::RequestExtScope; use crate::config::{Config, ConfigBuilder, RequestLevelConfig}; use crate::{http, Agent, AsSendBody, Body, Error}; use std::ops::Deref; use ureq_proto::http::{Request, Response}; /// Extension trait for [`http::Request`]. /// /// Adds additional convenience methods to the `Request` that are not available /// in the plain http API. pub trait RequestExt where S: AsSendBody, { /// Allows configuring the request behaviour, starting with the default [`Agent`]. /// /// This method allows configuring the request by using the default Agent, and performing /// additional configurations on top. /// This method returns a `WithAgent` struct that it is possible to call `configure()` and `run()` /// on to configure the request behaviour, or run the request. /// /// # Example /// /// ``` /// use ureq::{http, RequestExt, Error}; /// /// let request: Result, Error> = http::Request::builder() /// .method(http::Method::GET) /// .uri("http://foo.bar") /// .body(()) /// .unwrap() /// .with_default_agent() /// .configure() /// .http_status_as_error(false) /// .run(); /// ``` fn with_default_agent(self) -> WithAgent<'static, S> where Self: Sized, { let agent = Agent::new_with_defaults(); Self::with_agent(self, agent) } /// Allows configuring this request behaviour, using a specific [`Agent`]. /// /// This method allows configuring the request by using a user-provided `Agent` and performing /// additional configurations on top. /// This method returns a `WithAgent` struct that it is possible to call `configure()` and `run()` /// on to configure the request behaviour, or run the request. /// /// # Example /// /// ``` /// use ureq::{http, Agent, RequestExt, Error}; /// use std::time::Duration; /// let agent = Agent::config_builder() /// .timeout_global(Some(Duration::from_secs(30))) /// .build() /// .new_agent(); /// /// let request: Result, Error> = http::Request::builder() /// .method(http::Method::GET) /// .uri("http://foo.bar") /// .body(()) /// .unwrap() /// .with_agent(&agent) /// .run(); /// ``` /// # Example with further customizations /// /// In this example we use a specific agent, but apply a request-specific configuration on top. /// /// ``` /// use ureq::{http, Agent, RequestExt, Error}; /// use std::time::Duration; /// let mut agent = Agent::config_builder() /// .timeout_global(Some(Duration::from_secs(30))) /// .build() /// .new_agent(); /// /// let request: Result, Error> = http::Request::builder() /// .method(http::Method::GET) /// .uri("http://foo.bar") /// .body(()) /// .unwrap() /// .with_agent(&agent) /// .configure() /// .http_status_as_error(false) /// .run(); /// ``` fn with_agent<'a>(self, agent: impl Into>) -> WithAgent<'a, S>; } /// Wrapper struct that holds a [`Request`] associated with an [`Agent`]. pub struct WithAgent<'a, S: AsSendBody> { pub(crate) agent: AgentRef<'a>, pub(crate) request: Request, } impl<'a, S: AsSendBody> WithAgent<'a, S> { /// Returns a [`ConfigBuilder`] for configuring the request. /// /// This allows setting additional request-specific options before sending the request. pub fn configure(self) -> ConfigBuilder> { ConfigBuilder(RequestExtScope(self)) } /// Executes the request using the associated [`Agent`]. pub fn run(self) -> Result, Error> { self.agent.run(self.request) } } impl<'a, S: AsSendBody> WithAgent<'a, S> { pub(crate) fn request_level_config(&mut self) -> &mut Config { let request_level_config = self .request .extensions_mut() .get_mut::(); if request_level_config.is_none() { self.request .extensions_mut() .insert(self.agent.new_request_level_config()); } // Unwrap is safe because of the above check let req_level: &mut RequestLevelConfig = self .request .extensions_mut() .get_mut::() .unwrap(); &mut req_level.0 } } /// Reference type to hold an owned or borrowed [`Agent`]. pub enum AgentRef<'a> { Owned(Agent), Borrowed(&'a Agent), } impl RequestExt for http::Request { fn with_agent<'a>(self, agent: impl Into>) -> WithAgent<'a, S> { WithAgent { agent: agent.into(), request: self, } } } impl From for AgentRef<'static> { fn from(value: Agent) -> Self { AgentRef::Owned(value) } } impl<'a> From<&'a Agent> for AgentRef<'a> { fn from(value: &'a Agent) -> Self { AgentRef::Borrowed(value) } } impl Deref for AgentRef<'_> { type Target = Agent; fn deref(&self) -> &Self::Target { match self { AgentRef::Owned(agent) => agent, AgentRef::Borrowed(agent) => agent, } } } #[cfg(test)] mod tests { use super::*; use crate::config::RequestLevelConfig; use std::time::Duration; #[test] fn configure_request_with_default_agent() { // Create `http` crate request and configure with trait let request = http::Request::builder() .method(http::Method::GET) .uri("http://foo.bar") .body(()) .unwrap() .with_default_agent() .configure() .https_only(true) .build(); // Assert that the request-level configuration has been set let request_config = request .request .extensions() .get::() .cloned() .unwrap(); assert!(request_config.0.https_only()); } #[test] fn configure_request_default_agent_2() { // Create `http` crate request and configure with trait let request = http::Request::builder() .method(http::Method::GET) .uri("http://foo.bar") .body(()) .unwrap() .with_default_agent() .configure() .https_only(false) .build(); // Assert that the request-level configuration has been set let request_config = request .request .extensions() .get::() .cloned() .unwrap(); assert!(!request_config.0.https_only()); } #[test] fn configure_request_default_agent_3() { // Create `http` crate request let request = http::Request::builder() .method(http::Method::POST) .uri("http://foo.bar") .body("Some body") .unwrap(); // Configure with the trait let request = request .with_default_agent() .configure() .http_status_as_error(true) .build(); let request_config = request .request .extensions() .get::() .cloned() .unwrap(); assert!(request_config.0.http_status_as_error()); } #[test] fn configure_request_default_agent_4() { // Create `http` crate request let request = http::Request::builder() .method(http::Method::POST) .uri("http://foo.bar") .body("Some body") .unwrap(); // Configure with the trait let request = request .with_default_agent() .configure() .http_status_as_error(false) .build(); let request_config = request .request .extensions() .get::() .cloned() .unwrap(); assert!(!request_config.0.http_status_as_error()); } #[test] fn configure_request_specified_agent() { // Create `http` crate request let request = http::Request::builder() .method(http::Method::POST) .uri("http://foo.bar") .body("Some body") .unwrap(); // Configure with the trait let agent = Agent::config_builder() .timeout_per_call(Some(Duration::from_secs(60))) .build() .new_agent(); let request = request .with_agent(&agent) .configure() .http_status_as_error(false) .build(); let request_config = request .request .extensions() .get::() .cloned() .unwrap(); // The request-level config is the agent defaults + the explicitly configured stuff assert!(!request_config.0.http_status_as_error()); assert_eq!( request_config.0.timeouts().per_call, Some(Duration::from_secs(60)) ); } } algesten-ureq-d58cf18/src/response.rs000066400000000000000000000037151505724604200177200ustar00rootroot00000000000000use http::Uri; use crate::body::Body; use crate::http; #[derive(Debug, Clone)] pub(crate) struct ResponseUri(pub http::Uri); #[derive(Debug, Clone)] pub(crate) struct RedirectHistory(pub Vec); /// Extension trait for [`http::Response`]. /// /// Adds additional convenience methods to the `Response` that are not available /// in the plain http API. pub trait ResponseExt { /// The Uri that ultimately this Response is about. /// /// This can differ from the request uri when we have followed redirects. /// /// ``` /// use ureq::ResponseExt; /// /// let res = ureq::get("https://httpbin.org/redirect-to?url=%2Fget") /// .call().unwrap(); /// /// assert_eq!(res.get_uri(), "https://httpbin.org/get"); /// ``` fn get_uri(&self) -> &Uri; /// The full history of uris, including the request and final uri. /// /// Returns `None` when [`Config::save_redirect_history`][crate::config::Config::save_redirect_history] /// is `false`. /// /// /// ``` /// # use ureq::http::Uri; /// use ureq::ResponseExt; /// /// let uri1: Uri = "https://httpbin.org/redirect-to?url=%2Fget".parse().unwrap(); /// let uri2: Uri = "https://httpbin.org/get".parse::().unwrap(); /// /// let res = ureq::get(&uri1) /// .config() /// .save_redirect_history(true) /// .build() /// .call().unwrap(); /// /// let history = res.get_redirect_history().unwrap(); /// /// assert_eq!(history, &[uri1, uri2]); /// ``` fn get_redirect_history(&self) -> Option<&[Uri]>; } impl ResponseExt for http::Response { fn get_uri(&self) -> &Uri { &self .extensions() .get::() .expect("uri to have been set") .0 } fn get_redirect_history(&self) -> Option<&[Uri]> { self.extensions() .get::() .map(|r| r.0.as_ref()) } } algesten-ureq-d58cf18/src/run.rs000066400000000000000000000566461505724604200167010ustar00rootroot00000000000000use std::sync::{Arc, OnceLock}; use std::{io, mem}; use http::uri::Scheme; use http::{header, HeaderValue, Request, Response, Uri}; use ureq_proto::client::state::{Await100, RecvBody, RecvResponse, Redirect, SendRequest}; use ureq_proto::client::state::{Prepare, SendBody as SendBodyState}; use ureq_proto::client::{Await100Result, RecvBodyResult}; use ureq_proto::client::{RecvResponseResult, SendRequestResult}; use ureq_proto::BodyMode; use crate::body::ResponseInfo; use crate::config::{Config, RequestLevelConfig, DEFAULT_USER_AGENT}; use crate::http; use crate::pool::Connection; use crate::request::ForceSendBody; use crate::response::{RedirectHistory, ResponseUri}; use crate::timings::{CallTimings, CurrentTime}; use crate::transport::time::{Duration, Instant}; use crate::transport::ConnectionDetails; use crate::util::{DebugRequest, DebugResponse, DebugUri, HeaderMapExt, UriExt}; use crate::{Agent, Body, Error, SendBody, Timeout}; type Call = ureq_proto::client::Call; /// Run a request. /// /// This is the "main loop" of entire ureq. pub(crate) fn run( agent: &Agent, mut request: Request<()>, mut body: SendBody, ) -> Result, Error> { let mut redirect_count = 0; // Configuration on the request level overrides the agent level. let (config, request_level) = request .extensions_mut() .remove::() .map(|rl| (Arc::new(rl.0), true)) .unwrap_or_else(|| (agent.config.clone(), false)); let force_send_body = request.extensions_mut().remove::().is_some(); let mut redirect_history: Option> = config.save_redirect_history().then_some(Vec::new()); let timeouts = config.timeouts(); let mut timings = CallTimings::new(timeouts, CurrentTime::default()); let mut call = Call::new(request)?; if force_send_body { call.force_send_body(); } call.allow_non_standard_methods(config.allow_non_standard_methods()); let (response, handler) = loop { let timeout = timings.next_timeout(Timeout::Global); let timed_out = match timeout.after { Duration::Exact(v) => v.is_zero(), Duration::NotHappening => false, }; if timed_out { return Err(Error::Timeout(Timeout::Global)); } match call_run( agent, &config, request_level, call, &mut body, redirect_count, &mut redirect_history, &mut timings, )? { // Follow redirect CallResult::Redirect(rcall, rtimings) => { redirect_count += 1; call = handle_redirect(rcall, &config)?; timings = rtimings.new_call(); } // Return response CallResult::Response(response, handler) => break (response, handler), } }; let (parts, _) = response.into_parts(); let recv_body_mode = handler .call .as_ref() .map(|f| f.body_mode()) .unwrap_or(BodyMode::NoBody); let info = ResponseInfo::new(&parts.headers, recv_body_mode); let body = Body::new(handler, info); let response = Response::from_parts(parts, body); let status = response.status(); let is_err = status.is_client_error() || status.is_server_error(); if config.http_status_as_error() && is_err { return Err(Error::StatusCode(status.as_u16())); } Ok(response) } #[allow(clippy::too_many_arguments)] fn call_run( agent: &Agent, config: &Config, request_level: bool, mut call: Call, body: &mut SendBody, redirect_count: u32, redirect_history: &mut Option>, timings: &mut CallTimings, ) -> Result { let uri = call.uri().clone(); debug!("{} {:?}", call.method(), &DebugUri(call.uri())); if config.https_only() && uri.scheme() != Some(&Scheme::HTTPS) { return Err(Error::RequireHttpsOnly(uri.to_string())); } add_headers(&mut call, agent, config, body, &uri)?; let mut connection = connect(agent, config, request_level, &uri, timings)?; let mut call = call.proceed(); if log_enabled!(log::Level::Debug) { let headers = call.headers_map()?; let r = DebugRequest { method: call.method(), uri: call.uri(), version: call.version(), headers, }; debug!("{:?}", r); } let call = match send_request(call, &mut connection, timings)? { SendRequestResult::Await100(call) => match await_100(call, &mut connection, timings)? { Await100Result::SendBody(call) => send_body(call, body, &mut connection, timings)?, Await100Result::RecvResponse(call) => call, }, SendRequestResult::SendBody(call) => send_body(call, body, &mut connection, timings)?, SendRequestResult::RecvResponse(call) => call, }; let is_following_redirects = redirect_count < config.max_redirects(); let (mut response, response_result) = recv_response( call, &mut connection, config, timings, is_following_redirects, )?; debug!("{:?}", DebugResponse(&response)); #[cfg(feature = "cookies")] { let mut jar = agent.cookie_jar_lock(); let iter = response .headers() .get_all(http::header::SET_COOKIE) .iter() .filter_map(|h| h.to_str().ok()) .filter_map(|s| crate::Cookie::parse(s, &uri).ok()); jar.store_response_cookies(iter, &uri); } if let Some(history) = redirect_history.as_mut() { history.push(uri.clone()); response .extensions_mut() .insert(RedirectHistory(history.clone())); } response.extensions_mut().insert(ResponseUri(uri)); let ret = match response_result { RecvResponseResult::RecvBody(call) => { let timings = mem::take(timings); let mut handler = BodyHandler { call: Some(call), connection: Some(connection), timings, ..Default::default() }; if response.status().is_redirection() { if is_following_redirects { let call = handler.consume_redirect_body()?; CallResult::Redirect(call, handler.timings) } else if config.max_redirects_do_error() { return Err(Error::TooManyRedirects); } else { CallResult::Response(response, handler) } } else { CallResult::Response(response, handler) } } RecvResponseResult::Redirect(call) => { cleanup(connection, call.must_close_connection(), timings.now()); if is_following_redirects { CallResult::Redirect(call, mem::take(timings)) } else if config.max_redirects_do_error() { return Err(Error::TooManyRedirects); } else { CallResult::Response(response, BodyHandler::default()) } } RecvResponseResult::Cleanup(call) => { cleanup(connection, call.must_close_connection(), timings.now()); CallResult::Response(response, BodyHandler::default()) } }; Ok(ret) } /// Return type of [`call_run`]. #[allow(clippy::large_enum_variant)] enum CallResult { /// Call resulted in a redirect. Redirect(Call, CallTimings), /// Call resulted in a response. Response(Response<()>, BodyHandler), } fn add_headers( call: &mut Call, agent: &Agent, config: &Config, body: &mut SendBody, uri: &Uri, ) -> Result<(), Error> { let headers = call.headers(); let send_body_mode = if headers.has_send_body_mode() { None } else { Some(body.body_mode()?) }; let has_header_accept_enc = headers.has_accept_encoding(); let has_header_ua = headers.has_user_agent(); let has_header_accept = headers.has_accept(); #[cfg(not(feature = "cookies"))] { let _ = agent; let _ = uri; } #[cfg(feature = "cookies")] { let value = agent.jar.get_request_cookies(uri); if !value.is_empty() { let value = HeaderValue::from_str(&value) .map_err(|_| Error::CookieValue("Cookie value is an invalid http-header"))?; call.header(header::COOKIE, value)?; } } { static ACCEPTS: OnceLock = OnceLock::new(); let accepts = ACCEPTS.get_or_init(|| { #[allow(unused_mut)] let mut value = String::with_capacity(10); #[cfg(feature = "gzip")] value.push_str("gzip"); #[cfg(all(feature = "gzip", feature = "brotli"))] value.push_str(", "); #[cfg(feature = "brotli")] value.push_str("br"); value }); if !has_header_accept_enc { if let Some(v) = config.accept_encoding().as_str(accepts) { // unwrap is ok because above ACCEPTS will produce a valid value, // or the value is user provided in which case it must be valid. let value = HeaderValue::from_str(v).unwrap(); call.header(header::ACCEPT_ENCODING, value)?; } } } if let Some(send_body_mode) = send_body_mode { match send_body_mode { BodyMode::LengthDelimited(v) => { let value = HeaderValue::from(v); call.header(header::CONTENT_LENGTH, value)?; } BodyMode::Chunked => { let value = HeaderValue::from_static("chunked"); call.header(header::TRANSFER_ENCODING, value)?; } _ => {} } } if !has_header_ua { // unwrap is ok because a user might override the agent, and if they // set bad values, it's not really ureq's problem. if let Some(v) = config.user_agent().as_str(DEFAULT_USER_AGENT) { let value = HeaderValue::from_str(v).unwrap(); call.header(header::USER_AGENT, value)?; } } if !has_header_accept { // unwrap is ok because a user might override accepts header, and if they // set bad values, it's not really ureq's problem. if let Some(v) = config.accept().as_str("*/*") { let value = HeaderValue::from_str(v).unwrap(); call.header(header::ACCEPT, value)?; } } Ok(()) } fn connect( agent: &Agent, config: &Config, request_level: bool, uri: &Uri, timings: &mut CallTimings, ) -> Result { // Before resolving the URI we need to ensure it is a full URI. We // cannot make requests with partial uri like "/path". uri.ensure_valid_url()?; let is_proxy = config.proxy().is_some(); // For most proxy configs, the proxy itself should resolve the host name we are connecting to. // However for SOCKS4, we must do it and pass the resolved IP to the proxy. let is_proxy_local_resolve = config.proxy().map(|p| p.resolve_target()).unwrap_or(false); let addrs = if !is_proxy || is_proxy_local_resolve { agent .resolver .resolve(uri, config, timings.next_timeout(Timeout::Resolve))? } else { agent.resolver.empty() }; timings.record_time(Timeout::Resolve); let details = ConnectionDetails { uri, addrs, resolver: &*agent.resolver, config, request_level, now: timings.now(), timeout: timings.next_timeout(Timeout::Connect), run_connector: agent.run_connector.clone(), }; let connection = agent.pool.connect(&details, config.max_idle_age().into())?; if details.needs_tls() && !connection.is_tls() { return Err(Error::TlsRequired); } timings.record_time(Timeout::Connect); Ok(connection) } fn send_request( mut call: Call, connection: &mut Connection, timings: &mut CallTimings, ) -> Result { loop { if call.can_proceed() { break; } let buffers = connection.buffers(); let amount = call.write(buffers.output())?; let timeout = timings.next_timeout(Timeout::SendRequest); connection.transmit_output(amount, timeout)?; } timings.record_time(Timeout::SendRequest); // The request might be misconfigured. let call = call.proceed()?; // We checked can_proceed() above, this unwrap is fine. Ok(call.unwrap()) } fn await_100( mut call: Call, connection: &mut Connection, timings: &mut CallTimings, ) -> Result { while call.can_keep_await_100() { let timeout = timings.next_timeout(Timeout::Await100); if timeout.after.is_zero() { // Stop waiting for 100-continue. break; } match connection.maybe_await_input(timeout) { Ok(_) => { let input = connection.buffers().input(); if input.is_empty() { return Err(Error::disconnected("await_100 empty input")); } let amount = call.try_read_100(input)?; if amount > 0 { connection.consume_input(amount); break; } } Err(Error::Timeout(_)) => { // If we get a timeout while waiting for input, that is not an error, // we progress to send the request body. break; } Err(e) => return Err(e), } } timings.record_time(Timeout::Await100); // A misconfigured request might surface here. let call = call.proceed()?; Ok(call) } fn send_body( mut call: Call, body: &mut SendBody, connection: &mut Connection, timings: &mut CallTimings, ) -> Result, Error> { loop { if call.can_proceed() { break; } let buffers = connection.buffers(); let (tmp, output) = buffers.tmp_and_output(); let input_len = tmp.len(); let input_fitting_in_output = call.calculate_max_input(output.len()); let max_input = input_len.min(input_fitting_in_output); let output_used = if !call.is_chunked() { // For non-chunked, The body can be written directly to the output. // This optimizes away a memcopy if we were to go via call.write(). let output_used = body.read(output)?; // Size checking is still in the call. call.consume_direct_write(output_used)?; output_used } else { let tmp = &mut tmp[..max_input]; let n = body.read(tmp)?; let (input_used, output_used) = call.write(&tmp[..n], output)?; // Since output is "a bit" larger than the input (compensate for chunk ovexrhead), // the entire input we read from the body should also be shipped to the output. assert!(input_used == n); output_used }; let timeout = timings.next_timeout(Timeout::SendBody); connection.transmit_output(output_used, timeout)?; } timings.record_time(Timeout::SendBody); Ok(call.proceed().unwrap()) } fn recv_response( mut call: Call, connection: &mut Connection, config: &Config, timings: &mut CallTimings, is_following_redirects: bool, ) -> Result<(Response<()>, RecvResponseResult), Error> { let response = loop { let timeout = timings.next_timeout(Timeout::RecvResponse); let made_progress = connection.maybe_await_input(timeout)?; let input = connection.buffers().input(); // If cookies are disabled, we can allow ourselves to follow // the redirect as soon as we discover the `Location` header. // There are plenty of broken servers out there that don't send // the finishing \r\n on redirects. With cookies off, we can // handle that situation. // // If cookies are enabled, we risk mising a `Set-Cookie` header // with such a strategy. let cookies_enabled = cfg!(feature = "cookies"); // If we are not following redirects, do not allow partial returned // 302 responses. let allow_partial_redirect = !cookies_enabled && is_following_redirects; let (amount, maybe_response) = call.try_response(input, allow_partial_redirect)?; let check_size = if maybe_response.is_some() { // We got a parsed response, ensure the size is within // configured parameters. amount } else { // We did not parse a response, if input is too large, // we stop trying to get more data. input.len() }; if check_size > config.max_response_header_size() { return Err(Error::LargeResponseHeader( input.len(), config.max_response_header_size(), )); } connection.consume_input(amount); if let Some(response) = maybe_response { assert!(call.can_proceed()); break response; } else if !made_progress { return Err(Error::disconnected("recv_respone made no progress")); } }; timings.record_time(Timeout::RecvResponse); Ok((response, call.proceed().unwrap())) } fn handle_redirect(mut call: Call, config: &Config) -> Result, Error> { let maybe_new_call = call.as_new_call(config.redirect_auth_headers())?; let status = call.status(); if let Some(call) = maybe_new_call { debug!( "Redirect ({}): {} {:?}", status, call.method(), DebugUri(call.uri()) ); Ok(call) } else { Err(Error::RedirectFailed) } } fn cleanup(connection: Connection, must_close: bool, now: Instant) { if must_close { connection.close(); } else { connection.reuse(now) } } #[derive(Default)] pub(crate) struct BodyHandler { call: Option>, connection: Option, timings: CallTimings, remote_closed: bool, redirect: Option>>, } impl BodyHandler { fn do_read(&mut self, buf: &mut [u8]) -> Result { let (Some(call), Some(connection), timings) = (&mut self.call, &mut self.connection, &mut self.timings) else { return Ok(0); }; loop { let body_fulfilled = match call.body_mode() { BodyMode::NoBody => unreachable!("must be a BodyMode for BodyHandler"), // These modes are fulfilled by either reaching the content-length or // receiving an end chunk delimiter. BodyMode::LengthDelimited(_) | BodyMode::Chunked => call.can_proceed(), // This mode can only end once remote closes BodyMode::CloseDelimited => false, }; if body_fulfilled { self.ended()?; return Ok(0); } let has_buffered_input = connection.buffers().can_use_input(); // First try to use input already buffered if has_buffered_input { let input = connection.buffers().input(); let (input_used, output_used) = call.read(input, buf)?; connection.consume_input(input_used); if output_used > 0 { return Ok(output_used); } if input_used > 0 { // Body might be fulfilled now. continue; } } if self.remote_closed { // we should not try to await_input again. self.ended()?; return Ok(0); } let timeout = timings.next_timeout(Timeout::RecvBody); let made_progress = match connection.maybe_await_input(timeout) { Ok(v) => v, Err(Error::Io(e)) => match e.kind() { io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionAborted | io::ErrorKind::ConnectionReset => { self.remote_closed = true; true } _ => return Err(Error::Io(e)), }, Err(e) => return Err(e), }; let input = connection.buffers().input(); let input_ended = input.is_empty(); let (input_used, output_used) = call.read(input, buf)?; connection.consume_input(input_used); if output_used > 0 { return Ok(output_used); } else if input_ended { self.ended()?; return Ok(0); } else if made_progress { // The maybe_await_input() made progress, but handled amount is 0. This // can for instance happen if we read some data, but not enough for // decoding any gzip. continue; } else { // This is an error case we don't want to see. return Err(Error::BodyStalled); } } } fn ended(&mut self) -> Result<(), Error> { self.timings.record_time(Timeout::RecvBody); let call = self.call.take().expect("ended() called with body"); // In some cases, when reading chunked, the server send 0\r\n to indicate // the end of the body, and then abruptly does a FIN. In these cases we have // received the entire body, but must clean up the connection. let is_ended_chunked = call.is_ended_chunked(); let mut force_close = false; if !call.can_proceed() { if is_ended_chunked { // This case means we got 0\r\n, but can_proceed() is false because // it only goes true on fully received chunked bodies. debug!("Server ended connection after sending chunked 0\\r\\n"); force_close = true; } else { return Err(Error::disconnected("ended call cannot proceed")); } } let must_close_connection = force_close || match call.proceed().unwrap() { RecvBodyResult::Redirect(call) => { let c = call.must_close_connection(); self.redirect = Some(Box::new(call)); c } RecvBodyResult::Cleanup(v) => v.must_close_connection(), }; let connection = self.connection.take().expect("ended() called with body"); cleanup(connection, must_close_connection, self.timings.now()); Ok(()) } fn consume_redirect_body(&mut self) -> Result, Error> { let mut buf = vec![0; 1024]; loop { let amount = self.do_read(&mut buf)?; if amount == 0 { break; } } // Unwrap is OK, because we are only consuming the redirect body if // such a body was signalled by the remote. let redirect = self.redirect.take().map(|b| *b); Ok(redirect.expect("remote to have signaled redirect")) } } impl io::Read for BodyHandler { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.do_read(buf).map_err(|e| e.into_io()) } } algesten-ureq-d58cf18/src/send_body.rs000066400000000000000000000254641505724604200200350ustar00rootroot00000000000000use std::fs::File; use std::io::{self, Read, Stdin}; use std::net::TcpStream; use crate::body::{Body, BodyReader}; use crate::util::private::Private; use crate::{http, Error}; /// Request body for sending data via POST, PUT and PATCH. /// /// Typically not interacted with directly since the trait [`AsSendBody`] is implemented /// for the majority of the types of data a user might want to send to a remote server. /// That means if you want to send things like `String`, `&str` or `[u8]`, they can be /// used directly. See documentation for [`AsSendBody`]. /// /// The exception is when using [`Read`] trait bodies, in which case we wrap the request /// body directly. See below [`SendBody::from_reader`]. /// pub struct SendBody<'a> { inner: BodyInner<'a>, size: Option>, ended: bool, } impl<'a> SendBody<'a> { /// Creates an empty body. pub fn none() -> SendBody<'static> { (None, BodyInner::None).into() } /// Creates a body from a shared [`Read`] impl. pub fn from_reader(reader: &'a mut dyn Read) -> SendBody<'a> { (None, BodyInner::Reader(reader)).into() } /// Creates a body from an owned [`Read`] impl. pub fn from_owned_reader(reader: impl Read + 'static) -> SendBody<'static> { (None, BodyInner::OwnedReader(Box::new(reader))).into() } /// Creates a body to send as JSON from any [`Serialize`](serde::ser::Serialize) value. #[cfg(feature = "json")] pub fn from_json( value: &impl serde::ser::Serialize, ) -> Result, crate::Error> { let json = serde_json::to_vec_pretty(value)?; let len = json.len() as u64; let body = (Some(len), BodyInner::ByteVec(io::Cursor::new(json))).into(); Ok(body) } pub(crate) fn read(&mut self, buf: &mut [u8]) -> io::Result { let n = match &mut self.inner { BodyInner::None => { return Ok(0); } BodyInner::ByteSlice(v) => { let max = v.len().min(buf.len()); buf[..max].copy_from_slice(&v[..max]); *v = &v[max..]; Ok(max) } #[cfg(feature = "json")] BodyInner::ByteVec(v) => v.read(buf), BodyInner::Reader(v) => v.read(buf), BodyInner::OwnedReader(v) => v.read(buf), BodyInner::Body(v) => v.read(buf), }?; if n == 0 { self.ended = true; } Ok(n) } pub(crate) fn body_mode(&mut self) -> Result { // Lazily surface a potential error now. let size = match self.size { None => None, Some(Ok(v)) => Some(v), Some(Err(_)) => { // unwraps here are ok because we matched exactly this return Err(self.size.take().unwrap().unwrap_err()); } }; match &self.inner { BodyInner::None => return Ok(BodyMode::NoBody), BodyInner::Body(v) => return Ok(v.body_mode()), // The others fall through BodyInner::ByteSlice(_) => {} #[cfg(feature = "json")] BodyInner::ByteVec(_) => {} BodyInner::Reader(_) => {} BodyInner::OwnedReader(_) => {} }; // Any other body mode could be LengthDelimited depending on whether // we have got a size set. let mode = if let Some(size) = size { BodyMode::LengthDelimited(size) } else { BodyMode::Chunked }; Ok(mode) } /// Turn this `SendBody` into a reader. /// /// This is useful in [`Middleware`][crate::middleware::Middleware] to make changes to the /// body before sending it. /// /// ``` /// use ureq::{SendBody, Body}; /// use ureq::middleware::MiddlewareNext; /// use ureq::http::{Request, Response, header::HeaderValue}; /// use std::io::Read; /// /// fn my_middleware(req: Request, next: MiddlewareNext) /// -> Result, ureq::Error> { /// /// // Take apart the request. /// let (parts, body) = req.into_parts(); /// /// // Take the first 100 bytes of the incoming send body. /// let mut reader = body.into_reader().take(100); /// /// // Create a new SendBody. /// let new_body = SendBody::from_reader(&mut reader); /// /// // Reconstitute the request. /// let req = Request::from_parts(parts, new_body); /// /// // set my bespoke header and continue the chain /// next.handle(req) /// } /// ``` pub fn into_reader(self) -> impl Sized + io::Read + 'a { ReadAdapter(self) } } struct ReadAdapter<'a>(SendBody<'a>); impl<'a> io::Read for ReadAdapter<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf) } } use http::Response; use ureq_proto::BodyMode; /// Trait for common types to send in POST, PUT or PATCH. /// /// Sending common data types such as `String`, `&str` or `&[u8]` require no further wrapping /// and can be sent either by [`RequestBuilder::send()`][crate::RequestBuilder::send] or using the /// `http` crate [`Request`][http::Request] directly (see example below). /// /// Implemented for: /// /// * `&str` /// * `&String` /// * `&Vec` /// * `&File` /// * `&TcpStream` /// * `&[u8]` /// * `Response` /// * `String` /// * `Vec` /// * `File` /// * `Stdin` /// * `TcpStream` /// * `UnixStream` (not on windows) /// * `&[u8; N]` /// * `()` /// /// # Example /// /// These two examples are equivalent. /// /// ``` /// let data: &[u8] = b"My special request body data"; /// /// let response = ureq::post("https://httpbin.org/post") /// .send(data)?; /// # Ok::<_, ureq::Error>(()) /// ``` /// /// Using `http` crate API /// /// ``` /// use ureq::http; /// /// let data: &[u8] = b"My special request body data"; /// /// let request = http::Request::post("https://httpbin.org/post") /// .body(data)?; /// /// let response = ureq::run(request)?; /// # Ok::<_, ureq::Error>(()) /// ``` pub trait AsSendBody: Private { #[doc(hidden)] fn as_body(&mut self) -> SendBody; } impl<'a> Private for SendBody<'a> {} impl<'a> AsSendBody for SendBody<'a> { fn as_body(&mut self) -> SendBody { SendBody { inner: match &mut self.inner { BodyInner::None => BodyInner::None, BodyInner::ByteSlice(v) => BodyInner::ByteSlice(v), #[cfg(feature = "json")] BodyInner::ByteVec(v) => BodyInner::ByteSlice(v.get_ref()), BodyInner::Reader(v) => BodyInner::Reader(v), BodyInner::Body(v) => BodyInner::Reader(v), BodyInner::OwnedReader(v) => BodyInner::Reader(v), }, size: self.size.take(), ended: self.ended, } } } pub(crate) enum BodyInner<'a> { None, ByteSlice(&'a [u8]), #[cfg(feature = "json")] ByteVec(io::Cursor>), Body(Box>), Reader(&'a mut dyn Read), OwnedReader(Box), } impl Private for &[u8] {} impl AsSendBody for &[u8] { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice(self); (Some(self.len() as u64), inner).into() } } impl Private for &str {} impl AsSendBody for &str { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for String {} impl AsSendBody for String { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for Vec {} impl AsSendBody for Vec { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for &String {} impl AsSendBody for &String { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for &Vec {} impl AsSendBody for &Vec { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for &File {} impl AsSendBody for &File { fn as_body(&mut self) -> SendBody { let size = lazy_file_size(self); SendBody { inner: BodyInner::Reader(self), size: Some(size), ended: false, } } } impl Private for File {} impl AsSendBody for File { fn as_body(&mut self) -> SendBody { let size = lazy_file_size(self); SendBody { inner: BodyInner::Reader(self), size: Some(size), ended: false, } } } fn lazy_file_size(file: &File) -> Result { match file.metadata() { Ok(v) => Ok(v.len()), Err(e) => Err(e.into()), } } impl Private for &TcpStream {} impl AsSendBody for &TcpStream { fn as_body(&mut self) -> SendBody { (None, BodyInner::Reader(self)).into() } } impl Private for TcpStream {} impl AsSendBody for TcpStream { fn as_body(&mut self) -> SendBody { (None, BodyInner::Reader(self)).into() } } impl Private for Stdin {} impl AsSendBody for Stdin { fn as_body(&mut self) -> SendBody { (None, BodyInner::Reader(self)).into() } } // MSRV 1.78 // impl_into_body!(&Stdin, Reader); #[cfg(target_family = "unix")] use std::os::unix::net::UnixStream; #[cfg(target_family = "unix")] impl Private for UnixStream {} #[cfg(target_family = "unix")] impl AsSendBody for UnixStream { fn as_body(&mut self) -> SendBody { (None, BodyInner::Reader(self)).into() } } impl<'a> From<(Option, BodyInner<'a>)> for SendBody<'a> { fn from((size, inner): (Option, BodyInner<'a>)) -> Self { SendBody { inner, size: size.map(Ok), ended: false, } } } impl Private for Body {} impl AsSendBody for Body { fn as_body(&mut self) -> SendBody { let size = self.content_length(); (size, BodyInner::Body(Box::new(self.as_reader()))).into() } } impl Private for Response {} impl AsSendBody for Response { fn as_body(&mut self) -> SendBody { let size = self.body().content_length(); (size, BodyInner::Body(Box::new(self.body_mut().as_reader()))).into() } } impl Private for &[u8; N] {} impl AsSendBody for &[u8; N] { fn as_body(&mut self) -> SendBody { let inner = BodyInner::ByteSlice((*self).as_ref()); (Some(self.len() as u64), inner).into() } } impl Private for () {} impl AsSendBody for () { fn as_body(&mut self) -> SendBody { (None, BodyInner::None).into() } } algesten-ureq-d58cf18/src/timings.rs000066400000000000000000000152531505724604200175340ustar00rootroot00000000000000use std::fmt; use std::iter::once; use std::sync::Arc; use crate::config::Timeouts; use crate::transport::time::{Duration, Instant}; /// The various timeouts. /// /// Each enum corresponds to a value in /// [`ConfigBuilder::timeout_xxx`][crate::config::ConfigBuilder::timeout_global]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Timeout { /// Timeout for entire operation. Global, /// Timeout for the current call (when redirected). PerCall, /// Timeout in the resolver. Resolve, /// Timeout while opening the connection. Connect, /// Timeout while sending the request headers. SendRequest, /// Internal value never seen outside ureq (since awaiting 100 is expected /// to timeout). #[doc(hidden)] Await100, /// Timeout when sending then request body. SendBody, /// Timeout while receiving the response headers. RecvResponse, /// Timeout while receiving the response body. RecvBody, } impl Timeout { /// Give the immediate preceeding Timeout fn preceeding(&self) -> impl Iterator { let prev: &[Timeout] = match self { Timeout::Resolve => &[Timeout::PerCall], Timeout::Connect => &[Timeout::Resolve], Timeout::SendRequest => &[Timeout::Connect], Timeout::Await100 => &[Timeout::SendRequest], Timeout::SendBody => &[Timeout::SendRequest, Timeout::Await100], Timeout::RecvResponse => &[Timeout::SendRequest, Timeout::SendBody], Timeout::RecvBody => &[Timeout::RecvResponse], _ => &[], }; prev.iter().copied() } /// All timeouts to check fn timeouts_to_check(&self) -> impl Iterator { // Always check Global and PerCall once(*self) .chain(self.preceeding()) .chain([Timeout::Global, Timeout::PerCall]) } /// Get the corresponding configured timeout fn configured_timeout(&self, timeouts: &Timeouts) -> Option { match self { Timeout::Global => timeouts.global, Timeout::PerCall => timeouts.per_call, Timeout::Resolve => timeouts.resolve, Timeout::Connect => timeouts.connect, Timeout::SendRequest => timeouts.send_request, Timeout::Await100 => timeouts.await_100, Timeout::SendBody => timeouts.send_body, Timeout::RecvResponse => timeouts.recv_response, Timeout::RecvBody => timeouts.recv_body, } .map(Into::into) } } #[derive(Default, Debug)] pub(crate) struct CallTimings { timeouts: Box, current_time: CurrentTime, times: Vec<(Timeout, Instant)>, } impl CallTimings { pub(crate) fn new(timeouts: Timeouts, current_time: CurrentTime) -> Self { let mut times = Vec::with_capacity(8); let now = current_time.now(); times.push((Timeout::Global, now)); times.push((Timeout::PerCall, now)); CallTimings { timeouts: Box::new(timeouts), current_time, times, } } pub(crate) fn new_call(mut self) -> CallTimings { self.times.truncate(1); // Global is in position 0. self.times.push((Timeout::PerCall, self.current_time.now())); CallTimings { timeouts: self.timeouts, current_time: self.current_time, times: self.times, } } pub(crate) fn now(&self) -> Instant { self.current_time.now() } pub(crate) fn record_time(&mut self, timeout: Timeout) { // Each time should only be recorded once assert!( self.time_of(timeout).is_none(), "{:?} recorded more than once", timeout ); // There need to be at least one preceeding time recorded // since it follows a graph/call tree. let any_preceeding = timeout .preceeding() .filter_map(|to_check| self.time_of(to_check)) .any(|_| true); assert!(any_preceeding, "{:?} has no preceeding", timeout); // Record the time self.times.push((timeout, self.current_time.now())); } fn time_of(&self, timeout: Timeout) -> Option { self.times.iter().find(|x| x.0 == timeout).map(|x| x.1) } pub(crate) fn next_timeout(&self, timeout: Timeout) -> NextTimeout { let now = self.now(); let (reason, at) = timeout .timeouts_to_check() .filter_map(|to_check| { let time = if to_check == timeout { now } else { self.time_of(to_check)? }; let timeout = to_check.configured_timeout(&self.timeouts)?; Some((to_check, time + timeout)) }) .min_by(|a, b| a.1.cmp(&b.1)) .unwrap_or((Timeout::Global, Instant::NotHappening)); let after = at.duration_since(now); NextTimeout { after, reason } } } #[derive(Clone)] pub(crate) struct CurrentTime(Arc Instant + Send + Sync + 'static>); impl CurrentTime { pub(crate) fn now(&self) -> Instant { self.0() } } /// A pair of [`Duration`] and [`Timeout`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct NextTimeout { /// Duration until next timeout. pub after: Duration, /// The name of the next timeout.s pub reason: Timeout, } impl NextTimeout { /// Returns the duration of the timeout if the timeout must happen, but avoid instant timeouts /// /// If the timeout must happen but is zero, returns 1 second pub fn not_zero(&self) -> Option { if self.after.is_not_happening() { None } else if self.after.is_zero() { Some(Duration::from_secs(1)) } else { Some(self.after) } } } impl fmt::Debug for CurrentTime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("CurrentTime").finish() } } impl Default for CurrentTime { fn default() -> Self { Self(Arc::new(Instant::now)) } } impl fmt::Display for Timeout { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let r = match self { Timeout::Global => "global", Timeout::PerCall => "per call", Timeout::Resolve => "resolve", Timeout::Connect => "connect", Timeout::SendRequest => "send request", Timeout::SendBody => "send body", Timeout::Await100 => "await 100", Timeout::RecvResponse => "receive response", Timeout::RecvBody => "receive body", }; write!(f, "{}", r) } } algesten-ureq-d58cf18/src/tls/000077500000000000000000000000001505724604200163105ustar00rootroot00000000000000algesten-ureq-d58cf18/src/tls/cert.rs000066400000000000000000000210351505724604200176140ustar00rootroot00000000000000use std::fmt; use std::hash::{Hash, Hasher}; use crate::Error; /// An X509 certificate for a server or a client. /// /// These are either used as trust roots, or client authentication. /// /// The internal representation is DER form. The provided helpers for PEM /// translates to DER. #[derive(Clone, Hash)] pub struct Certificate<'a> { der: CertDer<'a>, } #[derive(Clone)] enum CertDer<'a> { Borrowed(&'a [u8]), Owned(Vec), Rustls(rustls_pki_types::CertificateDer<'static>), } impl Hash for CertDer<'_> { fn hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); self.as_ref().hash(state) } } impl<'a> AsRef<[u8]> for CertDer<'a> { fn as_ref(&self) -> &[u8] { match self { CertDer::Borrowed(v) => v, CertDer::Owned(v) => v, CertDer::Rustls(v) => v, } } } impl<'a> Certificate<'a> { /// Read an X509 certificate in DER form. /// /// Does not immediately validate whether the data provided is a valid DER formatted /// X509. That validation is the responsibility of the TLS provider. pub fn from_der(der: &'a [u8]) -> Self { let der = CertDer::Borrowed(der); Certificate { der } } /// Read an X509 certificate in PEM form. /// /// This is a shorthand for [`parse_pem`] followed by picking the first certificate. /// Fails with an error if there is no certificate found in the PEM given. /// /// Translates to DER format internally. pub fn from_pem(pem: &'a [u8]) -> Result, Error> { let item = parse_pem(pem) .find(|p| matches!(p, Err(_) | Ok(PemItem::Certificate(_)))) // None means there were no matches in the PEM chain .ok_or(Error::Tls("No pem encoded cert found"))??; let PemItem::Certificate(cert) = item else { unreachable!("matches! above for Certificate"); }; Ok(cert) } /// This certificate in DER (the internal) format. pub fn der(&self) -> &[u8] { self.der.as_ref() } /// Clones (allocates) to produce a static copy. pub fn to_owned(&self) -> Certificate<'static> { Certificate { der: CertDer::Owned(self.der.as_ref().to_vec()), } } } /// A private key used in client certificate auth. /// /// The internal representation is DER form. The provided helpers for PEM /// translates to DER. /// /// Deliberately not `Clone` to avoid accidental copies in memory. #[derive(Hash)] pub struct PrivateKey<'a> { kind: KeyKind, der: PrivateKeyDer<'a>, } enum PrivateKeyDer<'a> { Borrowed(&'a [u8]), Owned(Vec), Rustls(rustls_pki_types::PrivateKeyDer<'static>), } impl Hash for PrivateKeyDer<'_> { fn hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); match self { PrivateKeyDer::Borrowed(v) => v.hash(state), PrivateKeyDer::Owned(v) => v.hash(state), PrivateKeyDer::Rustls(v) => v.secret_der().as_ref().hash(state), } } } impl<'a> AsRef<[u8]> for PrivateKey<'a> { fn as_ref(&self) -> &[u8] { match &self.der { PrivateKeyDer::Borrowed(v) => v, PrivateKeyDer::Owned(v) => v, PrivateKeyDer::Rustls(v) => v.secret_der(), } } } /// The kind of private key. /// /// * For **rustls** any kind is valid. /// * For **native-tls** the only valid option is [`Pkcs8`](KeyKind::Pkcs8). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum KeyKind { /// An RSA private key Pkcs1, /// A PKCS#8 private key. /// /// Not encrypted with a passphrase. Pkcs8, /// A Sec1 private key Sec1, } impl<'a> PrivateKey<'a> { /// Read private key in DER form. /// /// Does not immediately validate whether the data provided is a valid DER. /// That validation is the responsibility of the TLS provider. pub fn from_der(kind: KeyKind, der: &'a [u8]) -> Self { let der = PrivateKeyDer::Borrowed(der); PrivateKey { kind, der } } /// Read a private key in PEM form. /// /// This is a shorthand for [`parse_pem`] followed by picking the first found key. /// Fails with an error if there are no keys found in the PEM given. /// /// Translates to DER format internally. pub fn from_pem(pem: &'a [u8]) -> Result, Error> { let item = parse_pem(pem) .find(|p| matches!(p, Err(_) | Ok(PemItem::PrivateKey(_)))) // None means there were no matches in the PEM chain .ok_or(Error::Tls("No pem encoded private key found"))??; let PemItem::PrivateKey(key) = item else { unreachable!("matches! above for PrivateKey"); }; Ok(key) } /// The key kind pub fn kind(&self) -> KeyKind { self.kind } /// This private key in DER (the internal) format. pub fn der(&self) -> &[u8] { self.as_ref() } /// Clones (allocates) to produce a static copy. pub fn to_owned(&self) -> PrivateKey<'static> { PrivateKey { kind: self.kind, der: match &self.der { PrivateKeyDer::Borrowed(v) => PrivateKeyDer::Owned(v.to_vec()), PrivateKeyDer::Owned(v) => PrivateKeyDer::Owned(v.to_vec()), PrivateKeyDer::Rustls(v) => PrivateKeyDer::Rustls(v.clone_key()), }, } } } /// Parser of PEM data. /// /// The data may contain one or many PEM items. The iterator produces the recognized PEM /// items and skip others. pub fn parse_pem(pem: &[u8]) -> impl Iterator, Error>> + '_ { PemIter(pem) } /// Kinds of PEM data found by [`parse_pem`] #[non_exhaustive] pub enum PemItem<'a> { /// An X509 certificate Certificate(Certificate<'a>), /// A private key PrivateKey(PrivateKey<'a>), } struct PemIter<'a>(&'a [u8]); impl<'a> Iterator for PemIter<'a> { type Item = Result, Error>; fn next(&mut self) -> Option { loop { match rustls_pemfile::read_one_from_slice(self.0) { Ok(Some((cert, rest))) => { // Move slice along for next iterator next() self.0 = rest; match cert { rustls_pemfile::Item::X509Certificate(der) => { return Some(Ok(Certificate { der: CertDer::Rustls(der), } .into())); } rustls_pemfile::Item::Pkcs1Key(der) => { return Some(Ok(PrivateKey { kind: KeyKind::Pkcs1, der: PrivateKeyDer::Rustls(der.into()), } .into())); } rustls_pemfile::Item::Pkcs8Key(der) => { return Some(Ok(PrivateKey { kind: KeyKind::Pkcs8, der: PrivateKeyDer::Rustls(der.into()), } .into())); } rustls_pemfile::Item::Sec1Key(der) => { return Some(Ok(PrivateKey { kind: KeyKind::Sec1, der: PrivateKeyDer::Rustls(der.into()), } .into())); } // Skip unhandled item type (CSR etc) _ => continue, } } // It's over Ok(None) => return None, Err(e) => { return Some(Err(Error::Pem(e))); } } } } } impl<'a> From> for PemItem<'a> { fn from(value: Certificate<'a>) -> Self { PemItem::Certificate(value) } } impl<'a> From> for PemItem<'a> { fn from(value: PrivateKey<'a>) -> Self { PemItem::PrivateKey(value) } } impl<'a> fmt::Debug for Certificate<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Certificate").finish() } } impl<'a> fmt::Debug for PrivateKey<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PrivateKey") .field("kind", &self.kind) .finish() } } algesten-ureq-d58cf18/src/tls/mod.rs000066400000000000000000000267151505724604200174500ustar00rootroot00000000000000//! TLS for handling `https`. use std::collections::hash_map::DefaultHasher; use std::fmt; use std::hash::{Hash, Hasher}; use std::sync::Arc; mod cert; pub use cert::{parse_pem, Certificate, PemItem, PrivateKey}; #[cfg(feature = "_rustls")] pub(crate) mod rustls; #[cfg(feature = "native-tls")] pub(crate) mod native_tls; /// Setting for which TLS provider to use. /// /// Defaults to [`Rustls`][Self::Rustls] because this has the highest chance /// to compile and "just work" straight out of the box without installing additional /// development dependencies. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum TlsProvider { /// [Rustls](https://crates.io/crates/rustls) with the /// [process-wide default cryptographic backend](https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html#method.install_default), /// or [Ring](https://crates.io/crates/ring) if no process-wide default is set. /// /// Requires the feature flag **rustls**. /// /// This is the default. Rustls, /// [Native-TLS](https://crates.io/crates/native-tls) for cases where it's important to /// use the TLS libraries installed on the host running ureq. /// /// Requires the feature flag **native-tls** and that using an [`Agent`](crate::Agent) with /// this config option set in the [`TlsConfig`]. /// /// The setting is never picked up automatically. NativeTls, } impl TlsProvider { pub(crate) fn is_feature_enabled(&self) -> bool { match self { TlsProvider::Rustls => { cfg!(feature = "_rustls") } TlsProvider::NativeTls => { cfg!(feature = "native-tls") } } } pub(crate) fn feature_name(&self) -> &'static str { match self { TlsProvider::Rustls => "rustls", TlsProvider::NativeTls => "native-tls", } } } /// Configuration of TLS. /// /// This configuration is in common for both the different TLS mechanisms (available through /// feature flags **rustls** and **native-tls**). #[derive(Clone)] pub struct TlsConfig { provider: TlsProvider, client_cert: Option, root_certs: RootCerts, use_sni: bool, disable_verification: bool, #[cfg(feature = "_rustls")] rustls_crypto_provider: Option>, } impl TlsConfig { /// Builder to make a bespoke config. pub fn builder() -> TlsConfigBuilder { TlsConfigBuilder { config: TlsConfig::default(), } } pub(crate) fn hash_value(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.hash(&mut hasher); hasher.finish() } } impl TlsConfig { /// The provider to use. /// /// Defaults to [`TlsProvider::Rustls`]. pub fn provider(&self) -> TlsProvider { self.provider } /// Client certificate chain with corresponding private key. /// /// Defaults to `None`. pub fn client_cert(&self) -> Option<&ClientCert> { self.client_cert.as_ref() } /// The set of trusted root certificates to use to validate server certificates. /// /// Defaults to `WebPki`. pub fn root_certs(&self) -> &RootCerts { &self.root_certs } /// Whether to send SNI (Server Name Indication) to the remote server. /// /// This is used by the server to determine which domain/certificate we are connecting /// to for servers where multiple domains/sites are hosted on the same IP. /// /// Defaults to `true`. pub fn use_sni(&self) -> bool { self.use_sni } /// **WARNING** Disable all server certificate verification. /// /// This breaks encryption and leaks secrets. Must never be enabled for code where /// any level of security is required. pub fn disable_verification(&self) -> bool { self.disable_verification } /// Specific `CryptoProvider` to use for `rustls`. /// /// # UNSTABLE API /// /// **NOTE: This API is not guaranteed for semver.** /// /// `rustls` is not (yet) semver 1.x and ureq can't promise that this API is upheld. /// If `rustls` makes a breaking change regarding `CryptoProvider` their configuration, /// or incompatible data types between rustls versions, ureq will _NOT_ bump a major version. /// /// ureq will update to the latest `rustls` minor version using ureq minor versions. #[cfg(feature = "_rustls")] pub fn unversioned_rustls_crypto_provider( &self, ) -> &Option> { &self.rustls_crypto_provider } } /// Builder of [`TlsConfig`] pub struct TlsConfigBuilder { config: TlsConfig, } impl TlsConfigBuilder { /// The provider to use. /// /// Defaults to [`TlsProvider::Rustls`]. pub fn provider(mut self, v: TlsProvider) -> Self { self.config.provider = v; self } /// Client certificate chain with corresponding private key. /// /// Defaults to `None`. pub fn client_cert(mut self, v: Option) -> Self { self.config.client_cert = v; self } /// The set of trusted root certificates to use to validate server certificates. /// /// Defaults to `WebPki`. pub fn root_certs(mut self, v: RootCerts) -> Self { self.config.root_certs = v; self } /// Whether to send SNI (Server Name Indication) to the remote server. /// /// This is used by the server to determine which domain/certificate we are connecting /// to for servers where multiple domains/sites are hosted on the same IP. /// /// Defaults to `true`. pub fn use_sni(mut self, v: bool) -> Self { self.config.use_sni = v; self } /// **WARNING** Disable all server certificate verification. /// /// This breaks encryption and leaks secrets. Must never be enabled for code where /// any level of security is required. pub fn disable_verification(mut self, v: bool) -> Self { self.config.disable_verification = v; self } /// Specific `CryptoProvider` to use for `rustls`. /// /// # UNSTABLE API /// /// **NOTE: This API is not guaranteed for semver.** /// /// `rustls` is not (yet) semver 1.x and ureq can't promise that this API is upheld. /// If `rustls` makes a breaking change regarding `CryptoProvider` their configuration, /// or incompatible data types between rustls versions, ureq will _NOT_ bump a major version. /// /// ureq will update to the latest `rustls` minor version using ureq minor versions. /// /// # Feature flags /// /// This requires either feature **rustls** or **rustls-no-provider**, you probably /// want the latter when configuring an explicit crypto provider since /// **rustls** compiles with `ring`, while **rustls-no-provider** does not. /// /// # Example /// /// This example uses `aws-lc-rs` for the [`Agent`][crate::Agent]. The following /// depdendencies would compile ureq without `ring` and only aws-lc-rs. /// /// * `Cargo.toml` /// /// ```text /// ureq = { version = "3", default-features = false, features = ["rustls-no-provider"] } /// rustls = { version = "0.23", features = ["aws-lc-rs"] } /// ``` /// /// * Agent /// /// ``` /// use std::sync::Arc; /// use ureq::{Agent}; /// use ureq::tls::{TlsConfig, TlsProvider}; /// use rustls::crypto; /// /// let crypto = Arc::new(crypto::aws_lc_rs::default_provider()); /// /// let agent = Agent::config_builder() /// .tls_config( /// TlsConfig::builder() /// .provider(TlsProvider::Rustls) /// // requires rustls or rustls-no-provider feature /// .unversioned_rustls_crypto_provider(crypto) /// .build() /// ) /// .build() /// .new_agent(); /// ``` #[cfg(feature = "_rustls")] pub fn unversioned_rustls_crypto_provider( mut self, v: Arc<::rustls::crypto::CryptoProvider>, ) -> Self { self.config.rustls_crypto_provider = Some(v); self } /// Finalize the config pub fn build(self) -> TlsConfig { self.config } } /// A client certificate. #[derive(Debug, Clone, Hash)] pub struct ClientCert(Arc<(Vec>, PrivateKey<'static>)>); impl ClientCert { /// Creates a new client certificate from a chain and a private key. pub fn new_with_certs(chain: &[Certificate<'static>], key: PrivateKey<'static>) -> Self { Self(Arc::new((chain.to_vec(), key))) } /// Client certificate chain. pub fn certs(&self) -> &[Certificate<'static>] { &self.0 .0 } /// Client certificate private key. pub fn private_key(&self) -> &PrivateKey<'static> { &self.0 .1 } } /// Configuration setting for root certs. #[derive(Debug, Clone, Hash)] #[non_exhaustive] pub enum RootCerts { /// Use these specific certificates as root certs. Specific(Arc>>), /// Use the platform's verifier. /// /// * For **rustls**, this uses the `rustls-platform-verifier` crate. It requires /// the feature **platform-verifier**. /// * For **native-tls**, this uses the roots that native-tls loads by default. PlatformVerifier, /// Use Mozilla's root certificates instead of the platform. /// /// This is useful when you can't trust the system roots, such as in /// environments where TLS is intercepted and decrypted by a proxy (MITM attack). /// /// This is the default value. WebPki, } impl RootCerts { /// Use these specific root certificates pub fn new_with_certs(certs: &[Certificate<'static>]) -> Self { certs.iter().cloned().into() } } impl>> From for RootCerts { fn from(value: I) -> Self { RootCerts::Specific(Arc::new(value.into_iter().collect())) } } impl Default for TlsConfig { fn default() -> Self { let provider = TlsProvider::default(); Self { provider, client_cert: None, root_certs: RootCerts::WebPki, use_sni: true, disable_verification: false, #[cfg(feature = "_rustls")] rustls_crypto_provider: None, } } } impl Default for TlsProvider { fn default() -> Self { Self::Rustls } } impl fmt::Debug for TlsConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TlsConfig") .field("provider", &self.provider) .field("client_cert", &self.client_cert) .field("root_certs", &self.root_certs) .field("use_sni", &self.use_sni) .field("disable_verification", &self.disable_verification) .finish() } } impl Hash for TlsConfig { fn hash(&self, state: &mut H) { self.provider.hash(state); self.client_cert.hash(state); self.root_certs.hash(state); self.use_sni.hash(state); self.disable_verification.hash(state); #[cfg(feature = "_rustls")] if let Some(arc) = &self.rustls_crypto_provider { (Arc::as_ptr(arc) as usize).hash(state); } } } #[cfg(test)] mod test { use super::*; use assert_no_alloc::*; #[test] fn tls_config_clone_does_not_allocate() { let c = TlsConfig::default(); assert_no_alloc(|| c.clone()); } } algesten-ureq-d58cf18/src/tls/native_tls.rs000066400000000000000000000311001505724604200210210ustar00rootroot00000000000000use std::convert::TryFrom; use std::io::{Read, Write}; use std::sync::{Arc, Mutex, OnceLock}; use std::{fmt, io}; use crate::tls::{RootCerts, TlsProvider}; use crate::{transport::time::Duration, transport::*, Error}; use der::pem::LineEnding; use der::Document; use native_tls::{Certificate, HandshakeError, Identity, TlsConnector}; use native_tls::{TlsConnectorBuilder, TlsStream}; use super::TlsConfig; /// Wrapper for TLS using native-tls. /// /// Requires feature flag **native-tls**. #[derive(Default)] pub struct NativeTlsConnector { connector: OnceLock, } struct CachedNativeTlsConnector { config_hash: u64, native_tls_connector: Arc, } impl Connector for NativeTlsConnector { type Out = Either; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { let Some(transport) = chained else { panic!("NativeTlsConnector requires a chained transport"); }; // Only add TLS if we are connecting via HTTPS and the transport isn't TLS // already, otherwise use chained transport as is. if !details.needs_tls() || transport.is_tls() { trace!("Skip"); return Ok(Some(Either::A(transport))); } if details.config.tls_config().provider != TlsProvider::NativeTls { debug!("Skip because config is not set to Native TLS"); return Ok(Some(Either::A(transport))); } trace!("Try wrap TLS"); let connector = self.get_cached_native_tls_connector(details)?; let domain = details .uri .authority() .expect("uri authority for tls") .host() .to_string(); let adapter = ErrorCapture::wrap(TransportAdapter::new(transport.boxed())); let stream = LazyStream::Unstarted(Some((connector, domain, adapter))); let buffers = LazyBuffers::new( details.config.input_buffer_size(), details.config.output_buffer_size(), ); let transport = NativeTlsTransport { buffers, stream }; debug!("Wrapped TLS"); Ok(Some(Either::B(transport))) } } impl NativeTlsConnector { fn get_cached_native_tls_connector( &self, details: &ConnectionDetails, ) -> Result, Error> { let tls_config = details.config.tls_config(); let connector = if details.request_level { // If the TlsConfig is request level, it is not allowed to // initialize the self.config OnceLock, but it should // reuse the cached value if it is the same TlsConfig // by comparing the config_hash value. let is_cached = self .connector .get() .map(|c| c.config_hash == tls_config.hash_value()) .unwrap_or(false); if is_cached { // unwrap is ok because if is_cached is true we must have had a value. self.connector.get().unwrap().native_tls_connector.clone() } else { build_connector(tls_config)?.native_tls_connector } } else { // Initialize the connector on first run. let connector_ref = match self.connector.get() { Some(v) => v, None => { // This is unlikely to be racy, but if it is, doesn't matter much. let c = build_connector(tls_config)?; // Maybe someone else set it first. Weird, but ok. let _ = self.connector.set(c); self.connector.get().unwrap() } }; connector_ref.native_tls_connector.clone() // cheap clone due to Arc }; Ok(connector) } } fn build_connector(tls_config: &TlsConfig) -> Result { let mut builder = TlsConnector::builder(); if tls_config.disable_verification { debug!("Certificate verification disabled"); builder.danger_accept_invalid_certs(true); builder.danger_accept_invalid_hostnames(true); } else { match &tls_config.root_certs { RootCerts::Specific(certs) => { // Only use the specific roots. builder.disable_built_in_roots(true); add_valid_der(certs.iter().map(|c| c.der()), &mut builder); } RootCerts::PlatformVerifier => { // We only use the built-in roots. builder.disable_built_in_roots(false); } RootCerts::WebPki => { // Only use the specific roots. builder.disable_built_in_roots(true); let certs = webpki_root_certs::TLS_SERVER_ROOT_CERTS .iter() .map(|c| c.as_ref()); add_valid_der(certs, &mut builder); } } } if let Some(certs_and_key) = &tls_config.client_cert { let (certs, key) = &*certs_and_key.0; let certs_pem = certs .iter() .map(|c| pemify(c.der(), "CERTIFICATE")) .collect::>()?; let key_pem = pemify(key.der(), "PRIVATE KEY")?; debug!("Use client certficiate with key kind {:?}", key.kind()); let identity = Identity::from_pkcs8(certs_pem.as_bytes(), key_pem.as_bytes())?; builder.identity(identity); } builder.use_sni(tls_config.use_sni); if !tls_config.use_sni { debug!("Disable SNI"); } let conn = builder.build()?; let cached = CachedNativeTlsConnector { config_hash: tls_config.hash_value(), native_tls_connector: Arc::new(conn), }; Ok(cached) } fn add_valid_der<'a, C>(certs: C, builder: &mut TlsConnectorBuilder) where C: Iterator, { let mut added = 0; let mut ignored = 0; for der in certs { let c = match Certificate::from_der(der) { Ok(v) => v, Err(e) => { // Invalid/expired/broken root certs are expected // in a native root store. trace!("Ignore invalid root cert: {}", e); ignored += 1; continue; } }; builder.add_root_certificate(c); added += 1; } debug!("Added {} and ignored {} root certs", added, ignored); } fn pemify(der: &[u8], label: &'static str) -> Result { let doc = Document::try_from(der)?; let pem = doc.to_pem(label, LineEnding::LF)?; Ok(pem) } pub struct NativeTlsTransport { buffers: LazyBuffers, stream: LazyStream, } impl Transport for NativeTlsTransport { fn buffers(&mut self) -> &mut dyn Buffers { &mut self.buffers } fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { let stream = self.stream.handshaken(timeout)?; stream.get_mut().get_mut().set_timeout(timeout); let output = &self.buffers.output()[..amount]; let ret = stream.write_all(output); // Surface errors capture below NativeTls primarily. stream.get_mut().take_captured()?; // Then NativeTls errors ret?; Ok(()) } fn await_input(&mut self, timeout: NextTimeout) -> Result { let stream = self.stream.handshaken(timeout)?; stream.get_mut().get_mut().set_timeout(timeout); let input = self.buffers.input_append_buf(); let result = stream.read(input); let amount = match result { Ok(v) => { if v == 0 { // NativeTls normalizes some error conditions to Ok(0) stream.get_mut().take_captured()?; } v } Err(e) => { // First captured stream.get_mut().take_captured()?; // Then NativeTls return Err(e.into()); } }; self.buffers.input_appended(amount); Ok(amount > 0) } fn is_open(&mut self) -> bool { let timeout = NextTimeout { after: Duration::Exact(std::time::Duration::from_secs(1)), reason: crate::Timeout::Global, }; self.stream .handshaken(timeout) .map(|c| c.get_mut().get_mut().get_mut().is_open()) .unwrap_or(false) } fn is_tls(&self) -> bool { true } } /// Helper to delay the handshake until we are starting IO. /// This normalizes native-tls to behave like rustls. enum LazyStream { Unstarted(Option<(Arc, String, ErrorCapture)>), Started(TlsStream>), } impl LazyStream { fn handshaken( &mut self, timeout: NextTimeout, ) -> Result<&mut TlsStream>, Error> { match self { LazyStream::Unstarted(v) => { let (conn, domain, mut adapter) = v.take().unwrap(); // Respect timeout during TLS handshake adapter.get_mut().set_timeout(timeout); let capture = adapter.capture(); let result = conn.connect(&domain, adapter).map_err(|e| match e { HandshakeError::Failure(e) => e, HandshakeError::WouldBlock(_) => unreachable!(), }); let stream = match result { Ok(v) => v, Err(e) => { // The error might originate in a Error::Timeout in the underlying adapter. // If so, we receive that error in this mpsc::Receiver. That's a more specific // error than the NativeTls::Error type. let mut lock = capture.lock().unwrap(); if let Some(error) = lock.take() { return Err(error); } return Err(e.into()); } }; *self = LazyStream::Started(stream); // Next time we hit the other match arm self.handshaken(timeout) } LazyStream::Started(v) => Ok(v), } } } impl fmt::Debug for NativeTlsConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("NativeTlsConnector").finish() } } impl fmt::Debug for NativeTlsTransport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("NativeTlsTransport").finish() } } /// A wrapper that captures and preserves underlying transport errors. /// /// Native-tls may normalize or obscure specific errors from the underlying transport, /// such as timeout errors. This wrapper intercepts and stores ureq errors for later /// retrieval, allowing us to surface more specific error information (like timeouts) /// rather than generic TLS errors. /// /// When an error occurs during IO operations, it's captured in the mutex and a generic /// "fake error" is returned to native-tls. struct ErrorCapture { stream: S, capture: Arc>>, } impl ErrorCapture { fn wrap(stream: S) -> Self { ErrorCapture { stream, capture: Arc::new(Mutex::new(None)), } } fn capture(&self) -> Arc>> { self.capture.clone() } fn get_mut(&mut self) -> &mut S { &mut self.stream } fn take_captured(&self) -> Result<(), Error> { let mut lock = self.capture.lock().unwrap(); if let Some(error) = lock.take() { return Err(error); } Ok(()) } } impl Read for ErrorCapture { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream .read(buf) .map_err(|e| capture_error(e, &self.capture)) } } impl Write for ErrorCapture { fn write(&mut self, buf: &[u8]) -> io::Result { self.stream .write(buf) .map_err(|e| capture_error(e, &self.capture)) } fn flush(&mut self) -> io::Result<()> { self.stream .flush() .map_err(|e| capture_error(e, &self.capture)) } } fn capture_error(e: io::Error, capture: &Arc>>) -> io::Error { let error: Error = e.into(); let mut lock = capture.lock().unwrap(); *lock = Some(error); io::Error::new(io::ErrorKind::Other, "fake error towards native-tls") } algesten-ureq-d58cf18/src/tls/rustls.rs000066400000000000000000000263471505724604200202260ustar00rootroot00000000000000use std::convert::TryInto; use std::fmt; use std::io::{Read, Write}; use std::sync::{Arc, OnceLock}; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::crypto::CryptoProvider; use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned, ALL_VERSIONS}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer}; use rustls_pki_types::{PrivateSec1KeyDer, ServerName}; use crate::tls::cert::KeyKind; use crate::tls::{RootCerts, TlsProvider}; use crate::transport::{Buffers, ConnectionDetails, Connector, LazyBuffers}; use crate::transport::{Either, NextTimeout, Transport, TransportAdapter}; use crate::Error; use super::TlsConfig; /// Wrapper for TLS using rustls. /// /// Requires feature flag **rustls**. #[derive(Default)] pub struct RustlsConnector { config: OnceLock, } struct CachedRustlConfig { config_hash: u64, rustls_config: Arc, } impl Connector for RustlsConnector { type Out = Either; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { let Some(transport) = chained else { panic!("RustlConnector requires a chained transport"); }; // Only add TLS if we are connecting via HTTPS and the transport isn't TLS // already, otherwise use chained transport as is. if !details.needs_tls() || transport.is_tls() { trace!("Skip"); return Ok(Some(Either::A(transport))); } if details.config.tls_config().provider != TlsProvider::Rustls { debug!("Skip because config is not set to Rustls"); return Ok(Some(Either::A(transport))); } trace!("Try wrap in TLS"); let config = self.get_cached_config(details)?; let name_borrowed: ServerName<'_> = details .uri .authority() .expect("uri authority for tls") .host() .try_into() .map_err(|e| { debug!("rustls invalid dns name: {}", e); Error::Tls("Rustls invalid dns name error") })?; let name = name_borrowed.to_owned(); let conn = ClientConnection::new(config, name)?; let stream = StreamOwned { conn, sock: TransportAdapter::new(transport.boxed()), }; let buffers = LazyBuffers::new( details.config.input_buffer_size(), details.config.output_buffer_size(), ); let transport = RustlsTransport { buffers, stream }; debug!("Wrapped TLS"); Ok(Some(Either::B(transport))) } } impl RustlsConnector { fn get_cached_config(&self, details: &ConnectionDetails) -> Result, Error> { let tls_config = details.config.tls_config(); Ok(if details.request_level { // If the TlsConfig is request level, it is not allowed to // initialize the self.config OnceLock, but it should // reuse the cached value if it is the same TlsConfig // by comparing the config_hash value. let is_cached = self .config .get() .map(|c| c.config_hash == tls_config.hash_value()) .unwrap_or(false); if is_cached { // unwrap is ok because if is_cached is true we must have had a value. self.config.get().unwrap().rustls_config.clone() } else { build_config(tls_config)?.rustls_config } } else { // On agent level, we initialize the config on first run. This is // the value we want to cache. // // NB: This init is a racey. The problem is that build_config() must // return a Result, and OnceLock::get_or_try_init is not stabilized // https://github.com/rust-lang/rust/issues/109737 // In case we're slamming a newly created Agent with many simultaneous // TLS requests, this might create some unnecessary/discarded rustls configs. loop { if let Some(config_ref) = self.config.get() { break config_ref.rustls_config.clone(); } else { let config = build_config(tls_config)?; let _ = self.config.set(config); } } }) } } fn build_config(tls_config: &TlsConfig) -> Result { // 1. Prefer provider set by TlsConfig. // 2. Use process wide default set in rustls library. // 3. Pick ring, if it is enabled (the default behavior). // 4. Error (never pick up a default from feature flags alone). let provider = tls_config .rustls_crypto_provider .clone() .or(rustls::crypto::CryptoProvider::get_default().cloned()) .unwrap_or_else(ring_if_enabled); #[cfg(feature = "_ring")] fn ring_if_enabled() -> Arc { Arc::new(rustls::crypto::ring::default_provider()) } #[cfg(not(feature = "_ring"))] fn ring_if_enabled() -> Arc { panic!( "No CryptoProvider for Rustls. Either enable feature `rustls`, or set process default using CryptoProvider::set_default(), or configure TlsConfig::rustls_crypto_provider()" ); } let builder = ClientConfig::builder_with_provider(provider.clone()) .with_protocol_versions(ALL_VERSIONS) .expect("all TLS versions"); let builder = if tls_config.disable_verification { debug!("Certificate verification disabled"); builder .dangerous() .with_custom_certificate_verifier(Arc::new(DisabledVerifier)) } else { match &tls_config.root_certs { RootCerts::Specific(certs) => { let root_certs = certs.iter().map(|c| CertificateDer::from(c.der())); let mut root_store = RootCertStore::empty(); let (added, ignored) = root_store.add_parsable_certificates(root_certs); debug!("Added {} and ignored {} root certs", added, ignored); builder.with_root_certificates(root_store) } #[cfg(not(feature = "platform-verifier"))] RootCerts::PlatformVerifier => { panic!("Rustls + PlatformVerifier requires feature: platform-verifier"); } #[cfg(feature = "platform-verifier")] RootCerts::PlatformVerifier => builder // This actually not dangerous. The rustls_platform_verifier is safe. .dangerous() .with_custom_certificate_verifier(Arc::new( rustls_platform_verifier::Verifier::new(provider)?, )), RootCerts::WebPki => { let root_store = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), }; builder.with_root_certificates(root_store) } } }; let mut config = if let Some(certs_and_key) = &tls_config.client_cert { let cert_chain = certs_and_key .certs() .iter() .map(|c| CertificateDer::from(c.der()).into_owned()); let key = certs_and_key.private_key(); let key_der = match key.kind() { KeyKind::Pkcs1 => PrivateKeyDer::Pkcs1(PrivatePkcs1KeyDer::from(key.der())), KeyKind::Pkcs8 => PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.der())), KeyKind::Sec1 => PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(key.der())), } .clone_key(); debug!("Use client certficiate with key kind {:?}", key.kind()); builder .with_client_auth_cert(cert_chain.collect(), key_der) .expect("valid client auth certificate") } else { builder.with_no_client_auth() }; config.enable_sni = tls_config.use_sni; if !tls_config.use_sni { debug!("Disable SNI"); } Ok(CachedRustlConfig { config_hash: tls_config.hash_value(), rustls_config: Arc::new(config), }) } pub struct RustlsTransport { buffers: LazyBuffers, stream: StreamOwned, } impl Transport for RustlsTransport { fn buffers(&mut self) -> &mut dyn Buffers { &mut self.buffers } fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { self.stream.get_mut().set_timeout(timeout); let output = &self.buffers.output()[..amount]; self.stream.write_all(output)?; Ok(()) } fn await_input(&mut self, timeout: NextTimeout) -> Result { self.stream.get_mut().set_timeout(timeout); let input = self.buffers.input_append_buf(); let amount = self.stream.read(input)?; self.buffers.input_appended(amount); Ok(amount > 0) } fn is_open(&mut self) -> bool { self.stream.get_mut().get_mut().is_open() } fn is_tls(&self) -> bool { true } } #[derive(Debug)] struct DisabledVerifier; impl ServerCertVerifier for DisabledVerifier { fn verify_server_cert( &self, _end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &rustls_pki_types::ServerName<'_>, _ocsp_response: &[u8], _now: rustls_pki_types::UnixTime, ) -> Result { Ok(ServerCertVerified::assertion()) } fn verify_tls12_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn verify_tls13_signature( &self, _message: &[u8], _cert: &CertificateDer<'_>, _dss: &rustls::DigitallySignedStruct, ) -> Result { Ok(HandshakeSignatureValid::assertion()) } fn supported_verify_schemes(&self) -> Vec { vec![ rustls::SignatureScheme::RSA_PKCS1_SHA1, rustls::SignatureScheme::RSA_PKCS1_SHA256, rustls::SignatureScheme::RSA_PKCS1_SHA384, rustls::SignatureScheme::RSA_PKCS1_SHA512, rustls::SignatureScheme::ECDSA_NISTP256_SHA256, rustls::SignatureScheme::ECDSA_NISTP384_SHA384, rustls::SignatureScheme::ECDSA_NISTP521_SHA512, rustls::SignatureScheme::RSA_PSS_SHA256, rustls::SignatureScheme::RSA_PSS_SHA384, rustls::SignatureScheme::RSA_PSS_SHA512, rustls::SignatureScheme::ED25519, rustls::SignatureScheme::ED448, ] } } impl fmt::Debug for RustlsConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RustlsConnector").finish() } } impl fmt::Debug for RustlsTransport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RustlsTransport") .field("chained", &self.stream.sock.inner()) .finish() } } algesten-ureq-d58cf18/src/unversioned/000077500000000000000000000000001505724604200200475ustar00rootroot00000000000000algesten-ureq-d58cf18/src/unversioned/mod.rs000066400000000000000000000011111505724604200211660ustar00rootroot00000000000000//! API that does not (yet) follow semver. //! //! All public types under `unversioned` are available to use, but are not considered final //! API in the semver sense. Breaking changes to anything under the module `unversioned`, //! like `Transport` or `Resolver` will NOT be reflected in a major version bump of the //! `ureq` crate. We do however commit to only make such changes in *minor* version bumps, //! not patch. //! //! In time, we will move these types out of `unversioned` and solidify the API. There //! is no set timeline for this. pub mod resolver; pub mod transport; algesten-ureq-d58cf18/src/unversioned/resolver.rs000066400000000000000000000141701505724604200222610ustar00rootroot00000000000000//! Name resolvers. //! //! **NOTE resolver does not (yet) [follow semver][super].** //! //! _NOTE: Resolver is deep configuration of ureq and is not required for regular use._ //! //! Name resolving is pluggable. The resolver's duty is to take a URI and translate it //! to a socket address (IP + port). This is done as a separate step in regular ureq use. //! The hostname is looked up and provided to the [`Connector`](crate::transport::Connector). //! //! In some situations it might be desirable to not do this lookup, or to use another system //! than DNS for it. use std::fmt::{self, Debug}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs}; use std::sync::mpsc::{self, RecvTimeoutError}; use std::thread::{self}; use std::vec::IntoIter; use http::uri::{Authority, Scheme}; use http::Uri; use crate::config::Config; use crate::http; use crate::transport::NextTimeout; use crate::util::{SchemeExt, UriExt}; use crate::Error; /// Trait for name resolvers. pub trait Resolver: Debug + Send + Sync + 'static { /// Resolve the URI to a socket address. /// /// The implementation should resolve within the given _timeout_. /// /// The resolver must guarantee at least one returned address, or error with /// `Error::HostNotFound`. fn resolve( &self, uri: &Uri, config: &Config, timeout: NextTimeout, ) -> Result; /// Produce an empty array of addresses. fn empty(&self) -> ResolvedSocketAddrs { fn uninited_socketaddr() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) } ArrayVec::from_fn(|_| uninited_socketaddr()) } } /// Max number of socket addresses to keep from the resolver. const MAX_ADDRS: usize = 16; pub use ureq_proto::ArrayVec; /// Addresses as returned by the resolver. pub type ResolvedSocketAddrs = ArrayVec; /// Default resolver implementation. /// /// Uses std::net [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) to /// do the lookup. Can optionally spawn a thread to abort lookup if the relevant timeout is set. #[derive(Default)] pub struct DefaultResolver { _private: (), } impl DefaultResolver { /// Helper to combine scheme host and port to a single string. /// /// This knows about the default ports for http, https and socks proxies which /// can then be omitted from the `Authority`. pub fn host_and_port(scheme: &Scheme, authority: &Authority) -> Option { let port = authority.port_u16().or_else(|| scheme.default_port())?; Some(format!("{}:{}", authority.host(), port)) } } impl Resolver for DefaultResolver { fn resolve( &self, uri: &Uri, config: &Config, timeout: NextTimeout, ) -> Result { uri.ensure_valid_url()?; // unwrap is ok due to ensure_full_url() above. let scheme = uri.scheme().unwrap(); let authority = uri.authority().unwrap(); if cfg!(feature = "_test") { let mut v = ArrayVec::from_fn(|_| "0.0.0.0:1".parse().unwrap()); v.push(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::new(10, 0, 0, 1), authority .port_u16() .or_else(|| scheme.default_port()) // unwrap is ok because ensure_valid_url() above. .unwrap(), ))); return Ok(v); } // This will be on the form "myspecialhost.org:1234". The port is mandatory. // unwrap is ok because ensure_valid_url() above. let addr = DefaultResolver::host_and_port(scheme, authority).unwrap(); // Determine if we want to use the async behavior. let use_sync = timeout.after.is_not_happening(); let iter = if use_sync { trace!("Resolve: {}", addr); // When timeout is not set, we do not spawn any threads. addr.to_socket_addrs()? } else { trace!("Resolve with timeout ({:?}): {} ", timeout, addr); resolve_async(addr, timeout)? }; let ip_family = config.ip_family(); let wanted = ip_family.keep_wanted(iter); let mut result = self.empty(); for addr in wanted.take(MAX_ADDRS) { result.push(addr); } debug!("Resolved: {:?}", result); if result.is_empty() { Err(Error::HostNotFound) } else { Ok(result) } } } fn resolve_async(addr: String, timeout: NextTimeout) -> Result, Error> { // TODO(martin): On Linux we have getaddrinfo_a which is a libc async way of // doing host lookup. We should make a subcrate that uses a native async method // when possible, and otherwise fall back on this thread behavior. let (tx, rx) = mpsc::sync_channel(1); thread::spawn(move || tx.send(addr.to_socket_addrs()).ok()); match rx.recv_timeout(*timeout.after) { Ok(v) => Ok(v?), Err(c) => match c { // Timeout results in None RecvTimeoutError::Timeout => Err(Error::Timeout(timeout.reason)), // The sender going away is nonsensical. Did the thread just die? RecvTimeoutError::Disconnected => unreachable!("mpsc sender gone"), }, } } impl fmt::Debug for DefaultResolver { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DefaultResolver").finish() } } #[cfg(test)] mod test { use crate::transport::time::Duration; use super::*; #[test] fn unknown_scheme() { let uri: Uri = "foo://some:42/123".parse().unwrap(); let config = Config::default(); let err = DefaultResolver::default() .resolve( &uri, &config, NextTimeout { after: Duration::NotHappening, reason: crate::Timeout::Global, }, ) .unwrap_err(); assert!(matches!(err, Error::BadUri(_))); assert_eq!(err.to_string(), "bad uri: unknown scheme: foo"); } } algesten-ureq-d58cf18/src/unversioned/transport/000077500000000000000000000000001505724604200221035ustar00rootroot00000000000000algesten-ureq-d58cf18/src/unversioned/transport/buf.rs000066400000000000000000000122301505724604200232230ustar00rootroot00000000000000use crate::util::ConsumeBuf; /// Abstraction over input/output buffers. /// /// In ureq, the buffers are provided by the [`Transport`](crate::transport::Transport). pub trait Buffers { /// Mut handle to output buffers to write new data. Data is always /// written from `0..`. fn output(&mut self) -> &mut [u8]; /// Unconsumed bytes in the input buffer as read only. /// /// The input buffer is written to by using [`Buffers::input_append_buf`] followed by /// [`Buffers::input_appended`] to indiciate how many additional bytes were added to the /// input. /// /// This buffer should return the total unconsumed bytes. /// /// Example: if the internal buffer is `input: Vec`, and we have counters for /// `filled: usize` and `consumed: usize`. This returns `&input[consumed..filled]`. fn input(&self) -> &[u8]; /// Input buffer to write to. This can be called despite there being unconsumed bytes /// left in the buffer already. /// /// Example: if the internal buffer is `input: Vec`, and we have counters for /// `filled: usize` and `consumed: usize`. This returns `&mut input[filled..]`. fn input_append_buf(&mut self) -> &mut [u8]; /// Add a number of read bytes into [`Buffers::input_append_buf()`]. /// /// Example: if the internal buffer is `input: Vec`, and we have counters for /// `filled: usize` and `consumed: usize`, this increases `filled`. fn input_appended(&mut self, amount: usize); /// Consume a number of bytes from `&input`. /// /// Example: if the internal buffer is `input: Vec`, and we have counters for /// `filled: usize` and `consumed: usize`, this increases `consumed`. fn input_consume(&mut self, amount: usize); /// Helper to get a scratch buffer (`tmp`) and the output buffer. This is used when /// sending the request body in which case we use a `Read` trait to read from the /// [`SendBody`](crate::SendBody) into tmp and then write it to the output buffer. fn tmp_and_output(&mut self) -> (&mut [u8], &mut [u8]); /// Helper to determine if the `&input` already holds unconsumed data or we need to /// read more input from the transport. This indicates two things: /// /// 1. There is unconsumed data in the input buffer /// 2. The last call to consume was > 0. /// /// Step 2 is because the input buffer might contain half a response body, and we /// cannot parse it until we got the entire buffer. In this case the transport must /// read more data first. fn can_use_input(&self) -> bool; } /// Default buffer implementation. /// /// The buffers are lazy such that no allocations are made until needed. That means /// a [`Transport`](crate::transport::Transport) implementation can freely instantiate /// the `LazyBuffers`. #[derive(Debug)] pub struct LazyBuffers { input_size: usize, output_size: usize, input: ConsumeBuf, output: Vec, progress: bool, } impl LazyBuffers { /// Create a new buffer. /// /// The sizes provided are not allocated until we need to. pub fn new(input_size: usize, output_size: usize) -> Self { assert!(input_size > 0); assert!(output_size > 0); LazyBuffers { input_size, output_size, // Vectors don't allocate until they get a size. input: ConsumeBuf::new(0), output: vec![], progress: false, } } fn ensure_allocation(&mut self) { if self.output.len() < self.output_size { self.output.resize(self.output_size, 0); } if self.input.unconsumed().len() < self.input_size { self.input.resize(self.input_size); } } } impl Buffers for LazyBuffers { fn output(&mut self) -> &mut [u8] { self.ensure_allocation(); &mut self.output } fn input(&self) -> &[u8] { self.input.unconsumed() } fn input_append_buf(&mut self) -> &mut [u8] { self.ensure_allocation(); self.input.free_mut() } fn tmp_and_output(&mut self) -> (&mut [u8], &mut [u8]) { self.ensure_allocation(); const MIN_TMP_SIZE: usize = 10 * 1024; let tmp_available = self.input.free_mut().len(); if tmp_available < MIN_TMP_SIZE { // The tmp space is used for reading the request body from the // Body as a Read. There's an outside chance there isn't any space // left in the input buffer if we have done Await100 and the peer // started sending a ton of data before we asked for it. // It's a pathological situation that we don't need to make work well. let needed = MIN_TMP_SIZE - tmp_available; self.input.resize(self.input.unconsumed().len() + needed); } (self.input.free_mut(), &mut self.output) } fn input_appended(&mut self, amount: usize) { self.input.add_filled(amount); } fn input_consume(&mut self, amount: usize) { self.progress = amount > 0; self.input.consume(amount); } fn can_use_input(&self) -> bool { !self.input.unconsumed().is_empty() && self.progress } } algesten-ureq-d58cf18/src/unversioned/transport/chain.rs000066400000000000000000000073141505724604200235400ustar00rootroot00000000000000use std::fmt; use std::marker::PhantomData; use super::{Connector, Transport}; /// Two chained connectors called one after another. /// /// Created by calling [`Connector::chain`] on the first connector. pub struct ChainedConnector(First, Second, PhantomData); impl Connector for ChainedConnector where In: Transport, First: Connector, Second: Connector, { type Out = Second::Out; fn connect( &self, details: &super::ConnectionDetails, chained: Option, ) -> Result, crate::Error> { let f_out = self.0.connect(details, chained)?; self.1.connect(details, f_out) } } impl ChainedConnector { pub(crate) fn new(first: First, second: Second) -> Self { ChainedConnector(first, second, PhantomData) } } impl fmt::Debug for ChainedConnector where In: Transport, First: Connector, Second: Connector, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("ChainedConnector") .field(&self.0) .field(&self.1) .finish() } } impl Clone for ChainedConnector where In: Transport, First: Connector + Clone, Second: Connector + Clone, { fn clone(&self) -> Self { ChainedConnector(self.0.clone(), self.1.clone(), PhantomData) } } /// A selection between two transports. #[derive(Debug)] pub enum Either { /// The first transport. A(A), /// The second transport. B(B), } impl Transport for Either { fn buffers(&mut self) -> &mut dyn super::Buffers { match self { Either::A(a) => a.buffers(), Either::B(b) => b.buffers(), } } fn transmit_output( &mut self, amount: usize, timeout: super::NextTimeout, ) -> Result<(), crate::Error> { match self { Either::A(a) => a.transmit_output(amount, timeout), Either::B(b) => b.transmit_output(amount, timeout), } } fn await_input(&mut self, timeout: super::NextTimeout) -> Result { match self { Either::A(a) => a.await_input(timeout), Either::B(b) => b.await_input(timeout), } } fn is_open(&mut self) -> bool { match self { Either::A(a) => a.is_open(), Either::B(b) => b.is_open(), } } fn is_tls(&self) -> bool { match self { Either::A(a) => a.is_tls(), Either::B(b) => b.is_tls(), } } } // Connector is implemented for () to start a chain of connectors. // // The `Out` transport is supposedly `()`, but this is never instantiated. impl Connector<()> for () { type Out = (); fn connect( &self, _: &super::ConnectionDetails, _: Option<()>, ) -> Result, crate::Error> { Ok(None) } } // () is a valid Transport for type reasons. // // It should never be instantiated as an actual transport. impl Transport for () { fn buffers(&mut self) -> &mut dyn super::Buffers { panic!("Unit transport is not valid") } fn transmit_output(&mut self, _: usize, _: super::NextTimeout) -> Result<(), crate::Error> { panic!("Unit transport is not valid") } fn await_input(&mut self, _: super::NextTimeout) -> Result { panic!("Unit transport is not valid") } fn is_open(&mut self) -> bool { panic!("Unit transport is not valid") } } algesten-ureq-d58cf18/src/unversioned/transport/connect.rs000066400000000000000000000120271505724604200241040ustar00rootroot00000000000000use base64::prelude::BASE64_STANDARD; use base64::Engine; use std::borrow::Cow; use std::fmt; use std::io::Write; use ureq_proto::parser::try_parse_response; use http::StatusCode; use crate::config::DEFAULT_USER_AGENT; use crate::http; use crate::transport::{ConnectionDetails, Connector, Either, Transport, TransportAdapter}; use crate::util::{SchemeExt, UriExt}; use crate::Error; /// Connector for CONNECT proxy settings. /// /// This operates on the previous chained transport typically a TcpConnector optionally /// wrapped in TLS. #[derive(Default)] pub struct ConnectProxyConnector(()); impl Connector for ConnectProxyConnector { type Out = Either>; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { // If there is already a connection, do nothing. if let Some(transport) = chained { return Ok(Some(Either::A(transport))); } // If we're using a CONNECT proxy, we need to resolve that hostname. let maybe_connect_uri = details.config.connect_proxy_uri(); let Some(connect_uri) = maybe_connect_uri else { // Not using CONNECT return Ok(None); }; let target = details.uri; let target_addrs = &details.addrs; // TODO(martin): it's a bit weird to put the CONNECT proxy // resolver timeout as part of Timeout::Connect, but we don't // have anything more granular for now. let proxy_addrs = details .resolver .resolve(connect_uri, details.config, details.timeout)?; let proxy_config = details.config.clone_without_proxy(); // ConnectionDetails to establish a connection to the CONNECT // proxy itself. let proxy_details = ConnectionDetails { uri: connect_uri, addrs: proxy_addrs, config: &proxy_config, request_level: details.request_level, resolver: details.resolver, now: details.now, timeout: details.timeout, run_connector: details.run_connector.clone(), }; let transport = (details.run_connector)(&proxy_details)?; // unwrap is ok because connect_proxy_uri() above checks it. let proxy = details.config.proxy().unwrap(); let mut w = TransportAdapter::new(transport); target.ensure_valid_url()?; // unwraps are ok because ensure_valid_url() checks it. let mut target_host = Cow::Borrowed(target.host().unwrap()); let target_port = target .port_u16() .unwrap_or(target.scheme().unwrap().default_port().unwrap()); if proxy.resolve_target() { // In run() we do the resolution of the target (proxied) host, at this // point we should have at least one IP address. // // TODO(martin): On fail try more addresses let resolved = target_addrs.first().expect("at least one resolved address"); target_host = Cow::Owned(resolved.to_string()); } write!(w, "CONNECT {}:{} HTTP/1.1\r\n", target_host, target_port)?; write!(w, "Host: {}:{}\r\n", target_host, target_port)?; if let Some(v) = details.config.user_agent().as_str(DEFAULT_USER_AGENT) { write!(w, "User-Agent: {}\r\n", v)?; } write!(w, "Proxy-Connection: Keep-Alive\r\n")?; let use_creds = proxy.username().is_some() || proxy.password().is_some(); if use_creds { let user = proxy.username().unwrap_or_default(); let pass = proxy.password().unwrap_or_default(); let creds = BASE64_STANDARD.encode(format!("{}:{}", user, pass)); write!(w, "Proxy-Authorization: basic {}\r\n", creds)?; } write!(w, "\r\n")?; w.flush()?; let mut transport = w.into_inner(); let response = loop { let made_progress = transport.maybe_await_input(details.timeout)?; let buffers = transport.buffers(); let input = buffers.input(); let Some((used_input, response)) = try_parse_response::<20>(input)? else { if !made_progress { let reason = "proxy server did not respond".to_string(); return Err(Error::ConnectProxyFailed(reason)); } continue; }; buffers.input_consume(used_input); break response; }; match response.status() { StatusCode::OK => { trace!("CONNECT proxy connected"); } x => { let reason = format!("proxy server responded {}/{}", x.as_u16(), x.as_str()); return Err(Error::ConnectProxyFailed(reason)); } } Ok(Some(Either::B(transport))) } } impl fmt::Debug for ConnectProxyConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ProxyConnector").finish() } } algesten-ureq-d58cf18/src/unversioned/transport/io.rs000066400000000000000000000046321505724604200230650ustar00rootroot00000000000000use std::io; use crate::Timeout; use super::time::Duration; use super::{NextTimeout, Transport}; /// Helper to turn a [`Transport`] into a std::io [`Read`](io::Read) and [`Write`](io::Write). /// /// This is useful when integrating with components that expect a regular `Read`/`Write`. In /// ureq this is used both for the [`RustlsConnector`](crate::unversioned::transport::RustlsConnector) and the /// [`NativeTlsConnector`](crate::unversioned::transport::NativeTlsConnector). pub struct TransportAdapter> { timeout: NextTimeout, transport: T, } impl TransportAdapter { /// Creates a new adapter pub fn new(transport: T) -> Self { Self { timeout: NextTimeout { after: Duration::NotHappening, reason: Timeout::Global, }, transport, } } /// Set a new value of the timeout. pub fn set_timeout(&mut self, timeout: NextTimeout) { self.timeout = timeout; } /// Reference to the adapted transport pub fn get_ref(&self) -> &dyn Transport { &self.transport } /// Mut reference to the adapted transport pub fn get_mut(&mut self) -> &mut dyn Transport { &mut self.transport } /// Reference to the inner transport. pub fn inner(&self) -> &dyn Transport { &self.transport } /// Turn the adapter back into the wrapped transport pub fn into_inner(self) -> T { self.transport } } impl io::Read for TransportAdapter { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.transport .maybe_await_input(self.timeout) .map_err(|e| e.into_io())?; let input = self.transport.buffers().input(); let max = buf.len().min(input.len()); buf[..max].copy_from_slice(&input[..max]); self.transport.buffers().input_consume(max); Ok(max) } } impl io::Write for TransportAdapter { fn write(&mut self, buf: &[u8]) -> io::Result { let output = self.transport.buffers().output(); let max = buf.len().min(output.len()); output[..max].copy_from_slice(&buf[..max]); self.transport .transmit_output(max, self.timeout) .map_err(|e| e.into_io())?; Ok(max) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } algesten-ureq-d58cf18/src/unversioned/transport/mod.rs000066400000000000000000000430121505724604200232300ustar00rootroot00000000000000//! HTTP/1.1 data transport. //! //! **NOTE: transport does not (yet) [follow semver][super].** //! //! _NOTE: Transport is deep configuration of ureq and is not required for regular use._ //! //! ureq provides a pluggable transport layer making it possible to write bespoke //! transports using the HTTP/1.1 protocol from point A to B. The //! [`Agent::with_parts()`](crate::Agent::with_parts) constructor takes an implementation //! of the [`Connector`] trait which is used for all connections made using that //! agent. //! //! The [DefaultConnector] covers the regular needs for HTTP/1.1: //! //! * TCP Sockets //! * SOCKS-proxy sockets //! * HTTPS/TLS using rustls (feature flag **rustls**) //! * HTTPS/TLS using native-tls (feature flag **native-tls** + [config](crate::tls::TlsProvider::NativeTls)) //! //! The [`Connector`] trait anticipates a chain of connectors that each decide //! whether to help perform the connection or not. It is for instance possible to make a //! connector handling other schemes than `http`/`https` without affecting "regular" connections. use std::fmt::Debug; use std::sync::Arc; use http::uri::Scheme; use http::Uri; use crate::config::Config; use crate::http; use crate::Error; use super::resolver::{ResolvedSocketAddrs, Resolver}; mod buf; pub use buf::{Buffers, LazyBuffers}; mod tcp; pub use self::tcp::TcpConnector; mod io; pub use io::TransportAdapter; mod chain; pub use chain::{ChainedConnector, Either}; mod connect; pub use connect::ConnectProxyConnector; #[cfg(feature = "_test")] mod test; #[cfg(feature = "_test")] pub use test::set_handler; #[cfg(feature = "socks-proxy")] mod socks; #[cfg(feature = "socks-proxy")] pub use self::socks::SocksConnector; #[cfg(feature = "_rustls")] pub use crate::tls::rustls::RustlsConnector; #[cfg(feature = "native-tls")] pub use crate::tls::native_tls::NativeTlsConnector; pub mod time; use self::time::Instant; pub use crate::timings::NextTimeout; /// Trait for components providing some aspect of connecting. /// /// A connector instance is reused to produce multiple [`Transport`] instances (where `Transport` /// instance would typically be a socket connection). /// /// A connector can be part of a chain of connectors. The [`DefaultConnector`] provides a chain that /// first tries to make a concrete socket connection (using [`TcpConnector`]) and then pass the /// resulting [`Transport`] to a TLS wrapping connector /// (see [`RustlsConnector`]). This makes it possible combine connectors /// in new ways. A user of ureq could implement bespoke connector (such as SCTP) and still use /// the `RustlsConnector` to wrap the underlying transport in TLS. /// /// The built-in [`DefaultConnector`] provides SOCKS, TCP sockets and TLS wrapping. /// /// # Errors /// /// When writing a bespoke connector chain we recommend handling errors like this: /// /// 1. Map to [`Error::Io`] as far as possible. /// 2. Map to any other [`Error`] where reasonable. /// 3. Fall back on [`Error::Other`] preserving the original error. /// 4. As a last resort [`Error::ConnectionFailed`] + logging. /// /// # Example /// /// ``` /// # #[cfg(all(feature = "rustls", not(feature = "_test")))] { /// use ureq::{Agent, config::Config}; /// /// // These types are not covered by the promises of semver (yet) /// use ureq::unversioned::transport::{Connector, TcpConnector, RustlsConnector}; /// use ureq::unversioned::resolver::DefaultResolver; /// /// // A connector chain that opens a TCP transport, then wraps it in a TLS. /// let connector = () /// .chain(TcpConnector::default()) /// .chain(RustlsConnector::default()); /// /// let config = Config::default(); /// let resolver = DefaultResolver::default(); /// /// // Creates an agent with a bespoke connector /// let agent = Agent::with_parts(config, connector, resolver); /// /// let mut res = agent.get("https://httpbin.org/get").call().unwrap(); /// let body = res.body_mut().read_to_string().unwrap(); /// # } /// ``` pub trait Connector: Debug + Send + Sync + 'static { /// The type of transport produced by this connector. type Out: Transport; /// Use this connector to make a [`Transport`]. /// /// * The [`ConnectionDetails`] parameter encapsulates config and the specific details of /// the connection being made currently (such as the [`Uri`]). /// * The `chained` parameter is used for connector chains and contains the [`Transport`] /// instantiated one of the previous connectors in the chain. All `Connector` instances /// can decide whether they want to pass this `Transport` along as is, wrap it in something /// like TLS or even ignore it to provide some other connection instead. /// /// Returns the [`Transport`] as produced by this connector, which could be just /// the incoming `chained` argument. fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error>; /// Chain this connector to another connector. /// /// This connector will be called first, and the output goes into the next connector. fn chain>(self, next: Next) -> ChainedConnector where Self: Sized, { ChainedConnector::new(self, next) } } /// Box a connector to erase the types. /// /// This is typically used after the chain of connectors is set up. pub(crate) fn boxed_connector(c: C) -> Box>> where In: Transport, C: Connector, { #[derive(Debug)] struct BoxingConnector; impl Connector for BoxingConnector { type Out = Box; fn connect( &self, _: &ConnectionDetails, chained: Option, ) -> Result, Error> { if let Some(transport) = chained { Ok(Some(Box::new(transport))) } else { Ok(None) } } } Box::new(c.chain(BoxingConnector)) } /// The parameters needed to create a [`Transport`]. pub struct ConnectionDetails<'a> { /// Full uri that is being requested. /// /// In the case of CONNECT (HTTP) proxy, this is the URI of the /// proxy, and the actual URI is in the `proxied` field. pub uri: &'a Uri, /// The resolved IP address + port for the uri being requested. See [`Resolver`]. /// /// For proxies, whetherh this holds real addresses depends on /// [`Proxy::resolve_target()`](crate::Proxy::resolve_target). pub addrs: ResolvedSocketAddrs, /// The configuration. /// /// Agent or Request level. pub config: &'a Config, /// Whether the config is request level. pub request_level: bool, /// The resolver configured on [`Agent`](crate::Agent). /// /// Typically the IP address of the host in the uri is already resolved to the `addr` /// property. However there might be cases where additional DNS lookups need to be /// made in the connector itself, such as resolving a SOCKS proxy server. pub resolver: &'a dyn Resolver, /// Current time. pub now: Instant, /// The next timeout for making the connection. // TODO(martin): Make mechanism to lower duration for each step in the connector chain. pub timeout: NextTimeout, /// Run the connector chain. /// /// Used for CONNECT proxy to establish a connection to the proxy server itself. pub run_connector: Arc, } pub(crate) type RunConnector = dyn Fn(&ConnectionDetails) -> Result, Error> + Send + Sync; impl<'a> ConnectionDetails<'a> { /// Tell if the requested socket need TLS wrapping. pub fn needs_tls(&self) -> bool { self.uri.scheme() == Some(&Scheme::HTTPS) } } /// Transport of HTTP/1.1 as created by a [`Connector`]. /// /// In ureq, [`Transport`] and [`Buffers`] go hand in hand. The rest of ureq tries to minimize /// the allocations, and the transport is responsible for providing the buffers required /// to perform the request. Unless the transport requires special buffer handling, the /// [`LazyBuffers`] implementation can be used. /// /// For sending data, the order of calls are: /// /// 1. [`Transport::buffers()`] to obtain the buffers. /// 2. [`Buffers::output()`] or [`Buffers::tmp_and_output`] /// depending where in the life cycle of the request ureq is. /// 3. [`Transport::transmit_output()`] to ask the transport to send/flush the `amount` of /// buffers used in 2. /// /// For receiving data, the order of calls are: /// /// 1. [`Transport::maybe_await_input()`] /// 2. The transport impl itself uses [`Buffers::input_append_buf()`] to fill a number /// of bytes from the underlying transport and use [`Buffers::input_appended()`] to /// tell the buffer how much been filled. /// 3. [`Transport::buffers()`] to obtain the buffers /// 4. [`Buffers::input()`] followed by [`Buffers::input_consume()`]. It's important to retain the /// unconsumed bytes for the next call to `maybe_await_input()`. This is handled by [`LazyBuffers`]. /// It's important to call [`Buffers::input_consume()`] also with 0 consumed bytes since that's /// how we keep track of whether the input is making progress. /// pub trait Transport: Debug + Send + Sync + 'static { /// Provide buffers for this transport. fn buffers(&mut self) -> &mut dyn Buffers; /// Transmit `amount` of the output buffer. ureq will always transmit the entirety /// of the data written to the output buffer. It is expected that the transport will /// transmit the entire requested `amount`. /// /// The timeout should be used to abort the transmission if the amount can't be written in time. /// If that happens the transport must return an [`Error::Timeout`] instance. fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error>; /// Await input from the transport. /// /// Early returns if [`Buffers::can_use_input()`], return true. #[doc(hidden)] fn maybe_await_input(&mut self, timeout: NextTimeout) -> Result { // If we already have input available, we don't wait. // This might be false even when there is input in the buffer // because the last use of the buffer made no progress. // Example: we might want to read the _entire_ http request headers, // not partially. if self.buffers().can_use_input() { return Ok(true); } self.await_input(timeout) } /// Wait for input and fill the buffer. /// /// 1. Use [`Buffers::input_append_buf()`] to fill the buffer /// 2. Followed by [`Buffers::input_appended()`] to report how many bytes were read. /// /// Returns `true` if it made progress, i.e. if it managed to fill the input buffer with any bytes. fn await_input(&mut self, timeout: NextTimeout) -> Result; /// Tell whether this transport is still functional. This must provide an accurate answer /// for connection pooling to work. fn is_open(&mut self) -> bool; /// Whether the transport is TLS. /// /// Defaults to `false`, override in TLS transports. fn is_tls(&self) -> bool { false } /// Turn this transport in a boxed version. // TODO(martin): is is complicating the public API? #[doc(hidden)] fn boxed(self) -> Box where Self: Sized + 'static, { Box::new(self) } } /// Default connector providing TCP sockets, TLS and SOCKS proxy. /// /// This connector is the following chain: /// /// 1. [`SocksConnector`] to handle proxy settings if set. /// 2. [`TcpConnector`] to open a socket directly if a proxy is not used. /// 3. [`RustlsConnector`] which wraps the /// connection from 1 or 2 in TLS if the scheme is `https` and the /// [`TlsConfig`](crate::tls::TlsConfig) indicate we are using **rustls**. /// This is the default TLS provider. /// 4. [`NativeTlsConnector`] which wraps /// the connection from 1 or 2 in TLS if the scheme is `https` and /// [`TlsConfig`](crate::tls::TlsConfig) indicate we are using **native-tls**. /// #[derive(Debug)] pub struct DefaultConnector { inner: Box>>, } impl DefaultConnector { /// Creates a default connector. pub fn new() -> Self { Self::default() } } impl Default for DefaultConnector { fn default() -> Self { let inner = (); // When enabled, all tests are connected to a dummy server and will not // make requests to the internet. #[cfg(feature = "_test")] let inner = inner.chain(test::TestConnector); // If we are using socks-proxy, that takes precedence over TcpConnector. #[cfg(feature = "socks-proxy")] let inner = inner.chain(SocksConnector::default()); // If the config indicates we ought to use a socks proxy // and the feature flag isn't enabled, we should warn the user. #[cfg(not(feature = "socks-proxy"))] let inner = inner.chain(no_proxy::WarnOnNoSocksConnector); // If this is a CONNECT proxy, we must "prepare" the socket // by setting up another connection and sending the `CONNECT host:port` line. let inner = inner.chain(ConnectProxyConnector::default()); // If we didn't get a socks-proxy, open a Tcp connection let inner = inner.chain(TcpConnector::default()); // If rustls is enabled, prefer that #[cfg(feature = "_rustls")] let inner = inner.chain(RustlsConnector::default()); // Panic if the config calls for rustls, the uri scheme is https and that // TLS provider is not enabled by feature flags. #[cfg(feature = "_tls")] let inner = inner.chain(no_tls::WarnOnMissingTlsProvider( crate::tls::TlsProvider::Rustls, )); // As a fallback if rustls isn't enabled, use native-tls #[cfg(feature = "native-tls")] let inner = inner.chain(NativeTlsConnector::default()); // Panic if the config calls for native-tls, the uri scheme is https and that // TLS provider is not enabled by feature flags. #[cfg(feature = "_tls")] let inner = inner.chain(no_tls::WarnOnMissingTlsProvider( crate::tls::TlsProvider::NativeTls, )); DefaultConnector { inner: boxed_connector(inner), } } } impl Connector<()> for DefaultConnector { type Out = Box; fn connect( &self, details: &ConnectionDetails, chained: Option<()>, ) -> Result, Error> { self.inner.connect(details, chained) } } #[cfg(not(feature = "socks-proxy"))] mod no_proxy { use super::{ConnectionDetails, Connector, Debug, Error, Transport}; #[derive(Debug)] pub(crate) struct WarnOnNoSocksConnector; impl Connector for WarnOnNoSocksConnector { type Out = In; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { if chained.is_none() { if let Some(proxy) = details.config.proxy() { if proxy.protocol().is_socks() { if proxy.is_from_env() { warn!( "Enable feature socks-proxy to use proxy configured by environment variables" ); } else { // If a user bothered to manually create a Config.proxy setting, // and it's not honored, assume it's a serious error. panic!( "Enable feature socks-proxy to use manually configured proxy" ); } } } } Ok(chained) } } } #[cfg(feature = "_tls")] mod no_tls { use crate::tls::TlsProvider; use super::{ConnectionDetails, Connector, Debug, Error, Transport}; #[derive(Debug)] pub(crate) struct WarnOnMissingTlsProvider(pub TlsProvider); impl Connector for WarnOnMissingTlsProvider { type Out = In; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { let already_tls = chained.as_ref().map(|c| c.is_tls()).unwrap_or(false); if already_tls { return Ok(chained); } let tls_config = details.config.tls_config(); if details.needs_tls() && tls_config.provider() == self.0 && !self.0.is_feature_enabled() { panic!( "uri scheme is https, provider is {:?} but feature is not enabled: {}", self.0, self.0.feature_name() ); } Ok(chained) } } } impl Transport for Box where T: ?Sized, { fn buffers(&mut self) -> &mut dyn Buffers { (**self).buffers() } fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { (**self).transmit_output(amount, timeout) } fn await_input(&mut self, timeout: NextTimeout) -> Result { (**self).await_input(timeout) } fn is_open(&mut self) -> bool { (**self).is_open() } fn is_tls(&self) -> bool { (**self).is_tls() } } algesten-ureq-d58cf18/src/unversioned/transport/socks.rs000066400000000000000000000135531505724604200236020ustar00rootroot00000000000000use std::fmt; use std::iter::once; use std::net::{SocketAddr, TcpStream}; use std::sync::mpsc::{self, RecvTimeoutError}; use std::{io, thread}; use socks::{Socks4Stream, Socks5Stream, ToTargetAddr}; use crate::proxy::{Proxy, ProxyProtocol}; use crate::util::UriExt; use crate::Error; use super::chain::Either; use super::ResolvedSocketAddrs; use super::tcp::TcpTransport; use super::{ConnectionDetails, Connector, LazyBuffers, NextTimeout, Transport}; /// Connector for SOCKS proxies. /// /// Requires the **socks-proxy** feature. /// /// The connector looks at the proxy settings in [`proxy`](crate::config::ConfigBuilder::proxy) to /// determine whether to attempt a proxy connection or not. #[derive(Default)] pub struct SocksConnector(()); impl Connector for SocksConnector { type Out = Either; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { let proxy = match details.config.proxy() { Some(v) if v.protocol().is_socks() => v, // If there is no proxy configured, or it isn't a SOCKS proxy, use whatever is chained. _ => { trace!("SOCKS not configured"); return Ok(chained.map(Either::A)); } }; if chained.is_some() { trace!("Skip"); return Ok(chained.map(Either::A)); } let proxy_addrs = details .resolver .resolve(proxy.uri(), details.config, details.timeout)?; let stream = if proxy.resolve_target() { // The target is already resolved by run(). let resolved = details.addrs.iter().cloned(); try_connect(&proxy_addrs, resolved, proxy, details.timeout)? } else { // Do not to resolve the target locally, instead pass (host, port) // to the proxy and let it resolve. let iter = once(details.uri.host_port()); try_connect(&proxy_addrs, iter, proxy, details.timeout)? }; if details.config.no_delay() { stream.set_nodelay(true)?; } let buffers = LazyBuffers::new( details.config.input_buffer_size(), details.config.output_buffer_size(), ); let transport = TcpTransport::new(stream, buffers); Ok(Some(Either::B(transport))) } } fn try_connect<'a, T: ToTargetAddr + fmt::Debug + Send + 'a + Clone>( proxy_addrs: &ResolvedSocketAddrs, target_addrs: impl Iterator, proxy: &Proxy, timeout: NextTimeout, ) -> Result { for target_addr in target_addrs { for proxy_addr in proxy_addrs { trace!( "Try connect {} {} -> {:?}", proxy.protocol(), proxy_addr, target_addr ); match try_connect_single(*proxy_addr, target_addr.clone(), proxy, timeout) { Ok(v) => { debug!( "{} connected {} -> {:?}", proxy.protocol(), proxy_addr, target_addr ); return Ok(v); } // Intercept ConnectionRefused to try next addrs Err(Error::Io(e)) if e.kind() == io::ErrorKind::ConnectionRefused => { trace!( "{} -> {:?} proxy connection refused", proxy_addr, target_addr ); continue; } // Other errors bail Err(e) => return Err(e), } } } debug!("Proxy failed to to connect to any resolved address"); Err(Error::Io(io::Error::new( io::ErrorKind::ConnectionRefused, "Connection refused", ))) } fn try_connect_single<'a, T: ToTargetAddr + Send + 'a>( proxy_addr: SocketAddr, target_addr: T, proxy: &Proxy, timeout: NextTimeout, ) -> Result { // The async behavior is only used if we want to time cap connecting. let use_sync = timeout.after.is_not_happening(); if use_sync { connect_proxy(proxy, proxy_addr, target_addr) } else { let (tx, rx) = mpsc::sync_channel(1); let proxy = proxy.clone(); thread::scope(move |s| { s.spawn(move || tx.send(connect_proxy(&proxy, proxy_addr, target_addr))); match rx.recv_timeout(*timeout.after) { Ok(v) => v, Err(RecvTimeoutError::Timeout) => Err(Error::Timeout(timeout.reason)), Err(RecvTimeoutError::Disconnected) => unreachable!("mpsc sender gone"), } }) } } fn connect_proxy<'a, T: ToTargetAddr + 'a>( proxy: &Proxy, proxy_addr: SocketAddr, target_addr: T, ) -> Result { let stream = match proxy.protocol() { ProxyProtocol::Socks4 | ProxyProtocol::Socks4A => { if proxy.username().is_some() { debug!("SOCKS4 does not support username/password"); } Socks4Stream::connect(proxy_addr, target_addr, "")?.into_inner() } ProxyProtocol::Socks5 => { if let Some(username) = proxy.username() { // Connect with authentication. let password = proxy.password().unwrap_or(""); Socks5Stream::connect_with_password(proxy_addr, target_addr, username, password)? } else { Socks5Stream::connect(proxy_addr, target_addr)? } .into_inner() } _ => unreachable!(), // HTTP(s) proxies. }; Ok(stream) } impl fmt::Debug for SocksConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SocksConnector").finish() } } algesten-ureq-d58cf18/src/unversioned/transport/tcp.rs000066400000000000000000000142351505724604200232440ustar00rootroot00000000000000use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::{fmt, io, time}; use crate::config::Config; use crate::util::IoResultExt; use crate::Error; use super::chain::Either; use super::ResolvedSocketAddrs; use super::time::Duration; use super::{Buffers, ConnectionDetails, Connector, LazyBuffers, NextTimeout, Transport}; #[derive(Default)] /// Connector for regular TCP sockets. pub struct TcpConnector(()); impl Connector for TcpConnector { type Out = Either; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { if chained.is_some() { // The chained connection overrides whatever we were to open here. // In the DefaultConnector chain this would be a SOCKS proxy connection. trace!("Skip"); return Ok(chained.map(Either::A)); } let config = &details.config; let stream = try_connect(&details.addrs, details.timeout, config)?; let buffers = LazyBuffers::new(config.input_buffer_size(), config.output_buffer_size()); let transport = TcpTransport::new(stream, buffers); Ok(Some(Either::B(transport))) } } fn try_connect( addrs: &ResolvedSocketAddrs, timeout: NextTimeout, config: &Config, ) -> Result { for addr in addrs { match try_connect_single(*addr, timeout, config) { // First that connects Ok(v) => return Ok(v), // Intercept ConnectionRefused to try next addrs Err(Error::Io(e)) if e.kind() == io::ErrorKind::ConnectionRefused => { trace!("{} connection refused", addr); continue; } // Other errors bail Err(e) => return Err(e), } } debug!("Failed to connect to any resolved address"); Err(Error::Io(io::Error::new( io::ErrorKind::ConnectionRefused, "Connection refused", ))) } fn try_connect_single( addr: SocketAddr, timeout: NextTimeout, config: &Config, ) -> Result { trace!("Try connect TcpStream to {}", addr); let maybe_stream = if let Some(when) = timeout.not_zero() { TcpStream::connect_timeout(&addr, *when) } else { TcpStream::connect(addr) } .normalize_would_block(); let stream = match maybe_stream { Ok(v) => v, Err(e) if e.kind() == io::ErrorKind::TimedOut => { return Err(Error::Timeout(timeout.reason)) } Err(e) => return Err(e.into()), }; if config.no_delay() { stream.set_nodelay(true)?; } debug!("Connected TcpStream to {}", addr); Ok(stream) } pub struct TcpTransport { stream: TcpStream, buffers: LazyBuffers, timeout_write: Option, timeout_read: Option, } impl TcpTransport { pub fn new(stream: TcpStream, buffers: LazyBuffers) -> TcpTransport { TcpTransport { stream, buffers, timeout_read: None, timeout_write: None, } } } // The goal here is to only cause a syscall to set the timeout if it's necessary. fn maybe_update_timeout( timeout: NextTimeout, previous: &mut Option, stream: &TcpStream, f: impl Fn(&TcpStream, Option) -> io::Result<()>, ) -> io::Result<()> { let maybe_timeout = timeout.not_zero(); if maybe_timeout != *previous { (f)(stream, maybe_timeout.map(|t| *t))?; *previous = maybe_timeout; } Ok(()) } impl Transport for TcpTransport { fn buffers(&mut self) -> &mut dyn Buffers { &mut self.buffers } fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { maybe_update_timeout( timeout, &mut self.timeout_write, &self.stream, TcpStream::set_write_timeout, )?; let output = &self.buffers.output()[..amount]; match self.stream.write_all(output).normalize_would_block() { Ok(v) => Ok(v), Err(e) if e.kind() == io::ErrorKind::TimedOut => Err(Error::Timeout(timeout.reason)), Err(e) => Err(e.into()), }?; Ok(()) } fn await_input(&mut self, timeout: NextTimeout) -> Result { // Proceed to fill the buffers from the TcpStream maybe_update_timeout( timeout, &mut self.timeout_read, &self.stream, TcpStream::set_read_timeout, )?; let input = self.buffers.input_append_buf(); let amount = match self.stream.read(input).normalize_would_block() { Ok(v) => Ok(v), Err(e) if e.kind() == io::ErrorKind::TimedOut => Err(Error::Timeout(timeout.reason)), Err(e) => Err(e.into()), }?; self.buffers.input_appended(amount); Ok(amount > 0) } fn is_open(&mut self) -> bool { probe_tcp_stream(&mut self.stream).unwrap_or(false) } } fn probe_tcp_stream(stream: &mut TcpStream) -> Result { // Temporary do non-blocking IO stream.set_nonblocking(true)?; let mut buf = [0]; match stream.read(&mut buf) { Err(e) if e.kind() == io::ErrorKind::WouldBlock => { // This is the correct condition. There should be no waiting // bytes, and therefore reading would block } // Any bytes read means the server sent some garbage we didn't ask for Ok(_) => { debug!("Unexpected bytes from server. Closing connection"); return Ok(false); } // Errors such as closed connection Err(_) => return Ok(false), }; // Reset back to blocking stream.set_nonblocking(false)?; Ok(true) } impl fmt::Debug for TcpConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TcpConnector").finish() } } impl fmt::Debug for TcpTransport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TcpTransport") .field("addr", &self.stream.peer_addr().ok()) .finish() } } algesten-ureq-d58cf18/src/unversioned/transport/test.rs000066400000000000000000000466101505724604200234370ustar00rootroot00000000000000#![allow(clippy::type_complexity)] use std::cell::RefCell; use std::io::Write; use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; use std::sync::{Arc, Mutex}; use std::{fmt, io, thread}; use http::{Method, Request, Uri}; use ureq_proto::parser::try_parse_request; use crate::http; use crate::Error; use super::chain::Either; use super::time::Duration; use super::{Buffers, ConnectionDetails, Connector, LazyBuffers, NextTimeout, Transport}; #[derive(Default)] pub(crate) struct TestConnector; thread_local!(static HANDLERS: RefCell> = const { RefCell::new(Vec::new()) }); impl Connector for TestConnector { type Out = Either; fn connect( &self, details: &ConnectionDetails, chained: Option, ) -> Result, Error> { if chained.is_some() { // The chained connection overrides whatever we were to open here. trace!("Skip"); return Ok(chained.map(Either::A)); } let config = details.config; let uri = details.uri.clone(); debug!("Test uri: {}", uri); let buffers = LazyBuffers::new(config.input_buffer_size(), config.output_buffer_size()); let (tx1, rx1) = mpsc::sync_channel(10); let (tx2, rx2) = mpsc::sync_channel(10); let mut handlers = HANDLERS.with(|h| (*h).borrow().clone()); setup_default_handlers(&mut handlers); thread::spawn(|| test_run(uri, rx1, tx2, handlers)); let transport = TestTransport { buffers, tx: tx1, rx: SyncReceiver(Mutex::new(rx2)), connected_tx: true, connected_rx: true, }; Ok(Some(Either::B(transport))) } } impl TestHandler { fn new( pattern: &'static str, handler: impl Fn(Uri, Request<()>, &mut dyn Write) -> io::Result<()> + Send + Sync + 'static, ) -> Self { TestHandler { pattern, handler: Arc::new(handler), } } } /// Helper for **_test** feature tests. #[cfg(feature = "_test")] #[doc(hidden)] pub fn set_handler(pattern: &'static str, status: u16, headers: &[(&str, &str)], body: &[u8]) { // Convert headers to a big string let mut headers_s = String::new(); for (k, v) in headers { headers_s.push_str(&format!("{}: {}\r\n", k, v)); } // Convert body to an owned vec let body = body.to_vec(); let handler = TestHandler::new(pattern, move |_uri, _req, w| { write!( w, "HTTP/1.1 {} OK\r\n\ {}\ \r\n", status, headers_s )?; w.write_all(&body) }); HANDLERS.with(|h| (*h).borrow_mut().push(handler)); } #[derive(Clone)] struct TestHandler { pattern: &'static str, handler: Arc, &mut dyn Write) -> io::Result<()> + Sync + Send>, } fn test_run( uri: Uri, rx: Receiver>, tx: mpsc::SyncSender>, handlers: Vec, ) { let mut reader = SaneBufReader(Some(RxRead(rx)), vec![]); let mut writer = TxWrite(tx); let uri_s = uri.to_string(); let req = loop { let input = reader.fill_buf().expect("test fill_buf"); let maybe = try_parse_request::<100>(input).expect("test parse request"); if let Some((amount, req)) = maybe { reader.consume(amount); break req; } else { continue; } }; for handler in handlers { if uri_s.contains(handler.pattern) { (handler.handler)(uri, req, &mut writer).expect("test handler to not fail"); return; } } panic!("test server unhandled url: {}", uri); } fn setup_default_handlers(handlers: &mut Vec) { fn maybe_add(handler: TestHandler, handlers: &mut Vec) { let already_declared = handlers.iter().any(|h| h.pattern == handler.pattern); if !already_declared { handlers.push(handler); } } maybe_add( TestHandler::new("www.google.com", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: text/html;charset=ISO-8859-1\r\n\ set-cookie: AEC=AVYB7cpadYFS8ZgaioQ17NnxHl1QcSQ_2aH2WEIg1KGDXD5kjk2HhpGVhfk; \ expires=Mon, 14-Apr-2050 17:23:39 GMT; path=/; domain=.google.com; \ Secure; HttpOnly; SameSite=lax\r\n\ set-cookie: __Secure-ENID=23.SE=WaDe-mOBoV2nk-IwHr73boNt6dYcjzQh1X_k8zv2UmUXBL\ m80a3pzLJyx1N1NOqBxDDOR8OJyvuNYw5phFf0VnbqzVtcKPijo2FY8O_vymzyc7x2VwFhGlgU\ WXSWYinjWL7Zvz_EOcA4kfnEXweW5ZDzLrvaLuBIrz5CA_-454AMIXpDiZAVPChCawbkzMptAr\ lMTikkon2EQVXsicqq1XnrMEMPZR5Ld2JC6lpBM8A; expires=Sun, 16-Nov-2050 09:41:57 \ GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax\r\n\ \r\n\ ureq test server here" ) }), handlers, ); maybe_add( TestHandler::new("example.com", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: text/html;charset=UTF-8\r\n\ \r\n\ ureq test server here" ) }), handlers, ); maybe_add( TestHandler::new("/bytes/100", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/octet-stream\r\n\ Content-Length: 100\r\n\ \r\n" )?; write!(w, "{}", "1".repeat(100)) }), handlers, ); maybe_add( TestHandler::new("/bytes/200000000", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/octet-stream\r\n\ Content-Length: 100\r\n\ \r\n" )?; // We don't actually want 200MB of data in memory. write!(w, "{}", "1".repeat(100)) }), handlers, ); maybe_add( TestHandler::new("/get", |_uri, req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_GET.len() )?; if req.method() != Method::HEAD { w.write_all(HTTPBIN_GET.as_bytes())?; } Ok(()) }), handlers, ); maybe_add( TestHandler::new("/?query=foo", |_uri, req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_GET.len() )?; if req.method() != Method::HEAD { w.write_all(HTTPBIN_GET.as_bytes())?; } Ok(()) }), handlers, ); maybe_add( TestHandler::new("/head", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_GET.len() ) }), handlers, ); maybe_add( TestHandler::new("/put", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_PUT.len() )?; w.write_all(HTTPBIN_PUT.as_bytes()) }), handlers, ); maybe_add( TestHandler::new("/post", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_PUT.len() )?; w.write_all(HTTPBIN_PUT.as_bytes()) }), handlers, ); maybe_add( TestHandler::new("/delete", |_uri, _req, w| { write!(w, "HTTP/1.1 200 OK\r\n\r\ndeleted\n") }), handlers, ); maybe_add( TestHandler::new("/robots.txt", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: text/plain\r\n\ Content-Length: 30\r\n\ \r\n\ User-agent: *\n\ Disallow: /deny\n" ) }), handlers, ); maybe_add( TestHandler::new("/json", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_JSON.len() )?; w.write_all(HTTPBIN_JSON.as_bytes()) }), handlers, ); maybe_add( TestHandler::new("/redirect-to", |uri, _req, w| { let location = uri.query().unwrap(); assert!(location.starts_with("url=")); let location = &location[4..]; let location = percent_encoding::percent_decode_str(location) .decode_utf8() .unwrap(); write!( w, "HTTP/1.1 302 FOUND\r\n\ Location: {}\r\n\ Content-Length: 22\r\n\ Connection: close\r\n\ \r\n\ You've been redirected\ ", location ) }), handlers, ); maybe_add( TestHandler::new("/partial-redirect", |_uri, _req, w| { write!( w, "HTTP/1.1 302 OK\r\n\ Location: /get\r\n\ set-cookie: AEC=AVYB7cpadYFS8ZgaioQ17NnxHl1QcSQ_2aH2WEIg1KGDXD5kjk2HhpGVhfk; \ expires=Mon, 14-Apr-2050 17:23:39 GMT; path=/; domain=.google.com; \ Secure; HttpOnly; SameSite=lax\r\n\ " // deliberately omit final \r\n ) }), handlers, ); maybe_add( TestHandler::new("/cookie-test", |_uri, req, w| { let mut all: Vec<_> = req .headers() .get_all("cookie") .iter() .map(|c| c.to_str().unwrap()) .collect(); all.sort(); assert_eq!(all, ["a=1;b=2"]); write!( w, "HTTP/1.1 200 OK\r\n\ content-length: 2\r\n\ \r\n\ ok", ) }), handlers, ); maybe_add( TestHandler::new("/connect-proxy", |_uri, req, w| { assert_eq!(req.uri(), "httpbin.org:80"); write!( w, "HTTP/1.1 200 OK\r\n\ \r\n\ HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_GET.len() )?; w.write_all(HTTPBIN_GET.as_bytes())?; Ok(()) }), handlers, ); maybe_add( TestHandler::new("/fnord", |_uri, req, w| { assert_eq!(req.method().as_str(), "FNORD"); write!( w, "HTTP/1.1 200 OK\r\n\ Content-Type: application/json\r\n\ Content-Length: {}\r\n\ \r\n", HTTPBIN_GET.len() )?; if req.method() != Method::HEAD { w.write_all(HTTPBIN_GET.as_bytes())?; } Ok(()) }), handlers, ); maybe_add( TestHandler::new("/1chunk-abort", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 2\r\n\ OK\r\n\ 0\r\n", )?; Ok(()) }), handlers, ); maybe_add( TestHandler::new("/2chunk-abort", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 2\r\n\ OK\r\n\ 0\r\n\ \r", // missing \n )?; Ok(()) }), handlers, ); maybe_add( TestHandler::new("/3chunk-abort", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 2\r\n\ OK\r\n\ 0\r\n\ \r\n", )?; Ok(()) }), handlers, ); maybe_add( TestHandler::new("/4chunk-abort", |_uri, _req, w| { write!( w, "HTTP/1.1 200 OK\r\n\ Transfer-Encoding: chunked\r\n\ \r\n\ 2\r\n\ OK\r\n\ 0\r\n\ \r\n", )?; Ok(()) }), handlers, ); #[cfg(feature = "charset")] { let (cow, _, _) = encoding_rs::WINDOWS_1252.encode("HTTP/1.1 302 Déplacé Temporairement\r\n\r\n"); let bytes = cow.to_vec(); maybe_add( TestHandler::new("/non-ascii-reason", move |_uri, _req, w| { w.write_all(&bytes)?; Ok(()) }), handlers, ); } } const HTTPBIN_GET: &str = r#" { "args": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "ureq/yeah", "X-Amzn-Trace-Id": "Root=1-6692ea70-181d2b331d51fb157521fba0" }, "origin": "1.2.3.4", "url": "http://httpbin.org/get" }"#; const HTTPBIN_PUT: &str = r#" { "args": {}, "data": "foo", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Content-Length": "3", "Content-Type": "application/octet-stream", "Host": "httpbin.org", "User-Agent": "curl/8.6.0", "X-Amzn-Trace-Id": "Root=1-6692eb75-0335ed3376385cc01144a4b6" }, "json": null, "origin": "1.2.3.4", "url": "http://httpbin.org/put" }"#; const HTTPBIN_JSON: &str = r#" { "slideshow": { "author": "Yours Truly", "date": "date of publication", "slides": [ { "title": "Wake up to WonderWidgets!", "type": "all" }, { "items": [ "Why WonderWidgets are great", "Who buys WonderWidgets" ], "title": "Overview", "type": "all" } ], "title": "Sample Slide Show" } }"#; struct RxRead(Receiver>); impl io::Read for RxRead { fn read(&mut self, buf: &mut [u8]) -> io::Result { let v = match self.0.recv() { Ok(v) => v, Err(_) => return Ok(0), // remote side is gone }; assert!(buf.len() >= v.len(), "{} > {}", buf.len(), v.len()); let max = buf.len().min(v.len()); buf[..max].copy_from_slice(&v[..]); Ok(max) } } struct TxWrite(mpsc::SyncSender>); impl io::Write for TxWrite { fn write(&mut self, buf: &[u8]) -> io::Result { self.0 .send(buf.to_vec()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } struct SaneBufReader(Option, Vec); impl io::Read for SaneBufReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if !self.1.is_empty() { let max = buf.len().min(self.1.len()); buf[..max].copy_from_slice(&self.1[..max]); self.1.drain(..max); return Ok(max); } let Some(reader) = &mut self.0 else { return Ok(0); }; reader.read(buf) } } impl SaneBufReader { pub fn fill_buf(&mut self) -> io::Result<&[u8]> { let Some(reader) = &mut self.0 else { return Ok(&self.1); }; let l = self.1.len(); self.1.resize(l + 1024, 0); let buf = &mut self.1[l..]; let n = reader.read(buf)?; if n == 0 { self.0 = None; } self.1.truncate(l + n); Ok(&self.1) } pub fn consume(&mut self, n: usize) { self.1.drain(..n); } } pub(crate) struct TestTransport { buffers: LazyBuffers, tx: mpsc::SyncSender>, rx: SyncReceiver>, connected_tx: bool, connected_rx: bool, } impl Transport for TestTransport { fn buffers(&mut self) -> &mut dyn Buffers { &mut self.buffers } fn transmit_output(&mut self, amount: usize, _timeout: NextTimeout) -> Result<(), Error> { let output = &self.buffers.output()[..amount]; if self.tx.send(output.to_vec()).is_err() { self.connected_tx = false; } Ok(()) } fn await_input(&mut self, timeout: NextTimeout) -> Result { if !self.connected_rx { return Err(Error::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "test server is not connected", ))); } let input = self.buffers.input_append_buf(); let mut buf = match self.rx.recv_timeout(timeout.after) { Ok(v) => v, Err(RecvTimeoutError::Timeout) => return Err(Error::Timeout(timeout.reason)), Err(RecvTimeoutError::Disconnected) => { trace!("Test server disconnected"); self.connected_rx = false; return Err(Error::Io(io::Error::new( io::ErrorKind::UnexpectedEof, "test server disconnected", ))); } }; let maybe_hangup = buf .windows(HANGUP.len()) .enumerate() .find(|(_, w)| *w == HANGUP) .map(|(pos, _)| pos); if let Some(pos) = maybe_hangup { debug!("TEST: Found "); buf.drain(pos..); self.connected_rx = false; } assert!(input.len() >= buf.len()); let max = input.len().min(buf.len()); input[..max].copy_from_slice(&buf[..]); self.buffers.input_appended(max); Ok(max > 0) } fn is_open(&mut self) -> bool { self.connected_tx } fn is_tls(&self) -> bool { // Pretend this is tls to not get TLS wrappers true } } const HANGUP: &[u8] = b""; // Workaround for std::mpsc::Receiver not being Sync struct SyncReceiver(Mutex>); impl SyncReceiver { fn recv_timeout(&self, timeout: Duration) -> Result { let lock = self.0.lock().unwrap(); lock.recv_timeout(*timeout) } } impl fmt::Debug for TestConnector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TestConnector").finish() } } impl fmt::Debug for TestTransport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TestTransport").finish() } } algesten-ureq-d58cf18/src/unversioned/transport/time.rs000066400000000000000000000116651505724604200234200ustar00rootroot00000000000000//! Internal time wrappers use std::cmp::Ordering; use std::ops::{Add, Deref}; use std::time; /// Wrapper for [`std::time::Instant`] that provides additional time points in the past or future #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Instant { /// A time in the past that already happened. AlreadyHappened, /// An exact instant. Exact(time::Instant), /// A time in the future that will never happen. NotHappening, } /// Wrapper for [`std::time::Duration`] that provides a duration to a distant future #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Duration { /// An exact duration. Exact(time::Duration), /// A duration so long it will never happen. NotHappening, } impl Duration { const ZERO: Duration = Duration::Exact(time::Duration::ZERO); /// Creates a duration from seconds. pub fn from_secs(secs: u64) -> Duration { Duration::Exact(time::Duration::from_secs(secs)) } /// Tells if this duration will ever happen. pub fn is_not_happening(&self) -> bool { *self == Duration::NotHappening } } const NOT_HAPPENING: time::Duration = time::Duration::from_secs(u64::MAX); impl Deref for Duration { type Target = time::Duration; fn deref(&self) -> &Self::Target { match self { Duration::Exact(v) => v, Duration::NotHappening => &NOT_HAPPENING, } } } impl Instant { /// Current time. pub fn now() -> Self { Instant::Exact(time::Instant::now()) } pub(crate) fn duration_since(&self, earlier: Instant) -> Duration { match (self, earlier) { (Instant::AlreadyHappened, Instant::AlreadyHappened) => Duration::ZERO, (Instant::AlreadyHappened, Instant::Exact(_)) => Duration::ZERO, (Instant::AlreadyHappened, Instant::NotHappening) => Duration::ZERO, (Instant::Exact(_), Instant::NotHappening) => Duration::ZERO, (Instant::Exact(v1), Instant::Exact(v2)) => { Duration::Exact(v1.saturating_duration_since(v2)) } (Instant::Exact(_), Instant::AlreadyHappened) => Duration::NotHappening, (Instant::NotHappening, Instant::AlreadyHappened) => Duration::NotHappening, (Instant::NotHappening, Instant::Exact(_)) => Duration::NotHappening, (Instant::NotHappening, Instant::NotHappening) => Duration::NotHappening, } } } impl Add for Instant { type Output = Instant; fn add(self, rhs: Duration) -> Self::Output { match (self, rhs) { (Instant::AlreadyHappened, Duration::Exact(_)) => Instant::AlreadyHappened, (Instant::AlreadyHappened, Duration::NotHappening) => Instant::AlreadyHappened, (Instant::Exact(v1), Duration::Exact(v2)) => Instant::Exact(v1.add(v2)), (Instant::Exact(_), Duration::NotHappening) => Instant::NotHappening, (Instant::NotHappening, Duration::Exact(_)) => Instant::NotHappening, (Instant::NotHappening, Duration::NotHappening) => Instant::NotHappening, } } } impl PartialOrd for Instant { fn partial_cmp(&self, other: &Self) -> Option { Some(Self::cmp(self, other)) } } impl Ord for Instant { fn cmp(&self, other: &Self) -> Ordering { match (self, other) { (Instant::AlreadyHappened, Instant::AlreadyHappened) => Ordering::Equal, (Instant::AlreadyHappened, Instant::Exact(_)) => Ordering::Less, (Instant::AlreadyHappened, Instant::NotHappening) => Ordering::Less, (Instant::Exact(_), Instant::AlreadyHappened) => Ordering::Greater, (Instant::Exact(v1), Instant::Exact(v2)) => v1.cmp(v2), (Instant::Exact(_), Instant::NotHappening) => Ordering::Less, (Instant::NotHappening, Instant::AlreadyHappened) => Ordering::Greater, (Instant::NotHappening, Instant::Exact(_)) => Ordering::Greater, (Instant::NotHappening, Instant::NotHappening) => Ordering::Equal, } } } impl PartialOrd for Duration { fn partial_cmp(&self, other: &Self) -> Option { Some(Self::cmp(self, other)) } } impl Ord for Duration { fn cmp(&self, other: &Self) -> Ordering { match (self, other) { (Duration::Exact(v1), Duration::Exact(v2)) => v1.cmp(v2), (Duration::Exact(_), Duration::NotHappening) => Ordering::Less, (Duration::NotHappening, Duration::Exact(_)) => Ordering::Greater, (Duration::NotHappening, Duration::NotHappening) => Ordering::Equal, } } } impl From for Duration { fn from(value: std::time::Duration) -> Self { Self::Exact(value) } } #[cfg(test)] mod test { use super::*; #[test] fn time_ord() { assert!(Instant::AlreadyHappened < Instant::now()); assert!(Instant::now() < Instant::NotHappening); assert!(Instant::AlreadyHappened < Instant::NotHappening); } } algesten-ureq-d58cf18/src/util.rs000066400000000000000000000246111505724604200170350ustar00rootroot00000000000000use std::convert::TryFrom; use std::io::{self, ErrorKind}; use std::{fmt, iter}; use http::header::{ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING}; use http::header::{CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE}; use http::header::{DATE, HOST, LOCATION, SERVER, TRANSFER_ENCODING, USER_AGENT}; use http::uri::{Authority, Scheme}; use http::{HeaderMap, HeaderName, HeaderValue, Method, Response, Uri, Version}; use crate::http; use crate::proxy::ProxyProtocol; use crate::Error; pub(crate) mod private { pub trait Private {} } 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). impl AuthorityExt for 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..])) } } pub(crate) trait SchemeExt { fn default_port(&self) -> Option; } impl SchemeExt for Scheme { fn default_port(&self) -> Option { if *self == Scheme::HTTPS { Some(443) } else if *self == Scheme::HTTP { Some(80) } else if let Ok(proxy) = ProxyProtocol::try_from(self.as_str()) { Some(proxy.default_port()) } else { debug!("Unknown scheme: {}", self); None } } } /// Windows causes kind `TimedOut` while unix does `WouldBlock`. Since we are not /// using non-blocking streams, we normalize `WouldBlock` -> `TimedOut`. pub(crate) trait IoResultExt { fn normalize_would_block(self) -> Self; } impl IoResultExt for io::Result { fn normalize_would_block(self) -> Self { match self { Ok(v) => Ok(v), Err(e) if e.kind() == ErrorKind::WouldBlock => { Err(io::Error::new(ErrorKind::TimedOut, e)) } Err(e) => Err(e), } } } #[derive(Debug)] pub(crate) struct ConsumeBuf { buf: Vec, filled: usize, consumed: usize, } impl ConsumeBuf { pub fn new(size: usize) -> Self { ConsumeBuf { buf: vec![0; size], filled: 0, consumed: 0, } } pub fn resize(&mut self, size: usize) { if size > 100 * 1024 * 1024 { panic!("ConsumeBuf grown to unreasonable size (>100MB)"); } self.buf.resize(size, 0); } pub fn add_space(&mut self, size: usize) { if size == 0 { return; } let wanted = self.buf.len() + size; self.resize(wanted); } pub fn free_mut(&mut self) -> &mut [u8] { self.maybe_shift(); &mut self.buf[self.filled..] } pub fn add_filled(&mut self, amount: usize) { self.filled += amount; assert!(self.filled <= self.buf.len()); } pub fn unconsumed(&self) -> &[u8] { &self.buf[self.consumed..self.filled] } pub fn unconsumed_mut(&mut self) -> &mut [u8] { &mut self.buf[self.consumed..self.filled] } pub fn consume(&mut self, amount: usize) { self.consumed += amount; assert!(self.consumed <= self.filled); } fn maybe_shift(&mut self) { if self.consumed == 0 { return; } if self.consumed == self.filled { self.consumed = 0; self.filled = 0; } else if self.filled > self.buf.len() / 2 { self.buf.copy_within(self.consumed..self.filled, 0); self.filled -= self.consumed; self.consumed = 0; } } } /// Wrapper to only log non-sensitive data. pub(crate) struct DebugRequest<'a> { pub method: &'a Method, pub uri: &'a Uri, pub version: Version, pub headers: HeaderMap, } impl<'a> fmt::Debug for DebugRequest<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Request") .field("method", &self.method) .field("uri", &DebugUri(self.uri)) .field("version", &self.version) .field("headers", &DebugHeaders(&self.headers)) .finish() } } /// Wrapper to only log non-sensitive data. pub(crate) struct DebugResponse<'a, B>(pub &'a Response); impl<'a, B> fmt::Debug for DebugResponse<'a, B> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Response") .field("status", &self.0.status()) .field("version", &self.0.version()) .field("headers", &DebugHeaders(self.0.headers())) .finish() } } pub(crate) struct DebugHeaders<'a>(pub &'a HeaderMap); const NON_SENSITIVE_HEADERS: &[HeaderName] = &[ DATE, CONTENT_TYPE, CONTENT_LENGTH, TRANSFER_ENCODING, CONNECTION, CONTENT_ENCODING, HOST, ACCEPT, ACCEPT_ENCODING, ACCEPT_CHARSET, SERVER, USER_AGENT, // LOCATION is also logged in a redacted form ]; impl<'a> fmt::Debug for DebugHeaders<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_map(); static REDACTED_LOCATION: HeaderValue = HeaderValue::from_static("******"); let has_location = self.0.has_location(); let filtered_headers = self .0 .iter() .filter(|(name, _)| NON_SENSITIVE_HEADERS.contains(name)); if has_location { let location_header = if log_enabled!(log::Level::Trace) { iter::once((&LOCATION, self.0.get(LOCATION).unwrap())) } else { iter::once((&LOCATION, &REDACTED_LOCATION)) }; debug.entries(filtered_headers.chain(location_header)); } else { debug.entries(filtered_headers); } let redact_count = self .0 .iter() .filter(|(name, _)| { // println!("{}", name); !NON_SENSITIVE_HEADERS.contains(name) }) .count() // location is logged, but redacted, so do not include in the count - if has_location { 1 } else { 0 }; if redact_count > 0 { debug.entry( &"", &format!("{} HEADERS ARE REDACTED", redact_count), ); } debug.finish() } } /// Wrapper to only log non-sensitive data. pub(crate) struct DebugUri<'a>(pub &'a Uri); impl<'a> fmt::Debug for DebugUri<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(s) = self.0.scheme_str() { write!(f, "{}://", s)?; } if let Some(a) = self.0.authority() { write!(f, "{:?}", DebugAuthority(a))?; } if let Some(q) = self.0.path_and_query() { if log_enabled!(log::Level::Trace) { write!(f, "{}", q)?; } else { write!(f, "/******")?; } } Ok(()) } } pub(crate) struct DebugAuthority<'a>(pub &'a Authority); impl<'a> fmt::Debug for DebugAuthority<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut at = false; if let Some(u) = self.0.username() { at = true; if let Some(x) = u.chars().next() { write!(f, "{}*****", x)?; } } if self.0.password().is_some() { at = true; write!(f, ":******")?; } if at { write!(f, "@")?; } write!(f, "{}", self.0.host())?; if let Some(p) = self.0.port_u16() { write!(f, ":{}", p)?; } Ok(()) } } pub(crate) trait UriExt { fn ensure_valid_url(&self) -> Result<(), Error>; #[cfg(feature = "_url")] fn try_into_url(&self) -> Result; #[allow(unused)] fn host_port(&self) -> (&str, u16); } impl UriExt for Uri { fn ensure_valid_url(&self) -> Result<(), Error> { let scheme = self .scheme() .ok_or_else(|| Error::BadUri(format!("{} is missing scheme", self)))?; scheme .default_port() .ok_or_else(|| Error::BadUri(format!("unknown scheme: {}", scheme)))?; self.authority() .ok_or_else(|| Error::BadUri(format!("{} is missing host", self)))?; Ok(()) } #[cfg(feature = "_url")] fn try_into_url(&self) -> Result { self.ensure_valid_url()?; let uri = self.to_string(); // If ensure_full_url() works, we expect to be able to parse it to a url let url = url::Url::parse(&uri).expect("parsed url"); Ok(url) } fn host_port(&self) -> (&str, u16) { ( self.host().unwrap(), self.port_u16() .unwrap_or(self.scheme().unwrap().default_port().unwrap_or(80)), ) } } pub(crate) trait HeaderMapExt { fn get_str(&self, k: HeaderName) -> Option<&str>; fn is_chunked(&self) -> bool; fn content_length(&self) -> Option; fn has_accept_encoding(&self) -> bool; fn has_user_agent(&self) -> bool; fn has_send_body_mode(&self) -> bool { self.is_chunked() || self.content_length().is_some() } fn has_accept(&self) -> bool; fn has_content_type(&self) -> bool; fn has_location(&self) -> bool; } impl HeaderMapExt for HeaderMap { fn get_str(&self, k: HeaderName) -> Option<&str> { self.get(k).and_then(|v| v.to_str().ok()) } fn is_chunked(&self) -> bool { self.get_str(TRANSFER_ENCODING) .map(|v| v.contains("chunked")) .unwrap_or(false) } fn content_length(&self) -> Option { let h = self.get_str(CONTENT_LENGTH)?; let len: u64 = h.parse().ok()?; Some(len) } fn has_accept_encoding(&self) -> bool { self.contains_key(ACCEPT_ENCODING) } fn has_user_agent(&self) -> bool { self.contains_key(USER_AGENT) } fn has_accept(&self) -> bool { self.contains_key(ACCEPT) } fn has_content_type(&self) -> bool { self.contains_key(CONTENT_TYPE) } fn has_location(&self) -> bool { self.contains_key(LOCATION) } }