pax_global_header00006660000000000000000000000064144714602120014513gustar00rootroot0000000000000052 comment=3e78df3ae3d972af4bae1b43bb6553297957fcb4 oxhttp-0.1.7/000077500000000000000000000000001447146021200130465ustar00rootroot00000000000000oxhttp-0.1.7/.github/000077500000000000000000000000001447146021200144065ustar00rootroot00000000000000oxhttp-0.1.7/.github/dependabot.yml000066400000000000000000000002711447146021200172360ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly oxhttp-0.1.7/.github/workflows/000077500000000000000000000000001447146021200164435ustar00rootroot00000000000000oxhttp-0.1.7/.github/workflows/build.yml000066400000000000000000000054371447146021200202760ustar00rootroot00000000000000name: build on: - pull_request jobs: fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: rustup update && rustup component add rustfmt - run: cargo fmt -- --check clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: rustup update && rustup component add clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets - run: cargo clippy --all-targets --features native-tls - run: cargo clippy --all-targets --features rustls - run: cargo clippy --all-targets --features rayon clippy_msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: rustup update && rustup override set 1.60.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 -- -D warnings -D clippy::all - run: cargo clippy --all-targets --features rayon -- -D warnings -D clippy::all test: strategy: matrix: os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo test --verbose --all-targets - run: cargo test --verbose --all-targets --features native-tls - run: cargo test --verbose --all-targets --features rustls - run: cargo test --verbose --all-targets --features rayon env: RUST_BACKTRACE: 1 rustdoc: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo doc --all-features --no-deps rustdoc_msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: rustup update && rustup override set 1.60.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@v3 - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo install cargo-deny || true - run: cargo deny check semver_checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: true - run: rustup update - uses: Swatinem/rust-cache@v2 - run: cargo install cargo-semver-checks || true - run: cargo semver-checks check-release typos: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 - run: cargo install typos-cli || true - run: typos oxhttp-0.1.7/.gitignore000066400000000000000000000000231447146021200150310ustar00rootroot00000000000000/target Cargo.lock oxhttp-0.1.7/CHANGELOG.md000066400000000000000000000042501447146021200146600ustar00rootroot00000000000000# Changelog ## [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.oxhttp-0.1.7/Cargo.toml000066400000000000000000000014551447146021200150030ustar00rootroot00000000000000[package] name = "oxhttp" version = "0.1.7" authors = ["Tpt "] license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["HTTP"] repository = "https://github.com/oxigraph/oxhttp" description = """ Very simple implementation of HTTP 1.1 (both client and server) """ edition = "2021" rust-version = "1.60" [dependencies] httparse = "1" lazy_static = "1" native-tls = { version = "0.2", optional = true } rayon-core = { version = "1", optional = true } rustls-crate = { version = "0.21", optional = true, package = "rustls" } rustls-native-certs = { version = "0.6", optional = true } url = "2" [features] default = ["client", "server"] rustls = ["rustls-crate", "rustls-native-certs"] rayon = ["rayon-core"] client = [] server = [] [package.metadata.docs.rs] all-features = true oxhttp-0.1.7/LICENSE-APACHE000066400000000000000000000251371447146021200150020ustar00rootroot00000000000000 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. oxhttp-0.1.7/LICENSE-MIT000066400000000000000000000020471447146021200145050ustar00rootroot00000000000000Copyright (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. oxhttp-0.1.7/README.md000066400000000000000000000054511447146021200143320ustar00rootroot00000000000000OxHTTP ====== [![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 naive 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 `native-tls` feature (to use the current system native implementation) or `rustls` feature (to use [Rustls](https://github.com/rustls/rustls)). Example: ```rust use oxhttp::Client; use oxhttp::model::{Request, Method, Status, HeaderName}; use std::io::Read; let client = Client::new(); let response = client.request(Request::builder(Method::GET, "http://example.com".parse().unwrap()).build()).unwrap(); assert_eq!(response.status(), Status::OK); assert_eq!(response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8"); 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 oxhttp::Server; use oxhttp::model::{Response, Status}; 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.url().path() == "/" { Response::builder(Status::OK).with_body("home") } else { Response::builder(Status::NOT_FOUND).build() } }); // Raise a timeout error if the client does not respond after 10s. server.set_global_timeout(Duration::from_secs(10)); // Listen to localhost:8080 server.listen(("localhost", 8080)).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. oxhttp-0.1.7/deny.toml000066400000000000000000000002161447146021200147010ustar00rootroot00000000000000[licenses] unlicensed = "deny" allow = [ "MIT", "Apache-2.0" ] default = "deny" [bans] multiple-versions = "warn" wildcards = "deny" oxhttp-0.1.7/src/000077500000000000000000000000001447146021200136355ustar00rootroot00000000000000oxhttp-0.1.7/src/client.rs000066400000000000000000000341121447146021200154620ustar00rootroot00000000000000use crate::io::{decode_response, encode_request}; use crate::model::{ HeaderName, HeaderValue, InvalidHeader, Method, Request, Response, Status, Url, }; use crate::utils::{invalid_data_error, invalid_input_error}; #[cfg(any(feature = "native-tls", feature = "rustls"))] use lazy_static::lazy_static; #[cfg(feature = "native-tls")] use native_tls::TlsConnector; #[cfg(feature = "rustls")] use rustls_crate::{ClientConfig, ClientConnection, RootCertStore, ServerName, StreamOwned}; #[cfg(feature = "rustls")] use rustls_native_certs::load_native_certs; use std::io::{BufReader, BufWriter, Error, ErrorKind, Result}; use std::net::{SocketAddr, TcpStream}; #[cfg(feature = "rustls")] use std::sync::Arc; use std::time::Duration; #[cfg(feature = "rustls")] lazy_static! { static ref RUSTLS_CONFIG: Arc = { let mut root_store = RootCertStore::empty(); match load_native_certs() { Ok(certs) => { for cert in certs { root_store.add_parsable_certificates(&[cert.0]); } } Err(e) => panic!("Error loading TLS certificates: {}", e), } Arc::new( ClientConfig::builder() .with_safe_defaults() .with_root_certificates(root_store) .with_no_client_auth(), ) }; } #[cfg(feature = "native-tls")] lazy_static! { static ref TLS_CONNECTOR: TlsConnector = { match TlsConnector::new() { Ok(connector) => connector, Err(e) => panic!("Error while loading TLS configuration: {}", e), } }; } /// 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 `native-tls` feature (to use the current system native implementation) or `rustls` feature (to use [Rustls](https://github.com/rustls/rustls)). /// /// The client does not follow redirections by default. Use [`Client::set_redirection_limit`] to set a limit to the number of consecutive redirections the server should follow. /// /// Missing: HSTS support, authentication and keep alive. /// /// ``` /// use oxhttp::Client; /// use oxhttp::model::{Request, Method, Status, HeaderName}; /// use std::io::Read; /// /// let client = Client::new(); /// let response = client.request(Request::builder(Method::GET, "http://example.com".parse()?).build())?; /// assert_eq!(response.status(), Status::OK); /// assert_eq!(response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8"); /// 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 set_global_timeout(&mut self, timeout: Duration) { self.timeout = Some(timeout); } /// 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 set_user_agent( &mut self, user_agent: impl Into, ) -> std::result::Result<(), InvalidHeader> { self.user_agent = Some(HeaderValue::try_from(user_agent.into())?); Ok(()) } /// Sets the number of time a redirection should be followed. /// By default the redirections are not followed (limit = 0). #[inline] pub fn set_redirection_limit(&mut self, limit: usize) { self.redirection_limit = limit; } pub fn request(&self, mut request: Request) -> Result { // 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)?; if let Some(location) = response.header(&HeaderName::LOCATION) { let new_method = match response.status() { Status::MOVED_PERMANENTLY | Status::FOUND | Status::SEE_OTHER => { if previous_method == Method::HEAD { Method::HEAD } else { Method::GET } } Status::TEMPORARY_REDIRECT | Status::PERMANENT_REDIRECT if previous_method.is_safe() => { previous_method } _ => return Ok(response), }; let location = location.to_str().map_err(invalid_data_error)?; let new_url = request.url().join(location).map_err(|e| { invalid_data_error(format!( "Invalid URL in Location header raising error {e}: {location}" )) })?; let mut request_builder = Request::builder(new_method, new_url); for (header_name, header_value) in request.headers() { request_builder .headers_mut() .set(header_name.clone(), header_value.clone()); } request = request_builder.build(); } else { return Ok(response); } } Err(Error::new( ErrorKind::Other, format!( "The server requested too many redirects ({}). The latest redirection target is {}", self.redirection_limit + 1, request.url() ), )) } #[allow(unreachable_code, clippy::needless_return)] fn single_request(&self, request: &mut Request) -> Result { // Additional headers set_header_fallback(request, HeaderName::USER_AGENT, &self.user_agent); request .headers_mut() .set(HeaderName::CONNECTION, HeaderValue::new_unchecked("close")); #[cfg(any(feature = "native-tls", feature = "rustls"))] let host = request .url() .host_str() .ok_or_else(|| invalid_input_error("No host provided"))?; match request.url().scheme() { "http" => { let addresses = get_and_validate_socket_addresses(request.url(), 80)?; let mut stream = self.connect(&addresses)?; encode_request(request, BufWriter::new(&mut stream))?; decode_response(BufReader::new(stream)) } "https" => { #[cfg(feature = "native-tls")] { let addresses = get_and_validate_socket_addresses(request.url(), 443)?; let stream = self.connect(&addresses)?; let mut stream = TLS_CONNECTOR .connect(host, stream) .map_err(|e| Error::new(ErrorKind::Other, e))?; encode_request(request, BufWriter::new(&mut stream))?; return decode_response(BufReader::new(stream)); } #[cfg(feature = "rustls")] { let addresses = get_and_validate_socket_addresses(request.url(), 443)?; let dns_name = ServerName::try_from(host).map_err(invalid_input_error)?; let connection = ClientConnection::new(RUSTLS_CONFIG.clone(), dns_name) .map_err(|e| Error::new(ErrorKind::Other, e))?; let mut stream = StreamOwned::new(connection, self.connect(&addresses)?); encode_request(request, BufWriter::new(&mut stream))?; return decode_response(BufReader::new(stream)); } #[cfg(not(any(feature = "native-tls", feature = "rustls")))] 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: {}", request.url().scheme() ))), } } fn connect(&self, addresses: &[SocketAddr]) -> Result { let stream = if let Some(timeout) = self.timeout { addresses.iter().fold( Err(Error::new( ErrorKind::InvalidInput, "Not able to resolve the provide addresses", )), |e, addr| match e { Ok(stream) => Ok(stream), Err(_) => TcpStream::connect_timeout(addr, timeout), }, ) } else { TcpStream::connect(addresses) }?; stream.set_read_timeout(self.timeout)?; stream.set_write_timeout(self.timeout)?; Ok(stream) } } // 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(url: &Url, default_port: u16) -> Result> { let addresses = url.socket_addrs(|| Some(default_port))?; 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 set_header_fallback( request: &mut Request, header_name: HeaderName, header_value: &Option, ) { if let Some(header_value) = header_value { if !request.headers().contains(&header_name) { request.headers_mut().set(header_name, header_value.clone()) } } } #[cfg(test)] mod tests { use super::*; use crate::model::{Method, Status}; #[test] fn test_http_get_ok() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder(Method::GET, "http://example.com".parse().unwrap()).build(), )?; assert_eq!(response.status(), Status::OK); assert_eq!( response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8" ); Ok(()) } #[test] fn test_http_get_ok_with_user_agent_and_timeout() -> Result<()> { let mut client = Client::new(); client.set_user_agent("OxHTTP/1.0").unwrap(); client.set_global_timeout(Duration::from_secs(5)); let response = client.request( Request::builder(Method::GET, "http://example.com".parse().unwrap()).build(), )?; assert_eq!(response.status(), Status::OK); assert_eq!( response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8" ); Ok(()) } #[test] fn test_http_get_ok_explicit_port() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder(Method::GET, "http://example.com:80".parse().unwrap()).build(), )?; assert_eq!(response.status(), Status::OK); assert_eq!( response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8" ); Ok(()) } #[test] fn test_http_wrong_port() { let client = Client::new(); assert!(client .request( Request::builder(Method::GET, "http://example.com:22".parse().unwrap()).build(), ) .is_err()); } #[cfg(any(feature = "native-tls", feature = "rustls"))] #[test] fn test_https_get_ok() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder(Method::GET, "https://example.com".parse().unwrap()).build(), )?; assert_eq!(response.status(), Status::OK); assert_eq!( response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"text/html; charset=UTF-8" ); Ok(()) } #[cfg(not(any(feature = "native-tls", feature = "rustls")))] #[test] fn test_https_get_err() { let client = Client::new(); assert!(client .request(Request::builder(Method::GET, "https://example.com".parse().unwrap()).build()) .is_err()); } #[test] fn test_http_get_not_found() -> Result<()> { let client = Client::new(); let response = client.request( Request::builder( Method::GET, "http://example.com/not_existing".parse().unwrap(), ) .build(), )?; assert_eq!(response.status(), Status::NOT_FOUND); Ok(()) } #[test] fn test_file_get_error() { let client = Client::new(); assert!(client .request( Request::builder( Method::GET, "file://example.com/not_existing".parse().unwrap(), ) .build(), ) .is_err()); } #[cfg(any(feature = "native-tls", feature = "rustls"))] #[test] fn test_redirection() -> Result<()> { let mut client = Client::new(); client.set_redirection_limit(5); let response = client.request( Request::builder(Method::GET, "http://wikipedia.org".parse().unwrap()).build(), )?; assert_eq!(response.status(), Status::OK); Ok(()) } } oxhttp-0.1.7/src/io/000077500000000000000000000000001447146021200142445ustar00rootroot00000000000000oxhttp-0.1.7/src/io/decoder.rs000066400000000000000000000544421447146021200162300ustar00rootroot00000000000000use crate::model::{ Body, ChunkedTransferPayload, HeaderName, HeaderValue, Headers, Method, Request, RequestBuilder, Response, Status, Url, }; use crate::utils::invalid_data_error; use std::cmp::min; use std::io::{BufRead, Error, ErrorKind, Read, Result}; use std::str; 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", )); } let method = Method::from_str( parsed_request .method .ok_or_else(|| invalid_data_error("No method in the HTTP request"))?, ) .map_err(invalid_data_error)?; let path = parsed_request .path .ok_or_else(|| invalid_data_error("No path in the HTTP request"))?; let url = if let Some(host) = parsed_request.headers.iter().find_map(|header| { if header.name.eq_ignore_ascii_case("host") { Some(header.value) } else { None } }) { let host = str::from_utf8(host) .map_err(|e| invalid_data_error(format!("Invalid host header value: {e}")))?; let base_url = Url::parse(&if is_connection_secure { format!("https://{host}") } else { format!("http://{host}") }) .map_err(|e| invalid_data_error(format!("Invalid host header value '{host}': {e}")))?; if path == "*" { base_url } else { base_url .join(path) .map_err(|e| invalid_data_error(format!("Invalid request path '{path}': {e}")))? } } else { Url::parse(path).map_err(|e| { invalid_data_error(format!( "No host header in HTTP request and not absolute path '{path}': {e}" )) })? }; // We validate that the URL is valid if !url.has_authority() { return Err(invalid_data_error("No host header in HTTP request")); } if is_connection_secure { if url.scheme() != "https" { return Err(invalid_data_error("The HTTPS URL scheme should be 'https")); } } else if url.scheme() != "http" { return Err(invalid_data_error("The HTTP URL scheme should be 'http")); } let mut request = Request::builder(method, url); for header in parsed_request.headers { request.headers_mut().append( HeaderName::new_unchecked(header.name.to_ascii_lowercase()), HeaderValue::new_unchecked(header.value), ); } if parsed_request.version == Some(0) { // Hack to fallback to default HTTP 1.0 behavior of closing connections if !request.headers().contains(&HeaderName::CONNECTION) { request .headers_mut() .append(HeaderName::CONNECTION, HeaderValue::new_unchecked("close")) } } Ok(request) } pub fn decode_request_body( request: RequestBuilder, reader: impl BufRead + 'static, ) -> Result { let body = decode_body(request.headers(), reader)?; Ok(request.with_body(body)) } 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 = Status::try_from( 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); for header in parsed_response.headers { response.headers_mut().append( HeaderName::new_unchecked(header.name.to_ascii_lowercase()), HeaderValue::new_unchecked(header.value), ); } let body = decode_body(response.headers(), reader)?; Ok(response.with_body(body)) } 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: &Headers, reader: impl BufRead + 'static) -> Result { let content_length = headers.get(&HeaderName::CONTENT_LENGTH); let transfer_encoding = headers.get(&HeaderName::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", )); } Ok(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 { let transfer_encoding = transfer_encoding.to_str().map_err(invalid_data_error)?; if transfer_encoding.eq_ignore_ascii_case("chunked") { Body::from_chunked_transfer_payload(ChunkedDecoder { reader, buffer: Vec::with_capacity(DEFAULT_SIZE), is_start: true, chunk_position: 1, chunk_size: 1, trailers: None, }) } else { return Err(invalid_data_error(format!( "Transfer-Encoding: {transfer_encoding} is not supported" ))); } } else { Body::default() }) } 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 { // 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; self.chunk_size = if let Ok(httparse::Status::Complete((read, chunk_size))) = httparse::parse_chunk_size(&self.buffer) { if read != self.buffer.len() { return Err(invalid_data_error("Chunked header containing a line jump")); } chunk_size.try_into().map_err(invalid_data_error)? } else { return Err(invalid_data_error("Invalid chunked header")); }; 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]; if let httparse::Status::Complete((read, parsed_trailers)) = httparse::parse_headers(&self.buffer[1..], &mut trailers) .map_err(invalid_data_error)? { if read != self.buffer.len() - 1 { return Err(invalid_data_error( "Invalid data at the end of the trailer section", )); } let mut trailers = Headers::new(); for trailer in parsed_trailers { trailers.append( HeaderName::new_unchecked(trailer.name.to_ascii_lowercase()), HeaderValue::new_unchecked(trailer.value), ); } self.trailers = Some(trailers); } else { return Err(invalid_data_error( "Partial HTTP headers containing two line jumps", )); } return Ok(0); } } } } impl ChunkedTransferPayload for ChunkedDecoder { fn trailers(&self) -> Option<&Headers> { self.trailers.as_ref() } } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; use std::ops::Deref; #[test] fn decode_request_target_origin_form() -> Result<()> { let request = decode_request_headers( &mut Cursor::new("GET /where?q=now HTTP/1.1\nHost: www.example.org\n\n"), false, )?; assert_eq!(request.url().as_str(), "http://www.example.org/where?q=now"); Ok(()) } #[test] fn decode_request_target_absolute_form_with_host() -> Result<()> { let request = decode_request_headers( &mut Cursor::new( "GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\nHost: example.com\n\n", ), false, )?; assert_eq!( request.url().as_str(), "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 Cursor::new("GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n"), false, )?; assert_eq!( request.url().as_str(), "http://www.example.org/pub/WWW/TheProject.html" ); Ok(()) } #[test] fn decode_request_target_relative_form_without_host() { assert!(decode_request_headers( &mut Cursor::new("GET /pub/WWW/TheProject.html HTTP/1.1\n\n"), false, ) .is_err()); } #[test] fn decode_request_target_absolute_form_wrong_scheme() { assert!(decode_request_headers( &mut Cursor::new("GET https://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n"), false, ) .is_err()); assert!(decode_request_headers( &mut Cursor::new("GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1\n\n"), true, ) .is_err()); } #[test] fn decode_invalid_request_target_relative_form_with_host() { assert!(decode_request_headers( &mut Cursor::new("GET /foo Result<()> { let request = decode_request_headers( &mut Cursor::new("OPTIONS * HTTP/1.1\nHost: www.example.org:8001\n\n"), false, )?; assert_eq!(request.url().as_str(), "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 Cursor::new( "GET / HTTP/1.1\nHost: www.example.org:8001\nFoo: v1\nbar: vbar\nfoo: v2\n\n", ), true, )?; assert_eq!(request.url().as_str(), "https://www.example.org:8001/"); assert_eq!( request .header(&HeaderName::from_str("foo").unwrap()) .unwrap() .as_ref(), b"v1, v2".as_ref() ); assert_eq!( request .header(&HeaderName::from_str("Bar").unwrap()) .unwrap() .as_ref(), b"vbar".as_ref() ); Ok(()) } #[test] fn decode_request_with_body() -> Result<()> { let mut read = Cursor::new( "GET / HTTP/1.1\nHost: www.example.org:8001\ncontent-length: 9\n\nfoobarbar", ); 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 Cursor::new("GET / HTTP/1.1\nHost: www.example.org:8001\n: foo"), false ) .is_err()); } #[test] fn decode_request_invalid_header_name_char() { assert!(decode_request_headers( &mut Cursor::new("GET / HTTP/1.1\nHost: www.example.org:8001\nConté: foo"), false ) .is_err()); } #[test] fn decode_request_invalid_header_value_char() { assert!(decode_request_headers( &mut Cursor::new( "GET / HTTP/1.1\nHost: www.example.org:8001\nCont\t: foo\rbar\r\nTest: test" ), false ) .is_err()); } #[test] fn decode_request_empty() { assert_eq!( decode_request_headers(&mut Cursor::new(""), false) .err() .map(|e| e.kind()), Some(ErrorKind::ConnectionAborted) ); } #[test] fn decode_request_stop_in_header() { assert_eq!( decode_request_headers(&mut Cursor::new("GET /\r\n"), false) .err() .map(|e| e.kind()), Some(ErrorKind::ConnectionAborted) ); } #[test] fn decode_request_stop_in_body() -> Result<()> { let mut read = Cursor::new("POST / HTTP/1.1\r\nhost: example.com\r\ncontent-length: 12\r\n\r\nfoobar"); 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 = Cursor::new("POST http://example.com/foo HTTP/1.0\r\ncontent-length: 12\r\n\r\nfoobar"); let request = decode_request_body(decode_request_headers(&mut read, false)?, read)?; assert_eq!(request.url().as_str(), "http://example.com/foo"); assert_eq!( request.header(&HeaderName::CONNECTION).unwrap().deref(), b"close" ); Ok(()) } #[test] fn decode_request_unsupported_transfer_encoding() -> Result<()> { let mut read = Cursor::new("POST / HTTP/1.1\r\nhost: example.com\r\ncontent-length: 12\r\ntransfer-encoding: foo\r\n\r\nfoobar"); 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(Cursor::new("HTTP/1.1 404 Not Found\r\n\r\n"))?; assert_eq!(response.status(), Status::NOT_FOUND); assert_eq!(response.body().len(), Some(0)); Ok(()) } #[test] fn decode_response_with_fixed_payload() -> Result<()> { let response = decode_response(Cursor::new( "HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length:12\r\n\r\ntestbodybody", ))?; assert_eq!(response.status(), Status::OK); assert_eq!( response .header(&HeaderName::CONTENT_TYPE) .unwrap() .to_str() .unwrap(), "text/plain" ); assert_eq!(response.into_body().to_string()?, "testbodybody"); Ok(()) } #[test] fn decode_response_with_chunked_payload() -> Result<()> { let response = decode_response(Cursor::new( "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", ))?; assert_eq!(response.status(), Status::OK); assert_eq!( response .header(&HeaderName::CONTENT_TYPE) .unwrap() .to_str() .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(Cursor::new( "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", ))?; assert_eq!(response.status(), Status::OK); assert_eq!( response .header(&HeaderName::CONTENT_TYPE) .unwrap() .to_str() .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_str("test").unwrap()) .unwrap() .as_ref(), b"foo" ); Ok(()) } #[test] fn decode_response_with_invalid_chunk_header() -> Result<()> { let response = decode_response(Cursor::new( "HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\nh\r\nWiki\r\n0\r\n\r\n", ))?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_with_invalid_trailer() -> Result<()> { let response = decode_response(Cursor::new( "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", ))?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_with_not_ended_trailer() -> Result<()> { let response = decode_response(Cursor::new( "HTTP/1.1 200 OK\r\ntransfer-encoding:chunked\r\n\r\nf\r\nWiki", ))?; assert!(response.into_body().to_string().is_err()); Ok(()) } #[test] fn decode_response_empty_header_name() { assert!(decode_response(Cursor::new( "HTTP/1.1 200 OK\nHost: www.example.org:8001\n: foo" )) .is_err()); } #[test] fn decode_response_invalid_header_name_char() { assert!(decode_response(Cursor::new( "HTTP/1.1 200 OK\nHost: www.example.org:8001\nConté: foo" )) .is_err()); } #[test] fn decode_response_invalid_header_value_char() { assert!(decode_response(Cursor::new( "HTTP/1.1 200 OK\nHost: www.example.org:8001\nCont\t: foo\rbar\r\nTest: test" )) .is_err()); } #[test] fn decode_response_empty() { assert!(decode_response(Cursor::new("")).is_err()); } #[test] fn decode_response_stop_in_header() { assert!(decode_response(Cursor::new("HTTP/1.1 404 Not Found\r\n")).is_err()); } #[test] fn decode_response_stop_in_body() -> Result<()> { assert!(decode_response(Cursor::new( "HTTP/1.1 200 OK\r\ncontent-length: 12\r\n\r\nfoobar" ))? .into_body() .to_vec() .is_err()); Ok(()) } #[test] fn decode_response_content_length_and_transfer_encoding() { assert!(decode_response( Cursor::new("HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ntransfer-encoding:chunked\r\ncontent-length: 222\r\n\r\n")).is_err()); } } oxhttp-0.1.7/src/io/encoder.rs000066400000000000000000000227211447146021200162350ustar00rootroot00000000000000use crate::model::{Body, HeaderName, Headers, Method, Request, Response, Status}; use crate::utils::invalid_input_error; use std::io::{copy, Read, Result, Write}; pub fn encode_request(request: &mut Request, mut writer: impl Write) -> Result<()> { if !request.url().username().is_empty() || request.url().password().is_some() { return Err(invalid_input_error( "Username and password are not allowed in HTTP URLs", )); } let host = request .url() .host_str() .ok_or_else(|| invalid_input_error("No host provided"))?; if let Some(query) = request.url().query() { write!( &mut writer, "{} {}?{} HTTP/1.1\r\n", request.method(), request.url().path(), query )?; } else { write!( &mut writer, "{} {} HTTP/1.1\r\n", request.method(), request.url().path(), )?; } // host if let Some(port) = request.url().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)?; writer.flush() } pub fn encode_response(response: &mut Response, mut writer: impl Write) -> Result<()> { write!(&mut writer, "HTTP/1.1 {}\r\n", response.status())?; 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)?; writer.flush() } fn encode_headers(headers: &Headers, writer: &mut impl Write) -> Result<()> { for (name, value) in headers { if !is_forbidden_name(name) { write!(writer, "{name}: ")?; writer.write_all(value)?; 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.as_ref() == "accept-charset" || *header == HeaderName::ACCEPT_ENCODING || header.as_ref() == "access-control-request-headers" || header.as_ref() == "access-control-request-method" || *header == HeaderName::CONNECTION || *header == HeaderName::CONTENT_LENGTH || *header == HeaderName::DATE || *header == HeaderName::EXPECT || *header == HeaderName::HOST || header.as_ref() == "keep-alive" || header.as_ref() == "origin" || *header == HeaderName::TE || *header == HeaderName::TRAILER || *header == HeaderName::TRANSFER_ENCODING || *header == HeaderName::UPGRADE || *header == HeaderName::VIA } fn does_request_must_include_body(method: &Method) -> bool { *method == Method::POST || *method == Method::PUT } fn does_response_must_include_body(status: Status) -> bool { !(status.is_informational() || status == Status::NO_CONTENT || status == Status::NOT_MODIFIED) } #[cfg(test)] mod tests { use super::*; use crate::model::{ChunkedTransferPayload, Headers, Method, Status}; use std::io::Cursor; use std::str; #[test] fn user_password_not_allowed_in_request() { let mut buffer = Vec::new(); assert!(encode_request( &mut Request::builder(Method::GET, "http://foo@example.com/".parse().unwrap()).build(), &mut buffer ) .is_err()); assert!(encode_request( &mut Request::builder(Method::GET, "http://foo:bar@example.com/".parse().unwrap()) .build(), &mut buffer ) .is_err()); } #[test] fn encode_get_request() -> Result<()> { let mut request = Request::builder( Method::GET, "http://example.com:81/foo/bar?query#fragment" .parse() .unwrap(), ) .with_header(HeaderName::ACCEPT, "application/json") .unwrap() .build(); let mut buffer = Vec::new(); encode_request(&mut request, &mut buffer)?; 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::POST, "http://example.com/foo/bar?query#fragment".parse().unwrap(), ) .with_header(HeaderName::ACCEPT, "application/json") .unwrap() .with_body(b"testbodybody".as_ref()); let mut buffer = Vec::new(); encode_request(&mut request, &mut buffer)?; 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::POST, "http://example.com/foo/bar?query#fragment".parse().unwrap(), ) .build(); let mut buffer = Vec::new(); encode_request(&mut request, &mut buffer)?; 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 = Headers::new(); trailers.append(HeaderName::CONTENT_LANGUAGE, "foo".parse().unwrap()); let mut request = Request::builder( Method::POST, "http://example.com/foo/bar?query#fragment".parse().unwrap(), ) .with_body(Body::from_chunked_transfer_payload(SimpleTrailers { read: Cursor::new("testbodybody"), trailers, })); let mut buffer = Vec::new(); encode_request(&mut request, &mut buffer)?; 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(Status::OK) .with_header(HeaderName::ACCEPT, "application/json") .unwrap() .with_body("test test2"); let mut buffer = Vec::new(); encode_response(&mut response, &mut buffer)?; 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::NOT_FOUND).build(); let mut buffer = Vec::new(); encode_response(&mut response, &mut buffer)?; 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::try_from(499).unwrap()).build(); let mut buffer = Vec::new(); encode_response(&mut response, &mut buffer)?; assert_eq!( str::from_utf8(&buffer).unwrap(), "HTTP/1.1 499 \r\ncontent-length: 0\r\n\r\n" ); Ok(()) } struct SimpleTrailers { read: Cursor<&'static str>, trailers: Headers, } impl Read for SimpleTrailers { fn read(&mut self, buf: &mut [u8]) -> Result { self.read.read(buf) } } impl ChunkedTransferPayload for SimpleTrailers { fn trailers(&self) -> Option<&Headers> { Some(&self.trailers) } } } oxhttp-0.1.7/src/io/mod.rs000066400000000000000000000002401447146021200153650ustar00rootroot00000000000000mod decoder; mod encoder; pub use decoder::{decode_request_body, decode_request_headers, decode_response}; pub use encoder::{encode_request, encode_response}; oxhttp-0.1.7/src/lib.rs000066400000000000000000000012531447146021200147520ustar00rootroot00000000000000#![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(all(target_feature = "native-tls", target_feature = "rustls"))] compile_error!( "Both `native-tls` and `rustls` options of oxhttp can't be enabled at the same time" ); #[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::Server; oxhttp-0.1.7/src/model/000077500000000000000000000000001447146021200147355ustar00rootroot00000000000000oxhttp-0.1.7/src/model/body.rs000066400000000000000000000155411447146021200162460ustar00rootroot00000000000000use crate::model::Headers; use std::cmp::min; 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), } 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))) } /// 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, } } /// 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<&Headers> { match &self.0 { BodyAlt::SimpleOwned(_) | BodyAlt::SimpleBorrowed(_) | BodyAlt::Sized { .. } => None, BodyAlt::Chunked(c) => c.trailers(), } } /// Reads the full body into a vector. /// /// WARNING: 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. /// /// WARNING: 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) } } impl Read for Body { #[inline] fn read(&mut self, buf: &mut [u8]) -> Result { match &mut self.0 { BodyAlt::SimpleOwned(c) => c.read(buf), BodyAlt::SimpleBorrowed(c) => c.read(buf), BodyAlt::Sized { content, total_len, consumed_len, } => { let filtered_buf_size = min(*total_len - *consumed_len, buf.len().try_into().unwrap()) .try_into() .unwrap(); if filtered_buf_size == 0 { return Ok(0); // No need to read anything } let additional = content.read(&mut buf[..filtered_buf_size])?; *consumed_len += u64::try_from(additional).unwrap(); if additional == 0 && consumed_len != total_len { // We check we do not miss 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(additional) } BodyAlt::Chunked(inner) => inner.read(buf), } } } impl Default for Body { #[inline] fn default() -> Self { b"".as_ref().into() } } 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 fmt::Debug for Body { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { BodyAlt::SimpleOwned(d) => f .debug_struct("Body") .field("len", &d.get_ref().len()) .finish(), BodyAlt::SimpleBorrowed(d) => f.debug_struct("Body").field("len", &d.len()).finish(), BodyAlt::Sized { total_len, .. } => { f.debug_struct("Body").field("len", total_len).finish() } BodyAlt::Chunked(_) => 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<&Headers>; } 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<&Headers> { None } } oxhttp-0.1.7/src/model/header.rs000066400000000000000000000466031447146021200165440ustar00rootroot00000000000000use std::borrow::{Borrow, Cow}; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::error::Error; use std::fmt; use std::fmt::Debug; use std::ops::Deref; use std::str; use std::str::{FromStr, Utf8Error}; /// A list of headers aka [fields](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields). /// /// ``` /// use oxhttp::model::{Headers, HeaderName, HeaderValue}; /// use std::str::FromStr; /// /// let mut headers = Headers::new(); /// headers.append(HeaderName::ACCEPT_LANGUAGE, "en".parse()?); /// headers.append(HeaderName::ACCEPT_LANGUAGE, "fr".parse()?); /// assert_eq!(headers.get(&HeaderName::ACCEPT_LANGUAGE).unwrap().as_ref(), b"en, fr"); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(PartialEq, Eq, Debug, Clone, Hash, Default)] pub struct Headers(BTreeMap); impl Headers { #[inline] pub fn new() -> Self { Self::default() } /// Adds a header to the list. /// /// It does not override the existing value(s) for the same header. #[inline] pub fn append(&mut self, name: HeaderName, value: HeaderValue) { match self.0.entry(name) { Entry::Occupied(e) => { let val = &mut e.into_mut().0; val.extend_from_slice(b", "); val.extend_from_slice(&value.0); } Entry::Vacant(e) => { e.insert(value); } } } /// Removes an header from the list. #[inline] pub fn remove(&mut self, name: &HeaderName) { self.0.remove(name); } /// Get an header value(s) from the list. #[inline] pub fn get(&self, name: &HeaderName) -> Option<&HeaderValue> { self.0.get(name) } #[inline] pub fn contains(&self, name: &HeaderName) -> bool { self.0.contains_key(name) } /// Sets a header it the list. /// /// It overrides the existing value(s) for the same header. #[inline] pub fn set(&mut self, name: HeaderName, value: HeaderValue) { self.0.insert(name, value); } #[inline] pub fn iter(&self) -> Iter<'_> { Iter(self.0.iter()) } /// Number of distinct headers #[inline] pub fn len(&self) -> usize { self.0.len() } #[inline] pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl IntoIterator for Headers { type Item = (HeaderName, HeaderValue); type IntoIter = IntoIter; #[inline] fn into_iter(self) -> IntoIter { IntoIter(self.0.into_iter()) } } impl<'a> IntoIterator for &'a Headers { type Item = (&'a HeaderName, &'a HeaderValue); type IntoIter = Iter<'a>; #[inline] fn into_iter(self) -> Iter<'a> { self.iter() } } /// A [header/field name](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.names). /// /// It is also normalized to lower case to ease equality checks. /// /// ``` /// use oxhttp::model::HeaderName; /// use std::str::FromStr; /// /// assert_eq!(HeaderName::from_str("content-Type")?, HeaderName::CONTENT_TYPE); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash)] pub struct HeaderName(Cow<'static, str>); impl HeaderName { #[inline] pub(crate) fn new_unchecked(name: impl Into>) -> Self { Self(name.into()) } /// [`Accept`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.accept) pub const ACCEPT: Self = Self(Cow::Borrowed("accept")); /// [`Accept-Encoding`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.accept-encoding) pub const ACCEPT_ENCODING: Self = Self(Cow::Borrowed("accept-encoding")); /// [`Accept-Language`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.accept-language) pub const ACCEPT_LANGUAGE: Self = Self(Cow::Borrowed("accept-language")); /// [`Allow`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.allow) pub const ACCEPT_RANGES: Self = Self(Cow::Borrowed("accept-ranges")); /// [`Allow`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.allow) pub const ALLOW: Self = Self(Cow::Borrowed("allow")); /// [`Authentication-Info`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.authentication-info) pub const AUTHENTICATION_INFO: Self = Self(Cow::Borrowed("authentication-info")); /// [`Authorization`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.authorization) pub const AUTHORIZATION: Self = Self(Cow::Borrowed("authorization")); /// [`Connection`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.connection) pub const CONNECTION: Self = Self(Cow::Borrowed("connection")); /// [`Content-Encoding`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-encoding) pub const CONTENT_ENCODING: Self = Self(Cow::Borrowed("content-encoding")); /// [`Content-Language`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-language) pub const CONTENT_LANGUAGE: Self = Self(Cow::Borrowed("content-language")); /// [`Content-Length`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-length) pub const CONTENT_LENGTH: Self = Self(Cow::Borrowed("content-length")); /// [`Content-Location`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-location) pub const CONTENT_LOCATION: Self = Self(Cow::Borrowed("content-location")); /// [`Content-Range`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-range) pub const CONTENT_RANGE: Self = Self(Cow::Borrowed("content-range")); /// [`Content-Type`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.content-type) pub const CONTENT_TYPE: Self = Self(Cow::Borrowed("content-type")); /// [`Date`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.date) pub const DATE: Self = Self(Cow::Borrowed("date")); /// [`ETag`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.etag) pub const ETAG: Self = Self(Cow::Borrowed("etag")); /// [`Expect`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.expect) pub const EXPECT: Self = Self(Cow::Borrowed("expect")); /// [`From`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.from) pub const FROM: Self = Self(Cow::Borrowed("from")); /// [`Host`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.host) pub const HOST: Self = Self(Cow::Borrowed("host")); /// [`If-Match`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.if-match) pub const IF_MATCH: Self = Self(Cow::Borrowed("if-match")); /// [`If-Modified-Since`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.if-modified-since) pub const IF_MODIFIED_SINCE: Self = Self(Cow::Borrowed("if-modified-since")); /// [`If-None-Match`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.if-none-match) pub const IF_NONE_MATCH: Self = Self(Cow::Borrowed("if-none-match")); /// [`If-Range`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.if-range) pub const IF_RANGE: Self = Self(Cow::Borrowed("if-range")); /// [`If-Unmodified-Since`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.if-unmodified-since) pub const IF_UNMODIFIED_SINCE: Self = Self(Cow::Borrowed("if-unmodified-since")); /// [`Last-Modified`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.last-modified) pub const LAST_MODIFIED: Self = Self(Cow::Borrowed("last-modified")); /// [`Location`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.location) pub const LOCATION: Self = Self(Cow::Borrowed("location")); /// [`Max-Forwards`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.max-forwards) pub const MAX_FORWARDS: Self = Self(Cow::Borrowed("max-forwards")); /// [`Proxy-Authenticate`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.proxy-authenticate) pub const PROXY_AUTHENTICATE: Self = Self(Cow::Borrowed("proxy-authenticate")); /// [`Proxy-Authentication-Info`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.proxy-authentication-info) pub const PROXY_AUTHENTICATION_INFO: Self = Self(Cow::Borrowed("proxy-authentication-info")); /// [`Proxy-Authorization`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.proxy-authorization) pub const PROXY_AUTHORIZATION: Self = Self(Cow::Borrowed("proxy-authorization")); /// [`Range`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.range) pub const RANGE: Self = Self(Cow::Borrowed("range")); /// [`Referer`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.referer) pub const REFERER: Self = Self(Cow::Borrowed("referer")); /// [`Retry-After`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.retry-after) pub const RETRY_AFTER: Self = Self(Cow::Borrowed("retry-after")); /// [`Server`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.server) pub const SERVER: Self = Self(Cow::Borrowed("server")); /// [`TE`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.te) pub const TE: Self = Self(Cow::Borrowed("te")); /// [`Trailer`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.trailer) pub const TRAILER: Self = Self(Cow::Borrowed("trailer")); /// [`Transfer-Encoding`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.transfer-encoding) pub const TRANSFER_ENCODING: Self = Self(Cow::Borrowed("transfer-encoding")); /// [`Upgrade`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.upgrade) pub const UPGRADE: Self = Self(Cow::Borrowed("upgrade")); /// [`User-Agent`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.user-agent) pub const USER_AGENT: Self = Self(Cow::Borrowed("user-agent")); /// [`Vary`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.vary) pub const VARY: Self = Self(Cow::Borrowed("vary")); /// [`Via`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.via) pub const VIA: Self = Self(Cow::Borrowed("via")); /// [`WWW-Authenticate`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.www-authenticate) pub const WWW_AUTHENTICATE: Self = Self(Cow::Borrowed("www-authenticate")); } impl Deref for HeaderName { type Target = str; #[inline] fn deref(&self) -> &str { &self.0 } } impl AsRef for HeaderName { #[inline] fn as_ref(&self) -> &str { &self.0 } } impl Borrow for HeaderName { #[inline] fn borrow(&self) -> &str { &self.0 } } impl FromStr for HeaderName { type Err = InvalidHeader; #[inline] fn from_str(name: &str) -> Result { Self::try_from(name) } } impl TryFrom<&str> for HeaderName { type Error = InvalidHeader; #[inline] fn try_from(value: &str) -> Result { Self::try_from(value.to_owned()) } } impl TryFrom for HeaderName { type Error = InvalidHeader; #[inline] fn try_from(mut name: String) -> Result { name.make_ascii_lowercase(); // We normalize to lowercase if name.is_empty() { Err(InvalidHeader(InvalidHeaderAlt::EmptyName)) } else { for c in name.chars() { if !matches!(c, '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~' | '0'..='9' | 'a'..='z') { return Err(InvalidHeader(InvalidHeaderAlt::InvalidNameChar { name: name.to_owned(), invalid_char: c, })); } } Ok(Self(name.into())) } } } impl fmt::Display for HeaderName { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } pub trait IntoHeaderName { fn try_into(self) -> Result; } impl IntoHeaderName for HeaderName { #[inline] fn try_into(self) -> Result { Ok(self) } } impl> IntoHeaderName for T { #[inline] fn try_into(self) -> Result { self.try_into() } } /// A [header/field value](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#fields.values). /// /// ``` /// use oxhttp::model::HeaderValue; /// use std::str::FromStr; /// /// assert_eq!(HeaderValue::from_str("foo")?.as_ref(), b"foo"); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash, Default)] pub struct HeaderValue(Vec); impl HeaderValue { #[inline] pub(crate) fn new_unchecked(value: impl Into>) -> Self { Self(value.into()) } #[inline] pub fn to_str(&self) -> Result<&str, Utf8Error> { str::from_utf8(self) } } impl Deref for HeaderValue { type Target = [u8]; #[inline] fn deref(&self) -> &[u8] { &self.0 } } impl AsRef<[u8]> for HeaderValue { #[inline] fn as_ref(&self) -> &[u8] { &self.0 } } impl Borrow<[u8]> for HeaderValue { #[inline] fn borrow(&self) -> &[u8] { &self.0 } } impl FromStr for HeaderValue { type Err = InvalidHeader; #[inline] fn from_str(value: &str) -> Result { Self::try_from(value) } } impl TryFrom<&str> for HeaderValue { type Error = InvalidHeader; #[inline] fn try_from(value: &str) -> Result { Self::try_from(value.to_owned()) } } impl TryFrom for HeaderValue { type Error = InvalidHeader; #[inline] fn try_from(value: String) -> Result { Self::try_from(value.into_bytes()) } } impl TryFrom<&[u8]> for HeaderValue { type Error = InvalidHeader; #[inline] fn try_from(value: &[u8]) -> Result { value.to_owned().try_into() } } impl TryFrom> for HeaderValue { type Error = InvalidHeader; #[inline] fn try_from(value: Vec) -> Result { // no tab or space at the beginning if let Some(c) = value.first().cloned() { if matches!(c, b'\t' | b' ') { return Err(InvalidHeader(InvalidHeaderAlt::InvalidValueByte { value, invalid_byte: c, })); } } // no tab or space at the end if let Some(c) = value.last().cloned() { if matches!(c, b'\t' | b' ') { return Err(InvalidHeader(InvalidHeaderAlt::InvalidValueByte { value, invalid_byte: c, })); } } // no line jump for c in &value { if matches!(*c, b'\r' | b'\n') { return Err(InvalidHeader(InvalidHeaderAlt::InvalidValueByte { value: value.clone(), invalid_byte: *c, })); } } Ok(HeaderValue(value)) } } impl fmt::Display for HeaderValue { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", String::from_utf8_lossy(&self.0)) } } #[derive(Debug)] pub struct Iter<'a>(std::collections::btree_map::Iter<'a, HeaderName, HeaderValue>); impl<'a> Iterator for Iter<'a> { type Item = (&'a HeaderName, &'a HeaderValue); #[inline] fn next(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> { self.0.next() } #[inline] fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } #[inline] fn last(self) -> Option<(&'a HeaderName, &'a HeaderValue)> { self.0.last() } } impl<'a> DoubleEndedIterator for Iter<'a> { #[inline] fn next_back(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> { self.0.next_back() } } impl<'a> ExactSizeIterator for Iter<'a> { #[inline] fn len(&self) -> usize { self.0.len() } } #[derive(Debug)] pub struct IntoIter(std::collections::btree_map::IntoIter); impl Iterator for IntoIter { type Item = (HeaderName, HeaderValue); #[inline] fn next(&mut self) -> Option<(HeaderName, HeaderValue)> { self.0.next() } #[inline] fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } #[inline] fn last(self) -> Option<(HeaderName, HeaderValue)> { self.0.last() } } impl DoubleEndedIterator for IntoIter { #[inline] fn next_back(&mut self) -> Option<(HeaderName, HeaderValue)> { self.0.next_back() } } impl ExactSizeIterator for IntoIter { #[inline] fn len(&self) -> usize { self.0.len() } } /// Error returned by [`HeaderName::try_from`]. #[derive(Debug, Clone)] pub struct InvalidHeader(InvalidHeaderAlt); #[derive(Debug, Clone)] enum InvalidHeaderAlt { EmptyName, InvalidNameChar { name: String, invalid_char: char }, InvalidValueByte { value: Vec, invalid_byte: u8 }, } impl fmt::Display for InvalidHeader { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { InvalidHeaderAlt::EmptyName => f.write_str("header names should not be empty"), InvalidHeaderAlt::InvalidNameChar { name, invalid_char } => write!( f, "The character '{invalid_char}' is not valid inside of header name '{name}'" ), InvalidHeaderAlt::InvalidValueByte { value, invalid_byte, } => write!( f, "The byte '{}' is not valid inside of header value '{}'", invalid_byte, String::from_utf8_lossy(value) ), } } } impl Error for InvalidHeader {} #[cfg(test)] mod tests { use super::*; #[test] fn validate_header_name() { assert!(HeaderName::from_str("").is_err()); assert!(HeaderName::from_str("ffo bar").is_err()); assert!(HeaderName::from_str("ffo\tbar").is_err()); assert!(HeaderName::from_str("ffo\rbar").is_err()); assert!(HeaderName::from_str("ffo\nbar").is_err()); assert!(HeaderName::from_str("ffoébar").is_err()); assert!(HeaderName::from_str("foo-bar").is_ok()); } #[test] fn validate_header_value() { assert!(HeaderValue::from_str("").is_ok()); assert!(HeaderValue::from_str(" ffobar").is_err()); assert!(HeaderValue::from_str("ffobar ").is_err()); assert!(HeaderValue::from_str("ffo\rbar").is_err()); assert!(HeaderValue::from_str("ffo\nbar").is_err()); assert!(HeaderValue::from_str("ffoébar").is_ok()); } } oxhttp-0.1.7/src/model/method.rs000066400000000000000000000116471447146021200165740ustar00rootroot00000000000000use std::borrow::{Borrow, Cow}; use std::error::Error; use std::fmt; use std::ops::Deref; use std::str::FromStr; /// An [HTTP method](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#methods) like `GET` or `POST`. /// /// ``` /// use oxhttp::model::Method; /// use std::str::FromStr; /// /// assert_eq!(Method::from_str("get")?, Method::GET); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(PartialEq, Eq, Debug, Clone, Hash)] pub struct Method(Cow<'static, str>); impl Method { /// Is the method [safe](https://httpwg.org/specs/rfc7231.html#safe.methods) pub(crate) fn is_safe(&self) -> bool { matches!(self.as_ref(), "GET" | "HEAD" | "OPTIONS" | "TRACE") } /// [CONNECT](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#CONNECT). pub const CONNECT: Method = Self(Cow::Borrowed("CONNECT")); /// [DELETE](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#DELETE). pub const DELETE: Method = Self(Cow::Borrowed("DELETE")); /// [GET](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#GET). pub const GET: Method = Self(Cow::Borrowed("GET")); /// [HEAD](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#HEAD). pub const HEAD: Method = Self(Cow::Borrowed("HEAD")); /// [OPTIONS](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#OPTIONS). pub const OPTIONS: Method = Self(Cow::Borrowed("OPTIONS")); /// [POST](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#POST). pub const POST: Method = Self(Cow::Borrowed("POST")); /// [PUT](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#PUT). pub const PUT: Method = Self(Cow::Borrowed("PUT")); /// [TRACE](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#TRACE). pub const TRACE: Method = Self(Cow::Borrowed("TRACE")); } impl Deref for Method { type Target = str; #[inline] fn deref(&self) -> &str { &self.0 } } impl AsRef for Method { #[inline] fn as_ref(&self) -> &str { &self.0 } } impl Borrow for Method { #[inline] fn borrow(&self) -> &str { &self.0 } } impl FromStr for Method { type Err = InvalidMethod; #[inline] fn from_str(name: &str) -> Result { for method in STATIC_METHODS { if method.eq_ignore_ascii_case(name) { return Ok(method); } } name.to_owned().try_into() } } impl TryFrom for Method { type Error = InvalidMethod; #[inline] fn try_from(name: String) -> Result { for method in STATIC_METHODS { if method.eq_ignore_ascii_case(&name) { return Ok(method); } } if name.is_empty() { Err(InvalidMethod(InvalidMethodAlt::Empty)) } else { for c in name.chars() { if !matches!(c, '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~' | '0'..='9' | 'a'..='z' | 'A'..='Z') { return Err(InvalidMethod(InvalidMethodAlt::InvalidChar { name: name.to_owned(), invalid_char: c, })); } } Ok(Self(name.into())) } } } impl fmt::Display for Method { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_ref()) } } const STATIC_METHODS: [Method; 8] = [ Method::CONNECT, Method::DELETE, Method::GET, Method::HEAD, Method::OPTIONS, Method::POST, Method::PUT, Method::TRACE, ]; /// Error returned by [`Method::try_from`]. #[derive(Debug, Clone)] pub struct InvalidMethod(InvalidMethodAlt); #[derive(Debug, Clone)] enum InvalidMethodAlt { Empty, InvalidChar { name: String, invalid_char: char }, } impl fmt::Display for InvalidMethod { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { InvalidMethodAlt::Empty => f.write_str("HTTP methods should not be empty"), InvalidMethodAlt::InvalidChar { name, invalid_char } => write!( f, "The character '{invalid_char}' is not valid inside of HTTP method '{name}'" ), } } } impl Error for InvalidMethod {} #[cfg(test)] mod tests { use super::*; #[test] fn validate_header_name() { assert!(Method::from_str("").is_err()); assert!(Method::from_str("ffo bar").is_err()); assert!(Method::from_str("ffo\tbar").is_err()); assert!(Method::from_str("ffo\rbar").is_err()); assert!(Method::from_str("ffo\nbar").is_err()); assert!(Method::from_str("ffoébar").is_err()); assert!(Method::from_str("foo-bar").is_ok()); } } oxhttp-0.1.7/src/model/mod.rs000066400000000000000000000007521447146021200160660ustar00rootroot00000000000000//! The HTTP model encoded in Rust type system. //! //! The main entry points are [`Request`] and [`Response`]. mod body; mod header; mod method; mod request; mod response; mod status; pub use body::{Body, ChunkedTransferPayload}; pub use header::{HeaderName, HeaderValue, Headers, InvalidHeader}; pub use method::{InvalidMethod, Method}; pub use request::{Request, RequestBuilder}; pub use response::{Response, ResponseBuilder}; pub use status::{InvalidStatus, Status}; pub use url::Url; oxhttp-0.1.7/src/model/request.rs000066400000000000000000000064001447146021200167730ustar00rootroot00000000000000use crate::model::header::IntoHeaderName; use crate::model::{Body, HeaderName, HeaderValue, Headers, InvalidHeader, Method, Url}; /// A HTTP request. /// /// ``` /// use oxhttp::model::{Request, Method, HeaderName, Body}; /// /// let request = Request::builder(Method::POST, "http://example.com:80/foo".parse()?) /// .with_header(HeaderName::CONTENT_TYPE, "application/json")? /// .with_body("{\"foo\": \"bar\"}"); /// /// assert_eq!(*request.method(), Method::POST); /// assert_eq!(request.url().as_str(), "http://example.com/foo"); /// assert_eq!(request.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"application/json"); /// assert_eq!(&request.into_body().to_vec()?, b"{\"foo\": \"bar\"}"); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(Debug)] pub struct Request { method: Method, url: Url, headers: Headers, body: Body, } impl Request { #[inline] pub fn builder(method: Method, url: Url) -> RequestBuilder { RequestBuilder { method, url, headers: Headers::new(), } } #[inline] pub fn method(&self) -> &Method { &self.method } #[inline] pub fn url(&self) -> &Url { &self.url } #[inline] pub fn headers(&self) -> &Headers { &self.headers } #[inline] pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } #[inline] pub fn header(&self, name: &HeaderName) -> Option<&HeaderValue> { self.headers.get(name) } #[inline] pub fn append_header( &mut self, name: impl IntoHeaderName, value: impl TryInto, ) -> Result<(), InvalidHeader> { self.headers_mut() .append(name.try_into()?, value.try_into()?); Ok(()) } #[inline] pub fn body(&self) -> &Body { &self.body } #[inline] pub fn body_mut(&mut self) -> &mut Body { &mut self.body } #[inline] pub fn into_body(self) -> Body { self.body } } /// Builder for [`Request`] pub struct RequestBuilder { method: Method, url: Url, headers: Headers, } impl RequestBuilder { #[inline] pub fn method(&self) -> &Method { &self.method } #[inline] pub fn url(&self) -> &Url { &self.url } #[inline] pub fn headers(&self) -> &Headers { &self.headers } #[inline] pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } #[inline] pub fn header(&self, name: &HeaderName) -> Option<&HeaderValue> { self.headers.get(name) } #[inline] pub fn with_header( mut self, name: impl IntoHeaderName, value: impl TryInto, ) -> Result { self.headers_mut() .append(name.try_into()?, value.try_into()?); Ok(self) } #[inline] pub fn with_body(self, body: impl Into) -> Request { Request { method: self.method, url: self.url, headers: self.headers, body: body.into(), } } #[inline] pub fn build(self) -> Request { self.with_body(Body::default()) } } oxhttp-0.1.7/src/model/response.rs000066400000000000000000000057441447146021200171530ustar00rootroot00000000000000use crate::model::header::IntoHeaderName; use crate::model::{Body, HeaderName, HeaderValue, Headers, InvalidHeader, Status}; /// A HTTP response. /// /// ``` /// use oxhttp::model::{HeaderName, Body, Response, Status}; /// /// let response = Response::builder(Status::OK) /// .with_header(HeaderName::CONTENT_TYPE, "application/json")? /// .with_header("X-Custom", "foo")? /// .with_body("{\"foo\": \"bar\"}"); /// /// assert_eq!(response.status(), Status::OK); /// assert_eq!(response.header(&HeaderName::CONTENT_TYPE).unwrap().as_ref(), b"application/json"); /// assert_eq!(&response.into_body().to_vec()?, b"{\"foo\": \"bar\"}"); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(Debug)] pub struct Response { status: Status, headers: Headers, body: Body, } impl Response { #[inline] pub fn builder(status: Status) -> ResponseBuilder { ResponseBuilder { status, headers: Headers::new(), } } #[inline] pub fn status(&self) -> Status { self.status } #[inline] pub fn headers(&self) -> &Headers { &self.headers } #[inline] pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } #[inline] pub fn header(&self, name: &HeaderName) -> Option<&HeaderValue> { self.headers.get(name) } #[inline] pub fn append_header( &mut self, name: impl IntoHeaderName, value: impl TryInto, ) -> Result<(), InvalidHeader> { self.headers_mut() .append(name.try_into()?, value.try_into()?); Ok(()) } #[inline] pub fn body(&self) -> &Body { &self.body } #[inline] pub fn body_mut(&mut self) -> &mut Body { &mut self.body } #[inline] pub fn into_body(self) -> Body { self.body } } /// Builder for [`Response`] pub struct ResponseBuilder { status: Status, headers: Headers, } impl ResponseBuilder { #[inline] pub fn status(&self) -> Status { self.status } #[inline] pub fn headers(&self) -> &Headers { &self.headers } #[inline] pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } #[inline] pub fn header(&self, name: &HeaderName) -> Option<&HeaderValue> { self.headers.get(name) } #[inline] pub fn with_header( mut self, name: impl IntoHeaderName, value: impl TryInto, ) -> Result { self.headers_mut() .append(name.try_into()?, value.try_into()?); Ok(self) } #[inline] pub fn with_body(self, body: impl Into) -> Response { Response { status: self.status, headers: self.headers, body: body.into(), } } #[inline] pub fn build(self) -> Response { self.with_body(Body::default()) } } oxhttp-0.1.7/src/model/status.rs000066400000000000000000000307451447146021200166370ustar00rootroot00000000000000use std::borrow::Borrow; use std::error::Error; use std::fmt; use std::ops::Deref; /// An HTTP [status](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.codes). /// /// ``` /// use oxhttp::model::Status; /// /// assert_eq!(Status::OK, Status::try_from(200)?); /// # Result::<_,Box>::Ok(()) /// ``` #[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)] pub struct Status(u16); impl Status { /// Is the status [informational](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.1xx). #[inline] pub fn is_informational(&self) -> bool { (100..=199).contains(&self.0) } /// Is the status [successful](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.2xx). #[inline] pub fn is_successful(&self) -> bool { (200..=299).contains(&self.0) } /// Is the status [related to redirections](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.3xx). #[inline] pub fn is_redirection(&self) -> bool { (300..=399).contains(&self.0) } /// Is the status a [client error](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.4xx). #[inline] pub fn is_client_error(&self) -> bool { (400..=499).contains(&self.0) } /// Is the status [server error](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.5xx). #[inline] pub fn is_server_error(&self) -> bool { (500..=599).contains(&self.0) } /// [100 Continue](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.100) pub const CONTINUE: Self = Self(100); /// [101 Switching Protocols](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.101) pub const SWITCHING_PROTOCOLS: Self = Self(101); /// [200 OK](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.200) pub const OK: Self = Self(200); /// [201 Created](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.201) pub const CREATED: Self = Self(201); /// [202 Accepted](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.202) pub const ACCEPTED: Self = Self(202); /// [203 Non-Authoritative Information](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.203) pub const NON_AUTHORITATIVE_INFORMATION: Self = Self(203); /// [204 No Content](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.204) pub const NO_CONTENT: Self = Self(204); /// [205 Reset Content](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.205) pub const RESET_CONTENT: Self = Self(205); /// [206 Partial Content](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.206) pub const PARTIAL_CONTENT: Self = Self(206); /// [300 Multiple Choices](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.300) pub const MULTIPLE_CHOICES: Self = Self(300); /// [301 Moved Permanently](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.301) pub const MOVED_PERMANENTLY: Self = Self(301); /// [302 Found](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.302) pub const FOUND: Self = Self(302); /// [303 See Other](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.303) pub const SEE_OTHER: Self = Self(303); /// [304 Not Modified](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.304) pub const NOT_MODIFIED: Self = Self(304); /// [305 Use Proxy](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.305) pub const USE_PROXY: Self = Self(305); /// [307 Temporary Redirect](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.307) pub const TEMPORARY_REDIRECT: Self = Self(307); /// [308 Permanent Redirect](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.308) pub const PERMANENT_REDIRECT: Self = Self(308); /// [400 Bad Request](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.400) pub const BAD_REQUEST: Self = Self(400); /// [401 Unauthorized](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.401) pub const UNAUTHORIZED: Self = Self(401); /// [402 Payment Required](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.402) pub const PAYMENT_REQUIRED: Self = Self(402); /// [403 Forbidden](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.403) pub const FORBIDDEN: Self = Self(403); /// [404 Not Found](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.404) pub const NOT_FOUND: Self = Self(404); /// [405 Method Not Allowed](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.405) pub const METHOD_NOT_ALLOWED: Self = Self(405); /// [406 Not Acceptable](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.406) pub const NOT_ACCEPTABLE: Self = Self(406); /// [407 Proxy Authentication Required](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.407) pub const PROXY_AUTHENTICATION_REQUIRED: Self = Self(407); /// [408 Request Timeout](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.408) pub const REQUEST_TIMEOUT: Self = Self(408); /// [409 Conflict](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.409) pub const CONFLICT: Self = Self(409); /// [410 Gone](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.410) pub const GONE: Self = Self(410); /// [411 Length Required](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.411) pub const LENGTH_REQUIRED: Self = Self(411); /// [412 Precondition Failed](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.412) pub const PRECONDITION_FAILED: Self = Self(412); /// [413 Content Too Large](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.413) pub const CONTENT_TOO_LARGE: Self = Self(413); /// [414 URI Too Long](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.414) pub const URI_TOO_LONG: Self = Self(414); /// [415 Unsupported Media Type](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.415) pub const UNSUPPORTED_MEDIA_TYPE: Self = Self(415); /// [416 Range Not Satisfiable](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.416) pub const RANGE_NOT_SATISFIABLE: Self = Self(416); /// [417 Expectation Failed](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.417) pub const EXPECTATION_FAILED: Self = Self(417); /// [421 Misdirected Request](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.421) pub const MISDIRECTED_REQUEST: Self = Self(421); /// [422 Unprocessable Content](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.422) pub const UNPROCESSABLE_CONTENT: Self = Self(422); /// [426 Upgrade Required](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.426) pub const UPGRADE_REQUIRED: Self = Self(426); /// [500 Internal Server Error](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.500) pub const INTERNAL_SERVER_ERROR: Self = Self(500); /// [501 Not Implemented](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.501) pub const NOT_IMPLEMENTED: Self = Self(501); /// [502 Bad Gateway](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.502) pub const BAD_GATEWAY: Self = Self(502); /// [503 Service Unavailable](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.503) pub const SERVICE_UNAVAILABLE: Self = Self(503); /// [504 Gateway Timeout](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.504) pub const GATEWAY_TIMEOUT: Self = Self(504); /// [505 HTTP Version Not Supported](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#status.505) pub const HTTP_VERSION_NOT_SUPPORTED: Self = Self(505); pub(crate) fn reason_phrase(&self) -> Option<&'static str> { match self.0 { 100 => Some("Continue"), 101 => Some("Switching Protocols"), 102 => Some("Processing"), 103 => Some("Early Hints"), 200 => Some("OK"), 201 => Some("Created"), 202 => Some("Accepted"), 203 => Some("Non-Authoritative Information"), 204 => Some("No Content"), 205 => Some("Reset Content"), 206 => Some("Partial Content"), 207 => Some("Multi-Status"), 208 => Some("Already Reported"), 226 => Some("IM Used"), 300 => Some("Multiple Choices"), 301 => Some("Moved Permanently"), 302 => Some("Found"), 303 => Some("See Other"), 304 => Some("Not Modified"), 305 => Some("Use Proxy"), 307 => Some("Temporary Redirect"), 308 => Some("Permanent Redirect"), 400 => Some("Bad Request"), 401 => Some("Unauthorized"), 402 => Some("Payment Required"), 403 => Some("Forbidden"), 404 => Some("Not Found"), 405 => Some("Method Not Allowed"), 406 => Some("Not Acceptable"), 407 => Some("Proxy Authentication Required"), 408 => Some("Request Timeout"), 409 => Some("Conflict"), 410 => Some("Gone"), 411 => Some("Length Required"), 412 => Some("Precondition Failed"), 413 => Some("Content Too Large"), 414 => Some("URI Too Long"), 415 => Some("Unsupported Media Type"), 416 => Some("Range Not Satisfiable"), 417 => Some("Expectation Failed"), 421 => Some("Misdirected Request"), 422 => Some("Unprocessable Content"), 423 => Some("Locked"), 424 => Some("Failed Dependency"), 425 => Some("Too Early"), 426 => Some("Upgrade Required"), 428 => Some("Precondition Required"), 429 => Some("Too Many Requests"), 431 => Some("Request Header Fields Too Large"), 451 => Some("Unavailable For Legal Reasons"), 500 => Some("Internal Server Error"), 501 => Some("Not Implemented"), 502 => Some("Bad Gateway"), 503 => Some("Service Unavailable"), 504 => Some("Gateway Timeout"), 505 => Some("HTTP Version Not Supported"), 506 => Some("Variant Also Negotiates"), 507 => Some("Insufficient Storage"), 508 => Some("Loop Detected"), 510 => Some("Not Extended"), 511 => Some("Network Authentication Required"), _ => None, } } } impl Deref for Status { type Target = u16; #[inline] fn deref(&self) -> &u16 { &self.0 } } impl AsRef for Status { #[inline] fn as_ref(&self) -> &u16 { &self.0 } } impl Borrow for Status { #[inline] fn borrow(&self) -> &u16 { &self.0 } } impl TryFrom for Status { type Error = InvalidStatus; #[inline] fn try_from(code: u16) -> Result { if (0..=999).contains(&code) { Ok(Self(code)) } else { Err(InvalidStatus(code)) } } } impl From for u16 { #[inline] fn from(status: Status) -> Self { status.0 } } impl fmt::Display for Status { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {}", self.0, self.reason_phrase().unwrap_or("")) } } /// Error returned by [`Status::try_from`]. #[allow(missing_copy_implementations)] #[derive(Debug, Clone)] pub struct InvalidStatus(u16); impl fmt::Display for InvalidStatus { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "The HTTP status code should be between 0 and 999, '{}' found", self.0 ) } } impl Error for InvalidStatus {} oxhttp-0.1.7/src/server.rs000066400000000000000000000365601447146021200155230ustar00rootroot00000000000000use crate::io::encode_response; use crate::io::{decode_request_body, decode_request_headers}; use crate::model::{ HeaderName, HeaderValue, InvalidHeader, Request, RequestBuilder, Response, Status, }; #[cfg(feature = "rayon")] use rayon_core::ThreadPoolBuilder; use std::io::{copy, sink, BufReader, BufWriter, Error, ErrorKind, Result, Write}; use std::net::{TcpListener, TcpStream, ToSocketAddrs}; use std::sync::Arc; #[cfg(feature = "rayon")] use std::thread::available_parallelism; #[cfg(not(feature = "rayon"))] use std::thread::Builder; use std::time::Duration; /// An HTTP server. /// /// It currently provides two threading approaches: /// - If the `rayon` feature is enabled a thread pool is used. /// The number of threads can be set with the [`Server::set_num_threads`] feature. /// By default 4 times the number of available logical cores is used. /// - If the `rayon` feature is not enabled, a new thread is started on each connection and kept while the client connection is not closed. /// Use it at our own risks! /// /// ```no_run /// use oxhttp::Server; /// use oxhttp::model::{Response, Status}; /// 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.url().path() == "/" { /// Response::builder(Status::OK).with_body("home") /// } else { /// Response::builder(Status::NOT_FOUND).build() /// } /// }); /// // Raise a timeout error if the client does not respond after 10s. /// server.set_global_timeout(Duration::from_secs(10)); /// // Listen to localhost:8080 /// server.listen(("localhost", 8080))?; /// # Result::<_,Box>::Ok(()) /// ``` #[allow(missing_copy_implementations)] pub struct Server { on_request: Arc Response + Send + Sync + 'static>, timeout: Option, server: Option, #[cfg(feature = "rayon")] num_threads: usize, } 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), timeout: None, server: None, #[cfg(feature = "rayon")] num_threads: available_parallelism().map_or(4, |c| c.get()) * 4, } } /// Sets the default value for the [`Server`](https://httpwg.org/http-core/draft-ietf-httpbis-semantics-latest.html#field.server) header. #[inline] pub fn set_server_name( &mut self, server: impl Into, ) -> std::result::Result<(), InvalidHeader> { self.server = Some(HeaderValue::try_from(server.into())?); Ok(()) } /// Sets the global timeout value (applies to both read and write). #[inline] pub fn set_global_timeout(&mut self, timeout: Duration) { self.timeout = Some(timeout); } /// Sets that the server should use a thread pool with this number of threads. #[cfg(feature = "rayon")] #[inline] pub fn set_num_threads(&mut self, num_threads: usize) { self.num_threads = num_threads; } /// Runs the server by listening to `address`. #[cfg(feature = "rayon")] pub fn listen(&self, address: impl ToSocketAddrs) -> Result<()> { let thread_pool = ThreadPoolBuilder::new() .num_threads(self.num_threads) .thread_name(|i| format!("OxHTTP server thread {i}")) .panic_handler(|error| eprintln!("Panic on OxHTTP server thread: {error:?}")) .build() .map_err(|e| Error::new(ErrorKind::Other, e))?; let listeners = open_tcp(address)?; if self.num_threads < listeners.len() + 1 { return Err(Error::new( ErrorKind::Other, format!("The thread pool only provides {} threads whereas the server is listening to {} connections. There are not enough threads for the server to work properly.", self.num_threads, listeners.len()), )); } thread_pool.scope(|p| { for listener in listeners { p.spawn(move |p| { for stream in listener.incoming() { match stream { Ok(stream) => { let on_request = self.on_request.clone(); let timeout = self.timeout; let server = self.server.clone(); p.spawn(move |_| { let peer = match stream.peer_addr() { Ok(peer) => peer, Err(error) => { eprintln!("OxHTTP TCP error when attempting to get the peer address: {error}"); return; } }; if let Err(error) = accept_request(stream, on_request, timeout, server) { eprintln!( "OxHTTP TCP error when writing response to {peer}: {error}" ); } }) }, Err(error) => { eprintln!("OxHTTP TCP error when opening stream: {error}"); } } } }) } }); Ok(()) } /// Runs the server by listening to `address`. #[cfg(not(feature = "rayon"))] pub fn listen(&self, address: impl ToSocketAddrs) -> Result<()> { // TODO: use scope? let timeout = self.timeout; let threads = open_tcp(address)? .into_iter() .map(|listener| { let on_request = self.on_request.clone(); let server = self.server.clone(); Builder::new().name(format!("Thread listening to {}", listener.local_addr()?)).spawn(move || { for stream in listener.incoming() { match stream { Ok(stream) => { let on_request = on_request.clone(); let server = server.clone(); if let Err(error) = Builder::new().spawn(move || { let peer = match stream.peer_addr() { Ok(peer) => peer, Err(error) => { eprintln!("OxHTTP TCP error when attempting to get the peer address: {error}"); return; } }; if let Err(error) = accept_request(stream, on_request, timeout, server) { eprintln!( "OxHTTP TCP error when writing response to {peer}: {error}" ) } }) { eprintln!("OxHTTP thread spawn error: {error}"); } } Err(error) => { eprintln!("OxHTTP TCP error when opening stream: {error}"); } } } }) }) .collect::>>()?; for thread in threads { thread .join() .map_err(|_| Error::new(ErrorKind::Other, "The server thread panicked"))?; } Ok(()) } } fn open_tcp(address: impl ToSocketAddrs) -> Result> { let mut listeners = Vec::new(); let mut last_error = None; for address in address.to_socket_addrs()? { match TcpListener::bind(address) { Ok(listener) => listeners.push(listener), Err(e) => last_error = Some(e), } } if listeners.is_empty() { Err(last_error.unwrap_or_else(|| { Error::new( ErrorKind::InvalidInput, "could not resolve to any addresses", ) })) } else { Ok(listeners) } } fn accept_request( mut stream: TcpStream, on_request: Arc 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::new(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.header(&HeaderName::EXPECT).cloned() { if expect.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.as_ref()) } else { ( build_text_response( Status::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.as_ref()) } } 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 set_header_fallback(&mut response, HeaderName::SERVER, &server); encode_response(&mut response, BufWriter::new(&mut stream))?; } 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 .header(&HeaderName::CONNECTION) .and_then(|v| { v.eq_ignore_ascii_case(b"close") .then(|| ConnectionState::Close) }) .unwrap_or(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 => Status::REQUEST_TIMEOUT, ErrorKind::InvalidData => Status::BAD_REQUEST, _ => Status::INTERNAL_SERVER_ERROR, }, error.to_string(), ) } fn build_text_response(status: Status, text: String) -> Response { Response::builder(status) .with_header(HeaderName::CONTENT_TYPE, "text/plain; charset=utf-8") .unwrap() .with_body(text) } fn set_header_fallback( response: &mut Response, header_name: HeaderName, header_value: &Option, ) { if let Some(header_value) = header_value { if !response.headers().contains(&header_name) { response .headers_mut() .set(header_name, header_value.clone()) } } } #[cfg(test)] mod tests { use super::*; use crate::model::Status; use std::io::Read; use std::thread::{sleep, spawn}; #[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<()> { spawn(move || { let mut server = Server::new(|request| { if request.url().path() == "/" { Response::builder(Status::OK).with_body("home") } else { Response::builder(Status::NOT_FOUND).build() } }); server.set_server_name("OxHTTP/1.0").unwrap(); server.set_global_timeout(Duration::from_secs(1)); server.listen(("localhost", server_port)).unwrap(); }); 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(()) } } oxhttp-0.1.7/src/utils.rs000066400000000000000000000005361447146021200153470ustar00rootroot00000000000000use 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) } oxhttp-0.1.7/typos.toml000066400000000000000000000000531447146021200151170ustar00rootroot00000000000000[default.extend-words] referer = "referer"