Skip to main content

rns_ctl/
auth.rs

1use crate::config::CtlConfig;
2use crate::http::{HttpRequest, HttpResponse};
3
4/// Check authentication on an HTTP request.
5/// Returns Ok(()) if authenticated, Err(response) with 401 if not.
6pub fn check_auth(req: &HttpRequest, config: &CtlConfig) -> Result<(), HttpResponse> {
7    if config.disable_auth {
8        return Ok(());
9    }
10
11    let expected = match &config.auth_token {
12        Some(t) => t.as_str(),
13        None => return Ok(()), // No token configured and auth not disabled = open (shouldn't happen)
14    };
15
16    let auth_header = req.headers.get("authorization");
17    match auth_header {
18        Some(val) => {
19            if let Some(token) = val.strip_prefix("Bearer ") {
20                if token == expected {
21                    Ok(())
22                } else {
23                    Err(HttpResponse::unauthorized("Invalid token"))
24                }
25            } else {
26                Err(HttpResponse::unauthorized("Expected Bearer token"))
27            }
28        }
29        None => Err(HttpResponse::unauthorized("Missing Authorization header")),
30    }
31}
32
33/// Check WebSocket auth via query parameter `?token=...`.
34pub fn check_ws_auth(query: &str, config: &CtlConfig) -> Result<(), HttpResponse> {
35    if config.disable_auth {
36        return Ok(());
37    }
38
39    let expected = match &config.auth_token {
40        Some(t) => t.as_str(),
41        None => return Ok(()),
42    };
43
44    let params = crate::http::parse_query(query);
45    match params.get("token") {
46        Some(token) if token == expected => Ok(()),
47        _ => Err(HttpResponse::unauthorized("Missing or invalid token")),
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use std::collections::HashMap;
55
56    fn make_config(token: Option<&str>, disable: bool) -> CtlConfig {
57        CtlConfig {
58            auth_token: token.map(String::from),
59            disable_auth: disable,
60            ..CtlConfig::default()
61        }
62    }
63
64    fn make_req(auth_header: Option<&str>) -> HttpRequest {
65        let mut headers = HashMap::new();
66        if let Some(val) = auth_header {
67            headers.insert("authorization".into(), val.into());
68        }
69        HttpRequest {
70            method: "GET".into(),
71            path: "/api/info".into(),
72            query: String::new(),
73            headers,
74            body: Vec::new(),
75        }
76    }
77
78    #[test]
79    fn auth_disabled() {
80        let config = make_config(Some("secret"), true);
81        assert!(check_auth(&make_req(None), &config).is_ok());
82    }
83
84    #[test]
85    fn auth_no_token_configured() {
86        let config = make_config(None, false);
87        assert!(check_auth(&make_req(None), &config).is_ok());
88    }
89
90    #[test]
91    fn auth_valid_token() {
92        let config = make_config(Some("secret"), false);
93        assert!(check_auth(&make_req(Some("Bearer secret")), &config).is_ok());
94    }
95
96    #[test]
97    fn auth_invalid_token() {
98        let config = make_config(Some("secret"), false);
99        assert!(check_auth(&make_req(Some("Bearer wrong")), &config).is_err());
100    }
101
102    #[test]
103    fn auth_missing_header() {
104        let config = make_config(Some("secret"), false);
105        assert!(check_auth(&make_req(None), &config).is_err());
106    }
107
108    #[test]
109    fn ws_auth_valid() {
110        let config = make_config(Some("abc"), false);
111        assert!(check_ws_auth("token=abc", &config).is_ok());
112    }
113
114    #[test]
115    fn ws_auth_invalid() {
116        let config = make_config(Some("abc"), false);
117        assert!(check_ws_auth("token=xyz", &config).is_err());
118    }
119}