Skip to content

Commit 4533625

Browse files
committed
Support path prefix in reverse proxy scenarios.
1 parent b494662 commit 4533625

17 files changed

Lines changed: 98 additions & 68 deletions

File tree

crates/wastebin_server/src/handlers/delete/form.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub async fn delete(
1717
async {
1818
let id = id.parse()?;
1919
db.delete_for(id, &uids).await?;
20-
Ok(Redirect::to("/"))
20+
Ok(crate::redirect("/"))
2121
}
2222
.await
2323
.map_err(|err| make_error(err, page.clone(), theme, lang))

crates/wastebin_server/src/handlers/extract.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ where
186186
.and_then(|referer| referer.to_str().ok())
187187
.map(|referer| {
188188
if referer.starts_with('/') && !referer.starts_with("//") {
189-
Redirect::to(referer)
189+
crate::redirect(referer)
190190
} else {
191191
referer
192192
.parse::<url::Url>()
@@ -198,10 +198,10 @@ where
198198
|q| Redirect::to(&format!("{path}?{q}")),
199199
)
200200
})
201-
.unwrap_or_else(|| Redirect::to("/"))
201+
.unwrap_or_else(|| crate::redirect("/"))
202202
}
203203
})
204-
.unwrap_or_else(|| Redirect::to("/"));
204+
.unwrap_or_else(|| crate::redirect("/"));
205205

206206
Ok(SafeReferer(redirect))
207207
}

