gempress 0.1.1

An Express.js inspired server framework for the gemini protocol
Documentation
extern crate native_tls;

mod error;
pub mod gemini;
mod io;
mod logger;

use std::io::Read;

use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::Arc;

use error::{GempressError, GempressResult};
use native_tls::{TlsAcceptor, TlsStream};

/// Configuration for a Gempress server.
#[derive(Clone, Debug)]
pub struct Config {
    // Path to the identity file
    identityPath: PathBuf,
    password: String,
}

impl Config {
    /// Create a new Gempress config by loading a TLS certificate at the given file path.
    ///
    /// To generate a self-signed certificate, you can execute the following commands (substitute
    /// `localhost` with your hostname, if applicable):
    ///
    /// ```
    /// openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'
    /// openssl pkcs12 -export -out identity.pfx -inkey key.pem -in cert.pem
    ///
    /// rm key.pem cert.pem # Cleanup
    /// ```
    ///
    ///
    /// # Examples
    ///
    /// ```
    ///     let config = gempress::Config::from_identity(PathBuf::from("identity.pfx"), "password".into());
    ///     let mut app = Gempress::new(config);
    /// ```
    pub fn from_identity(identityPath: PathBuf, password: String) -> Self {
        Self {
            identityPath,
            password,
        }
    }
}

/// A function handler
///
/// # Examples
///
/// ```
/// use gempress::Gempress;
/// use gempress::gemini;
/// use std::path::PathBuf;
///
/// let config = gempress::Config::from_identity(PathBuf::from("identity.pfx"), "password".into());
/// let mut app = Gempress::new(config);
///
/// // Define a function handler
/// fn index_handler(req: Box<gemini::Request>, mut res: Box<gemini::Response>) {
///     res.send("Hello from index route!".as_bytes());
/// }
///
/// // Apply function handler to path
/// app.on("/foo", &index_handler);
///
/// app.listen(1965, || {
///     println!("Listening on port 1965");
/// })
/// .unwrap();
/// ```
pub type Handler = dyn Fn(Box<gemini::Request>, Box<gemini::Response>);

struct Layer {
    handler: Box<Handler>,
    path: String,
}

impl std::fmt::Debug for Layer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Layer").field("path", &self.path).finish()
    }
}

/// A Gempress server
pub struct Gempress {
    pub config: Config,

    stack: Vec<Layer>,
}

impl Gempress {
    /// Create a new Gempress server with the given config
    ///
    /// # Examples
    ///
    /// ```
    ///     let config = gempress::Config::from_identity(PathBuf::from("identity.pfx"), "password".into());
    ///     let mut app = Gempress::new(config);
    /// ```
    pub fn new(config: Config) -> Self {
        Gempress {
            config,
            stack: Vec::new(),
        }
    }

    /// Registers a new route handler to a given path
    ///
    /// # Examples
    ///
    /// ```
    ///     use gempress::gemini;
    ///
    ///     let config = gempress::Config::from_identity(PathBuf::from("identity.pfx"), "password".into());
    ///     let mut app = Gempress::new(config);
    ///
    ///     fn index_handler(req: Box<gemini::Request>, mut res: Box<gemini::Response>) {
    ///         res.send("Hello from index route!".as_bytes());
    ///     }
    ///
    ///     fn foo_handler(req: Box<gemini::Request>, mut res: Box<gemini::Response>) {
    ///         res.send("This is the /foo route".as_bytes());
    ///     }
    ///
    ///     app.on("/", &index_handler);
    ///     app.on("/foo", &foo_handler);
    ///
    ///     app.listen(1965, || {
    ///         println!("Listening on port 1965");
    ///     })
    ///     .unwrap();
    /// ```
    pub fn on(&mut self, path: &str, handler: &'static Handler) {
        let layer = Layer {
            path: path.to_string(),
            handler: Box::new(handler.to_owned()),
        };
        self.stack.push(layer);
    }

    /// Bind the server to a network port, then execute the callback
    pub fn listen<F: Fn()>(self, port: u16, callback: F) -> GempressResult<()> {
        // Read certificate
        // TODO: Can a password be optional?
        let identity = io::load_cert(
            &self.config.identityPath.to_str().unwrap_or(""),
            &self.config.password,
        )?;

        let address = format!("0.0.0.0:{}", port);
        let listener = TcpListener::bind(address).map_err(GempressError::BindFailed)?;
        let acceptor = TlsAcceptor::new(identity).unwrap();
        let acceptor = Arc::new(acceptor);

        logger::info(format!("Listening on port {}", port));

        for stream in listener.incoming() {
            match stream {
                Ok(stream) => {
                    let acceptor = acceptor.clone();

                    match acceptor.accept(stream) {
                        Ok(stream) => {
                            if let Err(e) = self.handle_client(stream) {
                                logger::error(format!("Can't handle client: {}", e));
                            }
                        }
                        Err(e) => {
                            logger::error(format!("Can't handle stream: {}", e));
                        }
                    };
                }
                Err(err) => logger::error(err),
            }
        }

        (callback)();

        Ok(())
    }

    fn handle_client(&self, mut stream: TlsStream<TcpStream>) -> GempressResult<()> {
        let mut buffer = [0; 1024];

        stream
            .read(&mut buffer)
            .map_err(GempressError::StreamReadFailed)?;

        let raw_request = String::from_utf8(buffer.to_vec())?;

        let request = gemini::Request::parse(&raw_request)?;
        let response = gemini::Response::new(stream);

        let layer = self
            .stack
            .iter()
            .find(|&l| l.path == request.url.path())
            .unwrap();

        (layer.handler)(Box::new(request), Box::new(response));

        Ok(())
    }
}