├── .github └── workflows │ ├── license.yml │ ├── publish.yml │ └── rust.yml ├── .gitignore ├── .licenserc.yaml ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── async_client_bench.rs └── common.rs ├── src ├── client.rs ├── conn.rs ├── error.rs ├── lib.rs ├── meta.rs ├── params.rs ├── request.rs └── response.rs └── tests ├── client_get.rs ├── client_multi.rs ├── client_post.rs ├── common.rs └── php ├── big-response.php ├── body-size.php ├── index.php └── post.php /.github/workflows/license.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: License 16 | 17 | on: 18 | push: 19 | branches: [ master, develop ] 20 | pull_request: 21 | branches: [ "**" ] 22 | 23 | jobs: 24 | license: 25 | runs-on: ubuntu-20.04 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | submodules: 'recursive' 30 | - name: Check License Header 31 | uses: apache/skywalking-eyes/header/@501a28d2fb4a9b962661987e50cf0219631b32ff 32 | with: 33 | config: .licenserc.yaml 34 | - name: Check Dependencies License 35 | uses: apache/skywalking-eyes/dependency/@501a28d2fb4a9b962661987e50cf0219631b32ff 36 | with: 37 | config: .licenserc.yaml 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Publish 16 | 17 | on: 18 | push: 19 | tags: [ "**" ] 20 | 21 | env: 22 | RUST_LOG: debug 23 | CARGO_TERM_COLOR: always 24 | RUST_BACKTRACE: "1" 25 | RUSTFLAGS: "-D warnings" 26 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 27 | 28 | jobs: 29 | publish: 30 | name: Publish 31 | 32 | runs-on: ubuntu-20.04 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | with: 37 | submodules: 'recursive' 38 | 39 | - name: Install Rust Stable 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | toolchain: stable 43 | override: true 44 | 45 | - name: Cargo publish 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: publish 49 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Rust 16 | 17 | on: 18 | push: 19 | branches: [ master, develop ] 20 | pull_request: 21 | branches: [ "**" ] 22 | 23 | env: 24 | CARGO_TERM_COLOR: always 25 | RUST_BACKTRACE: 1 26 | RUSTFLAGS: "-D warnings" 27 | 28 | jobs: 29 | rust: 30 | runs-on: ubuntu-20.04 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | with: 35 | submodules: 'recursive' 36 | - name: Update rust 37 | run: rustup update 38 | - name: Setup nightly 39 | run: rustup toolchain install nightly --component rustfmt --allow-downgrade 40 | - name: Run php-fpm 41 | run: docker run -d --name php-fpm -v $PWD:$PWD -p 9000:9000 php:7.1.30-fpm -c /usr/local/etc/php/php.ini-development 42 | - name: Fmt 43 | run: cargo +nightly fmt --all -- --check 44 | - name: Check 45 | run: cargo check --release 46 | - name: Clippy 47 | run: cargo clippy --release 48 | - name: Test 49 | run: cargo test --release 50 | - name: Doc 51 | run: cargo rustdoc --release 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | header: 16 | license: 17 | spdx-id: Apache-2.0 18 | copyright-owner: jmjoy 19 | 20 | paths-ignore: 21 | - 'LICENSE' 22 | - 'NOTICE' 23 | - '**/*.md' 24 | - '**/*.lock' 25 | - '**/.gitignore' 26 | - '**/.gitmodules' 27 | - 'vendor' 28 | - '.cargo' 29 | - '.vscode' 30 | - '.idea' 31 | 32 | comment: on-failure 33 | 34 | dependency: 35 | files: 36 | - Cargo.toml 37 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | fn_params_layout = "Compressed" 16 | format_code_in_doc_comments = true 17 | format_macro_bodies = true 18 | format_macro_matchers = true 19 | format_strings = true 20 | imports_granularity = "Crate" 21 | merge_derives = true 22 | newline_style = "Unix" 23 | normalize_comments = true 24 | reorder_impl_items = true 25 | use_field_init_shorthand = true 26 | wrap_comments = true 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 jmjoy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "fastcgi-client" 17 | version = "0.9.0" 18 | authors = ["jmjoy "] 19 | edition = "2021" 20 | description = "Fastcgi client implemented for Rust." 21 | repository = "https://github.com/jmjoy/fastcgi-client-rs" 22 | license = "Apache-2.0" 23 | readme = "README.md" 24 | keywords = ["fastcgi", "fcgi", "client", "tokio", "php"] 25 | 26 | [dependencies] 27 | thiserror = "1.0.32" 28 | tokio = { version = "1.20.1", features = ["io-util", "sync", "time"] } 29 | tracing = "0.1.36" 30 | 31 | [dev-dependencies] 32 | tokio = { version = "1.20.1", features = ["full"] } 33 | tracing-subscriber = "0.3.15" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastcgi-client-rs 2 | 3 | [![Rust](https://github.com/jmjoy/fastcgi-client-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/jmjoy/fastcgi-client-rs/actions/workflows/rust.yml) 4 | [![Crate](https://img.shields.io/crates/v/fastcgi-client.svg)](https://crates.io/crates/fastcgi-client) 5 | [![API](https://docs.rs/fastcgi-client/badge.svg)](https://docs.rs/fastcgi-client) 6 | 7 | Fastcgi client implemented for Rust, power by [tokio](https://crates.io/crates/tokio). 8 | 9 | ## Installation 10 | 11 | Add dependencies to your `Cargo.toml` by `cargo add`: 12 | 13 | ```shell 14 | cargo add tokio --features full 15 | cargo add fastcgi-client 16 | ``` 17 | 18 | ## Examples 19 | 20 | Short connection mode: 21 | 22 | ```rust, no_run 23 | use fastcgi_client::{Client, Params, Request}; 24 | use std::env; 25 | use tokio::{io, net::TcpStream}; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | let script_filename = env::current_dir() 30 | .unwrap() 31 | .join("tests") 32 | .join("php") 33 | .join("index.php"); 34 | let script_filename = script_filename.to_str().unwrap(); 35 | let script_name = "/index.php"; 36 | 37 | // Connect to php-fpm default listening address. 38 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 39 | let mut client = Client::new(stream); 40 | 41 | // Fastcgi params, please reference to nginx-php-fpm config. 42 | let params = Params::default() 43 | .request_method("GET") 44 | .script_name(script_name) 45 | .script_filename(script_filename) 46 | .request_uri(script_name) 47 | .document_uri(script_name) 48 | .remote_addr("127.0.0.1") 49 | .remote_port(12345) 50 | .server_addr("127.0.0.1") 51 | .server_port(80) 52 | .server_name("jmjoy-pc") 53 | .content_type("") 54 | .content_length(0); 55 | 56 | // Fetch fastcgi server(php-fpm) response. 57 | let output = client.execute_once(Request::new(params, &mut io::empty())).await.unwrap(); 58 | 59 | // "Content-type: text/html; charset=UTF-8\r\n\r\nhello" 60 | let stdout = String::from_utf8(output.stdout.unwrap()).unwrap(); 61 | 62 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 63 | assert!(stdout.contains("hello")); 64 | assert_eq!(output.stderr, None); 65 | } 66 | ``` 67 | 68 | Keep alive mode: 69 | 70 | ```rust, no_run 71 | use fastcgi_client::{Client, Params, Request}; 72 | use std::env; 73 | use tokio::{io, net::TcpStream}; 74 | 75 | #[tokio::main] 76 | async fn main() { 77 | // Connect to php-fpm default listening address. 78 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 79 | let mut client = Client::new_keep_alive(stream); 80 | 81 | // Fastcgi params, please reference to nginx-php-fpm config. 82 | let params = Params::default(); 83 | 84 | for _ in (0..3) { 85 | // Fetch fastcgi server(php-fpm) response. 86 | let output = client.execute(Request::new(params.clone(), &mut io::empty())).await.unwrap(); 87 | 88 | // "Content-type: text/html; charset=UTF-8\r\n\r\nhello" 89 | let stdout = String::from_utf8(output.stdout.unwrap()).unwrap(); 90 | 91 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 92 | assert!(stdout.contains("hello")); 93 | assert_eq!(output.stderr, None); 94 | } 95 | } 96 | ``` 97 | 98 | ## License 99 | 100 | [Apache-2.0](https://github.com/jmjoy/fastcgi-client-rs/blob/master/LICENSE). 101 | -------------------------------------------------------------------------------- /benches/async_client_bench.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![feature(test)] 16 | 17 | extern crate test; 18 | 19 | use fastcgi_client::{conn::KeepAlive, request::Request, Client, Params}; 20 | use std::env::current_dir; 21 | use test::Bencher; 22 | use tokio::{ 23 | io::{self, AsyncRead, AsyncWrite}, 24 | net::TcpStream, 25 | }; 26 | 27 | mod common; 28 | 29 | async fn test_client(client: &mut Client) { 30 | let document_root = current_dir().unwrap().join("tests").join("php"); 31 | let document_root = document_root.to_str().unwrap(); 32 | let script_name = current_dir() 33 | .unwrap() 34 | .join("tests") 35 | .join("php") 36 | .join("index.php"); 37 | let script_name = script_name.to_str().unwrap(); 38 | 39 | let params = Params::default() 40 | .set_request_method("GET") 41 | .set_document_root(document_root) 42 | .set_script_name("/index.php") 43 | .set_script_filename(script_name) 44 | .set_request_uri("/index.php") 45 | .set_document_uri("/index.php") 46 | .set_remote_addr("127.0.0.1") 47 | .set_remote_port("12345") 48 | .set_server_addr("127.0.0.1") 49 | .set_server_port("80") 50 | .set_server_name("jmjoy-pc") 51 | .set_content_type("") 52 | .set_content_length("0"); 53 | 54 | let output = client 55 | .execute(Request::new(params, &mut io::empty())) 56 | .await 57 | .unwrap(); 58 | 59 | let stdout = String::from_utf8(output.get_stdout().unwrap_or(Default::default())).unwrap(); 60 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 61 | assert!(stdout.contains("\r\n\r\n")); 62 | assert!(stdout.contains("hello")); 63 | assert_eq!(output.get_stderr(), None); 64 | } 65 | 66 | #[bench] 67 | fn bench_execute(b: &mut Bencher) { 68 | common::setup(); 69 | 70 | let rt = tokio::runtime::Builder::new_multi_thread() 71 | .worker_threads(6) 72 | .enable_all() 73 | .build() 74 | .unwrap(); 75 | 76 | let mut client = rt.block_on(async { 77 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 78 | Client::new_keep_alive(stream) 79 | }); 80 | 81 | b.iter(|| { 82 | rt.block_on(async { 83 | test_client(&mut client).await; 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /benches/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Once; 16 | use tracing::Level; 17 | use tracing_subscriber::FmtSubscriber; 18 | 19 | static START: Once = Once::new(); 20 | 21 | /// Setup function that is only run once, even if called multiple times. 22 | pub fn setup() { 23 | START.call_once(|| { 24 | let subscriber = FmtSubscriber::builder() 25 | .with_max_level(Level::DEBUG) 26 | .finish(); 27 | 28 | tracing::subscriber::set_global_default(subscriber) 29 | .expect("setting default subscriber failed"); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::{ 16 | conn::{KeepAlive, Mode, ShortConn}, 17 | meta::{BeginRequestRec, EndRequestRec, Header, ParamPairs, RequestType, Role}, 18 | params::Params, 19 | request::Request, 20 | response::ResponseStream, 21 | ClientError, ClientResult, Response, 22 | }; 23 | use std::marker::PhantomData; 24 | use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; 25 | use tracing::debug; 26 | 27 | /// I refer to nginx fastcgi implementation, found the request id is always 1. 28 | /// 29 | /// 30 | const REQUEST_ID: u16 = 1; 31 | 32 | /// Async client for handling communication between fastcgi server. 33 | pub struct Client { 34 | stream: S, 35 | _mode: PhantomData, 36 | } 37 | 38 | impl Client { 39 | /// Construct a `Client` Object with stream, such as `tokio::net::TcpStream` 40 | /// or `tokio::net::UnixStream`, under short connection mode. 41 | pub fn new(stream: S) -> Self { 42 | Self { 43 | stream, 44 | _mode: PhantomData, 45 | } 46 | } 47 | 48 | /// Send request and receive response from fastcgi server, under short 49 | /// connection mode. 50 | pub async fn execute_once( 51 | mut self, request: Request<'_, I>, 52 | ) -> ClientResult { 53 | self.inner_execute(request).await 54 | } 55 | 56 | /// Send request and receive response stream from fastcgi server, under 57 | /// short connection mode. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ``` 62 | /// use fastcgi_client::{response::Content, Client, Params, Request}; 63 | /// use tokio::{io, net::TcpStream}; 64 | /// 65 | /// async fn stream() { 66 | /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 67 | /// let client = Client::new(stream); 68 | /// let mut stream = client 69 | /// .execute_once_stream(Request::new(Params::default(), &mut io::empty())) 70 | /// .await 71 | /// .unwrap(); 72 | /// 73 | /// while let Some(content) = stream.next().await { 74 | /// let content = content.unwrap(); 75 | /// 76 | /// match content { 77 | /// Content::Stdout(out) => todo!(), 78 | /// Content::Stderr(out) => todo!(), 79 | /// } 80 | /// } 81 | /// } 82 | /// ``` 83 | pub async fn execute_once_stream<'a, I: AsyncRead + Unpin>( 84 | mut self, request: Request<'_, I>, 85 | ) -> ClientResult> { 86 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 87 | Ok(ResponseStream::new(self.stream, REQUEST_ID)) 88 | } 89 | } 90 | 91 | impl Client { 92 | /// Construct a `Client` Object with stream, such as `tokio::net::TcpStream` 93 | /// or `tokio::net::UnixStream`, under keep alive connection mode. 94 | pub fn new_keep_alive(stream: S) -> Self { 95 | Self { 96 | stream, 97 | _mode: PhantomData, 98 | } 99 | } 100 | 101 | /// Send request and receive response from fastcgi server, under keep alive 102 | /// connection mode. 103 | pub async fn execute( 104 | &mut self, request: Request<'_, I>, 105 | ) -> ClientResult { 106 | self.inner_execute(request).await 107 | } 108 | 109 | /// Send request and receive response stream from fastcgi server, under 110 | /// keep alive connection mode. 111 | /// 112 | /// # Examples 113 | /// 114 | /// ``` 115 | /// use fastcgi_client::{response::Content, Client, Params, Request}; 116 | /// use tokio::{io, net::TcpStream}; 117 | /// 118 | /// async fn stream() { 119 | /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 120 | /// let mut client = Client::new_keep_alive(stream); 121 | /// 122 | /// for _ in (0..3) { 123 | /// let mut stream = client 124 | /// .execute_stream(Request::new(Params::default(), &mut io::empty())) 125 | /// .await 126 | /// .unwrap(); 127 | /// 128 | /// while let Some(content) = stream.next().await { 129 | /// let content = content.unwrap(); 130 | /// 131 | /// match content { 132 | /// Content::Stdout(out) => todo!(), 133 | /// Content::Stderr(out) => todo!(), 134 | /// } 135 | /// } 136 | /// } 137 | /// } 138 | /// ``` 139 | pub async fn execute_stream( 140 | &mut self, request: Request<'_, I>, 141 | ) -> ClientResult> { 142 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 143 | Ok(ResponseStream::new(&mut self.stream, REQUEST_ID)) 144 | } 145 | } 146 | 147 | impl Client { 148 | async fn inner_execute( 149 | &mut self, request: Request<'_, I>, 150 | ) -> ClientResult { 151 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 152 | Self::handle_response(&mut self.stream, REQUEST_ID).await 153 | } 154 | 155 | async fn handle_request<'a, I: AsyncRead + Unpin>( 156 | stream: &mut S, id: u16, params: Params<'a>, mut body: I, 157 | ) -> ClientResult<()> { 158 | Self::handle_request_start(stream, id).await?; 159 | Self::handle_request_params(stream, id, params).await?; 160 | Self::handle_request_body(stream, id, &mut body).await?; 161 | Self::handle_request_flush(stream).await?; 162 | Ok(()) 163 | } 164 | 165 | async fn handle_request_start(stream: &mut S, id: u16) -> ClientResult<()> { 166 | debug!(id, "Start handle request"); 167 | 168 | let begin_request_rec = 169 | BeginRequestRec::new(id, Role::Responder, ::is_keep_alive()).await?; 170 | 171 | debug!(id, ?begin_request_rec, "Send to stream."); 172 | 173 | begin_request_rec.write_to_stream(stream).await?; 174 | 175 | Ok(()) 176 | } 177 | 178 | async fn handle_request_params<'a>( 179 | stream: &mut S, id: u16, params: Params<'a>, 180 | ) -> ClientResult<()> { 181 | let param_pairs = ParamPairs::new(params); 182 | debug!(id, ?param_pairs, "Params will be sent."); 183 | 184 | Header::write_to_stream_batches( 185 | RequestType::Params, 186 | id, 187 | stream, 188 | &mut ¶m_pairs.to_content().await?[..], 189 | Some(|header| { 190 | debug!(id, ?header, "Send to stream for Params."); 191 | header 192 | }), 193 | ) 194 | .await?; 195 | 196 | Header::write_to_stream_batches( 197 | RequestType::Params, 198 | id, 199 | stream, 200 | &mut tokio::io::empty(), 201 | Some(|header| { 202 | debug!(id, ?header, "Send to stream for Params."); 203 | header 204 | }), 205 | ) 206 | .await?; 207 | 208 | Ok(()) 209 | } 210 | 211 | async fn handle_request_body( 212 | stream: &mut S, id: u16, body: &mut I, 213 | ) -> ClientResult<()> { 214 | Header::write_to_stream_batches( 215 | RequestType::Stdin, 216 | id, 217 | stream, 218 | body, 219 | Some(|header| { 220 | debug!(id, ?header, "Send to stream for Stdin."); 221 | header 222 | }), 223 | ) 224 | .await?; 225 | 226 | Header::write_to_stream_batches( 227 | RequestType::Stdin, 228 | id, 229 | stream, 230 | &mut tokio::io::empty(), 231 | Some(|header| { 232 | debug!(id, ?header, "Send to stream for Stdin."); 233 | header 234 | }), 235 | ) 236 | .await?; 237 | 238 | Ok(()) 239 | } 240 | 241 | async fn handle_request_flush(stream: &mut S) -> ClientResult<()> { 242 | stream.flush().await?; 243 | 244 | Ok(()) 245 | } 246 | 247 | async fn handle_response(stream: &mut S, id: u16) -> ClientResult { 248 | let mut response = Response::default(); 249 | 250 | let mut stderr = Vec::new(); 251 | let mut stdout = Vec::new(); 252 | 253 | loop { 254 | let header = Header::new_from_stream(stream).await?; 255 | if header.request_id != id { 256 | return Err(ClientError::ResponseNotFound { id }); 257 | } 258 | debug!(id, ?header, "Receive from stream."); 259 | 260 | match header.r#type { 261 | RequestType::Stdout => { 262 | stdout.extend(header.read_content_from_stream(stream).await?); 263 | } 264 | RequestType::Stderr => { 265 | stderr.extend(header.read_content_from_stream(stream).await?); 266 | } 267 | RequestType::EndRequest => { 268 | let end_request_rec = EndRequestRec::from_header(&header, stream).await?; 269 | debug!(id, ?end_request_rec, "Receive from stream."); 270 | 271 | end_request_rec 272 | .end_request 273 | .protocol_status 274 | .convert_to_client_result(end_request_rec.end_request.app_status)?; 275 | 276 | response.stdout = if stdout.is_empty() { 277 | None 278 | } else { 279 | Some(stdout) 280 | }; 281 | response.stderr = if stderr.is_empty() { 282 | None 283 | } else { 284 | Some(stderr) 285 | }; 286 | 287 | return Ok(response); 288 | } 289 | r#type => { 290 | return Err(ClientError::UnknownRequestType { 291 | request_type: r#type, 292 | }) 293 | } 294 | } 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/conn.rs: -------------------------------------------------------------------------------- 1 | /// Connection mode, indicate is keep alive or not. 2 | // Copyright 2022 jmjoy 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | pub trait Mode { 17 | fn is_keep_alive() -> bool; 18 | } 19 | 20 | /// Short connection mode. 21 | pub struct ShortConn; 22 | 23 | impl Mode for ShortConn { 24 | fn is_keep_alive() -> bool { 25 | false 26 | } 27 | } 28 | 29 | /// Keep alive connection mode. 30 | pub struct KeepAlive {} 31 | 32 | impl Mode for KeepAlive { 33 | fn is_keep_alive() -> bool { 34 | true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::meta::{ProtocolStatus, RequestType}; 16 | 17 | pub type ClientResult = Result; 18 | 19 | #[derive(Debug, thiserror::Error)] 20 | pub enum ClientError { 21 | /// Wapper of `tokio::io::Error` 22 | #[error(transparent)] 23 | Io(#[from] tokio::io::Error), 24 | 25 | /// Usually not happen. 26 | #[error("Response not found of request id `{id}`")] 27 | RequestIdNotFound { id: u16 }, 28 | 29 | /// Usually not happen. 30 | #[error("Response not found of request id `{id}`")] 31 | ResponseNotFound { id: u16 }, 32 | 33 | /// Maybe unimplemented request type received fom response. 34 | #[error("Response not found of request id `{request_type}`")] 35 | UnknownRequestType { request_type: RequestType }, 36 | 37 | /// Response not complete, first is protocol status and second is app 38 | /// status, see fastcgi protocol. 39 | #[error("This app can't multiplex [CantMpxConn]; AppStatus: {app_status}")] 40 | EndRequestCantMpxConn { app_status: u32 }, 41 | 42 | /// Response not complete, first is protocol status and second is app 43 | /// status, see fastcgi protocol. 44 | #[error("New request rejected; too busy [OVERLOADED]; AppStatus: {app_status}")] 45 | EndRequestOverloaded { app_status: u32 }, 46 | 47 | /// Response not complete, first is protocol status and second is app 48 | /// status, see fastcgi protocol. 49 | #[error("Role value not known [UnknownRole]; AppStatus: {app_status}")] 50 | EndRequestUnknownRole { app_status: u32 }, 51 | } 52 | 53 | impl ClientError { 54 | pub(crate) fn new_end_request_with_protocol_status( 55 | protocol_status: ProtocolStatus, app_status: u32, 56 | ) -> Self { 57 | match protocol_status { 58 | ProtocolStatus::CantMpxConn => ClientError::EndRequestCantMpxConn { app_status }, 59 | ProtocolStatus::Overloaded => ClientError::EndRequestOverloaded { app_status }, 60 | _ => ClientError::EndRequestUnknownRole { app_status }, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![warn(rust_2018_idioms)] 16 | #![warn(clippy::dbg_macro, clippy::print_stdout)] 17 | #![doc = include_str!("../README.md")] 18 | 19 | pub mod client; 20 | pub mod conn; 21 | mod error; 22 | mod meta; 23 | pub mod params; 24 | pub mod request; 25 | pub mod response; 26 | 27 | pub use crate::{client::Client, error::*, params::Params, request::Request, response::Response}; 28 | -------------------------------------------------------------------------------- /src/meta.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::{ 16 | error::{ClientError, ClientResult}, 17 | Params, 18 | }; 19 | use std::{ 20 | borrow::Cow, 21 | cmp::min, 22 | collections::HashMap, 23 | fmt::{self, Debug, Display}, 24 | mem::size_of, 25 | ops::{Deref, DerefMut}, 26 | }; 27 | use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; 28 | 29 | pub(crate) const VERSION_1: u8 = 1; 30 | pub(crate) const MAX_LENGTH: usize = 0xffff; 31 | pub(crate) const HEADER_LEN: usize = size_of::
(); 32 | 33 | #[derive(Debug, Clone)] 34 | #[repr(u8)] 35 | pub enum RequestType { 36 | BeginRequest = 1, 37 | AbortRequest = 2, 38 | EndRequest = 3, 39 | Params = 4, 40 | Stdin = 5, 41 | Stdout = 6, 42 | Stderr = 7, 43 | Data = 8, 44 | GetValues = 9, 45 | GetValuesResult = 10, 46 | UnknownType = 11, 47 | } 48 | 49 | impl RequestType { 50 | fn from_u8(u: u8) -> Self { 51 | match u { 52 | 1 => RequestType::BeginRequest, 53 | 2 => RequestType::AbortRequest, 54 | 3 => RequestType::EndRequest, 55 | 4 => RequestType::Params, 56 | 5 => RequestType::Stdin, 57 | 6 => RequestType::Stdout, 58 | 7 => RequestType::Stderr, 59 | 8 => RequestType::Data, 60 | 9 => RequestType::GetValues, 61 | 10 => RequestType::GetValuesResult, 62 | _ => RequestType::UnknownType, 63 | } 64 | } 65 | } 66 | 67 | impl Display for RequestType { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 69 | Display::fmt(&(self.clone() as u8), f) 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone)] 74 | pub(crate) struct Header { 75 | pub(crate) version: u8, 76 | pub(crate) r#type: RequestType, 77 | pub(crate) request_id: u16, 78 | pub(crate) content_length: u16, 79 | pub(crate) padding_length: u8, 80 | pub(crate) reserved: u8, 81 | } 82 | 83 | impl Header { 84 | pub(crate) async fn write_to_stream_batches( 85 | r#type: RequestType, request_id: u16, writer: &mut W, content: &mut R, 86 | before_write: Option, 87 | ) -> io::Result<()> 88 | where 89 | F: Fn(Header) -> Header, 90 | R: AsyncRead + Unpin, 91 | W: AsyncWrite + Unpin, 92 | { 93 | let mut buf: [u8; MAX_LENGTH] = [0; MAX_LENGTH]; 94 | let mut had_written = false; 95 | 96 | loop { 97 | let read = content.read(&mut buf).await?; 98 | if had_written && read == 0 { 99 | break; 100 | } 101 | 102 | let buf = &buf[..read]; 103 | let mut header = Self::new(r#type.clone(), request_id, buf); 104 | if let Some(ref f) = before_write { 105 | header = f(header); 106 | } 107 | header.write_to_stream(writer, buf).await?; 108 | 109 | had_written = true; 110 | } 111 | Ok(()) 112 | } 113 | 114 | fn new(r#type: RequestType, request_id: u16, content: &[u8]) -> Self { 115 | let content_length = min(content.len(), MAX_LENGTH) as u16; 116 | Self { 117 | version: VERSION_1, 118 | r#type, 119 | request_id, 120 | content_length, 121 | padding_length: (-(content_length as i16) & 7) as u8, 122 | reserved: 0, 123 | } 124 | } 125 | 126 | async fn write_to_stream( 127 | self, writer: &mut W, content: &[u8], 128 | ) -> io::Result<()> { 129 | let mut buf: Vec = Vec::new(); 130 | buf.push(self.version); 131 | buf.push(self.r#type as u8); 132 | buf.write_u16(self.request_id).await?; 133 | buf.write_u16(self.content_length).await?; 134 | buf.push(self.padding_length); 135 | buf.push(self.reserved); 136 | 137 | writer.write_all(&buf).await?; 138 | writer.write_all(content).await?; 139 | writer 140 | .write_all(&vec![0; self.padding_length as usize]) 141 | .await?; 142 | 143 | Ok(()) 144 | } 145 | 146 | pub(crate) async fn new_from_stream(reader: &mut R) -> io::Result { 147 | let mut buf: [u8; HEADER_LEN] = [0; HEADER_LEN]; 148 | reader.read_exact(&mut buf).await?; 149 | 150 | Ok(Self::new_from_buf(&buf)) 151 | } 152 | 153 | #[inline] 154 | pub(crate) fn new_from_buf(buf: &[u8; HEADER_LEN]) -> Self { 155 | Self { 156 | version: buf[0], 157 | r#type: RequestType::from_u8(buf[1]), 158 | request_id: be_buf_to_u16(&buf[2..4]), 159 | content_length: be_buf_to_u16(&buf[4..6]), 160 | padding_length: buf[6], 161 | reserved: buf[7], 162 | } 163 | } 164 | 165 | pub(crate) async fn read_content_from_stream( 166 | &self, reader: &mut R, 167 | ) -> io::Result> { 168 | let mut buf = vec![0; self.content_length as usize]; 169 | reader.read_exact(&mut buf).await?; 170 | let mut padding_buf = vec![0; self.padding_length as usize]; 171 | reader.read_exact(&mut padding_buf).await?; 172 | Ok(buf) 173 | } 174 | } 175 | 176 | #[derive(Debug, Clone, Copy)] 177 | #[repr(u16)] 178 | #[allow(dead_code)] 179 | pub enum Role { 180 | Responder = 1, 181 | Authorizer = 2, 182 | Filter = 3, 183 | } 184 | 185 | #[derive(Debug)] 186 | pub(crate) struct BeginRequest { 187 | pub(crate) role: Role, 188 | pub(crate) flags: u8, 189 | pub(crate) reserved: [u8; 5], 190 | } 191 | 192 | impl BeginRequest { 193 | pub(crate) fn new(role: Role, keep_alive: bool) -> Self { 194 | Self { 195 | role, 196 | flags: keep_alive as u8, 197 | reserved: [0; 5], 198 | } 199 | } 200 | 201 | pub(crate) async fn to_content(&self) -> io::Result> { 202 | let mut buf: Vec = Vec::new(); 203 | buf.write_u16(self.role as u16).await?; 204 | buf.push(self.flags); 205 | buf.extend_from_slice(&self.reserved); 206 | Ok(buf) 207 | } 208 | } 209 | 210 | pub(crate) struct BeginRequestRec { 211 | pub(crate) header: Header, 212 | pub(crate) begin_request: BeginRequest, 213 | pub(crate) content: Vec, 214 | } 215 | 216 | impl BeginRequestRec { 217 | pub(crate) async fn new(request_id: u16, role: Role, keep_alive: bool) -> io::Result { 218 | let begin_request = BeginRequest::new(role, keep_alive); 219 | let content = begin_request.to_content().await?; 220 | let header = Header::new(RequestType::BeginRequest, request_id, &content); 221 | Ok(Self { 222 | header, 223 | begin_request, 224 | content, 225 | }) 226 | } 227 | 228 | pub(crate) async fn write_to_stream( 229 | self, writer: &mut W, 230 | ) -> io::Result<()> { 231 | self.header.write_to_stream(writer, &self.content).await 232 | } 233 | } 234 | 235 | impl Debug for BeginRequestRec { 236 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 237 | Debug::fmt( 238 | &format!( 239 | "BeginRequestRec {{header: {:?}, begin_request: {:?}}}", 240 | self.header, self.begin_request 241 | ), 242 | f, 243 | ) 244 | } 245 | } 246 | 247 | #[derive(Debug, Clone, Copy)] 248 | pub enum ParamLength { 249 | Short(u8), 250 | Long(u32), 251 | } 252 | 253 | impl ParamLength { 254 | pub fn new(length: usize) -> Self { 255 | if length < 128 { 256 | ParamLength::Short(length as u8) 257 | } else { 258 | let mut length = length; 259 | length |= 1 << 31; 260 | ParamLength::Long(length as u32) 261 | } 262 | } 263 | 264 | pub async fn content(self) -> io::Result> { 265 | let mut buf: Vec = Vec::new(); 266 | match self { 267 | ParamLength::Short(l) => buf.push(l), 268 | ParamLength::Long(l) => buf.write_u32(l).await?, 269 | } 270 | Ok(buf) 271 | } 272 | } 273 | 274 | #[derive(Debug)] 275 | pub struct ParamPair<'a> { 276 | name_length: ParamLength, 277 | value_length: ParamLength, 278 | name_data: Cow<'a, str>, 279 | value_data: Cow<'a, str>, 280 | } 281 | 282 | impl<'a> ParamPair<'a> { 283 | fn new(name: Cow<'a, str>, value: Cow<'a, str>) -> Self { 284 | let name_length = ParamLength::new(name.len()); 285 | let value_length = ParamLength::new(value.len()); 286 | Self { 287 | name_length, 288 | value_length, 289 | name_data: name, 290 | value_data: value, 291 | } 292 | } 293 | 294 | async fn write_to_stream(&self, writer: &mut W) -> io::Result<()> { 295 | writer.write_all(&self.name_length.content().await?).await?; 296 | writer 297 | .write_all(&self.value_length.content().await?) 298 | .await?; 299 | writer.write_all(self.name_data.as_bytes()).await?; 300 | writer.write_all(self.value_data.as_bytes()).await?; 301 | Ok(()) 302 | } 303 | } 304 | 305 | #[derive(Debug)] 306 | pub(crate) struct ParamPairs<'a>(Vec>); 307 | 308 | impl<'a> ParamPairs<'a> { 309 | pub(crate) fn new(params: Params<'a>) -> Self { 310 | let mut param_pairs = Vec::new(); 311 | let params: HashMap, Cow<'a, str>> = params.into(); 312 | for (name, value) in params.into_iter() { 313 | let param_pair = ParamPair::new(name, value); 314 | param_pairs.push(param_pair); 315 | } 316 | 317 | Self(param_pairs) 318 | } 319 | 320 | pub(crate) async fn to_content(&self) -> io::Result> { 321 | let mut buf: Vec = Vec::new(); 322 | 323 | for param_pair in self.iter() { 324 | param_pair.write_to_stream(&mut buf).await?; 325 | } 326 | 327 | Ok(buf) 328 | } 329 | } 330 | 331 | impl<'a> Deref for ParamPairs<'a> { 332 | type Target = Vec>; 333 | 334 | fn deref(&self) -> &Self::Target { 335 | &self.0 336 | } 337 | } 338 | 339 | impl<'a> DerefMut for ParamPairs<'a> { 340 | fn deref_mut(&mut self) -> &mut Self::Target { 341 | &mut self.0 342 | } 343 | } 344 | 345 | #[derive(Debug)] 346 | #[repr(u8)] 347 | pub enum ProtocolStatus { 348 | RequestComplete = 0, 349 | CantMpxConn = 1, 350 | Overloaded = 2, 351 | UnknownRole = 3, 352 | } 353 | 354 | impl ProtocolStatus { 355 | pub fn from_u8(u: u8) -> Self { 356 | match u { 357 | 0 => ProtocolStatus::RequestComplete, 358 | 1 => ProtocolStatus::CantMpxConn, 359 | 2 => ProtocolStatus::Overloaded, 360 | _ => ProtocolStatus::UnknownRole, 361 | } 362 | } 363 | 364 | pub(crate) fn convert_to_client_result(self, app_status: u32) -> ClientResult<()> { 365 | match self { 366 | ProtocolStatus::RequestComplete => Ok(()), 367 | _ => Err(ClientError::new_end_request_with_protocol_status( 368 | self, app_status, 369 | )), 370 | } 371 | } 372 | } 373 | 374 | #[derive(Debug)] 375 | pub struct EndRequest { 376 | pub(crate) app_status: u32, 377 | pub(crate) protocol_status: ProtocolStatus, 378 | #[allow(dead_code)] 379 | reserved: [u8; 3], 380 | } 381 | 382 | #[derive(Debug)] 383 | pub(crate) struct EndRequestRec { 384 | #[allow(dead_code)] 385 | header: Header, 386 | pub(crate) end_request: EndRequest, 387 | } 388 | 389 | impl EndRequestRec { 390 | pub(crate) async fn from_header( 391 | header: &Header, reader: &mut R, 392 | ) -> io::Result { 393 | let header = header.clone(); 394 | let content = &*header.read_content_from_stream(reader).await?; 395 | Ok(Self::new_from_buf(header, content)) 396 | } 397 | 398 | pub(crate) fn new_from_buf(header: Header, buf: &[u8]) -> Self { 399 | let app_status = u32::from_be_bytes(<[u8; 4]>::try_from(&buf[0..4]).unwrap()); 400 | let protocol_status = 401 | ProtocolStatus::from_u8(u8::from_be_bytes(<[u8; 1]>::try_from(&buf[4..5]).unwrap())); 402 | let reserved = <[u8; 3]>::try_from(&buf[5..8]).unwrap(); 403 | Self { 404 | header, 405 | end_request: EndRequest { 406 | app_status, 407 | protocol_status, 408 | reserved, 409 | }, 410 | } 411 | } 412 | } 413 | 414 | fn be_buf_to_u16(buf: &[u8]) -> u16 { 415 | u16::from_be_bytes(<[u8; 2]>::try_from(buf).unwrap()) 416 | } 417 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | borrow::Cow, 17 | collections::HashMap, 18 | ops::{Deref, DerefMut}, 19 | }; 20 | 21 | /// Fastcgi params, please reference to nginx-php-fpm fastcgi_params. 22 | #[derive(Debug, Clone, PartialEq, Eq)] 23 | pub struct Params<'a>(HashMap, Cow<'a, str>>); 24 | 25 | impl<'a> Params<'a> { 26 | #[inline] 27 | pub fn gateway_interface>>(mut self, gateway_interface: S) -> Self { 28 | self.insert("GATEWAY_INTERFACE".into(), gateway_interface.into()); 29 | self 30 | } 31 | 32 | #[inline] 33 | pub fn server_software>>(mut self, server_software: S) -> Self { 34 | self.insert("SERVER_SOFTWARE".into(), server_software.into()); 35 | self 36 | } 37 | 38 | #[inline] 39 | pub fn server_protocol>>(mut self, server_protocol: S) -> Self { 40 | self.insert("SERVER_PROTOCOL".into(), server_protocol.into()); 41 | self 42 | } 43 | 44 | #[inline] 45 | pub fn request_method>>(mut self, request_method: S) -> Self { 46 | self.insert("REQUEST_METHOD".into(), request_method.into()); 47 | self 48 | } 49 | 50 | #[inline] 51 | pub fn script_filename>>(mut self, script_filename: S) -> Self { 52 | self.insert("SCRIPT_FILENAME".into(), script_filename.into()); 53 | self 54 | } 55 | 56 | #[inline] 57 | pub fn script_name>>(mut self, script_name: S) -> Self { 58 | self.insert("SCRIPT_NAME".into(), script_name.into()); 59 | self 60 | } 61 | 62 | #[inline] 63 | pub fn query_string>>(mut self, query_string: S) -> Self { 64 | self.insert("QUERY_STRING".into(), query_string.into()); 65 | self 66 | } 67 | 68 | #[inline] 69 | pub fn request_uri>>(mut self, request_uri: S) -> Self { 70 | self.insert("REQUEST_URI".into(), request_uri.into()); 71 | self 72 | } 73 | 74 | #[inline] 75 | pub fn document_root>>(mut self, document_root: S) -> Self { 76 | self.insert("DOCUMENT_ROOT".into(), document_root.into()); 77 | self 78 | } 79 | 80 | #[inline] 81 | pub fn document_uri>>(mut self, document_uri: S) -> Self { 82 | self.insert("DOCUMENT_URI".into(), document_uri.into()); 83 | self 84 | } 85 | 86 | #[inline] 87 | pub fn remote_addr>>(mut self, remote_addr: S) -> Self { 88 | self.insert("REMOTE_ADDR".into(), remote_addr.into()); 89 | self 90 | } 91 | 92 | #[inline] 93 | pub fn remote_port(mut self, remote_port: u16) -> Self { 94 | self.insert("REMOTE_PORT".into(), remote_port.to_string().into()); 95 | self 96 | } 97 | 98 | #[inline] 99 | pub fn server_addr>>(mut self, server_addr: S) -> Self { 100 | self.insert("SERVER_ADDR".into(), server_addr.into()); 101 | self 102 | } 103 | 104 | #[inline] 105 | pub fn server_port(mut self, server_port: u16) -> Self { 106 | self.insert("SERVER_PORT".into(), server_port.to_string().into()); 107 | self 108 | } 109 | 110 | #[inline] 111 | pub fn server_name>>(mut self, server_name: S) -> Self { 112 | self.insert("SERVER_NAME".into(), server_name.into()); 113 | self 114 | } 115 | 116 | #[inline] 117 | pub fn content_type>>(mut self, content_type: S) -> Self { 118 | self.insert("CONTENT_TYPE".into(), content_type.into()); 119 | self 120 | } 121 | 122 | #[inline] 123 | pub fn content_length(mut self, content_length: usize) -> Self { 124 | self.insert("CONTENT_LENGTH".into(), content_length.to_string().into()); 125 | self 126 | } 127 | } 128 | 129 | impl<'a> Default for Params<'a> { 130 | fn default() -> Self { 131 | Params(HashMap::new()) 132 | .gateway_interface("FastCGI/1.0") 133 | .server_software("fastcgi-client-rs") 134 | .server_protocol("HTTP/1.1") 135 | } 136 | } 137 | 138 | impl<'a> Deref for Params<'a> { 139 | type Target = HashMap, Cow<'a, str>>; 140 | 141 | fn deref(&self) -> &Self::Target { 142 | &self.0 143 | } 144 | } 145 | 146 | impl<'a> DerefMut for Params<'a> { 147 | fn deref_mut(&mut self) -> &mut Self::Target { 148 | &mut self.0 149 | } 150 | } 151 | 152 | impl<'a> From> for HashMap, Cow<'a, str>> { 153 | fn from(params: Params<'a>) -> Self { 154 | params.0 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::Params; 16 | use tokio::io::AsyncRead; 17 | 18 | /// fastcgi request. 19 | pub struct Request<'a, I: AsyncRead + Unpin> { 20 | pub(crate) params: Params<'a>, 21 | pub(crate) stdin: I, 22 | } 23 | 24 | impl<'a, I: AsyncRead + Unpin> Request<'a, I> { 25 | pub fn new(params: Params<'a>, stdin: I) -> Self { 26 | Self { params, stdin } 27 | } 28 | 29 | pub fn params(&self) -> &Params<'a> { 30 | &self.params 31 | } 32 | 33 | pub fn params_mut(&mut self) -> &mut Params<'a> { 34 | &mut self.params 35 | } 36 | 37 | pub fn stdin(&self) -> &I { 38 | &self.stdin 39 | } 40 | 41 | pub fn stdin_mut(&mut self) -> &mut I { 42 | &mut self.stdin 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::{ 16 | meta::{EndRequestRec, Header, RequestType}, 17 | ClientError, ClientResult, 18 | }; 19 | use std::{cmp::min, fmt, fmt::Debug, str}; 20 | use tokio::io::{AsyncRead, AsyncReadExt}; 21 | use tracing::debug; 22 | 23 | /// Output of fastcgi request, contains STDOUT and STDERR. 24 | #[derive(Default, Clone)] 25 | #[non_exhaustive] 26 | pub struct Response { 27 | pub stdout: Option>, 28 | pub stderr: Option>, 29 | } 30 | 31 | impl Debug for Response { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 33 | f.debug_struct("Response") 34 | .field("stdout", &self.stdout.as_deref().map(str::from_utf8)) 35 | .field("stderr", &self.stderr.as_deref().map(str::from_utf8)) 36 | .finish() 37 | } 38 | } 39 | 40 | pub enum Content<'a> { 41 | Stdout(&'a [u8]), 42 | Stderr(&'a [u8]), 43 | } 44 | 45 | #[derive(PartialEq)] 46 | enum ReadStep { 47 | Content, 48 | Padding, 49 | } 50 | 51 | /// Generated by 52 | /// [Client::execute_once_stream](crate::client::Client::execute_once_stream) or 53 | /// [Client::execute_stream](crate::client::Client::execute_stream). 54 | /// 55 | /// The [ResponseStream] does not implement `futures::Stream`, because 56 | /// `futures::Stream` does not yet support GAT, so manually provide the 57 | /// [next](ResponseStream::next) method, which support the `while let` syntax. 58 | pub struct ResponseStream { 59 | stream: S, 60 | id: u16, 61 | 62 | ended: bool, 63 | 64 | header: Option
, 65 | 66 | content_buf: Vec, 67 | content_read: usize, 68 | 69 | read_step: ReadStep, 70 | } 71 | 72 | impl ResponseStream { 73 | #[inline] 74 | pub(crate) fn new(stream: S, id: u16) -> Self { 75 | Self { 76 | stream, 77 | id, 78 | ended: false, 79 | header: None, 80 | content_buf: vec![0; 4096], 81 | content_read: 0, 82 | read_step: ReadStep::Content, 83 | } 84 | } 85 | 86 | pub async fn next(&mut self) -> Option>> { 87 | if self.ended { 88 | return None; 89 | } 90 | 91 | loop { 92 | if self.header.is_none() { 93 | match Header::new_from_stream(&mut self.stream).await { 94 | Ok(header) => { 95 | self.header = Some(header); 96 | } 97 | Err(err) => { 98 | self.ended = true; 99 | return Some(Err(err.into())); 100 | } 101 | }; 102 | } 103 | 104 | let header = self.header.as_ref().unwrap(); 105 | 106 | match header.r#type.clone() { 107 | RequestType::Stdout => match self.read_step { 108 | ReadStep::Content => { 109 | return self 110 | .read_to_content( 111 | header.content_length as usize, 112 | Content::Stdout, 113 | Self::prepare_for_read_padding, 114 | ) 115 | .await; 116 | } 117 | ReadStep::Padding => { 118 | self.read_to_content( 119 | header.padding_length as usize, 120 | Content::Stdout, 121 | Self::prepare_for_read_header, 122 | ) 123 | .await; 124 | continue; 125 | } 126 | }, 127 | RequestType::Stderr => match self.read_step { 128 | ReadStep::Content => { 129 | return self 130 | .read_to_content( 131 | header.content_length as usize, 132 | Content::Stderr, 133 | Self::prepare_for_read_padding, 134 | ) 135 | .await; 136 | } 137 | ReadStep::Padding => { 138 | self.read_to_content( 139 | header.padding_length as usize, 140 | Content::Stderr, 141 | Self::prepare_for_read_header, 142 | ) 143 | .await; 144 | continue; 145 | } 146 | }, 147 | RequestType::EndRequest => { 148 | let end_request_rec = 149 | match EndRequestRec::from_header(header, &mut self.stream).await { 150 | Ok(rec) => rec, 151 | Err(err) => { 152 | self.ended = true; 153 | return Some(Err(err.into())); 154 | } 155 | }; 156 | debug!(id = self.id, ?end_request_rec, "Receive from stream."); 157 | 158 | self.ended = true; 159 | 160 | return match end_request_rec 161 | .end_request 162 | .protocol_status 163 | .convert_to_client_result(end_request_rec.end_request.app_status) 164 | { 165 | Ok(_) => None, 166 | Err(err) => Some(Err(err)), 167 | }; 168 | } 169 | r#type => { 170 | self.ended = true; 171 | return Some(Err(ClientError::UnknownRequestType { 172 | request_type: r#type, 173 | })); 174 | } 175 | } 176 | } 177 | } 178 | 179 | async fn read_to_content<'a, T: 'a>( 180 | &'a mut self, length: usize, content_fn: impl FnOnce(&'a [u8]) -> T, 181 | prepare_for_next_fn: impl FnOnce(&mut Self), 182 | ) -> Option> { 183 | let content_len = self.content_buf.len(); 184 | let read = match self 185 | .stream 186 | .read(&mut self.content_buf[..min(content_len, length - self.content_read)]) 187 | .await 188 | { 189 | Ok(read) => read, 190 | Err(err) => { 191 | self.ended = true; 192 | return Some(Err(err.into())); 193 | } 194 | }; 195 | 196 | self.content_read += read; 197 | if self.content_read >= length { 198 | self.content_read = 0; 199 | prepare_for_next_fn(self); 200 | } 201 | 202 | Some(Ok(content_fn(&self.content_buf[..read]))) 203 | } 204 | 205 | fn prepare_for_read_padding(&mut self) { 206 | self.read_step = ReadStep::Padding; 207 | } 208 | 209 | fn prepare_for_read_header(&mut self) { 210 | self.header = None; 211 | self.read_step = ReadStep::Content; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests/client_get.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use fastcgi_client::{conn::ShortConn, request::Request, response::Content, Client, Params}; 16 | use std::env::current_dir; 17 | use tokio::{ 18 | io::{self, AsyncRead, AsyncWrite}, 19 | net::TcpStream, 20 | }; 21 | 22 | mod common; 23 | 24 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 25 | async fn test() { 26 | common::setup(); 27 | 28 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 29 | test_client(Client::new(stream)).await; 30 | } 31 | 32 | async fn test_client(client: Client) { 33 | let document_root = current_dir().unwrap().join("tests").join("php"); 34 | let document_root = document_root.to_str().unwrap(); 35 | let script_name = current_dir() 36 | .unwrap() 37 | .join("tests") 38 | .join("php") 39 | .join("index.php"); 40 | let script_name = script_name.to_str().unwrap(); 41 | 42 | let params = Params::default() 43 | .request_method("GET") 44 | .document_root(document_root) 45 | .script_name("/index.php") 46 | .script_filename(script_name) 47 | .request_uri("/index.php") 48 | .document_uri("/index.php") 49 | .remote_addr("127.0.0.1") 50 | .remote_port(12345) 51 | .server_addr("127.0.0.1") 52 | .server_port(80) 53 | .server_name("jmjoy-pc") 54 | .content_type("") 55 | .content_length(0); 56 | 57 | let output = client 58 | .execute_once(Request::new(params, &mut io::empty())) 59 | .await 60 | .unwrap(); 61 | 62 | assert_eq!( 63 | String::from_utf8(output.stdout.unwrap_or(Default::default())).unwrap(), 64 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\nhello" 65 | ); 66 | assert_eq!(output.stderr, None); 67 | } 68 | 69 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 70 | async fn test_stream() { 71 | common::setup(); 72 | 73 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 74 | test_client_stream(Client::new(stream)).await; 75 | } 76 | 77 | async fn test_client_stream(client: Client) { 78 | let document_root = current_dir().unwrap().join("tests").join("php"); 79 | let document_root = document_root.to_str().unwrap(); 80 | let script_name = current_dir() 81 | .unwrap() 82 | .join("tests") 83 | .join("php") 84 | .join("index.php"); 85 | let script_name = script_name.to_str().unwrap(); 86 | 87 | let params = Params::default() 88 | .request_method("GET") 89 | .document_root(document_root) 90 | .script_name("/index.php") 91 | .script_filename(script_name) 92 | .request_uri("/index.php") 93 | .document_uri("/index.php") 94 | .remote_addr("127.0.0.1") 95 | .remote_port(12345) 96 | .server_addr("127.0.0.1") 97 | .server_port(80) 98 | .server_name("jmjoy-pc") 99 | .content_type("") 100 | .content_length(0); 101 | 102 | let mut stream = client 103 | .execute_once_stream(Request::new(params, &mut io::empty())) 104 | .await 105 | .unwrap(); 106 | 107 | let mut stdout = Vec::::new(); 108 | while let Some(content) = stream.next().await { 109 | let content = content.unwrap(); 110 | match content { 111 | Content::Stdout(out) => { 112 | stdout.extend_from_slice(out); 113 | } 114 | Content::Stderr(_) => { 115 | panic!("stderr should not happened"); 116 | } 117 | } 118 | } 119 | 120 | assert_eq!( 121 | String::from_utf8(stdout).unwrap(), 122 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\nhello" 123 | ); 124 | } 125 | 126 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 127 | async fn test_big_response_stream() { 128 | common::setup(); 129 | 130 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 131 | test_client_big_response_stream(Client::new(stream)).await; 132 | } 133 | 134 | async fn test_client_big_response_stream( 135 | client: Client, 136 | ) { 137 | let document_root = current_dir().unwrap().join("tests").join("php"); 138 | let document_root = document_root.to_str().unwrap(); 139 | let script_name = current_dir() 140 | .unwrap() 141 | .join("tests") 142 | .join("php") 143 | .join("big-response.php"); 144 | let script_name = script_name.to_str().unwrap(); 145 | 146 | let params = Params::default() 147 | .request_method("GET") 148 | .document_root(document_root) 149 | .script_name("/big-response.php") 150 | .script_filename(script_name) 151 | .request_uri("/big-response.php") 152 | .document_uri("/big-response.php") 153 | .remote_addr("127.0.0.1") 154 | .remote_port(12345) 155 | .server_addr("127.0.0.1") 156 | .server_port(80) 157 | .server_name("jmjoy-pc") 158 | .content_type("") 159 | .content_length(0); 160 | 161 | let mut stream = client 162 | .execute_once_stream(Request::new(params, &mut io::empty())) 163 | .await 164 | .unwrap(); 165 | 166 | let mut stdout = Vec::::new(); 167 | while let Some(content) = stream.next().await { 168 | let content = content.unwrap(); 169 | match content { 170 | Content::Stdout(out) => { 171 | stdout.extend_from_slice(out); 172 | } 173 | Content::Stderr(_) => { 174 | panic!("stderr should not happened"); 175 | } 176 | } 177 | } 178 | 179 | assert_eq!( 180 | String::from_utf8(stdout).unwrap(), 181 | format!( 182 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\n{}", 183 | ".".repeat(10000) 184 | ) 185 | ); 186 | } 187 | -------------------------------------------------------------------------------- /tests/client_multi.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use fastcgi_client::{request::Request, response::Content, Client, Params}; 16 | use std::{env::current_dir, io::Cursor}; 17 | use tokio::net::TcpStream; 18 | 19 | mod common; 20 | 21 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 22 | async fn multi() { 23 | common::setup(); 24 | 25 | let tasks = (0..3).map(|_| tokio::spawn(single())).collect::>(); 26 | for task in tasks { 27 | task.await.unwrap(); 28 | } 29 | } 30 | 31 | async fn single() { 32 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 33 | let mut client = Client::new_keep_alive(stream); 34 | 35 | let document_root = current_dir().unwrap().join("tests").join("php"); 36 | let document_root = document_root.to_str().unwrap(); 37 | let script_name = current_dir() 38 | .unwrap() 39 | .join("tests") 40 | .join("php") 41 | .join("post.php"); 42 | let script_name = script_name.to_str().unwrap(); 43 | 44 | let body = b"p1=3&p2=4"; 45 | 46 | let params = Params::default() 47 | .request_method("POST") 48 | .document_root(document_root) 49 | .script_name("/post.php") 50 | .script_filename(script_name) 51 | .request_uri("/post.php?g1=1&g2=2") 52 | .query_string("g1=1&g2=2") 53 | .document_uri("/post.php") 54 | .remote_addr("127.0.0.1") 55 | .remote_port(12345) 56 | .server_addr("127.0.0.1") 57 | .server_port(80) 58 | .server_name("jmjoy-pc") 59 | .content_type("application/x-www-form-urlencoded") 60 | .content_length(body.len()); 61 | 62 | for _ in 0..3 { 63 | let output = client 64 | .execute(Request::new(params.clone(), Cursor::new(body))) 65 | .await 66 | .unwrap(); 67 | 68 | let stdout = String::from_utf8(output.stdout.unwrap_or(Default::default())).unwrap(); 69 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 70 | assert!(stdout.contains("\r\n\r\n")); 71 | assert!(stdout.contains("1234")); 72 | 73 | let stderr = String::from_utf8(output.stderr.unwrap_or(Default::default())).unwrap(); 74 | assert!(stderr.contains("PHP message: PHP Fatal error: Uncaught Exception: TEST")); 75 | } 76 | } 77 | 78 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 79 | async fn multi_stream() { 80 | common::setup(); 81 | 82 | let tasks = (0..3) 83 | .map(|_| tokio::spawn(single_stream())) 84 | .collect::>(); 85 | for task in tasks { 86 | task.await.unwrap(); 87 | } 88 | } 89 | 90 | async fn single_stream() { 91 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 92 | let mut client = Client::new_keep_alive(stream); 93 | 94 | let document_root = current_dir().unwrap().join("tests").join("php"); 95 | let document_root = document_root.to_str().unwrap(); 96 | let script_name = current_dir() 97 | .unwrap() 98 | .join("tests") 99 | .join("php") 100 | .join("post.php"); 101 | let script_name = script_name.to_str().unwrap(); 102 | 103 | let body = b"p1=3&p2=4"; 104 | 105 | let params = Params::default() 106 | .request_method("POST") 107 | .document_root(document_root) 108 | .script_name("/post.php") 109 | .script_filename(script_name) 110 | .request_uri("/post.php?g1=1&g2=2") 111 | .query_string("g1=1&g2=2") 112 | .document_uri("/post.php") 113 | .remote_addr("127.0.0.1") 114 | .remote_port(12345) 115 | .server_addr("127.0.0.1") 116 | .server_port(80) 117 | .server_name("jmjoy-pc") 118 | .content_type("application/x-www-form-urlencoded") 119 | .content_length(body.len()); 120 | 121 | for _ in 0..3 { 122 | let mut stream = client 123 | .execute_stream(Request::new(params.clone(), Cursor::new(body))) 124 | .await 125 | .unwrap(); 126 | 127 | let mut stdout = Vec::::new(); 128 | let mut stderr = Vec::::new(); 129 | 130 | while let Some(content) = stream.next().await { 131 | let content = content.unwrap(); 132 | match content { 133 | Content::Stdout(out) => { 134 | stdout.extend_from_slice(out); 135 | } 136 | Content::Stderr(err) => { 137 | stderr.extend_from_slice(err); 138 | } 139 | } 140 | } 141 | 142 | assert!(String::from_utf8(stdout).unwrap().starts_with( 143 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\n1234
\nFatal error: Uncaught Exception: TEST in" 145 | )); 146 | assert!(String::from_utf8(stderr) 147 | .unwrap() 148 | .starts_with("PHP message: PHP Fatal error: Uncaught Exception: TEST in")); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/client_post.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use fastcgi_client::{request::Request, Client, Params}; 16 | use std::{env::current_dir, time::Duration}; 17 | use tokio::{net::TcpStream, time::timeout}; 18 | 19 | mod common; 20 | 21 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 22 | async fn post_big_body() { 23 | common::setup(); 24 | 25 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 26 | let mut client = Client::new_keep_alive(stream); 27 | 28 | let document_root = current_dir().unwrap().join("tests").join("php"); 29 | let document_root = document_root.to_str().unwrap(); 30 | let script_name = current_dir() 31 | .unwrap() 32 | .join("tests") 33 | .join("php") 34 | .join("body-size.php"); 35 | let script_name = script_name.to_str().unwrap(); 36 | 37 | let body = [0u8; 131072]; 38 | 39 | let params = Params::default() 40 | .request_method("POST") 41 | .document_root(document_root) 42 | .script_name("/body-size.php") 43 | .script_filename(script_name) 44 | .request_uri("/body-size.php") 45 | .query_string("") 46 | .document_uri("/body-size.php") 47 | .remote_addr("127.0.0.1") 48 | .remote_port(12345) 49 | .server_addr("127.0.0.1") 50 | .server_port(80) 51 | .server_name("jmjoy-pc") 52 | .content_type("text/plain") 53 | .content_length(body.len()); 54 | 55 | let output = timeout( 56 | Duration::from_secs(3), 57 | client.execute(Request::new(params.clone(), &mut &body[..])), 58 | ) 59 | .await 60 | .unwrap() 61 | .unwrap(); 62 | 63 | let stdout = String::from_utf8(output.stdout.unwrap_or(Default::default())).unwrap(); 64 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 65 | assert!(stdout.contains("\r\n\r\n")); 66 | assert!(stdout.contains("131072")); 67 | } 68 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 jmjoy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Once; 16 | use tracing::Level; 17 | use tracing_subscriber::FmtSubscriber; 18 | 19 | static START: Once = Once::new(); 20 | 21 | /// Setup function that is only run once, even if called multiple times. 22 | pub fn setup() { 23 | START.call_once(|| { 24 | let subscriber = FmtSubscriber::builder() 25 | .with_max_level(Level::DEBUG) 26 | .finish(); 27 | 28 | tracing::subscriber::set_global_default(subscriber) 29 | .expect("setting default subscriber failed"); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /tests/php/big-response.php: -------------------------------------------------------------------------------- 1 |