diff --git a/Cargo.toml b/Cargo.toml index 6a7127b..cd33c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,24 +12,26 @@ readme = "README.md" include = ["src/**", "Cargo.*", "CHANGELOG.md", "LICENSE*", "README.md"] [features] -default = ["certgen"] +default = ["scgi_srv"] user_management = ["sled", "bincode", "serde/derive", "crc32fast", "lazy_static"] user_management_advanced = ["rust-argon2", "ring", "user_management"] user_management_routes = ["user_management"] serve_dir = ["mime_guess", "tokio/fs"] ratelimiting = ["dashmap"] -certgen = ["rcgen"] +certgen = ["rcgen", "gemini_srv"] +gemini_srv = ["tokio-rustls", "webpki", "rustls"] +scgi_srv = [] [dependencies] anyhow = "1.0.33" -rustls = { version = "0.18.1", features = ["dangerous_configuration"] } -tokio-rustls = "0.20.0" tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } uriparse = "0.6.3" percent-encoding = "2.1.0" log = "0.4.11" -webpki = "0.21.0" lazy_static = { version = "1.4.0", optional = true } +rustls = { version = "0.18.1", features = ["dangerous_configuration"], optional = true} +webpki = { version = "0.21.0", optional = true} +tokio-rustls = { version = "0.20.0", optional = true} mime_guess = { version = "2.0.3", optional = true } dashmap = { version = "3.11.10", optional = true } sled = { version = "0.34.6", optional = true } @@ -39,6 +41,7 @@ rust-argon2 = { version = "0.8.2", optional = true } crc32fast = { version = "1.2.1", optional = true } ring = { version = "0.16.15", optional = true } rcgen = { version = "0.8.5", optional = true } +squeegee = { git = "https://gitlab.com/Alch_Emi/squeegee.git", branch = "main", optional = true } [dev-dependencies] env_logger = "0.8.1" diff --git a/examples/certificates.rs b/examples/certificates.rs index a5ca78b..28e7e1d 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,7 +1,7 @@ use anyhow::*; use log::LevelFilter; use tokio::sync::RwLock; -use kochab::{Certificate, GEMINI_PORT, Request, Response, Server}; +use kochab::{Certificate, Request, Response, Server}; use std::collections::HashMap; use std::sync::Arc; @@ -16,9 +16,9 @@ async fn main() -> Result<()> { let users = Arc::>>::default(); - Server::bind(("0.0.0.0", GEMINI_PORT)) + Server::new() .add_route("/", move|req| handle_request(users.clone(), req)) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/document.rs b/examples/document.rs index a3d3a6c..032a03b 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use kochab::{Server, Response, GEMINI_PORT, Document}; +use kochab::{Server, Response, Document}; use kochab::document::HeadingLevel::*; #[tokio::main] @@ -38,8 +38,8 @@ async fn main() -> Result<()> { )) .into(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", response) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/ratelimiting.rs b/examples/ratelimiting.rs index 9838d82..455fc22 100644 --- a/examples/ratelimiting.rs +++ b/examples/ratelimiting.rs @@ -2,7 +2,7 @@ use std::time::Duration; use anyhow::*; use log::LevelFilter; -use kochab::{Server, Request, Response, GEMINI_PORT, Document}; +use kochab::{Server, Request, Response, Document}; #[tokio::main] async fn main() -> Result<()> { @@ -10,10 +10,10 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", handle_request) .ratelimit("/limit", 2, Duration::from_secs(60)) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/routing.rs b/examples/routing.rs index dfa871b..2586c58 100644 --- a/examples/routing.rs +++ b/examples/routing.rs @@ -1,6 +1,6 @@ use anyhow::*; use log::LevelFilter; -use kochab::{Document, document::HeadingLevel, Request, Response, GEMINI_PORT}; +use kochab::{Document, document::HeadingLevel, Request, Response}; #[tokio::main] async fn main() -> Result<()> { @@ -8,11 +8,11 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - kochab::Server::bind(("localhost", GEMINI_PORT)) + kochab::Server::new() .add_route("/", handle_base) .add_route("/route", handle_short) .add_route("/route/long", handle_long) - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index a41ca7e..fdbc846 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use anyhow::*; use log::LevelFilter; -use kochab::{Server, GEMINI_PORT}; +use kochab::Server; #[tokio::main] async fn main() -> Result<()> { @@ -10,9 +10,9 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("localhost", GEMINI_PORT)) + Server::new() .add_route("/", PathBuf::from("public")) // Serve directory listings & file contents .add_route("/about", PathBuf::from("README.md")) // Serve a single file - .serve() + .serve_unix("kochab.sock") .await } diff --git a/examples/user_management.rs b/examples/user_management.rs index bbb14f1..d0e945d 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,7 +1,6 @@ use anyhow::*; use log::LevelFilter; use kochab::{ - GEMINI_PORT, Document, Request, Response, @@ -26,7 +25,7 @@ async fn main() -> Result<()> { .filter_module("kochab", LevelFilter::Debug) .init(); - Server::bind(("0.0.0.0", GEMINI_PORT)) + Server::new() // Add our main routes .add_authenticated_route("/", handle_main) @@ -36,7 +35,7 @@ async fn main() -> Result<()> { .add_um_routes::() // Start the server - .serve() + .serve_unix("kochab.sock") .await } diff --git a/molly-brown.conf b/molly-brown.conf new file mode 100644 index 0000000..c27d70c --- /dev/null +++ b/molly-brown.conf @@ -0,0 +1,20 @@ +# This is a super simple molly brown config file for the purpose of testing SCGI +# applications. Although you are welcome to use this as a base for an actual webserver, +# please find somewhere better for your production sockets. +# +# You can get a copy of molly brown and more information about configuring it from: +# https://tildegit.org/solderpunk/molly-brown +# +# Once installed, run the test server using the command +# molly-brown -c molly-brown.conf + +Port = 1965 +Hostname = "localhost" +CertPath = "cert/cert.pem" +KeyPath = "cert/key.pem" + +AccessLog = "/dev/stdout" +ErrorLog = "/dev/stderr" + +[SCGIPaths] +"/" = "kochab.sock" diff --git a/src/lib.rs b/src/lib.rs index 83ae391..c820eb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,26 +1,43 @@ #[macro_use] extern crate log; use std::{ - convert::TryFrom, - io::BufReader, sync::Arc, - path::PathBuf, time::Duration, }; -#[cfg(feature = "ratelimiting")] -use std::net::IpAddr; +#[cfg(feature = "gemini_srv")] +use std::{ + convert::TryFrom, + path::PathBuf, +}; +#[cfg(feature = "scgi_srv")] +use std::{ + collections::HashMap, + net::SocketAddr, + str::FromStr, +}; use tokio::{ + io, + io::BufReader, + net::TcpListener, + net::ToSocketAddrs, prelude::*, - io::{self, BufStream}, - net::{TcpStream, ToSocketAddrs}, +}; +#[cfg(feature = "scgi_srv")] +use tokio::net::UnixListener; +#[cfg(feature = "gemini_srv")] +use tokio::{ time::timeout, + net::TcpStream, }; #[cfg(feature = "ratelimiting")] use tokio::time::interval; -use tokio::net::TcpListener; +#[cfg(feature = "gemini_srv")] use rustls::ClientCertVerifier; +#[cfg(feature = "gemini_srv")] use rustls::internal::msgs::handshake::DigitallySignedStruct; +#[cfg(feature = "gemini_srv")] use tokio_rustls::{rustls, TlsAcceptor}; +#[cfg(feature = "gemini_srv")] use rustls::*; use anyhow::*; use crate::util::opt_timeout; @@ -54,6 +71,7 @@ use handling::Handler; #[derive(Clone)] struct ServerInner { + #[cfg(feature = "gemini_srv")] tls_acceptor: TlsAcceptor, routes: Arc>, timeout: Duration, @@ -65,7 +83,7 @@ struct ServerInner { } impl ServerInner { - async fn serve(self, listener: TcpListener) -> Result<()> { + async fn serve_ip(self, listener: TcpListener) -> Result<()> { #[cfg(feature = "ratelimiting")] tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); @@ -82,48 +100,110 @@ impl ServerInner { } } - async fn serve_client(self, stream: TcpStream) -> Result<()> { - #[cfg(feature="ratelimiting")] - let peer_addr = stream.peer_addr()?.ip(); + #[cfg(feature = "scgi_srv")] + // Yeah it's code duplication, but I can't find a way around it, so this is what we're + // getting for now + async fn serve_unix(self, listener: UnixListener) -> Result<()> { + #[cfg(feature = "ratelimiting")] + tokio::spawn(prune_ratelimit_log(self.rate_limits.clone())); + loop { + let (stream, _addr) = listener.accept().await + .context("Failed to accept client")?; + let this = self.clone(); + + tokio::spawn(async move { + if let Err(err) = this.serve_client(stream).await { + error!("{:?}", err); + } + }); + } + } + + async fn serve_client( + &self, + #[cfg(feature = "gemini_srv")] + stream: TcpStream, + #[cfg(feature = "scgi_srv")] + stream: impl AsyncWrite + AsyncRead + Unpin, + ) -> Result<()> { let fut_accept_request = async { + #[cfg(feature = "gemini_srv")] let stream = self.tls_acceptor.accept(stream).await .context("Failed to establish TLS session")?; - let mut stream = BufStream::new(stream); + let mut stream = BufReader::new(stream); - #[cfg(feature="user_management")] let request = self.receive_request(&mut stream).await .context("Failed to receive request")?; - #[cfg(not(feature="user_management"))] - let request = Self::receive_request(&mut stream).await - .context("Failed to receive request")?; Result::<_, anyhow::Error>::Ok((request, stream)) }; - // Use a timeout for interacting with the client - let fut_accept_request = timeout(self.timeout, fut_accept_request); - let (mut request, mut stream) = fut_accept_request.await - .context("Client timed out while waiting for response")??; - #[cfg(feature="ratelimiting")] + // Wait for the request to be parsed + let (mut request, mut stream) = { + #[cfg(feature = "gemini_srv")] { + // Use a timeout for interacting with the client + let fut_accept_request = timeout(self.timeout, fut_accept_request); + fut_accept_request.await + .context("Client timed out while waiting for response")?? + } + #[cfg(feature = "scgi_srv")] + fut_accept_request.await? + }; + + // Determine the remote client's IP address for logging and ratelimiting + let peer_addr = { + #[cfg(feature = "gemini_srv")] { + stream.get_ref() + .get_ref() + .0 + .peer_addr()? + .ip() + } + #[cfg(feature = "scgi_srv")] { + SocketAddr::from_str( + request.headers() + .get("REMOTE_ADDR") + .ok_or(ParseError::Malformed("REMOTE_ADDR header not received"))? + .as_str() + ).context("Received malformed IP address from upstream")? + .ip() + } + }; + + #[cfg(feature = "ratelimiting")] + // Perform ratelimiting checks if let Some(resp) = self.check_rate_limits(peer_addr, &request) { + + // Log warning + warn!( + "Client from {} requesting {} was turned away by ratelimiting", + peer_addr, + request.uri() + ); + + // Send error response self.send_response(resp, &mut stream).await .context("Failed to send response")?; + + // Exit return Ok(()) } - debug!("Client requested: {}", request.uri()); + info!("{} requested: {}", peer_addr, request.uri()); // Identify the client certificate from the tls stream. This is the first // certificate in the certificate chain. - let client_cert = stream.get_ref() - .get_ref() - .1 - .get_peer_certificates() - .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); + #[cfg(feature = "gemini_srv")] { // This is done earlier for `scgi_srv` + let client_cert = stream.get_ref() + .get_ref() + .1 + .get_peer_certificates() + .and_then(|mut v| if v.is_empty() {None} else {Some(v.remove(0))}); - request.set_cert(client_cert); + request.set_cert(client_cert); + } let response = if let Some((trailing, handler)) = self.routes.match_request(&request) { request.set_trailing(trailing); @@ -197,13 +277,13 @@ impl ServerInner { None } + #[cfg(feature = "gemini_srv")] async fn receive_request( - #[cfg(feature="user_management")] &self, - stream: &mut (impl AsyncBufRead + Unpin) + stream: &mut (impl AsyncBufRead + Unpin), ) -> Result { - let limit = REQUEST_URI_MAX_LEN + "\r\n".len(); - let mut stream = stream.take(limit as u64); + const HEADER_LIMIT: usize = REQUEST_URI_MAX_LEN + "\r\n".len(); + let mut stream = stream.take(HEADER_LIMIT as u64); let mut uri = Vec::new(); stream.read_until(b'\n', &mut uri).await?; @@ -223,23 +303,123 @@ impl ServerInner { let uri = URIReference::try_from(&*uri) .context("Request URI is invalid")? .into_owned(); - let request = Request::from_uri( + + Request::new( uri, #[cfg(feature="user_management")] self.manager.clone(), - ) .context("Failed to create request from URI")?; + ).context("Failed to create request from URI") + } - Ok(request) + #[cfg(feature = "scgi_srv")] + async fn receive_request( + &self, + stream: &mut (impl AsyncBufRead + Unpin), + ) -> Result { + let mut buff = Vec::with_capacity(4); + + #[allow(clippy::char_lit_as_u8)] + // Read the length of the header netstring (e.g. "120:") + stream.read_until(':' as u8, &mut buff).await?; + + buff.pop(); // Remove the trailing ':' + let len = std::str::from_utf8(&*buff) + .ok() + .and_then(|s| usize::from_str(s).ok()) + .ok_or(ParseError::Malformed("netstring length"))?; + + // Read in the headers + buff.clear(); + buff.resize(len + 1, 0); + stream.read_exact(buff.as_mut()).await?; + buff.truncate(len - 1); // Remove the final \x00, + + // Parse the headers + let (maybe_trailing, headers) = buff.split(|b| *b == 0) // Headers are null delimiited + .map(|bytes| // Convert to an &str + std::str::from_utf8(bytes) + .map_err(|_| ParseError::Malformed("scgi headers")) + .map(str::trim) + ) + .try_fold( // Turn the array of [header, value, header, ...] into a map + (Option::<&str>::None, HashMap::::with_capacity(16)), + |(last_header, mut headers), s| { + s.map(|text| { + match last_header { + None => (Some(text), headers), + Some(header) => { + headers.insert(header.to_string(), text.to_string()); + (None, headers) + } + } + }) + } + )?; + + // If there's not the same number of headers as values, that's a problem + if maybe_trailing.is_some() { + bail!(ParseError::Malformed("trailing header")); + } + + // Check the content length info + let cont_len_val = headers.get("CONTENT_LENGTH") + .ok_or(ParseError::Malformed("No content length header!"))?; + let cont_len = usize::from_str(cont_len_val) + .map_err(|_| ParseError::Malformed("Malformed content length"))?; + if cont_len > 0 { + bail!(ParseError::Malformed("Gemini SCGI requests should not have a body")); + } + + // Spec requires setting an SCGI header to one + if *headers.get("SCGI").ok_or(ParseError::Malformed("No SCGI header"))? != "1" { + bail!(ParseError::Malformed("SCGI header not set to \"1\"")); + } + + trace!("Headers received: {:?}", headers); + + Ok(Request::new(headers)?) } } -pub struct Server { - addr: A, - cert_path: PathBuf, - key_path: PathBuf, +#[derive(Debug)] +#[cfg(feature = "scgi_srv")] +enum ParseError { + IO(io::Error), + Malformed(&'static str), +} + +#[cfg(feature = "scgi_srv")] +impl From for ParseError { + fn from(e: io::Error) -> Self { + Self::IO(e) + } +} + +#[cfg(feature = "scgi_srv")] +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::IO(e) => write!(f, "IO Error while parsing and responding SCGI: {}", e), + Self::Malformed(e) => write!(f, "SCGI request malformed at {}", e), + } + } +} + +#[cfg(feature = "scgi_srv")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + if let Self::IO(e) = self { Some(e) } else { None } + } +} + +pub struct Server { timeout: Duration, complex_body_timeout_override: Option, routes: RoutingNode, + #[cfg(feature = "gemini_srv")] + cert_path: PathBuf, + #[cfg(feature = "gemini_srv")] + key_path: PathBuf, #[cfg(feature="ratelimiting")] rate_limits: RoutingNode>, #[cfg(feature="user_management")] @@ -250,15 +430,16 @@ pub struct Server { certgen_mode: CertGenMode, } -impl Server { - pub fn bind(addr: A) -> Self { +impl Server { + pub fn new() -> Self { Self { - addr, timeout: Duration::from_secs(1), complex_body_timeout_override: Some(Duration::from_secs(30)), - cert_path: PathBuf::from("cert/cert.pem"), - key_path: PathBuf::from("cert/key.pem"), routes: RoutingNode::default(), + #[cfg(feature = "gemini_srv")] + cert_path: PathBuf::from("cert/cert.pem"), + #[cfg(feature = "gemini_srv")] + key_path: PathBuf::from("cert/key.pem"), #[cfg(feature="ratelimiting")] rate_limits: RoutingNode::default(), #[cfg(feature="user_management")] @@ -309,6 +490,7 @@ impl Server { self } + #[cfg(feature = "gemini_srv")] /// Sets the directory that kochab should look for TLS certs and keys into /// /// Northstar will look for files called `cert.pem` and `key.pem` in the provided @@ -324,6 +506,7 @@ impl Server { .set_key(dir.join("key.pem")) } + #[cfg(feature = "gemini_srv")] /// Set the path to the TLS certificate kochab will use /// /// This defaults to `cert/cert.pem`. @@ -335,6 +518,7 @@ impl Server { self } + #[cfg(feature = "gemini_srv")] /// Set the path to the ertificate key kochab will use /// /// This defaults to `cert/key.pem`. @@ -436,7 +620,8 @@ impl Server { self } - pub async fn serve(mut self) -> Result<()> { + fn build(mut self) -> Result { + #[cfg(feature = "gemini_srv")] let config = tls_config( &self.cert_path, &self.key_path, @@ -444,28 +629,51 @@ impl Server { self.certgen_mode ).context("Failed to create TLS config")?; - let listener = TcpListener::bind(self.addr).await - .context("Failed to create socket")?; - self.routes.shrink(); #[cfg(feature="user_management")] let data_dir = self.data_dir; - let server = ServerInner { - tls_acceptor: TlsAcceptor::from(config), + Ok(ServerInner { routes: Arc::new(self.routes), timeout: self.timeout, complex_timeout: self.complex_body_timeout_override, + #[cfg(feature = "gemini_srv")] + tls_acceptor: TlsAcceptor::from(config), #[cfg(feature="ratelimiting")] rate_limits: Arc::new(self.rate_limits), #[cfg(feature="user_management")] manager: UserManager::new( self.database.unwrap_or_else(move|| sled::open(data_dir).unwrap()) )?, - }; + }) + } - server.serve(listener).await + /// Start serving requests on a given bound address & port + /// + /// `addr` can be anything `tokio` can parse, including just a string like + /// "localhost:1965" + pub async fn serve_ip(self, addr: impl ToSocketAddrs) -> Result<()> { + let server = self.build()?; + let socket = TcpListener::bind(addr).await?; + server.serve_ip(socket).await + } + + #[cfg(feature = "scgi_srv")] + /// Start serving requests on a given unix socket + /// + /// Requires an address in the form of a path to bind to. This is only available when + /// in `scgi_srv` mode. + pub async fn serve_unix(self, addr: impl AsRef) -> Result<()> { + let server = self.build()?; + let socket = UnixListener::bind(addr)?; + server.serve_unix(socket).await + } +} + +impl Default for Server { + fn default() -> Self { + Self::new() } } @@ -512,6 +720,7 @@ async fn prune_ratelimit_log(rate_limits: Arc>>) } } +#[cfg(feature = "gemini_srv")] fn tls_config( cert_path: &PathBuf, key_path: &PathBuf, @@ -535,20 +744,22 @@ fn tls_config( Ok(config.into()) } +#[cfg(feature = "gemini_srv")] fn load_cert_chain(cert_path: &PathBuf) -> Result> { let certs = std::fs::File::open(cert_path) .with_context(|| format!("Failed to open `{:?}`", cert_path))?; - let mut certs = BufReader::new(certs); + let mut certs = std::io::BufReader::new(certs); let certs = rustls::internal::pemfile::certs(&mut certs) .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; Ok(certs) } +#[cfg(feature = "gemini_srv")] fn load_key(key_path: &PathBuf) -> Result { let keys = std::fs::File::open(key_path) .with_context(|| format!("Failed to open `{:?}`", key_path))?; - let mut keys = BufReader::new(keys); + let mut keys = std::io::BufReader::new(keys); let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; @@ -559,11 +770,14 @@ fn load_key(key_path: &PathBuf) -> Result { Ok(key) } +#[cfg(feature = "gemini_srv")] /// A client cert verifier that accepts all connections /// /// Unfortunately, rustls doesn't provide a ClientCertVerifier that accepts self-signed /// certificates, so we need to implement this ourselves. struct AllowAnonOrSelfsignedClient { } + +#[cfg(feature = "gemini_srv")] impl AllowAnonOrSelfsignedClient { /// Create a new verifier @@ -573,6 +787,7 @@ impl AllowAnonOrSelfsignedClient { } +#[cfg(feature = "gemini_srv")] impl ClientCertVerifier for AllowAnonOrSelfsignedClient { fn client_auth_root_subjects( diff --git a/src/types.rs b/src/types.rs index 0ad442d..ccbf1b2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,7 @@ +#[cfg(feature = "gemini_srv")] pub use rustls::Certificate; +#[cfg(feature = "scgi_srv")] +pub type Certificate = String; pub use uriparse::URIReference; mod meta; diff --git a/src/types/request.rs b/src/types/request.rs index a4889d3..db812c4 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,8 +1,13 @@ use std::ops; +#[cfg(feature = "scgi_srv")] +use std::{ + collections::HashMap, + convert::TryFrom, +}; use anyhow::*; use percent_encoding::percent_decode_str; use uriparse::URIReference; -use rustls::Certificate; +use crate::types::Certificate; #[cfg(feature="user_management")] use serde::{Serialize, de::DeserializeOwned}; @@ -16,28 +21,37 @@ pub struct Request { trailing_segments: Option>, #[cfg(feature="user_management")] manager: UserManager, + #[cfg(feature = "scgi_srv")] + headers: HashMap, } impl Request { - pub fn from_uri( - uri: URIReference<'static>, - #[cfg(feature="user_management")] - manager: UserManager, - ) -> Result { - Self::with_certificate( - uri, - None, - #[cfg(feature="user_management")] - manager - ) - } - - pub fn with_certificate( + pub fn new( + #[cfg(feature = "gemini_srv")] mut uri: URIReference<'static>, - certificate: Option, + #[cfg(feature = "scgi_srv")] + headers: HashMap, #[cfg(feature="user_management")] manager: UserManager, ) -> Result { + #[cfg(feature = "scgi_srv")] + let (mut uri, certificate) = ( + URIReference::try_from( + format!( + "{}{}", + headers.get("PATH_INFO") + .context("PATH_INFO header not received from SCGI client")? + .as_str(), + headers.get("QUERY_STRING") + .map(|q| format!("?{}", q)) + .unwrap_or_else(String::new), + ).as_str() + ) + .context("Request URI is invalid")? + .into_owned(), + headers.get("TLS_CLIENT_HASH").cloned(), + ); + uri.normalize(); let input = match uri.query() { @@ -54,8 +68,13 @@ impl Request { Ok(Self { uri, input, + #[cfg(feature = "scgi_srv")] certificate, + #[cfg(feature = "gemini_srv")] + certificate: None, trailing_segments: None, + #[cfg(feature = "scgi_srv")] + headers, #[cfg(feature="user_management")] manager, }) @@ -103,6 +122,24 @@ impl Request { self.input.as_deref() } + #[cfg(feature="scgi_srv")] + /// View any headers sent by the SCGI client + /// + /// When an SCGI client delivers a request (e.g. when your gemini server sends a + /// request to this app), it includes many headers which aren't always included in + /// the request otherwise. Bear in mind that **not all SCGI clients send the same + /// headers**, and these are *never* available when operating in `gemini_srv` mode. + /// + /// Some examples of headers mollybrown sets are: + /// - `REMOTE_ADDR` (The user's IP address and port) + /// - `TLS_CLIENT_SUBJECT_CN` (The CommonName on the user's certificate, when present) + /// - `SERVER_NAME` (The host name of the server the request was received on) + /// - `SERVER_SOFTWARE` (= "MOLLY_BROWN") + /// - `SCRIPT_PATH` (The prefix the script is being served on) + pub const fn headers(&self) -> &HashMap { + &self.headers + } + pub fn set_cert(&mut self, cert: Option) { self.certificate = cert; }