This commit is contained in:
netadmin committed 2026-10-25 22:48:38 +03:00
1 parent b0d63c9f0f
commit 552c009120
8 files changed
+114 -83

No files matched your search

+12 -52
View File
@@ -8,7 +8,11 @@ use axum::{
Json,
};
use futures_util::StreamExt;
use std::{io, net::IpAddr, sync::Arc};
use std::{
io,
net::IpAddr,
sync::{atomic::Ordering, Arc},
};
use tokio_util::io::ReaderStream;
const INDEX_HTML: &str = include_str!("static/index.html");
@@ -17,31 +21,11 @@ const APP_JS: &str = include_str!("static/app.js");
const CSP: &str = "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src 'self'; \
connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
// ───────────────────────── Защита UI (CSRF / DNS-rebinding) ─────────────────────────
// ───────────────────────── Защита UI (анти-CSRF / DNS-rebinding без токена) ─────────────────────────
pub struct UiGuard {
pub token: String,
pub cookie_name: String,
pub hosts: Vec<String>,
}
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}
fn cookie_ok(headers: &HeaderMap, g: &UiGuard) -> bool {
headers
.get_all(header::COOKIE)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|s| s.split(';'))
.any(|kv| {
kv.trim()
.strip_prefix(g.cookie_name.as_str())
.and_then(|r| r.strip_prefix('='))
.map_or(false, |v| ct_eq(v.as_bytes(), g.token.as_bytes()))
})
}
fn with_security_headers(mut resp: Response) -> Response {
let items: [(HeaderName, &'static str); 5] = [
(header::CONTENT_SECURITY_POLICY, CSP),
@@ -57,9 +41,8 @@ fn with_security_headers(mut resp: Response) -> Response {
resp
}
/// 1) Host только 127.0.0.1/localhost:порт (анти-rebinding)
/// 2) POST только с Origin нашего же хоста (анти-CSRF)
/// 3) Доступ по токену из консоли: /?t=… выставляет HttpOnly SameSite=Strict cookie
/// 1) Host только 127.0.0.1/localhost:порт (анти-DNS-rebinding)
/// 2) POST только с Origin нашего же локального хоста (анти-CSRF)
pub async fn ui_guard(State(g): State<Arc<UiGuard>>, req: Request, next: Next) -> Response {
let headers = req.headers();
@@ -82,31 +65,6 @@ pub async fn ui_guard(State(g): State<Arc<UiGuard>>, req: Request, next: Next) -
}
}
if req.uri().path() == "/" {
if let Some(q) = req.uri().query() {
let token_ok = q
.split('&')
.filter_map(|kv| kv.strip_prefix("t="))
.any(|t| ct_eq(t.as_bytes(), g.token.as_bytes()));
if token_ok {
let mut resp = StatusCode::SEE_OTHER.into_response();
resp.headers_mut().insert(header::LOCATION, HeaderValue::from_static("/"));
if let Ok(c) = HeaderValue::from_str(&format!(
"{}={}; Path=/; HttpOnly; SameSite=Strict",
g.cookie_name, g.token
)) {
resp.headers_mut().insert(header::SET_COOKIE, c);
}
return with_security_headers(resp);
}
}
}
if !cookie_ok(headers, &g) {
return with_security_headers(
(StatusCode::FORBIDDEN, "Откройте адрес из консоли (с токеном ?t=…)").into_response(),
);
}
with_security_headers(next.run(req).await)
}
@@ -137,7 +95,9 @@ pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json
"my_fingerprint": state.my_fingerprint,
"my_pin": short_id(&state.my_fingerprint),
"media_dir": state.media_dir.display().to_string(),
"ping_interval_sec": state.ping_interval_sec.load(std::sync::atomic::Ordering::Relaxed),
"ping_interval_sec": state.ping_interval_sec.load(Ordering::Relaxed),
"last_ping": state.last_ping.load(Ordering::Relaxed),
"now": now_sec(),
"ping_min": MIN_PING_SEC,
"ping_max": MAX_PING_SEC,
"peers": peers,
@@ -213,7 +173,7 @@ pub async fn ui_scan_range(
}
pub async fn ui_ping_peers(State(state): State<Arc<AppState>>) -> StatusCode {
tokio::spawn(async move { net::ping_all_peers(&state).await });
net::ping_all_peers(&state).await;
StatusCode::OK
}