diff --git a/src/con_handler.rs b/src/con_handler.rs new file mode 100644 index 0000000..75b9482 --- /dev/null +++ b/src/con_handler.rs @@ -0,0 +1,334 @@ +use new_mime_guess; +use std::fs; +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use url::Url; + +use crate::util; +use crate::conn; +use crate::status::Status; +use crate::cgi; +use crate::logger; +use crate::revproxy; + +type Result = std::result::Result>; + +fn get_mime(path: &PathBuf) -> String { + let mut mime = "text/gemini".to_string(); + if path.is_dir() { + return mime; + } + let ext = match path.extension() { + Some(p) => p.to_str().unwrap(), + None => return "text/plain".to_string(), + }; + + mime = match new_mime_guess::from_ext(ext).first() { + Some(m) => m.essence_str().to_string(), + None => "text/plain".to_string(), + }; + + return mime; +} + +async fn get_binary(mut con: conn::Connection, path: PathBuf, meta: String) -> io::Result<()> { + let fd = File::open(path)?; + let mut reader = BufReader::with_capacity(1024 * 1024, fd); + con.send_status(Status::Success, Some(&meta)) + .await?; + loop { + let len = { + let buf = reader.fill_buf()?; + con.send_raw(buf).await?; + buf.len() + }; + if len == 0 { + break; + } + reader.consume(len); + } + Ok(()) +} + +async fn get_content(path: PathBuf, u: &url::Url) -> Result { + let meta = tokio::fs::metadata(&path).await?; + if meta.is_file() { + return Ok(tokio::fs::read_to_string(path).await?); + } + + let mut dirs: Vec = Vec::new(); + let mut files: Vec = Vec::new(); + + // needs work + for file in fs::read_dir(&path)? { + if let Ok(file) = file { + let m = file.metadata()?; + let perm = m.permissions(); + if perm.mode() & 0o0444 != 0o0444 { + continue; + } + let file = file.path(); + let p = file.strip_prefix(&path).unwrap(); + let ps = match p.to_str() { + Some(s) => s, + None => continue, + }; + let ep = match u.join(ps) { + Ok(p) => p, + _ => continue, + }; + if m.is_dir() { + dirs.push(format!("=> {}/ {}/\r\n", ep, p.display())); + } else { + files.push(format!("=> {} {}\r\n", ep, p.display())); + } + } + } + + dirs.sort(); + files.sort(); + + let mut list = String::from("# Directory Listing\r\n\r\n"); + list.push_str(&format!("Path: {}\r\n\r\n", u.path())); + + for dir in dirs { + list.push_str(&dir); + } + for file in files { + list.push_str(&file); + } + + return Ok(list); +} + +// Handle CGI and return Ok(true), or indicate this request wasn't for CGI with Ok(false) +#[cfg(feature = "cgi")] +async fn handle_cgi( + con: &mut conn::Connection, + request: &str, + url: &Url, + full_path: &PathBuf, +) -> Result { + if con.srv.server.cgi.unwrap_or(false) { + let mut path = full_path.clone(); + let mut segments = url.path_segments().unwrap(); + let mut path_info = "".to_string(); + + // Find an ancestor url that matches a file + while !path.exists() { + if let Some(segment) = segments.next_back() { + path.pop(); + path_info = format!("/{}{}", &segment, path_info); + } else { + return Ok(false); + } + } + let script_name = format!("/{}", segments.collect::>().join("/")); + + let meta = tokio::fs::metadata(&path).await?; + let perm = meta.permissions(); + + match &con.srv.server.cgipath { + Some(c) => { + if path.starts_with(c) { + if perm.mode() & 0o0111 == 0o0111 { + cgi::cgi(con, path, url, script_name, path_info).await?; + return Ok(true); + } else { + logger::logger(con.peer_addr, Status::CGIError, request); + con.send_status(Status::CGIError, None).await?; + return Ok(true); + } + } + }, + None => { + if meta.is_file() && perm.mode() & 0o0111 == 0o0111 { + cgi::cgi(con, path, url, script_name, path_info).await?; + return Ok(true); + } + }, + } + } + Ok(false) +} + +// TODO Rewrite this monster. +pub async fn handle_connection( + mut con: conn::Connection, + url: url::Url +) -> Result { + let index = match &con.srv.server.index { + Some(i) => i.clone(), + None => "index.gemini".to_string(), + }; + + match &con.srv.server.redirect.to_owned() { + Some(re) => { + let u = match url.path() { + "/" => "/", + _ => url.path().trim_end_matches("/"), + }; + match re.get(u) { + Some(r) => { + logger::logger(con.peer_addr, Status::RedirectTemporary, &url.as_str()); + con.send_status(Status::RedirectTemporary, Some(r)).await?; + return Ok(()); + } + None => {} + } + } + None => {} + } + + #[cfg(feature = "proxy")] + if let Some(pr) = con.srv.server.proxy_all.to_owned() { + let host_port: Vec<&str> = pr.splitn(2, ':').collect(); + let host = host_port[0]; + let port: Option; + if host_port.len() == 2 { + port = host_port[1].parse().ok(); + } else { + port = None; + } + + let mut upstream_url = url.clone(); + upstream_url.set_host(Some(host)).unwrap(); + upstream_url.set_port(port).unwrap(); + + revproxy::proxy_all(pr.as_str(), upstream_url, con).await?; + return Ok(()); + } + + #[cfg(feature = "proxy")] + match &con.srv.server.proxy { + Some(pr) => match url.path_segments().map(|c| c.collect::>()) { + Some(s) => match pr.get(s[0]) { + Some(p) => { + revproxy::proxy(p.to_string(), url, con).await?; + return Ok(()); + } + None => {} + }, + None => {} + }, + None => {} + } + + #[cfg(feature = "scgi")] + match &con.srv.server.scgi { + Some(sc) => { + let u = match url.path() { + "/" => "/", + _ => url.path().trim_end_matches("/"), + }; + match sc.get(u) { + Some(r) => { + cgi::scgi(r.to_string(), url, con).await?; + return Ok(()); + } + None => {} + } + }, + None => {}, + } + + + let mut path = PathBuf::new(); + + if url.path().starts_with("/~") && con.srv.server.usrdir.unwrap_or(false) { + let usr = url.path().trim_start_matches("/~"); + let usr: Vec<&str> = usr.splitn(2, "/").collect(); + if cfg!(target_os = "macos") { + path.push("/Users/"); + } else { + path.push("/home/"); + } + if usr.len() == 2 { + path.push(format!("{}/{}/{}", usr[0], "public_gemini", util::url_decode(usr[1].as_bytes()))); + } else { + path.push(format!("{}/{}/", usr[0], "public_gemini")); + } + } else { + path.push(&con.srv.server.dir); + if url.path() != "" || url.path() != "/" { + let decoded = util::url_decode(url.path().trim_start_matches("/").as_bytes()); + path.push(decoded); + } + } + + if !path.exists() { + // See if it's a subpath of a CGI script before returning NotFound + #[cfg(feature = "cgi")] + if handle_cgi(&mut con, &url.as_str(), &url, &path).await? { + return Ok(()); + } + + logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); + con.send_status(Status::NotFound, None).await?; + return Ok(()); + } + + let mut meta = tokio::fs::metadata(&path).await?; + let mut perm = meta.permissions(); + + // TODO fix me + // This block is terrible + if meta.is_dir() { + if !url.path().ends_with("/") { + logger::logger(con.peer_addr, Status::RedirectPermanent, &url.as_str()); + con.send_status( + Status::RedirectPermanent, + Some(format!("{}/", url).as_str()), + ) + .await?; + return Ok(()); + } + if path.join(&index).exists() { + path.push(index); + meta = tokio::fs::metadata(&path).await?; + perm = meta.permissions(); + if perm.mode() & 0o0444 != 0o444 { + let mut p = path.clone(); + p.pop(); + path.push(format!("{}/", p.display())); + meta = tokio::fs::metadata(&path).await?; + perm = meta.permissions(); + } + } + } + + #[cfg(feature = "cgi")] + if handle_cgi(&mut con, &url.as_str(), &url, &path).await? { + return Ok(()); + } + + if meta.is_file() && perm.mode() & 0o0111 == 0o0111 { + logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); + con.send_status(Status::NotFound, None).await?; + return Ok(()); + } + + if perm.mode() & 0o0444 != 0o0444 { + logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); + con.send_status(Status::NotFound, None).await?; + return Ok(()); + } + + let mut mime = get_mime(&path); + if mime == "text/gemini" && con.srv.server.lang.is_some() { + mime += &("; lang=".to_string() + &con.srv.server.lang.to_owned().unwrap()); + } + if !mime.starts_with("text/") { + logger::logger(con.peer_addr, Status::Success, &url.as_str()); + get_binary(con, path, mime).await?; + return Ok(()); + } + let content = get_content(path, &url).await?; + con.send_body(Status::Success, Some(&mime), Some(content)) + .await?; + logger::logger(con.peer_addr, Status::Success, &url.as_str()); + + Ok(()) +} diff --git a/src/lib/errors.rs b/src/lib/errors.rs new file mode 100644 index 0000000..bb319aa --- /dev/null +++ b/src/lib/errors.rs @@ -0,0 +1,13 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct GemError(pub String); + +impl fmt::Display for GemError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for GemError {} diff --git a/src/lib/mod.rs b/src/lib/mod.rs index 0cef841..2cc8caf 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -2,3 +2,5 @@ pub mod util; pub mod status; pub mod conn; pub mod tls; +pub mod server; +pub mod errors; diff --git a/src/lib/server.rs b/src/lib/server.rs new file mode 100644 index 0000000..2ad2163 --- /dev/null +++ b/src/lib/server.rs @@ -0,0 +1,164 @@ +use tokio::net::TcpListener; +use tokio::io::AsyncReadExt; +use openssl::ssl::SslAcceptor; +use openssl::error::ErrorStack; +use openssl::ssl::NameType; +//use futures_util::future::TryFutureExt; +use url::Url; +use std::io; +use std::collections::HashMap; +use std::future::Future; +//use std::sync::Arc; +use std::pin::Pin; + +use crate::config; +use crate::conn; +use crate::logger; +use crate::status::Status; +use crate::errors; + +type Result = std::result::Result>; + +pub trait Handler: FnMut(conn::Connection, url::Url) -> Pin + Send>> + Send + Sync + Copy {} +impl Handler for T + where T: FnMut(conn::Connection, url::Url) -> Pin + Send>> + Send + Sync + Copy +{ +} + +pub fn force_boxed(f: fn(conn::Connection, url::Url) -> T) -> impl Handler +where + T: Future + Send + Sync + 'static, +{ + move |a, b| Box::pin(f(a, b)) as _ +} + +pub struct Server { + pub listener: TcpListener, + pub acceptor: SslAcceptor, +} + +impl Server { + pub async fn bind(addr: String, + acceptor: fn(config::Config) -> std::result::Result, + cfg: config::Config) -> io::Result + { + Ok(Server { + listener: TcpListener::bind(addr).await?, + acceptor: acceptor(cfg)?, + }) + } + + pub async fn serve(self, cmap: HashMap, default: String, + handler: impl Handler + 'static + Copy) -> Result + { + loop { + let (stream, peer_addr) = self.listener.accept().await?; + let acceptor = self.acceptor.clone(); + let cmap = cmap.clone(); + let default = default.clone(); + let mut handler = handler.clone(); + + let ssl = openssl::ssl::Ssl::new(acceptor.context()).unwrap(); + let mut stream = tokio_openssl::SslStream::new(ssl, stream).unwrap(); + + tokio::spawn(async move { + match Pin::new(&mut stream).accept().await { + Ok(s) => s, + Err(e) => { + log::error!("Error: {}",e); + return Ok(()); + }, + }; + let srv = match stream.ssl().servername(NameType::HOST_NAME) { + Some(s) => match cmap.get(s) { + Some(ss) => ss, + None => cmap.get(&default).unwrap(), + }, + None => cmap.get(&default).unwrap(), + }.to_owned(); + + let con = conn::Connection { stream, peer_addr, srv }; + let (con, url) = match get_request(con).await { + Ok((c, u)) => (c, u), + Err(_) => return Ok(()) as io::Result<()>, + }; + + match handler(con, url).await { + Ok(o) => o, + Err(_) => return Ok(()) as io::Result<()>, + } + + Ok(()) as io::Result<()> + }); + } + } +} + +pub async fn get_request(mut con: conn::Connection) -> Result<(conn::Connection, url::Url)> { + let mut buffer = [0; 1024]; + let len = match tokio::time::timeout(tokio::time::Duration::from_secs(5), con.stream.read(&mut buffer)).await { + Ok(result) => result.unwrap(), + Err(e) => { + logger::logger(con.peer_addr, Status::BadRequest, ""); + con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; + return Err(Box::new(e)); + } + }; + let mut request = match String::from_utf8(buffer[..len].to_vec()) { + Ok(request) => request, + Err(e) => { + logger::logger(con.peer_addr, Status::BadRequest, ""); + con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; + return Err(Box::new(e)); + } + }; + if request.starts_with("//") { + request = request.replacen("//", "gemini://", 1); + } + + if request.ends_with("\n") { + request.pop(); + if request.ends_with("\r") { + request.pop(); + } + } + + let url = match Url::parse(&request) { + Ok(url) => url, + Err(e) => { + logger::logger(con.peer_addr, Status::BadRequest, &request); + con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; + return Err(Box::new(e)); + } + }; + + match url.host_str() { + Some(h) => { + if con.srv.server.hostname.as_str() != h.to_lowercase() { + logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); + con.send_status(Status::ProxyRequestRefused, None).await.map_err(|e| e.to_string())?; + return Err(Box::new(errors::GemError("Wrong host".into()))); + } + }, + None => {} + } + + match url.port() { + Some(p) => { + if p != con.srv.port { + logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); + con.send_status(Status::ProxyRequestRefused, None) + .await.map_err(|e| e.to_string())?; + } + } + None => {} + } + + if url.scheme() != "gemini" { + logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); + con.send_status(Status::ProxyRequestRefused, None).await.map_err(|e| e.to_string())?; + return Err(Box::new(errors::GemError("scheme not gemini".into()))); + } + + return Ok((con, url)) +} \ No newline at end of file diff --git a/src/logger.rs b/src/logger.rs index a69cc6d..c353d7a 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -2,6 +2,24 @@ use crate::status; use log::{info, warn}; use std::net::SocketAddr; +pub fn init(loglev: &Option) { + let loglev = match loglev { + None => log::Level::Info, + Some(l) => { + match l.as_str() { + "error" => log::Level::Error, + "warn" => log::Level::Warn, + "info" => log::Level::Info, + _ => { + eprintln!("Incorrect log level in config file."); + std::process::exit(1); + }, + } + }, + }; + simple_logger::init_with_level(loglev).unwrap(); +} + pub fn logger(addr: SocketAddr, stat: status::Status, req: &str) { match stat as u8 { 20..=29 => info!("remote={} status={} request={}", addr, stat as u8, req), diff --git a/src/main.rs b/src/main.rs index 97457f1..202d533 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,422 +1,29 @@ #[macro_use] extern crate serde_derive; -use futures_util::future::TryFutureExt; -use new_mime_guess; -use openssl::ssl::NameType; use std::env; -use std::fs; -use std::pin::Pin; -use std::fs::File; -use std::io::{self, BufRead, BufReader}; +use std::io; use std::net::ToSocketAddrs; -use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; -use tokio::net::TcpListener; -use tokio::io::AsyncReadExt; -use tokio::runtime; -use url::Url; +use std::path::Path; mod lib; mod cgi; mod config; mod logger; mod revproxy; +mod con_handler; use lib::util; use lib::conn; use lib::status; -use status::Status; use lib::tls; +use lib::server; +use lib::errors; -fn get_mime(path: &PathBuf) -> String { - let mut mime = "text/gemini".to_string(); - if path.is_dir() { - return mime; - } - let ext = match path.extension() { - Some(p) => p.to_str().unwrap(), - None => return "text/plain".to_string(), - }; +type Result = std::result::Result>; - mime = match new_mime_guess::from_ext(ext).first() { - Some(m) => m.essence_str().to_string(), - None => "text/plain".to_string(), - }; - - return mime; -} - -async fn get_binary(mut con: conn::Connection, path: PathBuf, meta: String) -> io::Result<()> { - let fd = File::open(path)?; - let mut reader = BufReader::with_capacity(1024 * 1024, fd); - con.send_status(status::Status::Success, Some(&meta)) - .await?; - loop { - let len = { - let buf = reader.fill_buf()?; - con.send_raw(buf).await?; - buf.len() - }; - if len == 0 { - break; - } - reader.consume(len); - } - Ok(()) -} - -async fn get_content(path: PathBuf, u: &url::Url) -> Result { - let meta = tokio::fs::metadata(&path).await?; - if meta.is_file() { - return Ok(tokio::fs::read_to_string(path).await?); - } - - let mut dirs: Vec = Vec::new(); - let mut files: Vec = Vec::new(); - - // needs work - for file in fs::read_dir(&path)? { - if let Ok(file) = file { - let m = file.metadata()?; - let perm = m.permissions(); - if perm.mode() & 0o0444 != 0o0444 { - continue; - } - let file = file.path(); - let p = file.strip_prefix(&path).unwrap(); - let ps = match p.to_str() { - Some(s) => s, - None => continue, - }; - let ep = match u.join(ps) { - Ok(p) => p, - _ => continue, - }; - if m.is_dir() { - dirs.push(format!("=> {}/ {}/\r\n", ep, p.display())); - } else { - files.push(format!("=> {} {}\r\n", ep, p.display())); - } - } - } - - dirs.sort(); - files.sort(); - - let mut list = String::from("# Directory Listing\r\n\r\n"); - list.push_str(&format!("Path: {}\r\n\r\n", u.path())); - - for dir in dirs { - list.push_str(&dir); - } - for file in files { - list.push_str(&file); - } - - return Ok(list); -} - -// Handle CGI and return Ok(true), or indicate this request wasn't for CGI with Ok(false) -#[cfg(feature = "cgi")] -async fn handle_cgi( - con: &mut conn::Connection, - request: &str, - url: &Url, - full_path: &PathBuf, -) -> Result { - if con.srv.server.cgi.unwrap_or(false) { - let mut path = full_path.clone(); - let mut segments = url.path_segments().unwrap(); - let mut path_info = "".to_string(); - - // Find an ancestor url that matches a file - while !path.exists() { - if let Some(segment) = segments.next_back() { - path.pop(); - path_info = format!("/{}{}", &segment, path_info); - } else { - return Ok(false); - } - } - let script_name = format!("/{}", segments.collect::>().join("/")); - - let meta = tokio::fs::metadata(&path).await?; - let perm = meta.permissions(); - - match &con.srv.server.cgipath { - Some(c) => { - if path.starts_with(c) { - if perm.mode() & 0o0111 == 0o0111 { - cgi::cgi(con, path, url, script_name, path_info).await?; - return Ok(true); - } else { - logger::logger(con.peer_addr, Status::CGIError, request); - con.send_status(Status::CGIError, None).await?; - return Ok(true); - } - } - }, - None => { - if meta.is_file() && perm.mode() & 0o0111 == 0o0111 { - cgi::cgi(con, path, url, script_name, path_info).await?; - return Ok(true); - } - }, - } - } - Ok(false) -} - -async fn get_request(mut con: conn::Connection) -> Result<(conn::Connection, url::Url), String> { - let mut buffer = [0; 1024]; - let len = match tokio::time::timeout(tokio::time::Duration::from_secs(5), con.stream.read(&mut buffer)).await { - Ok(result) => result.unwrap(), - Err(e) => { - logger::logger(con.peer_addr, Status::BadRequest, ""); - con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; - return Err(e.to_string()); - } - }; - let mut request = match String::from_utf8(buffer[..len].to_vec()) { - Ok(request) => request, - Err(e) => { - logger::logger(con.peer_addr, Status::BadRequest, ""); - con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; - return Err(e.to_string()); - } - }; - if request.starts_with("//") { - request = request.replacen("//", "gemini://", 1); - } - - if request.ends_with("\n") { - request.pop(); - if request.ends_with("\r") { - request.pop(); - } - } - - let url = match Url::parse(&request) { - Ok(url) => url, - Err(_) => { - logger::logger(con.peer_addr, Status::BadRequest, &request); - con.send_status(Status::BadRequest, None).await.map_err(|e| e.to_string())?; - return Err("Bad url".to_string()); - } - }; - - match url.host_str() { - Some(h) => { - if con.srv.server.hostname.as_str() != h.to_lowercase() { - logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); - con.send_status(Status::ProxyRequestRefused, None).await.map_err(|e| e.to_string())?; - return Err("Wrong host".to_string()); - } - }, - None => {} - } - - match url.port() { - Some(p) => { - if p != con.srv.port { - logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); - con.send_status(status::Status::ProxyRequestRefused, None) - .await.map_err(|e| e.to_string())?; - } - } - None => {} - } - - if url.scheme() != "gemini" { - logger::logger(con.peer_addr, Status::ProxyRequestRefused, &url.as_str()); - con.send_status(Status::ProxyRequestRefused, None).await.map_err(|e| e.to_string())?; - return Err("scheme not gemini".to_string()); - } - - return Ok((con, url)) -} - -// TODO Rewrite this monster. -async fn handle_connection( - mut con: conn::Connection, - url: url::Url -) -> Result<(), io::Error> { - let index = match &con.srv.server.index { - Some(i) => i.clone(), - None => "index.gemini".to_string(), - }; - - match &con.srv.server.redirect.to_owned() { - Some(re) => { - let u = match url.path() { - "/" => "/", - _ => url.path().trim_end_matches("/"), - }; - match re.get(u) { - Some(r) => { - logger::logger(con.peer_addr, Status::RedirectTemporary, &url.as_str()); - con.send_status(Status::RedirectTemporary, Some(r)).await?; - return Ok(()); - } - None => {} - } - } - None => {} - } - - #[cfg(feature = "proxy")] - if let Some(pr) = con.srv.server.proxy_all.to_owned() { - let host_port: Vec<&str> = pr.splitn(2, ':').collect(); - let host = host_port[0]; - let port: Option; - if host_port.len() == 2 { - port = host_port[1].parse().ok(); - } else { - port = None; - } - - let mut upstream_url = url.clone(); - upstream_url.set_host(Some(host)).unwrap(); - upstream_url.set_port(port).unwrap(); - - revproxy::proxy_all(pr.as_str(), upstream_url, con).await?; - return Ok(()); - } - - #[cfg(feature = "proxy")] - match &con.srv.server.proxy { - Some(pr) => match url.path_segments().map(|c| c.collect::>()) { - Some(s) => match pr.get(s[0]) { - Some(p) => { - revproxy::proxy(p.to_string(), url, con).await?; - return Ok(()); - } - None => {} - }, - None => {} - }, - None => {} - } - - #[cfg(feature = "scgi")] - match &con.srv.server.scgi { - Some(sc) => { - let u = match url.path() { - "/" => "/", - _ => url.path().trim_end_matches("/"), - }; - match sc.get(u) { - Some(r) => { - cgi::scgi(r.to_string(), url, con).await?; - return Ok(()); - } - None => {} - } - }, - None => {}, - } - - - let mut path = PathBuf::new(); - - if url.path().starts_with("/~") && con.srv.server.usrdir.unwrap_or(false) { - let usr = url.path().trim_start_matches("/~"); - let usr: Vec<&str> = usr.splitn(2, "/").collect(); - if cfg!(target_os = "macos") { - path.push("/Users/"); - } else { - path.push("/home/"); - } - if usr.len() == 2 { - path.push(format!("{}/{}/{}", usr[0], "public_gemini", util::url_decode(usr[1].as_bytes()))); - } else { - path.push(format!("{}/{}/", usr[0], "public_gemini")); - } - } else { - path.push(&con.srv.server.dir); - if url.path() != "" || url.path() != "/" { - let decoded = util::url_decode(url.path().trim_start_matches("/").as_bytes()); - path.push(decoded); - } - } - - if !path.exists() { - // See if it's a subpath of a CGI script before returning NotFound - #[cfg(feature = "cgi")] - if handle_cgi(&mut con, &url.as_str(), &url, &path).await? { - return Ok(()); - } - - logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); - con.send_status(Status::NotFound, None).await?; - return Ok(()); - } - - let mut meta = tokio::fs::metadata(&path).await?; - let mut perm = meta.permissions(); - - // TODO fix me - // This block is terrible - if meta.is_dir() { - if !url.path().ends_with("/") { - logger::logger(con.peer_addr, Status::RedirectPermanent, &url.as_str()); - con.send_status( - Status::RedirectPermanent, - Some(format!("{}/", url).as_str()), - ) - .await?; - return Ok(()); - } - if path.join(&index).exists() { - path.push(index); - meta = tokio::fs::metadata(&path).await?; - perm = meta.permissions(); - if perm.mode() & 0o0444 != 0o444 { - let mut p = path.clone(); - p.pop(); - path.push(format!("{}/", p.display())); - meta = tokio::fs::metadata(&path).await?; - perm = meta.permissions(); - } - } - } - - #[cfg(feature = "cgi")] - if handle_cgi(&mut con, &url.as_str(), &url, &path).await? { - return Ok(()); - } - - if meta.is_file() && perm.mode() & 0o0111 == 0o0111 { - logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); - con.send_status(Status::NotFound, None).await?; - return Ok(()); - } - - if perm.mode() & 0o0444 != 0o0444 { - logger::logger(con.peer_addr, Status::NotFound, &url.as_str()); - con.send_status(Status::NotFound, None).await?; - return Ok(()); - } - - let mut mime = get_mime(&path); - if mime == "text/gemini" && con.srv.server.lang.is_some() { - mime += &("; lang=".to_string() + &con.srv.server.lang.to_owned().unwrap()); - } - if !mime.starts_with("text/") { - logger::logger(con.peer_addr, Status::Success, &url.as_str()); - get_binary(con, path, mime).await?; - return Ok(()); - } - let content = get_content(path, &url).await?; - con.send_body(status::Status::Success, Some(&mime), Some(content)) - .await?; - logger::logger(con.peer_addr, Status::Success, &url.as_str()); - - Ok(()) -} - -fn main() -> io::Result<()> { +#[tokio::main] +async fn main() -> Result { let args: Vec = env::args().collect(); if args.len() != 2 { println!("Please run with the path to the config file."); @@ -435,21 +42,9 @@ fn main() -> io::Result<()> { return Ok(()); }, }; - let loglev = match &cfg.log { - None => log::Level::Info, - Some(l) => { - match l.as_str() { - "error" => log::Level::Error, - "warn" => log::Level::Warn, - "info" => log::Level::Info, - _ => { - eprintln!("Incorrect log level in config file."); - return Ok(()); - }, - } - }, - }; - simple_logger::init_with_level(loglev).unwrap(); + + logger::init(&cfg.log); + let cmap = cfg.to_map(); let default = &cfg.server[0].hostname; println!("Serving {} vhosts", cfg.server.len()); @@ -459,57 +54,9 @@ fn main() -> io::Result<()> { .next() .ok_or_else(|| io::Error::from(io::ErrorKind::AddrNotAvailable))?; - let runtime = runtime::Builder::new_multi_thread() - // .threaded_scheduler() - .enable_io() - .enable_time() - .build()?; - - let handle = runtime.handle().clone(); - - let acceptor = tls::acceptor_conf(cfg.clone())?; - - let fut = async move { - let listener = TcpListener::bind(&addr).await?; - loop { - let (stream, peer_addr) = listener.accept().await?; - let acceptor = acceptor.clone(); - let cmap = cmap.clone(); - let default = default.clone(); - - let ssl = openssl::ssl::Ssl::new(acceptor.context()).unwrap(); - let mut stream = tokio_openssl::SslStream::new(ssl, stream).unwrap(); - - let fut = async move { - match Pin::new(&mut stream).accept().await { - Ok(s) => s, - Err(e) => { - log::error!("Error: {}",e); - return Ok(()); - }, - }; - - let srv = match stream.ssl().servername(NameType::HOST_NAME) { - Some(s) => match cmap.get(s) { - Some(ss) => ss, - None => cmap.get(&default).unwrap(), - }, - None => cmap.get(&default).unwrap(), - }.to_owned(); - - let con = conn::Connection { stream, peer_addr, srv }; - let (con, url) = match get_request(con).await { - Ok((c, u)) => (c, u), - Err(_) => return Ok(()) as io::Result<()>, - }; - handle_connection(con, url).await?; - - Ok(()) as io::Result<()> - }; - - handle.spawn(fut.unwrap_or_else(|err| eprintln!("{:?}", err))); - } + let server = server::Server::bind(addr, tls::acceptor_conf, cfg.clone()).await?; + if let Err(e) = server.serve(cmap, default.to_string(), server::force_boxed(con_handler::handle_connection)).await { + return Err(e) }; - - runtime.block_on(fut) + return Ok(()) }