1use std::net::SocketAddr;
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use tonic::service::Interceptor;
8use tonic::{Request, Status};
9
10#[derive(Clone)]
12pub struct TokenAuth {
13 expected: Option<Arc<str>>,
14}
15
16impl TokenAuth {
17 pub fn new(token: Option<String>) -> Self {
18 TokenAuth {
19 expected: token.map(Into::into),
20 }
21 }
22}
23
24impl Interceptor for TokenAuth {
25 fn call(&mut self, request: Request<()>) -> Result<Request<()>, Status> {
26 let Some(expected) = &self.expected else {
27 return Ok(request);
28 };
29 let presented = request
30 .metadata()
31 .get("authorization")
32 .and_then(|value| value.to_str().ok())
33 .and_then(|value| value.strip_prefix("Bearer "));
34 match presented {
35 Some(token) if constant_time_eq(token.as_bytes(), expected.as_bytes()) => Ok(request),
36 _ => Err(Status::unauthenticated("invalid or missing token")),
37 }
38 }
39}
40
41fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
43 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
44}
45
46pub fn generate_token() -> Result<String> {
48 let mut bytes = [0u8; 32];
49 getrandom::fill(&mut bytes).map_err(|e| anyhow::anyhow!("reading system randomness: {e}"))?;
50 Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
51}
52
53pub fn check_exposure(
56 listen: SocketAddr,
57 has_token: bool,
58 has_tls: bool,
59 allow_unauthenticated: bool,
60) -> Result<Option<&'static str>> {
61 let local = listen.ip().is_loopback();
62 if !local && !has_token && !allow_unauthenticated {
63 bail!(
64 "refusing to listen on {listen} without a token: anyone who can reach it could read your logs. \
65 Set --token (generate one with `telelog-server gen-token`), listen on 127.0.0.1, \
66 or pass --allow-unauthenticated if a proxy in front handles auth."
67 );
68 }
69 Ok(match (local, has_token, has_tls) {
70 (false, false, _) => Some("running without authentication on a non-local address"),
71 (false, true, false) => {
72 Some("token is sent in plain text; enable --tls-cert/--tls-key or terminate TLS in a proxy")
73 }
74 _ => None,
75 })
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 fn request(header: Option<&str>) -> Request<()> {
83 let mut request = Request::new(());
84 if let Some(header) = header {
85 request.metadata_mut().insert("authorization", header.parse().unwrap());
86 }
87 request
88 }
89
90 #[test]
91 fn accepts_only_the_right_token() {
92 let mut auth = TokenAuth::new(Some("s3cret".into()));
93 assert!(auth.call(request(Some("Bearer s3cret"))).is_ok());
94 for bad in [
95 None,
96 Some("Bearer wrong"),
97 Some("Bearer s3cre"),
98 Some("s3cret"),
99 Some("Basic s3cret"),
100 ] {
101 let status = auth.call(request(bad)).unwrap_err();
102 assert_eq!(status.code(), tonic::Code::Unauthenticated, "{bad:?}");
103 }
104 }
105
106 #[test]
107 fn no_token_configured_allows_everything() {
108 assert!(TokenAuth::new(None).call(request(None)).is_ok());
109 }
110
111 #[test]
112 fn generated_tokens_are_long_and_distinct() {
113 let (a, b) = (generate_token().unwrap(), generate_token().unwrap());
114 assert_eq!(a.len(), 64);
115 assert_ne!(a, b);
116 }
117
118 #[test]
119 fn exposure_rules() {
120 let local: SocketAddr = "127.0.0.1:7070".parse().unwrap();
121 let public: SocketAddr = "0.0.0.0:7070".parse().unwrap();
122 assert_eq!(check_exposure(local, false, false, false).unwrap(), None);
123 assert!(check_exposure(public, false, false, false).is_err());
124 assert!(check_exposure(public, false, false, true).unwrap().is_some());
125 assert!(check_exposure(public, true, false, false).unwrap().is_some());
126 assert_eq!(check_exposure(public, true, true, false).unwrap(), None);
127 }
128}