use async_std::task; use dnd_dice_roller::{dice::Dice, error::DiceError}; use maj::{ gemini::{Builder, Node}, route, seg, server::{Error, Handler as MajHandler, Request}, split, Response, }; use percent_encoding::percent_decode_str; use rustls::{ internal::pemfile::{certs, rsa_private_keys}, AllowAnyAnonymousOrAuthenticatedClient, Certificate, PrivateKey, RootCertStore, ServerConfig, }; use std::{ fs::File, io::{self, BufReader}, path::{Path, PathBuf}, str::FromStr, sync::Arc, }; use structopt::StructOpt; #[derive(StructOpt, Debug)] struct Options { /// host to listen on #[structopt(short = "H", long, env = "HOST", default_value = "0.0.0.0")] host: String, /// port to listen on #[structopt(short = "p", long, env = "PORT", default_value = "1965")] port: u16, /// cert file #[structopt(short = "c", long = "cert", env = "CERT_FILE")] cert: PathBuf, /// key file #[structopt(short = "k", long = "key", env = "KEY_FILE")] key: PathBuf, /// static path #[structopt(short = "s", long, env = "STATIC_PATH", default_value = "./static")] static_path: PathBuf, /// server hostname #[structopt( long = "hostname", env = "SERVER_HOSTNAME", default_value = "cetacean.club" )] hostname: String, } fn load_certs(path: &Path) -> io::Result> { certs(&mut BufReader::new(File::open(path)?)) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) } fn load_keys(path: &Path) -> io::Result> { rsa_private_keys(&mut BufReader::new(File::open(path)?)) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) } fn main() -> Result<(), maj::server::Error> { env_logger::init(); let opts = Options::from_args(); let certs = load_certs(&opts.cert)?; let mut keys = load_keys(&opts.key)?; log::info!( "serving gemini://{} on {}:{}", opts.hostname, opts.host, opts.port ); let mut config = ServerConfig::new(AllowAnyAnonymousOrAuthenticatedClient::new( RootCertStore::empty(), )); config .set_single_cert(certs, keys.remove(0)) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; task::block_on(maj::server::serve( Arc::new(Handler { hostname: opts.hostname, files: maj::server::files::Handler::new(opts.static_path), }), config, opts.host, opts.port, ))?; Ok(()) } struct Handler { hostname: String, files: maj::server::files::Handler, } async fn dice(req: Request) -> Result { fn dice_roll>(roll: T) -> Result { let mut dice = Dice::from_str(&roll.into())?; if dice.number_of_dice_to_roll > 100 { dice.number_of_dice_to_roll = 100; } if dice.sides > 100 { dice.sides = 100 } if dice.sides == 0 { dice.sides = 6; } let res = dice.roll_dice(); let reply = format!( "{}{} = {}\n", res.dice_results, match dice.modifier { Some(amt) => format!(" + {}", amt), None => "".into(), }, res.final_result[0] ); Ok(reply) } match req.url.query() { None => Ok(Response::input( "What do you want to roll? [n]dn[+n] [adv|dadv]", )), Some(q) => Ok({ let dice = percent_decode_str(q).decode_utf8()?; let b = Builder::new() .heading(1, "Dice Results") .text("") .text(format!("You rolled {} and you got:", dice)) .text("") .preformatted(format!("{}", dice_roll(dice)?)); Response::render(b.build()) }), } } #[async_trait::async_trait] impl MajHandler for Handler { async fn handle(&self, req: Request) -> Result { if req.url.has_host() && req.url.host_str().unwrap().to_string() != self.hostname { return Ok(Response::no_proxy()); } if req.url.path() == "" { return Ok(Response::perm_redirect(format!( "gemini://{}/", self.hostname ))); } route!(req.url.path(), { (/"dice") => dice(req).await; }); self.files.handle(req).await } }