Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ run-time behavior:
| `WASTEBIN_SIGNING_KEY` | Key to sign cookies. Must be at least 64 bytes long. | Random key generated at startup, i.e. cookies will become invalid after restarts and paste creators will not be able to delete their pastes. |
| `WASTEBIN_THEME` | Theme colors, one of `ayu`, `base16ocean`, `coldark`, `gruvbox`, `monokai`, `onehalf`, `solarized`. | `ayu` |
| `WASTEBIN_TITLE` | HTML page title. | `wastebin` |
| `WASTEBIN_PASTE_MAX_EXPIRATION` | Maximum expiration time in seconds. Disable with 0. | `0` |
| `RUST_LOG` | Log level. Besides the typical `trace`, `debug`, `info` etc. keys, you can also set the `tower_http` key to a log level to get additional request and response logs. | |


Expand Down
2 changes: 2 additions & 0 deletions crates/wastebin_core/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod vars {
pub const PASSWORD_SALT: &str = "WASTEBIN_PASSWORD_SALT";
/// Expirations list.
pub const PASTE_EXPIRATIONS: &str = "WASTEBIN_PASTE_EXPIRATIONS";
/// Maximum expiration time.
pub const PASTE_MAX_EXPIRATION: &str = "WASTEBIN_PASTE_MAX_EXPIRATION";
/// Signing key for signed cookie store.
pub const SIGNING_KEY: &str = "WASTEBIN_SIGNING_KEY";
/// Theme to use.
Expand Down
14 changes: 12 additions & 2 deletions crates/wastebin_server/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use crate::{expiration, highlight};
use axum_extra::extract::cookie::Key;
use std::env::VarError;
use std::net::SocketAddr;
use std::num::{NonZeroUsize, ParseIntError};
use std::num::{NonZero, NonZeroU32, NonZeroUsize, ParseIntError};
use std::path::PathBuf;
use std::time::Duration;
use wastebin_core::db;
use wastebin_core::env::vars::{
self, ADDRESS_PORT, BASE_URL, CACHE_SIZE, DATABASE_PATH, HTTP_TIMEOUT, MAX_BODY_SIZE,
PASTE_EXPIRATIONS, SIGNING_KEY,
PASTE_EXPIRATIONS, PASTE_MAX_EXPIRATION, SIGNING_KEY,
};

pub const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(5);
Expand All @@ -31,6 +31,8 @@ pub(crate) enum Error {
HttpTimeout(ParseIntError),
#[error("failed to parse {PASTE_EXPIRATIONS}: {0}")]
ParsePasteExpiration(#[from] expiration::Error),
#[error("failed to parse {PASTE_MAX_EXPIRATION}: {0}")]
ParsePasteMaxExpiration(ParseIntError),
#[error("unknown theme {0}")]
UnknownTheme(String),
}
Expand Down Expand Up @@ -138,3 +140,11 @@ pub fn expiration_set() -> Result<expiration::ExpirationSet, Error> {

Ok(set)
}

pub fn max_expiration() -> Result<Option<NonZeroU32>, Error> {
std::env::var(vars::PASTE_MAX_EXPIRATION)
.ok()
.map(|value| value.parse::<u32>().map_err(Error::ParsePasteMaxExpiration))
.transpose()
.map(|op| op.and_then(NonZero::new))
}
4 changes: 3 additions & 1 deletion crates/wastebin_server/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub(crate) enum Error {
Id(#[from] id::Error),
#[error("malformed form data")]
MalformedForm,
#[error("expires too far in the future")]
TooLongExpires,
}

#[derive(Serialize)]
Expand All @@ -45,7 +47,7 @@ impl From<Error> for StatusCode {
| Error::Database(db::Error::Crypto(crypto::Error::ChaCha20Poly1305Decrypt)) => {
StatusCode::FORBIDDEN
}
Error::Id(_) | Error::UrlParsing(_) => StatusCode::BAD_REQUEST,
Error::Id(_) | Error::UrlParsing(_) | Error::TooLongExpires => StatusCode::BAD_REQUEST,
Error::MalformedForm => StatusCode::UNPROCESSABLE_ENTITY,
Error::Join(_)
| Error::QrCode(_)
Expand Down
98 changes: 85 additions & 13 deletions crates/wastebin_server/src/handlers/insert/api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::errors::{Error, JsonErrorResponse};
use crate::AppState;
use crate::errors::JsonErrorResponse;
use axum::Json;
use axum::extract::State;
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
use wastebin_core::db::{Database, write};
use wastebin_core::db::write;
use wastebin_core::id::Id;

use super::common_insert;

#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Entry {
pub text: String,
Expand Down Expand Up @@ -36,19 +39,21 @@ impl From<Entry> for write::Entry {
}

pub async fn post(
State(db): State<Database>,
State(appstate): State<AppState>,
Json(entry): Json<Entry>,
) -> Result<Json<RedirectResponse>, JsonErrorResponse> {
let id = Id::rand();
let entry: write::Entry = entry.into();
let path = format!("/{}", id.to_url_path(&entry));
db.insert(id, entry).await.map_err(Error::Database)?;
common_insert(&appstate, id, entry).await?;

Ok(Json::from(RedirectResponse { path }))
}

#[cfg(test)]
mod tests {
use std::num::NonZero;

use crate::handlers::extract::PASSWORD_HEADER_NAME;
use crate::test_helpers::{Client, StoreCookies};
use reqwest::StatusCode;
Expand All @@ -63,14 +68,80 @@ mod tests {
..Default::default()
};

let res = client.post_json().json(&entry).send().await?;
let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);

let payload = res.json::<super::RedirectResponse>().await?;
let payload = res.json::<super::RedirectResponse>().await.unwrap();

let res = client.get(&format!("/raw{}", payload.path)).send().await?;
let res = client
.get(&format!("/raw{}", payload.path))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await?, "FooBarBaz");
assert_eq!(res.text().await.unwrap(), "FooBarBaz");

Ok(())
}

#[tokio::test]
async fn insert_fail_max_expire() -> Result<(), Box<dyn std::error::Error>> {
let client =
Client::new_with_max_expire(StoreCookies(false), Some(NonZero::new(10000).unwrap()))
.await;

{
let entry = Entry {
text: "FooBarBaz".to_string(),
..Default::default()
};

let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}

{
let entry = Entry {
text: "FooBarBaz".to_string(),
expires: None,
..Default::default()
};

let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}

{
let entry = Entry {
text: "FooBarBaz".to_string(),
expires: Some(NonZero::new(10001).unwrap()),
..Default::default()
};

let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}

{
let entry = Entry {
text: "FooBarBaz".to_string(),
expires: Some(NonZero::new(10000).unwrap()),
..Default::default()
};

let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);

let payload = res.json::<super::RedirectResponse>().await.unwrap();

let res = client
.get(&format!("/raw{}", payload.path))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await.unwrap(), "FooBarBaz");
}

Ok(())
}
Expand All @@ -81,7 +152,7 @@ mod tests {

let entry = "Hello World";

let res = client.post_json().json(&entry).send().await?;
let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);

