hashtree_cli/server/
auth.rs

1use axum::{
2    body::Body,
3    extract::State,
4    http::{header, Request, Response, StatusCode},
5    middleware::Next,
6};
7use crate::storage::HashtreeStore;
8use crate::webrtc::WebRTCState;
9use std::collections::HashSet;
10use std::sync::Arc;
11
12#[derive(Clone)]
13pub struct AppState {
14    pub store: Arc<HashtreeStore>,
15    pub auth: Option<AuthCredentials>,
16    /// WebRTC peer state for forwarding requests to connected P2P peers
17    pub webrtc_peers: Option<Arc<WebRTCState>>,
18    /// Maximum upload size in bytes for Blossom uploads (default: 5 MB)
19    pub max_upload_bytes: usize,
20    /// Allow anyone with valid Nostr auth to write (default: true)
21    /// When false, only allowed_pubkeys can write
22    pub public_writes: bool,
23    /// Pubkeys allowed to write (hex format, from config allowed_npubs)
24    pub allowed_pubkeys: HashSet<String>,
25}
26
27#[derive(Clone)]
28pub struct AuthCredentials {
29    pub username: String,
30    pub password: String,
31}
32
33/// Auth middleware - validates HTTP Basic Auth
34pub async fn auth_middleware(
35    State(state): State<AppState>,
36    request: Request<Body>,
37    next: Next,
38) -> Result<Response<Body>, StatusCode> {
39    // If auth is not enabled, allow request
40    let Some(auth) = &state.auth else {
41        return Ok(next.run(request).await);
42    };
43
44    // Check Authorization header
45    let auth_header = request
46        .headers()
47        .get(header::AUTHORIZATION)
48        .and_then(|v| v.to_str().ok());
49
50    let authorized = if let Some(header_value) = auth_header {
51        if let Some(credentials) = header_value.strip_prefix("Basic ") {
52            use base64::Engine;
53            let engine = base64::engine::general_purpose::STANDARD;
54            if let Ok(decoded) = engine.decode(credentials) {
55                if let Ok(decoded_str) = String::from_utf8(decoded) {
56                    let expected = format!("{}:{}", auth.username, auth.password);
57                    decoded_str == expected
58                } else {
59                    false
60                }
61            } else {
62                false
63            }
64        } else {
65            false
66        }
67    } else {
68        false
69    };
70
71    if authorized {
72        Ok(next.run(request).await)
73    } else {
74        Ok(Response::builder()
75            .status(StatusCode::UNAUTHORIZED)
76            .header(header::WWW_AUTHENTICATE, "Basic realm=\"hashtree\"")
77            .body(Body::from("Unauthorized"))
78            .unwrap())
79    }
80}