use crate::{
client::TitanResource,
err::{AppErr, StreamErr},
gem_call::GemCall,
Client, GemBytes,
};
use matchit::Router;
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod, SslVerifyMode};
use std::{net::SocketAddr, path::PathBuf, pin::Pin, sync::Arc, time};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
};
use tokio_openssl::SslStream;
use url::Url;
pub struct App<S = ()> {
state: S,
address: String,
not_found: String,
routes: Router<(Box<dyn GemCall<S> + Send + Sync>, bool, usize)>,
key: PathBuf,
cert: PathBuf,
}
impl Default for App<()> {
fn default() -> Self {
Self {
state: (),
address: std::env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:1965")),
not_found: String::from("Page not found."),
routes: Router::default(),
key: PathBuf::from("key.pem"),
cert: PathBuf::from("cert.pem"),
}
}
}
impl<S> App<S>
where
S: Send + Sync + Clone + 'static,
{
pub fn route(mut self, path: &str, func: impl GemCall<S> + 'static + Sync + Send) -> Self {
self.routes
.insert(path, (Box::new(func), false, 0))
.unwrap();
self
}
pub fn not_found(mut self, message: impl Into<String>) -> Self {
self.not_found = message.into();
self
}
pub fn address(mut self, addr: impl Into<String>) -> Self {
self.address = addr.into();
self
}
pub fn path_to_cert(mut self, path: impl Into<PathBuf>) -> Self {
self.cert = path.into();
self
}
pub fn path_to_key(mut self, path: impl Into<PathBuf>) -> Self {
self.key = path.into();
self
}
pub fn titan(
mut self,
path: &str,
func: impl GemCall<S> + 'static + Sync + Send,
max_size: usize,
) -> Self {
self.routes
.insert(path, (Box::new(func), true, max_size))
.unwrap();
self
}
pub fn state<T: Send + Sync + Clone>(self, state: T) -> App<T> {
App {
state,
routes: Router::default(),
address: self.address,
not_found: self.not_found,
cert: self.cert,
key: self.key,
}
}
pub async fn run(self) -> Result<(), AppErr> {
#[cfg(feature = "interactive")]
crate::interactive::gen_cert(&self.cert, &self.key)?;
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls_server())?;
builder
.set_private_key_file(&self.key, SslFiletype::PEM)
.map_err(|e| AppErr::Key(e, self.key.display().to_string()))?;
builder
.set_certificate_file(&self.cert, SslFiletype::PEM)
.map_err(|e| AppErr::Cert(e, self.cert.display().to_string()))?;
builder.check_private_key()?;
builder.set_verify_callback(SslVerifyMode::PEER, |_, _| true);
builder.set_session_id_context(
time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)?
.as_secs()
.to_string()
.as_bytes(),
)?;
let acceptor = builder.build();
let listener = TcpListener::bind(&self.address)
.await
.map_err(AppErr::Bind)?;
info!("🦊 App running [{}]", self.address);
let self_arc = Arc::new(self);
loop {
let Ok((stream, addr)) = listener.accept().await else {
continue;
};
let Ok(ssl) = Ssl::new(acceptor.context()) else {
continue;
};
let Ok(stream) = SslStream::new(ssl, stream) else {
continue;
};
let self_clone = Arc::clone(&self_arc);
tokio::spawn(async move {
match self_clone.handle_stream(stream, addr).await {
Ok(_) => (),
Err(e) => debug!("🦊 Stream error: {e}"),
}
});
}
}
async fn handle_stream(
&self,
mut stream: SslStream<TcpStream>,
addr: SocketAddr,
) -> Result<(), StreamErr> {
Pin::new(&mut stream).accept().await?;
let mut url: Vec<u8> = Vec::new();
let mut p = b' ';
for _ in 0..1020 {
let c = stream.read_u8().await.map_err(StreamErr::Read)?;
if p == b'\r' && c == b'\n' {
url.pop();
break;
}
url.push(c);
p = c;
}
let url = std::str::from_utf8(&url)?;
let (titan_params, url) = match &url[0..6] {
"titan:" => {
let mut params = url.splitn(4, ';');
let url = params
.next()
.ok_or(StreamErr::TitanParams(url.to_string()))?;
(Some(params), url)
}
_ => (None, url),
};
let mut url = Url::parse(url)?;
if url.path().is_empty() {
url.set_path("/");
}
let path = urlencoding::decode(url.path())
.map_err(StreamErr::UrlDecode)?
.to_string();
let response = match self.routes.at(&path) {
Ok(route) => {
let (route_fn, titan_enabled, titan_max) = route.value;
trace!("{addr} :: ✅🔗 Found [{url}]");
let mut titan_resource = None;
if *titan_enabled {
if let Some(titan_params) = &titan_params {
let mut r = TitanResource {
mime: String::new(),
size: 0,
token: None,
content: Vec::new(),
};
for param in titan_params.clone() {
if let Some((key, value)) = param.split_once('=') {
match key {
"token" => {
r.token = Some(
urlencoding::decode(value)
.map_err(StreamErr::TitanToken)?
.to_string(),
)
}
"mime" => r.mime = value.to_string(),
"size" => {
r.size =
value.parse::<usize>().map_err(StreamErr::TitanSize)?;
}
_ => (),
}
}
}
if r.size > *titan_max && *titan_max != 0 {
stream
.write_all(
format!("50 Content exceeds maximum number of bytes ({titan_max})\r\n")
.as_bytes(),
)
.await
.map_err(StreamErr::Write)?;
return Ok(());
}
r.content = vec![0u8; r.size];
stream
.read_exact(&mut r.content[..])
.await
.map_err(StreamErr::TitanRead)?;
titan_resource = Some(r);
}
}
if !(*titan_enabled) && titan_params.is_some() {
b"50 Titan content isn't accepted at this route.\r\n".into()
} else {
match Client::new(
self.state.clone(),
url,
stream.ssl().peer_certificate(),
&route.params,
addr,
titan_resource,
) {
Ok(client) => route_fn.gem_call(client).await,
Err(e) => (62, &e.to_string()).gem_bytes().await,
}
}
}
Err(e) => {
trace!("{addr} :: ❌🔗 Not found [{url}] :: {e}");
(51, &self.not_found).gem_bytes().await
}
};
stream
.write_all(&response)
.await
.map_err(StreamErr::Write)?;
stream.shutdown().await?;
Ok(())
}
}