You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
6.4 KiB
223 lines
6.4 KiB
extern crate openssl; |
|
|
|
/// Types representing the gemini specification |
|
pub mod gemini; |
|
pub mod error; |
|
|
|
mod io; |
|
mod logger; |
|
|
|
use std::io::Read; |
|
|
|
use std::net::{TcpListener, TcpStream}; |
|
use std::path::PathBuf; |
|
use std::sync::Arc; |
|
|
|
pub use error::{GempressError, GempressResult}; |
|
use openssl::ssl::{SslMethod, SslAcceptor, SslStream, SslFiletype}; |
|
|
|
/// Configuration for a Gempress server. |
|
#[derive(Clone, Debug)] |
|
pub struct Config { |
|
// Path to the identity file |
|
certPath: PathBuf, |
|
keyPath: PathBuf, |
|
} |
|
|
|
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): |
|
/// |
|
/// ```sh |
|
/// 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 new(certPath: PathBuf, keyPath: PathBuf) -> Self { |
|
Self { |
|
certPath, |
|
keyPath, |
|
} |
|
} |
|
} |
|
|
|
/// A function handler |
|
/// |
|
/// # Examples |
|
/// |
|
/// ``` |
|
/// use 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>) -> |
|
/// GempressResult<()> { |
|
/// res.send("Hello from index route!".as_bytes())?; |
|
/// Ok(()) |
|
/// } |
|
/// |
|
/// // 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>) -> error::GempressResult<()>; |
|
|
|
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; |
|
/// use gempress::*; |
|
/// |
|
/// 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>) -> |
|
/// GempressResult<()> { |
|
/// res.send("Hello from index route!".as_bytes())?; |
|
/// Ok(()) |
|
/// } |
|
/// |
|
/// fn foo_handler(req: Box<gemini::Request>, mut res: Box<gemini::Response>) -> |
|
/// GempressResult<()> { |
|
/// res.send("This is the /foo route".as_bytes()); |
|
/// Ok(()) |
|
/// } |
|
/// |
|
/// 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<()> { |
|
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); |
|
acceptor.set_private_key_file(&self.config.keyPath, SslFiletype::PEM).unwrap(); |
|
acceptor.set_certificate_chain_file(&self.config.certPath).unwrap(); |
|
acceptor.check_private_key().unwrap(); |
|
let acceptor = Arc::new(acceptor.build()); |
|
|
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).map_err(GempressError::BindFailed)?; |
|
|
|
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: SslStream<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 mut request = gemini::Request::parse(&raw_request)?; |
|
|
|
request.certificate = None; // TODO |
|
|
|
let mut response = gemini::Response::new(stream); |
|
response.status(gemini::StatusCode::Success)?; |
|
|
|
let maybe_layer = self |
|
.stack |
|
.iter() |
|
.find(|&l| l.path == request.url.path()); |
|
|
|
|
|
match maybe_layer { |
|
Some(layer) => { (layer.handler)(Box::new(request), Box::new(response))?; }, |
|
None => { response.status(gemini::StatusCode::NotFound)?.send("Not found".as_bytes())?; }, |
|
}; |
|
|
|
Ok(()) |
|
} |
|
}
|
|
|