fluffer 4.0.2

🦊 Fluffer is a fun and experimental gemini server framework.
Documentation
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;

/// 🖥️ A Fluffer App
///
/// ## Defaults
///
/// - Key: `./key.pem`
/// - Cert: `./cert.pem`
/// - Address: `127.0.0.1:1965` (or the `ADDRESS` environment variable)
///
/// ```
/// App::default()
///     .route("/", |_| async { "Hello :>" })
///     .run()
/// ```
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,
{
	/// Takes a generic function that implements [`GemCall`], and boxes it into our `routes` hashmap.
	///
	/// Will panic if an insert error occurs.
	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
	}

	/// Update the message displayed when a page isn't found.
	pub fn not_found(mut self, message: impl Into<String>) -> Self {
		self.not_found = message.into();
		self
	}

	/// Update the server's address.
	pub fn address(mut self, addr: impl Into<String>) -> Self {
		self.address = addr.into();
		self
	}

	/// Update certificate path.
	pub fn path_to_cert(mut self, path: impl Into<PathBuf>) -> Self {
		self.cert = path.into();
		self
	}

	/// Update key path.
	pub fn path_to_key(mut self, path: impl Into<PathBuf>) -> Self {
		self.key = path.into();
		self
	}

	/// The same as [`App::route`], but with titan enabled.
	///
	/// When the request's content exceeds `max_size`, a `PermanentFailure` is met.
	///
	/// Set `max_size` to 0 to disable this behaviour.
	///
	/// Will panic if an insert error occurs.
	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
	}

	/// Replace [`App`]'s unit state with State.
	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?;

		// 📖 Read url bytes
		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;
		}

		// 🌕 Split titan parameters from url
		let url = std::str::from_utf8(&url)?;
		let (titan_params, url) = match &url[0..6] {
			"titan:" => {
				// Separate titan parameters from url
				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)?;

		// 🔗 Normalize empty url paths
		if url.path().is_empty() {
			url.set_path("/");
		}

		// % Decode path
		let path = urlencoding::decode(url.path())
			.map_err(StreamErr::UrlDecode)?
			.to_string();

		// 🔁 Get response bytes
		let response = match self.routes.at(&path) {
			Ok(route) => {
				let (route_fn, titan_enabled, titan_max) = route.value;
				trace!("{addr} :: ✅🔗 Found [{url}]");

				// Build titan resource
				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(),
						};

						// Add parameters
						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)?;
									}
									_ => (),
								}
							}
						}

						// Error if upload size exceeds max
						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(());
						}

						// Allocate and read
						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 {
					// Build client
					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(())
	}
}