1use 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#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Params<'a>(HashMap<Cow<'a, str>, Cow<'a, str>>);
36
37impl<'a> Params<'a> {
38 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 (¶ms).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 = (¶ms).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}