Completely reworked request handling to be able to serve SCGI
Multi ~~track~~ protocol ~~drifting~~ abstraction!!
This commit is contained in:
parent
86ed240761
commit
244fd25112
13
Cargo.toml
13
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"
|
||||
|
|
|
@ -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::<RwLock::<HashMap<CertBytes, String>>>::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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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::<String>()
|
||||
|
||||
// Start the server
|
||||
.serve()
|
||||
.serve_unix("kochab.sock")
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
20
molly-brown.conf
Normal file
20
molly-brown.conf
Normal file
|
@ -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"
|
325
src/lib.rs
325
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<RoutingNode<Handler>>,
|
||||
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<Request> {
|
||||
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<Request> {
|
||||
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::<String, String>::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<A> {
|
||||
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<io::Error> 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<Duration>,
|
||||
routes: RoutingNode<Handler>,
|
||||
#[cfg(feature = "gemini_srv")]
|
||||
cert_path: PathBuf,
|
||||
#[cfg(feature = "gemini_srv")]
|
||||
key_path: PathBuf,
|
||||
#[cfg(feature="ratelimiting")]
|
||||
rate_limits: RoutingNode<RateLimiter<IpAddr>>,
|
||||
#[cfg(feature="user_management")]
|
||||
|
@ -250,15 +430,16 @@ pub struct Server<A> {
|
|||
certgen_mode: CertGenMode,
|
||||
}
|
||||
|
||||
impl<A: ToSocketAddrs> Server<A> {
|
||||
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<A: ToSocketAddrs> Server<A> {
|
|||
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<A: ToSocketAddrs> Server<A> {
|
|||
.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<A: ToSocketAddrs> Server<A> {
|
|||
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<A: ToSocketAddrs> Server<A> {
|
|||
self
|
||||
}
|
||||
|
||||
pub async fn serve(mut self) -> Result<()> {
|
||||
fn build(mut self) -> Result<ServerInner> {
|
||||
#[cfg(feature = "gemini_srv")]
|
||||
let config = tls_config(
|
||||
&self.cert_path,
|
||||
&self.key_path,
|
||||
|
@ -444,28 +629,51 @@ impl<A: ToSocketAddrs> Server<A> {
|
|||
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<std::path::Path>) -> 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<RoutingNode<RateLimiter<IpAddr>>>)
|
|||
}
|
||||
}
|
||||
|
||||
#[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<Vec<Certificate>> {
|
||||
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<PrivateKey> {
|
||||
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<PrivateKey> {
|
|||
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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Vec<String>>,
|
||||
#[cfg(feature="user_management")]
|
||||
manager: UserManager,
|
||||
#[cfg(feature = "scgi_srv")]
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn from_uri(
|
||||
uri: URIReference<'static>,
|
||||
#[cfg(feature="user_management")]
|
||||
manager: UserManager,
|
||||
) -> Result<Self> {
|
||||
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<Certificate>,
|
||||
#[cfg(feature = "scgi_srv")]
|
||||
headers: HashMap<String, String>,
|
||||
#[cfg(feature="user_management")]
|
||||
manager: UserManager,
|
||||
) -> Result<Self> {
|
||||
#[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<String, String> {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
pub fn set_cert(&mut self, cert: Option<Certificate>) {
|
||||
self.certificate = cert;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue