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
2 changes: 1 addition & 1 deletion crates/wastebin_server/src/handlers/delete/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions crates/wastebin_server/src/handlers/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<url::Url>()
Expand All @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions crates/wastebin_server/src/handlers/html/burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Theme>,
Expand Down
2 changes: 1 addition & 1 deletion crates/wastebin_server/src/handlers/html/paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub async fn get<E>(
}
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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/wastebin_server/src/handlers/insert/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub async fn post<E: std::fmt::Debug>(
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))
Expand Down
10 changes: 5 additions & 5 deletions crates/wastebin_server/src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a class=\"text-link\" href=\"/{0}\">this link</a>. The recipient will be shown a confirmation prompt. The paste is deleted the moment they confirm.",
"burn.body" => "Copy and send <a class=\"text-link\" href=\"{0}//{1}\">this link</a>. The recipient will be shown a confirmation prompt. The paste is deleted the moment they confirm.",

"burn_confirm.body" => "This paste will be <strong>permanently deleted</strong> the moment it is revealed. You will not be able to view it again.",
"burn_confirm.cancel" => "cancel",
Expand Down Expand Up @@ -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 <a class=\"text-link\" href=\"/{0}\">diesen Link</a>. Dem Empfänger wird eine Bestätigungsaufforderung angezeigt und der Paste nach Bestätigung gelöscht.",
"burn.body" => "Kopiere und schicke <a class=\"text-link\" href=\"{0}/{1}\">diesen Link</a>. Dem Empfänger wird eine Bestätigungsaufforderung angezeigt und der Paste nach Bestätigung gelöscht.",

"burn_confirm.body" => "Dieser Paste wird <strong>unwiderruflich gelöscht</strong>, sobald er angezeigt wird und kann danach nicht mehr eingesehen werden.",
"burn_confirm.cancel" => "Abbrechen",
Expand Down Expand Up @@ -245,7 +245,7 @@ static ZH: phf::Map<&'static str, &'static str> = phf_map! {
"stats.label.limit" => "限制",

"burn.title" => "阅后即焚",
"burn.body" => "复制并发送 <a class=\"text-link\" href=\"/{0}\">此链接</a>。收件人将看到确认提示。在他们确认的那一刻,剪贴将被删除。",
"burn.body" => "复制并发送 <a class=\"text-link\" href=\"{0}/{1}\">此链接</a>。收件人将看到确认提示。在他们确认的那一刻,剪贴将被删除。",

"burn_confirm.body" => "此剪贴在显示的那一刻将被 <strong>永久删除</strong>。您将无法再次查看它。",
"burn_confirm.cancel" => "取消",
Expand Down Expand Up @@ -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]
Expand Down
28 changes: 27 additions & 1 deletion crates/wastebin_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<page::Page>;

Expand Down Expand Up @@ -81,6 +86,26 @@ impl FromRef<AppState> 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(
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 23 additions & 22 deletions crates/wastebin_server/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% let path_prefix = crate::path_prefix() %}
<!DOCTYPE html>
<html lang="{{ lang.code() }}">
<head>
Expand All @@ -7,27 +8,27 @@
<title>{{ page.title }}{% block title_content %}{% endblock %}</title>
{% match theme %}
{% when Some with (Theme::Dark) %}
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}">
{% when Some with (Theme::Light) %}
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}">
{% when Some with (Theme::System) %}
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
{% when None %}
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
{% endmatch %}
<link rel="stylesheet" href="{{ page.assets.css.style.route() }}">
<link rel="icon" href="{{ page.assets.favicon.route() }}" type="image/png">
<noscript><link rel="stylesheet" href="{{ page.assets.css.no_js.route() }}"></noscript>
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.style.route() }}">
<link rel="icon" href="{{ path_prefix }}{{ page.assets.favicon.route() }}" type="image/png">
<noscript><link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.no_js.route() }}"></noscript>
{% block head %}{% endblock %}
</head>
<body{% block body_attrs %}{% endblock %}>
{% block body %}
<div id="main-container">
<header>
<div class="nav-group">
<a href="/" class="nav-button" title="{{ lang.t("nav.home") }}" aria-label="{{ lang.t("nav.home") }}">
<a href="{{ path_prefix }}/" class="nav-button" title="{{ lang.t("nav.home") }}" aria-label="{{ lang.t("nav.home") }}">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
</a>
{% block title %}{% endblock %}
Expand All @@ -37,43 +38,43 @@
<div class="theme-switcher">
{% match theme %}
{% when None %}
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
</a>
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</a>
<a href="/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</a>
{% when Some with (Theme::System) %}
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
</a>
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</a>
<a href="/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</a>
{% when Some with (Theme::Dark) %}
<a href="/theme?pref=dark" class="theme-opt active" title="{{ lang.t("theme.dark") }}">
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt active" title="{{ lang.t("theme.dark") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
</a>
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</a>
<a href="/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</a>
{% when Some with (Theme::Light) %}
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
</a>
<a href="/theme?pref=light" class="theme-opt active" title="{{ lang.t("theme.light") }}">
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt active" title="{{ lang.t("theme.light") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
</a>
<a href="/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</a>
{% endmatch %}
Expand Down
4 changes: 2 additions & 2 deletions crates/wastebin_server/templates/burn-confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
{% if let Some(title) = title %}
<div class="dialog-section dialog-subtitle">{{ title }}</div>
{% endif %}
<form class="dialog-section dialog-actions" action="/{{ id }}" method="post">
<form class="dialog-section dialog-actions" action="{{ path_prefix }}/{{ id }}" method="post">
<input type="hidden" name="confirm_burn" value="1">
<a class="button button-secondary" href="/">{{ lang.t("burn_confirm.cancel") }}</a>
<a class="button button-secondary" href="{{ path_prefix }}/">{{ lang.t("burn_confirm.cancel") }}</a>
<button class="button" type="submit">{{ lang.t("burn_confirm.reveal") }}</button>
</form>
</div>
Expand Down
2 changes: 1 addition & 1 deletion crates/wastebin_server/templates/burn.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="dialog">
<div class="dialog-header">{{ lang.t("burn.title") }}</div>
<div class="dialog-section">
{{ lang.t_with("burn.body", key) }}
{{ lang.t_with("burn.body", path_prefix, key) }}
</div>
<div class="dialog-section dialog-qr">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {{ code.size() + 4 }} {{ code.size() + 4 }}" stroke="none">
Expand Down
8 changes: 4 additions & 4 deletions crates/wastebin_server/templates/encrypted.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{% extends "base.html" %}

{% block head %}
<script defer src="{{ page.assets.password_toggle_js.route() }}"></script>
<script defer src="{{path_prefix}}{{ page.assets.password_toggle_js.route() }}"></script>
{% endblock %}

{% block content %}
<div class="flex-center">
<div class="dialog">
<div class="dialog-header">{{ lang.t("encrypted.title") }}</div>
<form action="/{{ id }}" method="post">
<input type="hidden" name="confirm_burn" value="1">
<form action="{{ path_prefix }}/{{ id }}" method="post">
<input type="hidden" name="``confirm_burn" value="1">
<div class="dialog-section">
<div class="password-input-wrap">
<input type="password" id="password" name="password" placeholder="{{ lang.t("encrypted.placeholder") }}" autofocus>
Expand All @@ -20,7 +20,7 @@
</div>
</div>
<div class="dialog-section dialog-actions">
<a class="button button-secondary" href="/">{{ lang.t("encrypted.cancel") }}</a>
<a class="button button-secondary" href="{{ path_prefix }}/">{{ lang.t("encrypted.cancel") }}</a>
<button class="button" type="submit">{{ lang.t("encrypted.decrypt") }}</button>
</div>
</form>
Expand Down
2 changes: 1 addition & 1 deletion crates/wastebin_server/templates/error.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{{ description }}
</div>
<div class="dialog-section dialog-actions">
<a class="button button-secondary" href="/">{{ lang.t("error.back") }}</a>
<a class="button button-secondary" href="{{ path_prefix }}/">{{ lang.t("error.back") }}</a>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions crates/wastebin_server/templates/formatted.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{% block view_toggle %}
{% if is_markdown %}
<a id="view-toggle" href="/md/{{ key }}" class="nav-button" title="{{ lang.t("nav.rendered") }}" aria-label="{{ lang.t("nav.rendered") }}">
<a id="view-toggle" href="{{ path_prefix }}/md/{{ key }}" class="nav-button" title="{{ lang.t("nav.rendered") }}" aria-label="{{ lang.t("nav.rendered") }}">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" stroke="none"><path d="M22.269 19.385H1.731A1.73 1.73 0 0 1 0 17.654V6.345a1.73 1.73 0 0 1 1.731-1.73h20.538A1.73 1.73 0 0 1 24 6.345v11.308a1.73 1.73 0 0 1-1.731 1.731zm-16.5-3.462v-4.5l2.308 2.885 2.307-2.885v4.5h2.308V8.078h-2.308l-2.307 2.885-2.308-2.885H3.46v7.847zM21.231 12h-2.308V8.077h-2.307V12h-2.308l3.461 4.039z"/></svg>
</a>
{% endif %}
Expand All @@ -14,6 +14,6 @@
</div>
{% if !is_available %}
<div id="burn-message" hidden data-message="{{ lang.t("paste.toast.burned") }}"></div>
<script defer src="{{ page.assets.burn_js.route()}}"></script>
<script defer src="{{ path_prefix }}{{ page.assets.burn_js.route()}}"></script>
{% endif %}
{% endblock %}
Loading