crates/wastebin_server/src/handlers/html/burn.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub async fn get(
4343
#[template(path = "burn.html", escape = "none")]
4444
pub(crate) struct Burn {
4545
page: Page,
46+
prefix: str,
4647
key: Key,
4748
code: qrcodegen::QrCode,
4849
theme: Option<Theme>,

crates/wastebin_server/src/handlers/html/paste.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub async fn get<E>(
9090
}
9191
let mut cookie = cookie("uid", serialize_uids(&new_uids));
9292
cookie.set_secure(true);
93-
return Ok((jar.add(cookie), Redirect::to(&format!("/{id}"))).into_response());
93+
return Ok((jar.add(cookie), crate::redirect(&format!("/{id}"))).into_response());
9494
}
9595

9696
async {

crates/wastebin_server/src/handlers/insert/form.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub async fn post<E: std::fmt::Debug>(
8787
let mut cookie = cookie("uid", serialize_uids(&uids));
8888
cookie.set_secure(true);
8989

90-
Ok((jar.add(cookie), Redirect::to(&url)))
90+
Ok((jar.add(cookie), crate::redirect(&url)))
9191
}
9292
.await
9393
.map_err(|err| make_error(err, page, theme, lang))

crates/wastebin_server/src/i18n.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ static EN: phf::Map<&'static str, &'static str> = phf_map! {
9797
"stats.label.limit" => "limit",
9898

9999
"burn.title" => "Burn after reading",
100-
"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.",
100+
"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.",
101101

102102
"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.",
103103
"burn_confirm.cancel" => "cancel",
@@ -171,7 +171,7 @@ static DE: phf::Map<&'static str, &'static str> = phf_map! {
171171
"stats.label.limit" => "Limit",
172172

173173
"burn.title" => "Nach Lesen vernichten",
174-
"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.",
174+
"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.",
175175

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

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

250250
"burn_confirm.body" => "此剪贴在显示的那一刻将被 <strong>永久删除</strong>。您将无法再次查看它。",
251251
"burn_confirm.cancel" => "取消",
@@ -277,8 +277,8 @@ mod tests {
277277

278278
#[test]
279279
fn t_with_substitutes_placeholder() {
280-
let s = Lang::En.t_with("burn.body", "abc123");
281-
assert!(s.contains("href=\"/abc123\""));
280+
let s = Lang::En.t_with("burn.body", "foobar", "abc123");
281+
assert!(s.contains("href=\"foobar/abc123\""));
282282
}
283283

284284
#[test]

crates/wastebin_server/src/main.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::time::Duration;
1515
use axum::extract::{DefaultBodyLimit, FromRef, Request, State};
1616
use axum::http::{HeaderName, HeaderValue, StatusCode};
1717
use axum::middleware::{Next, from_fn, from_fn_with_state};
18-
use axum::response::{IntoResponse, Response};
18+
use axum::response::{IntoResponse, Redirect, Response};
1919
use axum::routing::{Router, get, post};
2020
use axum_extra::extract::cookie::Key;
2121
use futures::future::TryFutureExt;
@@ -24,6 +24,7 @@ use http::header::{
2424
X_XSS_PROTECTION,
2525
};
2626
use tokio::net::{TcpListener, UnixListener};
27+
use tokio::task_local;
2728
use tower::ServiceBuilder;
2829
use tower_http::compression::CompressionLayer;
2930
use tower_http::timeout::TimeoutLayer;
@@ -36,6 +37,10 @@ use crate::handlers::{delete, download, html, insert, raw, robots, theme};
3637
use crate::i18n::Lang;
3738
use wastebin_core::db::Database;
3839

40+
task_local! {
41+
static PATH_PREFIX: String;
42+
}
43+
3944
/// Reference counted [`page::Page`] wrapper.
4045
pub(crate) type Page = Arc<page::Page>;
4146

@@ -81,6 +86,26 @@ impl FromRef<AppState> for Cache {
8186
}
8287
}
8388

89+
fn path_prefix() -> String {
90+
PATH_PREFIX.with(|val| val.to_string())
91+
}
92+
93+
fn redirect(uri: &str) -> Redirect {
94+
let prefixed_uri = path_prefix() + uri;
95+
Redirect::to(&prefixed_uri)
96+
}
97+
98+
async fn path_prefix_layer(req: Request, next: Next) -> Response {
99+
let prefix = req
100+
.headers()
101+
.get("x-forwarded-prefix")
102+
.and_then(|val| val.to_str().ok())
103+
.unwrap_or("")
104+
.to_string();
105+
106+
PATH_PREFIX.scope(prefix, next.run(req)).await
107+
}
108+
84109
async fn security_headers_layer(req: Request, next: Next) -> impl IntoResponse {
85110
// Rendered Markdown may embed remote images via `![](…)`; relax img-src for that route only.
86111
const CSP_STRICT: HeaderValue = HeaderValue::from_static(
@@ -239,6 +264,7 @@ fn make_app(state: AppState, timeout: Duration, max_body_size: usize) -> Router
239264
timeout,
240265
))
241266
.layer(from_fn_with_state(state.clone(), handle_service_errors))
267+
.layer(from_fn(path_prefix_layer))
242268
.layer(from_fn(security_headers_layer)),
243269
)
244270
.with_state(state)

crates/wastebin_server/templates/base.html

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{% let path_prefix = crate::path_prefix() %}
12
<!DOCTYPE html>
23
<html lang="{{ lang.code() }}">
34
<head>
@@ -7,27 +8,27 @@
78
<title>{{ page.title }}{% block title_content %}{% endblock %}</title>
89
{% match theme %}
910
{% when Some with (Theme::Dark) %}
10-
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}">
11+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}">
1112
{% when Some with (Theme::Light) %}
12-
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}">
13+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}">
1314
{% when Some with (Theme::System) %}
14-
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
15-
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
15+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
16+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
1617
{% when None %}
17-
<link rel="stylesheet" href="{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
18-
<link rel="stylesheet" href="{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
18+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.dark.route() }}" media="(prefers-color-scheme: dark)">
19+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.light.route() }}" media="(prefers-color-scheme: light)">
1920
{% endmatch %}
20-
<link rel="stylesheet" href="{{ page.assets.css.style.route() }}">
21-
<link rel="icon" href="{{ page.assets.favicon.route() }}" type="image/png">
22-
<noscript><link rel="stylesheet" href="{{ page.assets.css.no_js.route() }}"></noscript>
21+
<link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.style.route() }}">
22+
<link rel="icon" href="{{ path_prefix }}{{ page.assets.favicon.route() }}" type="image/png">
23+
<noscript><link rel="stylesheet" href="{{ path_prefix }}{{ page.assets.css.no_js.route() }}"></noscript>
2324
{% block head %}{% endblock %}
2425
</head>
2526
<body{% block body_attrs %}{% endblock %}>
2627
{% block body %}
2728
<div id="main-container">
2829
<header>
2930
<div class="nav-group">
30-
<a href="/" class="nav-button" title="{{ lang.t("nav.home") }}" aria-label="{{ lang.t("nav.home") }}">
31+
<a href="{{ path_prefix }}/" class="nav-button" title="{{ lang.t("nav.home") }}" aria-label="{{ lang.t("nav.home") }}">
3132
<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>
3233
</a>
3334
{% block title %}{% endblock %}
@@ -37,43 +38,43 @@
3738
<div class="theme-switcher">
3839
{% match theme %}
3940
{% when None %}
40-
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
41+
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
4142
<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>
4243
</a>
43-
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
44+
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
4445
<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>
4546
</a>
46-
<a href="/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
47+
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
4748
<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>
4849
</a>
4950
{% when Some with (Theme::System) %}
50-
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
51+
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
5152
<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>
5253
</a>
53-
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
54+
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
5455
<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>
5556
</a>
56-
<a href="/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
57+
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt active" title="{{ lang.t("theme.auto") }}">
5758
<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>
5859
</a>
5960
{% when Some with (Theme::Dark) %}
60-
<a href="/theme?pref=dark" class="theme-opt active" title="{{ lang.t("theme.dark") }}">
61+
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt active" title="{{ lang.t("theme.dark") }}">
6162
<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>
6263
</a>
63-
<a href="/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
64+
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt" title="{{ lang.t("theme.light") }}">
6465
<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>
6566
</a>
66-
<a href="/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
67+
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
6768
<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>
6869
</a>
6970
{% when Some with (Theme::Light) %}
70-
<a href="/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
71+
<a href="{{ path_prefix }}/theme?pref=dark" class="theme-opt" title="{{ lang.t("theme.dark") }}">
7172
<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>
7273
</a>
73-
<a href="/theme?pref=light" class="theme-opt active" title="{{ lang.t("theme.light") }}">
74+
<a href="{{ path_prefix }}/theme?pref=light" class="theme-opt active" title="{{ lang.t("theme.light") }}">
7475
<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>
7576
</a>
76-
<a href="/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
77+
<a href="{{ path_prefix }}/theme?pref=system" class="theme-opt" title="{{ lang.t("theme.auto") }}">
7778
<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>
7879
</a>
7980
{% endmatch %}

