// Copyright (c) 2020 Teddy Wing // // This file is part of FastCGI-Conduit. // // FastCGI-Conduit is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // FastCGI-Conduit is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with FastCGI-Conduit. If not, see . use std::io; use std::io::Read; use std::net::SocketAddr; use inflector::cases::traincase::to_train_case; use snafu::{ResultExt, Snafu}; /// Errors parsing a FastCGI request. #[derive(Debug, Snafu)] pub enum Error { /// The HTTP method is invalid. #[snafu(context(false))] InvalidMethod { source: http::method::InvalidMethod }, /// An invalid HTTP header name. #[snafu(context(false))] InvalidHeaderName { source: conduit::header::InvalidHeaderName }, /// An invalid HTTP header value. #[snafu(context(false))] InvalidHeaderValue { source: conduit::header::InvalidHeaderValue }, /// An invalid remote address. #[snafu(context(false))] InvalidRemoteAddr { source: RemoteAddrError }, } /// A convenience `Result` that contains a request `Error`. pub type RequestResult = std::result::Result; /// Errors parsing an HTTP remote address. #[derive(Debug, Snafu)] pub enum RemoteAddrError { /// Error parsing the address part. #[snafu(display("Could not parse address {}: {}", address, source))] AddrParseError { address: String, source: std::net::AddrParseError, }, /// Error parsing the port part. #[snafu(display("Could not parse port {}: {}", port, source))] PortParseError { port: String, source: std::num::ParseIntError }, } /// Wraps a [`fastcgi::Request`][fastcgi::Request] to implement /// [`conduit::RequestExt`][conduit::RequestExt]. /// /// [fastcgi::Request]: ../../fastcgi/struct.Request.html /// [conduit::RequestExt]: ../../conduit/trait.RequestExt.html pub struct FastCgiRequest<'a> { request: &'a mut fastcgi::Request, http_version: conduit::Version, host: String, method: conduit::Method, headers: conduit::HeaderMap, path: String, query: Option, remote_addr: SocketAddr, content_length: Option, extensions: conduit::Extensions, } impl<'a> FastCgiRequest<'a> { /// Create a new `FastCgiRequest`. pub fn new(request: &'a mut fastcgi::Request) -> RequestResult { let version = Self::version(request); let host = Self::host(request); let method = Self::method(request)?; let headers = Self::headers(request.params())?; let path = Self::path(request); let query = Self::query(request); let remote_addr = Self::remote_addr(request)?; let content_length = Self::content_length(request); Ok(Self { request: request, http_version: version, host: host, method: method, headers: headers, path: path, query: query, remote_addr: remote_addr, content_length: content_length, extensions: conduit::TypeMap::new(), }) } /// Extract the HTTP version. fn version(request: &fastcgi::Request) -> conduit::Version { match request.param("SERVER_PROTOCOL").unwrap_or_default().as_str() { "HTTP/0.9" => conduit::Version::HTTP_09, "HTTP/1.0" => conduit::Version::HTTP_10, "HTTP/1.1" => conduit::Version::HTTP_11, "HTTP/2.0" => conduit::Version::HTTP_2, "HTTP/3.0" => conduit::Version::HTTP_3, _ => conduit::Version::default(), } } /// Get the request scheme (HTTP or HTTPS). fn scheme(&self) -> conduit::Scheme { let scheme = self.request.param("REQUEST_SCHEME").unwrap_or_default(); if scheme == "https" { conduit::Scheme::Https } else { conduit::Scheme::Http } } /// Get the HTTP host. /// /// This looks like `localhost:8000`. fn host(request: &fastcgi::Request) -> String { request.param("HTTP_HOST").unwrap_or_default() } /// Get the HTTP method (GET, HEAD, POST, etc.). fn method( request: &fastcgi::Request ) -> Result { conduit::Method::from_bytes( request.param("REQUEST_METHOD") .unwrap_or_default() .as_bytes() ) } /// Build a map of request headers. fn headers(params: fastcgi::Params) -> RequestResult { let mut map = conduit::HeaderMap::new(); let headers = Self::headers_from_params(params); for (name, value) in headers .iter() .map(|(name, value)| (name.as_bytes(), value.as_bytes())) { map.append( conduit::header::HeaderName::from_bytes(name)?, conduit::header::HeaderValue::from_bytes(value)?, ); } Ok(map) } /// Extract headers from request params. Transform these into pairs of /// canonical header names and values. fn headers_from_params(params: fastcgi::Params) -> Vec<(String, String)> { return params .filter(|(key, _)| key.starts_with("HTTP_")) .map(|(key, value)| { let key = key.get(5..).unwrap_or_default(); let key = &key.replace("_", "-"); let key = &to_train_case(&key); (key.to_owned(), value) }) .collect() } /// Get the URI path. /// /// Returns `/path` when the URI is `http://localhost:8000/path?s=query`. /// When the path is empty, returns `/`. fn path(request: &fastcgi::Request) -> String { match request.param("SCRIPT_NAME") { Some(p) => p, None => "/".to_owned(), } } /// Get the URI query string. /// /// Returns `s=query&lang=en` when the URI is /// `http://localhost:8000/path?s=query&lang=en`. fn query(request: &fastcgi::Request) -> Option { request.param("QUERY_STRING") } /// Get the remote address of the request. fn remote_addr(request: &fastcgi::Request) -> Result { let addr = request.param("REMOTE_ADDR").unwrap_or_default(); let port = request.param("REMOTE_PORT").unwrap_or_default(); Ok( SocketAddr::new( addr.parse().context(AddrParseError { address: addr })?, port.parse().context(PortParseError { port })?, ) ) } /// Get the request's content length. fn content_length(request: &fastcgi::Request) -> Option { request.param("CONTENT_LENGTH").and_then(|l| l.parse().ok()) } } impl<'a> Read for FastCgiRequest<'a> { /// Read from the underlying FastCGI request's [`Stdin`][Stdin] /// /// [Stdin]: ../../fastcgi/struct.Stdin.html fn read(&mut self, buf: &mut [u8]) -> io::Result { self.request.stdin().read(buf) } } impl<'a> conduit::RequestExt for FastCgiRequest<'a> { fn http_version(&self) -> conduit::Version { self.http_version } fn method(&self) -> &conduit::Method { &self.method } fn scheme(&self) -> conduit::Scheme { self.scheme() } fn host(&self) -> conduit::Host<'_> { conduit::Host::Name(&self.host) } fn virtual_root(&self) -> std::option::Option<&str> { None } fn path(&self) -> &str { &self.path } fn query_string(&self) -> std::option::Option<&str> { self.query.as_ref() .map(|p| p.as_str()) } fn remote_addr(&self) -> std::net::SocketAddr { self.remote_addr } fn content_length(&self) -> std::option::Option { self.content_length } fn headers(&self) -> &conduit::HeaderMap { &self.headers } fn body(&mut self) -> &mut (dyn std::io::Read) { self } fn extensions(&self) -> &conduit::Extensions { &self.extensions } fn mut_extensions(&mut self) -> &mut conduit::Extensions { &mut self.extensions } }