├── .gitignore ├── tests ├── php │ ├── index.php │ ├── big-response.php │ ├── body-size.php │ └── post.php ├── common.rs ├── client_post.rs ├── client_multi.rs └── client_get.rs ├── .rustfmt.toml ├── .licenserc.yaml ├── src ├── lib.rs ├── conn.rs ├── request.rs ├── error.rs ├── response.rs ├── params.rs ├── client.rs └── meta.rs ├── benches ├── common.rs └── async_client_bench.rs ├── CHANGELOG.md ├── .github └── workflows │ ├── license.yml │ ├── rust.yml │ └── release-plz.yml ├── Cargo.toml ├── README.md └── LICENSE /.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 | -------------------------------------------------------------------------------- /tests/php/index.php: -------------------------------------------------------------------------------- 1 | "] 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 | bytes = "1.10.1" 28 | futures-core = { version = "0.3.31", default-features = false } 29 | futures-util = { version = "0.3.31", default-features = false } 30 | thiserror = "1.0.32" 31 | tokio = { version = "1.20.1", features = ["io-util", "sync", "time"] } 32 | tokio-util = { version = "0.7.15", features = ["io"] } 33 | tracing = "0.1.36" 34 | 35 | [dev-dependencies] 36 | tokio = { version = "1.20.1", features = ["full"] } 37 | tracing-subscriber = "0.3.15" 38 | -------------------------------------------------------------------------------- /src/conn.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 | //! Connection mode definitions for FastCGI clients. 16 | //! 17 | //! This module defines the different connection modes that can be used 18 | //! with the FastCGI client: short connection and keep-alive modes. 19 | 20 | /// Trait defining the behavior of different connection modes. 21 | pub trait Mode { 22 | /// Returns whether this mode supports keep-alive connections. 23 | fn is_keep_alive() -> bool; 24 | } 25 | 26 | /// Short connection mode. 27 | /// 28 | /// In this mode, the client establishes a new connection for each request 29 | /// and closes it after receiving the response. 30 | pub struct ShortConn; 31 | 32 | impl Mode for ShortConn { 33 | fn is_keep_alive() -> bool { 34 | false 35 | } 36 | } 37 | 38 | /// Keep alive connection mode. 39 | /// 40 | /// In this mode, the client maintains a persistent connection 41 | /// and can send multiple requests over the same connection. 42 | pub struct KeepAlive {} 43 | 44 | impl Mode for KeepAlive { 45 | fn is_keep_alive() -> bool { 46 | true 47 | } 48 | } 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-latest 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 | -------------------------------------------------------------------------------- /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 | //! FastCGI request structure and builders. 16 | //! 17 | //! This module provides the `Request` struct that encapsulates 18 | //! the parameters and stdin data for a FastCGI request. 19 | 20 | use crate::Params; 21 | use tokio::io::AsyncRead; 22 | 23 | /// FastCGI request containing parameters and stdin data. 24 | /// 25 | /// This structure represents a complete FastCGI request with all necessary 26 | /// parameters and an optional stdin stream for request body data. 27 | pub struct Request<'a, I: AsyncRead + Unpin> { 28 | pub(crate) params: Params<'a>, 29 | pub(crate) stdin: I, 30 | } 31 | 32 | impl<'a, I: AsyncRead + Unpin> Request<'a, I> { 33 | /// Creates a new FastCGI request with the given parameters and stdin. 34 | /// 35 | /// # Arguments 36 | /// 37 | /// * `params` - The FastCGI parameters 38 | /// * `stdin` - The stdin stream for request body data 39 | pub fn new(params: Params<'a>, stdin: I) -> Self { 40 | Self { params, stdin } 41 | } 42 | 43 | /// Returns a reference to the request parameters. 44 | pub fn params(&self) -> &Params<'a> { 45 | &self.params 46 | } 47 | 48 | /// Returns a mutable reference to the request parameters. 49 | pub fn params_mut(&mut self) -> &mut Params<'a> { 50 | &mut self.params 51 | } 52 | 53 | /// Returns a reference to the stdin stream. 54 | pub fn stdin(&self) -> &I { 55 | &self.stdin 56 | } 57 | 58 | /// Returns a mutable reference to the stdin stream. 59 | pub fn stdin_mut(&mut self) -> &mut I { 60 | &mut self.stdin 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.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: Release-plz 16 | 17 | permissions: 18 | pull-requests: write 19 | contents: write 20 | 21 | on: 22 | push: 23 | branches: 24 | - master 25 | 26 | jobs: 27 | 28 | # Release unpublished packages. 29 | release-plz-release: 30 | name: Release-plz release 31 | runs-on: ubuntu-latest 32 | if: ${{ github.repository_owner == 'jmjoy' }} 33 | permissions: 34 | contents: write 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | - name: Install Rust toolchain 41 | uses: dtolnay/rust-toolchain@stable 42 | - name: Run release-plz 43 | uses: release-plz/action@v0.5 44 | with: 45 | command: release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 49 | 50 | # Create a PR with the new versions and changelog, preparing the next release. 51 | release-plz-pr: 52 | name: Release-plz PR 53 | runs-on: ubuntu-latest 54 | if: ${{ github.repository_owner == 'jmjoy' }} 55 | permissions: 56 | contents: write 57 | pull-requests: write 58 | concurrency: 59 | group: release-plz-${{ github.ref }} 60 | cancel-in-progress: false 61 | steps: 62 | - name: Checkout repository 63 | uses: actions/checkout@v4 64 | with: 65 | fetch-depth: 0 66 | - name: Install Rust toolchain 67 | uses: dtolnay/rust-toolchain@stable 68 | - name: Run release-plz 69 | uses: release-plz/action@v0.5 70 | with: 71 | command: release-pr 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | .request_method("GET") 41 | .document_root(document_root) 42 | .script_name("/index.php") 43 | .script_filename(script_name) 44 | .request_uri("/index.php") 45 | .document_uri("/index.php") 46 | .remote_addr("127.0.0.1") 47 | .remote_port(12345) 48 | .server_addr("127.0.0.1") 49 | .server_port(80) 50 | .server_name("jmjoy-pc") 51 | .content_type("") 52 | .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.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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | //! Error types and result type aliases for FastCGI operations. 16 | //! 17 | //! This module defines the error types that can occur during FastCGI 18 | //! communication and provides convenient type aliases for results. 19 | 20 | use crate::meta::{ProtocolStatus, RequestType}; 21 | 22 | /// Result type alias for FastCGI client operations. 23 | pub type ClientResult = Result; 24 | 25 | /// Error types that can occur during FastCGI communication. 26 | #[derive(Debug, thiserror::Error)] 27 | pub enum ClientError { 28 | /// Wapper of `tokio::io::Error` 29 | #[error(transparent)] 30 | Io(#[from] tokio::io::Error), 31 | 32 | /// Usually not happen. 33 | #[error("Response not found of request id `{id}`")] 34 | RequestIdNotFound { 35 | /// The request ID that was not found 36 | id: u16, 37 | }, 38 | 39 | /// Usually not happen. 40 | #[error("Response not found of request id `{id}`")] 41 | ResponseNotFound { 42 | /// The request ID for which no response was found 43 | id: u16, 44 | }, 45 | 46 | /// Maybe unimplemented request type received fom response. 47 | #[error("Response not found of request id `{request_type}`")] 48 | UnknownRequestType { 49 | /// The unknown request type received 50 | request_type: RequestType, 51 | }, 52 | 53 | /// Response not complete, first is protocol status and second is app 54 | /// status, see fastcgi protocol. 55 | #[error("This app can't multiplex [CantMpxConn]; AppStatus: {app_status}")] 56 | EndRequestCantMpxConn { 57 | /// The application status code 58 | app_status: u32, 59 | }, 60 | 61 | /// Response not complete, first is protocol status and second is app 62 | /// status, see fastcgi protocol. 63 | #[error("New request rejected; too busy [OVERLOADED]; AppStatus: {app_status}")] 64 | EndRequestOverloaded { 65 | /// The application status code 66 | app_status: u32, 67 | }, 68 | 69 | /// Response not complete, first is protocol status and second is app 70 | /// status, see fastcgi protocol. 71 | #[error("Role value not known [UnknownRole]; AppStatus: {app_status}")] 72 | EndRequestUnknownRole { 73 | /// The application status code 74 | app_status: u32, 75 | }, 76 | } 77 | 78 | impl ClientError { 79 | /// Creates a new end request error based on the protocol status. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `protocol_status` - The protocol status returned by the FastCGI server 84 | /// * `app_status` - The application status code 85 | pub(crate) fn new_end_request_with_protocol_status( 86 | protocol_status: ProtocolStatus, app_status: u32, 87 | ) -> Self { 88 | match protocol_status { 89 | ProtocolStatus::CantMpxConn => ClientError::EndRequestCantMpxConn { app_status }, 90 | ProtocolStatus::Overloaded => ClientError::EndRequestOverloaded { app_status }, 91 | _ => ClientError::EndRequestUnknownRole { app_status }, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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 | use futures_util::stream::StreamExt; 20 | 21 | mod common; 22 | 23 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 24 | async fn multi() { 25 | common::setup(); 26 | 27 | let tasks = (0..3).map(|_| tokio::spawn(single())).collect::>(); 28 | for task in tasks { 29 | task.await.unwrap(); 30 | } 31 | } 32 | 33 | async fn single() { 34 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 35 | let mut client = Client::new_keep_alive(stream); 36 | 37 | let document_root = current_dir().unwrap().join("tests").join("php"); 38 | let document_root = document_root.to_str().unwrap(); 39 | let script_name = current_dir() 40 | .unwrap() 41 | .join("tests") 42 | .join("php") 43 | .join("post.php"); 44 | let script_name = script_name.to_str().unwrap(); 45 | 46 | let body = b"p1=3&p2=4"; 47 | 48 | let params = Params::default() 49 | .request_method("POST") 50 | .document_root(document_root) 51 | .script_name("/post.php") 52 | .script_filename(script_name) 53 | .request_uri("/post.php?g1=1&g2=2") 54 | .query_string("g1=1&g2=2") 55 | .document_uri("/post.php") 56 | .remote_addr("127.0.0.1") 57 | .remote_port(12345) 58 | .server_addr("127.0.0.1") 59 | .server_port(80) 60 | .server_name("jmjoy-pc") 61 | .content_type("application/x-www-form-urlencoded") 62 | .content_length(body.len()); 63 | 64 | for _ in 0..3 { 65 | let output = client 66 | .execute(Request::new(params.clone(), Cursor::new(body))) 67 | .await 68 | .unwrap(); 69 | 70 | let stdout = String::from_utf8(output.stdout.unwrap_or(Default::default())).unwrap(); 71 | assert!(stdout.contains("Content-type: text/html; charset=UTF-8")); 72 | assert!(stdout.contains("\r\n\r\n")); 73 | assert!(stdout.contains("1234")); 74 | 75 | let stderr = String::from_utf8(output.stderr.unwrap_or(Default::default())).unwrap(); 76 | assert!(stderr.contains("PHP message: PHP Fatal error: Uncaught Exception: TEST")); 77 | } 78 | } 79 | 80 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 81 | async fn multi_stream() { 82 | common::setup(); 83 | 84 | let tasks = (0..3) 85 | .map(|_| tokio::spawn(single_stream())) 86 | .collect::>(); 87 | for task in tasks { 88 | task.await.unwrap(); 89 | } 90 | } 91 | 92 | async fn single_stream() { 93 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 94 | let mut client = Client::new_keep_alive(stream); 95 | 96 | let document_root = current_dir().unwrap().join("tests").join("php"); 97 | let document_root = document_root.to_str().unwrap(); 98 | let script_name = current_dir() 99 | .unwrap() 100 | .join("tests") 101 | .join("php") 102 | .join("post.php"); 103 | let script_name = script_name.to_str().unwrap(); 104 | 105 | let body = b"p1=3&p2=4"; 106 | 107 | let params = Params::default() 108 | .request_method("POST") 109 | .document_root(document_root) 110 | .script_name("/post.php") 111 | .script_filename(script_name) 112 | .request_uri("/post.php?g1=1&g2=2") 113 | .query_string("g1=1&g2=2") 114 | .document_uri("/post.php") 115 | .remote_addr("127.0.0.1") 116 | .remote_port(12345) 117 | .server_addr("127.0.0.1") 118 | .server_port(80) 119 | .server_name("jmjoy-pc") 120 | .content_type("application/x-www-form-urlencoded") 121 | .content_length(body.len()); 122 | 123 | for _ in 0..3 { 124 | let mut stream = client 125 | .execute_stream(Request::new(params.clone(), Cursor::new(body))) 126 | .await 127 | .unwrap(); 128 | 129 | let mut stdout = Vec::::new(); 130 | let mut stderr = Vec::::new(); 131 | 132 | while let Some(content) = stream.next().await { 133 | let content = content.unwrap(); 134 | match content { 135 | Content::Stdout(out) => { 136 | stdout.extend_from_slice(&out); 137 | } 138 | Content::Stderr(err) => { 139 | stderr.extend_from_slice(&err); 140 | } 141 | } 142 | } 143 | 144 | assert!(String::from_utf8(stdout).unwrap().starts_with( 145 | "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" 147 | )); 148 | assert!(String::from_utf8(stderr) 149 | .unwrap() 150 | .starts_with("PHP message: PHP Fatal error: Uncaught Exception: TEST in")); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /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 | use futures_util::stream::StreamExt; 23 | 24 | mod common; 25 | 26 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 27 | async fn test() { 28 | common::setup(); 29 | 30 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 31 | test_client(Client::new(stream)).await; 32 | } 33 | 34 | async fn test_client(client: Client) { 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("index.php"); 42 | let script_name = script_name.to_str().unwrap(); 43 | 44 | let params = Params::default() 45 | .request_method("GET") 46 | .document_root(document_root) 47 | .script_name("/index.php") 48 | .script_filename(script_name) 49 | .request_uri("/index.php") 50 | .document_uri("/index.php") 51 | .remote_addr("127.0.0.1") 52 | .remote_port(12345) 53 | .server_addr("127.0.0.1") 54 | .server_port(80) 55 | .server_name("jmjoy-pc") 56 | .content_type("") 57 | .content_length(0); 58 | 59 | let output = client 60 | .execute_once(Request::new(params, &mut io::empty())) 61 | .await 62 | .unwrap(); 63 | 64 | assert_eq!( 65 | String::from_utf8(output.stdout.unwrap_or(Default::default())).unwrap(), 66 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\nhello" 67 | ); 68 | assert_eq!(output.stderr, None); 69 | } 70 | 71 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 72 | async fn test_stream() { 73 | common::setup(); 74 | 75 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 76 | test_client_stream(Client::new(stream)).await; 77 | } 78 | 79 | async fn test_client_stream(client: Client) { 80 | let document_root = current_dir().unwrap().join("tests").join("php"); 81 | let document_root = document_root.to_str().unwrap(); 82 | let script_name = current_dir() 83 | .unwrap() 84 | .join("tests") 85 | .join("php") 86 | .join("index.php"); 87 | let script_name = script_name.to_str().unwrap(); 88 | 89 | let params = Params::default() 90 | .request_method("GET") 91 | .document_root(document_root) 92 | .script_name("/index.php") 93 | .script_filename(script_name) 94 | .request_uri("/index.php") 95 | .document_uri("/index.php") 96 | .remote_addr("127.0.0.1") 97 | .remote_port(12345) 98 | .server_addr("127.0.0.1") 99 | .server_port(80) 100 | .server_name("jmjoy-pc") 101 | .content_type("") 102 | .content_length(0); 103 | 104 | let mut stream = client 105 | .execute_once_stream(Request::new(params, &mut io::empty())) 106 | .await 107 | .unwrap(); 108 | 109 | let mut stdout = Vec::::new(); 110 | while let Some(content) = stream.next().await { 111 | let content = content.unwrap(); 112 | match content { 113 | Content::Stdout(out) => { 114 | stdout.extend_from_slice(&out); 115 | } 116 | Content::Stderr(_) => { 117 | panic!("stderr should not happened"); 118 | } 119 | } 120 | } 121 | 122 | assert_eq!( 123 | String::from_utf8(stdout).unwrap(), 124 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\nhello" 125 | ); 126 | } 127 | 128 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 129 | async fn test_big_response_stream() { 130 | common::setup(); 131 | 132 | let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 133 | test_client_big_response_stream(Client::new(stream)).await; 134 | } 135 | 136 | async fn test_client_big_response_stream( 137 | client: Client, 138 | ) { 139 | let document_root = current_dir().unwrap().join("tests").join("php"); 140 | let document_root = document_root.to_str().unwrap(); 141 | let script_name = current_dir() 142 | .unwrap() 143 | .join("tests") 144 | .join("php") 145 | .join("big-response.php"); 146 | let script_name = script_name.to_str().unwrap(); 147 | 148 | let params = Params::default() 149 | .request_method("GET") 150 | .document_root(document_root) 151 | .script_name("/big-response.php") 152 | .script_filename(script_name) 153 | .request_uri("/big-response.php") 154 | .document_uri("/big-response.php") 155 | .remote_addr("127.0.0.1") 156 | .remote_port(12345) 157 | .server_addr("127.0.0.1") 158 | .server_port(80) 159 | .server_name("jmjoy-pc") 160 | .content_type("") 161 | .content_length(0); 162 | 163 | let mut stream = client 164 | .execute_once_stream(Request::new(params, &mut io::empty())) 165 | .await 166 | .unwrap(); 167 | 168 | let mut stdout = Vec::::new(); 169 | while let Some(content) = stream.next().await { 170 | let content = content.unwrap(); 171 | match content { 172 | Content::Stdout(out) => { 173 | stdout.extend_from_slice(&out); 174 | } 175 | Content::Stderr(_) => { 176 | panic!("stderr should not happened"); 177 | } 178 | } 179 | } 180 | 181 | assert_eq!( 182 | String::from_utf8(stdout).unwrap(), 183 | format!( 184 | "X-Powered-By: PHP/7.1.30\r\nContent-type: text/html; charset=UTF-8\r\n\r\n{}", 185 | ".".repeat(10000) 186 | ) 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /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 | //! FastCGI response types and streaming support. 16 | //! 17 | //! This module provides structures for handling FastCGI responses, 18 | //! including both complete responses and streaming responses. 19 | 20 | use std::{ 21 | fmt::{self, Debug}, 22 | pin::Pin, 23 | str, 24 | task::Poll, 25 | }; 26 | 27 | use bytes::{Bytes, BytesMut}; 28 | use futures_core::stream::Stream; 29 | use tokio::io::AsyncRead; 30 | use tokio_util::io::ReaderStream; 31 | use tracing::debug; 32 | 33 | use crate::{ 34 | meta::{EndRequestRec, Header, RequestType, HEADER_LEN}, 35 | ClientError, ClientResult, 36 | }; 37 | 38 | /// Output of FastCGI request, contains STDOUT and STDERR data. 39 | /// 40 | /// This structure represents a complete FastCGI response with 41 | /// both stdout and stderr output from the FastCGI server. 42 | #[derive(Default, Clone)] 43 | #[non_exhaustive] 44 | pub struct Response { 45 | /// The stdout output from the FastCGI server 46 | pub stdout: Option>, 47 | /// The stderr output from the FastCGI server 48 | pub stderr: Option>, 49 | } 50 | 51 | impl Debug for Response { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 53 | f.debug_struct("Response") 54 | .field("stdout", &self.stdout.as_deref().map(str::from_utf8)) 55 | .field("stderr", &self.stderr.as_deref().map(str::from_utf8)) 56 | .finish() 57 | } 58 | } 59 | 60 | /// Content type from a FastCGI response stream. 61 | /// 62 | /// This enum represents the different types of content that can be 63 | /// received from a FastCGI server during streaming. 64 | pub enum Content { 65 | /// Standard output content from the FastCGI server 66 | Stdout(Bytes), 67 | /// Standard error content from the FastCGI server 68 | Stderr(Bytes), 69 | } 70 | 71 | /// A streaming response from a FastCGI server. 72 | /// 73 | /// Generated by 74 | /// [Client::execute_once_stream](crate::client::Client::execute_once_stream) or 75 | /// [Client::execute_stream](crate::client::Client::execute_stream). 76 | /// 77 | /// This stream yields `Content` items as they are received from the server. 78 | pub struct ResponseStream { 79 | stream: ReaderStream, 80 | id: u16, 81 | eof: bool, 82 | header: Option
, 83 | buf: BytesMut, 84 | } 85 | 86 | impl ResponseStream { 87 | /// Creates a new response stream. 88 | /// 89 | /// # Arguments 90 | /// 91 | /// * `stream` - The underlying stream to read from 92 | /// * `id` - The request ID for this response 93 | #[inline] 94 | pub(crate) fn new(stream: S, id: u16) -> Self { 95 | Self { 96 | stream: ReaderStream::new(stream), 97 | id, 98 | eof: false, 99 | header: None, 100 | buf: BytesMut::new(), 101 | } 102 | } 103 | 104 | /// Reads a FastCGI header from the buffer. 105 | /// 106 | /// Returns `None` if there isn't enough data in the buffer. 107 | #[inline] 108 | fn read_header(&mut self) -> Option
{ 109 | if self.buf.len() < HEADER_LEN { 110 | return None; 111 | } 112 | let buf = self.buf.split_to(HEADER_LEN); 113 | let header = (&buf as &[u8]).try_into().expect("failed to read header"); 114 | Some(Header::new_from_buf(header)) 115 | } 116 | 117 | /// Reads content from the buffer based on the current header. 118 | /// 119 | /// Returns `None` if there isn't enough data in the buffer. 120 | #[inline] 121 | fn read_content(&mut self) -> Option { 122 | let header = self.header.as_ref().unwrap(); 123 | let block_length = header.content_length as usize + header.padding_length as usize; 124 | if self.buf.len() < block_length { 125 | return None; 126 | } 127 | let content = self.buf.split_to(header.content_length as usize); 128 | let _ = self.buf.split_to(header.padding_length as usize); 129 | self.header = None; 130 | Some(content.freeze()) 131 | } 132 | 133 | /// Processes a complete FastCGI message from the buffer. 134 | /// 135 | /// Returns `Ok(Some(Content))` if a complete message was processed, 136 | /// `Ok(None)` if more data is needed, or an error if processing failed. 137 | fn process_message(&mut self) -> Result, ClientError> { 138 | if self.buf.is_empty() { 139 | return Ok(None); 140 | } 141 | if self.header.is_none() { 142 | match self.read_header() { 143 | Some(header) => self.header = Some(header), 144 | None => return Ok(None), 145 | } 146 | } 147 | let header = self.header.as_ref().unwrap(); 148 | match header.r#type.clone() { 149 | RequestType::Stdout => { 150 | if let Some(data) = self.read_content() { 151 | return Ok(Some(Content::Stdout(data))); 152 | } 153 | } 154 | RequestType::Stderr => { 155 | if let Some(data) = self.read_content() { 156 | return Ok(Some(Content::Stderr(data))); 157 | } 158 | } 159 | RequestType::EndRequest => { 160 | let header = header.clone(); 161 | let Some(data) = self.read_content() else { 162 | return Ok(None); 163 | }; 164 | 165 | let end = EndRequestRec::new_from_buf(header, &data); 166 | debug!(id = self.id, ?end, "Receive from stream."); 167 | 168 | self.eof = true; 169 | end.end_request 170 | .protocol_status 171 | .convert_to_client_result(end.end_request.app_status)?; 172 | return Ok(None); 173 | } 174 | r#type => { 175 | self.eof = true; 176 | return Err(ClientError::UnknownRequestType { 177 | request_type: r#type, 178 | }); 179 | } 180 | } 181 | Ok(None) 182 | } 183 | } 184 | 185 | impl Stream for ResponseStream 186 | where 187 | S: AsyncRead + Unpin, 188 | { 189 | type Item = ClientResult; 190 | 191 | fn poll_next( 192 | mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, 193 | ) -> std::task::Poll> { 194 | let mut pending = false; 195 | loop { 196 | match Pin::new(&mut self.stream).poll_next(cx) { 197 | Poll::Ready(Some(Ok(data))) => { 198 | self.buf.extend_from_slice(&data); 199 | 200 | match self.process_message() { 201 | Ok(Some(data)) => return Poll::Ready(Some(Ok(data))), 202 | Ok(None) if self.eof => return Poll::Ready(None), 203 | Ok(None) => continue, 204 | Err(err) => return Poll::Ready(Some(Err(err))), 205 | } 206 | } 207 | Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err.into()))), 208 | Poll::Ready(None) => break, 209 | Poll::Pending => { 210 | pending = true; 211 | break; 212 | } 213 | } 214 | } 215 | match self.process_message() { 216 | Ok(Some(data)) => Poll::Ready(Some(Ok(data))), 217 | Ok(None) if !self.eof && pending => Poll::Pending, 218 | Ok(None) => Poll::Ready(None), 219 | Err(err) => Poll::Ready(Some(Err(err))), 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /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 | //! FastCGI parameters builder and container. 16 | //! 17 | //! This module provides the `Params` struct which acts as a builder 18 | //! for FastCGI parameters that are sent to the FastCGI server. 19 | //! It includes convenient methods for setting common CGI parameters. 20 | 21 | use std::{ 22 | borrow::Cow, 23 | collections::HashMap, 24 | ops::{Deref, DerefMut}, 25 | }; 26 | 27 | /// Fastcgi params, please reference to nginx-php-fpm fastcgi_params. 28 | #[derive(Debug, Clone, PartialEq, Eq)] 29 | pub struct Params<'a>(HashMap, Cow<'a, str>>); 30 | 31 | impl<'a> Params<'a> { 32 | /// Sets a custom parameter with the given key and value. 33 | /// 34 | /// # Arguments 35 | /// 36 | /// * `key` - The parameter name 37 | /// * `value` - The parameter value 38 | #[inline] 39 | pub fn custom>, S: Into>>( 40 | mut self, key: K, value: S, 41 | ) -> Self { 42 | self.insert(key.into(), value.into()); 43 | self 44 | } 45 | 46 | /// Sets the GATEWAY_INTERFACE parameter. 47 | /// 48 | /// # Arguments 49 | /// 50 | /// * `gateway_interface` - The gateway interface version (e.g., "CGI/1.1") 51 | #[inline] 52 | pub fn gateway_interface>>(mut self, gateway_interface: S) -> Self { 53 | self.insert("GATEWAY_INTERFACE".into(), gateway_interface.into()); 54 | self 55 | } 56 | 57 | /// Sets the SERVER_SOFTWARE parameter. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// * `server_software` - The server software name and version 62 | #[inline] 63 | pub fn server_software>>(mut self, server_software: S) -> Self { 64 | self.insert("SERVER_SOFTWARE".into(), server_software.into()); 65 | self 66 | } 67 | 68 | /// Sets the SERVER_PROTOCOL parameter. 69 | /// 70 | /// # Arguments 71 | /// 72 | /// * `server_protocol` - The server protocol version (e.g., "HTTP/1.1") 73 | #[inline] 74 | pub fn server_protocol>>(mut self, server_protocol: S) -> Self { 75 | self.insert("SERVER_PROTOCOL".into(), server_protocol.into()); 76 | self 77 | } 78 | 79 | /// Sets the REQUEST_METHOD parameter. 80 | /// 81 | /// # Arguments 82 | /// 83 | /// * `request_method` - The HTTP request method (e.g., "GET", "POST") 84 | #[inline] 85 | pub fn request_method>>(mut self, request_method: S) -> Self { 86 | self.insert("REQUEST_METHOD".into(), request_method.into()); 87 | self 88 | } 89 | 90 | /// Sets the SCRIPT_FILENAME parameter. 91 | /// 92 | /// # Arguments 93 | /// 94 | /// * `script_filename` - The full path to the script file 95 | #[inline] 96 | pub fn script_filename>>(mut self, script_filename: S) -> Self { 97 | self.insert("SCRIPT_FILENAME".into(), script_filename.into()); 98 | self 99 | } 100 | 101 | /// Sets the SCRIPT_NAME parameter. 102 | /// 103 | /// # Arguments 104 | /// 105 | /// * `script_name` - The URI part that identifies the script 106 | #[inline] 107 | pub fn script_name>>(mut self, script_name: S) -> Self { 108 | self.insert("SCRIPT_NAME".into(), script_name.into()); 109 | self 110 | } 111 | 112 | /// Sets the QUERY_STRING parameter. 113 | /// 114 | /// # Arguments 115 | /// 116 | /// * `query_string` - The query string part of the URL 117 | #[inline] 118 | pub fn query_string>>(mut self, query_string: S) -> Self { 119 | self.insert("QUERY_STRING".into(), query_string.into()); 120 | self 121 | } 122 | 123 | /// Sets the REQUEST_URI parameter. 124 | /// 125 | /// # Arguments 126 | /// 127 | /// * `request_uri` - The full request URI 128 | #[inline] 129 | pub fn request_uri>>(mut self, request_uri: S) -> Self { 130 | self.insert("REQUEST_URI".into(), request_uri.into()); 131 | self 132 | } 133 | 134 | /// Sets the DOCUMENT_ROOT parameter. 135 | /// 136 | /// # Arguments 137 | /// 138 | /// * `document_root` - The document root directory path 139 | #[inline] 140 | pub fn document_root>>(mut self, document_root: S) -> Self { 141 | self.insert("DOCUMENT_ROOT".into(), document_root.into()); 142 | self 143 | } 144 | 145 | /// Sets the DOCUMENT_URI parameter. 146 | /// 147 | /// # Arguments 148 | /// 149 | /// * `document_uri` - The document URI 150 | #[inline] 151 | pub fn document_uri>>(mut self, document_uri: S) -> Self { 152 | self.insert("DOCUMENT_URI".into(), document_uri.into()); 153 | self 154 | } 155 | 156 | /// Sets the REMOTE_ADDR parameter. 157 | /// 158 | /// # Arguments 159 | /// 160 | /// * `remote_addr` - The remote client IP address 161 | #[inline] 162 | pub fn remote_addr>>(mut self, remote_addr: S) -> Self { 163 | self.insert("REMOTE_ADDR".into(), remote_addr.into()); 164 | self 165 | } 166 | 167 | /// Sets the REMOTE_PORT parameter. 168 | /// 169 | /// # Arguments 170 | /// 171 | /// * `remote_port` - The remote client port number 172 | #[inline] 173 | pub fn remote_port(mut self, remote_port: u16) -> Self { 174 | self.insert("REMOTE_PORT".into(), remote_port.to_string().into()); 175 | self 176 | } 177 | 178 | /// Sets the SERVER_ADDR parameter. 179 | /// 180 | /// # Arguments 181 | /// 182 | /// * `server_addr` - The server IP address 183 | #[inline] 184 | pub fn server_addr>>(mut self, server_addr: S) -> Self { 185 | self.insert("SERVER_ADDR".into(), server_addr.into()); 186 | self 187 | } 188 | 189 | /// Sets the SERVER_PORT parameter. 190 | /// 191 | /// # Arguments 192 | /// 193 | /// * `server_port` - The server port number 194 | #[inline] 195 | pub fn server_port(mut self, server_port: u16) -> Self { 196 | self.insert("SERVER_PORT".into(), server_port.to_string().into()); 197 | self 198 | } 199 | 200 | /// Sets the SERVER_NAME parameter. 201 | /// 202 | /// # Arguments 203 | /// 204 | /// * `server_name` - The server name or hostname 205 | #[inline] 206 | pub fn server_name>>(mut self, server_name: S) -> Self { 207 | self.insert("SERVER_NAME".into(), server_name.into()); 208 | self 209 | } 210 | 211 | /// Sets the CONTENT_TYPE parameter. 212 | /// 213 | /// # Arguments 214 | /// 215 | /// * `content_type` - The content type of the request body 216 | #[inline] 217 | pub fn content_type>>(mut self, content_type: S) -> Self { 218 | self.insert("CONTENT_TYPE".into(), content_type.into()); 219 | self 220 | } 221 | 222 | /// Sets the CONTENT_LENGTH parameter. 223 | /// 224 | /// # Arguments 225 | /// 226 | /// * `content_length` - The length of the request body in bytes 227 | #[inline] 228 | pub fn content_length(mut self, content_length: usize) -> Self { 229 | self.insert("CONTENT_LENGTH".into(), content_length.to_string().into()); 230 | self 231 | } 232 | } 233 | 234 | impl<'a> Default for Params<'a> { 235 | fn default() -> Self { 236 | Params(HashMap::new()) 237 | .gateway_interface("FastCGI/1.0") 238 | .server_software("fastcgi-client-rs") 239 | .server_protocol("HTTP/1.1") 240 | } 241 | } 242 | 243 | impl<'a> Deref for Params<'a> { 244 | type Target = HashMap, Cow<'a, str>>; 245 | 246 | fn deref(&self) -> &Self::Target { 247 | &self.0 248 | } 249 | } 250 | 251 | impl<'a> DerefMut for Params<'a> { 252 | fn deref_mut(&mut self) -> &mut Self::Target { 253 | &mut self.0 254 | } 255 | } 256 | 257 | impl<'a> From> for HashMap, Cow<'a, str>> { 258 | fn from(params: Params<'a>) -> Self { 259 | params.0 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | //! FastCGI client implementation for async communication with FastCGI servers. 16 | //! 17 | //! This module provides the main `Client` struct that handles communication 18 | //! with FastCGI servers in both short connection and keep-alive modes. 19 | //! The client can execute requests and receive responses or response streams. 20 | 21 | use crate::{ 22 | conn::{KeepAlive, Mode, ShortConn}, 23 | meta::{BeginRequestRec, EndRequestRec, Header, ParamPairs, RequestType, Role}, 24 | params::Params, 25 | request::Request, 26 | response::ResponseStream, 27 | ClientError, ClientResult, Response, 28 | }; 29 | use std::marker::PhantomData; 30 | use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; 31 | use tracing::debug; 32 | 33 | /// I refer to nginx fastcgi implementation, found the request id is always 1. 34 | /// 35 | /// 36 | const REQUEST_ID: u16 = 1; 37 | 38 | /// Async client for handling communication between fastcgi server. 39 | pub struct Client { 40 | stream: S, 41 | _mode: PhantomData, 42 | } 43 | 44 | impl Client { 45 | /// Construct a `Client` Object with stream, such as `tokio::net::TcpStream` 46 | /// or `tokio::net::UnixStream`, under short connection mode. 47 | pub fn new(stream: S) -> Self { 48 | Self { 49 | stream, 50 | _mode: PhantomData, 51 | } 52 | } 53 | 54 | /// Send request and receive response from fastcgi server, under short 55 | /// connection mode. 56 | pub async fn execute_once( 57 | mut self, request: Request<'_, I>, 58 | ) -> ClientResult { 59 | self.inner_execute(request).await 60 | } 61 | 62 | /// Send request and receive response stream from fastcgi server, under 63 | /// short connection mode. 64 | /// 65 | /// # Examples 66 | /// 67 | /// ``` 68 | /// use fastcgi_client::{response::Content, Client, Params, Request, StreamExt}; 69 | /// use tokio::{io, net::TcpStream}; 70 | /// 71 | /// async fn stream() { 72 | /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 73 | /// let client = Client::new(stream); 74 | /// let mut stream = client 75 | /// .execute_once_stream(Request::new(Params::default(), &mut io::empty())) 76 | /// .await 77 | /// .unwrap(); 78 | /// 79 | /// while let Some(content) = stream.next().await { 80 | /// let content = content.unwrap(); 81 | /// 82 | /// match content { 83 | /// Content::Stdout(out) => todo!(), 84 | /// Content::Stderr(out) => todo!(), 85 | /// } 86 | /// } 87 | /// } 88 | /// ``` 89 | pub async fn execute_once_stream( 90 | mut self, request: Request<'_, I>, 91 | ) -> ClientResult> { 92 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 93 | Ok(ResponseStream::new(self.stream, REQUEST_ID)) 94 | } 95 | } 96 | 97 | impl Client { 98 | /// Construct a `Client` Object with stream, such as `tokio::net::TcpStream` 99 | /// or `tokio::net::UnixStream`, under keep alive connection mode. 100 | pub fn new_keep_alive(stream: S) -> Self { 101 | Self { 102 | stream, 103 | _mode: PhantomData, 104 | } 105 | } 106 | 107 | /// Send request and receive response from fastcgi server, under keep alive 108 | /// connection mode. 109 | pub async fn execute( 110 | &mut self, request: Request<'_, I>, 111 | ) -> ClientResult { 112 | self.inner_execute(request).await 113 | } 114 | 115 | /// Send request and receive response stream from fastcgi server, under 116 | /// keep alive connection mode. 117 | /// 118 | /// # Examples 119 | /// 120 | /// ``` 121 | /// use fastcgi_client::{response::Content, Client, Params, Request, StreamExt}; 122 | /// use tokio::{io, net::TcpStream}; 123 | /// 124 | /// async fn stream() { 125 | /// let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); 126 | /// let mut client = Client::new_keep_alive(stream); 127 | /// 128 | /// for _ in (0..3) { 129 | /// let mut stream = client 130 | /// .execute_stream(Request::new(Params::default(), &mut io::empty())) 131 | /// .await 132 | /// .unwrap(); 133 | /// 134 | /// while let Some(content) = stream.next().await { 135 | /// let content = content.unwrap(); 136 | /// 137 | /// match content { 138 | /// Content::Stdout(out) => todo!(), 139 | /// Content::Stderr(out) => todo!(), 140 | /// } 141 | /// } 142 | /// } 143 | /// } 144 | /// ``` 145 | pub async fn execute_stream( 146 | &mut self, request: Request<'_, I>, 147 | ) -> ClientResult> { 148 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 149 | Ok(ResponseStream::new(&mut self.stream, REQUEST_ID)) 150 | } 151 | } 152 | 153 | impl Client { 154 | /// Internal method to execute a request and return a complete response. 155 | /// 156 | /// # Arguments 157 | /// 158 | /// * `request` - The request to execute 159 | async fn inner_execute( 160 | &mut self, request: Request<'_, I>, 161 | ) -> ClientResult { 162 | Self::handle_request(&mut self.stream, REQUEST_ID, request.params, request.stdin).await?; 163 | Self::handle_response(&mut self.stream, REQUEST_ID).await 164 | } 165 | 166 | /// Handles the complete request process. 167 | /// 168 | /// # Arguments 169 | /// 170 | /// * `stream` - The stream to write to 171 | /// * `id` - The request ID 172 | /// * `params` - The request parameters 173 | /// * `body` - The request body stream 174 | async fn handle_request<'a, I: AsyncRead + Unpin>( 175 | stream: &mut S, id: u16, params: Params<'a>, mut body: I, 176 | ) -> ClientResult<()> { 177 | Self::handle_request_start(stream, id).await?; 178 | Self::handle_request_params(stream, id, params).await?; 179 | Self::handle_request_body(stream, id, &mut body).await?; 180 | Self::handle_request_flush(stream).await?; 181 | Ok(()) 182 | } 183 | 184 | /// Handles the start of a request by sending the begin request record. 185 | /// 186 | /// # Arguments 187 | /// 188 | /// * `stream` - The stream to write to 189 | /// * `id` - The request ID 190 | async fn handle_request_start(stream: &mut S, id: u16) -> ClientResult<()> { 191 | debug!(id, "Start handle request"); 192 | 193 | let begin_request_rec = 194 | BeginRequestRec::new(id, Role::Responder, ::is_keep_alive()).await?; 195 | 196 | debug!(id, ?begin_request_rec, "Send to stream."); 197 | 198 | begin_request_rec.write_to_stream(stream).await?; 199 | 200 | Ok(()) 201 | } 202 | 203 | /// Handles sending request parameters to the stream. 204 | /// 205 | /// # Arguments 206 | /// 207 | /// * `stream` - The stream to write to 208 | /// * `id` - The request ID 209 | /// * `params` - The request parameters 210 | async fn handle_request_params<'a>( 211 | stream: &mut S, id: u16, params: Params<'a>, 212 | ) -> ClientResult<()> { 213 | let param_pairs = ParamPairs::new(params); 214 | debug!(id, ?param_pairs, "Params will be sent."); 215 | 216 | Header::write_to_stream_batches( 217 | RequestType::Params, 218 | id, 219 | stream, 220 | &mut ¶m_pairs.to_content().await?[..], 221 | Some(|header| { 222 | debug!(id, ?header, "Send to stream for Params."); 223 | header 224 | }), 225 | ) 226 | .await?; 227 | 228 | Header::write_to_stream_batches( 229 | RequestType::Params, 230 | id, 231 | stream, 232 | &mut tokio::io::empty(), 233 | Some(|header| { 234 | debug!(id, ?header, "Send to stream for Params."); 235 | header 236 | }), 237 | ) 238 | .await?; 239 | 240 | Ok(()) 241 | } 242 | 243 | /// Handles sending the request body to the stream. 244 | /// 245 | /// # Arguments 246 | /// 247 | /// * `stream` - The stream to write to 248 | /// * `id` - The request ID 249 | /// * `body` - The request body stream 250 | async fn handle_request_body( 251 | stream: &mut S, id: u16, body: &mut I, 252 | ) -> ClientResult<()> { 253 | Header::write_to_stream_batches( 254 | RequestType::Stdin, 255 | id, 256 | stream, 257 | body, 258 | Some(|header| { 259 | debug!(id, ?header, "Send to stream for Stdin."); 260 | header 261 | }), 262 | ) 263 | .await?; 264 | 265 | Header::write_to_stream_batches( 266 | RequestType::Stdin, 267 | id, 268 | stream, 269 | &mut tokio::io::empty(), 270 | Some(|header| { 271 | debug!(id, ?header, "Send to stream for Stdin."); 272 | header 273 | }), 274 | ) 275 | .await?; 276 | 277 | Ok(()) 278 | } 279 | 280 | /// Flushes the stream to ensure all data is sent. 281 | /// 282 | /// # Arguments 283 | /// 284 | /// * `stream` - The stream to flush 285 | async fn handle_request_flush(stream: &mut S) -> ClientResult<()> { 286 | stream.flush().await?; 287 | 288 | Ok(()) 289 | } 290 | 291 | /// Handles reading and processing the response from the stream. 292 | /// 293 | /// # Arguments 294 | /// 295 | /// * `stream` - The stream to read from 296 | /// * `id` - The request ID to match 297 | async fn handle_response(stream: &mut S, id: u16) -> ClientResult { 298 | let mut response = Response::default(); 299 | 300 | let mut stderr = Vec::new(); 301 | let mut stdout = Vec::new(); 302 | 303 | loop { 304 | let header = Header::new_from_stream(stream).await?; 305 | if header.request_id != id { 306 | return Err(ClientError::ResponseNotFound { id }); 307 | } 308 | debug!(id, ?header, "Receive from stream."); 309 | 310 | match header.r#type { 311 | RequestType::Stdout => { 312 | stdout.extend(header.read_content_from_stream(stream).await?); 313 | } 314 | RequestType::Stderr => { 315 | stderr.extend(header.read_content_from_stream(stream).await?); 316 | } 317 | RequestType::EndRequest => { 318 | let end_request_rec = EndRequestRec::from_header(&header, stream).await?; 319 | debug!(id, ?end_request_rec, "Receive from stream."); 320 | 321 | end_request_rec 322 | .end_request 323 | .protocol_status 324 | .convert_to_client_result(end_request_rec.end_request.app_status)?; 325 | 326 | response.stdout = if stdout.is_empty() { 327 | None 328 | } else { 329 | Some(stdout) 330 | }; 331 | response.stderr = if stderr.is_empty() { 332 | None 333 | } else { 334 | Some(stderr) 335 | }; 336 | 337 | return Ok(response); 338 | } 339 | r#type => { 340 | return Err(ClientError::UnknownRequestType { 341 | request_type: r#type, 342 | }) 343 | } 344 | } 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /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 | //! Internal FastCGI protocol metadata structures and parsing. 16 | //! 17 | //! This module contains the internal structures and constants used 18 | //! for parsing and generating FastCGI protocol messages. 19 | 20 | use crate::{ 21 | error::{ClientError, ClientResult}, 22 | Params, 23 | }; 24 | use std::{ 25 | borrow::Cow, 26 | cmp::min, 27 | collections::HashMap, 28 | fmt::{self, Debug, Display}, 29 | mem::size_of, 30 | ops::{Deref, DerefMut}, 31 | }; 32 | use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; 33 | 34 | /// FastCGI protocol version 1 35 | pub(crate) const VERSION_1: u8 = 1; 36 | /// Maximum length for FastCGI content 37 | pub(crate) const MAX_LENGTH: usize = 0xffff; 38 | /// Length of FastCGI header in bytes 39 | pub(crate) const HEADER_LEN: usize = size_of::
(); 40 | 41 | /// FastCGI request types as defined in the protocol specification. 42 | #[derive(Debug, Clone)] 43 | #[repr(u8)] 44 | pub enum RequestType { 45 | /// Begin request record type 46 | BeginRequest = 1, 47 | /// Abort request record type 48 | AbortRequest = 2, 49 | /// End request record type 50 | EndRequest = 3, 51 | /// Parameters record type 52 | Params = 4, 53 | /// Stdin record type 54 | Stdin = 5, 55 | /// Stdout record type 56 | Stdout = 6, 57 | /// Stderr record type 58 | Stderr = 7, 59 | /// Data record type 60 | Data = 8, 61 | /// Get values record type 62 | GetValues = 9, 63 | /// Get values result record type 64 | GetValuesResult = 10, 65 | /// Unknown type record type 66 | UnknownType = 11, 67 | } 68 | 69 | impl RequestType { 70 | /// Converts a u8 value to RequestType. 71 | /// 72 | /// # Arguments 73 | /// 74 | /// * `u` - The numeric value to convert 75 | fn from_u8(u: u8) -> Self { 76 | match u { 77 | 1 => RequestType::BeginRequest, 78 | 2 => RequestType::AbortRequest, 79 | 3 => RequestType::EndRequest, 80 | 4 => RequestType::Params, 81 | 5 => RequestType::Stdin, 82 | 6 => RequestType::Stdout, 83 | 7 => RequestType::Stderr, 84 | 8 => RequestType::Data, 85 | 9 => RequestType::GetValues, 86 | 10 => RequestType::GetValuesResult, 87 | _ => RequestType::UnknownType, 88 | } 89 | } 90 | } 91 | 92 | impl Display for RequestType { 93 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 94 | Display::fmt(&(self.clone() as u8), f) 95 | } 96 | } 97 | 98 | #[derive(Debug, Clone)] 99 | pub(crate) struct Header { 100 | /// FastCGI protocol version 101 | pub(crate) version: u8, 102 | /// Type of the FastCGI record 103 | pub(crate) r#type: RequestType, 104 | /// Request ID for this record 105 | pub(crate) request_id: u16, 106 | /// Length of the content data 107 | pub(crate) content_length: u16, 108 | /// Length of padding data 109 | pub(crate) padding_length: u8, 110 | /// Reserved byte 111 | pub(crate) reserved: u8, 112 | } 113 | 114 | impl Header { 115 | /// Writes data to a stream in batches with proper FastCGI headers. 116 | /// 117 | /// # Arguments 118 | /// 119 | /// * `r#type` - The type of FastCGI record 120 | /// * `request_id` - The request ID 121 | /// * `writer` - The writer to write to 122 | /// * `content` - The content to write 123 | /// * `before_write` - Optional callback to modify header before writing 124 | pub(crate) async fn write_to_stream_batches( 125 | r#type: RequestType, request_id: u16, writer: &mut W, content: &mut R, 126 | before_write: Option, 127 | ) -> io::Result<()> 128 | where 129 | F: Fn(Header) -> Header, 130 | R: AsyncRead + Unpin, 131 | W: AsyncWrite + Unpin, 132 | { 133 | let mut buf: [u8; MAX_LENGTH] = [0; MAX_LENGTH]; 134 | let mut had_written = false; 135 | 136 | loop { 137 | let read = content.read(&mut buf).await?; 138 | if had_written && read == 0 { 139 | break; 140 | } 141 | 142 | let buf = &buf[..read]; 143 | let mut header = Self::new(r#type.clone(), request_id, buf); 144 | if let Some(ref f) = before_write { 145 | header = f(header); 146 | } 147 | header.write_to_stream(writer, buf).await?; 148 | 149 | had_written = true; 150 | } 151 | Ok(()) 152 | } 153 | 154 | /// Creates a new header with given parameters. 155 | /// 156 | /// # Arguments 157 | /// 158 | /// * `r#type` - The type of FastCGI record 159 | /// * `request_id` - The request ID 160 | /// * `content` - The content data 161 | fn new(r#type: RequestType, request_id: u16, content: &[u8]) -> Self { 162 | let content_length = min(content.len(), MAX_LENGTH) as u16; 163 | Self { 164 | version: VERSION_1, 165 | r#type, 166 | request_id, 167 | content_length, 168 | padding_length: (-(content_length as i16) & 7) as u8, 169 | reserved: 0, 170 | } 171 | } 172 | 173 | /// Writes the header and content to a stream. 174 | /// 175 | /// # Arguments 176 | /// 177 | /// * `writer` - The writer to write to 178 | /// * `content` - The content to write 179 | async fn write_to_stream( 180 | self, writer: &mut W, content: &[u8], 181 | ) -> io::Result<()> { 182 | let mut buf: Vec = Vec::new(); 183 | buf.push(self.version); 184 | buf.push(self.r#type as u8); 185 | buf.write_u16(self.request_id).await?; 186 | buf.write_u16(self.content_length).await?; 187 | buf.push(self.padding_length); 188 | buf.push(self.reserved); 189 | 190 | writer.write_all(&buf).await?; 191 | writer.write_all(content).await?; 192 | writer 193 | .write_all(&vec![0; self.padding_length as usize]) 194 | .await?; 195 | 196 | Ok(()) 197 | } 198 | 199 | /// Creates a new header by reading from a stream. 200 | /// 201 | /// # Arguments 202 | /// 203 | /// * `reader` - The reader to read from 204 | pub(crate) async fn new_from_stream(reader: &mut R) -> io::Result { 205 | let mut buf: [u8; HEADER_LEN] = [0; HEADER_LEN]; 206 | reader.read_exact(&mut buf).await?; 207 | 208 | Ok(Self::new_from_buf(&buf)) 209 | } 210 | 211 | /// Creates a new header from a buffer. 212 | /// 213 | /// # Arguments 214 | /// 215 | /// * `buf` - The buffer containing header data 216 | #[inline] 217 | pub(crate) fn new_from_buf(buf: &[u8; HEADER_LEN]) -> Self { 218 | Self { 219 | version: buf[0], 220 | r#type: RequestType::from_u8(buf[1]), 221 | request_id: be_buf_to_u16(&buf[2..4]), 222 | content_length: be_buf_to_u16(&buf[4..6]), 223 | padding_length: buf[6], 224 | reserved: buf[7], 225 | } 226 | } 227 | 228 | /// Reads content from a stream based on the header's content length. 229 | /// 230 | /// # Arguments 231 | /// 232 | /// * `reader` - The reader to read from 233 | pub(crate) async fn read_content_from_stream( 234 | &self, reader: &mut R, 235 | ) -> io::Result> { 236 | let mut buf = vec![0; self.content_length as usize]; 237 | reader.read_exact(&mut buf).await?; 238 | let mut padding_buf = vec![0; self.padding_length as usize]; 239 | reader.read_exact(&mut padding_buf).await?; 240 | Ok(buf) 241 | } 242 | } 243 | 244 | /// FastCGI application roles. 245 | #[derive(Debug, Clone, Copy)] 246 | #[repr(u16)] 247 | #[allow(dead_code)] 248 | pub enum Role { 249 | /// Responder role - handles requests and returns responses 250 | Responder = 1, 251 | /// Authorizer role - performs authorization checks 252 | Authorizer = 2, 253 | /// Filter role - filters data between web server and application 254 | Filter = 3, 255 | } 256 | 257 | /// Begin request record body data. 258 | #[derive(Debug)] 259 | pub(crate) struct BeginRequest { 260 | /// The role of the application 261 | pub(crate) role: Role, 262 | /// Flags byte (bit 0 = keep alive flag) 263 | pub(crate) flags: u8, 264 | /// Reserved bytes 265 | pub(crate) reserved: [u8; 5], 266 | } 267 | 268 | impl BeginRequest { 269 | /// Creates a new begin request record. 270 | /// 271 | /// # Arguments 272 | /// 273 | /// * `role` - The role of the application 274 | /// * `keep_alive` - Whether to keep the connection alive 275 | pub(crate) fn new(role: Role, keep_alive: bool) -> Self { 276 | Self { 277 | role, 278 | flags: keep_alive as u8, 279 | reserved: [0; 5], 280 | } 281 | } 282 | 283 | /// Converts the begin request to bytes. 284 | pub(crate) async fn to_content(&self) -> io::Result> { 285 | let mut buf: Vec = Vec::new(); 286 | buf.write_u16(self.role as u16).await?; 287 | buf.push(self.flags); 288 | buf.extend_from_slice(&self.reserved); 289 | Ok(buf) 290 | } 291 | } 292 | 293 | /// Complete begin request record with header and content. 294 | pub(crate) struct BeginRequestRec { 295 | /// The FastCGI header 296 | pub(crate) header: Header, 297 | /// The begin request data 298 | pub(crate) begin_request: BeginRequest, 299 | /// The serialized content 300 | pub(crate) content: Vec, 301 | } 302 | 303 | impl BeginRequestRec { 304 | /// Creates a new begin request record. 305 | /// 306 | /// # Arguments 307 | /// 308 | /// * `request_id` - The request ID 309 | /// * `role` - The role of the application 310 | /// * `keep_alive` - Whether to keep the connection alive 311 | pub(crate) async fn new(request_id: u16, role: Role, keep_alive: bool) -> io::Result { 312 | let begin_request = BeginRequest::new(role, keep_alive); 313 | let content = begin_request.to_content().await?; 314 | let header = Header::new(RequestType::BeginRequest, request_id, &content); 315 | Ok(Self { 316 | header, 317 | begin_request, 318 | content, 319 | }) 320 | } 321 | 322 | /// Writes the begin request record to a stream. 323 | /// 324 | /// # Arguments 325 | /// 326 | /// * `writer` - The writer to write to 327 | pub(crate) async fn write_to_stream( 328 | self, writer: &mut W, 329 | ) -> io::Result<()> { 330 | self.header.write_to_stream(writer, &self.content).await 331 | } 332 | } 333 | 334 | impl Debug for BeginRequestRec { 335 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { 336 | Debug::fmt( 337 | &format!( 338 | "BeginRequestRec {{header: {:?}, begin_request: {:?}}}", 339 | self.header, self.begin_request 340 | ), 341 | f, 342 | ) 343 | } 344 | } 345 | 346 | /// Parameter length encoding for FastCGI. 347 | #[derive(Debug, Clone, Copy)] 348 | pub enum ParamLength { 349 | /// Short length (0-127 bytes) 350 | Short(u8), 351 | /// Long length (128+ bytes) 352 | Long(u32), 353 | } 354 | 355 | impl ParamLength { 356 | /// Creates a new parameter length encoding. 357 | /// 358 | /// # Arguments 359 | /// 360 | /// * `length` - The length to encode 361 | pub fn new(length: usize) -> Self { 362 | if length < 128 { 363 | ParamLength::Short(length as u8) 364 | } else { 365 | let mut length = length; 366 | length |= 1 << 31; 367 | ParamLength::Long(length as u32) 368 | } 369 | } 370 | 371 | /// Converts the parameter length to bytes. 372 | pub async fn content(self) -> io::Result> { 373 | let mut buf: Vec = Vec::new(); 374 | match self { 375 | ParamLength::Short(l) => buf.push(l), 376 | ParamLength::Long(l) => buf.write_u32(l).await?, 377 | } 378 | Ok(buf) 379 | } 380 | } 381 | 382 | /// A single parameter name-value pair. 383 | #[derive(Debug)] 384 | pub struct ParamPair<'a> { 385 | /// Length of the parameter name 386 | name_length: ParamLength, 387 | /// Length of the parameter value 388 | value_length: ParamLength, 389 | /// The parameter name 390 | name_data: Cow<'a, str>, 391 | /// The parameter value 392 | value_data: Cow<'a, str>, 393 | } 394 | 395 | impl<'a> ParamPair<'a> { 396 | /// Creates a new parameter pair. 397 | /// 398 | /// # Arguments 399 | /// 400 | /// * `name` - The parameter name 401 | /// * `value` - The parameter value 402 | fn new(name: Cow<'a, str>, value: Cow<'a, str>) -> Self { 403 | let name_length = ParamLength::new(name.len()); 404 | let value_length = ParamLength::new(value.len()); 405 | Self { 406 | name_length, 407 | value_length, 408 | name_data: name, 409 | value_data: value, 410 | } 411 | } 412 | 413 | /// Writes the parameter pair to a stream. 414 | /// 415 | /// # Arguments 416 | /// 417 | /// * `writer` - The writer to write to 418 | async fn write_to_stream(&self, writer: &mut W) -> io::Result<()> { 419 | writer.write_all(&self.name_length.content().await?).await?; 420 | writer 421 | .write_all(&self.value_length.content().await?) 422 | .await?; 423 | writer.write_all(self.name_data.as_bytes()).await?; 424 | writer.write_all(self.value_data.as_bytes()).await?; 425 | Ok(()) 426 | } 427 | } 428 | 429 | /// Collection of parameter pairs. 430 | #[derive(Debug)] 431 | pub(crate) struct ParamPairs<'a>(Vec>); 432 | 433 | impl<'a> ParamPairs<'a> { 434 | /// Creates parameter pairs from a Params object. 435 | /// 436 | /// # Arguments 437 | /// 438 | /// * `params` - The parameters to convert 439 | pub(crate) fn new(params: Params<'a>) -> Self { 440 | let mut param_pairs = Vec::new(); 441 | let params: HashMap, Cow<'a, str>> = params.into(); 442 | for (name, value) in params.into_iter() { 443 | let param_pair = ParamPair::new(name, value); 444 | param_pairs.push(param_pair); 445 | } 446 | 447 | Self(param_pairs) 448 | } 449 | 450 | /// Converts the parameter pairs to bytes. 451 | pub(crate) async fn to_content(&self) -> io::Result> { 452 | let mut buf: Vec = Vec::new(); 453 | 454 | for param_pair in self.iter() { 455 | param_pair.write_to_stream(&mut buf).await?; 456 | } 457 | 458 | Ok(buf) 459 | } 460 | } 461 | 462 | impl<'a> Deref for ParamPairs<'a> { 463 | type Target = Vec>; 464 | 465 | fn deref(&self) -> &Self::Target { 466 | &self.0 467 | } 468 | } 469 | 470 | impl<'a> DerefMut for ParamPairs<'a> { 471 | fn deref_mut(&mut self) -> &mut Self::Target { 472 | &mut self.0 473 | } 474 | } 475 | 476 | /// FastCGI protocol status codes. 477 | #[derive(Debug)] 478 | #[repr(u8)] 479 | pub enum ProtocolStatus { 480 | /// Request completed successfully 481 | RequestComplete = 0, 482 | /// This app can't multiplex connections 483 | CantMpxConn = 1, 484 | /// New request rejected; too busy 485 | Overloaded = 2, 486 | /// Role value not known 487 | UnknownRole = 3, 488 | } 489 | 490 | impl ProtocolStatus { 491 | /// Converts a u8 value to ProtocolStatus. 492 | /// 493 | /// # Arguments 494 | /// 495 | /// * `u` - The numeric value to convert 496 | pub fn from_u8(u: u8) -> Self { 497 | match u { 498 | 0 => ProtocolStatus::RequestComplete, 499 | 1 => ProtocolStatus::CantMpxConn, 500 | 2 => ProtocolStatus::Overloaded, 501 | _ => ProtocolStatus::UnknownRole, 502 | } 503 | } 504 | 505 | /// Converts the protocol status to a client result. 506 | /// 507 | /// # Arguments 508 | /// 509 | /// * `app_status` - The application status code 510 | pub(crate) fn convert_to_client_result(self, app_status: u32) -> ClientResult<()> { 511 | match self { 512 | ProtocolStatus::RequestComplete => Ok(()), 513 | _ => Err(ClientError::new_end_request_with_protocol_status( 514 | self, app_status, 515 | )), 516 | } 517 | } 518 | } 519 | 520 | /// End request record body data. 521 | #[derive(Debug)] 522 | pub struct EndRequest { 523 | /// The application status code 524 | pub(crate) app_status: u32, 525 | /// The protocol status 526 | pub(crate) protocol_status: ProtocolStatus, 527 | /// Reserved bytes 528 | #[allow(dead_code)] 529 | reserved: [u8; 3], 530 | } 531 | 532 | /// Complete end request record with header and content. 533 | #[derive(Debug)] 534 | pub(crate) struct EndRequestRec { 535 | /// The FastCGI header 536 | #[allow(dead_code)] 537 | header: Header, 538 | /// The end request data 539 | pub(crate) end_request: EndRequest, 540 | } 541 | 542 | impl EndRequestRec { 543 | /// Creates an end request record from a header and reader. 544 | /// 545 | /// # Arguments 546 | /// 547 | /// * `header` - The FastCGI header 548 | /// * `reader` - The reader to read content from 549 | pub(crate) async fn from_header( 550 | header: &Header, reader: &mut R, 551 | ) -> io::Result { 552 | let header = header.clone(); 553 | let content = &*header.read_content_from_stream(reader).await?; 554 | Ok(Self::new_from_buf(header, content)) 555 | } 556 | 557 | /// Creates an end request record from a header and buffer. 558 | /// 559 | /// # Arguments 560 | /// 561 | /// * `header` - The FastCGI header 562 | /// * `buf` - The buffer containing the end request data 563 | pub(crate) fn new_from_buf(header: Header, buf: &[u8]) -> Self { 564 | let app_status = u32::from_be_bytes(<[u8; 4]>::try_from(&buf[0..4]).unwrap()); 565 | let protocol_status = 566 | ProtocolStatus::from_u8(u8::from_be_bytes(<[u8; 1]>::try_from(&buf[4..5]).unwrap())); 567 | let reserved = <[u8; 3]>::try_from(&buf[5..8]).unwrap(); 568 | Self { 569 | header, 570 | end_request: EndRequest { 571 | app_status, 572 | protocol_status, 573 | reserved, 574 | }, 575 | } 576 | } 577 | } 578 | 579 | /// Converts big-endian bytes to u16. 580 | /// 581 | /// # Arguments 582 | /// 583 | /// * `buf` - The buffer containing the bytes 584 | fn be_buf_to_u16(buf: &[u8]) -> u16 { 585 | u16::from_be_bytes(<[u8; 2]>::try_from(buf).unwrap()) 586 | } 587 | --------------------------------------------------------------------------------