diff options
| author | Teddy Wing | 2020-07-04 03:55:28 +0200 | 
|---|---|---|
| committer | Teddy Wing | 2020-07-04 03:55:28 +0200 | 
| commit | 44967e5ae07fc99f56c14dc440cf795851ae117f (patch) | |
| tree | b7b7925f24b28c3de977be041e23f6260b65853d /src | |
| parent | 23f23e7ee0540c1078db038c3b3cad93b312200a (diff) | |
| parent | d125b25f4245df3d0eb80aa726c2a7039945f3de (diff) | |
| download | fastcgi-conduit-44967e5ae07fc99f56c14dc440cf795851ae117f.tar.bz2 | |
Merge branch 'conduit'
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)) +} | 
