Skip to main content

ckb_sentry_types/
auth.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
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<DateTime<Utc>>,
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
64                        .parse()
65                        .ok()
66                        .and_then(|ts| timestamp_to_datetime(ts).single())
67                        .or_else(|| value.parse().ok());
68
69                    rv.timestamp = timestamp;
70                }
71                "sentry_client" => {
72                    rv.client = Some(value.into());
73                }
74                "sentry_version" => {
75                    rv.version = value
76                        .splitn(2, '.')
77                        .next()
78                        .and_then(|v| v.parse().ok())
79                        .ok_or(ParseAuthError::InvalidVersion)?;
80                }
81                "sentry_key" => {
82                    rv.key = value.into();
83                }
84                "sentry_secret" => {
85                    rv.secret = Some(value.into());
86                }
87                _ => {}
88            }
89        }
90
91        if rv.key.is_empty() {
92            return Err(ParseAuthError::MissingPublicKey);
93        }
94
95        Ok(rv)
96    }
97
98    /// Creates an auth header from a query string.
99    pub fn from_querystring(qs: &[u8]) -> Result<Auth, ParseAuthError> {
100        Auth::from_pairs(form_urlencoded::parse(qs))
101    }
102
103    /// Returns the unix timestamp the client defined
104    pub fn timestamp(&self) -> Option<DateTime<Utc>> {
105        self.timestamp
106    }
107
108    /// Returns the protocol version the client speaks
109    pub fn version(&self) -> u16 {
110        self.version
111    }
112
113    /// Returns the public key
114    pub fn public_key(&self) -> &str {
115        &self.key
116    }
117
118    /// Returns the client's secret if it authenticated with a secret.
119    pub fn secret_key(&self) -> Option<&str> {
120        self.secret.as_deref()
121    }
122
123    /// Returns true if the authentication implies public auth (no secret)
124    pub fn is_public(&self) -> bool {
125        self.secret.is_none()
126    }
127
128    /// Returns the client's agent
129    pub fn client_agent(&self) -> Option<&str> {
130        self.client.as_deref()
131    }
132}
133
134impl fmt::Display for Auth {
135    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136        write!(
137            f,
138            "Sentry sentry_key={}, sentry_version={}",
139            self.key, self.version
140        )?;
141        if let Some(ts) = self.timestamp {
142            write!(f, ", sentry_timestamp={}", datetime_to_timestamp(&ts))?;
143        }
144        if let Some(ref client) = self.client {
145            write!(f, ", sentry_client={}", client)?;
146        }
147        if let Some(ref secret) = self.secret {
148            write!(f, ", sentry_secret={}", secret)?;
149        }
150        Ok(())
151    }
152}
153
154impl FromStr for Auth {
155    type Err = ParseAuthError;
156
157    fn from_str(s: &str) -> Result<Auth, ParseAuthError> {
158        let mut base_iter = s.splitn(2, ' ');
159
160        let prefix = base_iter.next().unwrap_or("");
161        let items = base_iter.next().unwrap_or("");
162
163        if !prefix.eq_ignore_ascii_case("sentry") {
164            return Err(ParseAuthError::NonSentryAuth);
165        }
166
167        let auth = Self::from_pairs(items.split(',').filter_map(|item| {
168            let mut kviter = item.split('=');
169            Some((kviter.next()?.trim(), kviter.next()?.trim()))
170        }))?;
171
172        if auth.key.is_empty() {
173            return Err(ParseAuthError::MissingPublicKey);
174        }
175
176        Ok(auth)
177    }
178}
179
180pub(crate) fn auth_from_dsn_and_client(dsn: &Dsn, client: Option<&str>) -> Auth {
181    Auth {
182        timestamp: Some(Utc::now()),
183        client: client.map(|x| x.to_string()),
184        version: protocol::LATEST,
185        key: dsn.public_key().to_string(),
186        secret: dsn.secret_key().map(|x| x.to_string()),
187    }
188}