pax_global_header00006660000000000000000000000064150206417510014513gustar00rootroot0000000000000052 comment=8a5ee02f074f950d6c5214267a99c483d7c20c1b oxigraph-oxhttp-8a5ee02/000077500000000000000000000000001502064175100152735ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/.github/000077500000000000000000000000001502064175100166335ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/.github/dependabot.yml000066400000000000000000000003111502064175100214560ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly oxigraph-oxhttp-8a5ee02/.github/workflows/000077500000000000000000000000001502064175100206705ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/.github/workflows/build.yml000066400000000000000000000114511502064175100225140ustar00rootroot00000000000000name: build on: pull_request: branches: - main push: branches: - main schedule: - cron: "12 3 * * 0" jobs: fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: rustup component add rustfmt - run: cargo fmt -- --check clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: rustup default 1.82.0 && rustup component add clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features native-tls -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features rustls-ring-native -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features rustls-ring-webpki -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features rustls-aws-lc-native -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features rustls-aws-lc-webpki -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features flate2 -- -D warnings -D clippy::all - run: cargo clippy --all-targets --all-features -- -D warnings -D clippy::all test: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo test - run: cargo test --features native-tls - run: cargo test --features rustls-ring-native - run: cargo test --features rustls-ring-webpki if: ${{ matrix.os != 'windows-latest' }} - run: cargo test --features rustls-aws-lc-native if: ${{ matrix.os != 'windows-latest' }} - run: cargo test --features rustls-aws-lc-webpki if: ${{ matrix.os != 'windows-latest' }} - run: cargo test --features flate2 - run: cargo test --all-features if: ${{ matrix.os != 'windows-latest' }} test_msv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - run: rustup toolchain install nightly && rustup default 1.74.0 - uses: Swatinem/rust-cache@v2 - run: cargo +nightly update -Z direct-minimal-versions env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback - run: cargo test - run: cargo test --features native-tls - run: cargo test --features rustls-ring-native - run: cargo test --features rustls-ring-webpki - run: cargo test --features rustls-aws-lc-native - run: cargo test --features rustls-aws-lc-webpki - run: cargo test --features flate2 - run: cargo test --all-features rustdoc: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: rustup override set 1.82.0 - uses: Swatinem/rust-cache@v2 - run: cargo doc --all-features --no-deps env: RUSTDOCFLAGS: -D warnings deny: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@v2 with: { tool: cargo-deny } - run: cargo deny check semver_checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 with: { tool: cargo-semver-checks } - run: cargo semver-checks check-release typos: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@v2 with: { tool: typos-cli } - run: typos codecov: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 with: { tool: cargo-llvm-cov } - run: | source <(cargo llvm-cov show-env --export-prefix) export CARGO_TARGET_DIR=$CARGO_LLVM_COV_TARGET_DIR cargo llvm-cov clean --workspace cargo test --features native-tls cargo test --features rustls-ring-native cargo test --features rustls-ring-webpki cargo test --features flate2 cargo llvm-cov report --codecov --output-path codecov.json - uses: codecov/codecov-action@v5 with: files: codecov.json flags: rust fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} codspeed: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 with: { tool: cargo-codspeed } - run: cargo codspeed build - uses: CodSpeedHQ/action@v3 with: run: cargo codspeed run token: ${{ secrets.CODSPEED_TOKEN }} oxigraph-oxhttp-8a5ee02/.gitignore000066400000000000000000000000231502064175100172560ustar00rootroot00000000000000/target Cargo.lock oxigraph-oxhttp-8a5ee02/CHANGELOG.md000066400000000000000000000125131502064175100171060ustar00rootroot00000000000000# Changelog ## [0.3.1] - 2025-06-06 ### Changed - Bump`rustls-platform-verifier` to 0.6. ## [0.3.0] - 2025-02-02 ### Changed - Uses the `http` crate structs to represent the HTTP model (`Request`, `Response`, `StatusCode`...) instead of the ones defined by `oxhttp`. Only the `Body` struct is implemented by `oxhttp`. - The `rustls-*-native` features now rely on the `rustls-platform-verifier` crate to support certificate revocation. The now redundant `rustls-*-platform-verifier` features have been removed. ## [0.2.7] - 2024-12-23 ### Changed - Increases read and write buffer sizes to 16kB. - Bump `rustls-platform-verifier` to v0.5. ## [0.2.6] - 2024-12-09 ### Changed - Bump MSRV to 1.74. - Set TCP_NODELAY in client and server `TcpStream`s. ## [0.2.5] - 2024-12-06 ### Changed - Makes chunked transfer encoding decoder properly return `Ok(0)` after end. ## [0.2.4] - 2024-11-06 ### Changed - Upgrades to `rustls-platform-verifier` 0.4. ## [0.2.3] - 2024-10-03 ### Changed - Allows setting `HeaderName` in `append_header` and `with_header`. ## [0.2.2] - 2024-09-15 ### Changed - Upgrades to `rustls-native-certs` 0.8. ## [0.2.1] - 2024-08-20 ### Added - `rustls-aws-lc-platform-verifier` and `rustls-ring-platform-verifier` features to use the rustls-platform-verifier crate to validate TLS certificates. ## [0.2.0] - 2024-03-23 No change compared to the alpha releases. ## [0.2.0-alpha.4] - 2024-02-02 ### Changed - Upgrades `rustls` to 0.23 and its dependencies to compatible versions. - Splits the `rustls-native` and `rustls-webkpi` features into `rustls-ring-native`, `rustls-ring-webpki`, `rustls-aws-lc-native` and `rustls-aws-lc-webpki` to allow choosing which cryptographic library to use. ## [0.2.0-alpha.3] - 2023-12-07 ### Changed - Upgrades `rustls` to 0.22 and its dependencies to compatible versions. - Relaxes dependency requirements on `flate2` ## [0.2.0-alpha.2] - 2023-11-18 ### Added - `Server.bind` to set a socket the server should listen to. - `Server.spawn` to spawn the server in a new set of threads and return a handle to it. ### Removed - `Server.listen` function that is now replaced by `Server.bind(address).spawn().join()`. ### Changed - Renames `Server.max_num_threads` to `Server.with_max_concurrent_connections` ## [0.2.0-alpha.1] - 2023-09-23 ### Added - When the `flate2` crate is installed, the HTTP client and server are able to decode bodies with `Content-Encoding` set to `gzip` and `deflate` (no encoding yet). - `client` and `server` features to enable the HTTP client and server. They are both enabled by default. - `Server::with_max_num_threads` allows to set an upper bound to the number of threads running at the same time. ### Removed - Rayon-based thread pool. ### Changed - The `rustls` feature has been split into `rustls-native` and `rustls-webpki` to either rust the platform certificates or the ones from the [Common CA Database](https://www.ccadb.org/). - All the `set_` methods on `Client` and `Server` have been renamed to `with_` and now takes and returns the mutated objects by value (builder pattern). - Upgrades minimum supported Rust version to 1.70. - Upgrades `webpki-roots` dependency to 0.25. ## [0.1.7] - 2023-08-23 ### Changed - Upgrades `rustls` dependency to 0.21. ## [0.1.6] - 2023-03-18 ### Added - `IntoHeaderName` trait that allows to call methods with plain strings instead of explicit `HeaderName` objects. - `client` and `server` features to enable/disable the HTTP client and/or server (both features are enabled by default). ### Changed - Bindings to server localhost now properly binds to both IPv4 and IPv6 at the same time. - Set minimum supported Rust version to 1.60. ## [0.1.5] - 2022-08-16 ### Changed - A body is now always written on POST and PUT request and on response that have not the status 1xx, 204 and 304. This allows clients to not wait for an existing body in case the connection is kept alive. - The TLS configuration is now initialized once and shared between clients and saved during the complete process lifetime. ## [0.1.4] - 2022-01-24 ### Added - `Server`: It is now possible to use a [Rayon](https://github.com/rayon-rs/rayon) thread pool instead of spawning a new thread on each call. ### Changed - [Chunk Transfer Encoding](https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#chunked.encoding) serialization was invalid: the last empty chunk was ending with two line jumps instead of one as expected by the specification. - `Server`: Thread spawn operation is restarted if it fails. - `Server`: `text/plain; charset=utf8` media type is now returned on errors instead of the simpler `text/plain`. ## [0.1.3] - 2021-12-05 ### Added - [Rustls](https://github.com/rustls/rustls) usage is now available behind the `rustls` feature (disabled by default). ## [0.1.2] - 2021-11-03 ### Added - Redirections support to the `Client`. By default the client does not follow redirects. The `Client::set_redirection_limit` method allows to set the maximum number of allowed consecutive redirects (0 by default). ### Changed - `Server`: Do not display a TCP error if the client disconnects without having sent the `Connection: close` header. ## [0.1.1] - 2021-09-30 ### Changed - Fixes a possible DOS attack vector by sending very long headers. ## [0.1.0] - 2021-09-29 ### Added - Basic `Client` and `Server` implementations. oxigraph-oxhttp-8a5ee02/Cargo.toml000066400000000000000000000030421502064175100172220ustar00rootroot00000000000000[package] name = "oxhttp" version = "0.3.1" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" documentation = "https://docs.rs/oxhttp" keywords = ["HTTP"] repository = "https://github.com/oxigraph/oxhttp" description = """ Simple implementation of HTTP 1.1 (both client and server) """ edition = "2021" rust-version = "1.74" [dependencies] flate2 = { version = "1", optional = true } http = "1.1" httparse = "1.8" url = { version = "2.4", optional = true } native-tls = { version = "0.2.11", optional = true } rustls = { version = "0.23.27", optional = true, default-features = false, features = ["std", "tls12"] } rustls-pki-types = { version = "1.11", optional = true } rustls-platform-verifier = { version = "0.6", optional = true } webpki-roots = { version = ">=0.26,<2.0", optional = true } [dev-dependencies] codspeed-criterion-compat = "2" [features] default = ["client", "server"] native-tls = ["dep:native-tls"] rustls-ring-native = ["dep:rustls", "rustls/ring", "dep:rustls-platform-verifier", "dep:rustls-pki-types"] rustls-ring-webpki = ["dep:rustls", "rustls/ring", "dep:rustls-pki-types", "dep:webpki-roots"] rustls-aws-lc-native = ["dep:rustls", "rustls/aws_lc_rs", "dep:rustls-platform-verifier", "dep:rustls-pki-types"] rustls-aws-lc-webpki = ["dep:rustls", "rustls/aws_lc_rs", "dep:rustls-pki-types", "dep:webpki-roots"] client = ["dep:url"] server = [] flate2 = ["dep:flate2"] [[bench]] name = "lib" harness = false [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] oxigraph-oxhttp-8a5ee02/LICENSE-APACHE000066400000000000000000000251371502064175100172270ustar00rootroot00000000000000 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. oxigraph-oxhttp-8a5ee02/LICENSE-MIT000066400000000000000000000020471502064175100167320ustar00rootroot00000000000000Copyright (c) 2018 Oxigraph developers 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. oxigraph-oxhttp-8a5ee02/README.md000066400000000000000000000075571502064175100165700ustar00rootroot00000000000000OxHTTP ====== [![actions status](https://github.com/oxigraph/oxhttp/workflows/build/badge.svg)](https://github.com/oxigraph/oxhttp/actions) [![Latest Version](https://img.shields.io/crates/v/oxhttp.svg)](https://crates.io/crates/oxhttp) [![Released API docs](https://docs.rs/oxhttp/badge.svg)](https://docs.rs/oxhttp) OxHTTP is a simple and naive synchronous implementation of [HTTP 1.1](https://httpwg.org/http-core/) in Rust. It provides both a client and a server. It does not aim to be a fully-working-in-all-cases HTTP implementation but to be only a simple one to be use in simple usecases. ## Client OxHTTP provides [a client](https://docs.rs/oxhttp/latest/oxhttp/struct.Client.html). It aims at following the basic concepts of the [Web Fetch standard](https://fetch.spec.whatwg.org/) without the bits specific to web browsers (context, CORS...). HTTPS is supported behind the disabled by default features. To enable it, you need to enable one of the following features: * `native-tls` to use the current system native implementation. * `rustls-ring-webpki` to use [Rustls](https://github.com/rustls/rustls) with the [Ring](https://github.com/briansmith/ring) cryptographic library and the [Common CA Database](https://www.ccadb.org/). * `rustls-ring-native` to use [Rustls](https://github.com/rustls/rustls) with the [Ring](https://github.com/briansmith/ring) cryptographic library and the host certificates. * `rustls-aws-lc-webpki` to use [Rustls](https://github.com/rustls/rustls) with the [AWS Libcrypto for Rust](https://github.com/aws/aws-lc-rs) and the [Common CA Database](https://www.ccadb.org/). * `rustls-aws-lc-native` to use [Rustls](https://github.com/rustls/rustls) with the [AWS Libcrypto for Rust](https://github.com/aws/aws-lc-rs) and the host certificates. Example: ```rust use oxhttp::Client; use oxhttp::model::{Body, Request, Method, StatusCode, HeaderName}; use oxhttp::model::header::CONTENT_TYPE; use std::io::Read; let client = Client::new(); let response = client.request(Request::builder().uri("http://example.com").body(Body::empty()).unwrap()).unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); let body = response.into_body().to_string().unwrap(); ``` ## Server OxHTTP provides [a threaded HTTP server](https://docs.rs/oxhttp/latest/oxhttp/struct.Server.html). It is still a work in progress. Use at your own risks behind a reverse proxy! Example: ```rust no_run use std::net::{Ipv4Addr, Ipv6Addr}; use oxhttp::Server; use oxhttp::model::{Body, Response, StatusCode}; use std::time::Duration; // Builds a new server that returns a 404 everywhere except for "/" where it returns the body 'home' let mut server = Server::new( | request| { if request.uri().path() == "/" { Response::builder().body(Body::from("home")).unwrap() } else { Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap() } }); // We bind the server to localhost on both IPv4 and v6 server = server.bind((Ipv4Addr::LOCALHOST, 8080)).bind((Ipv6Addr::LOCALHOST, 8080)); // Raise a timeout error if the client does not respond after 10s. server = server.with_global_timeout(Duration::from_secs(10)); // Limits the max number of concurrent connections to 128. server = server.with_max_concurrent_connections(128); // We spawn the server and block on it server.spawn().unwrap().join().unwrap(); ``` ## License This project is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ``) * MIT license ([LICENSE-MIT](LICENSE-MIT) or ``) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in OxHTTP by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. oxigraph-oxhttp-8a5ee02/benches/000077500000000000000000000000001502064175100167025ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/benches/lib.rs000066400000000000000000000054071502064175100200240ustar00rootroot00000000000000use codspeed_criterion_compat::{criterion_group, criterion_main, Criterion}; use oxhttp::model::{Body, Request, Response, Uri}; use oxhttp::{Client, Server}; use std::io; use std::io::Read; use std::net::{Ipv4Addr, SocketAddrV4}; fn client_server_no_body(c: &mut Criterion) { Server::new(|_| Response::builder().body(Body::empty()).unwrap()) .bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3456)) .spawn() .unwrap(); let client = Client::new(); let uri = Uri::try_from("http://localhost:3456").unwrap(); c.bench_function("client_server_no_body", |b| { b.iter(|| { client .request(Request::builder().uri(uri.clone()).body(()).unwrap()) .unwrap(); }) }); } fn client_server_fixed_body(c: &mut Criterion) { Server::new(|request| { let mut body = Vec::new(); request.body_mut().read_to_end(&mut body).unwrap(); Response::builder().body(Body::from(body)).unwrap() }) .bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3457)) .spawn() .unwrap(); let client = Client::new(); let uri = Uri::try_from("http://localhost:3456").unwrap(); let body = vec![16u8; 1024]; c.bench_function("client_server_fixed_body", |b| { b.iter(|| { client .request( Request::builder() .uri(uri.clone()) .body(body.clone()) .unwrap(), ) .unwrap(); }) }); } fn client_server_chunked_body(c: &mut Criterion) { Server::new(|request| { let mut body = Vec::new(); request.body_mut().read_to_end(&mut body).unwrap(); Response::builder().body(Body::empty()).unwrap() }) .bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3458)) .spawn() .unwrap(); let client = Client::new(); let uri = Uri::try_from("http://localhost:3456").unwrap(); c.bench_function("client_server_chunked_body", |b| { b.iter(|| { client .request( Request::builder() .uri(uri.clone()) .body(Body::from_read(ChunkedReader::default())) .unwrap(), ) .unwrap(); }) }); } criterion_group!( client_server, client_server_no_body, client_server_fixed_body, client_server_chunked_body ); criterion_main!(client_server); #[derive(Default)] struct ChunkedReader { counter: usize, } impl Read for ChunkedReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.counter >= 10 { return Ok(0); } self.counter += 1; buf.fill(12); Ok(buf.len()) } } oxigraph-oxhttp-8a5ee02/deny.toml000066400000000000000000000002031502064175100171220ustar00rootroot00000000000000[licenses] allow = [ "Apache-2.0", "MIT", "Unicode-DFS-2016", "Unicode-3.0" ] [sources] unknown-registry = "deny" oxigraph-oxhttp-8a5ee02/src/000077500000000000000000000000001502064175100160625ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/src/client.rs000066400000000000000000000476621502064175100177250ustar00rootroot00000000000000#![allow(unreachable_code, clippy::needless_return)] use crate::io::{decode_response, encode_request, BUFFER_CAPACITY}; use crate::model::header::{ InvalidHeaderValue, ACCEPT_ENCODING, CONNECTION, LOCATION, RANGE, USER_AGENT, }; use crate::model::uri::Scheme; use crate::model::{Body, HeaderValue, Method, Request, Response, StatusCode, Uri}; use crate::utils::{invalid_data_error, invalid_input_error}; use http::Version; #[cfg(feature = "native-tls")] use native_tls::TlsConnector; #[cfg(all( any(feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki"), not(feature = "native-tls"), not(feature = "rustls-aws-lc-native"), not(feature = "rustls-ring-native"), ))] use rustls::RootCertStore; #[cfg(all( any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native" ), not(feature = "native-tls") ))] use rustls::{ClientConfig, ClientConnection, StreamOwned}; #[cfg(all( any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native" ), not(feature = "native-tls") ))] use rustls_pki_types::ServerName; #[cfg(all( any(feature = "rustls-aws-lc-native", feature = "rustls-ring-native"), not(feature = "native-tls") ))] use rustls_platform_verifier::ConfigVerifierExt; use std::io::{BufReader, BufWriter, Error, ErrorKind, Result}; use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; #[cfg(all( any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native" ), not(feature = "native-tls") ))] use std::sync::Arc; #[cfg(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" ))] use std::sync::OnceLock; use std::time::Duration; use url::Url; #[cfg(all( any(feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki"), not(feature = "native-tls"), not(feature = "rustls-aws-lc-native"), not(feature = "rustls-ring-native"), ))] use webpki_roots::TLS_SERVER_ROOTS; /// An HTTP client. /// /// It aims at following the basic concepts of the [Web Fetch standard](https://fetch.spec.whatwg.org/) without the bits specific to web browsers (context, CORS...). /// /// HTTPS is supported behind the disabled by default features. /// To enable it you need to enable one of the following features: /// /// * `native-tls` to use the current system native implementation. /// * `rustls-ring-webpki` to use [Rustls](https://github.com/rustls/rustls) with /// the [Ring](https://github.com/briansmith/ring) cryptographic library and /// the [Common CA Database](https://www.ccadb.org/). /// * `rustls-ring-native` to use [Rustls](https://github.com/rustls/rustls) with /// the [Ring](https://github.com/briansmith/ring) cryptographic library and the host certificates. /// * `rustls-aws-lc-webpki` to use [Rustls](https://github.com/rustls/rustls) with /// the [AWS Libcrypto for Rust](https://github.com/aws/aws-lc-rs) and /// the [Common CA Database](https://www.ccadb.org/). /// * `rustls-aws-lc-native` to use [Rustls](https://github.com/rustls/rustls) with /// the [AWS Libcrypto for Rust](https://github.com/aws/aws-lc-rs) and the host certificates. /// /// If the `flate2` feature is enabled, the client will automatically decode `gzip` and `deflate` content-encodings. /// /// The client does not follow redirections by default. Use [`Client::with_redirection_limit`] to set a limit to the number of consecutive redirections the server should follow. /// /// Missing: HSTS support, authentication and keep alive. /// /// ``` /// use http::header::CONTENT_TYPE; /// use oxhttp::model::{Body, HeaderName, Method, Request, StatusCode}; /// use oxhttp::Client; /// use std::io::Read; /// /// let client = Client::new(); /// let response = client.request( /// Request::builder() /// .uri("http://example.com") /// .body(Body::empty())?, /// )?; /// assert_eq!(response.status(), StatusCode::OK); /// assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); /// let body = response.into_body().to_string()?; /// # Result::<_,Box>::Ok(()) /// ``` #[derive(Default)] pub struct Client { timeout: Option, user_agent: Option, redirection_limit: usize, } impl Client { #[inline] pub fn new() -> Self { Self::default() } /// Sets the global timeout value (applies to both read, write and connection). #[inline] pub fn with_global_timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } /// Sets the default value for the [`User-Agent`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.user-agent) header. #[inline] pub fn with_user_agent( mut self, user_agent: impl Into, ) -> std::result::Result { self.user_agent = Some(HeaderValue::try_from(user_agent.into())?); Ok(self) } /// Sets the number of time a redirection should be followed. /// By default the redirections are not followed (limit = 0). #[inline] pub fn with_redirection_limit(mut self, limit: usize) -> Self { self.redirection_limit = limit; self } pub fn request(&self, request: Request>) -> Result> { let mut request = request.map(Into::into); // Loops the number of allowed redirections + 1 for _ in 0..(self.redirection_limit + 1) { let previous_method = request.method().clone(); let response = self.single_request(&mut request)?; let Some(location) = response.headers().get(LOCATION) else { return Ok(response); }; let mut request_builder = Request::builder(); request_builder = request_builder.method(match response.status() { StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::SEE_OTHER => { if previous_method == Method::HEAD { Method::HEAD } else { Method::GET } } StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT if previous_method.is_safe() => { previous_method } _ => return Ok(response), }); let location = location.to_str().map_err(invalid_data_error)?; request_builder = request_builder.uri(join_urls(request.uri(), location)?); for (header_name, header_value) in request.headers() { request_builder = request_builder.header(header_name, header_value); } request = request_builder.body(Body::empty()).map_err(|e| { invalid_input_error(format!( "Failure when trying to build the redirected request: {e}" )) })?; } Err(Error::other(format!( "The server requested too many redirects ({}). The latest redirection target is {}", self.redirection_limit + 1, request.uri() ))) } fn single_request(&self, request: &mut Request) -> Result> { // Additional headers { let request_version = request.version(); let headers = request.headers_mut(); if request_version >= Version::HTTP_11 { headers.insert(CONNECTION, HeaderValue::from_static("close")); } if let Some(user_agent) = &self.user_agent { headers .entry(USER_AGENT) .or_insert_with(|| user_agent.clone()); } if cfg!(feature = "flate2") && !headers.contains_key(RANGE) { headers .entry(ACCEPT_ENCODING) .or_insert_with(|| HeaderValue::from_static("gzip,deflate")); } } #[cfg(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" ))] let host = request .uri() .host() .ok_or_else(|| invalid_input_error("No host provided"))?; let scheme = request.uri().scheme().ok_or_else(|| { invalid_input_error(format!("A URI scheme must be set, found {}", request.uri())) })?; if *scheme == Scheme::HTTP { let addresses = get_and_validate_socket_addresses(request.uri(), 80)?; let stream = self.connect(&addresses)?; let stream = encode_request(request, BufWriter::with_capacity(BUFFER_CAPACITY, stream))? .into_inner() .map_err(|e| e.into_error())?; return decode_response(BufReader::with_capacity(BUFFER_CAPACITY, stream)); } #[cfg(feature = "native-tls")] if *scheme == Scheme::HTTPS { static TLS_CONNECTOR: OnceLock = OnceLock::new(); let addresses = get_and_validate_socket_addresses(request.uri(), 443)?; let stream = self.connect(&addresses)?; let stream = TLS_CONNECTOR .get_or_init(|| match TlsConnector::new() { Ok(connector) => connector, Err(e) => panic!("Error while loading TLS configuration: {}", e), // TODO: use get_or_try_init }) .connect(host, stream) .map_err(|e| Error::new(ErrorKind::Other, e))?; let stream = encode_request(request, BufWriter::with_capacity(BUFFER_CAPACITY, stream))? .into_inner() .map_err(|e| e.into_error())?; return decode_response(BufReader::with_capacity(BUFFER_CAPACITY, stream)); } #[cfg(all( any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native" ), not(feature = "native-tls") ))] if *scheme == Scheme::HTTPS { static RUSTLS_CONFIG: OnceLock> = OnceLock::new(); // TODO: use get_or_try_init #[cfg(any(feature = "rustls-aws-lc-native", feature = "rustls-ring-native"))] let rustls_config = RUSTLS_CONFIG.get_or_init(|| { Arc::new( ClientConfig::with_platform_verifier() .expect("Failed to load the certificate needed to build TLS configuration"), ) }); #[cfg(all( any(feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki"), not(feature = "rustls-aws-lc-native"), not(feature = "rustls-ring-native") ))] let rustls_config = RUSTLS_CONFIG.get_or_init(|| { Arc::new( ClientConfig::builder() .with_root_certificates(RootCertStore { roots: TLS_SERVER_ROOTS.to_vec(), }) .with_no_client_auth(), ) }); let addresses = get_and_validate_socket_addresses(request.uri(), 443)?; let dns_name = ServerName::try_from(host) .map_err(invalid_input_error)? .to_owned(); let connection = ClientConnection::new(Arc::clone(rustls_config), dns_name).map_err(Error::other)?; let stream = StreamOwned::new(connection, self.connect(&addresses)?); let stream = encode_request(request, BufWriter::with_capacity(BUFFER_CAPACITY, stream))? .into_inner() .map_err(|e| e.into_error())?; return decode_response(BufReader::with_capacity(BUFFER_CAPACITY, stream)); } #[cfg(not(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" )))] if *scheme == Scheme::HTTPS { return Err(invalid_input_error("HTTPS is not supported by the client. You should enable the `native-tls` or `rustls` feature of the `oxhttp` crate")); } Err(invalid_input_error(format!( "Not supported URL scheme: {scheme}" ))) } fn connect(&self, addresses: &[SocketAddr]) -> Result { let stream = if let Some(timeout) = self.timeout { Self::connect_timeout(addresses, timeout) } else { TcpStream::connect(addresses) }?; stream.set_read_timeout(self.timeout)?; stream.set_write_timeout(self.timeout)?; stream.set_nodelay(true)?; Ok(stream) } fn connect_timeout(addresses: &[SocketAddr], timeout: Duration) -> Result { let mut error = Error::new( ErrorKind::InvalidInput, "Not able to resolve the provide addresses", ); for address in addresses { match TcpStream::connect_timeout(address, timeout) { Ok(stream) => return Ok(stream), Err(e) => error = e, } } Err(error) } } // Bad ports https://fetch.spec.whatwg.org/#bad-port // Should be sorted const BAD_PORTS: [u16; 80] = [ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, 10080, ]; fn get_and_validate_socket_addresses(uri: &Uri, default_port: u16) -> Result> { let host = uri .host() .ok_or_else(|| invalid_input_error(format!("No host in request URL {uri}")))?; let port = uri.port_u16().unwrap_or(default_port); let addresses = (host, port).to_socket_addrs()?.collect::>(); for address in &addresses { if BAD_PORTS.binary_search(&address.port()).is_ok() { return Err(invalid_input_error(format!( "The port {} is not allowed for HTTP(S) because it is dedicated to an other use", address.port() ))); } } Ok(addresses) } fn join_urls(base: &Uri, relative: &str) -> Result { Uri::try_from( Url::parse(&base.to_string()) .map_err(|e| { Error::new( ErrorKind::InvalidInput, format!("Invalid base URL '{base}': {e}"), ) })? .join(relative) .map_err(|e| { Error::new( ErrorKind::InvalidData, format!("Invalid location header URL '{relative}': {e}"), ) })? .to_string(), ) .map_err(|e| { Error::new( ErrorKind::InvalidData, format!("Invalid location header URL '{relative}': {e}"), ) }) } #[cfg(test)] mod tests { use super::*; use crate::model::header::CONTENT_TYPE; #[test] fn test_http_get_ok() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder() .uri("http://example.com") .body(()) .unwrap(), )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); let body = response.into_body().to_string()?; assert!(body.contains(" Result<()> { let client = Client::new() .with_user_agent("OxHTTP/1.0") .unwrap() .with_global_timeout(Duration::from_secs(5)); let response = client.request( Request::builder() .uri("http://example.com") .body(()) .unwrap(), )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); Ok(()) } #[test] fn test_http_get_ok_explicit_port() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder() .uri("http://example.com:80") .body(()) .unwrap(), )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); Ok(()) } #[test] fn test_http_wrong_port() { let client = Client::new(); assert!(client .request( Request::builder() .uri("http://example.com:22") .body(()) .unwrap(), ) .is_err()); } #[cfg(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" ))] #[test] fn test_https_get_ok() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder() .uri("https://example.com") .body(()) .unwrap(), )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/html"); Ok(()) } #[cfg(not(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" )))] #[test] fn test_https_get_err() { let client = Client::new(); assert!(client .request( Request::builder() .uri("https://example.com") .body(()) .unwrap() ) .is_err()); } #[test] fn test_http_get_not_found() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder() .uri("http://example.com/not_existing") .body(()) .unwrap(), )?; assert!(matches!( response.status(), StatusCode::NOT_FOUND | StatusCode::INTERNAL_SERVER_ERROR )); Ok(()) } #[test] fn test_file_get_error() { let client = Client::new(); assert!(client .request( Request::builder() .uri("file://example.com/not_existing") .body(()) .unwrap(), ) .is_err()); } #[cfg(any( feature = "rustls-aws-lc-webpki", feature = "rustls-ring-webpki", feature = "rustls-aws-lc-native", feature = "rustls-ring-native", feature = "native-tls" ))] #[test] fn test_redirection() -> Result<()> { let client = Client::new().with_redirection_limit(5); let response = client.request( Request::builder() .uri("http://wikipedia.org") .body(()) .unwrap(), )?; assert_eq!(response.status(), StatusCode::OK); Ok(()) } } oxigraph-oxhttp-8a5ee02/src/io/000077500000000000000000000000001502064175100164715ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/src/io/decoder.rs000066400000000000000000000610451502064175100204520ustar00rootroot00000000000000use crate::model::header::{CONTENT_ENCODING, CONTENT_LENGTH, HOST, TRANSFER_ENCODING}; use crate::model::request::Builder as RequestBuilder; use crate::model::uri::{Authority, Parts as UriParts, PathAndQuery, Scheme}; use crate::model::{ Body, ChunkedTransferPayload, HeaderMap, HeaderName, HeaderValue, Method, Request, Response, StatusCode, Uri, Version, }; use crate::utils::invalid_data_error; use httparse::Header; use std::cmp::min; use std::io::{BufRead, Error, ErrorKind, Read, Result}; use std::str::FromStr; const DEFAULT_SIZE: usize = 1024; const MAX_HEADER_SIZE: u64 = 8 * 1024; pub fn decode_request_headers( reader: &mut impl BufRead, is_connection_secure: bool, ) -> Result { // Let's read the headers let buffer = read_header_bytes(reader)?; let mut headers = [httparse::EMPTY_HEADER; DEFAULT_SIZE]; let mut parsed_request = httparse::Request::new(&mut headers); if parsed_request .parse(&buffer) .map_err(invalid_data_error)? .is_partial() { return Err(invalid_data_error( "Partial HTTP headers containing two line jumps", )); } // We build the request let mut request = Request::builder(); decode_headers(parsed_request.headers, request.headers_mut().unwrap())?; if let Some(version) = parsed_request.version { request = request.version(match version { 0 => Version::HTTP_10, 1 => Version::HTTP_11, _ => { return Err(invalid_data_error(format!( "Unsupported HTTP version {version}" ))) } }); } // Method request = request.method( Method::from_str( parsed_request .method .ok_or_else(|| invalid_data_error("No method in the HTTP request"))?, ) .map_err(invalid_data_error)?, ); // URI let path = parsed_request .path .ok_or_else(|| invalid_data_error("No path in the HTTP request"))?; let mut uri_parts = if path == "*" { let mut uri_parts = UriParts::default(); uri_parts.path_and_query = Some(PathAndQuery::from_static("")); uri_parts } else { Uri::try_from(if path == "*" { "" } else { path }) .map_err(invalid_data_error)? .into_parts() }; if is_connection_secure { if *uri_parts.scheme.get_or_insert(Scheme::HTTPS) != Scheme::HTTPS { return Err(invalid_data_error("The HTTPS URL scheme must be 'https")); } } else if *uri_parts.scheme.get_or_insert(Scheme::HTTP) != Scheme::HTTP { return Err(invalid_data_error("The HTTP URL scheme must be 'http")); } if uri_parts.authority.is_none() { uri_parts.authority = Some( Authority::try_from( request .headers_ref() .unwrap() .get(HOST) .ok_or_else(|| invalid_data_error("No host header in HTTP request"))? .as_bytes(), ) .map_err(|e| invalid_data_error(format!("Invalid host header value: {e}")))?, ); } request = request.uri(Uri::from_parts(uri_parts).unwrap()); Ok(request) } pub fn decode_request_body( request: RequestBuilder, reader: impl BufRead + 'static, ) -> Result> { let body = if let Some(headers) = request.headers_ref() { decode_body(headers, reader)? } else { Body::empty() }; request .body(body) .map_err(|e| invalid_data_error(format!("Unexpected error when parsing the request: {e}"))) } pub fn decode_response(mut reader: impl BufRead + 'static) -> Result> { // Let's read the headers let buffer = read_header_bytes(&mut reader)?; let mut headers = [httparse::EMPTY_HEADER; DEFAULT_SIZE]; let mut parsed_response = httparse::Response::new(&mut headers); if parsed_response .parse(&buffer) .map_err(invalid_data_error)? .is_partial() { return Err(invalid_data_error( "Partial HTTP headers containing two line jumps", )); } let status = StatusCode::from_u16( parsed_response .code .ok_or_else(|| invalid_data_error("No status code in the HTTP response"))?, ) .map_err(invalid_data_error)?; // Let's build the response let mut response = Response::builder().status(status); decode_headers(parsed_response.headers, response.headers_mut().unwrap())?; let body = if let Some(headers) = response.headers_ref() { decode_body(headers, reader)? } else { Body::empty() }; Ok(response.body(body).unwrap()) } fn read_header_bytes(reader: impl BufRead) -> Result> { let mut reader = reader.take(2 * MAX_HEADER_SIZE); // Makes sure we do not buffer too much let mut buffer = Vec::with_capacity(DEFAULT_SIZE); loop { if reader.read_until(b'\n', &mut buffer)? == 0 { return Err(Error::new( ErrorKind::ConnectionAborted, if buffer.is_empty() { "Empty HTTP request" } else { "Interrupted HTTP request" }, )); } // We normalize line ends to plain \n if buffer.ends_with(b"\r\n") { buffer.pop(); buffer.pop(); buffer.push(b'\n') } if buffer.len() > (MAX_HEADER_SIZE as usize) { return Err(invalid_data_error("The headers size should fit in 8kb")); } if buffer.ends_with(b"\n\n") { break; // end of buffer } } Ok(buffer) } fn decode_body(headers: &HeaderMap, reader: impl BufRead + 'static) -> Result { let content_length = headers.get(CONTENT_LENGTH); let transfer_encoding = headers.get(TRANSFER_ENCODING); if transfer_encoding.is_some() && content_length.is_some() { return Err(invalid_data_error( "Transfer-Encoding and Content-Length should not be set at the same time", )); } let body = if let Some(content_length) = content_length { let len = content_length .to_str() .map_err(invalid_data_error)? .parse::() .map_err(invalid_data_error)?; Body::from_read_and_len(reader, len) } else if let Some(transfer_encoding) = transfer_encoding { if transfer_encoding.as_ref().eq_ignore_ascii_case(b"chunked") { Body::from_chunked_transfer_payload(ChunkedDecoder { reader, buffer: Vec::with_capacity(DEFAULT_SIZE), is_start: true, chunk_position: 0, chunk_size: 0, trailers: None, }) } else { return Err(invalid_data_error(format!( "Transfer-Encoding: {} is not supported", transfer_encoding.to_str().map_err(invalid_data_error)? ))); } } else { Body::empty() }; decode_content_encoding(body, headers) } fn decode_headers(from: &[Header<'_>], to: &mut HeaderMap) -> Result<()> { for header in from { to.try_append( HeaderName::try_from(header.name) .map_err(|e| invalid_data_error(format!("Invalid header name: {e}")))?, HeaderValue::try_from(header.value) .map_err(|e| invalid_data_error(format!("Invalid header value: {e}")))?, ) .map_err(|e| invalid_data_error(format!("Too many headers: {e}")))?; } Ok(()) } fn decode_content_encoding(body: Body, headers: &HeaderMap) -> Result { let Some(content_encoding) = headers.get(CONTENT_ENCODING) else { return Ok(body); }; match content_encoding.as_ref() { b"identity" => Ok(body), #[cfg(feature = "flate2")] b"gzip" => Ok(body.decode_gzip()), #[cfg(feature = "flate2")] b"deflate" => Ok(body.decode_deflate()), _ => Ok(body), } } struct ChunkedDecoder { reader: R, buffer: Vec, is_start: bool, chunk_position: usize, chunk_size: usize, trailers: Option, } impl Read for ChunkedDecoder { fn read(&mut self, buf: &mut [u8]) -> Result { loop { // In case we still have data if self.chunk_position < self.chunk_size { let inner_buf = self.reader.fill_buf()?; if inner_buf.is_empty() { return Err(invalid_data_error( "Unexpected stream end in the middle of a chunked content", )); } let size = min( min(buf.len(), inner_buf.len()), self.chunk_size - self.chunk_position, ); buf[..size].copy_from_slice(&inner_buf[..size]); self.reader.consume(size); self.chunk_position += size; return Ok(size); } if self.is_start { self.is_start = false; } else if self.trailers.is_some() { return Ok(0); // We already read the trailers, it means we have finished reading } else { // chunk end self.buffer.clear(); self.reader.read_until(b'\n', &mut self.buffer)?; if self.buffer != b"\r\n" && self.buffer != b"\n" { return Err(invalid_data_error("Invalid chunked element end")); } } // We load a new chunk self.buffer.clear(); self.reader.read_until(b'\n', &mut self.buffer)?; self.chunk_position = 0; let Ok(httparse::Status::Complete((read, chunk_size))) = httparse::parse_chunk_size(&self.buffer) else { return Err(invalid_data_error("Invalid chunked header")); }; if read != self.buffer.len() { return Err(invalid_data_error("Chunked header containing a line jump")); } self.chunk_size = chunk_size.try_into().map_err(invalid_data_error)?; if self.chunk_size == 0 { // we read the trailers self.buffer.clear(); self.buffer.push(b'\n'); loop { if self.reader.read_until(b'\n', &mut self.buffer)? == 0 { return Err(invalid_data_error("Missing chunked encoding end")); } if self.buffer.len() > 8 * 1024 { return Err(invalid_data_error("The trailers size should fit in 8kb")); } if self.buffer.ends_with(b"\r\n") { self.buffer.pop(); self.buffer.pop(); self.buffer.push(b'\n') } if self.buffer.ends_with(b"\n\n") { break; // end of buffer } } let mut trailers = [httparse::EMPTY_HEADER; DEFAULT_SIZE]; let httparse::Status::Complete((read, parsed_trailers)) = httparse::parse_headers(&self.buffer[1..], &mut trailers) .map_err(invalid_data_error)? else { return Err(invalid_data_error( "Partial HTTP headers containing two line jumps", )); }; if read != self.buffer.len() - 1 { return Err(invalid_data_error( "Invalid data at the end of the trailer section", )); } let mut trailers = HeaderMap::new(); decode_headers(parsed_trailers, &mut trailers)?; self.trailers = Some(trailers); return Ok(0); } } } } impl ChunkedTransferPayload for ChunkedDecoder { fn trailers(&self) -> Option<&HeaderMap> { self.trailers.as_ref() } } #[cfg(test)] mod tests { use super::*; use crate::model::header::CONTENT_TYPE; use crate::model::HeaderName; #[test] fn decode_request_target_origin_form() -> Result<()> { let request = decode_request_headers( &mut b"GET /where?q=now HTTP/1.1\nHost: www.example.org\n\n".as_slice(), false, )? .body(()) .unwrap(); assert_eq!( request.uri().to_string(), "http://www.example.org/where?q=now" ); Ok(()) } #[test] fn decode_request_target_absolute_form_with_host() -> Result<()> { let request = decode_request_headers( &mut b"GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\nHost: example.com\n\n".as_slice() , false, )?.body(()).unwrap(); assert_eq!( request.uri().to_string(), "http://www.example.org/pub/WWW/TheProject.html" ); Ok(()) } #[test] fn decode_request_target_absolute_form_without_host() -> Result<()> { let request = decode_request_headers( &mut b"GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n".as_slice(), false, )? .body(()) .unwrap(); assert_eq!( request.uri().to_string(), "http://www.example.org/pub/WWW/TheProject.html" ); Ok(()) } #[test] fn decode_request_target_relative_form_without_host() { assert!(decode_request_headers( &mut b"GET /pub/WWW/TheProject.html HTTP/1.1\n\n".as_slice(), false, ) .is_err()); } #[test] fn decode_request_target_absolute_form_wrong_scheme() { assert!(decode_request_headers( &mut b"GET https://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n".as_slice(), false, ) .is_err()); assert!(decode_request_headers( &mut b"GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n".as_slice(), true, ) .is_err()); } #[test] fn decode_invalid_request_target_relative_form_with_host() { assert!(decode_request_headers( &mut b"GET /foo Result<()> { let request = decode_request_headers( &mut b"OPTIONS * HTTP/1.1\nHost: www.example.org:8001\n\n".as_slice(), false, )? .body(()) .unwrap(); assert_eq!(request.uri().to_string(), "http://www.example.org:8001/"); // TODO: should be http://www.example.org:8001 Ok(()) } #[test] fn decode_request_with_header() -> Result<()> { let request = decode_request_headers( &mut b"GET / HTTP/1.1\nHost: www.example.org:8001\nFoo: v1\nbar: vbar\nfoo: v2\n\n" .as_slice(), true, )? .body(()) .unwrap(); assert_eq!(request.uri().to_string(), "https://www.example.org:8001/"); assert_eq!( request .headers() .get_all(HeaderName::from_str("foo").unwrap()) .into_iter() .collect::>(), vec!["v1", "v2"] ); assert_eq!( request .headers() .get(HeaderName::from_str("Bar").unwrap()) .unwrap(), "vbar" ); Ok(()) } #[test] fn decode_request_with_body() -> Result<()> { let mut read = b"GET / HTTP/1.1\nHost: www.example.org:8001\ncontent-length: 9\n\nfoobarbar" .as_slice(); let request = decode_request_body(decode_request_headers(&mut read, false)?, read)?; assert_eq!(request.into_body().to_string()?, "foobarbar"); Ok(()) } #[test] fn decode_request_empty_header_name() { assert!(decode_request_headers( &mut b"GET / HTTP/1.1\nHost: www.example.org:8001\n: foo".as_slice(), false ) .is_err()); } #[test] fn decode_request_invalid_header_name_char() { assert!(decode_request_headers( &mut b"GET / HTTP/1.1\nHost: www.example.org:8001\nCont\xE9: foo".as_slice(), false ) .is_err()); } #[test] fn decode_request_invalid_header_value_char() { assert!(decode_request_headers( &mut b"GET / HTTP/1.1\nHost: www.example.org:8001\nCont\t: foo\rbar\r\nTest: test" .as_slice(), false ) .is_err()); } #[test] fn decode_request_empty() { assert_eq!( decode_request_headers(&mut b"".as_slice(), false) .err() .map(|e| e.kind()), Some(ErrorKind::ConnectionAborted) ); } #[test] fn decode_request_stop_in_header() { assert_eq!( decode_request_headers(&mut b"GET /\r\n".as_slice(), false) .err() .map(|e| e.kind()), Some(ErrorKind::ConnectionAborted) ); } #[test] fn decode_request_stop_in_body() -> Result<()> { let mut read = b"POST / HTTP/1.1\r\nhost: example.com\r\ncontent-length: 12\r\n\r\nfoobar".as_slice(); assert_eq!( decode_request_body(decode_request_headers(&mut read, false)?, read)? .into_body() .to_vec() .err() .map(|e| e.kind()), Some(ErrorKind::ConnectionAborted) ); Ok(()) } #[test] fn decode_request_http_1_0() -> Result<()> { let mut read = b"POST http://example.com/foo HTTP/1.0\r\ncontent-length: 12\r\n\r\nfoobar".as_slice(); let request = decode_request_body(decode_request_headers(&mut read, false)?, read)?; assert_eq!(request.version(), Version::HTTP_10); assert_eq!(request.uri().to_string(), "http://example.com/foo"); Ok(()) } #[test] fn decode_request_unsupported_transfer_encoding() -> Result<()> { let mut read = b"POST / HTTP/1.1\r\nhost: example.com\r\ncontent-length: 12\r\ntransfer-encoding: foo\r\n\r\nfoobar".as_slice(); assert!(decode_request_body(decode_request_headers(&mut read, false)?, read).is_err()); Ok(()) } #[test] fn decode_response_without_payload() -> Result<()> { let response = decode_response(b"HTTP/1.1 404 Not Found\r\n\r\n".as_slice())?; assert_eq!(response.status(), StatusCode::NOT_FOUND); assert_eq!(response.body().len(), Some(0)); Ok(()) } #[test] fn decode_response_with_fixed_payload() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length:12\r\n\r\ntestbodybody" .as_slice(), )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); assert_eq!(response.into_body().to_string()?, "testbodybody"); Ok(()) } #[test] fn decode_response_with_chunked_payload() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n".as_slice() )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); assert_eq!( response.into_body().to_string()?, "Wikipedia in\r\n\r\nchunks." ); Ok(()) } #[test] fn decode_response_with_trailer() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\ntest: foo\r\n\r\n".as_slice() )?; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); let mut buf = String::new(); let mut body = response.into_body(); body.read_to_string(&mut buf)?; assert_eq!(buf, "Wikipedia in\r\n\r\nchunks."); assert_eq!( body.trailers() .unwrap() .get(HeaderName::from_static("test")) .unwrap(), "foo" ); Ok(()) } #[test] #[cfg(feature = "flate2")] fn decode_gzip_response() -> Result<()> { let response = decode_response(b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-encoding: gzip\r\ncontent-length: 23\r\n\r\n\x1f\x8b\x08\x00\xac\x94\xdfd\x02\xffK\xcb\xcf\x07\x00!es\x8c\x03\x00\x00\x00".as_slice())?; assert_eq!(response.into_body().to_string()?, "foo"); Ok(()) } #[test] #[cfg(feature = "flate2")] fn decode_deflate_response() -> Result<()> { let response = decode_response(b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-encoding: deflate\r\ncontent-length: 5\r\n\r\nK\xcb\xcf\x07\x00".as_slice())?; assert_eq!(response.into_body().to_string()?, "foo"); Ok(()) } #[test] fn decode_unknown_response() -> Result<()> { let response = decode_response(b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-encoding: foo\r\ncontent-length: 5\r\n\r\nfoooo".as_slice())?; assert_eq!(response.headers().get(CONTENT_ENCODING).unwrap(), "foo"); assert_eq!(response.into_body().to_string()?, "foooo"); Ok(()) } #[test] fn decode_response_with_invalid_chunk_header() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\nh\r\nWiki\r\n0\r\n\r\n" .as_slice(), )?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_with_invalid_trailer() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\nf\r\nWiki\r\n0\r\ntest\n: foo\r\n\r\n" .as_slice())?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_with_not_ended_trailer() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\nf\r\nWiki".as_slice(), )?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_empty_header_name() { assert!( decode_response(b"HTTP/1.1 200 OK\nHost: www.example.org:8001\n: foo".as_slice()) .is_err() ); } #[test] fn decode_response_invalid_header_name_char() { assert!(decode_response( b"HTTP/1.1 200 OK\nHost: www.example.org:8001\nCont\xE9: foo".as_slice() ) .is_err()); } #[test] fn decode_response_invalid_header_value_char() { assert!(decode_response( b"HTTP/1.1 200 OK\nHost: www.example.org:8001\nCont\t: foo\rbar\r\nTest: test" .as_slice() ) .is_err()); } #[test] fn decode_response_empty() { assert!(decode_response(b"".as_slice()).is_err()); } #[test] fn decode_response_stop_in_header() { assert!(decode_response(b"HTTP/1.1 404 Not Found\r\n".as_slice()).is_err()); } #[test] fn decode_response_stop_in_body() -> Result<()> { assert!(decode_response( b"HTTP/1.1 200 OK\r\ncontent-length: 12\r\n\r\nfoobar".as_slice() )? .into_body() .to_vec() .is_err()); Ok(()) } #[test] fn decode_response_content_length_and_transfer_encoding() { assert!(decode_response( b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\ncontent-length: 222\r\n\r\n".as_slice()).is_err()); } #[test] fn decode_response_with_chunked_payload_read_after_end() -> Result<()> { let response = decode_response( b"HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n".as_slice() )?; assert_eq!(response.status(), StatusCode::OK); let mut body = response.into_body(); body.read_to_end(&mut Vec::new())?; assert_eq!(body.read(&mut [0; 1])?, 0); Ok(()) } } oxigraph-oxhttp-8a5ee02/src/io/encoder.rs000066400000000000000000000250341502064175100204620ustar00rootroot00000000000000use crate::model::header::{ ACCEPT_CHARSET, ACCEPT_ENCODING, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_REQUEST_HEADERS, CONNECTION, CONTENT_LENGTH, DATE, EXPECT, HOST, ORIGIN, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, VIA, }; use crate::model::{Body, HeaderMap, HeaderName, Method, Request, Response, StatusCode, Version}; use crate::utils::invalid_input_error; use std::io::{copy, Read, Result, Write}; pub fn encode_request(request: &mut Request, mut writer: W) -> Result { if request .uri() .authority() .is_some_and(|a| a.as_str().contains('@')) { return Err(invalid_input_error( "Username and password are not allowed in HTTP URLs", )); } let host = request .uri() .host() .ok_or_else(|| invalid_input_error("No host provided"))?; let version_str = serialize_version(request.version())?; if let Some(query) = request.uri().query() { write!( &mut writer, "{} {}?{query} {version_str}\r\n", request.method(), request.uri().path() )?; } else { write!( &mut writer, "{} {} {version_str}\r\n", request.method(), request.uri().path(), )?; } // host if let Some(port) = request.uri().port() { write!(writer, "host: {host}:{port}\r\n")?; } else { write!(writer, "host: {host}\r\n")?; } // headers encode_headers(request.headers(), &mut writer)?; // body with content-length if existing let must_include_body = does_request_must_include_body(request.method()); encode_body(request.body_mut(), &mut writer, must_include_body)?; Ok(writer) } pub fn encode_response(response: &mut Response, mut writer: W) -> Result { let status = response.status(); let version_str = serialize_version(response.version())?; write!(&mut writer, "{version_str} {status}\r\n")?; encode_headers(response.headers(), &mut writer)?; let must_include_body = does_response_must_include_body(response.status()); encode_body(response.body_mut(), &mut writer, must_include_body)?; Ok(writer) } fn encode_headers(headers: &HeaderMap, writer: &mut impl Write) -> Result<()> { for (name, value) in headers { if !is_forbidden_name(name) { write!(writer, "{name}: ")?; writer.write_all(value.as_bytes())?; write!(writer, "\r\n")?; } } Ok(()) } fn encode_body(body: &mut Body, writer: &mut impl Write, must_include_body: bool) -> Result<()> { if let Some(length) = body.len() { if must_include_body || length > 0 { write!(writer, "content-length: {length}\r\n\r\n")?; copy(body, writer)?; } else { write!(writer, "\r\n")?; } } else { write!(writer, "transfer-encoding: chunked\r\n\r\n")?; let mut buffer = vec![b'\0'; 4096]; loop { let mut read = 0; while read < 1024 { // We try to avoid too small chunks let new_read = body.read(&mut buffer[read..])?; if new_read == 0 { break; // EOF } read += new_read; } write!(writer, "{read:X}\r\n")?; writer.write_all(&buffer[..read])?; if read == 0 { break; // Done } else { write!(writer, "\r\n")?; } } if let Some(trailers) = body.trailers() { encode_headers(trailers, writer)?; } write!(writer, "\r\n")?; } Ok(()) } /// Checks if it is a [forbidden header name](https://fetch.spec.whatwg.org/#forbidden-header-name) /// /// We removed some of them not managed by this library (`Access-Control-Request-Headers`, `Access-Control-Request-Method`, `DNT`, `Cookie`, `Cookie2`, `Referer`, `Proxy-`, `Sec-`, `Via`...) fn is_forbidden_name(header: &HeaderName) -> bool { header == ACCEPT_CHARSET || header == ACCEPT_ENCODING || header == ACCESS_CONTROL_REQUEST_HEADERS || header == ACCESS_CONTROL_ALLOW_METHODS || header == CONNECTION || header == CONTENT_LENGTH || header == DATE || header == EXPECT || header == HOST || header.as_str() == "keep-alive" || header == ORIGIN || header == TE || header == TRAILER || header == TRANSFER_ENCODING || header == UPGRADE || header == VIA } fn does_request_must_include_body(method: &Method) -> bool { *method == Method::POST || *method == Method::PUT } fn does_response_must_include_body(status: StatusCode) -> bool { !(status.is_informational() || status == StatusCode::NO_CONTENT || status == StatusCode::NOT_MODIFIED) } fn serialize_version(version: Version) -> Result<&'static str> { match version { Version::HTTP_10 => Ok("HTTP/1.0"), Version::HTTP_11 => Ok("HTTP/1.1"), _ => Err(invalid_input_error( "HTTP version {version:?} is not supported", )), } } #[cfg(test)] mod tests { use super::*; use crate::model::header::{ACCEPT, CONTENT_LANGUAGE}; use crate::model::{ChunkedTransferPayload, HeaderMap, HeaderValue}; use std::str; #[test] fn user_password_not_allowed_in_request() { let mut buffer = Vec::new(); assert!(encode_request( &mut Request::builder() .uri("http://foo@example.com/") .body(Body::empty()) .unwrap(), &mut buffer ) .is_err()); assert!(encode_request( &mut Request::builder() .uri("http://foo:bar@example.com/") .body(Body::empty()) .unwrap(), &mut buffer ) .is_err()); } #[test] fn encode_get_request() -> Result<()> { let mut request = Request::builder() .uri("http://example.com:81/foo/bar?query#fragment") .header(ACCEPT, "application/json") .body(Body::empty()) .unwrap(); let buffer = encode_request(&mut request, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "GET /foo/bar?query HTTP/1.1\r\nhost: example.com:81\r\naccept: application/json\r\n\r\n" ); Ok(()) } #[test] fn encode_post_request() -> Result<()> { let mut request = Request::builder() .method(Method::POST) .uri("http://example.com/foo/bar?query#fragment") .header(ACCEPT, "application/json") .body(Body::from("testbodybody")) .unwrap(); let buffer = encode_request(&mut request, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "POST /foo/bar?query HTTP/1.1\r\nhost: example.com\r\naccept: application/json\r\ncontent-length: 12\r\n\r\ntestbodybody" ); Ok(()) } #[test] fn encode_post_request_without_body() -> Result<()> { let mut request = Request::builder() .method(Method::POST) .uri("http://example.com/foo/bar?query#fragment") .body(Body::empty()) .unwrap(); let buffer = encode_request(&mut request, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "POST /foo/bar?query HTTP/1.1\r\nhost: example.com\r\ncontent-length: 0\r\n\r\n" ); Ok(()) } #[test] fn encode_post_request_with_chunked() -> Result<()> { let mut trailers = HeaderMap::new(); trailers.append(CONTENT_LANGUAGE, HeaderValue::from_static("foo")); let mut request = Request::builder() .method(Method::POST) .uri("http://example.com/foo/bar?query#fragment") .body(Body::from_chunked_transfer_payload(SimpleTrailers { read: b"testbodybody".as_slice(), trailers, })) .unwrap(); let buffer = encode_request(&mut request, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "POST /foo/bar?query HTTP/1.1\r\nhost: example.com\r\ntransfer-encoding: chunked\r\n\r\nC\r\ntestbodybody\r\n0\r\ncontent-language: foo\r\n\r\n" ); Ok(()) } #[test] fn encode_response_ok() -> Result<()> { let mut response = Response::builder() .header(ACCEPT, "application/json") .body(Body::from("test test2")) .unwrap(); let buffer = encode_response(&mut response, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "HTTP/1.1 200 OK\r\naccept: application/json\r\ncontent-length: 10\r\n\r\ntest test2" ); Ok(()) } #[test] fn encode_response_not_found() -> Result<()> { let mut response = Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()) .unwrap(); let buffer = encode_response(&mut response, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\n\r\n" ); Ok(()) } #[test] fn encode_response_custom_code() -> Result<()> { let mut response = Response::builder().status(499).body(Body::empty()).unwrap(); let buffer = encode_response(&mut response, Vec::new())?; assert_eq!( str::from_utf8(&buffer).unwrap(), "HTTP/1.1 499 \r\ncontent-length: 0\r\n\r\n" ); Ok(()) } #[test] fn http_2_not_serializable() { assert!(encode_request( &mut Request::builder() .uri("http://foo:bar@example.com/") .version(Version::HTTP_2) .body(Body::empty()) .unwrap(), &mut Vec::new() ) .is_err()); assert!(encode_response( &mut Response::builder() .version(Version::HTTP_2) .body(Body::empty()) .unwrap(), &mut Vec::new() ) .is_err()); } struct SimpleTrailers { read: &'static [u8], trailers: HeaderMap, } impl Read for SimpleTrailers { fn read(&mut self, buf: &mut [u8]) -> Result { self.read.read(buf) } } impl ChunkedTransferPayload for SimpleTrailers { fn trailers(&self) -> Option<&HeaderMap> { Some(&self.trailers) } } } oxigraph-oxhttp-8a5ee02/src/io/mod.rs000066400000000000000000000005251502064175100176200ustar00rootroot00000000000000mod decoder; mod encoder; pub use decoder::{decode_request_body, decode_request_headers, decode_response}; pub use encoder::{encode_request, encode_response}; /// Capacity for buffers. /// /// Should be significantly greater than BufWriter capacity to avoid flush in the `copy` method. pub(super) const BUFFER_CAPACITY: usize = 16 * 1024; oxigraph-oxhttp-8a5ee02/src/lib.rs000066400000000000000000000010131502064175100171710ustar00rootroot00000000000000#![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![deny( future_incompatible, nonstandard_style, rust_2018_idioms, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unused_qualifications )] #[cfg(feature = "client")] mod client; mod io; pub mod model; #[cfg(feature = "server")] mod server; mod utils; #[cfg(feature = "client")] pub use client::Client; #[cfg(feature = "server")] pub use server::{ListeningServer, Server}; oxigraph-oxhttp-8a5ee02/src/model/000077500000000000000000000000001502064175100171625ustar00rootroot00000000000000oxigraph-oxhttp-8a5ee02/src/model/body.rs000066400000000000000000000220111502064175100204610ustar00rootroot00000000000000use crate::model::HeaderMap; #[cfg(feature = "flate2")] use flate2::read::{DeflateDecoder, GzDecoder}; use std::borrow::Cow; use std::fmt; use std::io::{Cursor, Error, ErrorKind, Read, Result}; /// A request or response [body](https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#message.body). /// /// It implements the [`Read`] API. pub struct Body(BodyAlt); enum BodyAlt { SimpleOwned(Cursor>), SimpleBorrowed(&'static [u8]), Sized { content: Box, total_len: u64, consumed_len: u64, }, Chunked(Box), #[cfg(feature = "flate2")] DecodingDeflate(DeflateDecoder>), #[cfg(feature = "flate2")] DecodingGzip(GzDecoder>), } impl Body { /// Creates a new body from a [`Read`] implementation. /// /// If the body is sent as an HTTP request or response it will be streamed using [chunked transfer encoding](https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#chunked.encoding). #[inline] pub fn from_read(read: impl Read + 'static) -> Self { Self::from_chunked_transfer_payload(SimpleChunkedTransferEncoding(read)) } #[inline] pub(crate) fn from_read_and_len(read: impl Read + 'static, len: u64) -> Self { Self(BodyAlt::Sized { total_len: len, consumed_len: 0, content: Box::new(read.take(len)), }) } /// Creates a [chunked transfer encoding](https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#chunked.encoding) body with optional [trailers](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#trailer.fields). #[inline] pub fn from_chunked_transfer_payload(payload: impl ChunkedTransferPayload + 'static) -> Self { Self(BodyAlt::Chunked(Box::new(payload))) } #[cfg(feature = "flate2")] pub(crate) fn decode_gzip(self) -> Self { Self(BodyAlt::DecodingGzip(GzDecoder::new(Box::new(self)))) } #[cfg(feature = "flate2")] pub(crate) fn decode_deflate(self) -> Self { Self(BodyAlt::DecodingDeflate(DeflateDecoder::new(Box::new( self, )))) } /// The empty body #[inline] pub fn empty() -> Self { Self(BodyAlt::SimpleBorrowed(b"")) } /// The number of bytes in the body (if known). #[allow(clippy::len_without_is_empty)] #[inline] pub fn len(&self) -> Option { match &self.0 { BodyAlt::SimpleOwned(d) => Some(d.get_ref().len().try_into().unwrap()), BodyAlt::SimpleBorrowed(d) => Some(d.len().try_into().unwrap()), BodyAlt::Sized { total_len, .. } => Some(*total_len), BodyAlt::Chunked(_) => None, #[cfg(feature = "flate2")] BodyAlt::DecodingDeflate(_) | BodyAlt::DecodingGzip(_) => None, } } /// Returns the chunked transfer encoding trailers if they exists and are already received. /// You should fully consume the body before attempting to fetch them. #[inline] pub fn trailers(&self) -> Option<&HeaderMap> { match &self.0 { BodyAlt::SimpleOwned(_) | BodyAlt::SimpleBorrowed(_) | BodyAlt::Sized { .. } => None, BodyAlt::Chunked(c) => c.trailers(), #[cfg(feature = "flate2")] BodyAlt::DecodingDeflate(c) => c.get_ref().trailers(), #[cfg(feature = "flate2")] BodyAlt::DecodingGzip(c) => c.get_ref().trailers(), } } /// Reads the full body into a vector. /// ///
Beware of the body size!
/// /// ``` /// use oxhttp::model::Body; /// use std::io::Cursor; /// /// let mut body = Body::from_read(b"foo".as_ref()); /// assert_eq!(&body.to_vec()?, b"foo"); /// # Result::<_,Box>::Ok(()) /// ``` #[inline] pub fn to_vec(mut self) -> Result> { let mut buf = Vec::new(); self.read_to_end(&mut buf)?; Ok(buf) } /// Reads the full body into a string. /// ///
Beware of the body size!
/// /// ``` /// use oxhttp::model::Body; /// use std::io::Cursor; /// /// let mut body = Body::from_read(b"foo".as_ref()); /// assert_eq!(&body.to_string()?, "foo"); /// # Result::<_,Box>::Ok(()) /// ``` #[inline] pub fn to_string(mut self) -> Result { let mut buf = String::new(); self.read_to_string(&mut buf)?; Ok(buf) } fn debug_fields<'a, 'b, 'c>( &'b self, s: &'c mut fmt::DebugStruct<'b, 'a>, ) -> &'c mut fmt::DebugStruct<'b, 'a> { match &self.0 { BodyAlt::SimpleOwned(d) => s.field("content-length", &d.get_ref().len()), BodyAlt::SimpleBorrowed(d) => s.field("content-length", &d.len()), BodyAlt::Sized { total_len, .. } => s.field("content-length", total_len), BodyAlt::Chunked(_) => s.field("transfer-encoding", &"chunked"), #[cfg(feature = "flate2")] BodyAlt::DecodingDeflate(inner) => inner .get_ref() .debug_fields(s.field("content-encoding", &"deflate")), #[cfg(feature = "flate2")] BodyAlt::DecodingGzip(inner) => inner .get_ref() .debug_fields(s.field("content-encoding", &"gzip")), } } } impl Read for Body { #[inline] fn read(&mut self, mut buf: &mut [u8]) -> Result { match &mut self.0 { BodyAlt::SimpleOwned(c) => c.read(buf), BodyAlt::SimpleBorrowed(c) => c.read(buf), BodyAlt::Sized { content, consumed_len, total_len, } => { let remaining_size = *total_len - *consumed_len; if remaining_size < u64::try_from(buf.len()).unwrap() { buf = &mut buf[..usize::try_from(remaining_size).unwrap()]; } if buf.is_empty() { return Ok(0); // Nothing to read } let read = content.read(buf)?; *consumed_len += u64::try_from(read).unwrap(); if read == 0 { // We are missing some bytes return Err(Error::new(ErrorKind::ConnectionAborted, format!("The body was expected to contain {total_len} bytes but we have been able to only read {consumed_len}"))); } Ok(read) } BodyAlt::Chunked(inner) => inner.read(buf), #[cfg(feature = "flate2")] BodyAlt::DecodingDeflate(inner) => inner.read(buf), #[cfg(feature = "flate2")] BodyAlt::DecodingGzip(inner) => inner.read(buf), } } } impl Default for Body { #[inline] fn default() -> Self { Self::empty() } } impl From> for Body { #[inline] fn from(data: Vec) -> Self { Self(BodyAlt::SimpleOwned(Cursor::new(data))) } } impl From for Body { #[inline] fn from(data: String) -> Self { data.into_bytes().into() } } impl From<&'static [u8]> for Body { #[inline] fn from(data: &'static [u8]) -> Self { Self(BodyAlt::SimpleBorrowed(data)) } } impl From<&'static str> for Body { #[inline] fn from(data: &'static str) -> Self { data.as_bytes().into() } } impl From> for Body { #[inline] fn from(data: Cow<'static, [u8]>) -> Self { match data { Cow::Borrowed(data) => data.into(), Cow::Owned(data) => data.into(), } } } impl From> for Body { #[inline] fn from(data: Cow<'static, str>) -> Self { match data { Cow::Borrowed(data) => data.into(), Cow::Owned(data) => data.into(), } } } impl From<()> for Body { #[inline] fn from(_: ()) -> Self { Self::empty() } } impl fmt::Debug for Body { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.debug_fields(&mut f.debug_struct("Body")).finish() } } /// Trait to give to [`Body::from_chunked_transfer_payload`] a body to serialize /// as [chunked transfer encoding](https://httpwg.org/http-core/draft-ietf-httpbis-messaging-latest.html#chunked.encoding). /// /// It allows to provide [trailers](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#trailer.fields) to serialize. pub trait ChunkedTransferPayload: Read { /// The [trailers](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#trailer.fields) to serialize. fn trailers(&self) -> Option<&HeaderMap>; } struct SimpleChunkedTransferEncoding(R); impl Read for SimpleChunkedTransferEncoding { #[inline] fn read(&mut self, buf: &mut [u8]) -> Result { self.0.read(buf) } } impl ChunkedTransferPayload for SimpleChunkedTransferEncoding { #[inline] fn trailers(&self) -> Option<&HeaderMap> { None } } oxigraph-oxhttp-8a5ee02/src/model/mod.rs000066400000000000000000000004161502064175100203100ustar00rootroot00000000000000//! The HTTP model encoded in Rust type system. //! //! This reexport the [`http`](https://docs.rs/http) crate except for [`Body`]. //! //! The main entry points are [`Request`] and [`Response`]. mod body; pub use body::{Body, ChunkedTransferPayload}; pub use http::*; oxigraph-oxhttp-8a5ee02/src/server.rs000066400000000000000000000422511502064175100177420ustar00rootroot00000000000000use crate::io::{decode_request_body, decode_request_headers, encode_response, BUFFER_CAPACITY}; use crate::model::header::{InvalidHeaderValue, CONNECTION, CONTENT_TYPE, EXPECT, SERVER}; use crate::model::request::Builder as RequestBuilder; use crate::model::{Body, HeaderValue, Request, Response, StatusCode, Version}; use std::fmt; use std::io::{copy, sink, BufReader, BufWriter, Error, ErrorKind, Result, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::{Arc, Condvar, Mutex}; use std::thread::{Builder as ThreadBuilder, JoinHandle}; use std::time::Duration; /// An HTTP server. /// /// It uses a very simple threading mechanism: a new thread is started on each connection and closed when the client connection is closed. /// To avoid crashes it is possible to set an upper bound to the number of concurrent connections using the [`Server::with_max_concurrent_connections`] function. /// /// ```no_run /// use std::net::{Ipv4Addr, Ipv6Addr}; /// use oxhttp::Server; /// use oxhttp::model::{Body, Response, StatusCode}; /// use std::time::Duration; /// /// // Builds a new server that returns a 404 everywhere except for "/" where it returns the body 'home' /// let mut server = Server::new(|request| { /// if request.uri().path() == "/" { /// Response::builder().body(Body::from("home")).unwrap() /// } else { /// Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap() /// } /// }); /// // We bind the server to localhost on both IPv4 and v6 /// server = server.bind((Ipv4Addr::LOCALHOST, 8080)).bind((Ipv6Addr::LOCALHOST, 8080)); /// // Raise a timeout error if the client does not respond after 10s. /// server = server.with_global_timeout(Duration::from_secs(10)); /// // Limits the number of concurrent connections to 128. /// server = server.with_max_concurrent_connections(128); /// // We spawn the server and block on it /// server.spawn()?.join()?; /// # Result::<_,Box>::Ok(()) /// ``` #[allow(missing_copy_implementations)] pub struct Server { #[allow(clippy::type_complexity)] on_request: Arc) -> Response + Send + Sync + 'static>, socket_addrs: Vec, timeout: Option, server: Option, max_num_thread: Option, } impl Server { /// Builds the server using the given `on_request` method that builds a `Response` from a given `Request`. #[inline] pub fn new( on_request: impl Fn(&mut Request) -> Response + Send + Sync + 'static, ) -> Self { Self { on_request: Arc::new(on_request), socket_addrs: Vec::new(), timeout: None, server: None, max_num_thread: None, } } /// Ask the server to listen to a given socket when spawned. pub fn bind(mut self, addr: impl Into) -> Self { let addr = addr.into(); if !self.socket_addrs.contains(&addr) { self.socket_addrs.push(addr); } self } /// Sets the default value for the [`Server`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.server) header. #[inline] pub fn with_server_name( mut self, server: impl Into, ) -> std::result::Result { self.server = Some(HeaderValue::try_from(server.into())?); Ok(self) } /// Sets the global timeout value (applies to both read and write). #[inline] pub fn with_global_timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } /// Sets the number maximum number of threads this server can spawn. #[inline] pub fn with_max_concurrent_connections(mut self, max_num_thread: usize) -> Self { self.max_num_thread = Some(max_num_thread); self } /// Spawns the server by listening to the given addresses. /// /// Note that this is not blocking. /// To wait for the server to terminate indefinitely, call [`join`](ListeningServer::join) on the result. pub fn spawn(self) -> Result { let timeout = self.timeout; let thread_limit = self.max_num_thread.map(Semaphore::new); let listener_threads = self.socket_addrs .into_iter() .map(|listener_addr| { let listener = TcpListener::bind(listener_addr)?; let thread_name = format!("{listener_addr}: listener thread of OxHTTP"); let thread_limit = thread_limit.clone(); let on_request = Arc::clone(&self.on_request); let server = self.server.clone(); ThreadBuilder::new().name(thread_name).spawn(move || { for stream in listener.incoming() { match stream { Ok(stream) => { let peer_addr = match stream.peer_addr() { Ok(peer) => peer, Err(error) => { eprintln!("OxHTTP TCP error when attempting to get the peer address: {error}"); continue; } }; if let Err(error) = stream.set_nodelay(true) { eprintln!("OxHTTP TCP error when attempting to set the TCP_NODELAY option: {error}"); } let thread_name = format!("{peer_addr}: responding thread of OxHTTP"); let thread_guard = thread_limit.as_ref().map(|s| s.lock()); let on_request = Arc::clone(&on_request); let server = server.clone(); if let Err(error) = ThreadBuilder::new().name(thread_name).spawn( move || { if let Err(error) = accept_request(stream, &*on_request, timeout, &server) { eprintln!( "OxHTTP TCP error when writing response to {peer_addr}: {error}" ) } drop(thread_guard); } ) { eprintln!("OxHTTP thread spawn error: {error}"); } } Err(error) => { eprintln!("OxHTTP TCP error when opening stream: {error}"); } } } }) }) .collect::>>()?; Ok(ListeningServer { threads: listener_threads, }) } } /// Handle to a running server created by [`Server::spawn`]. pub struct ListeningServer { threads: Vec>, } impl ListeningServer { /// Join the server threads and wait for them indefinitely except in case of crash. pub fn join(self) -> Result<()> { for thread in self.threads { thread.join().map_err(|e| { Error::other(if let Ok(e) = e.downcast::<&dyn fmt::Display>() { format!("The server thread panicked with error: {e}") } else { "The server thread panicked with an unknown error".into() }) })?; } Ok(()) } } fn accept_request( mut stream: TcpStream, on_request: &dyn Fn(&mut Request) -> Response, timeout: Option, server: &Option, ) -> Result<()> { stream.set_read_timeout(timeout)?; stream.set_write_timeout(timeout)?; let mut connection_state = ConnectionState::KeepAlive; while connection_state == ConnectionState::KeepAlive { let mut reader = BufReader::with_capacity(BUFFER_CAPACITY, stream.try_clone()?); let (mut response, new_connection_state) = match decode_request_headers(&mut reader, false) { Ok(request) => { // Handles Expect header if let Some(expect) = request.headers_ref().unwrap().get(EXPECT).cloned() { if request .version_ref() .map_or(true, |v| *v >= Version::HTTP_11) && expect.as_bytes().eq_ignore_ascii_case(b"100-continue") { stream.write_all(b"HTTP/1.1 100 Continue\r\n\r\n")?; read_body_and_build_response(request, reader, on_request) } else { ( build_text_response( StatusCode::EXPECTATION_FAILED, format!( "Expect header value '{}' is not supported.", String::from_utf8_lossy(expect.as_ref()) ), ), ConnectionState::Close, ) } } else { read_body_and_build_response(request, reader, on_request) } } Err(error) => { if error.kind() == ErrorKind::ConnectionAborted { return Ok(()); // The client is disconnected. Let's ignore this error and do not try to write an answer that won't be received. } else { (build_error(error), ConnectionState::Close) } } }; connection_state = new_connection_state; // Additional headers if let Some(server) = server { response .headers_mut() .entry(SERVER) .or_insert_with(|| server.clone()); } stream = encode_response( &mut response, BufWriter::with_capacity(BUFFER_CAPACITY, stream), )? .into_inner() .map_err(|e| e.into_error())?; } Ok(()) } #[derive(Eq, PartialEq, Debug, Copy, Clone)] enum ConnectionState { Close, KeepAlive, } fn read_body_and_build_response( request: RequestBuilder, reader: BufReader, on_request: &dyn Fn(&mut Request) -> Response, ) -> (Response, ConnectionState) { match decode_request_body(request, reader) { Ok(mut request) => { let response = on_request(&mut request); // We make sure to finish reading the body if let Err(error) = copy(request.body_mut(), &mut sink()) { (build_error(error), ConnectionState::Close) // TODO: ignore? } else { let connection_state = request .headers() .get(CONNECTION) .and_then(|v| { v.as_bytes() .eq_ignore_ascii_case(b"close") .then_some(ConnectionState::Close) }) .unwrap_or_else(|| { if request.version() <= Version::HTTP_10 { ConnectionState::Close } else { ConnectionState::KeepAlive } }); (response, connection_state) } } Err(error) => (build_error(error), ConnectionState::Close), } } fn build_error(error: Error) -> Response { build_text_response( match error.kind() { ErrorKind::TimedOut => StatusCode::REQUEST_TIMEOUT, ErrorKind::InvalidData => StatusCode::BAD_REQUEST, _ => StatusCode::INTERNAL_SERVER_ERROR, }, error.to_string(), ) } fn build_text_response(status: StatusCode, text: String) -> Response { Response::builder() .status(status) .header(CONTENT_TYPE, "text/plain; charset=utf-8") .body(Body::from(text)) .unwrap() } /// Dumb semaphore allowing to overflow capacity #[derive(Clone)] struct Semaphore { inner: Arc, } struct InnerSemaphore { count: Mutex, capacity: usize, condvar: Condvar, } impl Semaphore { fn new(capacity: usize) -> Self { Self { inner: Arc::new(InnerSemaphore { count: Mutex::new(0), capacity, condvar: Condvar::new(), }), } } fn lock(&self) -> SemaphoreGuard { let data = &self.inner; *data .condvar .wait_while(data.count.lock().unwrap(), |count| *count >= data.capacity) .unwrap() += 1; SemaphoreGuard { inner: Arc::clone(&self.inner), } } } struct SemaphoreGuard { inner: Arc, } impl Drop for SemaphoreGuard { fn drop(&mut self) { let data = &self.inner; *data.count.lock().unwrap() -= 1; data.condvar.notify_one(); } } #[cfg(test)] mod tests { use super::*; use std::io::Read; use std::net::{Ipv4Addr, Ipv6Addr}; use std::thread::sleep; #[test] fn test_regular_http_operations() -> Result<()> { test_server("localhost", 9999, [ "GET / HTTP/1.1\nhost: localhost:9999\n\n", "POST /foo HTTP/1.1\nhost: localhost:9999\nexpect: 100-continue\nconnection:close\ncontent-length:4\n\nabcd", ], [ "HTTP/1.1 200 OK\r\nserver: OxHTTP/1.0\r\ncontent-length: 4\r\n\r\nhome", "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 404 Not Found\r\nserver: OxHTTP/1.0\r\ncontent-length: 0\r\n\r\n" ]) } #[test] fn test_bad_request() -> Result<()> { test_server( "::1", 9998, ["GET / HTTP/1.1\nhost: localhost:9999\nfoo\n\n"], ["HTTP/1.1 400 Bad Request\r\ncontent-type: text/plain; charset=utf-8\r\nserver: OxHTTP/1.0\r\ncontent-length: 19\r\n\r\ninvalid header name"], ) } #[test] fn test_bad_expect() -> Result<()> { test_server( "127.0.0.1", 9997, ["GET / HTTP/1.1\nhost: localhost:9999\nexpect: bad\n\n"], ["HTTP/1.1 417 Expectation Failed\r\ncontent-type: text/plain; charset=utf-8\r\nserver: OxHTTP/1.0\r\ncontent-length: 43\r\n\r\nExpect header value 'bad' is not supported."], ) } fn test_server( request_host: &'static str, server_port: u16, requests: impl IntoIterator, responses: impl IntoIterator, ) -> Result<()> { Server::new(|request| { if request.uri().path() == "/" { Response::builder().body(Body::from("home")).unwrap() } else { Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()) .unwrap() } }) .bind((Ipv4Addr::LOCALHOST, server_port)) .bind((Ipv6Addr::LOCALHOST, server_port)) .with_server_name("OxHTTP/1.0") .unwrap() .with_global_timeout(Duration::from_secs(1)) .spawn()?; sleep(Duration::from_millis(100)); // Makes sure the server is up let mut stream = TcpStream::connect((request_host, server_port))?; for (request, response) in requests.into_iter().zip(responses) { stream.write_all(request.as_bytes())?; let mut output = vec![b'\0'; response.len()]; stream.read_exact(&mut output)?; assert_eq!(String::from_utf8(output).unwrap(), response); } Ok(()) } #[test] fn test_thread_limit() -> Result<()> { let server_port = 9996; let request = b"GET / HTTP/1.1\nhost: localhost:9999\n\n"; let response = b"HTTP/1.1 200 OK\r\nserver: OxHTTP/1.0\r\ncontent-length: 4\r\n\r\nhome"; Server::new(|_| Response::builder().body(Body::from("home")).unwrap()) .bind((Ipv4Addr::LOCALHOST, server_port)) .bind((Ipv6Addr::LOCALHOST, server_port)) .with_server_name("OxHTTP/1.0") .unwrap() .with_global_timeout(Duration::from_secs(1)) .with_max_concurrent_connections(2) .spawn()?; sleep(Duration::from_millis(100)); // Makes sure the server is up let streams = (0..128) .map(|_| { let mut stream = TcpStream::connect(("localhost", server_port))?; stream.write_all(request)?; Ok(stream) }) .collect::>>()?; for mut stream in streams { let mut output = vec![b'\0'; response.len()]; stream.read_exact(&mut output)?; assert_eq!(output, response); } Ok(()) } } oxigraph-oxhttp-8a5ee02/src/utils.rs000066400000000000000000000005361502064175100175740ustar00rootroot00000000000000use std::error::Error; use std::io; #[inline] pub fn invalid_data_error(error: impl Into>) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, error) } #[inline] pub fn invalid_input_error(error: impl Into>) -> io::Error { io::Error::new(io::ErrorKind::InvalidInput, error) } oxigraph-oxhttp-8a5ee02/typos.toml000066400000000000000000000000721502064175100173450ustar00rootroot00000000000000[default.extend-words] flate = "flate" referer = "referer"