Skip to main content

fastcgi_client/
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
21use std::{
22    borrow::Cow,
23    collections::HashMap,
24    ops::{Deref, DerefMut},
25};
26
27#[cfg(feature = "http")]
28use std::str::FromStr;
29
30#[cfg(feature = "http")]
31use crate::{HttpConversionError, HttpConversionResult};
32
33/// Fastcgi params, please reference to nginx-php-fpm fastcgi_params.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Params<'a>(HashMap<Cow<'a, str>, Cow<'a, str>>);
36
37impl<'a> Params<'a> {
38    /// Sets a custom parameter with the given key and value.
39    ///
40    /// # Arguments
41    ///
42    /// * `key` - The parameter name
43    /// * `value` - The parameter value
44    #[inline]
45    pub fn custom<K: Into<Cow<'a, str>>, S: Into<Cow<'a, str>>>(
46        mut self, key: K, value: S,
47    ) -> Self {
48        self.insert(key.into(), value.into());
49        self
50    }
51
52    /// Sets the GATEWAY_INTERFACE parameter.
53    ///
54    /// # Arguments
55    ///
56    /// * `gateway_interface` - The gateway interface version (e.g., "CGI/1.1")
57    #[inline]
58    pub fn gateway_interface<S: Into<Cow<'a, str>>>(mut self, gateway_interface: S) -> Self {
59        self.insert("GATEWAY_INTERFACE".into(), gateway_interface.into());
60        self
61    }
62
63    /// Sets the SERVER_SOFTWARE parameter.
64    ///
65    /// # Arguments
66    ///
67    /// * `server_software` - The server software name and version
68    #[inline]
69    pub fn server_software<S: Into<Cow<'a, str>>>(mut self, server_software: S) -> Self {
70        self.insert("SERVER_SOFTWARE".into(), server_software.into());
71        self
72    }
73
74    /// Sets the SERVER_PROTOCOL parameter.
75    ///
76    /// # Arguments
77    ///
78    /// * `server_protocol` - The server protocol version (e.g., "HTTP/1.1")
79    #[inline]
80    pub fn server_protocol<S: Into<Cow<'a, str>>>(mut self, server_protocol: S) -> Self {
81        self.insert("SERVER_PROTOCOL".into(), server_protocol.into());
82        self
83    }
84
85    /// Sets the REQUEST_METHOD parameter.
86    ///
87    /// # Arguments
88    ///
89    /// * `request_method` - The HTTP request method (e.g., "GET", "POST")
90    #[inline]
91    pub fn request_method<S: Into<Cow<'a, str>>>(mut self, request_method: S) -> Self {
92        self.insert("REQUEST_METHOD".into(), request_method.into());
93        self
94    }
95
96    /// Sets the SCRIPT_FILENAME parameter.
97    ///
98    /// # Arguments
99    ///
100    /// * `script_filename` - The full path to the script file
101    #[inline]
102    pub fn script_filename<S: Into<Cow<'a, str>>>(mut self, script_filename: S) -> Self {
103        self.insert("SCRIPT_FILENAME".into(), script_filename.into());
104        self
105    }
106
107    /// Sets the SCRIPT_NAME parameter.
108    ///
109    /// # Arguments
110    ///
111    /// * `script_name` - The URI part that identifies the script
112    #[inline]
113    pub fn script_name<S: Into<Cow<'a, str>>>(mut self, script_name: S) -> Self {
114        self.insert("SCRIPT_NAME".into(), script_name.into());
115        self
116    }
117
118    /// Sets the QUERY_STRING parameter.
119    ///
120    /// # Arguments
121    ///
122    /// * `query_string` - The query string part of the URL
123    #[inline]
124    pub fn query_string<S: Into<Cow<'a, str>>>(mut self, query_string: S) -> Self {
125        self.insert("QUERY_STRING".into(), query_string.into());
126        self
127    }
128
129    /// Sets the REQUEST_URI parameter.
130    ///
131    /// # Arguments
132    ///
133    /// * `request_uri` - The full request URI
134    #[inline]
135    pub fn request_uri<S: Into<Cow<'a, str>>>(mut self, request_uri: S) -> Self {
136        self.insert("REQUEST_URI".into(), request_uri.into());
137        self
138    }
139
140    /// Sets the DOCUMENT_ROOT parameter.
141    ///
142    /// # Arguments
143    ///
144    /// * `document_root` - The document root directory path
145    #[inline]
146    pub fn document_root<S: Into<Cow<'a, str>>>(mut self, document_root: S) -> Self {
147        self.insert("DOCUMENT_ROOT".into(), document_root.into());
148        self
149    }
150
151    /// Sets the DOCUMENT_URI parameter.
152    ///
153    /// # Arguments
154    ///
155    /// * `document_uri` - The document URI
156    #[inline]
157    pub fn document_uri<S: Into<Cow<'a, str>>>(mut self, document_uri: S) -> Self {
158        self.insert("DOCUMENT_URI".into(), document_uri.into());
159        self
160    }
161
162    /// Sets the REMOTE_ADDR parameter.
163    ///
164    /// # Arguments
165    ///
166    /// * `remote_addr` - The remote client IP address
167    #[inline]
168    pub fn remote_addr<S: Into<Cow<'a, str>>>(mut self, remote_addr: S) -> Self {
169        self.insert("REMOTE_ADDR".into(), remote_addr.into());
170        self
171    }
172
173    /// Sets the REMOTE_PORT parameter.
174    ///
175    /// # Arguments
176    ///
177    /// * `remote_port` - The remote client port number
178    #[inline]
179    pub fn remote_port(mut self, remote_port: u16) -> Self {
180        self.insert("REMOTE_PORT".into(), remote_port.to_string().into());
181        self
182    }
183
184    /// Sets the SERVER_ADDR parameter.
185    ///
186    /// # Arguments
187    ///
188    /// * `server_addr` - The server IP address
189    #[inline]
190    pub fn server_addr<S: Into<Cow<'a, str>>>(mut self, server_addr: S) -> Self {
191        self.insert("SERVER_ADDR".into(), server_addr.into());
192        self
193    }
194
195    /// Sets the SERVER_PORT parameter.
196    ///
197    /// # Arguments
198    ///
199    /// * `server_port` - The server port number
200    #[inline]
201    pub fn server_port(mut self, server_port: u16) -> Self {
202        self.insert("SERVER_PORT".into(), server_port.to_string().into());
203        self
204    }
205
206    /// Sets the SERVER_NAME parameter.
207    ///
208    /// # Arguments
209    ///
210    /// * `server_name` - The server name or hostname
211    #[inline]
212    pub fn server_name<S: Into<Cow<'a, str>>>(mut self, server_name: S) -> Self {
213        self.insert("SERVER_NAME".into(), server_name.into());
214        self
215    }
216
217    /// Sets the CONTENT_TYPE parameter.
218    ///
219    /// # Arguments
220    ///
221    /// * `content_type` - The content type of the request body
222    #[inline]
223    pub fn content_type<S: Into<Cow<'a, str>>>(mut self, content_type: S) -> Self {
224        self.insert("CONTENT_TYPE".into(), content_type.into());
225        self
226    }
227
228    /// Sets the CONTENT_LENGTH parameter.
229    ///
230    /// # Arguments
231    ///
232    /// * `content_length` - The length of the request body in bytes
233    #[inline]
234    pub fn content_length(mut self, content_length: usize) -> Self {
235        self.insert("CONTENT_LENGTH".into(), content_length.to_string().into());
236        self
237    }
238}
239
240impl Default for Params<'_> {
241    fn default() -> Self {
242        Params(HashMap::new())
243            .gateway_interface("FastCGI/1.0")
244            .server_software("fastcgi-client-rs")
245            .server_protocol("HTTP/1.1")
246    }
247}
248
249impl<'a> Deref for Params<'a> {
250    type Target = HashMap<Cow<'a, str>, Cow<'a, str>>;
251
252    fn deref(&self) -> &Self::Target {
253        &self.0
254    }
255}
256
257impl DerefMut for Params<'_> {
258    fn deref_mut(&mut self) -> &mut Self::Target {
259        &mut self.0
260    }
261}
262
263impl<'a> From<Params<'a>> for HashMap<Cow<'a, str>, Cow<'a, str>> {
264    fn from(params: Params<'a>) -> Self {
265        params.0
266    }
267}
268
269#[cfg(feature = "http")]
270impl<'a> TryFrom<&Params<'a>> for ::http::request::Parts {
271    type Error = HttpConversionError;
272
273    fn try_from(params: &Params<'a>) -> Result<Self, Self::Error> {
274        let method =
275            ::http::Method::from_bytes(required_param(params, "REQUEST_METHOD")?.as_bytes())?;
276        let version = params
277            .get("SERVER_PROTOCOL")
278            .map(|protocol| parse_server_protocol(protocol))
279            .transpose()?
280            .unwrap_or(::http::Version::HTTP_11);
281        let uri = build_request_uri(params)?;
282
283        let mut builder = ::http::Request::builder()
284            .method(method)
285            .uri(uri)
286            .version(version);
287        let headers = builder
288            .headers_mut()
289            .expect("request builder should provide headers");
290        for (name, value) in params_to_headers(params)? {
291            headers.append(name, value);
292        }
293        let (parts, _) = builder.body(())?.into_parts();
294        Ok(parts)
295    }
296}
297
298#[cfg(feature = "http")]
299impl<'a> TryFrom<Params<'a>> for ::http::request::Parts {
300    type Error = HttpConversionError;
301
302    fn try_from(params: Params<'a>) -> Result<Self, Self::Error> {
303        (&params).try_into()
304    }
305}
306
307#[cfg(feature = "http")]
308impl TryFrom<&::http::request::Parts> for Params<'static> {
309    type Error = HttpConversionError;
310
311    fn try_from(parts: &::http::request::Parts) -> Result<Self, Self::Error> {
312        let mut params = Params::default().request_method(parts.method.as_str().to_owned());
313
314        if let Some(path_and_query) = parts.uri.path_and_query() {
315            params = params
316                .request_uri(path_and_query.as_str().to_owned())
317                .document_uri(parts.uri.path().to_owned());
318        } else {
319            params = params.request_uri("/").document_uri("/");
320        }
321
322        if let Some(query) = parts.uri.query() {
323            params = params.query_string(query.to_owned());
324        }
325
326        params = params.server_protocol(version_to_server_protocol(parts.version));
327
328        if let Some(authority) = parts.uri.authority() {
329            params = params.custom("HTTP_HOST", authority.as_str().to_owned());
330        }
331
332        for (name, value) in &parts.headers {
333            let param_name = header_name_to_param_name(name.as_str());
334            let header_value =
335                value
336                    .to_str()
337                    .map_err(|_| HttpConversionError::MalformedHttpResponse {
338                        message: "HTTP header value is not valid ASCII/UTF-8",
339                    })?;
340            params = params.custom(param_name, header_value.to_owned());
341        }
342
343        Ok(params)
344    }
345}
346
347#[cfg(feature = "http")]
348impl TryFrom<::http::request::Parts> for Params<'static> {
349    type Error = HttpConversionError;
350
351    fn try_from(parts: ::http::request::Parts) -> Result<Self, Self::Error> {
352        (&parts).try_into()
353    }
354}
355
356#[cfg(feature = "http")]
357fn build_request_uri(params: &Params<'_>) -> HttpConversionResult<::http::Uri> {
358    let request_uri = params
359        .get("REQUEST_URI")
360        .map(|value| value.as_ref())
361        .unwrap_or("/");
362    let query = params
363        .get("QUERY_STRING")
364        .map(|value| value.as_ref())
365        .unwrap_or("");
366    let uri = if query.is_empty() || request_uri.contains('?') {
367        request_uri.to_owned()
368    } else {
369        format!("{request_uri}?{query}")
370    };
371    Ok(::http::Uri::from_str(&uri)?)
372}
373
374#[cfg(feature = "http")]
375fn parse_server_protocol(protocol: &str) -> HttpConversionResult<::http::Version> {
376    match protocol {
377        "HTTP/0.9" => Ok(::http::Version::HTTP_09),
378        "HTTP/1.0" => Ok(::http::Version::HTTP_10),
379        "HTTP/1.1" => Ok(::http::Version::HTTP_11),
380        "HTTP/2" | "HTTP/2.0" => Ok(::http::Version::HTTP_2),
381        "HTTP/3" | "HTTP/3.0" => Ok(::http::Version::HTTP_3),
382        _ => Err(HttpConversionError::MalformedHttpResponse {
383            message: "unsupported SERVER_PROTOCOL value",
384        }),
385    }
386}
387
388#[cfg(feature = "http")]
389fn version_to_server_protocol(version: ::http::Version) -> &'static str {
390    match version {
391        ::http::Version::HTTP_09 => "HTTP/0.9",
392        ::http::Version::HTTP_10 => "HTTP/1.0",
393        ::http::Version::HTTP_11 => "HTTP/1.1",
394        ::http::Version::HTTP_2 => "HTTP/2.0",
395        ::http::Version::HTTP_3 => "HTTP/3.0",
396        _ => "HTTP/1.1",
397    }
398}
399
400#[cfg(feature = "http")]
401fn required_param<'a>(params: &'a Params<'_>, name: &'static str) -> HttpConversionResult<&'a str> {
402    params
403        .get(name)
404        .map(|value| value.as_ref())
405        .ok_or(HttpConversionError::MissingFastcgiParam { name })
406}
407
408#[cfg(feature = "http")]
409fn params_to_headers(
410    params: &Params<'_>,
411) -> HttpConversionResult<Vec<(::http::header::HeaderName, ::http::header::HeaderValue)>> {
412    let mut headers = Vec::new();
413
414    for (name, value) in params.iter() {
415        let header_name = match name.as_ref() {
416            "CONTENT_TYPE" => Some(::http::header::CONTENT_TYPE),
417            "CONTENT_LENGTH" => Some(::http::header::CONTENT_LENGTH),
418            _ => name
419                .strip_prefix("HTTP_")
420                .map(|header_name| {
421                    ::http::header::HeaderName::from_bytes(
422                        param_name_to_header_name(header_name).as_bytes(),
423                    )
424                })
425                .transpose()?,
426        };
427
428        if let Some(header_name) = header_name {
429            headers.push((
430                header_name,
431                ::http::header::HeaderValue::from_str(value.as_ref())?,
432            ));
433        }
434    }
435
436    Ok(headers)
437}
438
439#[cfg(feature = "http")]
440fn param_name_to_header_name(name: &str) -> String {
441    name.chars()
442        .map(|ch| match ch {
443            '_' => '-',
444            _ => ch.to_ascii_lowercase(),
445        })
446        .collect()
447}
448
449#[cfg(feature = "http")]
450fn header_name_to_param_name(name: &str) -> String {
451    match name {
452        "content-type" => "CONTENT_TYPE".to_owned(),
453        "content-length" => "CONTENT_LENGTH".to_owned(),
454        _ => format!(
455            "HTTP_{}",
456            name.chars()
457                .map(|ch| match ch {
458                    '-' => '_',
459                    _ => ch.to_ascii_uppercase(),
460                })
461                .collect::<String>()
462        ),
463    }
464}
465
466#[cfg(all(test, feature = "http"))]
467mod http_tests {
468    use super::Params;
469
470    #[test]
471    fn params_into_http_parts() {
472        let params = Params::default()
473            .request_method("POST")
474            .request_uri("/index.php")
475            .query_string("foo=bar")
476            .content_type("application/json")
477            .content_length(3)
478            .custom("HTTP_HOST", "example.com")
479            .custom("HTTP_X_TRACE_ID", "abc");
480
481        let parts: ::http::request::Parts = (&params).try_into().unwrap();
482
483        assert_eq!(parts.method, ::http::Method::POST);
484        assert_eq!(parts.uri, "/index.php?foo=bar");
485        assert_eq!(parts.headers[::http::header::HOST], "example.com");
486        assert_eq!(parts.headers["x-trace-id"], "abc");
487        assert_eq!(
488            parts.headers[::http::header::CONTENT_TYPE],
489            "application/json"
490        );
491    }
492
493    #[test]
494    fn http_parts_into_params() {
495        let request = ::http::Request::builder()
496            .method(::http::Method::POST)
497            .uri("/submit?foo=bar")
498            .header(::http::header::HOST, "example.com")
499            .header(::http::header::CONTENT_TYPE, "text/plain")
500            .body(())
501            .unwrap();
502        let (parts, _) = request.into_parts();
503
504        let params = Params::try_from(parts).unwrap();
505
506        assert_eq!(params["REQUEST_METHOD"], "POST");
507        assert_eq!(params["REQUEST_URI"], "/submit?foo=bar");
508        assert_eq!(params["DOCUMENT_URI"], "/submit");
509        assert_eq!(params["QUERY_STRING"], "foo=bar");
510        assert_eq!(params["HTTP_HOST"], "example.com");
511        assert_eq!(params["CONTENT_TYPE"], "text/plain");
512    }
513}