129 lines
4.4 KiB
Rust
129 lines
4.4 KiB
Rust
use crate::session::SessionStore;
|
|
use std::net::IpAddr;
|
|
|
|
/// Methods that do not require a valid session cookie.
|
|
pub(super) const UNAUTHENTICATED_METHODS: &[&str] = &[
|
|
"auth.login",
|
|
"auth.login.totp",
|
|
"auth.login.backup",
|
|
"auth.isOnboardingComplete",
|
|
"auth.isSetup",
|
|
"auth.setup",
|
|
"auth.onboardingComplete",
|
|
"health",
|
|
// Server readiness check (Login.vue polls this before showing form)
|
|
"server.echo",
|
|
// Onboarding flow (before user has a session — DID creation, signing, backup)
|
|
"node.did",
|
|
"node.signChallenge",
|
|
"node.nostr-pubkey",
|
|
"node.createBackup",
|
|
"identity.create",
|
|
"identity.verify",
|
|
"identity.resolve-did",
|
|
// Seed management (onboarding — before user has a session)
|
|
"seed.generate",
|
|
"seed.verify",
|
|
"seed.restore",
|
|
"seed.save-encrypted",
|
|
// Onboarding restore (before user account exists)
|
|
"backup.restore-identity",
|
|
// Inter-node RPC: called by federated peers over Tor, no session cookies
|
|
"federation.peer-joined",
|
|
"federation.peer-address-changed",
|
|
"federation.peer-did-changed",
|
|
"federation.get-state",
|
|
// Fleet telemetry ingest: called by remote nodes posting reports
|
|
"telemetry.ingest",
|
|
];
|
|
|
|
/// Methods whose responses can be cached for a few seconds.
|
|
pub(super) const CACHEABLE_METHODS: &[&str] = &["system.stats", "federation.list-nodes"];
|
|
|
|
/// Sanitize error messages before returning to clients.
|
|
/// Keeps user-facing validation errors but strips internal system details.
|
|
pub(super) fn sanitize_error_message(msg: &str) -> String {
|
|
// Allow known validation errors through (these are user-actionable)
|
|
let user_facing_prefixes = [
|
|
"Invalid",
|
|
"Missing",
|
|
"Not found",
|
|
"Already exists",
|
|
"Rate limit",
|
|
"Unauthorized",
|
|
"Forbidden",
|
|
"Not supported",
|
|
"Requires",
|
|
"requires",
|
|
"must be",
|
|
"cannot",
|
|
"Password",
|
|
"Session",
|
|
"Failed to pull",
|
|
"Failed to start",
|
|
"Container",
|
|
"Image",
|
|
"Bitcoin address",
|
|
];
|
|
for prefix in &user_facing_prefixes {
|
|
if msg.starts_with(prefix) {
|
|
// Truncate long messages and strip file paths
|
|
let sanitized = msg
|
|
.replace("/var/lib/archipelago/", "[data]/")
|
|
.replace("/usr/local/bin/", "[bin]/")
|
|
.replace("/etc/", "[config]/");
|
|
return if sanitized.len() > 200 {
|
|
format!("{}...", &sanitized[..200])
|
|
} else {
|
|
sanitized
|
|
};
|
|
}
|
|
}
|
|
// For all other errors, return a generic message
|
|
"Operation failed. Check server logs for details.".to_string()
|
|
}
|
|
|
|
/// Derive a CSRF token from the session token via HMAC.
|
|
/// Deterministic: same session token always produces the same CSRF token.
|
|
/// Survives backend restarts because it depends only on the session token
|
|
/// and the on-disk remember secret (not ephemeral state).
|
|
pub(super) async fn derive_csrf_token(session_token: &str) -> String {
|
|
use hmac::{Hmac, Mac};
|
|
use sha2::Sha256;
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
let secret = SessionStore::load_or_create_remember_secret().await;
|
|
let mut mac = HmacSha256::new_from_slice(&secret).expect("HMAC key");
|
|
mac.update(format!("csrf:{}", session_token).as_bytes());
|
|
hex::encode(mac.finalize().into_bytes())
|
|
}
|
|
|
|
/// Extract a named cookie value from headers.
|
|
pub(super) fn extract_cookie(headers: &hyper::HeaderMap, name: &str) -> Option<String> {
|
|
let prefix = format!("{}=", name);
|
|
for value in headers.get_all("cookie") {
|
|
if let Ok(s) = value.to_str() {
|
|
for part in s.split(';') {
|
|
let part = part.trim();
|
|
if let Some(val) = part.strip_prefix(&prefix) {
|
|
let val = val.trim();
|
|
if !val.is_empty() {
|
|
return Some(val.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Extract the client IP from request headers (X-Real-IP or X-Forwarded-For).
|
|
pub(super) fn extract_client_ip(headers: &hyper::HeaderMap) -> IpAddr {
|
|
headers
|
|
.get("x-real-ip")
|
|
.or_else(|| headers.get("x-forwarded-for"))
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|s| s.split(',').next())
|
|
.and_then(|s| s.trim().parse::<IpAddr>().ok())
|
|
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
|
|
}
|