tortuga 0.7.2

A Rust-based CGI-specific HTTP web server.
Documentation
use crate::context::{ClientContext, ServerContext};
use crate::server::{self, request::CgiRequest, response::CgiResponse};
use http::uri::PathAndQuery;
use http::{HeaderValue, Method, Request, Response, StatusCode};
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::service::Service;
use server::handler::CgiHandler;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::AsyncReadExt;

#[derive(Clone)]
pub struct Router {
    server: Arc<ServerContext>,
    client: Arc<ClientContext>,
}

impl Router {
    pub fn new(server: Arc<ServerContext>, client: Arc<ClientContext>) -> Self {
        Self { server, client }
    }

    pub async fn route(
        self,
        request: Request<Incoming>,
    ) -> Result<Response<Full<Bytes>>, http::Error> {
        let request = match request.buffer().await {
            Ok(value) => value,
            Err(value) => return Ok(value),
        };

        let ignore_body = request.method() == Method::HEAD;
        let result = match (request.method(), request.uri().path()) {
            (_, path) if path.starts_with("/cgi-bin/") => self.invoke_cgi(request).await,
            (method, path) => self.load_file(method, path).await,
        };

        match result {
            Ok(mut response) => {
                if ignore_body {
                    *response.body_mut() = Full::default();
                }

                Ok(response)
            }
            Err(e) if e.kind() == io::ErrorKind::NotFound => Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Full::default()),
            Err(e) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Full::from(e.to_string())),
        }
    }

    async fn invoke_cgi(&self, mut request: Request<Bytes>) -> io::Result<Response<Full<Bytes>>> {
        for _ in 0..10 {
            let handler = CgiHandler::new(self.server.clone(), self.client.clone());

            let mut response = handler.serve(request.clone()).await?;

            if response.is_document() || response.is_client_redirect_with_document() {
                return Ok(response);
            } else if response.is_local_redirect() {
                let mut parts = request.uri().clone().into_parts();

                let location = response
                    .headers()
                    .get(http::header::LOCATION)
                    .map(HeaderValue::as_bytes)
                    .ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidInput, "Missing location header.")
                    })?;
                let path_and_query = PathAndQuery::try_from(location).map_err(|_| {
                    io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "Invalid path and query in location header.",
                    )
                })?;

                parts.path_and_query = Some(path_and_query);

                *request.uri_mut() = http::Uri::from_parts(parts).map_err(|_| {
                    io::Error::new(io::ErrorKind::InvalidInput, "Invalid URI parts.")
                })?;

                continue;
            } else if response.is_client_redirect() {
                *response.status_mut() = StatusCode::FOUND;
                return Ok(response);
            } else {
                return Err(io::Error::from(io::ErrorKind::Unsupported));
            }
        }

        Err(io::Error::new(
            io::ErrorKind::Unsupported,
            "Redirect loop detected.",
        ))
    }

    async fn load_file(&self, method: &Method, path: &str) -> io::Result<Response<Full<Bytes>>> {
        let file_path = self.server.resolve_path(path);

        if file_path.extension() == Some("cgi".as_ref()) {
            let mut response = Response::new(Full::default());
            *response.status_mut() = StatusCode::FORBIDDEN;
            return Ok(response);
        }

        match *method {
            Method::HEAD => {
                let file = File::open(file_path).await?;
                let metadata = file.metadata().await?;
                let mut response = Response::new(Full::default());

                response.headers_mut().insert(
                    http::header::CONTENT_LENGTH,
                    HeaderValue::from(metadata.len()),
                );

                Ok(response)
            }
            Method::GET => {
                let mut file = File::open(file_path).await?;

                let metadata = file.metadata().await?;
                let length = usize::try_from(metadata.len()).map_err(|_| {
                    io::Error::new(
                        io::ErrorKind::OutOfMemory,
                        "Unable to buffer file contents.",
                    )
                })?;

                let mut buffer = Vec::with_capacity(length);

                file.read_to_end(&mut buffer).await?;

                let mut response = Response::new(Full::from(buffer));

                response.headers_mut().insert(
                    http::header::CONTENT_LENGTH,
                    HeaderValue::from(metadata.len()),
                );

                Ok(response)
            }
            _ => {
                let mut response = Response::new(Full::default());
                *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
                Ok(response)
            }
        }
    }
}

impl Service<Request<Incoming>> for Router {
    type Response = Response<Full<Bytes>>;
    type Error = http::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn call(&self, request: Request<Incoming>) -> Self::Future {
        Box::pin(self.clone().route(request))
    }
}