diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 85 | ||||
-rw-r--r-- | src/request.rs | 235 | ||||
-rw-r--r-- | src/server.rs | 115 |
3 files changed, 355 insertions, 80 deletions
@@ -1,84 +1,9 @@ +extern crate conduit; extern crate fastcgi; extern crate http; +extern crate log; -use std::io::{BufReader, Write}; +mod request; +mod server; -use http::{Request, Response}; -use http::request; -use inflector::cases::traincase::to_train_case; - - -pub fn run<F, T>(handler: F) -where F: Fn(Request<()>) -> Response<T> + Send + Sync + 'static -{ - fastcgi::run(move |mut req| { - let r: http::request::Builder = From::from(&req); - - handler(r.body(()).unwrap()); - - let params = req.params() - .map(|(k, v)| k + ": " + &v) - .collect::<Vec<String>>() - .join("\n"); - - write!( - &mut req.stdout(), - "Content-Type: text/plain\n\n{}", - params - ) - .unwrap_or(()); - }); -} - -trait From<T>: Sized { - fn from(_: T) -> Self; -} - -impl From<&fastcgi::Request> for http::request::Builder { - fn from(request: &fastcgi::Request) -> Self { - let method = request.param("REQUEST_METHOD") - .unwrap_or("".to_owned()); - - let uri = format!( - "{}://{}{}", - request.param("REQUEST_SCHEME").unwrap_or("".to_owned()), - request.param("HTTP_HOST").unwrap_or("".to_owned()), - request.param("REQUEST_URI").unwrap_or("".to_owned()), - ); - - let mut http_request = http::request::Builder::new() - .method(&*method) - .uri(&uri); - - let headers = headers_from_params(request.params()); - for (k, v) in headers { - http_request = http_request.header(&k, &v); - } - - // TODO: Add request body - - http_request - - // let body = BufReader::new(request.stdin()); - // - // http_request.body(body) - - // HTTP_* params become headers - } -} - -fn headers_from_params(params: fastcgi::Params) -> Vec<(String, String)> { - return params - .filter(|(key, _)| key.starts_with("HTTP_")) - .map(|(key, value)| { - let mut key = key.get(5..).unwrap_or("").to_owned(); - key = key.replace("_", "-"); - key = to_train_case(&key); - - // Change _ to - - // Uppercase each word - - (key, value) - }) - .collect() -} +pub use server::Server; diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..2d90ea9 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,235 @@ +use std::io; +use std::io::Read; +use std::net::SocketAddr; + +use inflector::cases::traincase::to_train_case; + +use snafu::{ResultExt, Snafu}; + + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("{}", source))] + InvalidMethod { source: http::method::InvalidMethod }, + + #[snafu(display("{}", source))] + InvalidHeaderName { source: conduit::header::InvalidHeaderName }, + + #[snafu(display("{}", source))] + InvalidHeaderValue { source: conduit::header::InvalidHeaderValue }, + + #[snafu(display("{}", source))] + InvalidRemoteAddr { source: RemoteAddrError }, +} + +pub type RequestResult<T, E = Error> = std::result::Result<T, E>; + +#[derive(Debug, Snafu)] +pub enum RemoteAddrError { + #[snafu(display("Could not parse address {}: {}", address, source))] + AddrParseError { + address: String, + source: std::net::AddrParseError, + }, + + #[snafu(display("Could not parse port {}: {}", port, source))] + PortParseError { + port: String, + source: std::num::ParseIntError + }, +} + + +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<String>, + remote_addr: SocketAddr, + content_length: Option<u64>, + extensions: conduit::Extensions, +} + +impl<'a> FastCgiRequest<'a> { + pub fn new(request: &'a mut fastcgi::Request) -> RequestResult<Self> { + let version = Self::version(request); + let host = Self::host(request); + let method = Self::method(request).context(InvalidMethod)?; + let headers = Self::headers(request.params())?; + let path = Self::path(request); + let query = Self::query(request); + let remote_addr = Self::remote_addr(request).context(InvalidRemoteAddr)?; + 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(), + }) + } + + 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(), + } + } + + 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 + } + } + + fn host(request: &fastcgi::Request) -> String { + request.param("HTTP_HOST").unwrap_or_default() + } + + fn method( + request: &fastcgi::Request + ) -> Result<conduit::Method, http::method::InvalidMethod> { + conduit::Method::from_bytes( + request.param("REQUEST_METHOD") + .unwrap_or_default() + .as_bytes() + ) + } + + fn headers(params: fastcgi::Params) -> RequestResult<conduit::HeaderMap> { + 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) + .context(InvalidHeaderName)?, + conduit::header::HeaderValue::from_bytes(value) + .context(InvalidHeaderValue)?, + ); + } + + Ok(map) + } + + 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() + } + + fn path(request: &fastcgi::Request) -> String { + match request.param("SCRIPT_NAME") { + Some(p) => p, + None => "/".to_owned(), + } + } + + fn query(request: &fastcgi::Request) -> Option<String> { + request.param("QUERY_STRING") + } + + fn remote_addr(request: &fastcgi::Request) -> Result<SocketAddr, RemoteAddrError> { + 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 })?, + ) + ) + } + + fn content_length(request: &fastcgi::Request) -> Option<u64> { + request.param("CONTENT_LENGTH").and_then(|l| l.parse().ok()) + } +} + +impl<'a> Read for FastCgiRequest<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + 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<u64> { + 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 + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..948a470 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,115 @@ +use std::io; +use std::io::Write; + +use conduit::Handler; + +use log::error; + +use snafu::{ResultExt, Snafu}; + +use crate::request; + + +const HTTP_VERSION: &'static str = "HTTP/1.1"; + + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(context(false))] + Write { source: io::Error }, + + #[snafu(display("Couldn't build request: {}", source))] + RequestBuilder { source: request::Error }, + + #[snafu(display("Couldn't parse response: {}", source))] + ConduitResponse { source: conduit::BoxError }, +} + + +pub struct Server; + +impl Server { + pub fn start<H: Handler + 'static + Sync>(handler: H) -> io::Result<Server> { + fastcgi::run(move |mut raw_request| { + match handle_request(&mut raw_request, &handler) { + Ok(_) => (), + Err(e) => match e { + // Ignore write errors as clients will have closed the + // connection by this point. + Error::Write { .. } => error!("Write error: {}", e), + + Error::RequestBuilder { .. } => { + error!("Unable to build request: {}", e); + + internal_server_error(&mut raw_request.stdout()) + }, + Error::ConduitResponse { .. } => { + error!("Error getting response: {}", e); + + internal_server_error(&mut raw_request.stdout()) + }, + } + } + }); + + Ok(Server{}) + } +} + +fn handle_request<H>( + mut raw_request: &mut fastcgi::Request, + handler: &H, +) -> Result<(), Error> +where H: Handler + 'static + Sync +{ + let mut request = request::FastCgiRequest::new(&mut raw_request) + .context(RequestBuilder)?; + let response = handler.call(&mut request); + + let mut stdout = raw_request.stdout(); + + let (head, body) = response + .context(ConduitResponse)? + .into_parts(); + + write!( + &mut stdout, + "{} {} {}\r\n", + HTTP_VERSION, + head.status.as_str(), + head.status.canonical_reason().unwrap_or("UNKNOWN"), + )?; + + for (name, value) in head.headers.iter() { + write!(&mut stdout, "{}: ", name)?; + stdout.write(value.as_bytes())?; + stdout.write(b"\r\n")?; + } + + stdout.write(b"\r\n")?; + + match body { + conduit::Body::Static(slice) => + stdout.write(slice).map(|_| ())?, + conduit::Body::Owned(vec) => + stdout.write(&vec).map(|_| ())?, + conduit::Body::File(mut file) => + io::copy(&mut file, &mut stdout).map(|_| ())?, + }; + + Ok(()) +} + +fn internal_server_error<W: Write>(mut w: W) { + let code = conduit::StatusCode::INTERNAL_SERVER_ERROR; + + write!( + w, + "{} {} {}\r\n{}\r\n\r\n", + HTTP_VERSION, + code, + code.canonical_reason().unwrap_or_default(), + "Content-Length: 0", + ) + .unwrap_or_else(|e| error!("Write error: {}", e)) +} |