sentry_types/
auth.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::str::FromStr;
4use std::time::SystemTime;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use url::form_urlencoded;
9
10use crate::dsn::Dsn;
11use crate::protocol;
12use crate::utils::{datetime_to_timestamp, timestamp_to_datetime};
13
14/// Represents an auth header parsing error.
15#[derive(Debug, Error, Copy, Clone, Eq, PartialEq)]
16pub enum ParseAuthError {
17    /// Raised if the auth header is not indicating sentry auth
18    #[error("non sentry auth")]
19    NonSentryAuth,
20    /// Raised if the version value is invalid
21    #[error("invalid value for version")]
22    InvalidVersion,
23    /// Raised if the public key is missing entirely
24    #[error("missing public key in auth header")]
25    MissingPublicKey,
26}
27
28/// Represents an auth header.
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct Auth {
31    #[serde(skip)]
32    timestamp: Option<SystemTime>,
33    #[serde(rename = "sentry_client")]
34    client: Option<String>,
35    #[serde(rename = "sentry_version")]
36    version: u16,
37    #[serde(rename = "sentry_key")]
38    key: String,
39    #[serde(rename = "sentry_secret")]
40    secret: Option<String>,
41}
42
43impl Auth {
44    /// Creates an auth header from key value pairs.
45    pub fn from_pairs<'a, I, K, V>(pairs: I) -> Result<Auth, ParseAuthError>
46    where
47        I: IntoIterator<Item = (K, V)>,
48        K: AsRef<str>,
49        V: Into<Cow<'a, str>>,
50    {
51        let mut rv = Auth {
52            timestamp: None,
53            client: None,
54            version: protocol::LATEST,
55            key: "".into(),
56            secret: None,
57        };
58
59        for (key, value) in pairs {
60            let value = value.into();
61            match key.as_ref() {
62                "sentry_timestamp" => {
63                    let timestamp = value.parse().ok().and_then(timestamp_to_datetime);
64
65                    rv.timestamp = timestamp;
66                }
67                "sentry_client" => {
68                    rv.client = Some(value.into());
69                }
70                "sentry_version" => {
71                    rv.version = value
72                        .split('.')
73                        .next()
74                        .and_then(|v| v.parse().ok())
75                        .ok_or(ParseAuthError::InvalidVersion)?;
76                }
77                "sentry_key" => {
78                    rv.key = value.into();
79                }
80                "sentry_secret" => {
81                    rv.secret = Some(value.into());
82                }
83                _ => {}
84            }
85        }
86
87        if rv.key.is_empty() {
88            return Err(ParseAuthError::MissingPublicKey);
89        }
90
91        Ok(rv)
92    }
93
94    /// Creates an auth header from a query string.
95    pub fn from_querystring(qs: &[u8]) -> Result<Auth, ParseAuthError> {
96        Auth::from_pairs(form_urlencoded::parse(qs))
97    }
98
99    /// Returns the timestamp the client defined
100    pub fn timestamp(&self) -> Option<SystemTime> {
101        self.timestamp
102    }
103
104    /// Returns the protocol version the client speaks
105    pub fn version(&self) -> u16 {
106        self.version
107    }
108
109    /// Returns the public key
110    pub fn public_key(&self) -> &str {
111        &self.key
112    }
113
114    /// Returns the client's secret if it authenticated with a secret.
115    pub fn secret_key(&self) -> Option<&str> {
116        self.secret.as_deref()
117    }
118
119    /// Returns true if the authentication implies public auth (no secret)
120    pub fn is_public(&self) -> bool {
121        self.secret.is_none()
122    }
123
124    /// Returns the client's agent
125    pub fn client_agent(&self) -> Option<&str> {
126        self.client.as_deref()
127    }
128}
129
130impl fmt::Display for Auth {
131    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132        write!(
133            f,
134            "Sentry sentry_key={}, sentry_version={}",
135            self.key, self.version
136        )?;
137        if let Some(ts) = self.timestamp {
138            write!(f, ", sentry_timestamp={}", datetime_to_timestamp(&ts))?;
139        }
140        if let Some(ref client) = self.client {
141            write!(f, ", sentry_client={client}")?;
142        }
143        if let Some(ref secret) = self.secret {
144            write!(f, ", sentry_secret={secret}")?;
145        }
146        Ok(())
147    }
148}
149
150impl FromStr for Auth {
151    type Err = ParseAuthError;
152
153    fn from_str(s: &str) -> Result<Auth, ParseAuthError> {
154        let mut base_iter = s.splitn(2, ' ');
155
156        let prefix = base_iter.next().unwrap_or("");
157        let items = base_iter.next().unwrap_or("");
158
159        if !prefix.eq_ignore_ascii_case("sentry") {
160            return Err(ParseAuthError::NonSentryAuth);
161        }
162
163        let auth = Self::from_pairs(items.split(',').filter_map(|item| {
164            let mut kviter = item.split('=');
165            Some((kviter.next()?.trim(), kviter.next()?.trim()))
166        }))?;
167
168        if auth.key.is_empty() {
169            return Err(ParseAuthError::MissingPublicKey);
170        }
171
172        Ok(auth)
173    }
174}
175
176pub(crate) fn auth_from_dsn_and_client(dsn: &Dsn, client: Option<&str>) -> Auth {
177    Auth {
178        timestamp: Some(SystemTime::now()),
179        client: client.map(|x| x.to_string()),
180        version: protocol::LATEST,
181        key: dsn.public_key().to_string(),
182        secret: dsn.secret_key().map(|x| x.to_string()),
183    }
184}