Compare commits
18 commits
main
...
convert-ht
Author | SHA1 | Date | |
---|---|---|---|
|
32a83243fa | ||
|
a853d8796a | ||
|
4cef709f52 | ||
|
57e1b5c85b | ||
|
adcbae08ca | ||
|
3a4afbcaea | ||
|
63cfa014d5 | ||
|
4ed748cf1e | ||
|
2bfdf0ef49 | ||
|
26b4b20c1d | ||
|
7c07aac2af | ||
|
2cfe9fcb5f | ||
|
56dbe50d2f | ||
|
74e41de309 | ||
|
b9620e95a8 | ||
|
e55442c47f | ||
|
5d47635ce8 | ||
|
dd5b4c5238 |
15
src/lib.rs
15
src/lib.rs
|
@ -8,6 +8,7 @@
|
|||
//! ## Basic Usage
|
||||
//!
|
||||
//! ```
|
||||
//! # use pronouns_today::user_preferences::ParseError;
|
||||
//! use pronouns_today::InstanceSettings;
|
||||
//!
|
||||
//! let instance_settings = InstanceSettings::default(); // Or load from a config file
|
||||
|
@ -15,9 +16,10 @@
|
|||
//! // When you receive a request
|
||||
//! let user_name = Some("Emi");
|
||||
//! let user_prefstr = Some("acaqqbykawbag");
|
||||
//! let pronouns = instance_settings.select_pronouns(user_name, user_prefstr);
|
||||
//! let pronouns = instance_settings.select_pronouns(user_name, user_prefstr)?;
|
||||
//!
|
||||
//! println!("Your pronouns are: {}", pronouns);
|
||||
//! # Ok::<(), ParseError>(())
|
||||
//! ```
|
||||
//!
|
||||
//! ## Advanced Usage
|
||||
|
@ -38,6 +40,8 @@
|
|||
//!
|
||||
//! [up]: UserPreferences
|
||||
|
||||
#![allow(clippy::tabs_in_doc_comments)]
|
||||
|
||||
pub mod user_preferences;
|
||||
pub mod util;
|
||||
mod weighted_table;
|
||||
|
@ -74,15 +78,18 @@ impl InstanceSettings {
|
|||
/// This is shorthand for
|
||||
///
|
||||
/// ```
|
||||
/// # use crate::pronouns_today::user_preferences::Preference;
|
||||
/// # use pronouns_today::user_preferences::ParseError;
|
||||
/// # use pronouns_today::InstanceSettings;
|
||||
/// # let settings = InstanceSettings::default();
|
||||
/// # let name = String::from("Sashanoraa");
|
||||
/// # let prefstring = String::from("todo");
|
||||
/// let pronouns = InstanceSettings::parse_prefstring(Some(&prefstring))?.select_pronouns(&settings, Some(&name));
|
||||
/// # let prefstring = String::from("acaqebicadbqaaa");
|
||||
/// let pronouns = InstanceSettings::parse_prefstring(Some(&prefstring))?.select_pronoun(&settings, Some(&name));
|
||||
/// # assert_eq!(pronouns, settings.select_pronouns(Some(&name), Some(&prefstring)));
|
||||
/// # Ok::<(), ParseError>(())
|
||||
/// ```
|
||||
pub fn select_pronouns(&self, name: Option<&str>, pref_string: Option<&str>) -> Result<&Pronoun, ParseError> {
|
||||
Self::parse_prefstring(pref_string)?.select_pronoun(&self, name)
|
||||
Self::parse_prefstring(pref_string)?.select_pronoun(self, name)
|
||||
}
|
||||
|
||||
/// Parse a pref_string.
|
||||
|
|
|
@ -100,10 +100,14 @@ pub trait Preference {
|
|||
/// Is shorthand for
|
||||
///
|
||||
/// ```
|
||||
/// # use pronouns_today::InstanceSettings;
|
||||
/// # use pronouns_today::user_preferences::ParseError;
|
||||
/// # use pronouns_today::{InstanceSettings, UserPreferences};
|
||||
/// # use crate::pronouns_today::user_preferences::Preference;
|
||||
/// # let settings = InstanceSettings::default();
|
||||
/// # let seed = &[];
|
||||
/// self.into_weighted_table(settings)?.select_today(seed)
|
||||
/// # let prefs = UserPreferences::default();
|
||||
/// prefs.create_weighted_table(&settings)?.select_today(seed);
|
||||
/// # Ok::<(), ParseError>(())
|
||||
/// ```
|
||||
fn select_pronoun<'a>(&self, settings: &'a InstanceSettings, name: Option<&str>) -> Result<&'a Pronoun, ParseError> {
|
||||
let seed = match name {
|
||||
|
@ -126,7 +130,7 @@ impl Preference for UserPreferences {
|
|||
let version = version_byte >> 3;
|
||||
let varient = version_byte & 0b111;
|
||||
match (version, varient) {
|
||||
(0, 0) => UserPreferencesV0::from_prefstring_bytes(bytes).map(|prefs| UserPreferences::V0(prefs)),
|
||||
(0, 0) => UserPreferencesV0::from_prefstring_bytes(bytes).map(UserPreferences::V0),
|
||||
_ => Err(ParseError::VersionMismatch {
|
||||
expected_version: 0..1,
|
||||
expected_variant: 0..1,
|
||||
|
|
|
@ -94,7 +94,7 @@ impl Preference for UserPreferencesV0 {
|
|||
fn from_prefstring_bytes(pbytes: &[u8]) -> Result<Self, ParseError> {
|
||||
|
||||
// Some simple error checks
|
||||
if pbytes.len() == 0 {
|
||||
if pbytes.is_empty() {
|
||||
return Err(ParseError::ZeroLengthPrefstring);
|
||||
} else if pbytes[0] != 00 {
|
||||
return Err(ParseError::VersionMismatch {
|
||||
|
@ -152,6 +152,7 @@ impl Preference for UserPreferencesV0 {
|
|||
.unwrap();
|
||||
let default_enabled = num_zeros < prefs.len() / 2;
|
||||
let mut commands = Vec::new();
|
||||
#[allow(clippy::branches_sharing_code)]
|
||||
if default_enabled {
|
||||
let mut last_default = -1;
|
||||
for (i, w) in prefs.iter().enumerate() {
|
||||
|
@ -305,8 +306,8 @@ mod tests {
|
|||
|
||||
fn check_table(actual: WeightedTable<&Pronoun>, expected: Vec<(&str, u8)>) {
|
||||
let actual_simplified = actual.weights()
|
||||
.into_iter()
|
||||
.filter(|(pronoun, weight)| **weight > 0)
|
||||
.iter()
|
||||
.filter(|(_, weight)| **weight > 0)
|
||||
.map(|(pronoun, weight)| (pronoun.subject_pronoun.as_str(), *weight))
|
||||
.collect::<HashMap<&str, u8>>();
|
||||
let expected_owned = expected.into_iter()
|
||||
|
@ -362,6 +363,7 @@ mod tests {
|
|||
("they", 3),
|
||||
("it", 3),
|
||||
];
|
||||
check_table(table, expected_table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -103,8 +103,7 @@ impl<Loot: Eq + Ord + Hash + Copy + Debug> WeightedTable<Loot> {
|
|||
let random = pcg64(generator) % sum_weights;
|
||||
|
||||
rollable_table.iter()
|
||||
.filter(|(weight, _)| random < *weight)
|
||||
.next()
|
||||
.find(|(weight, _)| random < *weight)
|
||||
.expect("A table was generated with zero entries. This should be impossible.")
|
||||
.1
|
||||
}
|
||||
|
|
|
@ -7,13 +7,19 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
pronouns_today = {path = ".."}
|
||||
actix-web = "3"
|
||||
async-scgi = "0.1.0"
|
||||
async-executor = "1.4"
|
||||
async-net = "1.6"
|
||||
async-io = "1.6"
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
askama = "0.10"
|
||||
argh = "0.1.6"
|
||||
serde = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
form_urlencoded = "1.0.1"
|
||||
percent-encoding = "2.1.0"
|
||||
rusttype = { version = "0.9.2", optional = true }
|
||||
image = { version = "0.23.14", optional = true }
|
||||
lazy_static = { version = "1.4.0", optional = true }
|
||||
|
|
|
@ -6,6 +6,13 @@ port: 1312
|
|||
# The address the server should bind to
|
||||
address: 0.0.0.0
|
||||
|
||||
# The base URL the server will be running under, with no trailing slash
|
||||
#
|
||||
# This is also your opportunity to serve requests under a subpath. For example, if you
|
||||
# want pronouns.today to only run on the /pronouns route of your webserver, you can set
|
||||
# this to https://example.com/pronouns
|
||||
base_url: https://pronouns.today
|
||||
|
||||
# A list of pronouns recognized by the server
|
||||
#
|
||||
# WARNING: When adding pronouns, only add pronouns to the bottom of the list, and do not
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::net::Ipv4Addr;
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use pronouns_today::PronounList;
|
||||
use pronouns_today::UserPreferences;
|
||||
|
@ -64,13 +62,10 @@ pub struct Run {
|
|||
/// options will be filled in using sane defaults
|
||||
pub no_read_cfg: bool,
|
||||
|
||||
#[argh(option, short = 'p')]
|
||||
/// the port to listen on
|
||||
pub port: Option<u16>,
|
||||
|
||||
#[argh(option)]
|
||||
/// the address to bind to
|
||||
pub address: Option<IpAddr>,
|
||||
/// the address to bind to. can be an ip address and a port, like 0.0.0.0:1312, or a
|
||||
/// unix socket like /run/programming.sock. defaults to 0.0.0.0:1312
|
||||
pub bind: Option<String>,
|
||||
|
||||
#[argh(option)]
|
||||
/// default pronoun probabilites (formatted as a prefstring, like the ones in the
|
||||
|
@ -82,6 +77,11 @@ pub struct Run {
|
|||
/// for warnings when changing this value. formatted as a list of comma seperated
|
||||
/// five-form pronouns, e.g. she/her/her/hers/herself,he/him/his/his/himself
|
||||
pub pronouns: Option<PronounList>,
|
||||
|
||||
#[argh(option, long="base")]
|
||||
/// the base url content is served under, starting with the protocol (https://) and without a
|
||||
/// trailing slash
|
||||
pub base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Run {
|
||||
|
@ -129,15 +129,19 @@ pub struct Conf {
|
|||
/// The port for the server to bind to. Defaults to 1312
|
||||
pub port: u16,
|
||||
|
||||
/// The address to bind to. Defaults to 0.0.0.0
|
||||
pub address: IpAddr,
|
||||
/// The address to bind to. Can be an ip address and a port, like 0.0.0.0:1312, or a
|
||||
/// unix socket like /run/programming.sock. Defaults to 0.0.0.0:1312
|
||||
pub bind: String,
|
||||
|
||||
/// The base url the server will be running under, with no trailing slash
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
impl Conf {
|
||||
fn update_with(mut self, args: Run) -> Conf {
|
||||
self.port = args.port.unwrap_or(self.port);
|
||||
self.address = args.address.unwrap_or(self.address);
|
||||
self.bind = args.bind.unwrap_or(self.bind);
|
||||
self.instance_settings.pronoun_list = args.pronouns.map(Into::into).unwrap_or(self.instance_settings.pronoun_list);
|
||||
self.base_url = args.base_url.unwrap_or(self.base_url);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +151,8 @@ impl Default for Conf {
|
|||
Conf {
|
||||
instance_settings: InstanceSettings::default(),
|
||||
port: 1312,
|
||||
address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
bind: "0.0.0.0:1312".into(),
|
||||
base_url: "https://pronouns.today".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
737
web/src/main.rs
737
web/src/main.rs
|
@ -2,22 +2,32 @@ 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::collections::HashMap;
|
||||
use std::fmt::{self, Display};
|
||||
use std::ops::Deref;
|
||||
|
||||
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 askama::Template;
|
||||
use async_scgi::{ScgiReadError, ScgiRequest};
|
||||
use pronouns_today::user_preferences::Preference;
|
||||
use pronouns_today::{InstanceSettings, Pronoun};
|
||||
|
||||
|
@ -26,125 +36,10 @@ use image::{DynamicImage, ImageOutputFormat};
|
|||
#[cfg(feature = "ogp_images")]
|
||||
use ogp_images::render_today;
|
||||
|
||||
// TODO: Make this configurable
|
||||
const HOSTNAME: &str = "pronouns.today";
|
||||
const NOT_FOUND: &[u8] = include_bytes!("../templates/404.html");
|
||||
|
||||
#[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(
|
||||
settings: web::Data<InstanceSettings>,
|
||||
form: web::Form<HashMap<String, String>>,
|
||||
) -> Result<impl Responder> {
|
||||
let mut weights = vec![0; settings.pronoun_list.len()];
|
||||
for (k, v) in form.iter() {
|
||||
if let Ok(i) = k.parse::<usize>() {
|
||||
let w = v.parse::<u8>().map_err(|_| Error::InvlaidPrefString)?;
|
||||
if i < weights.len() - 1 {
|
||||
weights[i] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
let prefs = InstanceSettings::create_preferences(&weights);
|
||||
let pref_string = prefs.as_prefstring();
|
||||
let url = match form.get("name") {
|
||||
Some(name) if !name.is_empty() => format!("/{}/{}", name, pref_string),
|
||||
_ => format!("/{}", pref_string),
|
||||
};
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.header(header::LOCATION, url)
|
||||
.finish())
|
||||
}
|
||||
|
||||
fn form_full_url(host: &str, name: Option<&str>, prefstr: Option<&str>) -> String {
|
||||
["https:/", host].into_iter()
|
||||
.chain(name)
|
||||
.chain(prefstr)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
/// Determine some basic information about a request
|
||||
///
|
||||
/// 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<()> {
|
||||
/// Handles initialization, and runs top-level routing based on cli args
|
||||
fn main() {
|
||||
|
||||
env_logger::init();
|
||||
let args: configuration::PronounsTodayArgs = argh::from_env();
|
||||
|
@ -152,13 +47,14 @@ async fn main() -> std::io::Result<()> {
|
|||
match args.command {
|
||||
configuration::SubCommand::DumpStatics(_subargs) => {
|
||||
println!("Support for dumping statics not yet implemented");
|
||||
Ok(())
|
||||
return;
|
||||
}
|
||||
configuration::SubCommand::Run(subargs) => {
|
||||
let config = match subargs.load_config() {
|
||||
Ok(config) => config,
|
||||
Err(ConfigError::IoError(e)) => {
|
||||
return Err(e);
|
||||
eprintln!("IO Error while reading config! {}", e);
|
||||
exit(1000);
|
||||
}
|
||||
Err(ConfigError::MalformedConfig(e)) => {
|
||||
eprintln!("Error parsing config file:\n{}", e);
|
||||
|
@ -171,43 +67,528 @@ async fn main() -> std::io::Result<()> {
|
|||
exit(1002);
|
||||
}
|
||||
};
|
||||
|
||||
let executor = LocalExecutor::new();
|
||||
log::info!("Starting with configuration {:?}", config);
|
||||
start_server(config).await
|
||||
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: &LocalExecutor<'_>) -> std::io::Result<()> {
|
||||
|
||||
// Make the configuration immortal
|
||||
let config = Box::leak(Box::new(config));
|
||||
|
||||
// Where we binding bois
|
||||
let socket_addr = SocketAddr::new(config.address, config.port);
|
||||
if let Ok(socket_addr) = config.bind.parse() as Result<SocketAddr, _> {
|
||||
|
||||
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);
|
||||
println!("Starting pronouns-today-web on {}", &socket_addr);
|
||||
|
||||
let app = statics::STATIC_ASSETS.iter()
|
||||
.fold(app, |app, asset| app.service(asset.generate_resource()));
|
||||
let connection = TcpListener::bind(socket_addr).await?;
|
||||
let mut incoming = connection.incoming();
|
||||
|
||||
#[cfg(feature = "ogp_images")]
|
||||
let app = app
|
||||
.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));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
.service(resource("/") .to(handle_basic_request))
|
||||
.service(resource("/{prefs}") .to(handle_basic_request))
|
||||
.service(resource("/{name}/{prefs}").to(handle_basic_request))
|
||||
.default_service(web::to(not_found))
|
||||
})
|
||||
.bind(&socket_addr)?
|
||||
.run()
|
||||
.await
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 name = name.map(|n| (*n).to_owned());
|
||||
|
||||
if is_thumb {
|
||||
Route::SendThumbnail(name, prefs)
|
||||
} else {
|
||||
let uri = request_uri.trim_end_matches('/').to_owned();
|
||||
Route::SendPronounPage(name, prefs, uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<String>, Option<UserPreferences>, String),
|
||||
|
||||
/// 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(Vec<u8>),
|
||||
|
||||
/// 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::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<String>,
|
||||
prefs: Option<UserPreferences>,
|
||||
uri: String,
|
||||
conf: &Conf,
|
||||
) -> Result<Response, BadRequest> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
#[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: 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<u8>,
|
||||
settings: &InstanceSettings,
|
||||
) -> Result<Response, BadRequest> {
|
||||
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::<usize>() {
|
||||
let w = v.parse::<u8>().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<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
|
||||
///
|
||||
/// 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<IoSlice<'_>> {
|
||||
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)]
|
||||
|
@ -216,36 +597,22 @@ struct ErrorPage {
|
|||
msg: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
InvlaidPrefString,
|
||||
impl ErrorPage {
|
||||
fn from_msg(msg: &str) -> Vec<u8> {
|
||||
ErrorPage {
|
||||
msg: msg.into()
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
.into_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let msg = match self {
|
||||
&Error::InvlaidPrefString => "This URL contains an invalid pronoun preference string",
|
||||
};
|
||||
write!(f, "{}", msg)
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
struct IndexTemplate<'a> {
|
||||
name: Option<String>,
|
||||
pronoun: &'a Pronoun,
|
||||
pronouns: Vec<(usize, &'a Pronoun)>,
|
||||
url: String,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
@ -22,30 +20,20 @@ pub struct StaticAsset {
|
|||
}
|
||||
|
||||
impl StaticAsset {
|
||||
/// Generate a actix resource for serving this asset
|
||||
///
|
||||
/// The resource will handle requests at `/static/{filename}`. Caching headers are
|
||||
/// 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);
|
||||
const STATIC_HEADERS: &'static [(&'static [u8], Cow<'static, [u8]>)] = &[
|
||||
(b"Cache-Control", Cow::Borrowed(DEFAULT_CACHE)),
|
||||
(b"Trans-People", Cow::Borrowed(b"Are Gods")),
|
||||
];
|
||||
|
||||
let req_etag = req.headers().get("If-None-Match");
|
||||
match req_etag {
|
||||
Some(etag) if etag == env!("CARGO_PKG_VERSION") => {
|
||||
response.status(StatusCode::from_u16(304).unwrap()).finish()
|
||||
}
|
||||
_ => {
|
||||
response.body(bytes)
|
||||
}
|
||||
}
|
||||
})
|
||||
/// Generate the HTTP response for sending this asset to the client
|
||||
// I wrote all this code to make this a const fn, and then don't even use it in
|
||||
// compile-time :(
|
||||
pub const fn generate_response(&self) -> Response {
|
||||
Response {
|
||||
status: b"200",
|
||||
headers: Cow::Borrowed(StaticAsset::STATIC_HEADERS),
|
||||
body: Cow::Borrowed(self.bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +54,7 @@ macro_rules! static_asset {
|
|||
pub const FONT: StaticAsset = static_asset!("font.otf");
|
||||
|
||||
/// A list of static assets which should be served by the server
|
||||
pub const STATIC_ASSETS: &[StaticAsset] = &[
|
||||
pub const STATIC_ASSETS: &[&StaticAsset] = &[
|
||||
#[cfg(any(feature = "embed_static_assets"))]
|
||||
FONT,
|
||||
&FONT,
|
||||
];
|
||||
|
|
105
web/src/write_vectored_all.rs
Normal file
105
web/src/write_vectored_all.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::slice;
|
||||
use std::future::Future;
|
||||
use std::io::{Error, ErrorKind, IoSlice, Result};
|
||||
use std::mem::replace;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
|
||||
use futures_lite::AsyncWrite;
|
||||
use futures_lite::ready;
|
||||
|
||||
pub trait AsyncWriteAllVectored: AsyncWrite {
|
||||
fn write_all_vectored<'a>(&'a mut self, bufs: &'a mut [IoSlice<'a>]) -> WriteAllVectoredFuture<'a, Self>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
WriteAllVectoredFuture { writer: self, bufs }
|
||||
}
|
||||
}
|
||||
|
||||
impl <T: AsyncWrite> AsyncWriteAllVectored for T {}
|
||||
|
||||
pub struct WriteAllVectoredFuture<'a, W: Unpin + ?Sized> {
|
||||
writer: &'a mut W,
|
||||
bufs: &'a mut [IoSlice<'a>],
|
||||
}
|
||||
|
||||
impl<W: Unpin + ?Sized> Unpin for WriteAllVectoredFuture<'_, W> {}
|
||||
|
||||
impl<W: AsyncWrite + Unpin + ?Sized> Future for WriteAllVectoredFuture<'_, W> {
|
||||
type Output = Result<()>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
let Self { writer, bufs } = &mut *self;
|
||||
|
||||
// Guarantee that bufs is empty if it contains no data,
|
||||
// to avoid calling write_vectored if there is no data to be written.
|
||||
advance_slices(bufs, 0);
|
||||
while !bufs.is_empty() {
|
||||
match ready!(Pin::new(&mut ** writer).poll_write_vectored(cx, bufs)) {
|
||||
Ok(0) => {
|
||||
return Poll::Ready(Err(Error::new(
|
||||
ErrorKind::WriteZero,
|
||||
"failed to write whole buffer",
|
||||
)));
|
||||
}
|
||||
Ok(n) => advance_slices(bufs, n),
|
||||
Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
|
||||
Err(e) => return Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_slices(bufs: &mut &mut [IoSlice<'_>], n: usize) {
|
||||
// Number of buffers to remove.
|
||||
let mut remove = 0;
|
||||
// Total length of all the to be removed buffers.
|
||||
let mut accumulated_len = 0;
|
||||
for buf in bufs.iter() {
|
||||
if accumulated_len + buf.len() > n {
|
||||
break;
|
||||
} else {
|
||||
accumulated_len += buf.len();
|
||||
remove += 1;
|
||||
}
|
||||
}
|
||||
|
||||
*bufs = &mut replace(bufs, &mut [])[remove..];
|
||||
if !bufs.is_empty() {
|
||||
advance(&mut bufs[0], n - accumulated_len);
|
||||
}
|
||||
}
|
||||
|
||||
fn advance<'a>(buf: &mut IoSlice<'a>, n: usize) {
|
||||
if buf.len() < n {
|
||||
panic!("advancing IoSlice beyond its length");
|
||||
}
|
||||
// This is just a hacky way of advancing the pointer inside the IoSlice
|
||||
// SAFTEY: The newly constructed IoSlice has the same lifetime as the old and
|
||||
// this is guaranteed not to overflow the buffer due to the previous check
|
||||
unsafe {
|
||||
let mut ptr = buf.as_ptr() as *mut u8;
|
||||
ptr = ptr.add(n);
|
||||
let len = buf.len() - n;
|
||||
let new_slice: &'a [u8] = slice::from_raw_parts(ptr, len);
|
||||
*buf = IoSlice::new(new_slice);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_advance() {
|
||||
let expected: Vec<_> = (10..100).collect();
|
||||
let buf: Vec<_> = (0..100).collect();
|
||||
let mut io_slice = IoSlice::new(&buf);
|
||||
advance(&mut io_slice, 10);
|
||||
assert_eq!(io_slice.len(), 90);
|
||||
assert_eq!(&*io_slice, &expected);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue