Skip to main content

fastcgi_client/
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#[cfg(feature = "http")]
21use std::{borrow::Cow, collections::HashMap};
22
23use crate::{Params, io::AsyncRead};
24
25#[cfg(feature = "http")]
26use crate::{HttpConversionError, HttpConversionResult};
27
28#[cfg(feature = "tokio")]
29use crate::io::{TokioAsyncReadCompatExt, TokioCompat};
30
31/// FastCGI request containing parameters and stdin data.
32///
33/// This structure represents a complete FastCGI request with all necessary
34/// parameters and an optional stdin stream for request body data.
35pub struct Request<'a, I: AsyncRead + Unpin> {
36    pub(crate) params: Params<'a>,
37    pub(crate) stdin: I,
38}
39
40impl<'a, I: AsyncRead + Unpin> Request<'a, I> {
41    /// Creates a new FastCGI request with the given parameters and stdin.
42    ///
43    /// # Arguments
44    ///
45    /// * `params` - The FastCGI parameters
46    /// * `stdin` - The stdin stream for request body data
47    pub fn new(params: Params<'a>, stdin: I) -> Self {
48        Self { params, stdin }
49    }
50
51    /// Returns a reference to the request parameters.
52    pub fn params(&self) -> &Params<'a> {
53        &self.params
54    }
55
56    /// Returns a mutable reference to the request parameters.
57    pub fn params_mut(&mut self) -> &mut Params<'a> {
58        &mut self.params
59    }
60
61    /// Returns a reference to the stdin stream.
62    pub fn stdin(&self) -> &I {
63        &self.stdin
64    }
65
66    /// Returns a mutable reference to the stdin stream.
67    pub fn stdin_mut(&mut self) -> &mut I {
68        &mut self.stdin
69    }
70
71    /// Converts a FastCGI request into an `http::Request` without buffering the
72    /// body.
73    #[cfg(feature = "http")]
74    pub fn try_into_http(self) -> HttpConversionResult<::http::Request<I>> {
75        self.try_into()
76    }
77}
78
79#[cfg(feature = "http")]
80impl<I> Request<'static, I>
81where
82    I: AsyncRead + Unpin,
83{
84    /// Builds a FastCGI request from an `http::Request`, merging
85    /// caller-provided FastCGI extras such as `SCRIPT_FILENAME`.
86    pub fn try_from_http_with<'a>(
87        request: ::http::Request<I>, extras: Params<'a>,
88    ) -> HttpConversionResult<Self> {
89        let (parts, body) = request.into_parts();
90        let mut params: Params<'static> = parts.try_into()?;
91        for (name, value) in HashMap::<Cow<'a, str>, Cow<'a, str>>::from(extras) {
92            params.insert(
93                Cow::Owned(name.into_owned()),
94                Cow::Owned(value.into_owned()),
95            );
96        }
97        Ok(Request::new(params, body))
98    }
99
100    /// Builds a FastCGI request from an `http::Request` using only
101    /// HTTP-representable metadata.
102    pub fn try_from_http(request: ::http::Request<I>) -> HttpConversionResult<Self> {
103        Self::try_from_http_with(request, Params::default())
104    }
105}
106
107#[cfg(feature = "http")]
108impl<'a, I> TryFrom<Request<'a, I>> for ::http::Request<I>
109where
110    I: AsyncRead + Unpin,
111{
112    type Error = HttpConversionError;
113
114    fn try_from(request: Request<'a, I>) -> Result<Self, Self::Error> {
115        let (parts, body) = ((&request.params).try_into()?, request.stdin);
116        Ok(::http::Request::from_parts(parts, body))
117    }
118}
119
120#[cfg(feature = "tokio")]
121impl<'a, I> Request<'a, TokioCompat<I>>
122where
123    I: tokio::io::AsyncRead + Unpin,
124{
125    /// Creates a new FastCGI request from a Tokio reader.
126    ///
127    /// Requires the optional `tokio` feature; it merely wraps the reader with
128    /// [`tokio_util::compat`](crate::io::TokioAsyncReadCompatExt) before
129    /// delegating to [`Request::new`].
130    pub fn new_tokio(params: Params<'a>, stdin: I) -> Self {
131        Self::new(params, stdin.compat())
132    }
133}
134
135#[cfg(all(test, feature = "http"))]
136mod http_tests {
137    use crate::{Params, Request, io};
138
139    #[test]
140    fn request_from_http_with_extras() {
141        let request = ::http::Request::builder()
142            .method(::http::Method::POST)
143            .uri("/submit?foo=bar")
144            .header(::http::header::HOST, "example.com")
145            .body(io::Cursor::new(b"body".to_vec()))
146            .unwrap();
147
148        let extras = Params::default()
149            .script_filename("/srv/www/index.php")
150            .script_name("/index.php");
151        let request = Request::try_from_http_with(request, extras).unwrap();
152
153        assert_eq!(request.params()["REQUEST_METHOD"], "POST");
154        assert_eq!(request.params()["QUERY_STRING"], "foo=bar");
155        assert_eq!(request.params()["HTTP_HOST"], "example.com");
156        assert_eq!(request.params()["SCRIPT_FILENAME"], "/srv/www/index.php");
157    }
158
159    #[test]
160    fn request_into_http_preserves_stream_body() {
161        let params = Params::default()
162            .request_method("GET")
163            .request_uri("/index.php");
164        let request = Request::new(params, io::empty());
165
166        let http_request = request.try_into_http().unwrap();
167
168        assert_eq!(http_request.method(), ::http::Method::GET);
169        assert_eq!(http_request.uri(), "/index.php");
170    }
171}