├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── blocking.rs ├── brotli.rs ├── chunked.rs ├── cookies.rs ├── deflate.rs ├── form.rs ├── gzip.rs ├── json_dynamic.rs ├── json_typed.rs ├── multiple_hosts.rs ├── redirects.rs ├── simple.rs └── twitter.rs ├── src ├── cookie.rs ├── error.rs ├── into_url.rs ├── lib.rs ├── lunatic_impl │ ├── body.rs │ ├── client │ │ ├── builder.rs │ │ └── mod.rs │ ├── decoder.rs │ ├── http_stream.rs │ ├── mod.rs │ ├── request.rs │ ├── response.rs │ └── upgrade.rs ├── redirect.rs ├── response.rs ├── tls.rs ├── util.rs └── version.rs └── tests ├── badssl.rs ├── blocking.rs ├── brotli.rs ├── chunked.rs ├── client.rs ├── cookie.rs ├── deflate.rs ├── gzip.rs ├── multipart.rs ├── proxy.rs ├── redirect.rs ├── support └── mod.rs ├── timeouts.rs └── upgrade.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasi" 3 | 4 | [target.wasm32-wasi] 5 | runner = "lunatic" 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Only enable cargo, turn off npm from wasm example 4 | updates: 5 | - package-ecosystem: "cargo" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | # disable regular version updates, security updates are unaffected 10 | open-pull-requests-limit: 0 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | env: 10 | REQWEST_TEST_BODY_FULL: 1 11 | RUST_BACKTRACE: 1 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: "Check out repository" 18 | uses: actions/checkout@v1 19 | # Rust builds can take some time, cache them. 20 | - uses: Swatinem/rust-cache@v1 21 | - name: "Install lunatic" 22 | run: cargo install --git https://github.com/lunatic-solutions/lunatic --bin lunatic-runtime 23 | - name: Install rust 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | target: wasm32-wasi 28 | override: true 29 | components: rustfmt, clippy 30 | - name: "Run tests" 31 | run: cargo test --features cookies 32 | - name: "Run clippy" 33 | run: cargo clippy --features cookies -- -D warnings 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.0 2 | 3 | Initial release 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Yuriy Voshchepynets "] 3 | autotests = true 4 | categories = ["web-programming::http-client", "wasm"] 5 | description = "higher level HTTP client library for the lunatic runtime" 6 | documentation = "https://docs.rs/nightfly" 7 | edition = "2018" 8 | keywords = ["http", "request", "client"] 9 | license = "MIT/Apache-2.0" 10 | name = "nightfly" 11 | readme = "README.md" 12 | repository = "https://github.com/SquattingSocrates/nightfly" 13 | version = "0.1.6" 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | targets = ["wasm32-wasi"] 19 | 20 | [package.metadata.playground] 21 | features = [ 22 | "json", # "multipart", 23 | ] 24 | 25 | [features] 26 | cookies = ["cookie_crate", "cookie_store", "proc-macro-hack"] 27 | default = ["cookies"] 28 | 29 | # multipart = ["mime_guess"] 30 | 31 | [dependencies] 32 | base64 = "0.13" 33 | bytes = "1.0" 34 | encoding_rs = "0.8.31" 35 | http = "0.2" 36 | http-body = "0.4.5" 37 | httparse = "1.7.1" 38 | ipnet = "2.5.0" 39 | lunatic = "0.13.1" 40 | lunatic-log = "0.4" 41 | mime = "0.3.16" 42 | percent-encoding = "2.2.0" 43 | serde = "1.0" 44 | serde_urlencoded = "0.7.1" 45 | thiserror = "1.0" 46 | tower-service = "0.3" 47 | url = {version = "2.2", features = ["serde"]} 48 | 49 | # Optional deps... 50 | 51 | ## json 52 | serde_json = "1.0" 53 | ## multipart 54 | mime_guess = {version = "2.0", default-features = false, optional = true} 55 | # Optional deps... 56 | brotli = {version = "3.3.4"} 57 | 58 | ## cookies 59 | cookie_crate = {version = "0.15", package = "cookie", optional = true} 60 | cookie_store = {version = "0.15", optional = true} 61 | proc-macro-hack = {version = "0.5.19", optional = true} 62 | 63 | ## compression 64 | flate2 = {version = "^1.0.24"} 65 | 66 | [dev-dependencies] 67 | # criterion = {git = "https://github.com/bheisler/criterion.rs", branch = "version-0.4", default-features = false} 68 | submillisecond = {version = "0.3", features = [ 69 | "cookies", 70 | "json", 71 | "logging", 72 | "query", 73 | "websocket", 74 | ]}# for examples 75 | 76 | [[example]] 77 | name = "blocking" 78 | path = "examples/blocking.rs" 79 | 80 | [[example]] 81 | name = "json_dynamic" 82 | path = "examples/json_dynamic.rs" 83 | # required-features = ["json"] 84 | 85 | [[example]] 86 | name = "json_typed" 87 | path = "examples/json_typed.rs" 88 | # required-features = ["json"] 89 | 90 | [[example]] 91 | name = "form" 92 | path = "examples/form.rs" 93 | 94 | [[example]] 95 | name = "simple" 96 | path = "examples/simple.rs" 97 | 98 | [[test]] 99 | name = "blocking" 100 | path = "tests/blocking.rs" 101 | 102 | [[test]] 103 | name = "cookie" 104 | path = "tests/cookie.rs" 105 | required-features = ["cookies"] 106 | 107 | [[test]] 108 | name = "gzip" 109 | path = "tests/gzip.rs" 110 | 111 | [[test]] 112 | name = "brotli" 113 | path = "tests/brotli.rs" 114 | 115 | [[test]] 116 | name = "deflate" 117 | path = "tests/deflate.rs" 118 | 119 | [[test]] 120 | name = "chunked" 121 | path = "tests/chunked.rs" 122 | 123 | # [[test]] 124 | # name = "multipart" 125 | # path = "tests/multipart.rs" 126 | # required-features = ["multipart"] 127 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Sean McArthur 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sean McArthur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nightfly 2 | 3 | This project is an ongoing effort to port the reqwest library to the lunatic runtime 4 | 5 | ## What works: 6 | 7 | * [x] json, text and bytes for request and response bodies 8 | * [x] decompression with brotli, gzip and deflate 9 | * [x] redirect handling 10 | * [x] cookies 11 | * [x] chunked responses 12 | * [x] handling of multiple open tcp streams per client 13 | * [x] timeouts (needs some more testing) 14 | * [ ] Piping of responses (requires chunk-encoding) 15 | * [ ] pooling of connections (needs more usage of lib to find a good approach) 16 | * [ ] proxy handling 17 | * [ ] upgrade, socks5 support and websockets 18 | * [ ] custom dns resolver 19 | 20 | 21 | 22 | [![MIT/Apache-2 licensed](https://img.shields.io/crates/l/nightfly.svg)](./LICENSE-APACHE) 23 | [![CI](https://github.com/SquattingSocrates/nightfly/workflows/CI/badge.svg)](https://github.com/SquattingSocrates/nightfly/actions?query=workflow%3ACI) 24 | 25 | An ergonomic, batteries-included HTTP Client for the lunatic runtime written in Rust. 26 | 27 | - Plain bodies, JSON, urlencoded, multipart (see examples) 28 | - Redirects with different policies 29 | - HTTPS via lunatic-native TLS (see examples) 30 | - Cookie Store 31 | - Customizable function-based redirect policy (IN PROGRESS) 32 | - HTTP Proxies (IN PROGRESS) 33 | 34 | 35 | ## Example 36 | 37 | This example uses [Lunatic](https://lunatic.rs) and enables some 38 | optional features, so your `Cargo.toml` could look like this: 39 | 40 | ```toml 41 | [dependencies] 42 | nightfly = { "0.1.0" } 43 | lunatic = { "0.12.0" } 44 | ``` 45 | 46 | And then the code: 47 | 48 | ```rust,no_run 49 | use std::collections::HashMap; 50 | 51 | #[lunatic::main] 52 | fn main() { 53 | let resp = nightfly::get("https://httpbin.org/ip") 54 | .unwrap() 55 | .json::>() 56 | .unwrap(); 57 | println!("{:#?}", resp); 58 | Ok(()) 59 | } 60 | ``` 61 | 62 | ## Requirements 63 | 64 | - A running version of the [lunatic VM](https://github.com/lunatic-solutions/lunatic). 65 | 66 | ## License 67 | 68 | Licensed under either of 69 | 70 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0) 71 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 72 | 73 | ### Contribution 74 | 75 | Unless you explicitly state otherwise, any contribution intentionally submitted 76 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 77 | be dual licensed as above, without any additional terms or conditions. 78 | -------------------------------------------------------------------------------- /examples/blocking.rs: -------------------------------------------------------------------------------- 1 | //! `cargo run --example blocking --features=blocking` 2 | #![deny(warnings)] 3 | 4 | fn main() -> Result<(), Box> { 5 | // Some simple CLI args requirements... 6 | let url = match std::env::args().nth(1) { 7 | Some(url) => url, 8 | None => { 9 | println!("No CLI URL provided, using default."); 10 | "https://hyper.rs".into() 11 | } 12 | }; 13 | 14 | eprintln!("Fetching {:?}...", url); 15 | 16 | // nightfly::blocking::get() is a convenience function. 17 | // 18 | // In most cases, you should create/build a nightfly::Client and reuse 19 | // it for all requests. 20 | let res = nightfly::get(url)?; 21 | 22 | eprintln!("Response: {:?} {}", res.version(), res.status()); 23 | eprintln!("Headers: {:#?}\n", res.headers()); 24 | 25 | // copy the response body directly to stdout 26 | println!("Body: {:?}", res.text()); 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/brotli.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | 6 | // This is using the `lunatic` runtime. 7 | // 8 | #[lunatic::main] 9 | fn main(_: Mailbox<()>) -> () { 10 | // Some simple CLI args requirements... 11 | let url = match std::env::args().nth(1) { 12 | Some(url) => url, 13 | None => { 14 | println!("No CLI URL provided, using default."); 15 | "http://eu.httpbin.org/brotli".into() 16 | } 17 | }; 18 | 19 | eprintln!("Fetching {:?}...", url); 20 | 21 | // nightfly::get() is a convenience function. 22 | // 23 | // In most cases, you should create/build a nightfly::Client and reuse 24 | // it for all requests. 25 | let res = nightfly::get(url).unwrap(); 26 | 27 | eprintln!("Response: {:?} {}", res.version(), res.status()); 28 | eprintln!("Headers: {:#?}\n", res.headers()); 29 | 30 | let body = res.text().unwrap(); 31 | 32 | println!("BODY {}", body); 33 | 34 | // Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/chunked.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use flate2::{read::GzEncoder, Compression}; 4 | use lunatic::{spawn_link, Mailbox}; 5 | use nightfly::Client; 6 | use submillisecond::{response::Response as SubmsResponse, router, Application, RequestContext}; 7 | 8 | static CHUNKS: [&str; 10] = [ 9 | "4\r\n", 10 | "Wiki\r\n", 11 | "6\r\n", 12 | "pedia \r\n", 13 | "E\r\n", 14 | "in \r\n", 15 | "\r\n", 16 | "chunks.\r\n", 17 | "0\r\n", 18 | "\r\n", 19 | ]; 20 | 21 | // static GZIP: &str = "eJwLz8zOLEhNyUxUyMxTiCmKyQPh5IzSvOxiPQCZQQqZ"; 22 | // static GZIP: [u8; 44] = [ 23 | // // length = 22; 0x16 24 | // 31, 139, 8, 0, 0, 0, 0, 0, 4, 255, 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 25 | // // length = 21; 0x15 26 | // 83, 224, 229, 226, 229, 74, 206, 40, 205, 203, 46, 214, 3, 0, 102, 210, 154, 109, 24, 0, 0, 27 | // // length = 1; 0x1 28 | // 0, 29 | // ]; 30 | #[rustfmt::skip] 31 | static GZIP_CHUNKED: [u8; 66] = [ 32 | b'1', b'6', b'\r', b'\n', 33 | 31, 139, 8, 0, 0, 0, 0, 0, 4, 255, 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, b'\r', b'\n', 34 | b'1', b'5', b'\r', b'\n', 35 | 83, 224, 229, 226, 229, 74, 206, 40, 205, 203, 46, 214, 3, 0, 102, 210, 154, 109, 24, 0, 0, b'\r', b'\n', 36 | // single byte in last chunk 37 | b'1', b'\r', b'\n', 38 | 0, b'\r', b'\n', 39 | // zero length chunk 40 | b'0', b'\r', b'\n', 41 | // end of data 42 | b'\r', b'\n', 43 | ]; 44 | 45 | // original buffer 46 | // static DEFLATE: [u8; 24] = [ 47 | // 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 83, 224, 226, 74, 206, 40, 205, 203, 46, 48 | // 214, 3, 0, 49 | // ]; 50 | 51 | #[rustfmt::skip] 52 | static DEFLATE_CHUNKED: [u8; 38] = [ 53 | b'1', b'5', b'\r', b'\n', 54 | 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 83, 224, 226, 74, 206, 40, 205, 203, 46, 55 | // three bytes in last chunk 56 | b'3', b'\r', b'\n', 57 | 214, 3, 0, b'\r', b'\n', 58 | // zero length chunk 59 | b'0', b'\r', b'\n', // end of data 60 | b'\r', b'\n', 61 | ]; 62 | 63 | fn chunked() -> SubmsResponse { 64 | SubmsResponse::builder() 65 | .header("Transfer-Encoding", "chunked") 66 | .header("Content-Length", "24") 67 | .body(CHUNKS.join("").to_owned().into_bytes()) 68 | .unwrap() 69 | } 70 | 71 | fn chunked_gzip() -> SubmsResponse { 72 | SubmsResponse::builder() 73 | .header("Transfer-Encoding", "chunked") 74 | .header("Content-encoding", "gzip") 75 | .body(GZIP_CHUNKED.to_vec()) 76 | .unwrap() 77 | } 78 | 79 | fn chunked_deflate() -> SubmsResponse { 80 | SubmsResponse::builder() 81 | .header("Transfer-Encoding", "chunked") 82 | .header("Content-encoding", "deflate") 83 | .body(DEFLATE_CHUNKED.to_vec()) 84 | .unwrap() 85 | } 86 | 87 | static ADDR: &'static str = "0.0.0.0:3001"; 88 | pub type RouterFn = 89 | fn() -> fn(req: ::submillisecond::RequestContext) -> ::submillisecond::response::Response; 90 | 91 | static ROUTER: RouterFn = router! { 92 | GET "/chunked" => chunked 93 | GET "/gzip" => chunked_gzip 94 | GET "/deflate" => chunked_deflate 95 | }; 96 | 97 | fn start_server() { 98 | Application::new(ROUTER).serve(ADDR).unwrap(); 99 | } 100 | 101 | #[lunatic::main] 102 | fn main(_: Mailbox<()>) -> () { 103 | spawn_link!(|| { start_server() }); 104 | let mut ret_vec = Vec::new(); 105 | let input = "Wikipedia in \r\n\r\nchunks.".to_string().into_bytes(); 106 | let mut gz = GzEncoder::new(&input[..], Compression::fast()); 107 | let _count = gz.read_to_end(&mut ret_vec).unwrap(); 108 | println!("ENCODED GZ {:?}", ret_vec); 109 | // Some simple CLI args requirements... 110 | let url = match std::env::args().nth(1) { 111 | Some(url) => url, 112 | None => { 113 | println!("No CLI URL provided, using default."); 114 | "http://0.0.0.0:3001/gzip".into() 115 | } 116 | }; 117 | 118 | eprintln!("Fetching {:?}...", url); 119 | 120 | // nightfly::get() is a convenience function. 121 | // 122 | // In most cases, you should create/build a nightfly::Client and reuse 123 | // it for all requests. 124 | let client = Client::new(); 125 | let res = client.get(url).send().unwrap(); 126 | 127 | eprintln!("Response: {:?} {}", res.version(), res.status()); 128 | eprintln!("Headers: {:#?}\n", res.headers()); 129 | 130 | let body = res.text().unwrap(); 131 | 132 | println!("{}", body); 133 | 134 | // second call 135 | // Some simple CLI args requirements... 136 | let url = match std::env::args().nth(1) { 137 | Some(url) => url, 138 | None => { 139 | println!("No CLI URL provided, using default."); 140 | "http://0.0.0.0:3001/gzip".into() 141 | } 142 | }; 143 | 144 | eprintln!("Fetching {:?}...", url); 145 | 146 | // nightfly::get() is a convenience function. 147 | // 148 | // In most cases, you should create/build a nightfly::Client and reuse 149 | // it for all requests. 150 | let res = client.get(url).send().unwrap(); 151 | 152 | eprintln!("Response: {:?} {}", res.version(), res.status()); 153 | eprintln!("Headers: {:#?}\n", res.headers()); 154 | 155 | let body = res.text().unwrap(); 156 | 157 | println!("{}", body); 158 | } 159 | -------------------------------------------------------------------------------- /examples/cookies.rs: -------------------------------------------------------------------------------- 1 | //! `cargo run --example cookies --features=features` 2 | #![deny(warnings)] 3 | 4 | fn main() -> Result<(), Box> { 5 | // Some simple CLI args requirements... 6 | let url = match std::env::args().nth(1) { 7 | Some(url) => url, 8 | None => { 9 | println!("No CLI URL provided, using default."); 10 | "http://eu.httpbin.org/cookies".into() 11 | } 12 | }; 13 | 14 | eprintln!("Fetching {:?}...", url); 15 | 16 | // nightfly::blocking::get() is a convenience function. 17 | // 18 | // In most cases, you should create/build a nightfly::Client and reuse 19 | // it for all requests. 20 | let res = nightfly::get(url)?; 21 | 22 | eprintln!("Response: {:?} {}", res.version(), res.status()); 23 | eprintln!("Headers: {:#?}\n", res.headers()); 24 | 25 | // copy the response body directly to stdout 26 | println!("Body: {:?}", res.text()); 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/deflate.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | 6 | // This is using the `lunatic` runtime. 7 | // 8 | #[lunatic::main] 9 | fn main(_: Mailbox<()>) -> () { 10 | // Some simple CLI args requirements... 11 | let url = match std::env::args().nth(1) { 12 | Some(url) => url, 13 | None => { 14 | println!("No CLI URL provided, using default."); 15 | // "https://hyper.rs".into() 16 | // "http://localhost:3000".into() 17 | "http://eu.httpbin.org/deflate".into() 18 | } 19 | }; 20 | 21 | eprintln!("Fetching {:?}...", url); 22 | 23 | // nightfly::get() is a convenience function. 24 | // 25 | // In most cases, you should create/build a nightfly::Client and reuse 26 | // it for all requests. 27 | let res = nightfly::get(url).unwrap(); 28 | 29 | eprintln!("Response: {:?} {}", res.version(), res.status()); 30 | eprintln!("Headers: {:#?}\n", res.headers()); 31 | 32 | let body = res.text().unwrap(); 33 | 34 | println!("{}", body); 35 | 36 | // Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/form.rs: -------------------------------------------------------------------------------- 1 | use lunatic::Mailbox; 2 | 3 | // Short example of a POST request with form data. 4 | // 5 | // 6 | #[lunatic::main] 7 | fn main(_: Mailbox<()>) { 8 | let response = nightfly::Client::new() 9 | .post("http://www.baidu.com") 10 | .form(&[("one", "1")]) 11 | .send() 12 | .expect("send"); 13 | println!("Response status {}", response.status()); 14 | } 15 | -------------------------------------------------------------------------------- /examples/gzip.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | 6 | // This is using the `lunatic` runtime. 7 | // 8 | #[lunatic::main] 9 | fn main(_: Mailbox<()>) -> () { 10 | // Some simple CLI args requirements... 11 | let url = match std::env::args().nth(1) { 12 | Some(url) => url, 13 | None => { 14 | println!("No CLI URL provided, using default."); 15 | "http://eu.httpbin.org/gzip".into() 16 | } 17 | }; 18 | 19 | eprintln!("Fetching {:?}...", url); 20 | 21 | // nightfly::get() is a convenience function. 22 | // 23 | // In most cases, you should create/build a nightfly::Client and reuse 24 | // it for all requests. 25 | let res = nightfly::get(url).unwrap(); 26 | 27 | eprintln!("Response: {:?} {}", res.version(), res.status()); 28 | eprintln!("Headers: {:#?}\n", res.headers()); 29 | 30 | let body = res.text().unwrap(); 31 | 32 | println!("{}", body); 33 | 34 | // Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/json_dynamic.rs: -------------------------------------------------------------------------------- 1 | //! This example illustrates the way to send and receive arbitrary JSON. 2 | //! 3 | //! This is useful for some ad-hoc experiments and situations when you don't 4 | //! really care about the structure of the JSON and just need to display it or 5 | //! process it at runtime. 6 | 7 | use lunatic::Mailbox; 8 | 9 | #[lunatic::main] 10 | fn main(_: Mailbox<()>) -> Result<(), nightfly::Error> { 11 | let echo_json: serde_json::Value = nightfly::Client::builder() 12 | .user_agent("my-own-user-agent") 13 | .build() 14 | .unwrap() 15 | .post("http://eu.httpbin.org/anything") 16 | .json(&serde_json::json!({ 17 | "title": "Nightfly.rs", 18 | "body": "https://docs.rs/nightfly", 19 | "userId": 1 20 | })) 21 | .send() 22 | .unwrap() 23 | .json() 24 | .unwrap(); 25 | 26 | println!("{:#?}", echo_json); 27 | // Object( 28 | // { 29 | // "body": String( 30 | // "https://docs.rs/nightfly" 31 | // ), 32 | // "id": Number( 33 | // 101 34 | // ), 35 | // "title": String( 36 | // "Nightfly.rs" 37 | // ), 38 | // "userId": Number( 39 | // 1 40 | // ) 41 | // } 42 | // ) 43 | // Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /examples/json_typed.rs: -------------------------------------------------------------------------------- 1 | //! This example illustrates the way to send and receive statically typed JSON. 2 | //! 3 | //! In contrast to the arbitrary JSON example, this brings up the full power of 4 | //! Rust compile-time type system guaranties though it requires a little bit 5 | //! more code. 6 | 7 | use std::collections::HashMap; 8 | 9 | use lunatic::Mailbox; 10 | // These require the `serde` dependency. 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | struct Post { 15 | id: Option, 16 | title: String, 17 | body: String, 18 | #[serde(rename = "userId")] 19 | user_id: i32, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | struct AnythingResponse { 24 | args: HashMap, 25 | data: String, 26 | files: HashMap, 27 | form: HashMap, 28 | headers: HashMap, 29 | json: Option, 30 | method: String, 31 | origin: String, 32 | url: String, 33 | } 34 | 35 | // This is using the `lunatic` runtime 36 | // 37 | #[lunatic::main] 38 | fn main(_: Mailbox<()>) -> Result<(), nightfly::Error> { 39 | let new_post = Post { 40 | id: None, 41 | title: "Nightfly.rs".into(), 42 | body: "https://docs.rs/nightfly".into(), 43 | user_id: 1, 44 | }; 45 | let new_post: AnythingResponse = nightfly::Client::new() 46 | .post("http://eu.httpbin.org/anything") 47 | .json(&new_post) 48 | .send() 49 | .unwrap() 50 | .json() 51 | .unwrap(); 52 | 53 | println!("{:#?}", new_post); 54 | // Post { 55 | // id: Some( 56 | // 101 57 | // ), 58 | // title: "Nightfly.rs", 59 | // body: "https://docs.rs/nightfly", 60 | // user_id: 1 61 | // } 62 | // Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /examples/multiple_hosts.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | use nightfly::Client; 6 | 7 | // This is using the `lunatic` runtime. 8 | // 9 | #[lunatic::main] 10 | fn main(_: Mailbox<()>) -> () { 11 | // first, start the client pool 12 | let client = Client::new(); 13 | let res1 = client.get("https://hyper.rs").send().unwrap(); 14 | println!("Call to https://hyper.rs {}", res1.text().unwrap()); 15 | 16 | let res2 = client 17 | .get("http://anglesharp.azurewebsites.net/Chunked") 18 | .send() 19 | .unwrap(); 20 | 21 | println!( 22 | "Delayed chunking test at http://anglesharp.azurewebsites.net/Chunked {}", 23 | res2.text().unwrap() 24 | ); 25 | 26 | let res3 = client.get("https://rust-lang.org").send().unwrap(); 27 | println!("Call to https://rust-lang.org {}", res3.text().unwrap()); 28 | } 29 | -------------------------------------------------------------------------------- /examples/redirects.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | 6 | // This is using the `lunatic` runtime. 7 | // 8 | #[lunatic::main] 9 | fn main(_: Mailbox<()>) -> () { 10 | // Some simple CLI args requirements... 11 | let url = match std::env::args().nth(1) { 12 | Some(url) => url, 13 | None => { 14 | println!("No CLI URL provided, using default."); 15 | "http://eu.httpbin.org/redirect-to?url=%2Fget".into() 16 | } 17 | }; 18 | 19 | eprintln!("Fetching {:?}...", url); 20 | 21 | // nightfly::get() is a convenience function. 22 | // 23 | // In most cases, you should create/build a nightfly::Client and reuse 24 | // it for all requests. 25 | let res = nightfly::get(url).unwrap(); 26 | 27 | eprintln!("Response: {:?} {}", res.version(), res.status()); 28 | eprintln!("Headers: {:#?}\n", res.headers()); 29 | 30 | let body = res.text().unwrap(); 31 | 32 | println!("BODY {}", body); 33 | 34 | // Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | 6 | // This is using the `lunatic` runtime. 7 | // 8 | #[lunatic::main] 9 | fn main(_: Mailbox<()>) -> () { 10 | // Some simple CLI args requirements... 11 | let url = match std::env::args().nth(1) { 12 | Some(url) => url, 13 | None => { 14 | println!("No CLI URL provided, using default."); 15 | // "https://hyper.rs".into() 16 | // "http://localhost:3000".into() 17 | "http://eu.httpbin.org/get".into() 18 | } 19 | }; 20 | 21 | eprintln!("Fetching {:?}...", url); 22 | 23 | // nightfly::get() is a convenience function. 24 | // 25 | // In most cases, you should create/build a nightfly::Client and reuse 26 | // it for all requests. 27 | let res = nightfly::get(url).unwrap(); 28 | 29 | eprintln!("Response: {:?} {}", res.version(), res.status()); 30 | eprintln!("Headers: {:#?}\n", res.headers()); 31 | 32 | let body = res.text().unwrap(); 33 | 34 | println!("{}", body); 35 | 36 | // Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/twitter.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | extern crate nightfly; 3 | 4 | use lunatic::Mailbox; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | struct TwitterUser { 9 | id: String, 10 | name: String, 11 | #[serde(rename = "username")] 12 | user_name: String, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | struct TwitterResponse { 17 | data: T, 18 | } 19 | 20 | // This is using the `lunatic` runtime. 21 | // 22 | #[lunatic::main] 23 | fn main(_: Mailbox<()>) -> () { 24 | // nightfly::get() is a convenience function. 25 | // 26 | // In most cases, you should create/build a nightfly::Client and reuse 27 | // it for all requests. 28 | let token = std::env::var("TWITTER_BEARER_TOKEN").expect("TWITTER_BEARER_TOKEN not set"); 29 | let res = nightfly::Client::new() 30 | .get("https://api.twitter.com/2/users/by/username/NASA") 31 | .header("Authorization", &format!("Bearer {}", token)) 32 | .header("Accept", "application/json") 33 | // choose encoding if desired 34 | // .header("Accept-Encoding", "gzip") 35 | .send() 36 | .unwrap(); 37 | 38 | eprintln!("Response: {:?} {}", res.version(), res.status()); 39 | eprintln!("Headers: {:#?}\n", res.headers()); 40 | 41 | let body: TwitterResponse = res.json().unwrap(); 42 | 43 | println!("Loaded twitter user {body:?}"); 44 | } 45 | -------------------------------------------------------------------------------- /src/cookie.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Cookies 2 | 3 | use std::convert::TryInto; 4 | use std::fmt; 5 | use std::sync::RwLock; 6 | use std::time::SystemTime; 7 | 8 | use crate::header::{HeaderValue, SET_COOKIE}; 9 | use bytes::Bytes; 10 | use http::HeaderMap; 11 | 12 | /// Actions for a persistent cookie store providing session support. 13 | pub trait CookieStore: Send + Sync { 14 | /// Store a set of Set-Cookie header values received from `url` 15 | fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url); 16 | /// Get any Cookie values in the store for `url` 17 | fn cookies(&self, url: &url::Url) -> Option; 18 | } 19 | 20 | /// A single HTTP cookie. 21 | pub struct Cookie<'a>(cookie_crate::Cookie<'a>); 22 | 23 | /// A good default `CookieStore` implementation. 24 | /// 25 | /// This is the implementation used when simply calling `cookie_store(true)`. 26 | /// This type is exposed to allow creating one and filling it with some 27 | /// existing cookies more easily, before creating a `Client`. 28 | /// 29 | /// For more advanced scenarios, such as needing to serialize the store or 30 | /// manipulate it between requests, you may refer to the 31 | /// [reqwest_cookie_store crate](https://crates.io/crates/reqwest_cookie_store). 32 | #[derive(Debug, Default)] 33 | pub struct Jar(RwLock); 34 | 35 | // ===== impl Cookie ===== 36 | 37 | impl<'a> Cookie<'a> { 38 | fn parse(value: &'a HeaderValue) -> Result, CookieParseError> { 39 | std::str::from_utf8(value.as_bytes()) 40 | .map_err(cookie_crate::ParseError::from) 41 | .and_then(cookie_crate::Cookie::parse) 42 | .map_err(CookieParseError) 43 | .map(Cookie) 44 | } 45 | 46 | /// The name of the cookie. 47 | pub fn name(&self) -> &str { 48 | self.0.name() 49 | } 50 | 51 | /// The value of the cookie. 52 | pub fn value(&self) -> &str { 53 | self.0.value() 54 | } 55 | 56 | /// Returns true if the 'HttpOnly' directive is enabled. 57 | pub fn http_only(&self) -> bool { 58 | self.0.http_only().unwrap_or(false) 59 | } 60 | 61 | /// Returns true if the 'Secure' directive is enabled. 62 | pub fn secure(&self) -> bool { 63 | self.0.secure().unwrap_or(false) 64 | } 65 | 66 | /// Returns true if 'SameSite' directive is 'Lax'. 67 | pub fn same_site_lax(&self) -> bool { 68 | self.0.same_site() == Some(cookie_crate::SameSite::Lax) 69 | } 70 | 71 | /// Returns true if 'SameSite' directive is 'Strict'. 72 | pub fn same_site_strict(&self) -> bool { 73 | self.0.same_site() == Some(cookie_crate::SameSite::Strict) 74 | } 75 | 76 | /// Returns the path directive of the cookie, if set. 77 | pub fn path(&self) -> Option<&str> { 78 | self.0.path() 79 | } 80 | 81 | /// Returns the domain directive of the cookie, if set. 82 | pub fn domain(&self) -> Option<&str> { 83 | self.0.domain() 84 | } 85 | 86 | /// Get the Max-Age information. 87 | pub fn max_age(&self) -> Option { 88 | self.0.max_age().map(|d| { 89 | d.try_into() 90 | .expect("time::Duration into std::time::Duration") 91 | }) 92 | } 93 | 94 | /// The cookie expiration time. 95 | pub fn expires(&self) -> Option { 96 | match self.0.expires() { 97 | Some(cookie_crate::Expiration::DateTime(offset)) => Some(SystemTime::from(offset)), 98 | None | Some(cookie_crate::Expiration::Session) => None, 99 | } 100 | } 101 | } 102 | 103 | impl<'a> fmt::Debug for Cookie<'a> { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 | self.0.fmt(f) 106 | } 107 | } 108 | 109 | pub(crate) fn extract_response_cookie_headers( 110 | headers: &HeaderMap, 111 | ) -> impl Iterator { 112 | headers.get_all(SET_COOKIE).iter() 113 | } 114 | 115 | pub(crate) fn extract_response_cookies( 116 | headers: &HeaderMap, 117 | ) -> impl Iterator> { 118 | headers.get_all(SET_COOKIE).iter().map(Cookie::parse) 119 | } 120 | 121 | /// Error representing a parse failure of a 'Set-Cookie' header. 122 | pub(crate) struct CookieParseError(cookie_crate::ParseError); 123 | 124 | impl fmt::Debug for CookieParseError { 125 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 126 | self.0.fmt(f) 127 | } 128 | } 129 | 130 | impl fmt::Display for CookieParseError { 131 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 132 | self.0.fmt(f) 133 | } 134 | } 135 | 136 | impl std::error::Error for CookieParseError {} 137 | 138 | // ===== impl Jar ===== 139 | 140 | impl Jar { 141 | /// Add a cookie to this jar. 142 | /// 143 | /// # Example 144 | /// 145 | /// ``` 146 | /// use reqwest::{cookie::Jar, Url}; 147 | /// 148 | /// let cookie = "foo=bar; Domain=yolo.local"; 149 | /// let url = "https://yolo.local".parse::().unwrap(); 150 | /// 151 | /// let jar = Jar::default(); 152 | /// jar.add_cookie_str(cookie, &url); 153 | /// 154 | /// // and now add to a `ClientBuilder`? 155 | /// ``` 156 | pub fn add_cookie_str(&self, cookie: &str, url: &url::Url) { 157 | let cookies = cookie_crate::Cookie::parse(cookie) 158 | .ok() 159 | .map(|c| c.into_owned()) 160 | .into_iter(); 161 | self.0.write().unwrap().store_response_cookies(cookies, url); 162 | } 163 | } 164 | 165 | impl CookieStore for Jar { 166 | fn set_cookies(&self, cookie_headers: &mut dyn Iterator, url: &url::Url) { 167 | let iter = 168 | cookie_headers.filter_map(|val| Cookie::parse(val).map(|c| c.0.into_owned()).ok()); 169 | 170 | self.0.write().unwrap().store_response_cookies(iter, url); 171 | } 172 | 173 | fn cookies(&self, url: &url::Url) -> Option { 174 | let s = self 175 | .0 176 | .read() 177 | .unwrap() 178 | .get_request_values(url) 179 | .map(|(name, value)| format!("{}={}", name, value)) 180 | .collect::>() 181 | .join("; "); 182 | 183 | if s.is_empty() { 184 | return None; 185 | } 186 | 187 | HeaderValue::from_maybe_shared(Bytes::from(s)).ok() 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use std::error::Error as StdError; 4 | use std::fmt; 5 | use std::io; 6 | 7 | use crate::SerializableResponse; 8 | use crate::{StatusCode, Url}; 9 | 10 | /// A `Result` alias where the `Err` case is `nightfly::Error`. 11 | pub type Result = std::result::Result; 12 | 13 | #[derive(Serialize, Deserialize)] 14 | pub struct ResponseResult { 15 | result: std::result::Result, 16 | } 17 | 18 | /// The Errors that may occur when processing a `Request`. 19 | /// 20 | /// Note: Errors may include the full URL used to make the `Request`. If the URL 21 | /// contains sensitive information (e.g. an API key as a query parameter), be 22 | /// sure to remove it ([`without_url`](Error::without_url)) 23 | #[derive(Serialize, Deserialize, Clone)] 24 | pub struct Error { 25 | inner: Box, 26 | } 27 | 28 | pub(crate) type BoxError = Box; 29 | 30 | #[derive(Serialize, Deserialize)] 31 | struct Inner { 32 | kind: Kind, 33 | #[serde(skip)] 34 | source: Option, 35 | url: Option, 36 | } 37 | 38 | impl Clone for Inner { 39 | fn clone(&self) -> Self { 40 | Inner { 41 | kind: self.kind.clone(), 42 | source: None, 43 | url: self.url.clone(), 44 | } 45 | } 46 | } 47 | 48 | impl Error { 49 | pub(crate) fn new(kind: Kind, source: Option) -> Error 50 | where 51 | E: Into, 52 | { 53 | Error { 54 | inner: Box::new(Inner { 55 | kind, 56 | source: source.map(Into::into), 57 | url: None, 58 | }), 59 | } 60 | } 61 | 62 | /// Returns a possible URL related to this error. 63 | /// 64 | /// # Examples 65 | /// 66 | /// ``` 67 | /// # fn run() { 68 | /// // displays last stop of a redirect loop 69 | /// let response = nightfly::get("http://site.with.redirect.loop"); 70 | /// if let Err(e) = response { 71 | /// if e.is_redirect() { 72 | /// if let Some(final_stop) = e.url() { 73 | /// println!("redirect loop at {}", final_stop); 74 | /// } 75 | /// } 76 | /// } 77 | /// # } 78 | /// ``` 79 | pub fn url(&self) -> Option<&Url> { 80 | self.inner.url.as_ref() 81 | } 82 | 83 | /// Returns a mutable reference to the URL related to this error 84 | /// 85 | /// This is useful if you need to remove sensitive information from the URL 86 | /// (e.g. an API key in the query), but do not want to remove the URL 87 | /// entirely. 88 | pub fn url_mut(&mut self) -> Option<&mut Url> { 89 | self.inner.url.as_mut() 90 | } 91 | 92 | /// Add a url related to this error (overwriting any existing) 93 | pub fn with_url(mut self, url: Url) -> Self { 94 | self.inner.url = Some(url); 95 | self 96 | } 97 | 98 | /// Strip the related url from this error (if, for example, it contains 99 | /// sensitive information) 100 | pub fn without_url(mut self) -> Self { 101 | self.inner.url = None; 102 | self 103 | } 104 | 105 | /// Returns true if the error is from a type Builder. 106 | pub fn is_builder(&self) -> bool { 107 | matches!(self.inner.kind, Kind::Builder) 108 | } 109 | 110 | /// Returns true if the error is from a `RedirectPolicy`. 111 | pub fn is_redirect(&self) -> bool { 112 | matches!(self.inner.kind, Kind::Redirect) 113 | } 114 | 115 | /// Returns true if the error is from `Response::error_for_status`. 116 | pub fn is_status(&self) -> bool { 117 | matches!(self.inner.kind, Kind::Status(_)) 118 | } 119 | 120 | /// Returns true if the error is related to a timeout. 121 | pub fn is_timeout(&self) -> bool { 122 | let mut source = self.source(); 123 | 124 | while let Some(err) = source { 125 | if err.is::() { 126 | return true; 127 | } 128 | source = err.source(); 129 | } 130 | 131 | false 132 | } 133 | 134 | /// Returns true if the error is related to the request 135 | pub fn is_request(&self) -> bool { 136 | matches!(self.inner.kind, Kind::Request) 137 | } 138 | 139 | /// Returns true if the error is related to the request or response body 140 | // pub fn is_body(&self) -> bool { 141 | // matches!(self.inner.kind, Kind::Body) 142 | // } 143 | 144 | /// Returns true if the error is related to the serialisation of the body 145 | pub fn is_serialization(&self) -> bool { 146 | matches!(self.inner.kind, Kind::Serialization) 147 | } 148 | 149 | /// Returns true if the error is related to decoding the response's body 150 | pub fn is_decode(&self) -> bool { 151 | matches!(self.inner.kind, Kind::Decode) 152 | } 153 | 154 | /// Returns the status code, if the error was generated from a response. 155 | pub fn status(&self) -> Option { 156 | match self.inner.kind { 157 | Kind::Status(code) => Some(StatusCode::from_u16(code).unwrap()), 158 | _ => None, 159 | } 160 | } 161 | 162 | // private 163 | 164 | #[allow(unused)] 165 | pub(crate) fn into_io(self) -> io::Error { 166 | io::Error::new(io::ErrorKind::Other, self) 167 | } 168 | } 169 | 170 | impl fmt::Debug for Error { 171 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 172 | let mut builder = f.debug_struct("nightfly::Error"); 173 | 174 | builder.field("kind", &self.inner.kind); 175 | 176 | if let Some(ref url) = self.inner.url { 177 | builder.field("url", url); 178 | } 179 | if let Some(ref source) = self.inner.source { 180 | builder.field("source", source); 181 | } 182 | 183 | builder.finish() 184 | } 185 | } 186 | 187 | impl fmt::Display for Error { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | match self.inner.kind { 190 | Kind::Builder => f.write_str("builder error")?, 191 | Kind::Request => f.write_str("error sending request")?, 192 | Kind::Body => f.write_str("request or response body error")?, 193 | Kind::Decode => f.write_str("error decoding response body")?, 194 | Kind::Redirect => f.write_str("error following redirect")?, 195 | Kind::Serialization => f.write_str("error while serialising body")?, 196 | // Kind::Upgrade => f.write_str("error upgrading connection")?, 197 | Kind::Status(ref code) => { 198 | let status = StatusCode::from_u16(*code).unwrap(); 199 | let prefix = if status.is_client_error() { 200 | "HTTP status client error" 201 | } else { 202 | debug_assert!(status.is_server_error()); 203 | "HTTP status server error" 204 | }; 205 | write!(f, "{} ({})", prefix, code)?; 206 | } 207 | }; 208 | 209 | if let Some(url) = &self.inner.url { 210 | write!(f, " for url ({})", url.as_str())?; 211 | } 212 | 213 | if let Some(e) = &self.inner.source { 214 | write!(f, ": {}", e)?; 215 | } 216 | 217 | Ok(()) 218 | } 219 | } 220 | 221 | impl StdError for Error { 222 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 223 | self.inner.source.as_ref().map(|e| &**e as _) 224 | } 225 | } 226 | 227 | #[derive(Debug, Serialize, Deserialize, Clone)] 228 | pub(crate) enum Kind { 229 | Builder, 230 | Request, 231 | Redirect, 232 | Status(u16), 233 | Body, 234 | Decode, 235 | Serialization, 236 | // Upgrade, 237 | } 238 | 239 | // constructors 240 | 241 | pub(crate) fn builder>(e: E) -> Error { 242 | Error::new(Kind::Builder, Some(e)) 243 | } 244 | 245 | pub(crate) fn serialization>(e: E) -> Error { 246 | Error::new(Kind::Serialization, Some(e)) 247 | } 248 | 249 | // pub(crate) fn body>(e: E) -> Error { 250 | // Error::new(Kind::Body, Some(e)) 251 | // } 252 | 253 | pub(crate) fn decode>(e: E) -> Error { 254 | Error::new(Kind::Decode, Some(e)) 255 | } 256 | 257 | pub(crate) fn request>(e: E) -> Error { 258 | Error::new(Kind::Request, Some(e)) 259 | } 260 | 261 | pub(crate) fn timeout(url: Url) -> Error { 262 | Error::new(Kind::Request, Some(TimedOut)).with_url(url) 263 | } 264 | 265 | pub(crate) fn redirect>(e: E, url: Url) -> Error { 266 | Error::new(Kind::Redirect, Some(e)).with_url(url) 267 | } 268 | 269 | pub(crate) fn status_code(url: Url, status: StatusCode) -> Error { 270 | Error::new(Kind::Status(status.as_u16()), None::).with_url(url) 271 | } 272 | 273 | pub(crate) fn url_bad_scheme(url: Url) -> Error { 274 | Error::new(Kind::Builder, Some(BadScheme)).with_url(url) 275 | } 276 | 277 | // pub(crate) fn upgrade>(e: E) -> Error { 278 | // Error::new(Kind::Upgrade, Some(e)) 279 | // } 280 | 281 | // io::Error helpers 282 | 283 | #[allow(unused)] 284 | pub(crate) fn into_io(e: Error) -> io::Error { 285 | e.into_io() 286 | } 287 | 288 | #[allow(unused)] 289 | pub(crate) fn decode_io(e: io::Error) -> Error { 290 | if e.get_ref().map(|r| r.is::()).unwrap_or(false) { 291 | *e.into_inner() 292 | .expect("io::Error::get_ref was Some(_)") 293 | .downcast::() 294 | .expect("StdError::is() was true") 295 | } else { 296 | decode(e) 297 | } 298 | } 299 | 300 | // internal Error "sources" 301 | 302 | #[derive(Debug)] 303 | pub(crate) struct TimedOut; 304 | 305 | impl fmt::Display for TimedOut { 306 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 307 | f.write_str("operation timed out") 308 | } 309 | } 310 | 311 | impl StdError for TimedOut {} 312 | 313 | #[derive(Debug)] 314 | pub(crate) struct BadScheme; 315 | 316 | impl fmt::Display for BadScheme { 317 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 318 | f.write_str("URL scheme is not allowed") 319 | } 320 | } 321 | 322 | impl StdError for BadScheme {} 323 | 324 | // #[cfg(test)] 325 | mod tests { 326 | use super::*; 327 | 328 | #[lunatic::test] 329 | fn mem_size_of() { 330 | use std::mem::size_of; 331 | assert_eq!(size_of::(), size_of::()); 332 | } 333 | 334 | #[lunatic::test] 335 | fn from_unknown_io_error() { 336 | let orig = io::Error::new(io::ErrorKind::Other, "orly"); 337 | let err = super::decode_io(orig); 338 | match err.inner.kind { 339 | Kind::Decode => (), 340 | _ => panic!("{:?}", err), 341 | } 342 | } 343 | 344 | #[lunatic::test] 345 | fn is_timeout() { 346 | let err = super::timeout(Url::parse("http://localhost:3000/api").unwrap()); 347 | assert!(err.is_timeout()); 348 | 349 | let io = io::Error::new(io::ErrorKind::Other, err); 350 | let nested = super::request(io); 351 | assert!(nested.is_timeout()); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/into_url.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | /// A trait to try to convert some type into a `Url`. 4 | /// 5 | /// This trait is "sealed", such that only types within nightfly can 6 | /// implement it. 7 | pub trait IntoUrl: IntoUrlSealed {} 8 | 9 | impl IntoUrl for Url {} 10 | impl IntoUrl for String {} 11 | impl<'a> IntoUrl for &'a str {} 12 | impl<'a> IntoUrl for &'a String {} 13 | 14 | pub trait IntoUrlSealed { 15 | // Besides parsing as a valid `Url`, the `Url` must be a valid 16 | // `http::Uri`, in that it makes sense to use in a network request. 17 | fn into_url(self) -> crate::Result; 18 | 19 | fn as_str(&self) -> &str; 20 | } 21 | 22 | impl IntoUrlSealed for Url { 23 | fn into_url(self) -> crate::Result { 24 | if self.has_host() { 25 | Ok(self) 26 | } else { 27 | Err(crate::error::url_bad_scheme(self)) 28 | } 29 | } 30 | 31 | fn as_str(&self) -> &str { 32 | self.as_ref() 33 | } 34 | } 35 | 36 | impl<'a> IntoUrlSealed for &'a str { 37 | fn into_url(self) -> crate::Result { 38 | Url::parse(self).map_err(crate::error::builder)?.into_url() 39 | } 40 | 41 | fn as_str(&self) -> &str { 42 | self 43 | } 44 | } 45 | 46 | impl<'a> IntoUrlSealed for &'a String { 47 | fn into_url(self) -> crate::Result { 48 | (&**self).into_url() 49 | } 50 | 51 | fn as_str(&self) -> &str { 52 | self.as_ref() 53 | } 54 | } 55 | 56 | impl IntoUrlSealed for String { 57 | fn into_url(self) -> crate::Result { 58 | (&*self).into_url() 59 | } 60 | 61 | fn as_str(&self) -> &str { 62 | self.as_ref() 63 | } 64 | } 65 | 66 | // pub(crate) fn expect_uri(url: &Url) -> http::Uri { 67 | // url.as_str() 68 | // .parse() 69 | // .expect("a parsed Url should always be a valid Uri") 70 | // } 71 | 72 | pub(crate) fn try_uri(url: &Url) -> Option { 73 | url.as_str().parse().ok() 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | 80 | #[lunatic::test] 81 | fn into_url_file_scheme() { 82 | let err = "file:///etc/hosts".into_url().unwrap_err(); 83 | assert_eq!( 84 | err.to_string(), 85 | "builder error for url (file:///etc/hosts): URL scheme is not allowed" 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![deny(missing_debug_implementations)] 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | // #![cfg_attr(test, deny(warnings))] 5 | #![doc(html_root_url = "https://docs.rs/nightfly/0.1.4")] 6 | 7 | //! # nightfly 8 | //! 9 | //! The `nightfly` crate provides a convenient, higher-level HTTP 10 | //! [`Client`][client]. 11 | //! 12 | //! It handles many of the things that most people just expect an HTTP client 13 | //! to do for them. 14 | //! 15 | //! - Reusable, cloneable and serialisable Clients 16 | //! - Plain bodies, [JSON](#json), [urlencoded](#forms) 17 | //! - Customizable [redirect policy](#redirect-policies) 18 | //! - Uses vm-native [TLS] 19 | //! - Cookies 20 | //! 21 | //! Additional learning resources include: 22 | //! 23 | //! - [Nightfly Repository Examples](https://github.com/SquattingSocrates/nightfly/tree/master/examples) 24 | //! 25 | //! ## Making a GET request 26 | //! 27 | //! For a single request, you can use the [`get`][get] shortcut method. 28 | //! 29 | //! ```rust 30 | //! # fn run() -> Result<(), nightfly::Error> { 31 | //! let body = nightfly::get("https://www.rust-lang.org").text(); 32 | //! 33 | //! println!("body = {:?}", body); 34 | //! # Ok(()) 35 | //! # } 36 | //! ``` 37 | //! 38 | //! **NOTE**: If you plan to perform multiple requests, it is best to create a 39 | //! [`Client`][client] and reuse it, taking advantage of keep-alive connection 40 | //! pooling. 41 | //! 42 | //! ## Making POST requests (or setting request bodies) 43 | //! 44 | //! There are several ways you can set the body of a request. The basic one is 45 | //! by using the `body()` method of a [`RequestBuilder`][builder]. This lets you set the 46 | //! exact raw bytes of what the body should be. It accepts various types, 47 | //! including `String` and `Vec`. If you wish to pass a custom 48 | //! type, you can use the `nightfly::Body` constructors. 49 | //! 50 | //! ```rust 51 | //! # use nightfly::Error; 52 | //! # 53 | //! # fn run() -> Result<(), Error> { 54 | //! let client = nightfly::Client::new(); 55 | //! let res = client.post("http://httpbin.org/post") 56 | //! .body("the exact body that is sent") 57 | //! .send(); 58 | //! # Ok(()) 59 | //! # } 60 | //! ``` 61 | //! 62 | //! ### Forms 63 | //! 64 | //! It's very common to want to send form data in a request body. This can be 65 | //! done with any type that can be serialized into form data. 66 | //! 67 | //! This can be an array of tuples, or a `HashMap`, or a custom type that 68 | //! implements [`Serialize`][serde]. 69 | //! 70 | //! ```rust 71 | //! # use nightfly::Error; 72 | //! # 73 | //! # fn run() -> Result<(), Error> { 74 | //! // This will POST a body of `foo=bar&baz=quux` 75 | //! let params = [("foo", "bar"), ("baz", "quux")]; 76 | //! let client = nightfly::Client::new(); 77 | //! let res = client.post("http://httpbin.org/post") 78 | //! .form(¶ms) 79 | //! .send(); 80 | //! # Ok(()) 81 | //! # } 82 | //! ``` 83 | //! 84 | //! ### JSON 85 | //! 86 | //! There is also a `json` method helper on the [`RequestBuilder`][builder] that works in 87 | //! a similar fashion the `form` method. It can take any value that can be 88 | //! serialized into JSON. The feature `json` is required. 89 | //! 90 | //! ```rust 91 | //! # use nightfly::Error; 92 | //! # use std::collections::HashMap; 93 | //! # 94 | //! # fn run() -> Result<(), Error> { 95 | //! // This will POST a body of `{"lang":"rust","body":"json"}` 96 | //! let mut map = HashMap::new(); 97 | //! map.insert("lang", "rust"); 98 | //! map.insert("body", "json"); 99 | //! 100 | //! let client = nightfly::Client::new(); 101 | //! let res = client.post("http://httpbin.org/post") 102 | //! .json(&map) 103 | //! .send(); 104 | //! # Ok(()) 105 | //! # } 106 | //! ``` 107 | //! 108 | //! ## Redirect Policies 109 | //! 110 | //! By default, a `Client` will automatically handle HTTP redirects, having a 111 | //! maximum redirect chain of 10 hops. To customize this behavior, a 112 | //! [`redirect::Policy`][redirect] can be used with a `ClientBuilder`. 113 | //! 114 | //! ## Cookies 115 | //! 116 | //! The automatic storing and sending of session cookies can be enabled with 117 | //! the [`cookie_store`][ClientBuilder::cookie_store] method on `ClientBuilder`. 118 | //! 119 | //! ## Optional Features 120 | //! 121 | //! The following are a list of [Cargo features][cargo-features] that can be 122 | //! enabled or disabled: 123 | //! 124 | //! - **cookies**: Provides cookie session support. 125 | //! 126 | //! 127 | //! [client]: ./struct.Client.html 128 | //! [response]: ./struct.Response.html 129 | //! [get]: ./fn.get.html 130 | //! [builder]: ./struct.RequestBuilder.html 131 | //! [serde]: http://serde.rs 132 | //! [redirect]: crate::redirect 133 | //! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section 134 | 135 | pub use http::header; 136 | pub use http::Method; 137 | pub use http::StatusCode; 138 | pub use http::{HeaderMap, HeaderValue}; 139 | pub use url::Url; 140 | 141 | // universal mods 142 | #[macro_use] 143 | mod error; 144 | mod into_url; 145 | mod response; 146 | 147 | pub use self::error::{Error, Result}; 148 | pub use self::into_url::IntoUrl; 149 | pub use self::response::ResponseBuilderExt; 150 | 151 | /// Shortcut method to quickly make a `GET` request. 152 | /// 153 | /// See also the methods on the [`nightfly::Response`](./struct.Response.html) 154 | /// type. 155 | /// 156 | /// **NOTE**: This function creates a new internal `Client` on each call, 157 | /// and so should not be used if making many requests. Create a 158 | /// [`Client`](./struct.Client.html) instead. 159 | /// 160 | /// # Examples 161 | /// 162 | /// ```rust 163 | /// # fn run() -> Result<(), nightfly::Error> { 164 | /// let body = nightfly::get("https://www.rust-lang.org") 165 | /// .text(); 166 | /// # Ok(()) 167 | /// # } 168 | /// ``` 169 | /// 170 | /// # Errors 171 | /// 172 | /// This function fails if: 173 | /// 174 | /// - native TLS backend cannot be initialized 175 | /// - supplied `Url` cannot be parsed 176 | /// - there was an error while sending request 177 | /// - redirect limit was exhausted 178 | pub fn get(url: T) -> crate::Result { 179 | Client::new().get(url).send() 180 | } 181 | 182 | fn _assert_impls() { 183 | fn assert_send() {} 184 | fn assert_sync() {} 185 | fn assert_clone() {} 186 | 187 | assert_send::(); 188 | // assert_sync::(); 189 | assert_clone::(); 190 | 191 | assert_send::(); 192 | assert_send::(); 193 | 194 | assert_send::(); 195 | assert_sync::(); 196 | } 197 | 198 | // #[cfg(test)] 199 | // #[macro_use] 200 | // extern crate doc_comment; 201 | 202 | // #[cfg(test)] 203 | // doctest!("../README.md"); 204 | 205 | // #[cfg(feature = "multipart")] 206 | // pub use self::lunatic_impl::multipart; 207 | pub use self::lunatic_impl::{ 208 | Body, Client, ClientBuilder, HttpResponse, Request, RequestBuilder, SerializableResponse, 209 | }; 210 | #[cfg(feature = "__tls")] 211 | // Re-exports, to be removed in a future release 212 | pub use tls::{Certificate, Identity}; 213 | 214 | #[cfg(feature = "cookies")] 215 | pub mod cookie; 216 | mod lunatic_impl; 217 | pub mod redirect; 218 | #[cfg(feature = "__tls")] 219 | pub mod tls; 220 | mod util; 221 | mod version; 222 | pub use version::Version; 223 | -------------------------------------------------------------------------------- /src/lunatic_impl/body.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Body struct 5 | #[derive(Default, Debug, Serialize, Deserialize, Clone)] 6 | pub struct Body(Vec); 7 | 8 | impl From for Body { 9 | fn from(s: String) -> Body { 10 | Body(s.into()) 11 | } 12 | } 13 | 14 | impl From<&str> for Body { 15 | fn from(s: &str) -> Body { 16 | Body(s.into()) 17 | } 18 | } 19 | 20 | impl From for Body { 21 | fn from(b: Bytes) -> Body { 22 | Body(b.into()) 23 | } 24 | } 25 | 26 | impl From> for Body { 27 | fn from(v: Vec) -> Body { 28 | Body(v) 29 | } 30 | } 31 | 32 | impl From<&[u8]> for Body { 33 | fn from(slice: &[u8]) -> Body { 34 | Body(slice.into()) 35 | } 36 | } 37 | 38 | impl From<()> for Body { 39 | fn from(_: ()) -> Body { 40 | Body::empty() 41 | } 42 | } 43 | 44 | impl From for Body { 45 | fn from(res: HttpResponse) -> Self { 46 | res.body.into() 47 | } 48 | } 49 | 50 | impl From for Bytes { 51 | fn from(body: Body) -> Bytes { 52 | Bytes::from(body.0) 53 | } 54 | } 55 | 56 | impl TryInto for Body { 57 | type Error = FromUtf8Error; 58 | 59 | fn try_into(self) -> Result { 60 | String::from_utf8(self.0) 61 | } 62 | } 63 | 64 | impl Body { 65 | /// empty body 66 | pub fn empty() -> Body { 67 | Body(vec![]) 68 | } 69 | 70 | /// length of body 71 | pub fn len(&self) -> usize { 72 | self.0.len() 73 | } 74 | 75 | /// tells whether body is empty 76 | pub fn is_empty(&self) -> bool { 77 | self.0.is_empty() 78 | } 79 | 80 | /// retrieve body 81 | pub fn inner(self) -> Vec { 82 | self.0 83 | } 84 | 85 | /// create a json body 86 | pub fn json(data: T) -> crate::Result { 87 | match serde_json::to_string(&data) { 88 | Ok(r) => Ok(Body(r.into())), 89 | Err(_e) => Err(crate::Error::new( 90 | crate::error::Kind::Request, 91 | Some("".to_string()), 92 | )), 93 | } 94 | } 95 | 96 | /// create a regular text body 97 | pub fn text>>(data: T) -> crate::Result { 98 | Ok(Body(data.into())) 99 | } 100 | } 101 | 102 | impl Read for Body { 103 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 104 | Cursor::new(self.0.clone()).read(buf) 105 | } 106 | } 107 | 108 | use std::{ 109 | convert::TryInto, 110 | io::{Cursor, Read}, 111 | string::FromUtf8Error, 112 | }; 113 | 114 | use crate::HttpResponse; 115 | -------------------------------------------------------------------------------- /src/lunatic_impl/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; 2 | 3 | pub use builder::*; 4 | 5 | use std::collections::HashMap; 6 | use std::convert::TryInto; 7 | use std::fmt; 8 | use std::io::Write; 9 | use std::time::Duration; 10 | 11 | use http::header::{self, Entry, HeaderMap, HeaderValue, ACCEPT_ENCODING, RANGE}; 12 | use http::Version; 13 | use lunatic::ap::{AbstractProcess, Config, ProcessRef}; 14 | use lunatic::{abstract_process, Tag}; 15 | use serde::{Deserialize, Serialize}; 16 | 17 | #[cfg(feature = "cookies")] 18 | use crate::cookie; 19 | use crate::error; 20 | use crate::lunatic_impl::request::{hashmap_from_header_map, InnerRequest}; 21 | use crate::lunatic_impl::response::SerializableResponse; 22 | use crate::lunatic_impl::{ 23 | decoder::{parse_response, Accepts}, 24 | http_stream::HttpStream, 25 | request::{PendingRequest, Request, RequestBuilder}, 26 | response::HttpResponse, 27 | }; 28 | use crate::redirect; 29 | pub use crate::{Body, ClientBuilder}; 30 | use crate::{IntoUrl, Method, Url}; 31 | #[cfg(feature = "cookies")] 32 | use std::sync::Arc; 33 | 34 | #[derive(Clone)] 35 | pub struct InnerClient { 36 | pub(crate) accepts: Accepts, 37 | #[cfg(feature = "cookies")] 38 | pub(crate) cookie_store: Option>, 39 | pub(crate) headers: HeaderMap, 40 | pub(crate) redirect_policy: redirect::Policy, 41 | pub(crate) referer: bool, 42 | pub(crate) request_timeout: Option, 43 | // pub(crate) proxies: Arc>, 44 | // pub(crate) proxies_maybe_http_auth: bool, 45 | pub(crate) https_only: bool, 46 | pub(crate) stream_map: HashMap, 47 | } 48 | 49 | /// encode request as http text 50 | pub fn request_to_vec( 51 | method: Method, 52 | uri: Url, 53 | mut headers: HeaderMap, 54 | body: Option, 55 | version: Version, 56 | ) -> Vec { 57 | let mut request_buffer: Vec = Vec::new(); 58 | if let Some(body) = &body { 59 | headers.append(header::CONTENT_LENGTH, HeaderValue::from(body.len())); 60 | } 61 | 62 | // writing status line 63 | let path = if let Some(query) = uri.query() { 64 | format!("{}?{}", uri.path(), query) 65 | } else { 66 | uri.path().to_string() 67 | }; 68 | request_buffer.extend(format!("{} {} {:?}\r\n", method, path, version,).as_bytes()); 69 | // writing headers 70 | for (key, value) in headers.iter() { 71 | if let Ok(value) = String::from_utf8(value.as_ref().to_vec()) { 72 | request_buffer.extend(format!("{}: {}\r\n", key, value).as_bytes()); 73 | } 74 | } 75 | // separator between header and data 76 | request_buffer.extend("\r\n".as_bytes()); 77 | if let Some(body) = body { 78 | request_buffer.extend(body.inner()); 79 | } 80 | 81 | request_buffer 82 | } 83 | 84 | #[abstract_process(visibility = pub)] 85 | impl InnerClient { 86 | // type Arg = ClientBuilder; 87 | // type State = Self; 88 | 89 | #[init] 90 | fn init(_: Config, builder: ClientBuilder) -> Result { 91 | builder.build_inner() 92 | } 93 | 94 | #[terminate] 95 | fn terminate(&self) { 96 | println!("Shutdown process"); 97 | } 98 | 99 | #[handle_link_death] 100 | fn handle_link_trapped(&mut self, _: Tag) { 101 | println!("Link trapped"); 102 | } 103 | 104 | #[handle_request] 105 | fn handle_http_request( 106 | &mut self, 107 | request: InnerRequest, 108 | ) -> crate::Result { 109 | let res = self.execute_request(request, vec![])?; 110 | Ok(SerializableResponse { 111 | body: res.body, 112 | status: res.status.as_u16(), 113 | version: res.version, 114 | headers: hashmap_from_header_map(res.headers), 115 | url: res.url, 116 | redirect_chain: res.redirect_chain, 117 | }) 118 | } 119 | 120 | #[handle_request] 121 | fn get_request_timeout(&mut self) -> Option { 122 | self.request_timeout 123 | } 124 | } 125 | 126 | /// An http `Client` to make Requests with. 127 | /// 128 | /// The Client is a wrapper for a process so 129 | /// The Client has various configuration values to tweak, but the defaults 130 | /// are set to what is usually the most commonly desired value. To configure a 131 | /// `Client`, use `Client::builder()`. 132 | /// 133 | /// The `Client` holds a connection pool internally, so it is advised that 134 | /// you create one and **reuse** it. 135 | /// 136 | /// You do **not** have to wrap the `Client` in an [`Rc`] or [`Arc`] to **reuse** it, 137 | /// because it already wraps a ProcessRef and that ensures that any incoming messages 138 | /// will be processed in order, even if called at the same time from different processes. 139 | /// 140 | /// Of course, as any usual ProcessRef, the Client struct is cloneable and serialisable 141 | /// so it's easy to pass around between processes. A client can connect to multiple 142 | /// different hosts and manage different connections. 143 | #[derive(Debug, Deserialize, Serialize, Clone)] 144 | pub struct Client(pub ProcessRef); 145 | 146 | impl Default for Client { 147 | fn default() -> Self { 148 | let builder = ClientBuilder::new(); 149 | let proc = InnerClient::link().start(builder); 150 | Client(proc.expect("failed to spawn client")) 151 | } 152 | } 153 | 154 | impl Client { 155 | /// Constructs a new `Client`. 156 | /// 157 | /// # Panics 158 | /// 159 | /// This method panics if a TLS backend cannot be initialized, or the resolver 160 | /// cannot load the system configuration. 161 | /// 162 | /// Use `Client::builder()` if you wish to handle the failure as an `Error` 163 | /// instead of panicking. 164 | pub fn new() -> Client { 165 | Client::default() 166 | } 167 | 168 | /// Convenience method to make a `GET` request to a URL. 169 | /// 170 | /// # Errors 171 | /// 172 | /// This method fails whenever the supplied `Url` cannot be parsed. 173 | pub fn get(&self, url: U) -> RequestBuilder 174 | where 175 | U: IntoUrl, 176 | { 177 | self.request(Method::GET, url) 178 | } 179 | 180 | /// Convenience method to make a `POST` request to a URL. 181 | /// 182 | /// # Errors 183 | /// 184 | /// This method fails whenever the supplied `Url` cannot be parsed. 185 | pub fn post(&self, url: U) -> RequestBuilder { 186 | self.request(Method::POST, url) 187 | } 188 | 189 | /// Convenience method to make a `PUT` request to a URL. 190 | /// 191 | /// # Errors 192 | /// 193 | /// This method fails whenever the supplied `Url` cannot be parsed. 194 | pub fn put(&self, url: U) -> RequestBuilder { 195 | self.request(Method::PUT, url) 196 | } 197 | 198 | /// Convenience method to make a `PATCH` request to a URL. 199 | /// 200 | /// # Errors 201 | /// 202 | /// This method fails whenever the supplied `Url` cannot be parsed. 203 | pub fn patch(&self, url: U) -> RequestBuilder { 204 | self.request(Method::PATCH, url) 205 | } 206 | 207 | /// Convenience method to make a `DELETE` request to a URL. 208 | /// 209 | /// # Errors 210 | /// 211 | /// This method fails whenever the supplied `Url` cannot be parsed. 212 | pub fn delete(&self, url: U) -> RequestBuilder { 213 | self.request(Method::DELETE, url) 214 | } 215 | 216 | /// Convenience method to make a `HEAD` request to a URL. 217 | /// 218 | /// # Errors 219 | /// 220 | /// This method fails whenever the supplied `Url` cannot be parsed. 221 | pub fn head(&self, url: U) -> RequestBuilder { 222 | self.request(Method::HEAD, url) 223 | } 224 | 225 | /// Start building a `Request` with the `Method` and `Url`. 226 | /// 227 | /// Returns a `RequestBuilder`, which will allow setting headers and 228 | /// the request body before sending. 229 | /// 230 | /// # Errors 231 | /// 232 | /// This method fails whenever the supplied `Url` cannot be parsed. 233 | pub fn request(&self, method: Method, url: U) -> RequestBuilder { 234 | let req = url.into_url().map(move |url| Request::new(method, url)); 235 | RequestBuilder::new(self.clone(), req) 236 | } 237 | 238 | /// Executes a `Request`. 239 | /// 240 | /// A `Request` can be built manually with `Request::new()` or obtained 241 | /// from a RequestBuilder with `RequestBuilder::build()`. 242 | /// 243 | /// You should prefer to use the `RequestBuilder` and 244 | /// `RequestBuilder::send()`. 245 | /// 246 | /// # Errors 247 | /// 248 | /// This method fails if there was an error while sending request, 249 | /// redirect loop was detected or redirect limit was exhausted. 250 | pub fn execute(&mut self, request: Request) -> Result { 251 | let inner: InnerRequest = request.try_into()?; 252 | let url = inner.url.clone(); 253 | let user_timeout = inner.timeout.or_else(|| self.0.get_request_timeout()); 254 | let res = if let Some(timeout) = user_timeout { 255 | self.0 256 | .with_timeout(timeout) 257 | .handle_http_request(inner) 258 | .unwrap_or_else(|_| Err(crate::error::timeout(url)))? 259 | } else { 260 | self.0.handle_http_request(inner)? 261 | }; 262 | res.try_into() 263 | } 264 | 265 | /// Creates a `ClientBuilder` to configure a `Client`. 266 | /// 267 | /// This is the same as `ClientBuilder::new()`. 268 | pub fn builder() -> ClientBuilder { 269 | ClientBuilder::new() 270 | } 271 | } 272 | 273 | #[derive(Debug, Serialize, Clone, Deserialize, Hash, PartialEq, Eq)] 274 | pub(crate) enum HostRef { 275 | Http(String), 276 | Https(String), 277 | } 278 | 279 | impl HostRef { 280 | pub(crate) fn new(url: &Url) -> Self { 281 | let protocol = url.scheme(); 282 | if protocol == "https" { 283 | return HostRef::Https(format!("{}", url.host().unwrap())); 284 | } 285 | let conn_str = format!("{}:{}", url.host().unwrap(), url.port().unwrap_or(80)); 286 | HostRef::Http(conn_str) 287 | } 288 | } 289 | 290 | impl InnerClient { 291 | pub(crate) fn accepts(&self) -> Accepts { 292 | self.accepts 293 | } 294 | 295 | /// ensures connection 296 | pub fn ensure_connection(&mut self, url: Url) -> crate::Result { 297 | let host_ref = HostRef::new(&url); 298 | if let Some(stream) = self.stream_map.get(&host_ref) { 299 | return Ok(stream.to_owned()); 300 | } 301 | HttpStream::connect(url) 302 | } 303 | 304 | fn fmt_fields(&self, f: &mut fmt::DebugStruct<'_, '_>) { 305 | // Instead of deriving Debug, only print fields when their output 306 | // would provide relevant or interesting data. 307 | 308 | #[cfg(feature = "cookies")] 309 | { 310 | if self.cookie_store.is_some() { 311 | f.field("cookie_store", &true); 312 | } 313 | } 314 | 315 | f.field("accepts", &self.accepts); 316 | 317 | // if !self.proxies.is_empty() { 318 | // f.field("proxies", &self.proxies); 319 | // } 320 | 321 | // if !self.redirect_policy.is_default() { 322 | // f.field("redirect_policy", &self.redirect_policy); 323 | // } 324 | 325 | if self.referer { 326 | f.field("referer", &true); 327 | } 328 | 329 | f.field("default_headers", &self.headers); 330 | 331 | if let Some(ref d) = self.request_timeout { 332 | f.field("timeout", d); 333 | } 334 | } 335 | 336 | pub(crate) fn execute_request( 337 | &mut self, 338 | req: InnerRequest, 339 | urls: Vec, 340 | ) -> crate::Result { 341 | let (method, url, mut headers, body, _timeout, version) = req.clone().pieces(); 342 | if url.scheme() != "http" && url.scheme() != "https" { 343 | return Err(error::url_bad_scheme(url)); 344 | } 345 | 346 | // check if we're in https_only mode and check the scheme of the current URL 347 | if self.https_only && url.scheme() != "https" { 348 | return Err(error::url_bad_scheme(url)); 349 | } 350 | 351 | if let Some(host) = url.host() { 352 | if !self.headers.contains_key("Host") { 353 | headers.append("Host", HeaderValue::from_str(&host.to_string()).unwrap()); 354 | } 355 | } 356 | 357 | // insert default headers in the request headers 358 | // without overwriting already appended headers. 359 | for (key, value) in &self.headers { 360 | if let Entry::Vacant(entry) = headers.entry(key) { 361 | entry.insert(value.clone()); 362 | } 363 | } 364 | 365 | // Add cookies from the cookie store. 366 | #[cfg(feature = "cookies")] 367 | { 368 | if let Some(cookie_store) = self.cookie_store.as_ref() { 369 | if headers.get(crate::header::COOKIE).is_none() { 370 | add_cookie_header(&mut headers, cookie_store.clone(), &url); 371 | } 372 | } 373 | } 374 | 375 | let accept_encoding = self.accepts.as_str(); 376 | 377 | if let Some(accept_encoding) = accept_encoding { 378 | if !headers.contains_key(ACCEPT_ENCODING) && !headers.contains_key(RANGE) { 379 | headers.insert(ACCEPT_ENCODING, HeaderValue::from_static(accept_encoding)); 380 | } 381 | } 382 | 383 | // let uri = expect_uri(&url); 384 | 385 | // self.proxy_auth(&uri, &mut headers); 386 | 387 | let encoded = request_to_vec( 388 | method, 389 | url.clone(), 390 | headers.clone(), 391 | body, 392 | version.try_into().unwrap(), 393 | ); 394 | lunatic_log::debug!( 395 | "Encoded headers {:?} | Encoded request {:?}", 396 | headers, 397 | String::from_utf8(encoded.clone()) 398 | ); 399 | 400 | let mut stream = self.ensure_connection(url)?; 401 | // if let Some(timeout) = self.request_timeout { 402 | // stream.set 403 | // } 404 | 405 | stream.write_all(&encoded).unwrap(); 406 | 407 | let response_buffer = Vec::new(); 408 | 409 | match parse_response(response_buffer, stream.clone(), req.clone(), self) { 410 | Ok(res) => PendingRequest::new(res, self, req, urls).resolve(), 411 | Err(_e) => unimplemented!(), 412 | } 413 | } 414 | 415 | // fn proxy_auth(&self, dst: &Uri, headers: &mut HeaderMap) { 416 | // if !self.proxies_maybe_http_auth { 417 | // return; 418 | // } 419 | 420 | // // Only set the header here if the destination scheme is 'http', 421 | // // since otherwise, the header will be included in the CONNECT tunnel 422 | // // request instead. 423 | // if dst.scheme() != Some(&Scheme::HTTP) { 424 | // return; 425 | // } 426 | 427 | // // if headers.contains_key(PROXY_AUTHORIZATION) { 428 | // // return; 429 | // // } 430 | 431 | // // for proxy in self.proxies.iter() { 432 | // // if proxy.is_match(dst) { 433 | // // if let Some(header) = proxy.http_basic_auth(dst) { 434 | // // headers.insert(PROXY_AUTHORIZATION, header); 435 | // // } 436 | 437 | // // break; 438 | // // } 439 | // // } 440 | // } 441 | } 442 | 443 | impl fmt::Debug for InnerClient { 444 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 445 | let mut builder = f.debug_struct("Client"); 446 | self.fmt_fields(&mut builder); 447 | builder.finish() 448 | } 449 | } 450 | 451 | impl fmt::Debug for ClientBuilder { 452 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 453 | let mut builder = f.debug_struct("ClientBuilder"); 454 | self.config.fmt_fields(&mut builder); 455 | builder.finish() 456 | } 457 | } 458 | 459 | // impl PendingRequest { 460 | // fn in_flight(self: Pin<&mut Self>) -> Pin<&mut ResponseFuture> { 461 | // self.project().in_flight 462 | // } 463 | 464 | // fn timeout(self: Pin<&mut Self>) -> Pin<&mut Option>>> { 465 | // self.project().timeout 466 | // } 467 | 468 | // fn urls(self: Pin<&mut Self>) -> &mut Vec { 469 | // self.project().urls 470 | // } 471 | 472 | // fn headers(self: Pin<&mut Self>) -> &mut HeaderMap { 473 | // self.project().headers 474 | // } 475 | 476 | // fn retry_error(mut self: Pin<&mut Self>, err: &(dyn std::error::Error + 'static)) -> bool { 477 | // if !is_retryable_error(err) { 478 | // return false; 479 | // } 480 | 481 | // trace!("can retry {:?}", err); 482 | 483 | // let body = match self.body { 484 | // Some(Some(ref body)) => Body::reusable(body.clone()), 485 | // Some(None) => { 486 | // debug!("error was retryable, but body not reusable"); 487 | // return false; 488 | // } 489 | // None => Body::empty(), 490 | // }; 491 | 492 | // if self.retry_count >= 2 { 493 | // trace!("retry count too high"); 494 | // return false; 495 | // } 496 | // self.retry_count += 1; 497 | 498 | // let uri = expect_uri(&self.url); 499 | // let mut req = Request::builder() 500 | // .method(self.method.clone()) 501 | // .uri(uri) 502 | // .body(body.into_stream()) 503 | // .expect("valid request parts"); 504 | 505 | // *req.headers_mut() = self.headers.clone(); 506 | 507 | // *self.as_mut().in_flight().get_mut() = self.client.hyper.request(req); 508 | 509 | // true 510 | // } 511 | // } 512 | 513 | // fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { 514 | // if let Some(cause) = err.source() { 515 | // if let Some(err) = cause.downcast_ref::() { 516 | // // They sent us a graceful shutdown, try with a new connection! 517 | // return err.is_go_away() 518 | // && err.is_remote() 519 | // && err.reason() == Some(h2::Reason::NO_ERROR); 520 | // } 521 | // } 522 | // false 523 | // } 524 | 525 | #[cfg(feature = "cookies")] 526 | pub(crate) fn add_cookie_header( 527 | headers: &mut HeaderMap, 528 | cookie_store: Arc, 529 | url: &Url, 530 | ) { 531 | use crate::cookie::CookieStore; 532 | 533 | if let Some(header) = cookie_store.cookies(url) { 534 | headers.insert(crate::header::COOKIE, header); 535 | } 536 | } 537 | 538 | #[cfg(test)] 539 | mod tests { 540 | #[lunatic::test] 541 | fn execute_request_rejects_invald_urls() { 542 | let url_str = "hxxps://www.rust-lang.org/"; 543 | let url = url::Url::parse(url_str).unwrap(); 544 | let result = crate::get(url.clone()); 545 | 546 | assert!(result.is_err()); 547 | let err = result.err().unwrap(); 548 | assert!(err.is_builder()); 549 | assert_eq!(url_str, err.url().unwrap().as_str()); 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /src/lunatic_impl/http_stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | use lunatic::net::{TcpStream, TlsStream}; 4 | use serde::{Deserialize, Serialize}; 5 | use url::Url; 6 | 7 | use crate::error::Kind; 8 | 9 | #[derive(Clone, Serialize, Deserialize, Debug)] 10 | pub enum HttpStream { 11 | Tcp(TcpStream), 12 | Tls(TlsStream), 13 | } 14 | 15 | impl HttpStream { 16 | pub fn connect(url: Url) -> crate::Result { 17 | let protocol = url.scheme(); 18 | if protocol == "https" { 19 | let conn_str = format!("{}", url.host().unwrap()); 20 | return match TlsStream::connect(&conn_str, url.port().unwrap_or(443).into()) { 21 | Ok(stream) => Ok(HttpStream::Tls(stream)), 22 | Err(e) => { 23 | lunatic_log::error!("Failed to connect via TLS {:?}", e); 24 | Err(crate::Error::new( 25 | Kind::Builder, 26 | Some("Failed to connect".to_string()), 27 | )) 28 | } 29 | }; 30 | } 31 | let conn_str = format!("{}:{}", url.host().unwrap(), url.port().unwrap_or(80)); 32 | lunatic_log::debug!("Connecting {:?} | {:?}", protocol, conn_str); 33 | match TcpStream::connect(conn_str) { 34 | Ok(stream) => Ok(HttpStream::Tcp(stream)), 35 | Err(e) => { 36 | lunatic_log::error!("Failed to connect via TCP {:?}", e); 37 | Err(crate::Error::new( 38 | Kind::Builder, 39 | Some("Failed to connect".to_string()), 40 | )) 41 | } 42 | } 43 | } 44 | } 45 | 46 | impl Read for HttpStream { 47 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 48 | match self { 49 | HttpStream::Tcp(stream) => stream.read(buf), 50 | HttpStream::Tls(stream) => stream.read(buf), 51 | } 52 | } 53 | } 54 | 55 | impl Write for HttpStream { 56 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 57 | match self { 58 | HttpStream::Tcp(stream) => stream.write(buf), 59 | HttpStream::Tls(stream) => stream.write(buf), 60 | } 61 | } 62 | 63 | fn flush(&mut self) -> std::io::Result<()> { 64 | match self { 65 | HttpStream::Tcp(stream) => stream.flush(), 66 | HttpStream::Tls(stream) => stream.flush(), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lunatic_impl/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::body::Body; 2 | pub use self::client::{Client, ClientBuilder, InnerClient}; 3 | pub use self::request::{Request, RequestBuilder}; 4 | pub use self::response::{HttpResponse, SerializableResponse}; 5 | // pub use self::upgrade::Upgraded; 6 | 7 | pub mod body; 8 | pub mod client; 9 | pub mod decoder; 10 | mod http_stream; 11 | // #[cfg(feature = "multipart")] 12 | // pub mod multipart; 13 | pub(crate) mod request; 14 | mod response; 15 | // mod upgrade; 16 | -------------------------------------------------------------------------------- /src/lunatic_impl/response.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | use std::net::SocketAddr; 4 | use std::{borrow::Cow, collections::HashMap}; 5 | 6 | use bytes::Bytes; 7 | use encoding_rs::{Encoding, UTF_8}; 8 | use http::{HeaderMap, HeaderValue, StatusCode}; 9 | use mime::Mime; 10 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 11 | use url::Url; 12 | 13 | #[cfg(feature = "cookies")] 14 | use crate::cookie; 15 | use crate::Version; 16 | 17 | use super::request::header_map_from_hashmap; 18 | 19 | // /// Extra information about the transport when an HttpConnector is used. 20 | // #[derive(Clone, Debug)] 21 | // pub struct HttpInfo { 22 | // remote_addr: SocketAddr, 23 | // local_addr: SocketAddr, 24 | // } 25 | 26 | /// A Response to a submitted `Request`. 27 | #[derive(Debug, Serialize, Deserialize, Clone)] 28 | pub struct SerializableResponse { 29 | /// body of response 30 | pub body: Vec, 31 | /// The response's status as u16 32 | pub status: u16, 33 | 34 | /// The response's version 35 | pub version: Version, 36 | 37 | /// The response's headers as hashmap from Headermap 38 | pub headers: HashMap>, 39 | 40 | /// url of where the final response came from 41 | /// in case any redirects happened 42 | pub url: Url, 43 | /// list of urls hopped during redirects 44 | pub redirect_chain: Vec, 45 | // pub info: HttpInfo, 46 | } 47 | 48 | impl TryFrom for HttpResponse { 49 | type Error = crate::Error; 50 | 51 | fn try_from(res: SerializableResponse) -> Result { 52 | Ok(HttpResponse { 53 | body: res.body, 54 | status: StatusCode::from_u16(res.status).unwrap(), 55 | version: res.version, 56 | headers: header_map_from_hashmap(res.headers), 57 | url: res.url, 58 | redirect_chain: res.redirect_chain, 59 | }) 60 | } 61 | } 62 | 63 | /// Response of an http request 64 | pub struct HttpResponse { 65 | /// body of response 66 | pub body: Vec, 67 | /// The response's status 68 | pub status: StatusCode, 69 | 70 | /// The response's version 71 | pub version: Version, 72 | 73 | /// The response's headers 74 | pub headers: HeaderMap, 75 | 76 | /// url of response 77 | pub url: Url, 78 | 79 | /// chain of urls if any redirection happened 80 | pub redirect_chain: Vec, 81 | // pub info: HttpInfo, 82 | } 83 | 84 | impl HttpResponse { 85 | /// Get the `StatusCode` of this `Response`. 86 | #[inline] 87 | pub fn status(&self) -> StatusCode { 88 | self.status 89 | } 90 | 91 | /// Get the HTTP `Version` of this `Response`. 92 | #[inline] 93 | pub fn version(&self) -> Version { 94 | self.version 95 | } 96 | 97 | /// Get the `Headers` of this `Response`. 98 | #[inline] 99 | pub fn headers(&self) -> &HeaderMap { 100 | &self.headers 101 | } 102 | 103 | /// Get a mutable reference to the `Headers` of this `Response`. 104 | #[inline] 105 | pub fn headers_mut(&mut self) -> &mut HeaderMap { 106 | &mut self.headers 107 | } 108 | 109 | /// Get the content-length of this response, if known. 110 | /// 111 | /// Reasons it may not be known: 112 | /// 113 | /// - The server didn't send a `content-length` header. 114 | /// - The response is compressed and automatically decoded (thus changing 115 | /// the actual decoded length). 116 | pub fn content_length(&self) -> Option { 117 | // add 12 because there are 3 pairs of \r\n which take up 4 bytes/octects each 118 | Some(self.body().len() as u64) 119 | } 120 | 121 | /// Retrieve the cookies contained in the response. 122 | /// 123 | /// Note that invalid 'Set-Cookie' headers will be ignored. 124 | /// 125 | /// # Optional 126 | /// 127 | /// This requires the optional `cookies` feature to be enabled. 128 | #[cfg(feature = "cookies")] 129 | #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] 130 | pub fn cookies(&self) -> impl Iterator { 131 | cookie::extract_response_cookies(self.headers()).filter_map(Result::ok) 132 | } 133 | 134 | /// Get the final `Url` of this `Response`. 135 | #[inline] 136 | pub fn url(&self) -> &Url { 137 | &self.url 138 | } 139 | 140 | /// Get the remote address used to get this `Response`. 141 | pub fn remote_addr(&self) -> Option { 142 | None 143 | // self.res 144 | // .extensions() 145 | // .get::() 146 | // .map(|info| info.remote_addr()) 147 | } 148 | 149 | // /// Returns a reference to the associated extensions. 150 | // pub fn extensions(&self) -> &http::Extensions { 151 | // self.res.extensions() 152 | // } 153 | 154 | // /// Returns a mutable reference to the associated extensions. 155 | // pub fn extensions_mut(&mut self) -> &mut http::Extensions { 156 | // self.res.extensions_mut() 157 | // } 158 | 159 | // body methods 160 | 161 | /// Get the full response text. 162 | /// 163 | /// This method decodes the response body with BOM sniffing 164 | /// and with malformed sequences replaced with the REPLACEMENT CHARACTER. 165 | /// Encoding is determinated from the `charset` parameter of `Content-Type` header, 166 | /// and defaults to `utf-8` if not presented. 167 | /// 168 | /// # Example 169 | /// 170 | /// ``` 171 | /// # fn run() -> Result<(), Box> { 172 | /// let content = nightfly::get("http://httpbin.org/range/26") 173 | /// .text(); 174 | /// 175 | /// println!("text: {:?}", content); 176 | /// # Ok(()) 177 | /// # } 178 | /// ``` 179 | pub fn text(self) -> crate::Result { 180 | self.text_with_charset("utf-8") 181 | } 182 | 183 | /// Get the full response text given a specific encoding. 184 | /// 185 | /// This method decodes the response body with BOM sniffing 186 | /// and with malformed sequences replaced with the REPLACEMENT CHARACTER. 187 | /// You can provide a default encoding for decoding the raw message, while the 188 | /// `charset` parameter of `Content-Type` header is still prioritized. For more information 189 | /// about the possible encoding name, please go to [`encoding_rs`] docs. 190 | /// 191 | /// [`encoding_rs`]: https://docs.rs/encoding_rs/0.8/encoding_rs/#relationship-with-windows-code-pages 192 | /// 193 | /// # Example 194 | /// 195 | /// ``` 196 | /// # fn run() -> Result<(), Box> { 197 | /// let content = nightfly::get("http://httpbin.org/range/26") 198 | /// .text_with_charset("utf-8") 199 | /// ; 200 | /// 201 | /// println!("text: {:?}", content); 202 | /// # Ok(()) 203 | /// # } 204 | /// ``` 205 | pub fn text_with_charset(self, default_encoding: &str) -> crate::Result { 206 | let content_type = self 207 | .headers() 208 | .get(crate::header::CONTENT_TYPE) 209 | .and_then(|value| value.to_str().ok()) 210 | .and_then(|value| value.parse::().ok()); 211 | let encoding_name = content_type 212 | .as_ref() 213 | .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str())) 214 | .unwrap_or(default_encoding); 215 | let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8); 216 | 217 | let full = self.body(); 218 | 219 | let (text, _, _) = encoding.decode(&full); 220 | if let Cow::Owned(s) = text { 221 | return Ok(s); 222 | } 223 | unsafe { 224 | // decoding returned Cow::Borrowed, meaning these bytes 225 | // are already valid utf8 226 | Ok(String::from_utf8_unchecked(full.to_vec())) 227 | } 228 | } 229 | 230 | /// Try to deserialize the response body as JSON. 231 | /// 232 | /// # Optional 233 | /// 234 | /// This requires the optional `json` feature enabled. 235 | /// 236 | /// # Examples 237 | /// 238 | /// ``` 239 | /// # extern crate nightfly; 240 | /// # extern crate serde; 241 | /// # 242 | /// # use nightfly::Error; 243 | /// # use serde::Deserialize; 244 | /// # 245 | /// // This `derive` requires the `serde` dependency. 246 | /// #[derive(Deserialize)] 247 | /// struct Ip { 248 | /// origin: String, 249 | /// } 250 | /// 251 | /// # fn run() -> Result<(), Error> { 252 | /// let ip = nightfly::get("http://httpbin.org/ip") 253 | /// 254 | /// .json::() 255 | /// ; 256 | /// 257 | /// println!("ip: {}", ip.origin); 258 | /// # Ok(()) 259 | /// # } 260 | /// # 261 | /// # fn main() { } 262 | /// ``` 263 | /// 264 | /// # Errors 265 | /// 266 | /// This method fails whenever the response body is not in JSON format 267 | /// or it cannot be properly deserialized to target type `T`. For more 268 | /// details please see [`serde_json::from_reader`]. 269 | /// 270 | /// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html 271 | // #[cfg(feature = "json")] 272 | // #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 273 | pub fn json(self) -> crate::Result { 274 | let full = self.body(); 275 | 276 | serde_json::from_slice(&full).map_err(crate::error::decode) 277 | } 278 | 279 | /// Get the full response body as `Bytes`. 280 | /// 281 | /// # Example 282 | /// 283 | /// ``` 284 | /// # fn run() -> Result<(), Box> { 285 | /// let bytes = nightfly::get("http://httpbin.org/ip") 286 | /// 287 | /// .bytes() 288 | /// ; 289 | /// 290 | /// println!("bytes: {:?}", bytes); 291 | /// # Ok(()) 292 | /// # } 293 | /// ``` 294 | pub fn bytes(self) -> crate::Result { 295 | Bytes::try_from(self.body) 296 | .map_err(|e| crate::Error::new(crate::error::Kind::Decode, Some(e))) 297 | } 298 | 299 | /// return vec 300 | pub fn body(&self) -> Vec { 301 | self.body.clone() 302 | } 303 | 304 | /// Stream a chunk of the response body. 305 | /// 306 | /// When the response body has been exhausted, this will return `None`. 307 | /// 308 | /// # Example 309 | /// 310 | /// ``` 311 | /// # fn run() -> Result<(), Box> { 312 | /// let mut res = nightfly::get("https://hyper.rs"); 313 | /// 314 | /// while let Some(chunk) = res.chunk() { 315 | /// println!("Chunk: {:?}", chunk); 316 | /// } 317 | /// # Ok(()) 318 | /// # } 319 | /// ``` 320 | pub fn chunk(&mut self) -> crate::Result> { 321 | // if let Some(item) = self.res.body_mut().next() { 322 | // Ok(Some(item?)) 323 | // } else { 324 | Ok(None) 325 | // } 326 | } 327 | 328 | // util methods 329 | 330 | /// Turn a response into an error if the server returned an error. 331 | /// 332 | /// # Example 333 | /// 334 | /// ``` 335 | /// # use nightfly::Response; 336 | /// fn on_response(res: Response) { 337 | /// match res.error_for_status() { 338 | /// Ok(_res) => (), 339 | /// Err(err) => { 340 | /// // asserting a 400 as an example 341 | /// // it could be any status between 400...599 342 | /// assert_eq!( 343 | /// err.status(), 344 | /// Some(nightfly::StatusCode::BAD_REQUEST) 345 | /// ); 346 | /// } 347 | /// } 348 | /// } 349 | /// # fn main() {} 350 | /// ``` 351 | pub fn error_for_status(self) -> crate::Result { 352 | let status = self.status(); 353 | if status.is_client_error() || status.is_server_error() { 354 | Err(crate::error::status_code(self.url, status)) 355 | } else { 356 | Ok(self) 357 | } 358 | } 359 | 360 | /// Turn a reference to a response into an error if the server returned an error. 361 | /// 362 | /// # Example 363 | /// 364 | /// ``` 365 | /// # use nightfly::Response; 366 | /// fn on_response(res: &Response) { 367 | /// match res.error_for_status_ref() { 368 | /// Ok(_res) => (), 369 | /// Err(err) => { 370 | /// // asserting a 400 as an example 371 | /// // it could be any status between 400...599 372 | /// assert_eq!( 373 | /// err.status(), 374 | /// Some(nightfly::StatusCode::BAD_REQUEST) 375 | /// ); 376 | /// } 377 | /// } 378 | /// } 379 | /// # fn main() {} 380 | /// ``` 381 | pub fn error_for_status_ref(&self) -> crate::Result<&Self> { 382 | let status = self.status(); 383 | if status.is_client_error() || status.is_server_error() { 384 | Err(crate::error::status_code(self.url.clone(), status)) 385 | } else { 386 | Ok(self) 387 | } 388 | } 389 | 390 | // private 391 | 392 | // The Response's body is an implementation detail. 393 | // You no longer need to get a reference to it, there are async methods 394 | // on the `Response` itself. 395 | // 396 | // This method is just used by the blocking API. 397 | #[cfg(feature = "blocking")] 398 | pub(crate) fn body_mut(&mut self) -> &mut Decoder { 399 | self.res.body_mut() 400 | } 401 | } 402 | 403 | impl fmt::Debug for HttpResponse { 404 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 405 | f.debug_struct("Response") 406 | .field("url", self.url()) 407 | .field("status", &self.status()) 408 | .field("headers", self.headers()) 409 | .finish() 410 | } 411 | } 412 | 413 | // /// A `Response` can be piped as the `Body` of another request. 414 | // impl From for Body { 415 | // fn from(r: Response) -> Body { 416 | // Body::stream(r.res.into_body()) 417 | // } 418 | // } 419 | 420 | // #[cfg(test)] 421 | // mod tests { 422 | // use super::Response; 423 | // use crate::ResponseBuilderExt; 424 | // use http::response::Builder; 425 | // use url::Url; 426 | 427 | // #[test] 428 | // fn test_from_http_response() { 429 | // let url = Url::parse("http://example.com").unwrap(); 430 | // let response = Builder::new() 431 | // .status(200) 432 | // .url(url.clone()) 433 | // .body("foo") 434 | // .unwrap(); 435 | // let response = Response::from(response); 436 | 437 | // assert_eq!(response.status(), 200); 438 | // assert_eq!(*response.url(), url); 439 | // } 440 | // } 441 | -------------------------------------------------------------------------------- /src/lunatic_impl/upgrade.rs: -------------------------------------------------------------------------------- 1 | // use std::pin::Pin; 2 | // use std::task::{self, Poll}; 3 | // use std::{fmt, io}; 4 | 5 | // /// An upgraded HTTP connection. 6 | // pub struct Upgraded { 7 | // inner: hyper::upgrade::Upgraded, 8 | // } 9 | 10 | // impl AsyncRead for Upgraded { 11 | // fn poll_read( 12 | // mut self: Pin<&mut Self>, 13 | // cx: &mut task::Context<'_>, 14 | // buf: &mut ReadBuf<'_>, 15 | // ) -> Poll> { 16 | // Pin::new(&mut self.inner).poll_read(cx, buf) 17 | // } 18 | // } 19 | 20 | // impl AsyncWrite for Upgraded { 21 | // fn poll_write( 22 | // mut self: Pin<&mut Self>, 23 | // cx: &mut task::Context<'_>, 24 | // buf: &[u8], 25 | // ) -> Poll> { 26 | // Pin::new(&mut self.inner).poll_write(cx, buf) 27 | // } 28 | 29 | // fn poll_write_vectored( 30 | // mut self: Pin<&mut Self>, 31 | // cx: &mut task::Context<'_>, 32 | // bufs: &[io::IoSlice<'_>], 33 | // ) -> Poll> { 34 | // Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) 35 | // } 36 | 37 | // fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 38 | // Pin::new(&mut self.inner).poll_flush(cx) 39 | // } 40 | 41 | // fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 42 | // Pin::new(&mut self.inner).poll_shutdown(cx) 43 | // } 44 | 45 | // fn is_write_vectored(&self) -> bool { 46 | // self.inner.is_write_vectored() 47 | // } 48 | // } 49 | 50 | // impl fmt::Debug for Upgraded { 51 | // fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | // f.debug_struct("Upgraded").finish() 53 | // } 54 | // } 55 | 56 | // impl From for Upgraded { 57 | // fn from(inner: hyper::upgrade::Upgraded) -> Self { 58 | // Upgraded { inner } 59 | // } 60 | // } 61 | 62 | // impl super::response::Response { 63 | // /// Consumes the response and returns a future for a possible HTTP upgrade. 64 | // pub fn upgrade(self) -> crate::Result { 65 | // hyper::upgrade::on(self.res) 66 | // .map_ok(Upgraded::from) 67 | // .map_err(crate::error::upgrade) 68 | // } 69 | // } 70 | -------------------------------------------------------------------------------- /src/redirect.rs: -------------------------------------------------------------------------------- 1 | //! Redirect Handling 2 | //! 3 | //! By default, a `Client` will automatically handle HTTP redirects, having a 4 | //! maximum redirect chain of 10 hops. To customize this behavior, a 5 | //! `redirect::Policy` can be used with a `ClientBuilder`. 6 | 7 | use std::error::Error as StdError; 8 | use std::fmt; 9 | 10 | use crate::header::{HeaderMap, AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, WWW_AUTHENTICATE}; 11 | use http::StatusCode; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | use crate::Url; 15 | 16 | /// A type that controls the policy on how to handle the following of redirects. 17 | /// 18 | /// The default value will catch redirect loops, and has a maximum of 10 19 | /// redirects it will follow in a chain before returning an error. 20 | /// 21 | /// - `limited` can be used have the same as the default behavior, but adjust 22 | /// the allowed maximum redirect hops in a chain. 23 | /// - `none` can be used to disable all redirect behavior. 24 | /// - `custom` can be used to create a customized policy. 25 | #[derive(Clone, Serialize, Deserialize)] 26 | pub struct Policy { 27 | inner: PolicyKind, 28 | } 29 | 30 | /// A type that holds information on the next request and previous requests 31 | /// in redirect chain. 32 | #[derive(Debug)] 33 | pub struct Attempt<'a> { 34 | status: StatusCode, 35 | next: &'a Url, 36 | previous: &'a [Url], 37 | } 38 | 39 | /// An action to perform when a redirect status code is found. 40 | #[derive(Debug)] 41 | pub struct Action { 42 | inner: ActionKind, 43 | } 44 | 45 | impl Policy { 46 | /// Create a `Policy` with a maximum number of redirects. 47 | /// 48 | /// An `Error` will be returned if the max is reached. 49 | pub fn limited(max: usize) -> Self { 50 | Self { 51 | inner: PolicyKind::Limit(max), 52 | } 53 | } 54 | 55 | /// Create a `Policy` that does not follow any redirect. 56 | pub fn none() -> Self { 57 | Self { 58 | inner: PolicyKind::None, 59 | } 60 | } 61 | 62 | // /// Create a custom `Policy` using the passed function. 63 | // /// 64 | // /// # Note 65 | // /// 66 | // /// The default `Policy` handles a maximum loop 67 | // /// chain, but the custom variant does not do that for you automatically. 68 | // /// The custom policy should have some way of handling those. 69 | // /// 70 | // /// Information on the next request and previous requests can be found 71 | // /// on the [`Attempt`] argument passed to the closure. 72 | // /// 73 | // /// Actions can be conveniently created from methods on the 74 | // /// [`Attempt`]. 75 | // /// 76 | // /// # Example 77 | // /// 78 | // /// ```rust 79 | // /// # use nightfly::{Error, redirect}; 80 | // /// # 81 | // /// # fn run() -> Result<(), Error> { 82 | // /// let custom = redirect::Policy::custom(|attempt| { 83 | // /// if attempt.previous().len() > 5 { 84 | // /// attempt.error("too many redirects") 85 | // /// } else if attempt.url().host_str() == Some("example.domain") { 86 | // /// // prevent redirects to 'example.domain' 87 | // /// attempt.stop() 88 | // /// } else { 89 | // /// attempt.follow() 90 | // /// } 91 | // /// }); 92 | // /// let client = nightfly::Client::builder() 93 | // /// .redirect(custom) 94 | // /// .build()?; 95 | // /// # Ok(()) 96 | // /// # } 97 | // /// ``` 98 | // /// 99 | // /// [`Attempt`]: struct.Attempt.html 100 | // pub fn custom(policy: T) -> Self 101 | // where 102 | // T: Fn(Attempt) -> Action + Send + Sync + 'static, 103 | // { 104 | // Self { 105 | // inner: PolicyKind::Custom(Box::new(policy)), 106 | // } 107 | // } 108 | 109 | /// Apply this policy to a given [`Attempt`] to produce a [`Action`]. 110 | /// 111 | /// # Note 112 | /// 113 | /// This method can be used together with `Policy::custom()` 114 | /// to construct one `Policy` that wraps another. 115 | /// 116 | /// # Example 117 | /// 118 | /// ```rust 119 | /// # use nightfly::{Error, redirect}; 120 | /// # 121 | /// # fn run() -> Result<(), Error> { 122 | /// let custom = redirect::Policy::custom(|attempt| { 123 | /// eprintln!("{}, Location: {:?}", attempt.status(), attempt.url()); 124 | /// redirect::Policy::default().redirect(attempt) 125 | /// }); 126 | /// # Ok(()) 127 | /// # } 128 | /// ``` 129 | pub fn redirect(&self, attempt: Attempt) -> Action { 130 | match self.inner { 131 | // PolicyKind::Custom(ref custom) => custom(attempt), 132 | PolicyKind::Limit(max) => { 133 | if attempt.previous.len() >= max { 134 | attempt.error(TooManyRedirects) 135 | } else { 136 | attempt.follow() 137 | } 138 | } 139 | PolicyKind::None => attempt.stop(), 140 | } 141 | } 142 | 143 | pub(crate) fn check(&self, status: StatusCode, next: &Url, previous: &[Url]) -> ActionKind { 144 | self.redirect(Attempt { 145 | status, 146 | next, 147 | previous, 148 | }) 149 | .inner 150 | } 151 | 152 | pub(crate) fn is_default(&self) -> bool { 153 | matches!(self.inner, PolicyKind::Limit(10)) 154 | } 155 | } 156 | 157 | impl Default for Policy { 158 | fn default() -> Policy { 159 | // Keep `is_default` in sync 160 | Policy::limited(10) 161 | } 162 | } 163 | 164 | impl<'a> Attempt<'a> { 165 | /// Get the type of redirect. 166 | pub fn status(&self) -> StatusCode { 167 | self.status 168 | } 169 | 170 | /// Get the next URL to redirect to. 171 | pub fn url(&self) -> &Url { 172 | self.next 173 | } 174 | 175 | /// Get the list of previous URLs that have already been requested in this chain. 176 | pub fn previous(&self) -> &[Url] { 177 | self.previous 178 | } 179 | /// Returns an action meaning nightfly should follow the next URL. 180 | pub fn follow(self) -> Action { 181 | Action { 182 | inner: ActionKind::Follow, 183 | } 184 | } 185 | 186 | /// Returns an action meaning nightfly should not follow the next URL. 187 | /// 188 | /// The 30x response will be returned as the `Ok` result. 189 | pub fn stop(self) -> Action { 190 | Action { 191 | inner: ActionKind::Stop, 192 | } 193 | } 194 | 195 | /// Returns an action failing the redirect with an error. 196 | /// 197 | /// The `Error` will be returned for the result of the sent request. 198 | pub fn error>>(self, error: E) -> Action { 199 | Action { 200 | inner: ActionKind::Error(error.into()), 201 | } 202 | } 203 | } 204 | 205 | #[derive(Clone, Serialize, Deserialize)] 206 | enum PolicyKind { 207 | // Custom(Box Action + Send + Sync + 'static>), 208 | Limit(usize), 209 | None, 210 | } 211 | 212 | impl fmt::Debug for Policy { 213 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 214 | f.debug_tuple("Policy").field(&self.inner).finish() 215 | } 216 | } 217 | 218 | impl fmt::Debug for PolicyKind { 219 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 220 | match *self { 221 | // PolicyKind::Custom(..) => f.pad("Custom"), 222 | PolicyKind::Limit(max) => f.debug_tuple("Limit").field(&max).finish(), 223 | PolicyKind::None => f.pad("None"), 224 | } 225 | } 226 | } 227 | 228 | // pub(crate) 229 | 230 | #[derive(Debug)] 231 | pub(crate) enum ActionKind { 232 | Follow, 233 | Stop, 234 | Error(Box), 235 | } 236 | 237 | pub(crate) fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Url, previous: &[Url]) { 238 | if let Some(previous) = previous.last() { 239 | let cross_host = next.host_str() != previous.host_str() 240 | || next.port_or_known_default() != previous.port_or_known_default(); 241 | if cross_host { 242 | headers.remove(AUTHORIZATION); 243 | headers.remove(COOKIE); 244 | headers.remove("cookie2"); 245 | headers.remove(PROXY_AUTHORIZATION); 246 | headers.remove(WWW_AUTHENTICATE); 247 | } 248 | } 249 | } 250 | 251 | #[derive(Debug)] 252 | struct TooManyRedirects; 253 | 254 | impl fmt::Display for TooManyRedirects { 255 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 256 | f.write_str("too many redirects") 257 | } 258 | } 259 | 260 | impl StdError for TooManyRedirects {} 261 | 262 | #[lunatic::test] 263 | fn test_redirect_policy_limit() { 264 | let policy = Policy::default(); 265 | let next = Url::parse("http://x.y/z").unwrap(); 266 | let mut previous = (0..9) 267 | .map(|i| Url::parse(&format!("http://a.b/c/{}", i)).unwrap()) 268 | .collect::>(); 269 | 270 | match policy.check(StatusCode::FOUND, &next, &previous) { 271 | ActionKind::Follow => (), 272 | other => panic!("unexpected {:?}", other), 273 | } 274 | 275 | previous.push(Url::parse("http://a.b.d/e/33").unwrap()); 276 | 277 | match policy.check(StatusCode::FOUND, &next, &previous) { 278 | ActionKind::Error(err) if err.is::() => (), 279 | other => panic!("unexpected {:?}", other), 280 | } 281 | } 282 | 283 | #[lunatic::test] 284 | fn test_redirect_policy_limit_to_0() { 285 | let policy = Policy::limited(0); 286 | let next = Url::parse("http://x.y/z").unwrap(); 287 | let previous = vec![Url::parse("http://a.b/c").unwrap()]; 288 | 289 | match policy.check(StatusCode::FOUND, &next, &previous) { 290 | ActionKind::Error(err) if err.is::() => (), 291 | other => panic!("unexpected {:?}", other), 292 | } 293 | } 294 | 295 | // #[test] 296 | // fn test_redirect_policy_custom() { 297 | // let policy = Policy::custom(|attempt| { 298 | // if attempt.url().host_str() == Some("foo") { 299 | // attempt.stop() 300 | // } else { 301 | // attempt.follow() 302 | // } 303 | // }); 304 | 305 | // let next = Url::parse("http://bar/baz").unwrap(); 306 | // match policy.check(StatusCode::FOUND, &next, &[]) { 307 | // ActionKind::Follow => (), 308 | // other => panic!("unexpected {:?}", other), 309 | // } 310 | 311 | // let next = Url::parse("http://foo/baz").unwrap(); 312 | // match policy.check(StatusCode::FOUND, &next, &[]) { 313 | // ActionKind::Stop => (), 314 | // other => panic!("unexpected {:?}", other), 315 | // } 316 | // } 317 | 318 | #[lunatic::test] 319 | fn test_remove_sensitive_headers() { 320 | use http::header::{HeaderValue, ACCEPT, AUTHORIZATION, COOKIE}; 321 | 322 | let mut headers = HeaderMap::new(); 323 | headers.insert(ACCEPT, HeaderValue::from_static("*/*")); 324 | headers.insert(AUTHORIZATION, HeaderValue::from_static("let me in")); 325 | headers.insert(COOKIE, HeaderValue::from_static("foo=bar")); 326 | 327 | let next = Url::parse("http://initial-domain.com/path").unwrap(); 328 | let mut prev = vec![Url::parse("http://initial-domain.com/new_path").unwrap()]; 329 | let mut filtered_headers = headers.clone(); 330 | 331 | remove_sensitive_headers(&mut headers, &next, &prev); 332 | assert_eq!(headers, filtered_headers); 333 | 334 | prev.push(Url::parse("http://new-domain.com/path").unwrap()); 335 | filtered_headers.remove(AUTHORIZATION); 336 | filtered_headers.remove(COOKIE); 337 | 338 | remove_sensitive_headers(&mut headers, &next, &prev); 339 | assert_eq!(headers, filtered_headers); 340 | } 341 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub(crate) struct ResponseUrl(pub Url); 5 | 6 | /// Extension trait for http::response::Builder objects 7 | /// 8 | /// Allows the user to add a `Url` to the http::Response 9 | pub trait ResponseBuilderExt { 10 | /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` 11 | /// to the `http::Response` 12 | fn url(self, url: Url) -> Self; 13 | } 14 | 15 | impl ResponseBuilderExt for http::response::Builder { 16 | fn url(self, url: Url) -> Self { 17 | self.extension(ResponseUrl(url)) 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::{ResponseBuilderExt, ResponseUrl}; 24 | use http::response::Builder; 25 | use url::Url; 26 | 27 | #[lunatic::test] 28 | fn test_response_builder_ext() { 29 | let url = Url::parse("http://example.com").unwrap(); 30 | let response = Builder::new() 31 | .status(200) 32 | .url(url.clone()) 33 | .body(()) 34 | .unwrap(); 35 | 36 | assert_eq!( 37 | response.extensions().get::(), 38 | Some(&ResponseUrl(url)) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/tls.rs: -------------------------------------------------------------------------------- 1 | //! TLS configuration 2 | //! 3 | //! By default, a `Client` will make use of system-native transport layer 4 | //! security to connect to HTTPS destinations. This means schannel on Windows, 5 | //! Security-Framework on macOS, and OpenSSL on Linux. 6 | //! 7 | //! - Additional X509 certificates can be configured on a `ClientBuilder` with the 8 | //! [`Certificate`](Certificate) type. 9 | //! - Client certificates can be add to a `ClientBuilder` with the 10 | //! [`Identity`][Identity] type. 11 | //! - Various parts of TLS can also be configured or even disabled on the 12 | //! `ClientBuilder`. 13 | 14 | #[cfg(feature = "__rustls")] 15 | use rustls::{ 16 | client::HandshakeSignatureValid, client::ServerCertVerified, client::ServerCertVerifier, 17 | internal::msgs::handshake::DigitallySignedStruct, Error as TLSError, ServerName, 18 | }; 19 | use std::fmt; 20 | 21 | /// Represents a server X509 certificate. 22 | #[derive(Clone)] 23 | pub struct Certificate { 24 | #[cfg(feature = "native-tls-crate")] 25 | native: native_tls_crate::Certificate, 26 | #[cfg(feature = "__rustls")] 27 | original: Cert, 28 | } 29 | 30 | #[cfg(feature = "__rustls")] 31 | #[derive(Clone)] 32 | enum Cert { 33 | Der(Vec), 34 | Pem(Vec), 35 | } 36 | 37 | /// Represents a private key and X509 cert as a client certificate. 38 | #[derive(Clone)] 39 | pub struct Identity { 40 | #[cfg_attr(not(any(feature = "native-tls", feature = "__rustls")), allow(unused))] 41 | inner: ClientCert, 42 | } 43 | 44 | #[derive(Clone)] 45 | enum ClientCert { 46 | #[cfg(feature = "native-tls")] 47 | Pkcs12(native_tls_crate::Identity), 48 | #[cfg(feature = "__rustls")] 49 | Pem { 50 | key: rustls::PrivateKey, 51 | certs: Vec, 52 | }, 53 | } 54 | 55 | impl Certificate { 56 | /// Create a `Certificate` from a binary DER encoded certificate 57 | /// 58 | /// # Examples 59 | /// 60 | /// ``` 61 | /// # use std::fs::File; 62 | /// # use std::io::Read; 63 | /// # fn cert() -> Result<(), Box> { 64 | /// let mut buf = Vec::new(); 65 | /// File::open("my_cert.der")? 66 | /// .read_to_end(&mut buf)?; 67 | /// let cert = nightfly::Certificate::from_der(&buf)?; 68 | /// # drop(cert); 69 | /// # Ok(()) 70 | /// # } 71 | /// ``` 72 | pub fn from_der(der: &[u8]) -> crate::Result { 73 | Ok(Certificate { 74 | #[cfg(feature = "native-tls-crate")] 75 | native: native_tls_crate::Certificate::from_der(der).map_err(crate::error::builder)?, 76 | #[cfg(feature = "__rustls")] 77 | original: Cert::Der(der.to_owned()), 78 | }) 79 | } 80 | 81 | /// Create a `Certificate` from a PEM encoded certificate 82 | /// 83 | /// # Examples 84 | /// 85 | /// ``` 86 | /// # use std::fs::File; 87 | /// # use std::io::Read; 88 | /// # fn cert() -> Result<(), Box> { 89 | /// let mut buf = Vec::new(); 90 | /// File::open("my_cert.pem")? 91 | /// .read_to_end(&mut buf)?; 92 | /// let cert = nightfly::Certificate::from_pem(&buf)?; 93 | /// # drop(cert); 94 | /// # Ok(()) 95 | /// # } 96 | /// ``` 97 | pub fn from_pem(pem: &[u8]) -> crate::Result { 98 | Ok(Certificate { 99 | #[cfg(feature = "native-tls-crate")] 100 | native: native_tls_crate::Certificate::from_pem(pem).map_err(crate::error::builder)?, 101 | #[cfg(feature = "__rustls")] 102 | original: Cert::Pem(pem.to_owned()), 103 | }) 104 | } 105 | 106 | #[cfg(feature = "native-tls-crate")] 107 | pub(crate) fn add_to_native_tls(self, tls: &mut native_tls_crate::TlsConnectorBuilder) { 108 | tls.add_root_certificate(self.native); 109 | } 110 | 111 | #[cfg(feature = "__rustls")] 112 | pub(crate) fn add_to_rustls( 113 | self, 114 | root_cert_store: &mut rustls::RootCertStore, 115 | ) -> crate::Result<()> { 116 | use std::io::Cursor; 117 | 118 | match self.original { 119 | Cert::Der(buf) => root_cert_store 120 | .add(&rustls::Certificate(buf)) 121 | .map_err(crate::error::builder)?, 122 | Cert::Pem(buf) => { 123 | let mut pem = Cursor::new(buf); 124 | let certs = rustls_pemfile::certs(&mut pem).map_err(|_| { 125 | crate::error::builder(TLSError::General(String::from( 126 | "No valid certificate was found", 127 | ))) 128 | })?; 129 | for c in certs { 130 | root_cert_store 131 | .add(&rustls::Certificate(c)) 132 | .map_err(crate::error::builder)?; 133 | } 134 | } 135 | } 136 | Ok(()) 137 | } 138 | } 139 | 140 | impl Identity { 141 | /// Parses a DER-formatted PKCS #12 archive, using the specified password to decrypt the key. 142 | /// 143 | /// The archive should contain a leaf certificate and its private key, as well any intermediate 144 | /// certificates that allow clients to build a chain to a trusted root. 145 | /// The chain certificates should be in order from the leaf certificate towards the root. 146 | /// 147 | /// PKCS #12 archives typically have the file extension `.p12` or `.pfx`, and can be created 148 | /// with the OpenSSL `pkcs12` tool: 149 | /// 150 | /// ```bash 151 | /// openssl pkcs12 -export -out identity.pfx -inkey key.pem -in cert.pem -certfile chain_certs.pem 152 | /// ``` 153 | /// 154 | /// # Examples 155 | /// 156 | /// ``` 157 | /// # use std::fs::File; 158 | /// # use std::io::Read; 159 | /// # fn pkcs12() -> Result<(), Box> { 160 | /// let mut buf = Vec::new(); 161 | /// File::open("my-ident.pfx")? 162 | /// .read_to_end(&mut buf)?; 163 | /// let pkcs12 = nightfly::Identity::from_pkcs12_der(&buf, "my-privkey-password")?; 164 | /// # drop(pkcs12); 165 | /// # Ok(()) 166 | /// # } 167 | /// ``` 168 | /// 169 | /// # Optional 170 | /// 171 | /// This requires the `native-tls` Cargo feature enabled. 172 | #[cfg(feature = "native-tls")] 173 | pub fn from_pkcs12_der(der: &[u8], password: &str) -> crate::Result { 174 | Ok(Identity { 175 | inner: ClientCert::Pkcs12( 176 | native_tls_crate::Identity::from_pkcs12(der, password) 177 | .map_err(crate::error::builder)?, 178 | ), 179 | }) 180 | } 181 | 182 | /// Parses PEM encoded private key and certificate. 183 | /// 184 | /// The input should contain a PEM encoded private key 185 | /// and at least one PEM encoded certificate. 186 | /// 187 | /// Note: The private key must be in RSA, SEC1 Elliptic Curve or PKCS#8 format. 188 | /// 189 | /// # Examples 190 | /// 191 | /// ``` 192 | /// # use std::fs::File; 193 | /// # use std::io::Read; 194 | /// # fn pem() -> Result<(), Box> { 195 | /// let mut buf = Vec::new(); 196 | /// File::open("my-ident.pem")? 197 | /// .read_to_end(&mut buf)?; 198 | /// let id = nightfly::Identity::from_pem(&buf)?; 199 | /// # drop(id); 200 | /// # Ok(()) 201 | /// # } 202 | /// ``` 203 | /// 204 | /// # Optional 205 | /// 206 | /// This requires the `rustls-tls(-...)` Cargo feature enabled. 207 | #[cfg(feature = "__rustls")] 208 | pub fn from_pem(buf: &[u8]) -> crate::Result { 209 | use std::io::Cursor; 210 | 211 | let (key, certs) = { 212 | let mut pem = Cursor::new(buf); 213 | let mut sk = Vec::::new(); 214 | let mut certs = Vec::::new(); 215 | 216 | for item in std::iter::from_fn(|| rustls_pemfile::read_one(&mut pem).transpose()) { 217 | match item.map_err(|_| { 218 | crate::error::builder(TLSError::General(String::from( 219 | "Invalid identity PEM file", 220 | ))) 221 | })? { 222 | rustls_pemfile::Item::X509Certificate(cert) => { 223 | certs.push(rustls::Certificate(cert)) 224 | } 225 | rustls_pemfile::Item::PKCS8Key(key) => sk.push(rustls::PrivateKey(key)), 226 | rustls_pemfile::Item::RSAKey(key) => sk.push(rustls::PrivateKey(key)), 227 | rustls_pemfile::Item::ECKey(key) => sk.push(rustls::PrivateKey(key)), 228 | _ => { 229 | return Err(crate::error::builder(TLSError::General(String::from( 230 | "No valid certificate was found", 231 | )))) 232 | } 233 | } 234 | } 235 | 236 | if let (Some(sk), false) = (sk.pop(), certs.is_empty()) { 237 | (sk, certs) 238 | } else { 239 | return Err(crate::error::builder(TLSError::General(String::from( 240 | "private key or certificate not found", 241 | )))); 242 | } 243 | }; 244 | 245 | Ok(Identity { 246 | inner: ClientCert::Pem { key, certs }, 247 | }) 248 | } 249 | 250 | #[cfg(feature = "native-tls")] 251 | pub(crate) fn add_to_native_tls( 252 | self, 253 | tls: &mut native_tls_crate::TlsConnectorBuilder, 254 | ) -> crate::Result<()> { 255 | match self.inner { 256 | ClientCert::Pkcs12(id) => { 257 | tls.identity(id); 258 | Ok(()) 259 | } 260 | #[cfg(feature = "__rustls")] 261 | ClientCert::Pem { .. } => Err(crate::error::builder("incompatible TLS identity type")), 262 | } 263 | } 264 | 265 | #[cfg(feature = "__rustls")] 266 | pub(crate) fn add_to_rustls( 267 | self, 268 | config_builder: rustls::ConfigBuilder< 269 | rustls::ClientConfig, 270 | rustls::client::WantsTransparencyPolicyOrClientCert, 271 | >, 272 | ) -> crate::Result { 273 | match self.inner { 274 | ClientCert::Pem { key, certs } => config_builder 275 | .with_single_cert(certs, key) 276 | .map_err(crate::error::builder), 277 | #[cfg(feature = "native-tls")] 278 | ClientCert::Pkcs12(..) => Err(crate::error::builder("incompatible TLS identity type")), 279 | } 280 | } 281 | } 282 | 283 | impl fmt::Debug for Certificate { 284 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 285 | f.debug_struct("Certificate").finish() 286 | } 287 | } 288 | 289 | impl fmt::Debug for Identity { 290 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 291 | f.debug_struct("Identity").finish() 292 | } 293 | } 294 | 295 | /// A TLS protocol version. 296 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 297 | pub struct Version(InnerVersion); 298 | 299 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 300 | #[non_exhaustive] 301 | enum InnerVersion { 302 | Tls1_0, 303 | Tls1_1, 304 | Tls1_2, 305 | Tls1_3, 306 | } 307 | 308 | // These could perhaps be From/TryFrom implementations, but those would be 309 | // part of the public API so let's be careful 310 | impl Version { 311 | /// Version 1.0 of the TLS protocol. 312 | pub const TLS_1_0: Version = Version(InnerVersion::Tls1_0); 313 | /// Version 1.1 of the TLS protocol. 314 | pub const TLS_1_1: Version = Version(InnerVersion::Tls1_1); 315 | /// Version 1.2 of the TLS protocol. 316 | pub const TLS_1_2: Version = Version(InnerVersion::Tls1_2); 317 | /// Version 1.3 of the TLS protocol. 318 | pub const TLS_1_3: Version = Version(InnerVersion::Tls1_3); 319 | 320 | #[cfg(feature = "default-tls")] 321 | pub(crate) fn to_native_tls(self) -> Option { 322 | match self.0 { 323 | InnerVersion::Tls1_0 => Some(native_tls_crate::Protocol::Tlsv10), 324 | InnerVersion::Tls1_1 => Some(native_tls_crate::Protocol::Tlsv11), 325 | InnerVersion::Tls1_2 => Some(native_tls_crate::Protocol::Tlsv12), 326 | InnerVersion::Tls1_3 => None, 327 | } 328 | } 329 | 330 | #[cfg(feature = "__rustls")] 331 | pub(crate) fn from_rustls(version: rustls::ProtocolVersion) -> Option { 332 | match version { 333 | rustls::ProtocolVersion::SSLv2 => None, 334 | rustls::ProtocolVersion::SSLv3 => None, 335 | rustls::ProtocolVersion::TLSv1_0 => Some(Self(InnerVersion::Tls1_0)), 336 | rustls::ProtocolVersion::TLSv1_1 => Some(Self(InnerVersion::Tls1_1)), 337 | rustls::ProtocolVersion::TLSv1_2 => Some(Self(InnerVersion::Tls1_2)), 338 | rustls::ProtocolVersion::TLSv1_3 => Some(Self(InnerVersion::Tls1_3)), 339 | _ => None, 340 | } 341 | } 342 | } 343 | 344 | pub(crate) enum TlsBackend { 345 | #[cfg(feature = "default-tls")] 346 | Default, 347 | #[cfg(feature = "native-tls")] 348 | BuiltNativeTls(native_tls_crate::TlsConnector), 349 | #[cfg(feature = "__rustls")] 350 | Rustls, 351 | #[cfg(feature = "__rustls")] 352 | BuiltRustls(rustls::ClientConfig), 353 | #[cfg(any(feature = "native-tls", feature = "__rustls",))] 354 | UnknownPreconfigured, 355 | } 356 | 357 | impl fmt::Debug for TlsBackend { 358 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 359 | match self { 360 | #[cfg(feature = "default-tls")] 361 | TlsBackend::Default => write!(f, "Default"), 362 | #[cfg(feature = "native-tls")] 363 | TlsBackend::BuiltNativeTls(_) => write!(f, "BuiltNativeTls"), 364 | #[cfg(feature = "__rustls")] 365 | TlsBackend::Rustls => write!(f, "Rustls"), 366 | #[cfg(feature = "__rustls")] 367 | TlsBackend::BuiltRustls(_) => write!(f, "BuiltRustls"), 368 | #[cfg(any(feature = "native-tls", feature = "__rustls",))] 369 | TlsBackend::UnknownPreconfigured => write!(f, "UnknownPreconfigured"), 370 | } 371 | } 372 | } 373 | 374 | impl Default for TlsBackend { 375 | fn default() -> TlsBackend { 376 | #[cfg(feature = "default-tls")] 377 | { 378 | TlsBackend::Default 379 | } 380 | 381 | #[cfg(all(feature = "__rustls", not(feature = "default-tls")))] 382 | { 383 | TlsBackend::Rustls 384 | } 385 | } 386 | } 387 | 388 | #[cfg(feature = "__rustls")] 389 | pub(crate) struct NoVerifier; 390 | 391 | #[cfg(feature = "__rustls")] 392 | impl ServerCertVerifier for NoVerifier { 393 | fn verify_server_cert( 394 | &self, 395 | _end_entity: &rustls::Certificate, 396 | _intermediates: &[rustls::Certificate], 397 | _server_name: &ServerName, 398 | _scts: &mut dyn Iterator, 399 | _ocsp_response: &[u8], 400 | _now: std::time::SystemTime, 401 | ) -> Result { 402 | Ok(ServerCertVerified::assertion()) 403 | } 404 | 405 | fn verify_tls12_signature( 406 | &self, 407 | _message: &[u8], 408 | _cert: &rustls::Certificate, 409 | _dss: &DigitallySignedStruct, 410 | ) -> Result { 411 | Ok(HandshakeSignatureValid::assertion()) 412 | } 413 | 414 | fn verify_tls13_signature( 415 | &self, 416 | _message: &[u8], 417 | _cert: &rustls::Certificate, 418 | _dss: &DigitallySignedStruct, 419 | ) -> Result { 420 | Ok(HandshakeSignatureValid::assertion()) 421 | } 422 | } 423 | 424 | #[cfg(test)] 425 | mod tests { 426 | use super::*; 427 | 428 | #[lunatic::test] 429 | fn certificate_from_der_invalid() { 430 | Certificate::from_der(b"not der").unwrap_err(); 431 | } 432 | 433 | #[lunatic::lunatic::test] 434 | fn certificate_from_pem_invalid() { 435 | Certificate::from_pem(b"not pem").unwrap_err(); 436 | } 437 | 438 | #[lunatic::test] 439 | fn identity_from_pkcs12_der_invalid() { 440 | Identity::from_pkcs12_der(b"not der", "nope").unwrap_err(); 441 | } 442 | 443 | #[lunatic::test] 444 | fn identity_from_pem_invalid() { 445 | Identity::from_pem(b"not pem").unwrap_err(); 446 | } 447 | 448 | #[lunatic::test] 449 | fn identity_from_pem_pkcs1_key() { 450 | let pem = b"-----BEGIN CERTIFICATE-----\n\ 451 | -----END CERTIFICATE-----\n\ 452 | -----BEGIN RSA PRIVATE KEY-----\n\ 453 | -----END RSA PRIVATE KEY-----\n"; 454 | 455 | Identity::from_pem(pem).unwrap(); 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::header::{Entry, HeaderMap, OccupiedEntry}; 2 | 3 | pub(crate) fn replace_headers(dst: &mut HeaderMap, src: HeaderMap) { 4 | // IntoIter of HeaderMap yields (Option, HeaderValue). 5 | // The first time a name is yielded, it will be Some(name), and if 6 | // there are more values with the same name, the next yield will be 7 | // None. 8 | 9 | let mut prev_entry: Option> = None; 10 | for (key, value) in src { 11 | match key { 12 | Some(key) => match dst.entry(key) { 13 | Entry::Occupied(mut e) => { 14 | e.insert(value); 15 | prev_entry = Some(e); 16 | } 17 | Entry::Vacant(e) => { 18 | let e = e.insert_entry(value); 19 | prev_entry = Some(e); 20 | } 21 | }, 22 | None => match prev_entry { 23 | Some(ref mut entry) => { 24 | entry.append(value); 25 | } 26 | None => unreachable!("HeaderMap::into_iter yielded None first"), 27 | }, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | //! HTTP version 2 | //! 3 | //! This module contains a definition of the `Version` type. The `Version` 4 | //! type is intended to be accessed through the root of the crate 5 | //! (`http::Version`) rather than this module. 6 | //! 7 | //! The `Version` type contains constants that represent the various versions 8 | //! of the HTTP protocol. 9 | //! 10 | //! # Examples 11 | //! 12 | //! ``` 13 | //! use http::Version; 14 | //! 15 | //! let http11 = Version::HTTP_11; 16 | //! let http2 = Version::HTTP_2; 17 | //! assert!(http11 != http2); 18 | //! 19 | //! println!("{:?}", http2); 20 | //! ``` 21 | 22 | use std::fmt; 23 | 24 | use serde::{Deserialize, Serialize}; 25 | 26 | /// Represents a version of the HTTP spec. 27 | #[derive(PartialEq, PartialOrd, Copy, Clone, Eq, Ord, Hash, Deserialize, Serialize)] 28 | pub struct Version(Http); 29 | 30 | impl Version { 31 | /// `HTTP/0.9` 32 | pub const HTTP_09: Version = Version(Http::Http09); 33 | 34 | /// `HTTP/1.0` 35 | pub const HTTP_10: Version = Version(Http::Http10); 36 | 37 | /// `HTTP/1.1` 38 | pub const HTTP_11: Version = Version(Http::Http11); 39 | 40 | /// `HTTP/2.0` 41 | pub const HTTP_2: Version = Version(Http::H2); 42 | 43 | /// `HTTP/3.0` 44 | pub const HTTP_3: Version = Version(Http::H3); 45 | } 46 | 47 | impl From for Version { 48 | fn from(version: http::Version) -> Self { 49 | match version { 50 | http::Version::HTTP_09 => Version::HTTP_09, 51 | http::Version::HTTP_10 => Version::HTTP_10, 52 | http::Version::HTTP_11 => Version::HTTP_11, 53 | http::Version::HTTP_2 => Version::HTTP_2, 54 | http::Version::HTTP_3 => Version::HTTP_3, 55 | _ => unimplemented!(), 56 | } 57 | } 58 | } 59 | 60 | impl From for http::Version { 61 | fn from(version: Version) -> Self { 62 | match version { 63 | Version::HTTP_09 => http::Version::HTTP_09, 64 | Version::HTTP_10 => http::Version::HTTP_10, 65 | Version::HTTP_11 => http::Version::HTTP_11, 66 | Version::HTTP_2 => http::Version::HTTP_2, 67 | Version::HTTP_3 => http::Version::HTTP_3, 68 | _ => unimplemented!(), 69 | } 70 | } 71 | } 72 | 73 | #[derive(PartialEq, PartialOrd, Copy, Clone, Eq, Ord, Hash, Deserialize, Serialize)] 74 | enum Http { 75 | Http09, 76 | Http10, 77 | Http11, 78 | H2, 79 | H3, 80 | __NonExhaustive, 81 | } 82 | 83 | impl Default for Version { 84 | #[inline] 85 | fn default() -> Version { 86 | Version::HTTP_11 87 | } 88 | } 89 | 90 | impl fmt::Debug for Version { 91 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 92 | use self::Http::*; 93 | 94 | f.write_str(match self.0 { 95 | Http09 => "HTTP/0.9", 96 | Http10 => "HTTP/1.0", 97 | Http11 => "HTTP/1.1", 98 | H2 => "HTTP/2.0", 99 | H3 => "HTTP/3.0", 100 | __NonExhaustive => unreachable!(), 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/badssl.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "__tls", not(feature = "rustls-tls-manual-roots")))] 2 | #[lunatic::test] 3 | fn test_badssl_modern() { 4 | let text = nightfly::Client::builder() 5 | .no_proxy() 6 | .build() 7 | .unwrap() 8 | .get("https://mozilla-modern.badssl.com/") 9 | .send() 10 | .unwrap() 11 | .text() 12 | .unwrap(); 13 | 14 | assert!(text.contains("mozilla-modern.badssl.com")); 15 | } 16 | 17 | #[cfg(any( 18 | feature = "rustls-tls-webpki-roots", 19 | feature = "rustls-tls-native-roots" 20 | ))] 21 | #[lunatic::test] 22 | fn test_rustls_badssl_modern() { 23 | let text = nightfly::Client::builder() 24 | .use_rustls_tls() 25 | .no_proxy() 26 | .build() 27 | .unwrap() 28 | .get("https://mozilla-modern.badssl.com/") 29 | .send() 30 | .unwrap() 31 | .text() 32 | .unwrap(); 33 | 34 | assert!(text.contains("mozilla-modern.badssl.com")); 35 | } 36 | 37 | #[cfg(feature = "__tls")] 38 | #[lunatic::test] 39 | fn test_badssl_self_signed() { 40 | let text = nightfly::Client::builder() 41 | .danger_accept_invalid_certs(true) 42 | .no_proxy() 43 | .build() 44 | .unwrap() 45 | .get("https://self-signed.badssl.com/") 46 | .send() 47 | .unwrap() 48 | .text() 49 | .unwrap(); 50 | 51 | assert!(text.contains("self-signed.badssl.com")); 52 | } 53 | 54 | #[cfg(feature = "__tls")] 55 | #[lunatic::test] 56 | fn test_badssl_no_built_in_roots() { 57 | let result = nightfly::Client::builder() 58 | .tls_built_in_root_certs(false) 59 | .no_proxy() 60 | .build() 61 | .unwrap() 62 | .get("https://mozilla-modern.badssl.com/") 63 | .send(); 64 | 65 | assert!(result.is_err()); 66 | } 67 | 68 | #[cfg(feature = "native-tls")] 69 | #[lunatic::test] 70 | fn test_badssl_wrong_host() { 71 | let text = nightfly::Client::builder() 72 | .danger_accept_invalid_hostnames(true) 73 | .no_proxy() 74 | .build() 75 | .unwrap() 76 | .get("https://wrong.host.badssl.com/") 77 | .send() 78 | .unwrap() 79 | .text() 80 | .unwrap(); 81 | 82 | assert!(text.contains("wrong.host.badssl.com")); 83 | 84 | let result = nightfly::Client::builder() 85 | .danger_accept_invalid_hostnames(true) 86 | .build() 87 | .unwrap() 88 | .get("https://self-signed.badssl.com/") 89 | .send(); 90 | 91 | assert!(result.is_err()); 92 | } 93 | -------------------------------------------------------------------------------- /tests/blocking.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use http::{HeaderMap, HeaderValue}; 4 | use nightfly::StatusCode; 5 | use submillisecond::{response::Response as SubmsResponse, router, Json, RequestContext}; 6 | use support::RouterFn; 7 | 8 | fn index() -> &'static str { 9 | "Hello" 10 | } 11 | 12 | fn non_utf8_text() -> SubmsResponse { 13 | SubmsResponse::builder() 14 | .header("Content-Type", "text/plain; charset=gbk") 15 | .body(b"\xc4\xe3\xba\xc3".to_vec()) 16 | .unwrap() 17 | } 18 | 19 | fn ensure_hello(hello: String) -> SubmsResponse { 20 | assert_eq!(hello, "Hello".to_string()); 21 | SubmsResponse::default() 22 | } 23 | 24 | fn empty_response() -> SubmsResponse { 25 | SubmsResponse::default() 26 | } 27 | 28 | fn res_400() -> (StatusCode, &'static str) { 29 | (StatusCode::BAD_REQUEST, "Resource not found") 30 | } 31 | 32 | fn get_json() -> Json { 33 | Json("Hello".to_string()) 34 | } 35 | 36 | fn default_headers(headers: HeaderMap) -> SubmsResponse { 37 | assert_eq!( 38 | headers.get("nightfly-test"), 39 | Some(&HeaderValue::from_str("orly").unwrap()) 40 | ); 41 | SubmsResponse::default() 42 | } 43 | 44 | fn overwrite_headers(headers: HeaderMap) -> SubmsResponse { 45 | assert_eq!( 46 | headers.get("authorization"), 47 | Some(&HeaderValue::from_str("secret").unwrap()) 48 | ); 49 | SubmsResponse::default() 50 | } 51 | 52 | fn appended_headers(headers: HeaderMap) -> SubmsResponse { 53 | let mut h = headers.get_all("accept").iter(); 54 | lunatic_log::info!( 55 | "GOT HEADERS {:?} | {:?} | {:?}", 56 | headers, 57 | h.next(), 58 | h.next() 59 | ); 60 | let mut accepts = headers.get_all("accept").into_iter(); 61 | assert_eq!(accepts.next().unwrap(), "application/json"); 62 | assert_eq!(accepts.next().unwrap(), "application/json+hal"); 63 | assert_eq!(accepts.next(), None); 64 | SubmsResponse::default() 65 | } 66 | 67 | static ADDR: &'static str = "0.0.0.0:3000"; 68 | 69 | static ROUTER: RouterFn = router! { 70 | GET "/text" => index 71 | GET "/non_utf8_text" => non_utf8_text 72 | GET "/1" => empty_response 73 | POST "/2" => ensure_hello 74 | GET "/err_400" => res_400 75 | GET "/json" => get_json 76 | GET "/default_headers" => default_headers 77 | GET "/overwrite_headers" => overwrite_headers 78 | GET "/4" => appended_headers 79 | }; 80 | 81 | wrap_server!(server, ROUTER, ADDR); 82 | 83 | #[lunatic::test] 84 | fn test_response_text() { 85 | let _ = server::ensure_server(); 86 | 87 | let url = format!("http://{}/text", ADDR); 88 | let res = nightfly::get(&url).unwrap(); 89 | assert_eq!(res.url().as_str(), &url); 90 | assert_eq!(res.status(), nightfly::StatusCode::OK); 91 | assert_eq!(res.content_length(), Some(5)); 92 | 93 | let body = res.text().unwrap(); 94 | assert_eq!(b"Hello", body.as_bytes()); 95 | } 96 | 97 | #[lunatic::test] 98 | fn test_response_non_utf_8_text() { 99 | // maybe wait for server to spawn 100 | let _ = server::ensure_server(); 101 | 102 | let url = format!("http://{}/non_utf8_text", ADDR); 103 | let res = nightfly::get(&url).unwrap(); 104 | assert_eq!(res.url().as_str(), &url); 105 | assert_eq!(res.status(), nightfly::StatusCode::OK); 106 | assert_eq!(res.content_length(), Some(4)); 107 | 108 | let body = res.text().unwrap(); 109 | assert_eq!("你好", &body); 110 | assert_eq!(b"\xe4\xbd\xa0\xe5\xa5\xbd", body.as_bytes()); // Now it's utf-8 111 | } 112 | 113 | #[lunatic::test] 114 | // #[cfg(feature = "json")] 115 | fn test_response_json() { 116 | // maybe wait for server to spawn 117 | let _ = server::ensure_server(); 118 | 119 | let url = format!("http://{}/json", ADDR); 120 | let res = nightfly::get(&url).unwrap(); 121 | assert_eq!(res.url().as_str(), &url); 122 | assert_eq!(res.status(), nightfly::StatusCode::OK); 123 | assert_eq!(res.content_length(), Some(7)); 124 | 125 | let body = res.json::().unwrap(); 126 | assert_eq!("Hello", body); 127 | } 128 | 129 | #[lunatic::test] 130 | fn test_get() { 131 | // maybe wait for server to spawn 132 | let _ = server::ensure_server(); 133 | 134 | let url = format!("http://{}/1", ADDR); 135 | let res = nightfly::get(&url).unwrap(); 136 | 137 | assert_eq!(res.url().as_str(), &url); 138 | assert_eq!(res.status(), nightfly::StatusCode::OK); 139 | // assert_eq!(res.remote_addr(), Some(ADDR)); 140 | 141 | assert_eq!(res.text().unwrap().len(), 0) 142 | } 143 | 144 | #[lunatic::test] 145 | fn test_post() { 146 | // maybe wait for server to spawn 147 | let _ = server::ensure_server(); 148 | 149 | let url = format!("http://{}/2", ADDR); 150 | let res = nightfly::Client::new() 151 | .post(&url) 152 | .text("Hello") 153 | .send() 154 | .unwrap(); 155 | 156 | assert_eq!(res.url().as_str(), &url); 157 | assert_eq!(res.status(), nightfly::StatusCode::OK); 158 | } 159 | 160 | // #[lunatic::test] 161 | // fn test_post_form() { 162 | // let server = server::http(move |req| async move { 163 | // assert_eq!(req.method(), "POST"); 164 | // assert_eq!(req.headers()["content-length"], "24"); 165 | // assert_eq!( 166 | // req.headers()["content-type"], 167 | // "application/x-www-form-urlencoded" 168 | // ); 169 | 170 | // let data = hyper::body::to_bytes(req.into_body()).unwrap(); 171 | // assert_eq!(&*data, b"hello=world&sean=monstar"); 172 | 173 | // http::Response::default() 174 | // }); 175 | 176 | // let form = &[("hello", "world"), ("sean", "monstar")]; 177 | 178 | // let url = format!("http://{}/form", ADDR); 179 | // let res = nightfly::Client::new() 180 | // .post(&url) 181 | // .form(form) 182 | // .send() 183 | // .expect("request send"); 184 | 185 | // assert_eq!(res.url().as_str(), &url); 186 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 187 | // } 188 | 189 | /// Calling `Response::error_for_status`` on a response with status in 4xx 190 | /// returns a error. 191 | #[lunatic::test] 192 | fn test_error_for_status_4xx() { 193 | // maybe wait for server to spawn 194 | let _ = server::ensure_server(); 195 | 196 | let url = format!("http://{}/err_400", ADDR); 197 | let res = nightfly::get(&url).unwrap(); 198 | 199 | let err = res.error_for_status().unwrap_err(); 200 | assert!(err.is_status()); 201 | assert_eq!(err.status(), Some(nightfly::StatusCode::BAD_REQUEST)); 202 | } 203 | 204 | /// Calling `Response::error_for_status`` on a response with status in 5xx 205 | /// returns a error. 206 | #[lunatic::test] 207 | fn test_error_for_status_5xx() { 208 | // maybe wait for server to spawn 209 | let _ = server::ensure_server(); 210 | 211 | let url = format!("http://{}/2", ADDR); 212 | let res = nightfly::Client::new() 213 | .post(&url) 214 | .text("invalid string") 215 | .send() 216 | .unwrap(); 217 | 218 | let err = res.error_for_status().unwrap_err(); 219 | assert!(err.is_status()); 220 | assert_eq!( 221 | err.status(), 222 | Some(nightfly::StatusCode::INTERNAL_SERVER_ERROR) 223 | ); 224 | } 225 | 226 | #[lunatic::test] 227 | fn test_default_headers() { 228 | // maybe wait for server to spawn 229 | let _ = server::ensure_server(); 230 | 231 | let mut headers = http::HeaderMap::with_capacity(1); 232 | headers.insert("nightfly-test", "orly".parse().unwrap()); 233 | let client = nightfly::Client::builder() 234 | .default_headers(headers) 235 | .build() 236 | .unwrap(); 237 | 238 | let url = format!("http://{}/default_headers", ADDR); 239 | let res = client.get(&url).send().unwrap(); 240 | 241 | assert_eq!(res.url().as_str(), &url); 242 | println!("GOT DEFAULT HEADERS {res:?}"); 243 | assert_eq!(res.status(), nightfly::StatusCode::OK); 244 | } 245 | 246 | #[lunatic::test] 247 | fn test_override_default_headers() { 248 | // maybe wait for server to spawn 249 | let _ = server::ensure_server(); 250 | 251 | let mut headers = http::HeaderMap::with_capacity(1); 252 | headers.insert( 253 | http::header::AUTHORIZATION, 254 | http::header::HeaderValue::from_static("iamatoken"), 255 | ); 256 | let client = nightfly::Client::builder() 257 | .default_headers(headers) 258 | .build() 259 | .unwrap(); 260 | 261 | let url = format!("http://{}/overwrite_headers", ADDR); 262 | let res = client 263 | .get(&url) 264 | .header( 265 | http::header::AUTHORIZATION, 266 | http::header::HeaderValue::from_static("secret"), 267 | ) 268 | .send() 269 | .unwrap(); 270 | 271 | assert_eq!(res.url().as_str(), &url); 272 | assert_eq!(res.status(), nightfly::StatusCode::OK); 273 | } 274 | 275 | #[lunatic::test] 276 | fn test_appended_headers_not_overwritten() { 277 | // maybe wait for server to spawn 278 | let _ = server::ensure_server(); 279 | 280 | let client = nightfly::Client::new(); 281 | 282 | let url = format!("http://{}/4", ADDR); 283 | let res = client 284 | .get(&url) 285 | .header(header::ACCEPT, "application/json") 286 | .header(header::ACCEPT, "application/json+hal") 287 | .send() 288 | .unwrap(); 289 | 290 | assert_eq!(res.url().as_str(), &url); 291 | println!("GOT RES {:?}", res); 292 | assert_eq!(res.status(), nightfly::StatusCode::OK); 293 | 294 | // make sure this also works with default headers 295 | use nightfly::header; 296 | let mut headers = header::HeaderMap::with_capacity(1); 297 | headers.insert( 298 | header::ACCEPT, 299 | header::HeaderValue::from_static("text/html"), 300 | ); 301 | let client = nightfly::Client::builder() 302 | .default_headers(headers) 303 | .build() 304 | .unwrap(); 305 | 306 | let url = format!("http://{}/4", ADDR); 307 | let res = client 308 | .get(&url) 309 | .header(header::ACCEPT, "application/json") 310 | .header(header::ACCEPT, "application/json+hal") 311 | .send() 312 | .unwrap(); 313 | 314 | assert_eq!(res.url().as_str(), &url); 315 | assert_eq!(res.status(), nightfly::StatusCode::OK); 316 | } 317 | 318 | // #[cfg(feature = "default-tls")] 319 | // #[lunatic::test] 320 | // fn test_allowed_methods_blocking() { 321 | // let resp = nightfly::Client::builder() 322 | // .https_only(true) 323 | // .build() 324 | // .expect("client builder") 325 | // .get("https://google.com") 326 | // .send(); 327 | 328 | // assert_eq!(resp.is_err(), false); 329 | 330 | // let resp = nightfly::Client::builder() 331 | // .https_only(true) 332 | // .build() 333 | // .expect("client builder") 334 | // .get("http://google.com") 335 | // .send(); 336 | 337 | // assert_eq!(resp.is_err(), true); 338 | // } 339 | 340 | /// Test that a [`nightfly::Body`] can be created from [`bytes::Bytes`]. 341 | #[lunatic::test] 342 | fn test_body_from_bytes() { 343 | let body = "abc"; 344 | // No external calls are needed. Only the request building is tested. 345 | let request = nightfly::Client::builder() 346 | .build() 347 | .expect("Could not build the client") 348 | .put("https://google.com") 349 | .body(bytes::Bytes::from(body)) 350 | .build() 351 | .expect("Invalid body"); 352 | 353 | let inner = request.body().unwrap().clone().inner(); 354 | assert_eq!(&inner[..], body.as_bytes()); 355 | } 356 | -------------------------------------------------------------------------------- /tests/brotli.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 4 | use support::RouterFn; 5 | 6 | fn brotli(req: RequestContext) -> SubmsResponse { 7 | assert_eq!(req.method(), "HEAD"); 8 | 9 | SubmsResponse::builder() 10 | .header("content-encoding", "br") 11 | .header("content-length", 100) 12 | .body(vec![]) 13 | .unwrap() 14 | } 15 | 16 | fn accept(req: RequestContext) -> SubmsResponse { 17 | assert_eq!(req.headers()["accept"], "application/json"); 18 | assert!(req.headers()["accept-encoding"] 19 | .to_str() 20 | .unwrap() 21 | .contains("br")); 22 | SubmsResponse::default() 23 | } 24 | 25 | fn accept_encoding(req: RequestContext) -> SubmsResponse { 26 | assert_eq!(req.headers()["accept"], "*/*"); 27 | assert_eq!(req.headers()["accept-encoding"], "identity"); 28 | SubmsResponse::default() 29 | } 30 | 31 | static ROUTER: RouterFn = router! { 32 | HEAD "/brotli" => brotli 33 | GET "/accept" => accept 34 | GET "/accept-encoding" => accept_encoding 35 | }; 36 | 37 | static ADDR: &'static str = "0.0.0.0:3000"; 38 | 39 | wrap_server!(server, ROUTER, ADDR); 40 | 41 | // ==================================== 42 | // Test cases 43 | // ==================================== 44 | 45 | #[lunatic::test] 46 | fn test_brotli_empty_body() { 47 | let _ = server::ensure_server(); 48 | 49 | let client = nightfly::Client::new(); 50 | let res = client 51 | .head(&format!("http://{}/brotli", ADDR)) 52 | .send() 53 | .unwrap(); 54 | 55 | let body = res.text().unwrap(); 56 | 57 | assert_eq!(body, ""); 58 | } 59 | 60 | #[lunatic::test] 61 | fn test_accept_header_is_not_changed_if_set() { 62 | let _ = server::ensure_server(); 63 | 64 | let client = nightfly::Client::new(); 65 | 66 | let res = client 67 | .get(&format!("http://{}/accept", ADDR)) 68 | .header( 69 | nightfly::header::ACCEPT, 70 | nightfly::header::HeaderValue::from_static("application/json"), 71 | ) 72 | .send() 73 | .unwrap(); 74 | 75 | assert_eq!(res.status(), nightfly::StatusCode::OK); 76 | } 77 | 78 | #[lunatic::test] 79 | fn test_accept_encoding_header_is_not_changed_if_set() { 80 | let _ = server::ensure_server(); 81 | 82 | let client = nightfly::Client::new(); 83 | 84 | let res = client 85 | .get(&format!("http://{}/accept-encoding", ADDR)) 86 | .header( 87 | nightfly::header::ACCEPT_ENCODING, 88 | nightfly::header::HeaderValue::from_static("identity"), 89 | ) 90 | .send() 91 | .unwrap(); 92 | 93 | assert_eq!(res.status(), nightfly::StatusCode::OK); 94 | } 95 | -------------------------------------------------------------------------------- /tests/chunked.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod support; 3 | 4 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 5 | use support::RouterFn; 6 | 7 | static CHUNKS: [&str; 10] = [ 8 | "4\r\n", 9 | "Wiki\r\n", 10 | "6\r\n", 11 | "pedia \r\n", 12 | "E\r\n", 13 | "in \r\n", 14 | "\r\n", 15 | "chunks.\r\n", 16 | "0\r\n", 17 | "\r\n", 18 | ]; 19 | 20 | // original gzip for reference 21 | // static GZIP_STRING: &str = "H4sIAAAAAAAACgvPzM4sSE3JTFTIzFPg4krOKM3LLtYDAFW43D4WAAAA"; 22 | // static GZIP: [u8; 44] = [ 23 | // // length = 22; 0x16 24 | // 31, 139, 8, 0, 0, 0, 0, 0, 4, 255, 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 25 | // // length = 21; 0x15 26 | // 83, 224, 229, 226, 229, 74, 206, 40, 205, 203, 46, 214, 3, 0, 102, 210, 154, 109, 24, 0, 0, 27 | // // length = 1; 0x1 28 | // 0, 29 | // ]; 30 | #[rustfmt::skip] 31 | static GZIP_CHUNKED: [u8; 66] = [ 32 | b'1', b'6', b'\r', b'\n', 33 | 31, 139, 8, 0, 0, 0, 0, 0, 4, 255, 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, b'\r', b'\n', 34 | b'1', b'5', b'\r', b'\n', 35 | 83, 224, 229, 226, 229, 74, 206, 40, 205, 203, 46, 214, 3, 0, 102, 210, 154, 109, 24, 0, 0, b'\r', b'\n', 36 | // single byte in last chunk 37 | b'1', b'\r', b'\n', 38 | 0, b'\r', b'\n', 39 | // zero length chunk 40 | b'0', b'\r', b'\n', 41 | // end of data 42 | b'\r', b'\n', 43 | ]; 44 | 45 | // original buffer for comparison 46 | // static DEFLATE_ORIG: &str = "C8/MzixITclMVMjMU+DiSs4ozcsu1gMA"; 47 | // static DEFLATE: [u8; 24] = [ 48 | // 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 83, 224, 226, 74, 206, 40, 205, 203, 46, 49 | // 214, 3, 0, 50 | // ]; 51 | 52 | #[rustfmt::skip] 53 | static DEFLATE_CHUNKED: [u8; 40] = [ 54 | b'1', b'5', b'\r', b'\n', 55 | 11, 207, 204, 206, 44, 72, 77, 201, 76, 84, 200, 204, 83, 224, 226, 74, 206, 40, 205, 203, 46, b'\r', b'\n', 56 | // three bytes in last chunk 57 | b'3', b'\r', b'\n', 58 | 214, 3, 0, b'\r', b'\n', 59 | // zero length chunk 60 | b'0', b'\r', b'\n', // end of data 61 | b'\r', b'\n', 62 | ]; 63 | 64 | fn chunked() -> SubmsResponse { 65 | SubmsResponse::builder() 66 | .header("Transfer-Encoding", "chunked") 67 | .header("Content-Length", "24") 68 | .body(CHUNKS.join("").to_owned().into_bytes()) 69 | .unwrap() 70 | } 71 | 72 | fn chunked_gzip() -> SubmsResponse { 73 | SubmsResponse::builder() 74 | .header("Transfer-Encoding", "chunked") 75 | .header("Content-encoding", "gzip") 76 | .body(GZIP_CHUNKED.to_vec()) 77 | .unwrap() 78 | } 79 | 80 | fn chunked_deflate() -> SubmsResponse { 81 | SubmsResponse::builder() 82 | .header("Transfer-Encoding", "chunked") 83 | .header("Content-encoding", "deflate") 84 | .body(DEFLATE_CHUNKED.to_vec()) 85 | .unwrap() 86 | } 87 | 88 | static ADDR: &'static str = "0.0.0.0:3001"; 89 | 90 | static ROUTER: RouterFn = router! { 91 | GET "/chunked" => chunked 92 | GET "/gzip" => chunked_gzip 93 | GET "/deflate" => chunked_deflate 94 | }; 95 | 96 | wrap_server!(chunked_server, ROUTER, ADDR); 97 | 98 | #[lunatic::test] 99 | fn test_chunked_uncompressed_body() { 100 | let _ = chunked_server::ensure_server(); 101 | 102 | let client = nightfly::Client::new(); 103 | let res = client 104 | .get(&format!("http://{}/chunked", ADDR)) 105 | .send() 106 | .unwrap(); 107 | 108 | let body = res.text().unwrap(); 109 | 110 | assert_eq!(body, "Wikipedia in \r\n\r\nchunks."); 111 | } 112 | 113 | #[lunatic::test] 114 | fn test_chunked_gzip_body() { 115 | let _ = chunked_server::ensure_server(); 116 | 117 | let client = nightfly::Client::new(); 118 | let res = client.get(&format!("http://{}/gzip", ADDR)).send().unwrap(); 119 | 120 | let body = res.text().unwrap(); 121 | 122 | assert_eq!(body, "Wikipedia in \r\n\r\nchunks."); 123 | } 124 | 125 | // #[lunatic::test] 126 | // fn test_chunked_deflate_body() { 127 | // let _ = chunked_server::ensure_server(); 128 | 129 | // let client = nightfly::Client::new(); 130 | // let res = client 131 | // .get(&format!("http://{}/deflate", ADDR)) 132 | // .send() 133 | // .unwrap(); 134 | 135 | // let body = res.text().unwrap(); 136 | 137 | // assert_eq!(body, "Wikipedia in \r\n\r\nchunks."); 138 | // } 139 | -------------------------------------------------------------------------------- /tests/client.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use http::HeaderMap; 4 | use nightfly::Client; 5 | 6 | // use lunatic::net::ToSocketAddrs; 7 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 8 | use support::RouterFn; 9 | 10 | fn text() -> SubmsResponse { 11 | SubmsResponse::new("Hello".into()) 12 | } 13 | 14 | fn user_agent(req: RequestContext) -> SubmsResponse { 15 | assert_eq!(req.headers()["user-agent"], "nightfly-test-agent"); 16 | SubmsResponse::default() 17 | } 18 | 19 | fn auto_headers(req: RequestContext) -> SubmsResponse { 20 | assert_eq!(req.method(), "GET"); 21 | 22 | assert_eq!(req.headers()["accept"], "*/*"); 23 | assert_eq!(req.headers().get("user-agent"), None); 24 | if cfg!(feature = "gzip") { 25 | assert!(req.headers()["accept-encoding"] 26 | .to_str() 27 | .unwrap() 28 | .contains("gzip")); 29 | } 30 | if cfg!(feature = "brotli") { 31 | assert!(req.headers()["accept-encoding"] 32 | .to_str() 33 | .unwrap() 34 | .contains("br")); 35 | } 36 | if cfg!(feature = "deflate") { 37 | assert!(req.headers()["accept-encoding"] 38 | .to_str() 39 | .unwrap() 40 | .contains("deflate")); 41 | } 42 | 43 | http::Response::default() 44 | } 45 | 46 | fn get_handler() -> SubmsResponse { 47 | SubmsResponse::new("pipe me".into()) 48 | } 49 | 50 | fn pipe_response(body: Vec, _headers: HeaderMap) -> SubmsResponse { 51 | lunatic_log::info!("BODY {:?} | header {:?}", body, _headers); 52 | // assert_eq!(headers["transfer-encoding"], "chunked"); 53 | 54 | assert_eq!(body, b"pipe me".to_vec()); 55 | 56 | SubmsResponse::default() 57 | } 58 | 59 | static ROUTER: RouterFn = router! { 60 | GET "/text" => text 61 | GET "/user-agent" => user_agent 62 | GET "/auto_headers" => auto_headers 63 | GET "/get" => get_handler 64 | POST "/pipe" => pipe_response 65 | }; 66 | static ADDR: &'static str = "0.0.0.0:3002"; 67 | 68 | wrap_server!(server, ROUTER, ADDR); 69 | 70 | #[lunatic::test] 71 | fn test_auto_headers() { 72 | let _ = server::ensure_server(); 73 | 74 | println!("BEFORE CALLING AUTO_HEADERS"); 75 | 76 | let url = format!("http://{}/auto_headers", ADDR); 77 | let res = nightfly::Client::builder() 78 | // .no_proxy() 79 | .build() 80 | .unwrap() 81 | .get(&url) 82 | .send() 83 | .unwrap(); 84 | 85 | println!("AUTO HEADERS {:?}", res); 86 | assert_eq!(res.url().as_str(), &url); 87 | assert_eq!(res.status(), nightfly::StatusCode::OK); 88 | // assert_eq!(res.remote_addr(), ADDR.to_socket_addrs().unwrap().next()); 89 | } 90 | 91 | #[lunatic::test] 92 | fn test_user_agent() { 93 | let _ = server::ensure_server(); 94 | 95 | let url = format!("http://{}/user-agent", ADDR); 96 | let res = nightfly::Client::builder() 97 | .user_agent("nightfly-test-agent") 98 | .build() 99 | .expect("client builder") 100 | .get(&url) 101 | .send() 102 | .expect("request"); 103 | 104 | assert_eq!(res.status(), nightfly::StatusCode::OK); 105 | } 106 | 107 | #[lunatic::test] 108 | fn test_response_text() { 109 | let _ = server::ensure_server(); 110 | 111 | let client = Client::new(); 112 | 113 | let res = client 114 | .get(&format!("http://{}/text", ADDR)) 115 | .send() 116 | .expect("Failed to get"); 117 | assert_eq!(res.content_length(), Some(5)); 118 | let text = res.text().expect("Failed to get text"); 119 | assert_eq!("Hello", text); 120 | } 121 | 122 | #[lunatic::test] 123 | fn test_response_bytes() { 124 | let _ = server::ensure_server(); 125 | 126 | let client = Client::new(); 127 | 128 | let res = client 129 | .get(&format!("http://{}/text", ADDR)) 130 | .send() 131 | .expect("Failed to get"); 132 | assert_eq!(res.content_length(), Some(5)); 133 | let bytes = res.bytes().expect("res.bytes()"); 134 | assert_eq!("Hello", bytes); 135 | } 136 | 137 | #[lunatic::test] 138 | #[cfg(feature = "json")] 139 | fn response_json() { 140 | let _ = server::ensure_server(); 141 | 142 | let server = server::http(move |_req| async { http::Response::new("\"Hello\"".into()) }); 143 | 144 | let client = Client::new(); 145 | 146 | let res = client 147 | .get(&format!("http://{}/json", ADDR)) 148 | .send() 149 | .expect("Failed to get"); 150 | let text = res.json::().expect("Failed to get json"); 151 | assert_eq!("Hello", text); 152 | } 153 | 154 | #[lunatic::test] 155 | fn body_pipe_response() { 156 | let _ = server::ensure_server(); 157 | 158 | let client = Client::new(); 159 | 160 | let res1 = client 161 | .get(&format!("http://{}/get", ADDR)) 162 | .send() 163 | .expect("get1"); 164 | 165 | assert_eq!(res1.status(), nightfly::StatusCode::OK); 166 | assert_eq!(res1.content_length(), Some(7)); 167 | 168 | println!("GOT THIS RES1 {:?}", res1.body()); 169 | 170 | // and now ensure we can "pipe" the response to another request 171 | let res2 = client 172 | .post(&format!("http://{}/pipe", ADDR)) 173 | .body(res1) 174 | .send() 175 | .expect("res2"); 176 | 177 | assert_eq!(res2.status(), nightfly::StatusCode::OK); 178 | } 179 | 180 | // #[lunatic::test] 181 | // fn overridden_dns_resolution_with_gai() { 182 | // let _ = server::ensure_server(); 183 | // let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); 184 | 185 | // let overridden_domain = "rust-lang.org"; 186 | // let url = format!( 187 | // "http://{}:{}/domain_override", 188 | // overridden_domain, 189 | // ADDR.port() 190 | // ); 191 | // let client = nightfly::Client::builder() 192 | // .resolve(overridden_domain, ADDR) 193 | // .build() 194 | // .expect("client builder"); 195 | // let req = client.get(&url); 196 | // let res = req.send().expect("request"); 197 | 198 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 199 | // let text = res.text().expect("Failed to get text"); 200 | // assert_eq!("Hello", text); 201 | // } 202 | 203 | // #[lunatic::test] 204 | // fn overridden_dns_resolution_with_gai_multiple() { 205 | // let _ = server::ensure_server(); 206 | // let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); 207 | 208 | // let overridden_domain = "rust-lang.org"; 209 | // let url = format!( 210 | // "http://{}:{}/domain_override", 211 | // overridden_domain, 212 | // ADDR.port() 213 | // ); 214 | // // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs 215 | // // algorithm decide which address to use. 216 | // let client = nightfly::Client::builder() 217 | // .resolve_to_addrs( 218 | // overridden_domain, 219 | // &[ 220 | // std::net::SocketAddr::new( 221 | // std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 222 | // ADDR.port(), 223 | // ), 224 | // ADDR, 225 | // ], 226 | // ) 227 | // .build() 228 | // .expect("client builder"); 229 | // let req = client.get(&url); 230 | // let res = req.send().expect("request"); 231 | 232 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 233 | // let text = res.text().expect("Failed to get text"); 234 | // assert_eq!("Hello", text); 235 | // } 236 | 237 | #[cfg(feature = "trust-dns")] 238 | #[lunatic::test] 239 | fn overridden_dns_resolution_with_trust_dns() { 240 | let _ = env_logger::builder().is_test(true).try_init(); 241 | let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); 242 | 243 | let overridden_domain = "rust-lang.org"; 244 | let url = format!( 245 | "http://{}:{}/domain_override", 246 | overridden_domain, 247 | ADDR.port() 248 | ); 249 | let client = nightfly::Client::builder() 250 | .resolve(overridden_domain, ADDR) 251 | .trust_dns(true) 252 | .build() 253 | .expect("client builder"); 254 | let req = client.get(&url); 255 | let res = req.send().expect("request"); 256 | 257 | assert_eq!(res.status(), nightfly::StatusCode::OK); 258 | let text = res.text().expect("Failed to get text"); 259 | assert_eq!("Hello", text); 260 | } 261 | 262 | #[cfg(feature = "trust-dns")] 263 | #[lunatic::test] 264 | fn overridden_dns_resolution_with_trust_dns_multiple() { 265 | let _ = env_logger::builder().is_test(true).try_init(); 266 | let server = server::http(move |_req| async { http::Response::new("Hello".into()) }); 267 | 268 | let overridden_domain = "rust-lang.org"; 269 | let url = format!( 270 | "http://{}:{}/domain_override", 271 | overridden_domain, 272 | ADDR.port() 273 | ); 274 | // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs 275 | // algorithm decide which address to use. 276 | let client = nightfly::Client::builder() 277 | .resolve_to_addrs( 278 | overridden_domain, 279 | &[ 280 | std::net::SocketAddr::new( 281 | std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 282 | ADDR.port(), 283 | ), 284 | ADDR, 285 | ], 286 | ) 287 | .trust_dns(true) 288 | .build() 289 | .expect("client builder"); 290 | let req = client.get(&url); 291 | let res = req.send().expect("request"); 292 | 293 | assert_eq!(res.status(), nightfly::StatusCode::OK); 294 | let text = res.text().expect("Failed to get text"); 295 | assert_eq!("Hello", text); 296 | } 297 | 298 | #[cfg(any(feature = "native-tls", feature = "__rustls",))] 299 | #[test] 300 | fn use_preconfigured_tls_with_bogus_backend() { 301 | struct DefinitelyNotTls; 302 | 303 | nightfly::Client::builder() 304 | .use_preconfigured_tls(DefinitelyNotTls) 305 | .build() 306 | .expect_err("definitely is not TLS"); 307 | } 308 | 309 | #[cfg(feature = "native-tls")] 310 | #[test] 311 | fn use_preconfigured_native_tls_default() { 312 | extern crate native_tls_crate; 313 | 314 | let tls = native_tls_crate::TlsConnector::builder() 315 | .build() 316 | .expect("tls builder"); 317 | 318 | nightfly::Client::builder() 319 | .use_preconfigured_tls(tls) 320 | .build() 321 | .expect("preconfigured default tls"); 322 | } 323 | 324 | #[cfg(feature = "__rustls")] 325 | #[test] 326 | fn use_preconfigured_rustls_default() { 327 | extern crate rustls; 328 | 329 | let root_cert_store = rustls::RootCertStore::empty(); 330 | let tls = rustls::ClientConfig::builder() 331 | .with_safe_defaults() 332 | .with_root_certificates(root_cert_store) 333 | .with_no_client_auth(); 334 | 335 | nightfly::Client::builder() 336 | .use_preconfigured_tls(tls) 337 | .build() 338 | .expect("preconfigured rustls tls"); 339 | } 340 | 341 | #[cfg(feature = "__rustls")] 342 | #[lunatic::test] 343 | #[ignore = "Needs TLS support in the test server"] 344 | fn http2_upgrade() { 345 | let server = server::http(move |_| async move { http::Response::default() }); 346 | 347 | let url = format!("https://localhost:{}", ADDR.port()); 348 | let res = nightfly::Client::builder() 349 | .danger_accept_invalid_certs(true) 350 | .use_rustls_tls() 351 | .build() 352 | .expect("client builder") 353 | .get(&url) 354 | .send() 355 | .expect("request"); 356 | 357 | assert_eq!(res.status(), nightfly::StatusCode::OK); 358 | assert_eq!(res.version(), nightfly::Version::HTTP_2); 359 | } 360 | 361 | #[lunatic::test] 362 | fn test_allowed_methods() { 363 | let resp = nightfly::Client::builder() 364 | .https_only(true) 365 | .build() 366 | .expect("client builder") 367 | .get("https://google.com") 368 | .send(); 369 | 370 | assert!(resp.is_ok()); 371 | 372 | let resp = nightfly::Client::builder() 373 | .https_only(true) 374 | .build() 375 | .expect("client builder") 376 | .get("http://google.com") 377 | .send(); 378 | 379 | assert!(resp.is_err()); 380 | } 381 | -------------------------------------------------------------------------------- /tests/cookie.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 4 | use support::RouterFn; 5 | 6 | fn max_age(req: RequestContext) -> SubmsResponse { 7 | assert_eq!(req.headers().get("cookie"), None); 8 | http::Response::builder() 9 | .header("Set-Cookie", "key=val; Max-Age=0") 10 | .body(Default::default()) 11 | .unwrap() 12 | } 13 | 14 | fn cookie_overwrite(req: RequestContext) -> SubmsResponse { 15 | if req.uri() == "/overwrite" { 16 | http::Response::builder() 17 | .header("Set-Cookie", "key=val") 18 | .body(Default::default()) 19 | .unwrap() 20 | } else if req.uri() == "/overwrite/2" { 21 | assert_eq!(req.headers()["cookie"], "key=val"); 22 | http::Response::builder() 23 | .header("Set-Cookie", "key=val2") 24 | .body(Default::default()) 25 | .unwrap() 26 | } else { 27 | assert_eq!(req.uri(), "/overwrite/3"); 28 | assert_eq!(req.headers()["cookie"], "key=val2"); 29 | SubmsResponse::default() 30 | } 31 | } 32 | 33 | fn cookie_simple(req: RequestContext) -> SubmsResponse { 34 | if req.uri() == "/2" { 35 | assert_eq!(req.headers()["cookie"], "key=val"); 36 | } 37 | http::Response::builder() 38 | .header("Set-Cookie", "key=val; HttpOnly") 39 | .body(Default::default()) 40 | .unwrap() 41 | } 42 | 43 | fn cookie_response() -> SubmsResponse { 44 | SubmsResponse::builder() 45 | .header("Set-Cookie", "key=val") 46 | .header( 47 | "Set-Cookie", 48 | "expires=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT", 49 | ) 50 | .header("Set-Cookie", "path=1; Path=/the-path") 51 | .header("Set-Cookie", "maxage=1; Max-Age=100") 52 | .header("Set-Cookie", "domain=1; Domain=mydomain") 53 | .header("Set-Cookie", "secure=1; Secure") 54 | .header("Set-Cookie", "httponly=1; HttpOnly") 55 | .header("Set-Cookie", "samesitelax=1; SameSite=Lax") 56 | .header("Set-Cookie", "samesitestrict=1; SameSite=Strict") 57 | .body(Default::default()) 58 | .unwrap() 59 | } 60 | 61 | fn expires(req: RequestContext) -> SubmsResponse { 62 | assert_eq!(req.headers().get("cookie"), None); 63 | http::Response::builder() 64 | .header( 65 | "Set-Cookie", 66 | "key=val; Expires=Wed, 21 Oct 2015 07:28:00 GMT", 67 | ) 68 | .body(Default::default()) 69 | .unwrap() 70 | } 71 | 72 | fn path(req: RequestContext) -> SubmsResponse { 73 | if req.uri() == "/path" { 74 | assert_eq!(req.headers().get("cookie"), None); 75 | SubmsResponse::builder() 76 | .header("Set-Cookie", "key=val; Path=/subpath") 77 | .body(Default::default()) 78 | .unwrap() 79 | } else { 80 | assert_eq!(req.uri(), "/subpath"); 81 | assert_eq!(req.headers()["cookie"], "key=val"); 82 | SubmsResponse::default() 83 | } 84 | } 85 | 86 | static ROUTER: RouterFn = router! { 87 | GET "/" => cookie_response 88 | GET "/1" => cookie_simple 89 | GET "/2" => cookie_simple 90 | GET "/overwrite" => cookie_overwrite 91 | GET "/overwrite/2" => cookie_overwrite 92 | GET "/overwrite/3" => cookie_overwrite 93 | GET "/max-age" => max_age 94 | GET "/expires" => expires 95 | GET "/path" => path 96 | GET "/subpath" => path 97 | }; 98 | 99 | static ADDR: &'static str = "0.0.0.0:3000"; 100 | 101 | wrap_server!(server, ROUTER, ADDR); 102 | 103 | #[lunatic::test] 104 | fn cookie_response_accessor() { 105 | let _ = server::ensure_server(); 106 | 107 | let client = nightfly::Client::new(); 108 | 109 | let url = format!("http://{}/", ADDR); 110 | let res = client.get(&url).send().unwrap(); 111 | 112 | let cookies = res.cookies().collect::>(); 113 | 114 | // key=val 115 | assert_eq!(cookies[0].name(), "key"); 116 | assert_eq!(cookies[0].value(), "val"); 117 | 118 | // expires 119 | assert_eq!(cookies[1].name(), "expires"); 120 | assert_eq!( 121 | cookies[1].expires().unwrap(), 122 | std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1445412480) 123 | ); 124 | 125 | // path 126 | assert_eq!(cookies[2].name(), "path"); 127 | assert_eq!(cookies[2].path().unwrap(), "/the-path"); 128 | 129 | // max-age 130 | assert_eq!(cookies[3].name(), "maxage"); 131 | assert_eq!( 132 | cookies[3].max_age().unwrap(), 133 | std::time::Duration::from_secs(100) 134 | ); 135 | 136 | // domain 137 | assert_eq!(cookies[4].name(), "domain"); 138 | assert_eq!(cookies[4].domain().unwrap(), "mydomain"); 139 | 140 | // secure 141 | assert_eq!(cookies[5].name(), "secure"); 142 | assert_eq!(cookies[5].secure(), true); 143 | 144 | // httponly 145 | assert_eq!(cookies[6].name(), "httponly"); 146 | assert_eq!(cookies[6].http_only(), true); 147 | 148 | // samesitelax 149 | assert_eq!(cookies[7].name(), "samesitelax"); 150 | assert!(cookies[7].same_site_lax()); 151 | 152 | // samesitestrict 153 | assert_eq!(cookies[8].name(), "samesitestrict"); 154 | assert!(cookies[8].same_site_strict()); 155 | } 156 | 157 | #[lunatic::test] 158 | fn cookie_store_simple() { 159 | let _ = server::ensure_server(); 160 | 161 | let client = nightfly::Client::builder().build().unwrap(); 162 | 163 | let url = format!("http://{}/1", ADDR); 164 | client.get(&url).send().unwrap(); 165 | 166 | let url = format!("http://{}/2", ADDR); 167 | client.get(&url).send().unwrap(); 168 | } 169 | 170 | #[lunatic::test] 171 | fn cookie_store_overwrite_existing() { 172 | let _ = server::ensure_server(); 173 | 174 | let client = nightfly::Client::builder().build().unwrap(); 175 | 176 | let url = format!("http://{}/overwrite", ADDR); 177 | client.get(&url).send().unwrap(); 178 | 179 | let url = format!("http://{}/overwrite/2", ADDR); 180 | client.get(&url).send().unwrap(); 181 | 182 | let url = format!("http://{}/overwrite/3", ADDR); 183 | client.get(&url).send().unwrap(); 184 | } 185 | 186 | #[lunatic::test] 187 | fn cookie_store_max_age() { 188 | let _ = server::ensure_server(); 189 | 190 | let client = nightfly::Client::builder().build().unwrap(); 191 | let url = format!("http://{}/max-age", ADDR); 192 | client.get(&url).send().unwrap(); 193 | client.get(&url).send().unwrap(); 194 | } 195 | 196 | #[lunatic::test] 197 | fn cookie_store_expires() { 198 | let _ = server::ensure_server(); 199 | 200 | let client = nightfly::Client::builder().build().unwrap(); 201 | 202 | let url = format!("http://{}/expires", ADDR); 203 | client.get(&url).send().unwrap(); 204 | client.get(&url).send().unwrap(); 205 | } 206 | 207 | #[lunatic::test] 208 | fn cookie_store_path() { 209 | let _ = server::ensure_server(); 210 | 211 | let client = nightfly::Client::builder().build().unwrap(); 212 | 213 | let url = format!("http://{}/path", ADDR); 214 | client.get(&url).send().unwrap(); 215 | client.get(&url).send().unwrap(); 216 | 217 | let url = format!("http://{}/subpath", ADDR); 218 | client.get(&url).send().unwrap(); 219 | } 220 | -------------------------------------------------------------------------------- /tests/deflate.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 4 | use support::RouterFn; 5 | 6 | fn deflate(req: RequestContext) -> SubmsResponse { 7 | assert_eq!(req.method(), "HEAD"); 8 | 9 | SubmsResponse::builder() 10 | .header("content-encoding", "deflate") 11 | .header("content-length", 100) 12 | .body(Default::default()) 13 | .unwrap() 14 | } 15 | 16 | fn accept(req: RequestContext) -> SubmsResponse { 17 | assert_eq!(req.headers()["accept"], "application/json"); 18 | assert!(req.headers()["accept-encoding"] 19 | .to_str() 20 | .unwrap() 21 | .contains("deflate")); 22 | SubmsResponse::default() 23 | } 24 | 25 | fn accept_encoding(req: RequestContext) -> SubmsResponse { 26 | assert_eq!(req.headers()["accept"], "*/*"); 27 | assert_eq!(req.headers()["accept-encoding"], "identity"); 28 | SubmsResponse::default() 29 | } 30 | 31 | static ADDR: &'static str = "0.0.0.0:3001"; 32 | 33 | static ROUTER: RouterFn = router! { 34 | HEAD "/deflate" => deflate 35 | GET "/accept" => accept 36 | GET "/accept-encoding" => accept_encoding 37 | }; 38 | 39 | wrap_server!(deflate_server, ROUTER, ADDR); 40 | 41 | // ==================================== 42 | // Test cases 43 | // ==================================== 44 | 45 | #[lunatic::test] 46 | fn test_deflate_empty_body() { 47 | let _ = deflate_server::ensure_server(); 48 | 49 | let client = nightfly::Client::new(); 50 | let res = client 51 | .head(&format!("http://{}/deflate", ADDR)) 52 | .send() 53 | .unwrap(); 54 | 55 | let body = res.text().unwrap(); 56 | 57 | assert_eq!(body, ""); 58 | } 59 | 60 | #[lunatic::test] 61 | fn test_accept_header_is_not_changed_if_set() { 62 | let _ = deflate_server::ensure_server(); 63 | 64 | let client = nightfly::Client::new(); 65 | 66 | let res = client 67 | .get(&format!("http://{}/accept", ADDR)) 68 | .header( 69 | nightfly::header::ACCEPT, 70 | nightfly::header::HeaderValue::from_static("application/json"), 71 | ) 72 | .send() 73 | .unwrap(); 74 | 75 | assert_eq!(res.status(), nightfly::StatusCode::OK); 76 | } 77 | 78 | #[lunatic::test] 79 | fn test_accept_encoding_header_is_not_changed_if_set() { 80 | let _ = deflate_server::ensure_server(); 81 | 82 | let client = nightfly::Client::new(); 83 | 84 | let res = client 85 | .get(&format!("http://{}/accept-encoding", ADDR)) 86 | .header( 87 | nightfly::header::ACCEPT_ENCODING, 88 | nightfly::header::HeaderValue::from_static("identity"), 89 | ) 90 | .send() 91 | .unwrap(); 92 | 93 | assert_eq!(res.status(), nightfly::StatusCode::OK); 94 | } 95 | -------------------------------------------------------------------------------- /tests/gzip.rs: -------------------------------------------------------------------------------- 1 | mod support; 2 | 3 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 4 | use support::RouterFn; 5 | 6 | fn gzip(req: RequestContext) -> SubmsResponse { 7 | assert_eq!(req.method(), "HEAD"); 8 | 9 | SubmsResponse::builder() 10 | .header("content-encoding", "gzip") 11 | .header("content-length", 100) 12 | .body(Default::default()) 13 | .unwrap() 14 | } 15 | 16 | fn accept(req: RequestContext) -> SubmsResponse { 17 | assert_eq!(req.headers()["accept"], "application/json"); 18 | assert!(req.headers()["accept-encoding"] 19 | .to_str() 20 | .unwrap() 21 | .contains("gzip")); 22 | SubmsResponse::default() 23 | } 24 | 25 | fn accept_encoding(req: RequestContext) -> SubmsResponse { 26 | assert_eq!(req.headers()["accept"], "*/*"); 27 | assert_eq!(req.headers()["accept-encoding"], "identity"); 28 | SubmsResponse::default() 29 | } 30 | 31 | static ADDR: &'static str = "0.0.0.0:3001"; 32 | 33 | static ROUTER: RouterFn = router! { 34 | HEAD "/gzip" => gzip 35 | GET "/accept" => accept 36 | GET "/accept-encoding" => accept_encoding 37 | }; 38 | 39 | wrap_server!(gzip_server, ROUTER, ADDR); 40 | 41 | // ==================================== 42 | // Test cases 43 | // ==================================== 44 | 45 | #[lunatic::test] 46 | fn test_gzip_empty_body() { 47 | let _ = gzip_server::ensure_server(); 48 | 49 | let client = nightfly::Client::new(); 50 | let res = client 51 | .head(&format!("http://{}/gzip", ADDR)) 52 | .send() 53 | .unwrap(); 54 | 55 | let body = res.text().unwrap(); 56 | 57 | assert_eq!(body, ""); 58 | } 59 | 60 | #[lunatic::test] 61 | fn test_accept_header_is_not_changed_if_set() { 62 | let _ = gzip_server::ensure_server(); 63 | 64 | let client = nightfly::Client::new(); 65 | 66 | let res = client 67 | .get(&format!("http://{}/accept", ADDR)) 68 | .header( 69 | nightfly::header::ACCEPT, 70 | nightfly::header::HeaderValue::from_static("application/json"), 71 | ) 72 | .send() 73 | .unwrap(); 74 | 75 | assert_eq!(res.status(), nightfly::StatusCode::OK); 76 | } 77 | 78 | #[lunatic::test] 79 | fn test_accept_encoding_header_is_not_changed_if_set() { 80 | let _ = gzip_server::ensure_server(); 81 | 82 | let client = nightfly::Client::new(); 83 | 84 | let res = client 85 | .get(&format!("http://{}/accept-encoding", ADDR)) 86 | .header( 87 | nightfly::header::ACCEPT_ENCODING, 88 | nightfly::header::HeaderValue::from_static("identity"), 89 | ) 90 | .send() 91 | .unwrap(); 92 | 93 | assert_eq!(res.status(), nightfly::StatusCode::OK); 94 | } 95 | -------------------------------------------------------------------------------- /tests/multipart.rs: -------------------------------------------------------------------------------- 1 | // mod support; 2 | // use support::*; 3 | 4 | // #[lunatic::test] 5 | // fn text_part() { 6 | // let _ = env_logger::try_init(); 7 | 8 | // let form = nightfly::multipart::Form::new().text("foo", "bar"); 9 | 10 | // let expected_body = format!( 11 | // "\ 12 | // --{0}\r\n\ 13 | // Content-Disposition: form-data; name=\"foo\"\r\n\r\n\ 14 | // bar\r\n\ 15 | // --{0}--\r\n\ 16 | // ", 17 | // form.boundary() 18 | // ); 19 | 20 | // let ct = format!("multipart/form-data; boundary={}", form.boundary()); 21 | 22 | // let server = server::http(move |mut req| { 23 | // let ct = ct.clone(); 24 | // let expected_body = expected_body.clone(); 25 | // async move { 26 | // assert_eq!(req.method(), "POST"); 27 | // assert_eq!(req.headers()["content-type"], ct); 28 | // assert_eq!( 29 | // req.headers()["content-length"], 30 | // expected_body.len().to_string() 31 | // ); 32 | 33 | // let mut full: Vec = Vec::new(); 34 | // while let Some(item) = req.body_mut().next() { 35 | // full.extend(&*item.unwrap()); 36 | // } 37 | 38 | // assert_eq!(full, expected_body.as_bytes()); 39 | 40 | // http::Response::default() 41 | // } 42 | // }); 43 | 44 | // let url = format!("http://{}/multipart/1", server.addr()); 45 | 46 | // let res = nightfly::Client::new() 47 | // .post(&url) 48 | // .multipart(form) 49 | // .send() 50 | // .unwrap(); 51 | 52 | // assert_eq!(res.url().as_str(), &url); 53 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 54 | // } 55 | 56 | // #[cfg(feature = "blocking")] 57 | // #[test] 58 | // fn blocking_file_part() { 59 | // let _ = env_logger::try_init(); 60 | 61 | // let form = nightfly::blocking::multipart::Form::new() 62 | // .file("foo", "Cargo.lock") 63 | // .unwrap(); 64 | 65 | // let fcontents = std::fs::read_to_string("Cargo.lock").unwrap(); 66 | 67 | // let expected_body = format!( 68 | // "\ 69 | // --{0}\r\n\ 70 | // Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\ 71 | // Content-Type: application/octet-stream\r\n\r\n\ 72 | // {1}\r\n\ 73 | // --{0}--\r\n\ 74 | // ", 75 | // form.boundary(), 76 | // fcontents 77 | // ); 78 | 79 | // let ct = format!("multipart/form-data; boundary={}", form.boundary()); 80 | 81 | // let server = server::http(move |mut req| { 82 | // let ct = ct.clone(); 83 | // let expected_body = expected_body.clone(); 84 | // async move { 85 | // assert_eq!(req.method(), "POST"); 86 | // assert_eq!(req.headers()["content-type"], ct); 87 | // // files know their exact size 88 | // assert_eq!( 89 | // req.headers()["content-length"], 90 | // expected_body.len().to_string() 91 | // ); 92 | 93 | // let mut full: Vec = Vec::new(); 94 | // while let Some(item) = req.body_mut().next() { 95 | // full.extend(&*item.unwrap()); 96 | // } 97 | 98 | // assert_eq!(full, expected_body.as_bytes()); 99 | 100 | // http::Response::default() 101 | // } 102 | // }); 103 | 104 | // let url = format!("http://{}/multipart/2", server.addr()); 105 | 106 | // let res = nightfly::blocking::Client::new() 107 | // .post(&url) 108 | // .multipart(form) 109 | // .send() 110 | // .unwrap(); 111 | 112 | // assert_eq!(res.url().as_str(), &url); 113 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 114 | // } 115 | -------------------------------------------------------------------------------- /tests/proxy.rs: -------------------------------------------------------------------------------- 1 | // mod support; 2 | // use support::*; 3 | 4 | // use std::env; 5 | 6 | // use nightfly::Client; 7 | 8 | // use lunatic::{ 9 | // abstract_process, 10 | // net::ToSocketAddrs, 11 | // process::{ProcessRef, StartProcess}, 12 | // spawn_link, 13 | // supervisor::{Supervisor, SupervisorStrategy}, 14 | // Process, Tag, 15 | // }; 16 | // use submillisecond::{response::Response as SubmsResponse, router, Application, RequestContext}; 17 | 18 | // struct ServerSup; 19 | 20 | // struct ServerProcess(Process<()>); 21 | 22 | // #[abstract_process] 23 | // impl ServerProcess { 24 | // #[init] 25 | // fn init(_: ProcessRef, _: ()) -> Self { 26 | // Self(spawn_link!(|| { 27 | // start_server().unwrap(); 28 | // })) 29 | // } 30 | 31 | // #[terminate] 32 | // fn terminate(self) { 33 | // println!("Shutdown process"); 34 | // } 35 | 36 | // #[handle_link_trapped] 37 | // fn handle_link_trapped(&self, _: Tag) { 38 | // println!("Link trapped"); 39 | // } 40 | // } 41 | 42 | // impl Supervisor for ServerSup { 43 | // type Arg = String; 44 | // type Children = ServerProcess; 45 | 46 | // fn init(config: &mut lunatic::supervisor::SupervisorConfig, name: Self::Arg) { 47 | // // If a child fails, just restart it. 48 | // config.set_strategy(SupervisorStrategy::OneForOne); 49 | // // Start One `ServerProcess` 50 | // config.children_args(((), Some(name))); 51 | // } 52 | // } 53 | 54 | // fn max_age(req: RequestContext) -> SubmsResponse { 55 | // assert_eq!(req.headers().get("cookie"), None); 56 | // http::Response::builder() 57 | // .header("Set-Cookie", "key=val; Max-Age=0") 58 | // .body(Default::default()) 59 | // .unwrap() 60 | // } 61 | 62 | // fn text() -> SubmsResponse { 63 | // SubmsResponse::new("Hello".into()) 64 | // } 65 | 66 | // fn user_agent(req: RequestContext) -> SubmsResponse { 67 | // assert_eq!(req.headers()["user-agent"], "nightfly-test-agent"); 68 | // SubmsResponse::default() 69 | // } 70 | 71 | // fn auto_headers(req: RequestContext) -> SubmsResponse { 72 | // assert_eq!(req.method(), "GET"); 73 | 74 | // assert_eq!(req.headers()["accept"], "*/*"); 75 | // assert_eq!(req.headers().get("user-agent"), None); 76 | // if cfg!(feature = "gzip") { 77 | // assert!(req.headers()["accept-encoding"] 78 | // .to_str() 79 | // .unwrap() 80 | // .contains("gzip")); 81 | // } 82 | // if cfg!(feature = "brotli") { 83 | // assert!(req.headers()["accept-encoding"] 84 | // .to_str() 85 | // .unwrap() 86 | // .contains("br")); 87 | // } 88 | // if cfg!(feature = "deflate") { 89 | // assert!(req.headers()["accept-encoding"] 90 | // .to_str() 91 | // .unwrap() 92 | // .contains("deflate")); 93 | // } 94 | 95 | // http::Response::default() 96 | // } 97 | 98 | // fn get_handler(req: RequestContext) -> SubmsResponse { 99 | // SubmsResponse::new("pipe me".into()) 100 | // } 101 | 102 | // fn pipe_response(req: RequestContext) -> SubmsResponse { 103 | // assert_eq!(req.headers()["transfer-encoding"], "chunked"); 104 | 105 | // let body = req.body().as_slice(); 106 | // assert_eq!(body, b"pipe me".to_vec()); 107 | 108 | // SubmsResponse::default() 109 | // } 110 | 111 | // fn start_server() -> std::io::Result<()> { 112 | // Application::new(router! { 113 | // // GET "/" => cookie_response 114 | // GET "/text" => text 115 | // GET "/user-agent" => user_agent 116 | // GET "/auto_headers" => auto_headers 117 | // GET "/get" => get_handler 118 | // GET "/pipe" => pipe_response 119 | // }) 120 | // .serve(ADDR) 121 | // } 122 | 123 | // static ADDR: &'static str = "0.0.0.0:3000"; 124 | 125 | // fn ensure_server() { 126 | // if let Some(_) = Process::>::lookup("__server__") { 127 | // return; 128 | // } 129 | // ServerSup::start("__server__".to_owned(), None); 130 | // } 131 | 132 | // #[lunatic::test] 133 | // fn http_proxy() { 134 | // let url = "http://hyper.rs/prox"; 135 | // let server = server::http(move |req| { 136 | // assert_eq!(req.method(), "GET"); 137 | // assert_eq!(req.uri(), url); 138 | // assert_eq!(req.headers()["host"], "hyper.rs"); 139 | 140 | // async { http::Response::default() } 141 | // }); 142 | 143 | // let proxy = format!("http://{}", server.addr()); 144 | 145 | // let res = nightfly::Client::builder() 146 | // .proxy(nightfly::Proxy::http(&proxy).unwrap()) 147 | // .build() 148 | // .unwrap() 149 | // .get(url) 150 | // .send() 151 | // .unwrap(); 152 | 153 | // assert_eq!(res.url().as_str(), url); 154 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 155 | // } 156 | 157 | // #[lunatic::test] 158 | // fn http_proxy_basic_auth() { 159 | // let url = "http://hyper.rs/prox"; 160 | // let server = server::http(move |req| { 161 | // assert_eq!(req.method(), "GET"); 162 | // assert_eq!(req.uri(), url); 163 | // assert_eq!(req.headers()["host"], "hyper.rs"); 164 | // assert_eq!( 165 | // req.headers()["proxy-authorization"], 166 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 167 | // ); 168 | 169 | // async { http::Response::default() } 170 | // }); 171 | 172 | // let proxy = format!("http://{}", server.addr()); 173 | 174 | // let res = nightfly::Client::builder() 175 | // .proxy( 176 | // nightfly::Proxy::http(&proxy) 177 | // .unwrap() 178 | // .basic_auth("Aladdin", "open sesame"), 179 | // ) 180 | // .build() 181 | // .unwrap() 182 | // .get(url) 183 | // .send() 184 | // .unwrap(); 185 | 186 | // assert_eq!(res.url().as_str(), url); 187 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 188 | // } 189 | 190 | // #[lunatic::test] 191 | // fn http_proxy_basic_auth_parsed() { 192 | // let url = "http://hyper.rs/prox"; 193 | // let server = server::http(move |req| { 194 | // assert_eq!(req.method(), "GET"); 195 | // assert_eq!(req.uri(), url); 196 | // assert_eq!(req.headers()["host"], "hyper.rs"); 197 | // assert_eq!( 198 | // req.headers()["proxy-authorization"], 199 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 200 | // ); 201 | 202 | // async { http::Response::default() } 203 | // }); 204 | 205 | // let proxy = format!("http://Aladdin:open sesame@{}", server.addr()); 206 | 207 | // let res = nightfly::Client::builder() 208 | // .proxy(nightfly::Proxy::http(&proxy).unwrap()) 209 | // .build() 210 | // .unwrap() 211 | // .get(url) 212 | // .send() 213 | // .unwrap(); 214 | 215 | // assert_eq!(res.url().as_str(), url); 216 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 217 | // } 218 | 219 | // #[lunatic::test] 220 | // fn system_http_proxy_basic_auth_parsed() { 221 | // let url = "http://hyper.rs/prox"; 222 | // let server = server::http(move |req| { 223 | // assert_eq!(req.method(), "GET"); 224 | // assert_eq!(req.uri(), url); 225 | // assert_eq!(req.headers()["host"], "hyper.rs"); 226 | // assert_eq!( 227 | // req.headers()["proxy-authorization"], 228 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" 229 | // ); 230 | 231 | // async { http::Response::default() } 232 | // }); 233 | 234 | // // save system setting first. 235 | // let system_proxy = env::var("http_proxy"); 236 | 237 | // // set-up http proxy. 238 | // env::set_var( 239 | // "http_proxy", 240 | // format!("http://Aladdin:open sesame@{}", server.addr()), 241 | // ); 242 | 243 | // let res = nightfly::Client::builder() 244 | // .build() 245 | // .unwrap() 246 | // .get(url) 247 | // .send() 248 | // .unwrap(); 249 | 250 | // assert_eq!(res.url().as_str(), url); 251 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 252 | 253 | // // reset user setting. 254 | // match system_proxy { 255 | // Err(_) => env::remove_var("http_proxy"), 256 | // Ok(proxy) => env::set_var("http_proxy", proxy), 257 | // } 258 | // } 259 | 260 | // #[lunatic::test] 261 | // fn test_no_proxy() { 262 | // let server = server::http(move |req| { 263 | // assert_eq!(req.method(), "GET"); 264 | // assert_eq!(req.uri(), "/4"); 265 | 266 | // async { http::Response::default() } 267 | // }); 268 | // let proxy = format!("http://{}", server.addr()); 269 | // let url = format!("http://{}/4", server.addr()); 270 | 271 | // // set up proxy and use no_proxy to clear up client builder proxies. 272 | // let res = nightfly::Client::builder() 273 | // .proxy(nightfly::Proxy::http(&proxy).unwrap()) 274 | // .no_proxy() 275 | // .build() 276 | // .unwrap() 277 | // .get(&url) 278 | // .send() 279 | // .unwrap(); 280 | 281 | // assert_eq!(res.url().as_str(), &url); 282 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 283 | // } 284 | 285 | // #[cfg_attr(not(feature = "__internal_proxy_sys_no_cache"), ignore)] 286 | // #[lunatic::test] 287 | // fn test_using_system_proxy() { 288 | // let url = "http://not.a.real.sub.hyper.rs/prox"; 289 | // let server = server::http(move |req| { 290 | // assert_eq!(req.method(), "GET"); 291 | // assert_eq!(req.uri(), url); 292 | // assert_eq!(req.headers()["host"], "not.a.real.sub.hyper.rs"); 293 | 294 | // async { http::Response::default() } 295 | // }); 296 | 297 | // // Note: we're relying on the `__internal_proxy_sys_no_cache` feature to 298 | // // check the environment every time. 299 | 300 | // // save system setting first. 301 | // let system_proxy = env::var("http_proxy"); 302 | // // set-up http proxy. 303 | // env::set_var("http_proxy", format!("http://{}", server.addr())); 304 | 305 | // // system proxy is used by default 306 | // let res = nightfly::get(url).unwrap(); 307 | 308 | // assert_eq!(res.url().as_str(), url); 309 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 310 | 311 | // // reset user setting. 312 | // match system_proxy { 313 | // Err(_) => env::remove_var("http_proxy"), 314 | // Ok(proxy) => env::set_var("http_proxy", proxy), 315 | // } 316 | // } 317 | 318 | // #[lunatic::test] 319 | // fn http_over_http() { 320 | // let url = "http://hyper.rs/prox"; 321 | 322 | // let server = server::http(move |req| { 323 | // assert_eq!(req.method(), "GET"); 324 | // assert_eq!(req.uri(), url); 325 | // assert_eq!(req.headers()["host"], "hyper.rs"); 326 | 327 | // async { http::Response::default() } 328 | // }); 329 | 330 | // let proxy = format!("http://{}", server.addr()); 331 | 332 | // let res = nightfly::Client::builder() 333 | // .proxy(nightfly::Proxy::http(&proxy).unwrap()) 334 | // .build() 335 | // .unwrap() 336 | // .get(url) 337 | // .send() 338 | // .unwrap(); 339 | 340 | // assert_eq!(res.url().as_str(), url); 341 | // assert_eq!(res.status(), nightfly::StatusCode::OK); 342 | // } 343 | -------------------------------------------------------------------------------- /tests/redirect.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod support; 3 | 4 | use submillisecond::{response::Response as SubmsResponse, router, RequestContext}; 5 | use support::RouterFn; 6 | 7 | fn redirect(code: u16) -> SubmsResponse { 8 | http::Response::builder() 9 | .status(code) 10 | .header("location", "/dst") 11 | .header("server", "test-redirect") 12 | .body(Default::default()) 13 | .unwrap() 14 | } 15 | 16 | fn handle_301() -> SubmsResponse { 17 | redirect(301) 18 | } 19 | 20 | fn handle_302() -> SubmsResponse { 21 | redirect(302) 22 | } 23 | 24 | fn handle_303() -> SubmsResponse { 25 | redirect(303) 26 | } 27 | 28 | fn handle_307() -> SubmsResponse { 29 | println!("HANDLING 307"); 30 | redirect(307) 31 | } 32 | 33 | fn handle_308() -> SubmsResponse { 34 | redirect(308) 35 | } 36 | 37 | fn dst(body: Vec) -> SubmsResponse { 38 | SubmsResponse::builder() 39 | .header("server", "test-dst") 40 | .body(body) 41 | .unwrap() 42 | } 43 | 44 | fn dst_get() -> SubmsResponse { 45 | dst("GET".into()) 46 | } 47 | 48 | fn dst_post() -> SubmsResponse { 49 | dst("POST".into()) 50 | } 51 | 52 | fn end_server(req: RequestContext) -> SubmsResponse { 53 | lunatic_log::info!("END SERVER {:?}", req.headers()); 54 | assert_eq!(req.headers().get("cookie"), None); 55 | 56 | assert_eq!( 57 | req.headers()["referer"], 58 | format!("http://{}/sensitive", ADDR) 59 | ); 60 | http::Response::default() 61 | } 62 | 63 | fn mid_server(req: RequestContext) -> SubmsResponse { 64 | assert_eq!(req.headers()["cookie"], "foo=bar"); 65 | http::Response::builder() 66 | .status(302) 67 | .header("location", format!("http://{}/end", END_ADDR)) 68 | .body(Default::default()) 69 | .unwrap() 70 | } 71 | 72 | fn loop_handler(req: RequestContext) -> SubmsResponse { 73 | assert_eq!(req.uri(), "/loop"); 74 | http::Response::builder() 75 | .status(302) 76 | .header("location", "/loop") 77 | .body(Default::default()) 78 | .unwrap() 79 | } 80 | 81 | fn no_redirect() -> SubmsResponse { 82 | http::Response::builder() 83 | .status(302) 84 | .header("location", "/dont") 85 | .body(Default::default()) 86 | .unwrap() 87 | } 88 | 89 | fn no_referer() -> SubmsResponse { 90 | SubmsResponse::builder() 91 | .status(302) 92 | .header("location", "/dst-no-refer") 93 | .body(Default::default()) 94 | .unwrap() 95 | } 96 | 97 | fn dst_no_referer(req: RequestContext) -> SubmsResponse { 98 | assert_eq!(req.uri(), "/dst"); 99 | assert_eq!(req.headers().get("referer"), None); 100 | 101 | SubmsResponse::default() 102 | } 103 | 104 | fn yikes() -> SubmsResponse { 105 | http::Response::builder() 106 | .status(302) 107 | .header("location", "http://www.yikes{KABOOM}") 108 | .body(Default::default()) 109 | .unwrap() 110 | } 111 | 112 | fn handle_302_cookie() -> SubmsResponse { 113 | http::Response::builder() 114 | .status(302) 115 | .header("location", "/dst") 116 | .header("set-cookie", "key=value") 117 | .body(Default::default()) 118 | .unwrap() 119 | } 120 | 121 | fn dst_cookie(req: RequestContext) -> SubmsResponse { 122 | assert_eq!(req.headers()["cookie"], "key=value"); 123 | http::Response::default() 124 | } 125 | 126 | static ROUTER: RouterFn = router! { 127 | POST "/301" => handle_301 128 | POST "/302" => handle_302 129 | POST "/303" => handle_303 130 | POST "/307" => handle_307 131 | POST "/308" => handle_308 132 | GET "/307" => handle_307 133 | GET "/308" => handle_308 134 | GET "/dst" => dst_get 135 | POST "/dst" => dst_post 136 | GET "/sensitive" => mid_server 137 | GET "/loop" => loop_handler 138 | GET "/no-redirect" => no_redirect 139 | GET "/no-refer" => no_referer 140 | GET "/dst-no-refer" => dst_no_referer 141 | GET "/yikes" => yikes 142 | GET "/dst-cookie" => dst_cookie 143 | GET "/302-cookie" => handle_302_cookie 144 | }; 145 | 146 | static END_ROUTER: RouterFn = router! { 147 | GET "/end" => end_server 148 | }; 149 | 150 | static ADDR: &'static str = "0.0.0.0:3000"; 151 | static END_ADDR: &'static str = "0.0.0.0:3005"; 152 | 153 | wrap_server!(server, ROUTER, ADDR); 154 | wrap_server!(end_server, END_ROUTER, END_ADDR); 155 | 156 | #[lunatic::test] 157 | fn test_redirect_301_and_302_and_303_changes_post_to_get() { 158 | let _ = server::ensure_server(); 159 | let client = nightfly::Client::new(); 160 | let codes = [301u16, 302, 303]; 161 | 162 | for &code in codes.iter() { 163 | let url = format!("http://{}/{}", ADDR, code); 164 | let dst = format!("http://{}/{}", ADDR, "dst"); 165 | let res = client.post(&url).send().unwrap(); 166 | println!("RES code {} -> {:?}", code, res); 167 | assert_eq!(res.url().as_str(), dst); 168 | assert_eq!(res.status(), nightfly::StatusCode::OK); 169 | assert_eq!( 170 | res.headers().get(nightfly::header::SERVER).unwrap(), 171 | &"test-dst" 172 | ); 173 | assert_eq!(res.body, b"GET".to_vec()); 174 | } 175 | } 176 | 177 | #[lunatic::test] 178 | fn test_redirect_307_and_308_tries_to_get_again() { 179 | let _ = server::ensure_server(); 180 | 181 | let client = nightfly::Client::new(); 182 | let codes = [307u16, 308]; 183 | for &code in codes.iter() { 184 | let url = format!("http://{}/{}", ADDR, code); 185 | let dst = format!("http://{}/{}", ADDR, "dst"); 186 | let res = client.get(&url).send().unwrap(); 187 | assert_eq!(res.url().as_str(), dst); 188 | assert_eq!(res.status(), nightfly::StatusCode::OK); 189 | assert_eq!( 190 | res.headers().get(nightfly::header::SERVER).unwrap(), 191 | &"test-dst" 192 | ); 193 | assert_eq!(res.body, b"GET".to_vec()); 194 | } 195 | } 196 | 197 | #[lunatic::test] 198 | fn test_redirect_307_and_308_tries_to_post_again() { 199 | let _ = server::ensure_server(); 200 | 201 | let client = nightfly::Client::new(); 202 | let codes = [307u16, 308]; 203 | for &code in codes.iter() { 204 | let url = format!("http://{}/{}", ADDR, code); 205 | let dst = format!("http://{}/{}", ADDR, "dst"); 206 | let res = client.post(&url).body("Hello").send().unwrap(); 207 | assert_eq!(res.url().as_str(), dst); 208 | assert_eq!(res.status(), nightfly::StatusCode::OK); 209 | assert_eq!( 210 | res.headers().get(nightfly::header::SERVER).unwrap(), 211 | &"test-dst" 212 | ); 213 | assert_eq!(res.body, b"POST".to_vec()); 214 | } 215 | } 216 | 217 | #[lunatic::test] 218 | fn test_redirect_removes_sensitive_headers() { 219 | let _ = server::ensure_server(); 220 | let _ = end_server::ensure_server(); 221 | let res = nightfly::Client::builder() 222 | .build() 223 | .unwrap() 224 | .get(&format!("http://{}/sensitive", ADDR)) 225 | .header( 226 | nightfly::header::COOKIE, 227 | nightfly::header::HeaderValue::from_static("foo=bar"), 228 | ) 229 | .send() 230 | .unwrap(); 231 | println!("SENSITIVE {:?}", res); 232 | assert_eq!(res.status, 200); 233 | } 234 | 235 | #[lunatic::test] 236 | fn test_redirect_policy_can_return_errors() { 237 | let _ = server::ensure_server(); 238 | 239 | let url = format!("http://{}/loop", ADDR); 240 | let err = nightfly::get(&url).unwrap_err(); 241 | assert!(err.is_redirect()); 242 | } 243 | 244 | #[lunatic::test] 245 | fn test_redirect_policy_can_stop_redirects_without_an_error() { 246 | let _ = server::ensure_server(); 247 | 248 | let url = format!("http://{}/no-redirect", ADDR); 249 | 250 | let res = nightfly::Client::builder() 251 | .redirect(nightfly::redirect::Policy::none()) 252 | .build() 253 | .unwrap() 254 | .get(&url) 255 | .send(); 256 | 257 | let res = res.unwrap(); 258 | assert_eq!(res.url().as_str(), url); 259 | assert_eq!(res.status(), nightfly::StatusCode::FOUND); 260 | } 261 | 262 | #[lunatic::test] 263 | fn test_referer_is_not_set_if_disabled() { 264 | let _ = server::ensure_server(); 265 | 266 | nightfly::Client::builder() 267 | .referer(false) 268 | .build() 269 | .unwrap() 270 | .get(&format!("http://{}/no-refer", ADDR)) 271 | .send() 272 | .unwrap(); 273 | } 274 | 275 | #[lunatic::test] 276 | fn test_invalid_location_stops_redirect_gh484() { 277 | let _ = server::ensure_server(); 278 | 279 | let url = format!("http://{}/yikes", ADDR); 280 | 281 | let res = nightfly::get(&url).unwrap(); 282 | 283 | assert_eq!(res.url().as_str(), url); 284 | assert_eq!(res.status(), nightfly::StatusCode::FOUND); 285 | } 286 | 287 | #[cfg(feature = "cookies")] 288 | #[lunatic::test] 289 | fn test_redirect_302_with_set_cookies() { 290 | let _ = server::ensure_server(); 291 | 292 | let url = format!("http://{}/302-cookie", ADDR); 293 | let dst = format!("http://{}/{}", ADDR, "dst"); 294 | 295 | let client = nightfly::ClientBuilder::new().build().unwrap(); 296 | let res = client.get(&url).send().unwrap(); 297 | 298 | assert_eq!(res.url().as_str(), dst); 299 | assert_eq!(res.status(), nightfly::StatusCode::OK); 300 | } 301 | 302 | // #[cfg(feature = "__rustls")] 303 | // #[lunatic::test] 304 | // #[ignore = "Needs TLS support in the test server"] 305 | // fn test_redirect_https_only_enforced_gh1312() { 306 | // let server = server::http(move |_req| async move { 307 | // http::Response::builder() 308 | // .status(302) 309 | // .header("location", "http://insecure") 310 | // .body(Default::default()) 311 | // .unwrap() 312 | // }); 313 | 314 | // let url = format!("https://{}/yikes", ADDR); 315 | 316 | // let res = nightfly::Client::builder() 317 | // .danger_accept_invalid_certs(true) 318 | // .use_rustls_tls() 319 | // .https_only(true) 320 | // .build() 321 | // .expect("client builder") 322 | // .get(&url) 323 | // .send(); 324 | 325 | // let err = res.unwrap_err(); 326 | // assert!(err.is_redirect()); 327 | // } 328 | -------------------------------------------------------------------------------- /tests/support/mod.rs: -------------------------------------------------------------------------------- 1 | use lunatic::Tag; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // TODO: remove once done converting to new support server? 5 | #[allow(unused)] 6 | pub static DEFAULT_USER_AGENT: &str = 7 | concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 8 | 9 | // fn ensure_server(server: Fn(req: RequestContext) -> submillisecond::response::Response) { 10 | // if let Some(_) = Process::>::lookup("__server__") { 11 | // return; 12 | // } 13 | // ServerSup::start("__server__".to_owned(), None); 14 | // } 15 | 16 | #[derive(Serialize, Deserialize, Clone, Debug)] 17 | pub struct DummyError; 18 | pub struct DummyProcess; 19 | 20 | #[lunatic::abstract_process] 21 | impl DummyProcess { 22 | #[init] 23 | fn init(_: lunatic::ap::Config, _: ()) -> Result { 24 | Ok(Self) 25 | } 26 | 27 | #[terminate] 28 | fn terminate(self) { 29 | println!("Shutdown process"); 30 | } 31 | 32 | #[handle_link_death] 33 | fn handle_link_trapped(&self, _: Tag) { 34 | println!("Link trapped"); 35 | } 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! wrap_server { 40 | ($name:ident, $router:ident, $addr:ident) => { 41 | mod $name { 42 | 43 | use lunatic::{ 44 | abstract_process, spawn_link, 45 | supervisor::{Supervisor, SupervisorStrategy}, 46 | AbstractProcess, Process, Tag, 47 | }; 48 | use submillisecond::Application; 49 | 50 | struct ServerProcess(Process<()>); 51 | struct ServerSup; 52 | 53 | #[abstract_process] 54 | impl ServerProcess { 55 | #[init] 56 | fn init( 57 | _: lunatic::ap::Config, 58 | _: (), 59 | ) -> Result { 60 | Ok(Self(spawn_link!(|| { 61 | Application::new(super::$router) 62 | .serve(super::$addr) 63 | .unwrap(); 64 | }))) 65 | } 66 | 67 | #[terminate] 68 | fn terminate(self) { 69 | println!("Shutdown process"); 70 | } 71 | 72 | #[handle_link_death] 73 | fn handle_link_trapped(&self, _: Tag) { 74 | println!("Link trapped"); 75 | } 76 | } 77 | 78 | impl Supervisor for ServerSup { 79 | type Arg = String; 80 | type Children = (ServerProcess, crate::support::DummyProcess); 81 | 82 | fn init(config: &mut lunatic::supervisor::SupervisorConfig, name: Self::Arg) { 83 | // If a child fails, just restart it. 84 | config.set_strategy(SupervisorStrategy::OneForOne); 85 | // Start One `ServerProcess` 86 | config.children_args((((), None), ((), None))); 87 | } 88 | } 89 | 90 | pub fn ensure_server() { 91 | let name = format!("__{}__", stringify!($name)); 92 | if let Some(_) = Process::>::lookup(&name) { 93 | return; 94 | } 95 | ServerSup::start(name.to_owned()).expect("should have started server"); 96 | } 97 | } 98 | }; 99 | } 100 | 101 | pub type RouterFn = 102 | fn() -> fn(req: ::submillisecond::RequestContext) -> ::submillisecond::response::Response; 103 | -------------------------------------------------------------------------------- /tests/timeouts.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod support; 3 | 4 | use std::time::Duration; 5 | 6 | use submillisecond::{response::Response as SubmsResponse, router}; 7 | use support::RouterFn; 8 | 9 | fn slow() -> SubmsResponse { 10 | // delay returning the response 11 | lunatic::sleep(Duration::from_secs(2)); 12 | println!("AFTER 2 seconds..."); 13 | SubmsResponse::default() 14 | } 15 | 16 | static ROUTER: RouterFn = router! { 17 | GET "/slow" => slow 18 | }; 19 | 20 | static ADDR: &'static str = "0.0.0.0:3008"; 21 | 22 | wrap_server!(server, ROUTER, ADDR); 23 | 24 | #[lunatic::test] 25 | fn client_timeout() { 26 | let _ = server::ensure_server(); 27 | 28 | let client = nightfly::Client::builder() 29 | .timeout(Duration::from_millis(500)) 30 | .build() 31 | .unwrap(); 32 | 33 | let url = format!("http://{}/slow", ADDR); 34 | 35 | let res = client.get(&url).send(); 36 | 37 | println!("GOT RES {res:?}"); 38 | let err = res.unwrap_err(); 39 | 40 | assert!(err.is_timeout()); 41 | assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str())); 42 | } 43 | 44 | #[lunatic::test] 45 | fn request_timeout() { 46 | let _ = server::ensure_server(); 47 | 48 | let client = nightfly::Client::builder().build().unwrap(); 49 | 50 | let url = format!("http://{}/slow", ADDR); 51 | 52 | let res = client.get(&url).timeout(Duration::from_millis(500)).send(); 53 | 54 | let err = res.unwrap_err(); 55 | 56 | assert!(err.is_timeout()); 57 | assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str())); 58 | } 59 | 60 | // #[lunatic::test] 61 | // fn connect_timeout() { 62 | // let client = nightfly::Client::builder() 63 | // .connect_timeout(Duration::from_millis(100)) 64 | // .build() 65 | // .unwrap(); 66 | 67 | // let url = "http://10.255.255.1:81/slow"; 68 | 69 | // let res = client.get(url).timeout(Duration::from_millis(1000)).send(); 70 | 71 | // let err = res.unwrap_err(); 72 | 73 | // assert!(err.is_timeout()); 74 | // } 75 | 76 | // #[lunatic::test] 77 | // fn response_timeout() { 78 | // let _ = server::ensure_server(); 79 | 80 | // let server = server::http(move |_req| { 81 | // async { 82 | // // immediate response, but delayed body 83 | // lunatic::sleep(Duration::from_secs(2)); 84 | // let body = Ok::<_, std::convert::Infallible>("Hello"); 85 | 86 | // http::Response::new(body) 87 | // } 88 | // }); 89 | 90 | // let client = nightfly::Client::builder() 91 | // .timeout(Duration::from_millis(500)) 92 | // .no_proxy() 93 | // .build() 94 | // .unwrap(); 95 | 96 | // let url = format!("http://{}/slow", ADDR); 97 | // let res = client.get(&url).send().expect("Failed to get"); 98 | // let body = res.text(); 99 | 100 | // let err = body.unwrap_err(); 101 | 102 | // assert!(err.is_timeout()); 103 | // } 104 | 105 | // /// Tests that internal client future cancels when the oneshot channel 106 | // /// is canceled. 107 | // #[test] 108 | // fn timeout_closes_connection() { 109 | // let _ = env_logger::try_init(); 110 | 111 | // // Make Client drop *after* the Server, so the background doesn't 112 | // // close too early. 113 | // let client = nightfly::blocking::Client::builder() 114 | // .timeout(Duration::from_millis(500)) 115 | // .build() 116 | // .unwrap(); 117 | 118 | // let server = server::http(move |_req| { 119 | // async { 120 | // // delay returning the response 121 | // lunatic::time::sleep(Duration::from_secs(2)); 122 | // http::Response::default() 123 | // } 124 | // }); 125 | 126 | // let url = format!("http://{}/closes", ADDR); 127 | // let err = client.get(&url).send().unwrap_err(); 128 | 129 | // assert!(err.is_timeout()); 130 | // assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str())); 131 | // } 132 | 133 | #[cfg(feature = "blocking")] 134 | #[test] 135 | fn timeout_blocking_request() { 136 | let _ = env_logger::try_init(); 137 | 138 | // Make Client drop *after* the Server, so the background doesn't 139 | // close too early. 140 | let client = nightfly::blocking::Client::builder().build().unwrap(); 141 | 142 | let server = server::http(move |_req| { 143 | async { 144 | // delay returning the response 145 | lunatic::time::sleep(Duration::from_secs(2)); 146 | http::Response::default() 147 | } 148 | }); 149 | 150 | let url = format!("http://{}/closes", ADDR); 151 | let err = client 152 | .get(&url) 153 | .timeout(Duration::from_millis(500)) 154 | .send() 155 | .unwrap_err(); 156 | 157 | assert!(err.is_timeout()); 158 | assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str())); 159 | } 160 | 161 | #[cfg(feature = "blocking")] 162 | #[test] 163 | fn blocking_request_timeout_body() { 164 | let _ = env_logger::try_init(); 165 | 166 | let client = nightfly::blocking::Client::builder() 167 | // this should be overridden 168 | .connect_timeout(Duration::from_millis(200)) 169 | // this should be overridden 170 | .timeout(Duration::from_millis(200)) 171 | .build() 172 | .unwrap(); 173 | 174 | let server = server::http(move |_req| { 175 | async { 176 | // immediate response, but delayed body 177 | let body = hyper::Body::wrap_stream(futures_util::stream::once(async { 178 | lunatic::time::sleep(Duration::from_secs(1)); 179 | Ok::<_, std::convert::Infallible>("Hello") 180 | })); 181 | 182 | http::Response::new(body) 183 | } 184 | }); 185 | 186 | let url = format!("http://{}/closes", ADDR); 187 | let res = client 188 | .get(&url) 189 | // longer than client timeout 190 | .timeout(Duration::from_secs(5)) 191 | .send() 192 | .expect("get response"); 193 | 194 | let text = res.text().unwrap(); 195 | assert_eq!(text, "Hello"); 196 | } 197 | 198 | #[cfg(feature = "blocking")] 199 | #[test] 200 | fn write_timeout_large_body() { 201 | let _ = env_logger::try_init(); 202 | let body = vec![b'x'; 20_000]; 203 | let len = 8192; 204 | 205 | // Make Client drop *after* the Server, so the background doesn't 206 | // close too early. 207 | let client = nightfly::blocking::Client::builder() 208 | .timeout(Duration::from_millis(500)) 209 | .build() 210 | .unwrap(); 211 | 212 | let server = server::http(move |_req| { 213 | async { 214 | // delay returning the response 215 | lunatic::time::sleep(Duration::from_secs(2)); 216 | http::Response::default() 217 | } 218 | }); 219 | 220 | let cursor = std::io::Cursor::new(body); 221 | let url = format!("http://{}/write-timeout", ADDR); 222 | let err = client 223 | .post(&url) 224 | .body(nightfly::blocking::Body::sized(cursor, len as u64)) 225 | .send() 226 | .unwrap_err(); 227 | 228 | assert!(err.is_timeout()); 229 | assert_eq!(err.url().map(|u| u.as_str()), Some(url.as_str())); 230 | } 231 | -------------------------------------------------------------------------------- /tests/upgrade.rs: -------------------------------------------------------------------------------- 1 | // mod support; 2 | // use support::*; 3 | 4 | // #[lunatic::test] 5 | // fn http_upgrade() { 6 | // let server = server::http(move |req| { 7 | // assert_eq!(req.method(), "GET"); 8 | // assert_eq!(req.headers()["connection"], "upgrade"); 9 | // assert_eq!(req.headers()["upgrade"], "foobar"); 10 | 11 | // lunatic::spawn(async move { 12 | // let mut upgraded = hyper::upgrade::on(req).unwrap(); 13 | 14 | // let mut buf = vec![0; 7]; 15 | // upgraded.read_exact(&mut buf).unwrap(); 16 | // assert_eq!(buf, b"foo=bar"); 17 | 18 | // upgraded.write_all(b"bar=foo").unwrap(); 19 | // }); 20 | 21 | // async { 22 | // http::Response::builder() 23 | // .status(http::StatusCode::SWITCHING_PROTOCOLS) 24 | // .header(http::header::CONNECTION, "upgrade") 25 | // .header(http::header::UPGRADE, "foobar") 26 | // .body(hyper::Body::empty()) 27 | // .unwrap() 28 | // } 29 | // }); 30 | 31 | // let res = nightfly::Client::builder() 32 | // .build() 33 | // .unwrap() 34 | // .get(format!("http://{}", server.addr())) 35 | // .header(http::header::CONNECTION, "upgrade") 36 | // .header(http::header::UPGRADE, "foobar") 37 | // .send() 38 | // .unwrap(); 39 | 40 | // assert_eq!(res.status(), http::StatusCode::SWITCHING_PROTOCOLS); 41 | // let mut upgraded = res.upgrade().unwrap(); 42 | 43 | // upgraded.write_all(b"foo=bar").unwrap(); 44 | 45 | // let mut buf = vec![]; 46 | // upgraded.read_to_end(&mut buf).unwrap(); 47 | // assert_eq!(buf, b"bar=foo"); 48 | // } 49 | --------------------------------------------------------------------------------