Skip to main content

rtsp_runtime/
auth.rs

1//! Authentication wiring — RFC 2326 §14 (RTSP reuses HTTP auth).
2//!
3//! RTSP shares HTTP's Basic and Digest schemes verbatim (RFC 2326 §16); see
4//! [`docs/auth.md`](../docs/auth.md). The [`Credentials`] model and the
5//! challenge-parsing/response-computation logic now live in the shared
6//! [`broadcast_auth`] crate — re-exported here so `rtsp-runtime` callers keep
7//! using `rtsp_runtime::Credentials`/`Authenticator` unchanged. This crate
8//! contributes only the one RTSP-specific rule: the `uri` used in the digest
9//! computation is the RTSP request URI (e.g. `rtsp://host/stream`), not an
10//! HTTP URL (RFC 2326 §14) — [`client::ClientSession`](crate::client::ClientSession)
11//! passes it through as-is.
12//!
13//! Sharing this logic with HTTP clients (rather than re-implementing it per
14//! client) is the point: `broadcast-auth` also backs `multimux`'s HTTP input
15//! adapters (TS-over-HTTP, HLS-pull) and adds Bearer (RFC 6750), which this
16//! crate did not previously support.
17
18pub use broadcast_auth::{Authenticator, Credentials, RequestContext};
19
20use crate::error::Error;
21
22impl From<broadcast_auth::Error> for Error {
23    fn from(e: broadcast_auth::Error) -> Self {
24        Error::Auth(e.to_string())
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    const CHALLENGE: &str = "Digest realm=\"IP Camera\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",qop=\"auth\",algorithm=MD5";
33
34    #[test]
35    fn digest_authorization_contains_required_fields() {
36        let mut auth =
37            Authenticator::from_challenge(CHALLENGE, Credentials::new("admin", "12345")).unwrap();
38        let value = auth
39            .authorization(&RequestContext::new(
40                "DESCRIBE",
41                "rtsp://camera.example.com/live",
42            ))
43            .unwrap();
44        assert!(value.starts_with("Digest "), "got: {value}");
45        for needle in ["response=", "realm=", "nonce=", "uri=", "cnonce=", "nc="] {
46            assert!(value.contains(needle), "missing {needle} in {value}");
47        }
48        assert!(value.contains("uri=\"rtsp://camera.example.com/live\""));
49    }
50
51    #[test]
52    fn basic_authorization_is_computed() {
53        let mut auth = Authenticator::from_challenge(
54            "Basic realm=\"IP Camera\"",
55            Credentials::new("admin", "12345"),
56        )
57        .unwrap();
58        let value = auth
59            .authorization(&RequestContext::new("DESCRIBE", "rtsp://c/live"))
60            .unwrap();
61        assert!(value.starts_with("Basic "));
62        assert_ne!(value, "Basic ");
63    }
64
65    #[test]
66    fn bearer_authorization_is_computed() {
67        let mut auth =
68            Authenticator::from_challenge("", Credentials::bearer("mytoken123")).unwrap();
69        let value = auth
70            .authorization(&RequestContext::new("DESCRIBE", "rtsp://c/live"))
71            .unwrap();
72        assert_eq!(value, "Bearer mytoken123");
73    }
74}