use crate::error::{bad_req, error, Context, ErrorConv, GemError, Result};
use crate::CONF;
use async_net::SocketAddr;
use regex::Captures;
use std::convert::From;
use std::convert::TryFrom;
use std::path::PathBuf;
use uzers::User;
use x509_parser::prelude::X509Certificate;
#[derive(Debug, Clone)]
pub struct Route {
pub domain: String,
pub path: RoutePath,
pub rewrite: Option<String>,
pub lang: Option<String>,
pub charset: Option<String>,
pub route_type: RouteType,
pub cert_key_path: Option<(PathBuf, PathBuf)>,
pub client_cert: Option<X509Certificate<'static>>,
}
#[derive(Debug, Clone)]
pub enum RouteType {
Static(StaticRoute),
Cgi(CGIRoute),
Scgi(SCGIRoute),
Redirect(RedirectRoute),
}
impl Display for RouteType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Static(_) => write!(f, "static"),
Self::Cgi(_) => write!(f, "cgi"),
Self::Scgi(_) => write!(f, "scgi"),
Self::Redirect(_) => write!(f, "redirect"),
}
}
}
#[derive(Debug, Clone)]
pub struct StaticRoute {
pub root: PathBuf,
pub index: String,
pub auto_index: bool,
pub mime_override: Option<String>,
}
impl From<StaticRoute> for RouteType {
fn from(route: StaticRoute) -> Self {
RouteType::Static(route)
}
}
#[derive(Debug, Clone)]
pub struct CGIRoute {
pub root: PathBuf,
pub index: String,
pub user: Option<User>,
pub timeout: Option<u64>,
}
impl From<CGIRoute> for RouteType {
fn from(route: CGIRoute) -> Self {
RouteType::Cgi(route)
}
}
#[derive(Debug, Clone)]
pub struct SCGIRoute {
pub addr: SCGIAddress,
pub timeout: Option<u64>,
}
impl From<SCGIRoute> for RouteType {
fn from(route: SCGIRoute) -> Self {
RouteType::Scgi(route)
}
}
#[derive(Debug, Clone)]
pub enum SCGIAddress {
Unix(PathBuf),
Tcp(SocketAddr),
}
#[derive(Debug, Clone)]
pub struct RedirectRoute {
pub url: String,
pub permanent: bool,
pub redirect_rewrite: bool,
}
impl From<RedirectRoute> for RouteType {
fn from(route: RedirectRoute) -> Self {
RouteType::Redirect(route)
}
}
#[derive(Debug, Clone)]
pub enum RoutePath {
All,
Prefix(String),
Exact(String),
Regex(Box<regex::Regex>),
}
pub fn route(uri: &str) -> Result<(&'static Route, Request)> {
let mut req = Request::from_uri(uri)?;
let mut domain_found = false;
for site in &CONF.routes {
if site.domain != req.host {
continue;
}
domain_found = true;
if match &site.path {
RoutePath::All => true,
RoutePath::Prefix(s) => req.path.starts_with(s),
RoutePath::Exact(s) => req.path == *s,
RoutePath::Regex(r) => {
if let Some(captures) = r.captures(&req.path) {
if let Some(rewrite) = &site.rewrite {
if rewrite.contains('\\') {
let new_path = do_rewrite(rewrite, captures)
.map_err(|e| {
GemError::ServerError(
e.unwrap_err().context(format!(
"Error doing rewrite {rewrite}"
)),
)
})?;
req.path = new_path;
} else {
req.path.clone_from(rewrite);
};
}
true
} else {
false
}
}
} {
return Ok((site, req));
}
}
if domain_found {
Err(GemError::NotFound)
} else {
Err(GemError::NoProxy(anyhow::anyhow!(
"No route for host: {}",
req.host
)))
}
}
fn do_rewrite(input: &str, captures: Captures<'_>) -> Result<String> {
#[derive(PartialEq, Eq, Debug)]
enum State {
Text,
Capture,
Named(String),
}
let mut state = State::Text;
let mut output = String::with_capacity(input.len());
for c in input.chars() {
match &mut state {
State::Text if c == '\\' => state = State::Capture,
State::Text => output.push(c),
State::Capture if c.is_ascii_digit() => {
let idx = c.to_digit(10).unwrap() as usize;
let capture = captures.get(idx).with_context(|| {
format!("Url didn't contain capture `{idx}`")
})?;
output.push_str(capture.as_str());
state = State::Text;
}
State::Capture if c == '{' => state = State::Named(String::new()),
State::Capture => return Err(error("Invalid rewrite directive")),
State::Named(s) if c == '}' => {
let capture = captures.name(s).with_context(|| {
format!("Url didn't contain capture `{s}`")
})?;
output.push_str(capture.as_str());
state = State::Text;
}
State::Named(s) => {
s.push(c);
}
}
}
if state != State::Text {
return Err(error(
"Invalid rewrite directive, incomplete capture directive",
));
}
Ok(output)
}
#[derive(Debug, Clone)]
pub struct Request {
pub uri: String,
pub path: String,
pub query: String,
pub host: String,
}
impl Request {
fn from_uri(uri_str: &str) -> Result<Request> {
use uriparse::URIReference;
if uri_str.is_empty() {
return Err(bad_req("Empty URL"));
}
let mut uri = URIReference::try_from(uri_str)
.with_context(|| format!("Bad URI: {uri_str}"))
.into_bad_req()?;
let mut path = uri.path().to_owned();
let authority = uri
.authority()
.context("Username and password URI fields not allowed")
.into_bad_req()?;
if authority.has_password() || authority.has_username() {
return Err(bad_req(
"Username and password URI fields not allowed",
));
}
let host = authority.host().to_string().to_lowercase();
let port = authority.port();
let scheme = uri
.scheme()
.context("URI mush have a scheme")
.into_bad_req()?;
if scheme.as_str() != "gemini" {
return Err(GemError::NoProxy(anyhow::anyhow!(
"Schema {scheme} not supported"
)));
}
if let Some(port) = port {
if !CONF
.listen
.iter()
.fold(false, |found, addr| (addr.port() == port) || found)
{
return Err(GemError::NoProxy(anyhow::anyhow!(
"Port {port} not listened on",
)));
}
}
if host.is_empty() {
return Err(bad_req("No relative URLs"));
}
if !path.is_normalized(false) {
path.normalize(false);
uri.set_path(path).context("Path normalization failed")?;
return Err(GemError::Redirect(uri.to_string()));
}
let query = uri.query().map(|v| v.as_str()).unwrap_or("").to_owned();
Ok(Request {
uri: uri.to_string(),
host,
query,
path: path.to_string(),
})
}
}
use std::fmt::{self, Display, Formatter};
impl Display for Request {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.uri.fmt(f)
}
}