Ok(())
Expand All @@ -98,19 +169,20 @@ mod tests {
..Default::default()
};

let res = client.post_json().json(&entry).send().await?;
let res = client.post_json().json(&entry).send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);

let payload = res.json::<super::RedirectResponse>().await?;
let payload = res.json::<super::RedirectResponse>().await.unwrap();

let res = client
.get(&format!("/raw{}", payload.path))
.header(PASSWORD_HEADER_NAME, password)
.send()
.await?;
.await
.unwrap();

assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await?, "FooBarBaz");
assert_eq!(res.text().await.unwrap(), "FooBarBaz");

Ok(())
}
Expand Down
12 changes: 7 additions & 5 deletions crates/wastebin_server/src/handlers/insert/form.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use crate::Page;
use crate::handlers::extract::{Theme, Uid};
use crate::handlers::html::make_error;
use crate::{AppState, Page};
use axum::extract::{Form, State};
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::cookie::{Cookie, SameSite, SignedCookieJar};
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
use wastebin_core::db::{Database, write};
use wastebin_core::db::write;
use wastebin_core::id::Id;

use super::common_insert;

#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct Entry {
pub text: String,
Expand Down Expand Up @@ -44,7 +46,7 @@ impl From<Entry> for write::Entry {

pub async fn post<E: std::fmt::Debug>(
State(page): State<Page>,
State(db): State<Database>,
State(appstate): State<AppState>,
jar: SignedCookieJar,
headers: HeaderMap,
uid: Option<Uid>,
Expand Down Expand Up @@ -73,7 +75,7 @@ pub async fn post<E: std::fmt::Debug>(
let uid = if let Some(Uid(uid)) = uid {
uid
} else {
db.next_uid().await?
appstate.db.next_uid().await?
};

let mut entry: write::Entry = entry.into();
Expand All @@ -86,7 +88,7 @@ pub async fn post<E: std::fmt::Debug>(
url = format!("burn/{url}");
}

db.insert(id, entry).await?;
common_insert(&appstate, id, entry).await?;
let url = format!("/{url}");

let cookie = Cookie::build(("uid", uid.to_string()))
Expand Down
18 changes: 18 additions & 0 deletions crates/wastebin_server/src/handlers/insert/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
use wastebin_core::{db::write, id::Id};

use crate::AppState;
use crate::Error;
use crate::Error::TooLongExpires;

pub mod api;
pub mod form;

async fn common_insert(appstate: &AppState, id: Id, entry: write::Entry) -> Result<(), Error> {
if let Some(max_expiration) = appstate.page.max_expiration {
if entry.expires.is_none_or(|exp| exp > max_expiration) {
Err(TooLongExpires)?;
}
}

appstate.db.insert(id, entry).await?;

Ok(())
}
12 changes: 10 additions & 2 deletions crates/wastebin_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ async fn start() -> Result<(), Box<dyn std::error::Error>> {
let expirations = env::expiration_set()?;
let theme = env::theme()?;
let title = env::title();
let max_expiration = env::max_expiration()?;

let cache = Cache::new(cache_size);
let db = Database::new(method)?;
Expand All @@ -253,8 +254,15 @@ async fn start() -> Result<(), Box<dyn std::error::Error>> {
tracing::debug!("caching {cache_size} paste highlights");
tracing::debug!("restricting maximum body size to {max_body_size} bytes");
tracing::debug!("enforcing a http timeout of {timeout:#?}");

let page = Arc::new(page::Page::new(title, base_url, theme, expirations));
tracing::debug!("enforcing a maximum expiry of {max_expiration:?}");

let page = Arc::new(page::Page::new(
title,
base_url,
theme,
expirations,
max_expiration,
));
let highlighter = Arc::new(highlight::Highlighter::default());
let state = AppState {
db,
Expand Down
12 changes: 11 additions & 1 deletion crates/wastebin_server/src/page.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::num::NonZeroU32;

use crate::assets::{Asset, Css, Kind};
use crate::expiration::{Expiration, ExpirationSet};
use crate::highlight::Theme;
Expand All @@ -18,12 +20,19 @@ pub(crate) struct Page {
pub assets: Assets,
pub base_url: Url,
pub expirations: Vec<Expiration>,
pub max_expiration: Option<NonZeroU32>,
}

impl Page {
/// Create new page meta data from generated `assets`, `title` and optional `base_url`.
#[must_use]
pub fn new(title: String, base_url: Url, theme: Theme, expirations: ExpirationSet) -> Self {
pub fn new(
title: String,
base_url: Url,
theme: Theme,
expirations: ExpirationSet,
max_expiration: Option<NonZeroU32>,
) -> Self {
let assets = Assets::new(theme);
let expirations = expirations.into_inner();

Expand All @@ -33,6 +42,7 @@ impl Page {
assets,
base_url,
expirations,
max_expiration,
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion crates/wastebin_server/src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::page;
use axum_extra::extract::cookie::Key;
use reqwest::RequestBuilder;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
Expand All @@ -21,6 +21,13 @@ pub(crate) struct StoreCookies(pub bool);

impl Client {
pub(crate) async fn new(store_cookies: StoreCookies) -> Self {
Self::new_with_max_expire(store_cookies, None).await
}

pub(crate) async fn new_with_max_expire(
store_cookies: StoreCookies,
max_expiration: Option<NonZeroU32>,
) -> Self {
let db = Database::new(db::Open::Memory).expect("open memory database");
let cache = Cache::new(NonZeroUsize::new(128).unwrap());
let key = Key::generate();
Expand All @@ -30,6 +37,7 @@ impl Client {
url::Url::parse("https://localhost:8888").unwrap(),
Theme::Ayu,
expirations,
max_expiration,
));
let state = crate::AppState {
db,
Expand Down