Skip to main content

o_sfu/runtime/http_server/
access.rs

1//! HTTP authorization schemes and operator access bound to the serving listener.
2
3use std::net::SocketAddr;
4
5use axum::http::{HeaderMap, StatusCode, header};
6use secrecy::{ExposeSecret, SecretString};
7use sha2::{Digest, Sha256};
8use subtle::ConstantTimeEq;
9
10/// Operator authorization bound to the listener that serves the router.
11#[derive(Clone)]
12pub(super) struct OperatorAccessPolicy {
13    auth_digest: Option<[u8; 32]>,
14    listener_is_loopback: bool,
15}
16
17impl OperatorAccessPolicy {
18    pub(super) fn new(auth_token: Option<&SecretString>, listener_address: SocketAddr) -> Self {
19        Self {
20            auth_digest: auth_token
21                .map(|token| Sha256::digest(token.expose_secret().trim()).into()),
22            listener_is_loopback: listener_address.ip().is_loopback(),
23        }
24    }
25
26    /// Authorizes operator requests with the configured token or loopback listener.
27    ///
28    /// A configured token requires the `Bearer` authorization scheme and disables
29    /// the loopback fallback.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`StatusCode::UNAUTHORIZED`] when a configured token is missing or
34    /// does not match. Without a configured token, a non-loopback listener returns
35    /// [`StatusCode::FORBIDDEN`].
36    pub(super) fn authorize(&self, headers: &HeaderMap) -> Result<(), StatusCode> {
37        if let Some(expected_digest) = self.auth_digest.as_ref() {
38            return match bearer_authorization_token(headers) {
39                Some(actual_token) if token_matches(actual_token, expected_digest) => Ok(()),
40                _ => Err(StatusCode::UNAUTHORIZED),
41            };
42        }
43        if self.listener_is_loopback {
44            Ok(())
45        } else {
46            Err(StatusCode::FORBIDDEN)
47        }
48    }
49}
50
51pub(super) fn room_authorization_token(headers: &HeaderMap) -> Option<&str> {
52    authorization_token(headers, &["Bearer", "jwt"])
53}
54
55fn bearer_authorization_token(headers: &HeaderMap) -> Option<&str> {
56    authorization_token(headers, &["Bearer"])
57}
58
59fn authorization_token<'headers>(
60    headers: &'headers HeaderMap,
61    accepted_schemes: &[&str],
62) -> Option<&'headers str> {
63    let value = headers
64        .get(header::AUTHORIZATION)
65        .and_then(|value| value.to_str().ok())?;
66    let (scheme, token) = value.split_once(' ')?;
67    if !accepted_schemes
68        .iter()
69        .any(|accepted_scheme| scheme.eq_ignore_ascii_case(accepted_scheme))
70    {
71        return None;
72    }
73    let token = token.trim_start();
74    if token.is_empty() {
75        return None;
76    }
77    Some(token)
78}
79
80/// Compares fixed-size digests without secret-length or matching-prefix shortcuts.
81///
82/// Hashing and request parsing still depend on the supplied token length.
83/// See <https://docs.rs/subtle/latest/subtle/trait.ConstantTimeEq.html>.
84fn token_matches(actual: &str, expected: &[u8; 32]) -> bool {
85    let actual: [u8; 32] = Sha256::digest(actual).into();
86    bool::from(actual.ct_eq(expected))
87}