crates/wastebin_server/templates/burn-confirmation.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
{% if let Some(title) = title %}
1010
<div class="dialog-section dialog-subtitle">{{ title }}</div>
1111
{% endif %}
12-
<form class="dialog-section dialog-actions" action="/{{ id }}" method="post">
12+
<form class="dialog-section dialog-actions" action="{{ path_prefix }}/{{ id }}" method="post">
1313
<input type="hidden" name="confirm_burn" value="1">
14-
<a class="button button-secondary" href="/">{{ lang.t("burn_confirm.cancel") }}</a>
14+
<a class="button button-secondary" href="{{ path_prefix }}/">{{ lang.t("burn_confirm.cancel") }}</a>
1515
<button class="button" type="submit">{{ lang.t("burn_confirm.reveal") }}</button>
1616
</form>
1717
</div>

crates/wastebin_server/templates/burn.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="dialog">
55
<div class="dialog-header">{{ lang.t("burn.title") }}</div>
66
<div class="dialog-section">
7-
{{ lang.t_with("burn.body", key) }}
7+
{{ lang.t_with("burn.body", path_prefix, key) }}
88
</div>
99
<div class="dialog-section dialog-qr">
1010
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {{ code.size() + 4 }} {{ code.size() + 4 }}" stroke="none">

0 commit comments

Comments
 (0)