diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f5d6a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +Cargo.lock +**/*.rs.bk +.gemgit diff --git a/README b/README index 1b43932..6b71939 100644 --- a/README +++ b/README @@ -8,6 +8,7 @@ A gemini server written in rust. - CGI - User directories - Reverse proxy + - Redirect ## Installation and running @@ -19,7 +20,11 @@ A gemini server written in rust. ## Create server key/cert pairs -## CGI +## CGI Environments + +These variables are preset for you. If you need more you can define them in the +config file under "cgienv" + - GEMINI_URL - SERVER_NAME - SERVER_PROTOCOL diff --git a/cgi-scripts/agena-cgi/src/main.rs b/cgi-scripts/agena-cgi/src/main.rs index 54931f5..4ebc4db 100644 --- a/cgi-scripts/agena-cgi/src/main.rs +++ b/cgi-scripts/agena-cgi/src/main.rs @@ -6,10 +6,10 @@ const BASE: &'static str = "gemini://example.com/"; fn main() { let query = match env::var("QUERY_STRING") { Ok(q) => q, - _ => { + _ => { println!("10\tGopher url:\r\n"); return; - }, + } }; println!("30\t{}{}\r\n", BASE, query); } diff --git a/config.toml b/config.toml index 25285a9..0624e18 100644 --- a/config.toml +++ b/config.toml @@ -2,6 +2,8 @@ port = 1965 # use "::" for ipv6 and ipv4 or "0.0.0.0" for ipv4 only host = "::" +# There must be at least 1 server tag +# Server 1 [[server]] hostname = "example.com" dir = "/path/to/serv" @@ -9,12 +11,17 @@ key = "/path/to/key" cert = "/path/to/cert" # cgi dir is optional cgi = "/path/to/cgi-bin/" -# usrdir is optional. it'll look in /home/~usr/public_gemini +# cgienv is optional +cgienv = { "GIT_PROJECT_ROOT" = "/srv/git" } +# usrdir is optional. it'll look in /home/usr/public_gemini usrdir = true # proxy is optional # path is what comes after the hostname e.g. example.com/path proxy { path = "localhost:1966" } +# redirect is optional +redirect = { "/redirect" = "/", "/newdomain" = "gemini://example.net" } +# Server 2 [[server]] hostname = "example.net" dir = "/path/to/serv/" diff --git a/src/cgi.rs b/src/cgi.rs index 4704aee..f86c6d9 100644 --- a/src/cgi.rs +++ b/src/cgi.rs @@ -1,14 +1,20 @@ use std::collections::HashMap; use std::io; -use std::path::{PathBuf}; +use std::path::PathBuf; use std::process::Command; use url::Url; -use crate::status::Status; +use crate::config; use crate::conn; use crate::logger; +use crate::status::Status; -pub async fn cgi(mut con: conn::Connection, path: PathBuf, url: Url) -> Result<(), io::Error> { +pub async fn cgi( + mut con: conn::Connection, + srv: &config::ServerCfg, + path: PathBuf, + url: Url, +) -> Result<(), io::Error> { let mut envs = HashMap::new(); envs.insert("GATEWAY_INTERFACE", "CGI/1.1"); envs.insert("GEMINI_URL", url.as_str()); @@ -26,6 +32,12 @@ pub async fn cgi(mut con: conn::Connection, path: PathBuf, url: Url) -> Result<( envs.insert("QUERY_STRING", q); } + if srv.cgienv.len() != 0 { + for (k, v) in srv.cgienv.iter() { + envs.insert(&k, &v); + } + }; + let cmd = Command::new(path.to_str().unwrap()) .env_clear() .envs(&envs) diff --git a/src/config.rs b/src/config.rs index b18feae..48bcffc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,8 @@ extern crate serde_derive; extern crate toml; use std::collections::HashMap; -use toml::de::Error; use std::path::Path; +use toml::de::Error; #[derive(Debug, Deserialize, Clone)] pub struct Config { @@ -18,8 +18,10 @@ pub struct Server { pub key: String, pub cert: String, pub cgi: Option, + pub cgienv: Option>, pub usrdir: Option, pub proxy: Option>, + pub redirect: Option>, } #[derive(Debug, Clone)] @@ -29,9 +31,11 @@ pub struct ServerCfg { pub key: String, pub cert: String, pub cgi: String, + pub cgienv: HashMap, pub usrdir: bool, pub port: u16, pub proxy: HashMap, + pub redirect: HashMap, } impl Config { @@ -54,18 +58,36 @@ impl Config { let mut pmap = HashMap::new(); match &srv.proxy { Some(pr) => pmap = pr.clone(), - None => {}, + None => {} }; - map.insert(srv.hostname.clone(), ServerCfg { - hostname: srv.hostname.clone(), - dir: srv.dir.clone(), - key: srv.key.clone(), - cert: srv.cert.clone(), - cgi: cgi, - usrdir: usrdir, - port: self.port.clone(), - proxy: pmap.clone(), - }); + + let mut rmap = HashMap::new(); + match &srv.redirect { + Some(r) => rmap = r.clone(), + None => {} + }; + + let mut cmap = HashMap::new(); + match &srv.cgienv { + Some(c) => cmap = c.clone(), + None => {} + }; + + map.insert( + srv.hostname.clone(), + ServerCfg { + hostname: srv.hostname.clone(), + dir: srv.dir.clone(), + key: srv.key.clone(), + cert: srv.cert.clone(), + cgi: cgi, + cgienv: cmap.clone(), + usrdir: usrdir, + port: self.port.clone(), + proxy: pmap.clone(), + redirect: rmap.clone(), + }, + ); } map } diff --git a/src/conn.rs b/src/conn.rs index 1e3d0e2..39c4333 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -12,36 +12,32 @@ pub struct Connection { } impl Connection { -pub async fn send_status( - &mut self, - stat: Status, - meta: Option<&str>, -) -> Result<(), io::Error> { - self.send_body(stat, meta, None).await?; - Ok(()) -} - -pub async fn send_body( - &mut self, - stat: Status, - meta: Option<&str>, - body: Option, -) -> Result<(), io::Error> { - let meta = match meta { - Some(m) => m, - None => &stat.to_str(), - }; - let mut s = format!("{}\t{}\r\n", stat as u8, meta); - if let Some(b) = body { - s += &b; + pub async fn send_status(&mut self, stat: Status, meta: Option<&str>) -> Result<(), io::Error> { + self.send_body(stat, meta, None).await?; + Ok(()) } - self.send_raw(s.as_bytes()).await?; - Ok(()) -} -pub async fn send_raw(&mut self, body: &[u8]) -> Result<(), io::Error> { - self.stream.write_all(body).await?; - self.stream.flush().await?; - Ok(()) -} + pub async fn send_body( + &mut self, + stat: Status, + meta: Option<&str>, + body: Option, + ) -> Result<(), io::Error> { + let meta = match meta { + Some(m) => m, + None => &stat.to_str(), + }; + let mut s = format!("{}\t{}\r\n", stat as u8, meta); + if let Some(b) = body { + s += &b; + } + self.send_raw(s.as_bytes()).await?; + Ok(()) + } + + pub async fn send_raw(&mut self, body: &[u8]) -> Result<(), io::Error> { + self.stream.write_all(body).await?; + self.stream.flush().await?; + Ok(()) + } } diff --git a/src/logger.rs b/src/logger.rs index 418350c..a69cc6d 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,16 +1,10 @@ -use std::net::SocketAddr; -use log::{info, warn}; use crate::status; +use log::{info, warn}; +use std::net::SocketAddr; 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 - ), - _ => warn!( - "remote={} status={} request={}", - addr, stat as u8, req - ), + 20..=29 => info!("remote={} status={} request={}", addr, stat as u8, req), + _ => warn!("remote={} status={} request={}", addr, stat as u8, req), } } diff --git a/src/main.rs b/src/main.rs index 721fffb..f1cb734 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,30 +2,30 @@ extern crate serde_derive; use futures_util::future::TryFutureExt; +use mime; +use mime_guess; +use openssl::ssl::NameType; +use std::env; use std::fs; use std::fs::File; -use std::io::{self, BufReader, BufRead}; +use std::io::{self, BufRead, BufReader}; use std::net::ToSocketAddrs; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; -use std::env; use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::prelude::*; use tokio::runtime; -use openssl::ssl::NameType; use url::Url; -use mime_guess; -use mime; mod cgi; mod config; mod status; use status::Status; -mod tls; mod conn; -mod revproxy; mod logger; +mod revproxy; +mod tls; fn get_mime(path: &PathBuf) -> String { let mut mime = "text/gemini"; @@ -38,14 +38,15 @@ fn get_mime(path: &PathBuf) -> String { if ext != "gemini" { m = mime_guess::from_ext(ext).first().unwrap(); mime = m.essence_str(); - } + } mime.to_string() } -async fn get_binary(mut con: conn::Connection, path:PathBuf, meta: String) -> io::Result<()> { +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?; + 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()?; @@ -53,7 +54,7 @@ async fn get_binary(mut con: conn::Connection, path:PathBuf, meta: String) -> io buf.len() }; if len == 0 { - break + break; } reader.consume(len); } @@ -74,7 +75,7 @@ fn get_content(path: PathBuf, u: url::Url) -> Result { let m = file.metadata()?; let perm = m.permissions(); if perm.mode() & 0o0444 != 0o0444 { - continue + continue; } let file = file.path(); let p = file.strip_prefix(&path).unwrap(); @@ -88,9 +89,11 @@ fn get_content(path: PathBuf, u: url::Url) -> Result { return Ok(list); } - -// TODO Rewrite this monster. -async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) -> Result<(), io::Error> { +// TODO Rewrite this monster. +async fn handle_connection( + mut con: conn::Connection, + srv: &config::ServerCfg, +) -> Result<(), io::Error> { let mut buffer = [0; 1024]; con.stream.read(&mut buffer).await?; let mut request = match String::from_utf8(buffer[..].to_vec()) { @@ -98,7 +101,7 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - Err(_) => { logger::logger(con.peer_addr, Status::BadRequest, ""); con.send_status(Status::BadRequest, None).await?; - return Ok(()) + return Ok(()); } }; if request.starts_with("//") { @@ -110,7 +113,7 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - Err(_) => { logger::logger(con.peer_addr, Status::BadRequest, &request); con.send_status(Status::BadRequest, None).await?; - return Ok(()) + return Ok(()); } }; @@ -121,52 +124,62 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - } match url.port() { - Some(p) => { if p != srv.port { - logger::logger(con.peer_addr, Status::ProxyRequestRefused, &request); - con.send_status(status::Status::ProxyRequestRefused, None).await?; - }}, + Some(p) => { + if p != srv.port { + logger::logger(con.peer_addr, Status::ProxyRequestRefused, &request); + con.send_status(status::Status::ProxyRequestRefused, None) + .await?; + } + } None => {} } if url.scheme() != "gemini" { logger::logger(con.peer_addr, Status::ProxyRequestRefused, &request); - con.send_status(Status::ProxyRequestRefused, None) - .await?; + con.send_status(Status::ProxyRequestRefused, None).await?; return Ok(()); } + if srv.redirect.len() != 0 { + let u = url.path().trim_end_matches("/"); + match srv.redirect.get(u) { + Some(r) => { + logger::logger(con.peer_addr, Status::RedirectTemporary, &request); + con.send_status(Status::RedirectTemporary, Some(r)).await?; + return Ok(()); + } + None => {} + } + } if srv.proxy.len() != 0 { match url.path_segments().map(|c| c.collect::>()) { - Some(s) => { - match srv.proxy.get(s[0]) { - Some(p) => { - revproxy::proxy(p.to_string(), url, con).await?; - return Ok(()); - }, - None => {}, + Some(s) => match srv.proxy.get(s[0]) { + Some(p) => { + revproxy::proxy(p.to_string(), url, con).await?; + return Ok(()); } - } - None => {}, - } + None => {} + }, + None => {} + } } let mut path = PathBuf::new(); - if url.path().starts_with("/~") && srv.usrdir == true{ + if url.path().starts_with("/~") && srv.usrdir == true { let usr = url.path().trim_start_matches("/~"); let usr: Vec<&str> = usr.splitn(2, "/").collect(); path.push("/home/"); if usr.len() == 2 { path.push(format!("{}/{}/{}", usr[0], "public_gemini", usr[1])); } else { - path.push(format!("{}/{}/",usr[0], "public_gemini")); + path.push(format!("{}/{}/", usr[0], "public_gemini")); } } else { path.push(&srv.dir); if url.path() != "" || url.path() != "/" { path.push(url.path().trim_start_matches("/")); } - } if !path.exists() { @@ -197,7 +210,7 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - if perm.mode() & 0o0444 != 0o444 { let mut p = path.clone(); p.pop(); - path.push(format!("{}/",p.display())); + path.push(format!("{}/", p.display())); meta = fs::metadata(&path).expect("Unable to read metadata"); perm = meta.permissions(); } @@ -207,8 +220,8 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - // TODO add timeout if srv.cgi.trim_end_matches("/") == path.parent().unwrap().to_str().unwrap() { if perm.mode() & 0o0111 == 0o0111 { - cgi::cgi(con, path, url).await?; - return Ok(()); + cgi::cgi(con, srv, path, url).await?; + return Ok(()); } else { logger::logger(con.peer_addr, Status::CGIError, &request); con.send_status(Status::CGIError, None).await?; @@ -229,12 +242,8 @@ async fn handle_connection(mut con: conn::Connection, srv: &config::ServerCfg) - return Ok(()); } let content = get_content(path, url)?; - con.send_body( - status::Status::Success, - Some(mime.as_str()), - Some(content), - ) - .await?; + con.send_body(status::Status::Success, Some(mime.as_str()), Some(content)) + .await?; logger::logger(con.peer_addr, Status::Success, &request); Ok(()) @@ -279,7 +288,9 @@ fn main() -> io::Result<()> { let cmap = cmap.clone(); let fut = async move { - let mut stream = tokio_openssl::accept(&acceptor, stream).await.expect("Couldn't accept"); + let mut stream = tokio_openssl::accept(&acceptor, stream) + .await + .expect("Couldn't accept"); let sni = match stream.ssl().servername(NameType::HOST_NAME) { Some(s) => s, None => return Ok(()), @@ -292,13 +303,10 @@ fn main() -> io::Result<()> { stream.write_all(b"59\tNotFound!\r\n").await?; stream.flush().await?; return Ok(()); - }, - }; - - let con = conn::Connection { - stream, - peer_addr, + } }; + + let con = conn::Connection { stream, peer_addr }; handle_connection(con, srv).await?; Ok(()) as io::Result<()> diff --git a/src/revproxy.rs b/src/revproxy.rs index 7eca6fb..9cf5ed2 100644 --- a/src/revproxy.rs +++ b/src/revproxy.rs @@ -1,12 +1,12 @@ +use openssl::ssl::{SslConnector, SslMethod}; use std::io; use std::net::ToSocketAddrs; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; -use openssl::ssl::{SslConnector, SslMethod}; use crate::conn; -use crate::status::Status; use crate::logger; +use crate::status::Status; pub async fn proxy(addr: String, u: url::Url, mut con: conn::Connection) -> Result<(), io::Error> { let p: Vec<&str> = u.path().trim_start_matches("/").splitn(2, "/").collect(); @@ -18,7 +18,7 @@ pub async fn proxy(addr: String, u: url::Url, mut con: conn::Connection) -> Resu if p[1] == "" || p[1] == "/" { logger::logger(con.peer_addr, Status::NotFound, u.as_str()); con.send_status(Status::NotFound, None).await?; - return Ok(()) + return Ok(()); } let addr = addr .to_socket_addrs()? @@ -34,20 +34,20 @@ pub async fn proxy(addr: String, u: url::Url, mut con: conn::Connection) -> Resu Err(_) => { logger::logger(con.peer_addr, Status::ProxyError, u.as_str()); con.send_status(Status::ProxyError, None).await?; - return Ok(()) - }, + return Ok(()); + } }; let mut stream = match tokio_openssl::connect(config, "localhost", stream).await { Ok(s) => s, Err(_) => { logger::logger(con.peer_addr, Status::ProxyError, u.as_str()); con.send_status(Status::ProxyError, None).await?; - return Ok(()) - }, + return Ok(()); + } }; stream.write_all(p[1].as_bytes()).await?; stream.flush().await?; - + let mut buf = vec![]; stream.read_to_end(&mut buf).await?; // let req = String::from_utf8(buf[..].to_vec()).unwrap(); diff --git a/src/status.rs b/src/status.rs index 7e11135..244c774 100644 --- a/src/status.rs +++ b/src/status.rs @@ -52,7 +52,7 @@ impl Status { Status::FutureCertificateRejected => "Future Certificate Rejected", Status::ExpiredCertificateRejected => "Expired Certificate Rejected", }; - return meta + return meta; } } @@ -61,4 +61,3 @@ impl fmt::Display for Status { write!(f, "{:?}", self) } } - diff --git a/src/tls.rs b/src/tls.rs index e2d26e7..b41f584 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -2,12 +2,12 @@ extern crate openssl; extern crate tokio_openssl; use std::collections::HashMap; -use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; -use openssl::ssl::SniError; use openssl::error::ErrorStack; +use openssl::ssl::NameType; +use openssl::ssl::SniError; use openssl::ssl::SslContextBuilder; use openssl::ssl::SslVersion; -use openssl::ssl::NameType; +use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use crate::config; @@ -22,15 +22,15 @@ pub fn acceptor_conf(cfg: config::Config) -> Result { Ok(c) => c, Err(e) => { eprintln!("Error: Can't load key file"); - return Err(e) - }, + return Err(e); + } }; match ctx.set_certificate_chain_file(&server.cert) { Ok(c) => c, Err(e) => { eprintln!("Error: Can't load cert file"); - return Err(e) - }, + return Err(e); + } }; let ctx = ctx.build(); map.insert(server.hostname.clone(), ctx.clone()); @@ -53,7 +53,8 @@ pub fn acceptor_conf(cfg: config::Config) -> Result { } else { &map.get(&"default".to_string()).expect("Can't get default") } - }).expect("Can't get sni"); + }) + .expect("Can't get sni"); Ok(()) }); Ok(acceptor.build())