pub mod ogp_images; pub mod contrast; pub mod statics; pub mod configuration; mod write_vectored_all; use async_net::unix::UnixListener; use crate::configuration::Conf; use std::io::IoSlice; use crate::statics::StaticAsset; use async_io::block_on; use async_net::TcpListener; use async_executor::LocalExecutor; use futures_lite::io::AsyncWriteExt; use futures_lite::stream::StreamExt; use std::borrow::Cow; use form_urlencoded; use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; use pronouns_today::user_preferences::ParseError; use pronouns_today::UserPreferences; use configuration::ConfigError; use write_vectored_all::AsyncWriteAllVectored; use std::net::SocketAddr; use std::process::exit; use std::ops::Deref; use argh; use askama::Template; use async_scgi::{ScgiReadError, ScgiRequest}; use pronouns_today::user_preferences::Preference; use pronouns_today::{InstanceSettings, Pronoun}; #[cfg(feature = "ogp_images")] use image::{DynamicImage, ImageOutputFormat}; #[cfg(feature = "ogp_images")] use ogp_images::render_today; const NOT_FOUND: &[u8] = include_bytes!("../templates/404.html"); /// Handles initialization, and runs top-level routing based on cli args fn main() { env_logger::init(); let args: configuration::PronounsTodayArgs = argh::from_env(); match args.command { configuration::SubCommand::DumpStatics(_subargs) => { println!("Support for dumping statics not yet implemented"); return; } configuration::SubCommand::Run(subargs) => { let config = match subargs.load_config() { Ok(config) => config, Err(ConfigError::IoError(e)) => { eprintln!("IO Error while reading config! {}", e); exit(1000); } Err(ConfigError::MalformedConfig(e)) => { eprintln!("Error parsing config file:\n{}", e); exit(1001); } Err(ConfigError::ConfigCreated(path)) => { println!("A config file has been generated at {}! Please check it out \ and modify it to your liking, and then run this command \ again", path.display()); exit(1002); } }; let executor = LocalExecutor::new(); log::info!("Starting with configuration {:?}", config); block_on(executor.run(start_server(config, &executor))).unwrap(); } } } /// Bind the server and start receiving connections. /// /// Each connection will be spawned into a task running on the provided executor. Each /// connection is fully processed, but not in this method. Actual handling of the /// requests is delegated to the [`handle_request()`], which is the actual task that is /// spawned into the Executor. async fn start_server(config: configuration::Conf, executor: &LocalExecutor<'_>) -> std::io::Result<()> { // Make the configuration immortal let config = Box::leak(Box::new(config)); // Where we binding bois if let Ok(socket_addr) = config.bind.parse() as Result { println!("Starting pronouns-today-web on {}", &socket_addr); let connection = TcpListener::bind(socket_addr).await?; let mut incoming = connection.incoming(); while let Some(stream) = incoming.next().await { match stream { Ok(stream) => { executor.spawn(handle_request(stream, config)).detach(); }, Err(e) => { log::error!("IO Error with client/server connection: {}", e); } } } } else { let bind = config.bind.trim_start_matches("unix:"); let listener = UnixListener::bind(bind)?; println!("Listening for connections on unix:{}", bind); let mut incoming = listener.incoming(); while let Some(stream) = incoming.next().await { match stream { Ok(stream) => { executor.spawn(handle_request(stream, config)).detach(); }, Err(e) => { log::error!("IO Error with client/server connection: {}", e); } } } } Ok(()) } /// Handle one (1) connection from start to finish /// /// This accepts an ongoing connection, sets up IO, interprets it as an SCGI request, then /// determines what response to send, generates the response, serializes the response, and /// sends it back out to the client. /// /// Implementation details: /// - Interpretting the request is delegated to [`async_scgi::read_request()`] /// - Routing of the request is handled using [`route_request()`] /// - Generation of the response is done using [`Route::generate_response()`] /// - Serialization of the response is done using [`Response::into_bytes()`] async fn handle_request( raw_stream: impl futures_lite::AsyncRead + futures_lite::AsyncWrite + Unpin, conf: &Conf, ) { let mut stream = futures_lite::io::BufReader::new(raw_stream); let req = match async_scgi::read_request(&mut stream).await { Ok(req) => req, Err(ScgiReadError::IO(e)) => { log::error!("There was an IO error while reading a request. {}", e); exit(1003); }, Err(scgi_error) => { log::error!("Encountered a malformed SCGI request. It could be that your \ webserver is misconfigured. {}", scgi_error); return }, }; let response = route_request(&req) .generate_response(conf); let mut io_slices = response.into_io_slices(); if let Err(e) = stream.write_all_vectored(&mut io_slices).await { log::warn!("Encountered an IO error while sending response: {}", e); } if let Err(e) = stream.close().await { log::warn!( "Encountered an IO error while closing connection to server: {}", e ); } } /// Determine the appropriate response type for this request, aka perform routing /// /// For a given [`ScgiRequest`], attempt to figure out what route it falls under using a /// variety of factors. This also extracts important information from the request that is /// ultimately used to serve the request. /// /// See information about the return type, [`Route`] for more information. fn route_request(req: &ScgiRequest) -> Route { let method = req.headers .get("REQUEST_METHOD") .expect("Misconfigured server didn't send a REQUEST_METHOD header"); match method.as_str() { // GET requests are dealt with as below "GET" => (), // All POST requests are met with link generation "POST" => return Route::GenerateLink(req.body.clone()), // No other methods are supported _ => return Route::BadRequest(BadRequest::MethodNotAllowed), } let request_uri = req.headers .get("REQUEST_URI") .expect("Misconfigured server didn't send a REQUEST_URI header"); log::debug!("REQUEST_URI: {}", request_uri); let mut segments: Vec<_> = request_uri .split("/") .filter(|s| s.len() > 0) .collect(); #[cfg(feature = "embed_static_assets")] // If the route is /static/, check to see if it matches an asset first if segments.get(0).map(Deref::deref) == Some("static") { for asset in statics::STATIC_ASSETS { if Some(asset.filename) == segments.get(1).map(Deref::deref) { return Route::SendStatic(asset); } } } #[derive(PartialEq, Eq)] enum RouteType { Html, Thumbnail, Json, } // Determines if we need to respond with a thumbnail or a web page let route_type = match segments.last().map(Deref::deref) { Some("thumb.png") => RouteType::Thumbnail, Some("pns.json") => RouteType::Json, _ => RouteType::Html, }; if route_type != RouteType::Html { // Now that we've determined that, we don't need this segment anymore segments.pop(); } // Get the name and preferences let (name, prefs) = match (segments.get(0), segments.get(1)) { (None, None) => (None, None), (name@Some(_), Some(prefstr)) => { (name, match prefstr.parse() { Ok(prefs) => Some(prefs), Err(e) => return Route::BadRequest(BadRequest::MalformedPrefstr(e)), }) }, (Some(idk), None) => { // Try to parse the mystery arg as a prefstring, treat it as a name otherwise let result: Result = idk.parse(); if let Ok(preferences) = result { (None, Some(preferences)) } else { (Some(idk), None) } }, (None, Some(_)) => unreachable!(), }; let name = name.map(|n| (*n).to_owned()); match route_type { RouteType::Thumbnail => Route::SendThumbnail(name, prefs), RouteType::Html => { let uri = request_uri.trim_end_matches('/').to_owned(); Route::SendPronounPage(name, prefs, uri) } RouteType::Json => Route::SendJsonData(name, prefs), } } /// Determines the type of route that the request should be met with, and necessary data /// /// Used to facilitate request routing. The routing function ([`route_request()`]) can /// return a [`Route`] without needing to do any IO or actual computation. This helps to /// seperate the tasks of each function, and isolate any IO. /// /// Each variant of the enum is a type of response that needs to be issued. A variant /// might have parameters if these are necessary to generate the response. The idea is /// that the minimum of work should be able to determine the route of a request, and that /// a route may be turned into a response without needing any additional information from /// the request. /// /// The `impl` of struct also contains all of the routes themselves. You can either call /// a specific route using the contents of the [`Route`] enum, or call /// [`Route.generate_response()`] to automatically call the appropriate route. enum Route { #[cfg(feature = "embed_static_assets")] /// Respond to the request with one of the static files embeded in the application /// /// The wrapped pointer is a pointer to those bytes in memory. SendStatic(&'static StaticAsset), /// Respond with an HTML pronoun page. /// /// Takes the user's name (if specified), and the preferences of that user, or /// [`None`] for defults. The third string is the URI of the current page, /// ending without a / SendPronounPage(Option, Option, String), /// Respond with an image containing the user's pronouns /// /// Takes the user's name (optional), and the preferences of that user SendThumbnail(Option, Option), /// Respond with a json object containing the users pronouns /// /// Takes the user's name (optional), and the preferences of that user SendJsonData(Option, Option), /// Generate a link to the appropriate pronoun page based on the user's inputs. GenerateLink(Vec), /// There was a problem with the user's request, and an error page should be sent BadRequest(BadRequest), } /// Various ways clients can send a bad request enum BadRequest { /// The user sent a request with a nonsensical HTTP method MethodNotAllowed, /// The prefstr the user sent was bad and stinky MalformedPrefstr(ParseError), /// Happens when [`ParseError::PrefstringExceedsPronounCount`] is raised PrefsExceedPronounCount, /// The data sent in a form was malformed. The string is the part that failed to parse BadFormData(String), /// Happens when the user's preferences specify that no pronouns should be used /// /// Triggered by a [`ParseError::EmptyWeightedTable`] /// /// TODO maybe add a special display for this instead of calling it a bad request? NoPronouns, } impl Route { /// Actually perform the action that each route entails fn generate_response(self, conf: &Conf) -> Response { let settings = &conf.instance_settings; let result = match self { Route::SendStatic(data) => Ok(data.generate_response()), Route::SendPronounPage(name, prefs, url) => Route::send_pronoun_page(name, prefs, url, conf), Route::SendJsonData(name, prefs) => Route::send_pronoun_json(name, prefs, conf), Route::SendThumbnail(name, prefs) => Route::send_thumbnail(name, prefs, settings), Route::GenerateLink(body) => Route::generate_link(body, settings), Route::BadRequest(error) => Ok(Route::bad_request(error, settings)), }; result.unwrap_or_else(|e| Route::bad_request(e, settings)) } /// The method for the [`Route::SendPronounPage`] route fn send_pronoun_page( name: Option, prefs: Option, uri: String, conf: &Conf, ) -> Result { let settings = &conf.instance_settings; let pronoun = Route::get_pronoun(name.as_ref(), prefs, settings)?; let body = IndexTemplate { name: name.map( |name| percent_decode_str(&name) .decode_utf8_lossy() .into_owned() ), pronoun, pronouns: settings.pronoun_list.iter().enumerate().collect(), url: format!("{}{}", &conf.base_url, uri), } .render() .unwrap(); Ok(Response { status: b"200", headers: Cow::Borrowed(&[ (b"Trans-Women", Cow::Borrowed(b"don't owe you femininity")), (b"Trans-Men", Cow::Borrowed(b"don't owe you masculinity")), (b"And-Stop-Projecting-Your-Dated-Gender-Norms", Cow::Borrowed(b"onto nonbinary people's life experiences")), ]), body: body.into_bytes().into(), }) } /// The method for the [`Route::SendPronounPage`] route fn send_pronoun_json( name: Option, prefs: Option, conf: &Conf, ) -> Result { let settings = &conf.instance_settings; let pronoun = Route::get_pronoun(name.as_ref(), prefs, settings)?; let body = format!(r#" {{ "subject_pronoun": "{}", "object_pronoun": "{}", "possesive_determiner": "{}", "possesive_pronoun": "{}", "reflexive_pronoun": "{}" }} "#, pronoun.subject_pronoun, pronoun.object_pronoun, pronoun.possesive_determiner, pronoun.possesive_pronoun, pronoun.reflexive_pronoun); Ok(Response { status: b"200", headers: Cow::Borrowed(&[ (b"Trans-Women", Cow::Borrowed(b"don't owe you femininity")), (b"Trans-Men", Cow::Borrowed(b"don't owe you masculinity")), (b"And-Stop-Projecting-Your-Dated-Gender-Norms", Cow::Borrowed(b"onto nonbinary people's life experiences")), ]), body: body.into_bytes().into(), }) } #[cfg(feature = "ogp_images")] /// The method for the [`Route::SendThumbnail`] route fn send_thumbnail( name: Option, prefs: Option, settings: &InstanceSettings, ) -> Result { let pronoun = Route::get_pronoun(name.as_ref(), prefs, settings)?; let mut data: Vec = Vec::with_capacity(15_000); let image = DynamicImage::ImageRgb8(render_today(pronoun, name.as_ref().map(String::as_str).unwrap_or(""))); image.write_to(&mut data, ImageOutputFormat::Png) .expect("Error encoding thumbnail to PNG"); Ok(Response { status: b"200", headers: Cow::Borrowed(&[ (b"Eat", Cow::Borrowed(b"the rich!")), ]), body: data.into(), }) } /// The method for the [`Route::GenerateLink`] route fn generate_link( body: Vec, settings: &InstanceSettings, ) -> Result { let form = form_urlencoded::parse(&body); let mut weights = vec![0; settings.pronoun_list.len()]; let mut name = None; for (k, v) in form { if let Ok(i) = k.parse::() { let w = v.parse::().map_err(|_| BadRequest::BadFormData((*v).into()))?; if i < weights.len() - 1 { weights[i] = w; } } else if k == "name" { name = Some(v); } } let prefs = InstanceSettings::create_preferences(&weights); let pref_string = prefs.as_prefstring(); let url = match name { Some(name) if !name.is_empty() => format!( "/{}/{}", percent_encode(name.as_bytes(), NON_ALPHANUMERIC), pref_string ), _ => format!("/{}", pref_string), }; Ok(Response { status: b"303 See Other", headers: Cow::Owned(vec![ (&b"Location"[..], url.into_bytes().into()) ]), body: Cow::Borrowed(b""), }) } /// The method for the [`Route::BadRequest`] route fn bad_request( error: BadRequest, _settings: &InstanceSettings ) -> Response { match error { BadRequest::MethodNotAllowed => { Response { status: b"405", headers: Cow::Borrowed(&[ (b"Cache-Control", Cow::Borrowed(b"max-age=999999")), (b"And-Dont", Cow::Borrowed(b"Come Back!")), ]), body: Cow::Borrowed(b"405 Method Not Allowed\nGET OUTTA HERE"), } }, BadRequest::MalformedPrefstr(_) => { Response { status: b"404", headers: Cow::Borrowed(&[ (b"Cache-Control", Cow::Borrowed(b"max-age=631")), (b"Help-Im", Cow::Borrowed(b"Trapped in an HTTP header factory!")), ]), body: NOT_FOUND.into(), } }, BadRequest::BadFormData(yuckers) => { let body = ErrorPage::from_msg( &format!( "yeah that's some stinky form data you got there: {}", yuckers, ) ); Response { status: b"400", headers: Cow::Borrowed(&[ (b"Pretty-Gross", Cow::Borrowed(b"imo")), ]), body: body.into(), } }, BadRequest::PrefsExceedPronounCount => { let body = ErrorPage::from_msg( "Yoinkers Kersploinkers!! You know about more pronouns than \ us. Either you tried to copy a prefstring from another \ pronouns.today instance, or our admin was VERY NAUGHTY and CANT \ READ BASIC DIRECTIONS" ); Response { status: b"400", headers: Cow::Borrowed(&[ (b"I-Killed-God", Cow::Borrowed(b"And drank His blood")), (b"Now", Cow::Borrowed(b"Realty quivers before my visage")), ]), body: body.into(), } }, BadRequest::NoPronouns => { let body = ErrorPage { msg: "Someday we'll implement support for having no pronouns. You \ should probably yell at the devs to make us put this in.".into() } .render() .unwrap(); Response { status: b"400", headers: Cow::Borrowed(&[ (b"Cache-Control", Cow::Borrowed(b"max-age=540360")), (b"Actually-Tho-Please-Dont-Yell-At-Me", Cow::Borrowed(b"ill cry :(")), ]), body: body.into_bytes().into(), } }, } } /// Not a route, but a handy method to have around /// /// Provides a convinient way to get pronouns out of some of the common route /// components, and automatically converts the error into a harmless [`BadRequest`], /// is present. fn get_pronoun<'s>( name: Option<&String>, prefs: Option, settings: &'s InstanceSettings, ) -> Result<&'s Pronoun, BadRequest> { // TODO use instance defaults here let prefs = prefs.unwrap_or_else(|| "acaqeawdym".parse().unwrap()); match prefs.select_pronoun( settings, name.map(String::as_str), ) { Ok(p) => Ok(p), Err(ParseError::PrefstringExceedsPronounCount) => { Err(BadRequest::PrefsExceedPronounCount) }, Err(ParseError::EmptyWeightedTable) => { Err(BadRequest::NoPronouns) }, Err(_) => unreachable!(), } } } /// A response to be sent back to the SCGI client /// /// Technically just an HTTP response but don't tell me that. Provides comfy little /// methods for serializing itself right back into a series of bytes pub struct Response { /// The status of the response. Exactly the same as an HTTP status code /// /// This is a string of ASCII bytes. It can either be just the number (`b"200"`) or the number /// and a description (`b"200 Ok"`). It's like this so that into_io_slices works status: &'static [u8], /// A series of HTTP headers to send to the client /// /// wow emi that's a lot of cows SHUT UP I JUST WANNA CONSTRUCT THINGS IN CONST IS /// THAT TOO MUCH TO ASK headers: Cow<'static, [(&'static [u8], Cow<'static, [u8]>)]>, /// The body of the response. Wow what a useful doccomment Emi body: Cow<'static, [u8]>, } impl Response { pub fn into_io_slices(&self) -> Vec> { let mut output = Vec::with_capacity( // Is computing the exact length of the output necessary? No. // Do we gain any even vaguely measurable performance gain by preventing reallocation? No. // Am I doing it anyway? Yes. // It is late and I am tired and you are nothing to me you can't tell me what to do 5 + 4 * self.headers.len() ); output.extend_from_slice(&[ IoSlice::new(b"HTTP/1.1 "), IoSlice::new(self.status), IoSlice::new(b"\r\n"), ]); for (h1, h2) in self.headers.iter() { output.extend_from_slice(&[ IoSlice::new(h1), IoSlice::new(b": "), IoSlice::new(h2), IoSlice::new(b"\r\n"), ]); } output.extend_from_slice(&[ IoSlice::new(b"\r\n"), IoSlice::new(&*self.body), ]); return output; } } #[derive(Template)] #[template(path = "error.html")] struct ErrorPage { msg: String, } impl ErrorPage { fn from_msg(msg: &str) -> Vec { ErrorPage { msg: msg.into() } .render() .unwrap() .into_bytes() } } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { name: Option, pronoun: &'a Pronoun, pronouns: Vec<(usize, &'a Pronoun)>, url: String, }