[web] [WIP] Initial progress in converting to SCGI
This commit is contained in:
parent
26da64922d
commit
dd5b4c5238
|
@ -7,7 +7,8 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pronouns_today = {path = ".."}
|
pronouns_today = {path = ".."}
|
||||||
actix-web = "3"
|
async-scgi = "0.1.0"
|
||||||
|
smol = "1.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
askama = "0.10"
|
askama = "0.10"
|
||||||
|
|
622
web/src/main.rs
622
web/src/main.rs
|
@ -3,57 +3,34 @@ pub mod contrast;
|
||||||
pub mod statics;
|
pub mod statics;
|
||||||
pub mod configuration;
|
pub mod configuration;
|
||||||
|
|
||||||
|
use smol::io::AsyncWriteExt;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use pronouns_today::user_preferences::ParseError;
|
||||||
|
use pronouns_today::UserPreferences;
|
||||||
use configuration::ConfigError;
|
use configuration::ConfigError;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::collections::HashMap;
|
use std::ops::Deref;
|
||||||
use std::fmt::{self, Display};
|
|
||||||
|
|
||||||
use actix_web::dev::HttpResponseBuilder;
|
|
||||||
use actix_web::http::{header, StatusCode};
|
|
||||||
use actix_web::middleware::normalize::TrailingSlash;
|
|
||||||
use actix_web::middleware::{self, Logger};
|
|
||||||
use actix_web::web::resource;
|
|
||||||
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, ResponseError, Result, post, web};
|
|
||||||
use argh;
|
use argh;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use async_scgi::{ScgiReadError, ScgiRequest};
|
||||||
use pronouns_today::user_preferences::Preference;
|
use pronouns_today::user_preferences::Preference;
|
||||||
use pronouns_today::{InstanceSettings, Pronoun};
|
use pronouns_today::{InstanceSettings, Pronoun};
|
||||||
|
use smol;
|
||||||
|
|
||||||
#[cfg(feature = "ogp_images")]
|
#[cfg(feature = "ogp_images")]
|
||||||
use image::{DynamicImage, ImageOutputFormat};
|
use image::{DynamicImage, ImageOutputFormat};
|
||||||
#[cfg(feature = "ogp_images")]
|
#[cfg(feature = "ogp_images")]
|
||||||
use ogp_images::render_today;
|
use ogp_images::render_today;
|
||||||
|
|
||||||
// TODO: Make this configurable
|
/* Ill deal with this later
|
||||||
const HOSTNAME: &str = "pronouns.today";
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
struct IndexTemplate<'a> {
|
|
||||||
name: Option<String>,
|
|
||||||
pronoun: &'a Pronoun,
|
|
||||||
pronouns: Vec<(usize, &'a Pronoun)>,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option<String>, url: String) -> String {
|
|
||||||
IndexTemplate {
|
|
||||||
name,
|
|
||||||
pronoun,
|
|
||||||
pronouns: settings.pronoun_list.iter().enumerate().collect(),
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/")]
|
|
||||||
async fn create_link(
|
async fn create_link(
|
||||||
settings: web::Data<InstanceSettings>,
|
settings: &InstanceSettings,
|
||||||
form: web::Form<HashMap<String, String>>,
|
form: HashMap<String, String>,
|
||||||
) -> Result<impl Responder> {
|
) -> String {
|
||||||
let mut weights = vec![0; settings.pronoun_list.len()];
|
let mut weights = vec![0; settings.pronoun_list.len()];
|
||||||
for (k, v) in form.iter() {
|
for (k, v) in form.iter() {
|
||||||
if let Ok(i) = k.parse::<usize>() {
|
if let Ok(i) = k.parse::<usize>() {
|
||||||
|
@ -69,82 +46,13 @@ async fn create_link(
|
||||||
Some(name) if !name.is_empty() => format!("/{}/{}", name, pref_string),
|
Some(name) if !name.is_empty() => format!("/{}/{}", name, pref_string),
|
||||||
_ => format!("/{}", pref_string),
|
_ => format!("/{}", pref_string),
|
||||||
};
|
};
|
||||||
Ok(HttpResponse::SeeOther()
|
url
|
||||||
.header(header::LOCATION, url)
|
}*/
|
||||||
.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn form_full_url(host: &str, name: Option<&str>, prefstr: Option<&str>) -> String {
|
const NOT_FOUND: &[u8] = include_bytes!("../templates/404.html");
|
||||||
["https:/", host].into_iter()
|
|
||||||
.chain(name)
|
|
||||||
.chain(prefstr)
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join("/")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine some basic information about a request
|
/// Handles initialization, and runs top-level routing based on cli args
|
||||||
///
|
fn main() {
|
||||||
/// Determines the name and prefstring properties, if available, and also computes the pronoun that
|
|
||||||
/// should be responded using. This method is designed to facilitate creating the basic response
|
|
||||||
/// pages.
|
|
||||||
///
|
|
||||||
/// Both arguments should be passed directly from the caller, and the return values are, in order,
|
|
||||||
/// the prefstring, if available, the user's name, if available, and the computed pronouns.
|
|
||||||
fn get_request_info<'s, 'r>(
|
|
||||||
settings: &'s web::Data<InstanceSettings>,
|
|
||||||
req: &'r HttpRequest,
|
|
||||||
) -> (Option<&'r str>, Option<&'r str>, Result<&'s Pronoun, Error>) {
|
|
||||||
let prefs = req.match_info().get("prefs");
|
|
||||||
let name = req.match_info().get("name");
|
|
||||||
let pronoun = settings
|
|
||||||
.select_pronouns(name, prefs)
|
|
||||||
.map_err(|_| Error::InvlaidPrefString);
|
|
||||||
(prefs, name, pronoun)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_basic_request(
|
|
||||||
settings: web::Data<InstanceSettings>,
|
|
||||||
req: HttpRequest,
|
|
||||||
) -> Result<impl Responder> {
|
|
||||||
|
|
||||||
let (prefstr, name, pronoun) = get_request_info(&settings, &req);
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type("text/html; charset=utf-8")
|
|
||||||
.body(render_page(
|
|
||||||
pronoun?,
|
|
||||||
&settings,
|
|
||||||
name.map(str::to_owned),
|
|
||||||
form_full_url(HOSTNAME, name, prefstr)
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ogp_images")]
|
|
||||||
async fn handle_thumbnail_request(
|
|
||||||
settings: web::Data<InstanceSettings>,
|
|
||||||
req: HttpRequest,
|
|
||||||
) -> Result<impl Responder> {
|
|
||||||
|
|
||||||
let (_, name, pronoun) = get_request_info(&settings, &req);
|
|
||||||
|
|
||||||
let mut data: Vec<u8> = Vec::with_capacity(15_000);
|
|
||||||
let image = DynamicImage::ImageRgb8(render_today(pronoun?, name.unwrap_or("")));
|
|
||||||
image.write_to(&mut data, ImageOutputFormat::Png)
|
|
||||||
.expect("Error encoding thumbnail to PNG");
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type("image/png")
|
|
||||||
.body(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn not_found() -> impl Responder {
|
|
||||||
HttpResponse::NotFound()
|
|
||||||
.content_type("text/html; charset=utf-8")
|
|
||||||
.body(include_str!("../templates/404.html"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_web::main]
|
|
||||||
async fn main() -> std::io::Result<()> {
|
|
||||||
|
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let args: configuration::PronounsTodayArgs = argh::from_env();
|
let args: configuration::PronounsTodayArgs = argh::from_env();
|
||||||
|
@ -152,13 +60,14 @@ async fn main() -> std::io::Result<()> {
|
||||||
match args.command {
|
match args.command {
|
||||||
configuration::SubCommand::DumpStatics(_subargs) => {
|
configuration::SubCommand::DumpStatics(_subargs) => {
|
||||||
println!("Support for dumping statics not yet implemented");
|
println!("Support for dumping statics not yet implemented");
|
||||||
Ok(())
|
return;
|
||||||
}
|
}
|
||||||
configuration::SubCommand::Run(subargs) => {
|
configuration::SubCommand::Run(subargs) => {
|
||||||
let config = match subargs.load_config() {
|
let config = match subargs.load_config() {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(ConfigError::IoError(e)) => {
|
Err(ConfigError::IoError(e)) => {
|
||||||
return Err(e);
|
eprintln!("IO Error while reading config! {}", e);
|
||||||
|
exit(1000);
|
||||||
}
|
}
|
||||||
Err(ConfigError::MalformedConfig(e)) => {
|
Err(ConfigError::MalformedConfig(e)) => {
|
||||||
eprintln!("Error parsing config file:\n{}", e);
|
eprintln!("Error parsing config file:\n{}", e);
|
||||||
|
@ -171,43 +80,438 @@ async fn main() -> std::io::Result<()> {
|
||||||
exit(1002);
|
exit(1002);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let executor = smol::LocalExecutor::new();
|
||||||
log::info!("Starting with configuration {:?}", config);
|
log::info!("Starting with configuration {:?}", config);
|
||||||
start_server(config).await
|
smol::block_on(executor.run(start_server(config, &executor))).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_server(config: configuration::Conf) -> std::io::Result<()> {
|
/// 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: &smol::LocalExecutor<'_>) -> std::io::Result<()> {
|
||||||
// Where we binding bois
|
// Where we binding bois
|
||||||
let socket_addr = SocketAddr::new(config.address, config.port);
|
let socket_addr = SocketAddr::new(config.address, config.port);
|
||||||
|
|
||||||
println!("Starting pronouns-today-web on {}", &socket_addr);
|
println!("Starting pronouns-today-web on {}", &socket_addr);
|
||||||
HttpServer::new(move|| {
|
|
||||||
let logger = Logger::default();
|
|
||||||
let app = App::new()
|
|
||||||
.data(config.instance_settings.clone())
|
|
||||||
.wrap(logger)
|
|
||||||
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
|
|
||||||
.service(create_link);
|
|
||||||
|
|
||||||
let app = statics::STATIC_ASSETS.iter()
|
let connection = smol::net::TcpListener::bind(socket_addr).await?;
|
||||||
.fold(app, |app, asset| app.service(asset.generate_resource()));
|
let mut incoming = connection.incoming();
|
||||||
|
|
||||||
#[cfg(feature = "ogp_images")]
|
// Make the instance settings immortal
|
||||||
let app = app
|
let instance_settings = Box::leak(Box::new(config.instance_settings));
|
||||||
.service(resource("/thumb.png") .to(handle_thumbnail_request))
|
|
||||||
.service(resource("/{prefs}/thumb.png") .to(handle_thumbnail_request))
|
|
||||||
.service(resource("/{name}/{prefs}/thumb.png").to(handle_thumbnail_request));
|
|
||||||
|
|
||||||
app
|
while let Some(stream) = incoming.next().await {
|
||||||
.service(resource("/") .to(handle_basic_request))
|
match stream {
|
||||||
.service(resource("/{prefs}") .to(handle_basic_request))
|
Ok(stream) => {
|
||||||
.service(resource("/{name}/{prefs}").to(handle_basic_request))
|
executor.spawn(handle_request(stream, instance_settings)).detach();
|
||||||
.default_service(web::to(not_found))
|
},
|
||||||
})
|
Err(e) => {
|
||||||
.bind(&socket_addr)?
|
log::error!("IO Error with client/server connection: {}", e);
|
||||||
.run()
|
}
|
||||||
.await
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 smol::io::AsyncRead + smol::io::AsyncWrite + Unpin,
|
||||||
|
settings: &InstanceSettings
|
||||||
|
) {
|
||||||
|
let mut stream = smol::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);
|
||||||
|
todo!(); //TODO send a 500 error
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = stream.write_all(
|
||||||
|
route_request(&req)
|
||||||
|
.generate_response(settings)
|
||||||
|
.into_bytes()
|
||||||
|
.as_ref()
|
||||||
|
).await {
|
||||||
|
log::warn!(
|
||||||
|
"Encountered an IO error while sending response: {}", 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,
|
||||||
|
|
||||||
|
// No other methods are supported
|
||||||
|
_ => return Route::BadRequest(BadRequest::MethodNotAllowed),
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut segments: Vec<_> = req.headers
|
||||||
|
.get("REQUEST_URI")
|
||||||
|
.expect("Misconfigured server didn't send a REQUEST_URI header")
|
||||||
|
.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") {
|
||||||
|
// TODO try serve static files
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if we need to respond with a thumbnail or a web page
|
||||||
|
let is_thumb =
|
||||||
|
!segments.is_empty() &&
|
||||||
|
segments.last().map(Deref::deref) == Some("thumb.png");
|
||||||
|
if is_thumb {
|
||||||
|
// 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<UserPreferences, _> = idk.parse();
|
||||||
|
if let Ok(preferences) = result {
|
||||||
|
(None, Some(preferences))
|
||||||
|
} else {
|
||||||
|
(Some(idk), None)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(None, Some(_)) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_type = if is_thumb {
|
||||||
|
Route::SendThumbnail
|
||||||
|
} else {
|
||||||
|
Route::SendPronounPage
|
||||||
|
};
|
||||||
|
|
||||||
|
response_type(name.map(|n| (*n).to_owned()), 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 [u8]),
|
||||||
|
|
||||||
|
/// Respond with an HTML pronoun page.
|
||||||
|
///
|
||||||
|
/// Takes the user's name (if specified), and the preferences of that user, or
|
||||||
|
/// [`None`] for defults
|
||||||
|
SendPronounPage(Option<String>, Option<UserPreferences>),
|
||||||
|
|
||||||
|
/// Respond with an image containing the user's pronouns
|
||||||
|
///
|
||||||
|
/// Takes the user's name (optional), and the preferences of that user
|
||||||
|
SendThumbnail(Option<String>, Option<UserPreferences>),
|
||||||
|
|
||||||
|
/// Generate a link to the appropriate pronoun page based on the user's inputs.
|
||||||
|
GenerateLink, //TODO
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// 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, settings: &InstanceSettings) -> Response {
|
||||||
|
let result = match self {
|
||||||
|
Route::SendStatic(data) => Ok(Response {
|
||||||
|
status: 200,
|
||||||
|
headers: Cow::Borrowed(&[
|
||||||
|
]),
|
||||||
|
body: data.into(),
|
||||||
|
}),
|
||||||
|
Route::SendPronounPage(name, prefs) =>
|
||||||
|
Route::send_pronoun_page(name, prefs, settings),
|
||||||
|
Route::SendThumbnail(name, prefs) =>
|
||||||
|
Route::send_thumbnail(name, prefs, settings),
|
||||||
|
Route::GenerateLink =>
|
||||||
|
Route::generate_link(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<String>,
|
||||||
|
prefs: Option<UserPreferences>,
|
||||||
|
settings: &InstanceSettings,
|
||||||
|
) -> Result<Response, BadRequest> {
|
||||||
|
let pronoun = Route::get_pronoun(name.as_ref(), prefs, settings)?;
|
||||||
|
|
||||||
|
let body = IndexTemplate {
|
||||||
|
name,
|
||||||
|
pronoun,
|
||||||
|
pronouns: settings.pronoun_list.iter().enumerate().collect(),
|
||||||
|
url: String::new(), //TODO
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Response {
|
||||||
|
status: 200,
|
||||||
|
headers: Cow::Borrowed(&[]),
|
||||||
|
body: body.into_bytes().into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ogp_images")]
|
||||||
|
/// The method for the [`Route::SendThumbnail`] route
|
||||||
|
fn send_thumbnail(
|
||||||
|
name: Option<String>,
|
||||||
|
prefs: Option<UserPreferences>,
|
||||||
|
settings: &InstanceSettings,
|
||||||
|
) -> Result<Response, BadRequest> {
|
||||||
|
let pronoun = Route::get_pronoun(name.as_ref(), prefs, settings)?;
|
||||||
|
|
||||||
|
let mut data: Vec<u8> = 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: 200,
|
||||||
|
headers: Cow::Borrowed(&[
|
||||||
|
(b"Eat", Cow::Borrowed(b"the rich!")),
|
||||||
|
]),
|
||||||
|
body: data.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The method for the [`Route::GenerateLink`] route
|
||||||
|
fn generate_link(
|
||||||
|
_settings: &InstanceSettings,
|
||||||
|
) -> Result<Response, BadRequest> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The method for the [`Route::BadRequest`] route
|
||||||
|
fn bad_request(
|
||||||
|
error: BadRequest,
|
||||||
|
_settings: &InstanceSettings
|
||||||
|
) -> Response {
|
||||||
|
match error {
|
||||||
|
BadRequest::MethodNotAllowed => {
|
||||||
|
Response {
|
||||||
|
status: 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: 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::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: 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: 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<UserPreferences>,
|
||||||
|
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
|
||||||
|
status: u16,
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
fn into_bytes(self) -> Vec<u8> {
|
||||||
|
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
|
||||||
|
14 + // HTTP/1.1 200\r\n
|
||||||
|
self.headers
|
||||||
|
.iter()
|
||||||
|
.map(|(h1, h2)| h1.len() + h2.len() + 4)
|
||||||
|
.sum::<usize>() +
|
||||||
|
2 + // \r\n
|
||||||
|
self.body.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
output.extend_from_slice(b"HTTP/1.1 ");
|
||||||
|
output.extend_from_slice(self.status.to_string().as_bytes());
|
||||||
|
output.extend_from_slice(b"\r\n");
|
||||||
|
for (h1, h2) in self.headers.iter() {
|
||||||
|
output.extend_from_slice(h1);
|
||||||
|
output.extend_from_slice(b": ");
|
||||||
|
output.extend_from_slice(h2);
|
||||||
|
output.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
|
output.extend_from_slice(b"\r\n");
|
||||||
|
output.extend_from_slice(&*self.body);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -216,36 +520,22 @@ struct ErrorPage {
|
||||||
msg: String,
|
msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl ErrorPage {
|
||||||
enum Error {
|
fn from_msg(msg: &str) -> Vec<u8> {
|
||||||
InvlaidPrefString,
|
ErrorPage {
|
||||||
|
msg: msg.into()
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap()
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Error {
|
#[derive(Template)]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
#[template(path = "index.html")]
|
||||||
let msg = match self {
|
struct IndexTemplate<'a> {
|
||||||
&Error::InvlaidPrefString => "This URL contains an invalid pronoun preference string",
|
name: Option<String>,
|
||||||
};
|
pronoun: &'a Pronoun,
|
||||||
write!(f, "{}", msg)
|
pronouns: Vec<(usize, &'a Pronoun)>,
|
||||||
}
|
url: String,
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseError for Error {
|
|
||||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
|
||||||
match self {
|
|
||||||
&Error::InvlaidPrefString => StatusCode::BAD_REQUEST,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
|
||||||
HttpResponseBuilder::new(self.status_code())
|
|
||||||
.set_header(header::CONTENT_TYPE, "text/html; charset=utf-8")
|
|
||||||
.body(
|
|
||||||
ErrorPage {
|
|
||||||
msg: self.to_string(),
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
use actix_web::http::StatusCode;
|
|
||||||
use actix_web::HttpResponse;
|
|
||||||
use actix_web::HttpRequest;
|
|
||||||
use actix_web::web::resource;
|
|
||||||
use actix_web::Resource;
|
|
||||||
|
|
||||||
const DEFAULT_CACHE: &str = "max-age=86400";
|
use std::borrow::Cow;
|
||||||
|
use crate::Response;
|
||||||
|
|
||||||
|
const DEFAULT_CACHE: &[u8] = b"max-age=86400";
|
||||||
|
|
||||||
/// Represents a single static asset
|
/// Represents a single static asset
|
||||||
///
|
///
|
||||||
|
@ -22,30 +20,20 @@ pub struct StaticAsset {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticAsset {
|
impl StaticAsset {
|
||||||
/// Generate a actix resource for serving this asset
|
const STATIC_HEADERS: &'static [(&'static [u8], Cow<'static, [u8]>)] = &[
|
||||||
///
|
(b"Cache-Control", Cow::Borrowed(DEFAULT_CACHE)),
|
||||||
/// The resource will handle requests at `/static/{filename}`. Caching headers are
|
(b"Trans-People", Cow::Borrowed(b"Are Gods")),
|
||||||
/// applied to allow the content to be considered fresh up to one day, and are
|
];
|
||||||
/// validated against the crate version after that.
|
|
||||||
pub fn generate_resource(&self) -> Resource {
|
|
||||||
let bytes = self.bytes;
|
|
||||||
resource(format!("/static/{}", self.filename))
|
|
||||||
.to(move|req: HttpRequest| async move {
|
|
||||||
let mut response = HttpResponse::Ok();
|
|
||||||
response
|
|
||||||
.header("Etag", env!("CARGO_PKG_VERSION"))
|
|
||||||
.header("Cache-Control", DEFAULT_CACHE);
|
|
||||||
|
|
||||||
let req_etag = req.headers().get("If-None-Match");
|
/// Generate the HTTP response for sending this asset to the client
|
||||||
match req_etag {
|
// I wrote all this code to make this a const fn, and then don't even use it in
|
||||||
Some(etag) if etag == env!("CARGO_PKG_VERSION") => {
|
// compile-time :(
|
||||||
response.status(StatusCode::from_u16(304).unwrap()).finish()
|
const fn generate_response(&self) -> Response {
|
||||||
}
|
Response {
|
||||||
_ => {
|
status: 200,
|
||||||
response.body(bytes)
|
headers: Cow::Borrowed(StaticAsset::STATIC_HEADERS),
|
||||||
}
|
body: Cow::Borrowed(self.bytes),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue