diff --git a/src/config.rs b/src/config.rs index 1adab8d..93bd5fd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ pub struct Config { #[derive(Debug, Deserialize, Clone)] pub struct Server { - pub url: String, + pub hostname: String, pub dir: String, pub key: String, pub cert: String, @@ -25,10 +25,10 @@ impl Config { let config: Config = toml::from_str(&fd).unwrap(); return config; } - pub fn to_map(&self /*cfg: &config::Config*/) -> HashMap { + pub fn to_map(&self) -> HashMap { let mut map = HashMap::new(); for srv in &self.server { - map.insert(srv.url.clone(), srv.dir.clone()); + map.insert(srv.hostname.clone(), srv.dir.clone()); } map } diff --git a/src/main.rs b/src/main.rs index 0a1c0c6..44e548c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![allow(unused_imports)] +#![allow(dead_code)] #[macro_use] extern crate serde_derive; @@ -9,6 +10,7 @@ use std::fs; use std::fs::File; use std::io::{self, BufReader}; use std::net::ToSocketAddrs; +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::io::AsyncWriteExt; @@ -23,9 +25,27 @@ use tokio_rustls::TlsAcceptor; use url::Url; mod config; +mod status; mod tls; -fn get_content(mut path: PathBuf, u: url::Url) -> Result { +async fn send( + mut stream: TlsStream, + stat: status::Status, + meta: String, + body: Option, +) -> Result<(), io::Error> { + let mut s = format!("{}\t{}\r\n", stat as u8, meta); + stream.write_all(s.as_bytes()).await?; + stream.flush().await?; + if let Some(b) = body { + s = format!("{}", b); + } + stream.write_all(s.as_bytes()).await?; + stream.flush().await?; + Ok(()) +} + +fn get_content(path: PathBuf, u: url::Url) -> Result { let meta = fs::metadata(&path).expect("Unable to read metadata"); if meta.is_file() { return Ok(std::fs::read_to_string(path).expect("Unable to read file")); @@ -56,20 +76,30 @@ async fn handle_connection( let url = Url::parse(&request).unwrap(); if url.scheme() != "gemini" { - stream.write_all(&b"53\tnot gemini scheme!\r\n"[..]).await?; - stream.flush().await?; - panic!("Not gemini scheme"); + send( + stream, + status::Status::ProxyRequestRefused, + "Not a gemini scheme!\r\n".to_string(), + None, + ) + .await?; + return Ok(()); } if url.path().to_string().contains("..") { - stream.write_all(&b"50\tNot in path\r\n"[..]).await?; - stream.flush().await?; - panic!("Contains .."); + send( + stream, + status::Status::PermanentFailure, + "Not in path!".to_string(), + None, + ) + .await?; + return Ok(()); } let mut dir = String::new(); for server in cfg.server { - if Some(server.url.as_str()) == url.host_str() { + if Some(server.hostname.as_str()) == url.host_str() { dir = server.dir; } } @@ -80,31 +110,42 @@ async fn handle_connection( } if !path.exists() { - stream.write_all(&b"51\tNot found!\r\n"[..]).await?; - stream.flush().await?; + send( + stream, + status::Status::NotFound, + "Not found!\r\n".to_string(), + None, + ) + .await?; return Ok(()); } // add error let meta = fs::metadata(&path).expect("Unable to read metadata"); + if meta.is_dir() { if !url.path().ends_with("/") { - stream - .write_all(format!("31\t{}/\r\n", url).as_bytes()) - .await?; - stream.flush().await?; + send( + stream, + status::Status::RedirectPermanent, + format!("{}/\r\n", url), + None, + ) + .await?; + return Ok(()); } if path.join("index.gemini").exists() { path.push("index.gemini"); } } - - stream.write_all(&b"20\ttext/gemini\r\n"[..]).await?; - stream.flush().await?; - let content = get_content(path, url)?; - stream.write_all(content.as_bytes()).await?; - stream.flush().await?; + send( + stream, + status::Status::Success, + "text/gemini".to_string(), + Some(content), + ) + .await?; Ok(()) } diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..613f870 --- /dev/null +++ b/src/status.rs @@ -0,0 +1,44 @@ +use std::fmt; + +#[repr(u8)] +#[derive(Debug)] +pub enum Status { + Input = 10, + Success = 20, + SuccessEndOfSession = 21, + RedirectTemporary = 30, + RedirectPermanent = 31, + TemporaryFailure = 40, + ServerUnavailable = 41, + CGIError = 42, + ProxyError = 43, + SlowDown = 44, + PermanentFailure = 50, + NotFound = 51, + Gone = 52, + ProxyRequestRefused = 53, + BadRequest = 59, + ClientCertificateRequired = 60, + TransientCertificateRequested = 61, + AuthorisedCertificateRequired = 62, + CertificateNotAccepted = 63, + FutureCertificateRejected = 64, + ExpiredCertificateRejected = 65, +} + +impl fmt::Display for Status { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + // or, alternatively: + // fmt::Debug::fmt(self, f) + } +} + +impl Status { + pub fn as_str(&self) -> &'static str { + match *self { + Status::Success => "20\t", + _ => "", + } + } +} diff --git a/src/tls.rs b/src/tls.rs index c2b45d9..88f86fb 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -34,6 +34,8 @@ pub fn load_key(path: &String) -> PrivateKey { return key[0].clone(); } +// Rustls won't let self signed certs be used with sni which gemini requires. +// At 1.4.2 in https://gemini.circumlunar.space/docs/spec-spec.txt pub struct CertResolver { map: HashMap>, } @@ -44,19 +46,14 @@ impl CertResolver { for server in cfg.server.iter() { let key = load_key(&server.key); - // .chain_err(|| format!("Failed to load private key from {}", https.key_file))?; + // .chain_err(|| format!("Failed to load private key from {}", server.key))?; let certs = load_certs(&server.cert).unwrap(); // .chain_err(|| format!("Failed to load certificate from {}", server.cert)); - /* - let signer: Arc> = Arc::new(Box::new( - CertifiedKey::new(&key).map_err(|_| format!("Failed to create signer for {}", server.url)) - )); - */ let signing_key = RSASigningKey::new(&key).unwrap(); let signing_key_boxed: Arc> = Arc::new(Box::new(signing_key)); map.insert( - server.url.clone(), + server.hostname.clone(), Box::new(rustls::sign::CertifiedKey::new(certs, signing_key_boxed)), ); } @@ -69,8 +66,8 @@ impl CertResolver { impl ResolvesServerCert for CertResolver { fn resolve(&self, client_hello: ClientHello) -> Option { - if let Some(url) = client_hello.server_name() { - if let Some(cert) = self.map.get(url.into()) { + if let Some(hostname) = client_hello.server_name() { + if let Some(cert) = self.map.get(hostname.into()) { return Some(*cert.clone()); } }