diff --git a/crates/wastebin_server/src/handlers/delete/form.rs b/crates/wastebin_server/src/handlers/delete/form.rs index e52dc01..f0ac6a9 100644 --- a/crates/wastebin_server/src/handlers/delete/form.rs +++ b/crates/wastebin_server/src/handlers/delete/form.rs @@ -17,7 +17,7 @@ pub async fn delete( async { let id = id.parse()?; db.delete_for(id, &uids).await?; - Ok(Redirect::to("/")) + Ok(crate::redirect("/")) } .await .map_err(|err| make_error(err, page.clone(), theme, lang)) diff --git a/crates/wastebin_server/src/handlers/extract.rs b/crates/wastebin_server/src/handlers/extract.rs index 9f2efbf..c644e0e 100644 --- a/crates/wastebin_server/src/handlers/extract.rs +++ b/crates/wastebin_server/src/handlers/extract.rs @@ -186,7 +186,7 @@ where .and_then(|referer| referer.to_str().ok()) .map(|referer| { if referer.starts_with('/') && !referer.starts_with("//") { - Redirect::to(referer) + crate::redirect(referer) } else { referer .parse::() @@ -198,10 +198,10 @@ where |q| Redirect::to(&format!("{path}?{q}")), ) }) - .unwrap_or_else(|| Redirect::to("/")) + .unwrap_or_else(|| crate::redirect("/")) } }) - .unwrap_or_else(|| Redirect::to("/")); + .unwrap_or_else(|| crate::redirect("/")); Ok(SafeReferer(redirect)) } diff --git a/crates/wastebin_server/src/handlers/html/burn.rs b/crates/wastebin_server/src/handlers/html/burn.rs index fd425b5..8625704 100644 --- a/crates/wastebin_server/src/handlers/html/burn.rs +++ b/crates/wastebin_server/src/handlers/html/burn.rs @@ -43,6 +43,7 @@ pub async fn get( #[template(path = "burn.html", escape = "none")] pub(crate) struct Burn { page: Page, + prefix: str, key: Key, code: qrcodegen::QrCode, theme: Option, diff --git a/crates/wastebin_server/src/handlers/html/paste.rs b/crates/wastebin_server/src/handlers/html/paste.rs index fd03dfe..a6aa87c 100644 --- a/crates/wastebin_server/src/handlers/html/paste.rs +++ b/crates/wastebin_server/src/handlers/html/paste.rs @@ -90,7 +90,7 @@ pub async fn get( } let mut cookie = cookie("uid", serialize_uids(&new_uids)); cookie.set_secure(true); - return Ok((jar.add(cookie), Redirect::to(&format!("/{id}"))).into_response()); + return Ok((jar.add(cookie), crate::redirect(&format!("/{id}"))).into_response()); } async { diff --git a/crates/wastebin_server/src/handlers/insert/form.rs b/crates/wastebin_server/src/handlers/insert/form.rs index 13760cd..e1d496c 100644 --- a/crates/wastebin_server/src/handlers/insert/form.rs +++ b/crates/wastebin_server/src/handlers/insert/form.rs @@ -87,7 +87,7 @@ pub async fn post( let mut cookie = cookie("uid", serialize_uids(&uids)); cookie.set_secure(true); - Ok((jar.add(cookie), Redirect::to(&url))) + Ok((jar.add(cookie), crate::redirect(&url))) } .await .map_err(|err| make_error(err, page, theme, lang)) diff --git a/crates/wastebin_server/src/i18n.rs b/crates/wastebin_server/src/i18n.rs index c3d289a..4fc566b 100644 --- a/crates/wastebin_server/src/i18n.rs +++ b/crates/wastebin_server/src/i18n.rs @@ -97,7 +97,7 @@ static EN: phf::Map<&'static str, &'static str> = phf_map! { "stats.label.limit" => "limit", "burn.title" => "Burn after reading", - "burn.body" => "Copy and send this link. The recipient will be shown a confirmation prompt. The paste is deleted the moment they confirm.", + "burn.body" => "Copy and send this link. The recipient will be shown a confirmation prompt. The paste is deleted the moment they confirm.", "burn_confirm.body" => "This paste will be permanently deleted the moment it is revealed. You will not be able to view it again.", "burn_confirm.cancel" => "cancel", @@ -171,7 +171,7 @@ static DE: phf::Map<&'static str, &'static str> = phf_map! { "stats.label.limit" => "Limit", "burn.title" => "Nach Lesen vernichten", - "burn.body" => "Kopiere und schicke diesen Link. Dem Empfänger wird eine Bestätigungsaufforderung angezeigt und der Paste nach Bestätigung gelöscht.", + "burn.body" => "Kopiere und schicke diesen Link. Dem Empfänger wird eine Bestätigungsaufforderung angezeigt und der Paste nach Bestätigung gelöscht.", "burn_confirm.body" => "Dieser Paste wird unwiderruflich gelöscht, sobald er angezeigt wird und kann danach nicht mehr eingesehen werden.", "burn_confirm.cancel" => "Abbrechen", @@ -245,7 +245,7 @@ static ZH: phf::Map<&'static str, &'static str> = phf_map! { "stats.label.limit" => "限制", "burn.title" => "阅后即焚", - "burn.body" => "复制并发送 此链接。收件人将看到确认提示。在他们确认的那一刻,剪贴将被删除。", + "burn.body" => "复制并发送 此链接。收件人将看到确认提示。在他们确认的那一刻,剪贴将被删除。", "burn_confirm.body" => "此剪贴在显示的那一刻将被 永久删除。您将无法再次查看它。", "burn_confirm.cancel" => "取消", @@ -277,8 +277,8 @@ mod tests { #[test] fn t_with_substitutes_placeholder() { - let s = Lang::En.t_with("burn.body", "abc123"); - assert!(s.contains("href=\"/abc123\"")); + let s = Lang::En.t_with("burn.body", "foobar", "abc123"); + assert!(s.contains("href=\"foobar/abc123\"")); } #[test] diff --git a/crates/wastebin_server/src/main.rs b/crates/wastebin_server/src/main.rs index 46df896..ad7d242 100644 --- a/crates/wastebin_server/src/main.rs +++ b/crates/wastebin_server/src/main.rs @@ -15,7 +15,7 @@ use std::time::Duration; use axum::extract::{DefaultBodyLimit, FromRef, Request, State}; use axum::http::{HeaderName, HeaderValue, StatusCode}; use axum::middleware::{Next, from_fn, from_fn_with_state}; -use axum::response::{IntoResponse, Response}; +use axum::response::{IntoResponse, Redirect, Response}; use axum::routing::{Router, get, post}; use axum_extra::extract::cookie::Key; use futures::future::TryFutureExt; @@ -24,6 +24,7 @@ use http::header::{ X_XSS_PROTECTION, }; use tokio::net::{TcpListener, UnixListener}; +use tokio::task_local; use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::timeout::TimeoutLayer; @@ -36,6 +37,10 @@ use crate::handlers::{delete, download, html, insert, raw, robots, theme}; use crate::i18n::Lang; use wastebin_core::db::Database; +task_local! { + static PATH_PREFIX: String; +} + /// Reference counted [`page::Page`] wrapper. pub(crate) type Page = Arc; @@ -81,6 +86,26 @@ impl FromRef for Cache { } } +fn path_prefix() -> String { + PATH_PREFIX.with(|val| val.to_string()) +} + +fn redirect(uri: &str) -> Redirect { + let prefixed_uri = path_prefix() + uri; + Redirect::to(&prefixed_uri) +} + +async fn path_prefix_layer(req: Request, next: Next) -> Response { + let prefix = req + .headers() + .get("x-forwarded-prefix") + .and_then(|val| val.to_str().ok()) + .unwrap_or("") + .to_string(); + + PATH_PREFIX.scope(prefix, next.run(req)).await +} + async fn security_headers_layer(req: Request, next: Next) -> impl IntoResponse { // Rendered Markdown may embed remote images via `![](…)`; relax img-src for that route only. const CSP_STRICT: HeaderValue = HeaderValue::from_static( @@ -239,6 +264,7 @@ fn make_app(state: AppState, timeout: Duration, max_body_size: usize) -> Router timeout, )) .layer(from_fn_with_state(state.clone(), handle_service_errors)) + .layer(from_fn(path_prefix_layer)) .layer(from_fn(security_headers_layer)), ) .with_state(state) diff --git a/crates/wastebin_server/templates/base.html b/crates/wastebin_server/templates/base.html index 7dd4676..7bfb2e1 100644 --- a/crates/wastebin_server/templates/base.html +++ b/crates/wastebin_server/templates/base.html @@ -1,3 +1,4 @@ +{% let path_prefix = crate::path_prefix() %} @@ -7,19 +8,19 @@ {{ page.title }}{% block title_content %}{% endblock %} {% match theme %} {% when Some with (Theme::Dark) %} - + {% when Some with (Theme::Light) %} - + {% when Some with (Theme::System) %} - - + + {% when None %} - - + + {% endmatch %} - - - + + + {% block head %}{% endblock %} @@ -27,7 +28,7 @@
{% endblock %} + diff --git a/crates/wastebin_server/templates/rendered.html b/crates/wastebin_server/templates/rendered.html index 81f92bf..abca0aa 100644 --- a/crates/wastebin_server/templates/rendered.html +++ b/crates/wastebin_server/templates/rendered.html @@ -1,7 +1,7 @@ {% extends "paste.html" %} {% block view_toggle %} - + {% endblock %}