Menu
AkurAI-Build
publicLatest change 5ecb45c5915e3bb122d43dbaf90b4eba39484c7d - organize native templates and reserve runtime routes by Olafur Bui
//! Public Rust-native application contract.
//!
//! This module deliberately contains no dynamic loading or framework
//! compatibility. Applications are compiled into a Bunfork binary and expose
//! ordinary Rust route tables.
use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
use anyhow::Error;
use axum::{
body::Bytes,
http::{HeaderMap, Method, StatusCode, Uri},
response::Response,
};
use serde_json::Value;
use subtle::ConstantTimeEq;
pub const MAX_FORM_BYTES: usize = 64 * 1024;
pub const MAX_FORM_FIELDS: usize = 128;
pub const MAX_FORM_KEY_BYTES: usize = 256;
pub const MAX_FORM_VALUE_BYTES: usize = 4096;
pub type Query = BTreeMap<String, Vec<String>>;
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
pub type LoadFn = fn(RequestContext) -> BoxFuture<Result<Value, AppError>>;
pub type ActionFn = fn(RequestContext, Form) -> BoxFuture<Result<ActionResult, AppError>>;
pub trait App: Send + Sync + 'static {
fn routes(&self) -> &'static [Route];
}
pub struct Route {
pub pattern: &'static str,
pub methods: &'static [Method],
pub page: &'static str,
pub load: Option<LoadFn>,
pub action: Option<ActionFn>,
}
#[derive(Clone)]
pub struct RequestContext {
pub method: Method,
pub uri: Uri,
pub params: BTreeMap<String, String>,
pub query: Query,
pub headers: HeaderMap,
pub cookies: Cookies,
pub body: Bytes,
pub state: Arc<AppState>,
}
#[derive(Clone, Default)]
pub struct AppState {
pub store: Option<Arc<dyn Store>>,
}
pub trait Store: Send + Sync {
fn get(&self, key: &str) -> anyhow::Result<Option<Value>>;
fn put(&self, key: &str, value: &Value) -> anyhow::Result<()>;
}
pub enum ActionResult {
Render(Value),
Redirect {
location: String,
status: StatusCode,
},
Response(Response),
}
pub enum AppError {
Status {
status: StatusCode,
public: &'static str,
},
Redirect {
location: String,
status: StatusCode,
},
Internal(Error),
}
impl AppError {
pub fn bad_request(message: &'static str) -> Self {
Self::Status {
status: StatusCode::BAD_REQUEST,
public: message,
}
}
pub fn unauthorized() -> Self {
Self::Status {
status: StatusCode::UNAUTHORIZED,
public: "unauthorized",
}
}
pub fn validation(message: &'static str) -> Self {
Self::Status {
status: StatusCode::UNPROCESSABLE_ENTITY,
public: message,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Cookies(BTreeMap<String, String>);
impl Cookies {
pub fn parse(headers: &HeaderMap) -> Self {
let mut cookies = BTreeMap::new();
let Some(value) = headers
.get(axum::http::header::COOKIE)
.and_then(|v| v.to_str().ok())
else {
return Self(cookies);
};
for item in value.split(';') {
let Some((name, value)) = item.trim().split_once('=') else {
continue;
};
if !name.is_empty() {
cookies.insert(name.to_owned(), value.to_owned());
}
}
Self(cookies)
}
pub fn get(&self, name: &str) -> Option<&str> {
self.0.get(name).map(String::as_str)
}
}
#[derive(Clone, Debug, Default)]
pub struct Form(BTreeMap<String, Vec<String>>);
impl Form {
pub fn parse(body: &[u8]) -> anyhow::Result<Self> {
anyhow::ensure!(body.len() <= MAX_FORM_BYTES, "form body exceeds 64 KiB");
let text = std::str::from_utf8(body).map_err(|_| anyhow::anyhow!("form is not UTF-8"))?;
let mut fields = BTreeMap::new();
let mut field_count = 0;
for pair in text.split('&') {
if pair.is_empty() {
continue;
}
field_count += 1;
anyhow::ensure!(field_count <= MAX_FORM_FIELDS, "form has too many fields");
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
let key = decode_component(key)?;
let value = decode_component(value)?;
anyhow::ensure!(!key.is_empty(), "form field name cannot be empty");
anyhow::ensure!(
key.len() <= MAX_FORM_KEY_BYTES,
"form field name is too long"
);
anyhow::ensure!(
value.len() <= MAX_FORM_VALUE_BYTES,
"form field value is too long"
);
fields.entry(key).or_insert_with(Vec::new).push(value);
}
Ok(Self(fields))
}
pub fn get(&self, name: &str) -> Option<&str> {
self.0.get(name).and_then(|v| v.first()).map(String::as_str)
}
pub fn values(&self, name: &str) -> &[String] {
self.0.get(name).map(Vec::as_slice).unwrap_or(&[])
}
}
pub fn constant_time_equal(left: &str, right: &str) -> bool {
left.len() == right.len() && bool::from(left.as_bytes().ct_eq(right.as_bytes()))
}
pub fn validate_csrf(form: &Form, cookie: Option<&str>, field: &str) -> Result<(), AppError> {
let Some(cookie) = cookie else {
return Err(AppError::bad_request("missing CSRF cookie"));
};
let Some(token) = form.get(field) else {
return Err(AppError::bad_request("missing CSRF token"));
};
if constant_time_equal(cookie, token) {
Ok(())
} else {
Err(AppError::bad_request("invalid CSRF token"))
}
}
fn decode_component(value: &str) -> anyhow::Result<String> {
let value = value.replace('+', " ");
for (index, byte) in value.as_bytes().iter().enumerate() {
if *byte == b'%' {
anyhow::ensure!(index + 2 < value.len(), "malformed form encoding");
anyhow::ensure!(
value.as_bytes()[index + 1].is_ascii_hexdigit()
&& value.as_bytes()[index + 2].is_ascii_hexdigit(),
"malformed form encoding"
);
}
}
percent_encoding::percent_decode_str(&value)
.decode_utf8()
.map(|v| v.into_owned())
.map_err(Into::into)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn form_and_cookie_parsing_preserve_repeated_values() {
let form = Form::parse(b"tag=one&tag=two&name=rust+web").expect("valid form");
assert_eq!(form.values("tag"), &["one".to_owned(), "two".to_owned()]);
assert_eq!(form.get("name"), Some("rust web"));
let mut headers = HeaderMap::new();
headers.insert("cookie", "sid=abc; csrf=xyz".parse().expect("header"));
let cookies = Cookies::parse(&headers);
assert_eq!(cookies.get("csrf"), Some("xyz"));
}
#[test]
fn csrf_requires_matching_constant_time_token() {
let form = Form::parse(b"_csrf=secret").expect("valid form");
assert!(validate_csrf(&form, Some("secret"), "_csrf").is_ok());
assert!(validate_csrf(&form, Some("other"), "_csrf").is_err());
assert!(!constant_time_equal("a", "A"));
}
#[test]
fn form_limits_and_malformed_encoding_fail_closed() {
assert!(Form::parse(b"bad=%zz").is_err());
assert!(Form::parse(&vec![b'x'; MAX_FORM_BYTES + 1]).is_err());
let too_many = std::iter::repeat_n("x=y", MAX_FORM_FIELDS + 1)
.collect::<Vec<_>>()
.join("&");
assert!(Form::parse(too_many.as_bytes()).is_err());
}
}