From 1c28a6663e0e11d3ccc335001ae997f0afa1f12f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 26 Mar 2018 00:11:42 +0200 Subject: [PATCH] feat: Refactored and improved usability of dsn/auth types --- src/auth.rs | 60 ++++++++++++++++++------------------ src/dsn.rs | 6 ++++ src/lib.rs | 1 + src/protocol/mod.rs | 3 +- src/protocol/v7.rs | 2 +- src/utils.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_auth.rs | 41 +++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/utils.rs create mode 100644 tests/test_auth.rs diff --git a/src/auth.rs b/src/auth.rs index 9866461a..f1d8c050 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,6 +1,12 @@ use std::fmt; use std::str::FromStr; +use chrono::{DateTime, Utc}; + +use protocol; +use utils::{datetime_to_timestamp, timestamp_to_datetime}; +use dsn::Dsn; + /// Represents an auth header parsing error. #[derive(Debug, Fail)] pub enum AuthParseError { @@ -22,9 +28,9 @@ pub enum AuthParseError { } /// Represents an auth header. -#[derive(Default, Debug)] +#[derive(Debug)] pub struct Auth { - timestamp: Option, + timestamp: Option>, client: Option, version: u16, key: String, @@ -33,7 +39,7 @@ pub struct Auth { impl Auth { /// Returns the unix timestamp the client defined - pub fn timestamp(&self) -> Option { + pub fn timestamp(&self) -> Option> { self.timestamp } @@ -57,8 +63,8 @@ impl Auth { self.secret.is_none() } - /// Returns the client's relay - pub fn client_relay(&self) -> Option<&str> { + /// Returns the client's agent + pub fn client_agent(&self) -> Option<&str> { self.client.as_ref().map(|x| x.as_str()) } } @@ -71,7 +77,7 @@ impl fmt::Display for Auth { self.key, self.version )?; if let Some(ts) = self.timestamp { - write!(f, ", sentry_timestamp={}", ts)?; + write!(f, ", sentry_timestamp={}", datetime_to_timestamp(&ts))?; } if let Some(ref client) = self.client { write!(f, ", sentry_client={}", client)?; @@ -87,7 +93,13 @@ impl FromStr for Auth { type Err = AuthParseError; fn from_str(s: &str) -> Result { - let mut rv = Auth::default(); + let mut rv = Auth { + timestamp: None, + client: None, + version: protocol::LATEST, + key: "".into(), + secret: None, + }; let mut base_iter = s.splitn(2, ' '); if !base_iter .next() @@ -101,7 +113,8 @@ impl FromStr for Auth { let mut kviter = item.trim().split('='); match (kviter.next(), kviter.next()) { (Some("sentry_timestamp"), Some(ts)) => { - rv.timestamp = Some(ts.parse().map_err(|_| AuthParseError::InvalidTimestamp)?); + let f: f64 = ts.parse().map_err(|_| AuthParseError::InvalidTimestamp)?; + rv.timestamp = Some(timestamp_to_datetime(f)); } (Some("sentry_client"), Some(client)) => { rv.client = Some(client.into()); @@ -130,27 +143,12 @@ impl FromStr for Auth { } } -#[test] -fn test_auth_parsing() { - let auth: Auth = "Sentry sentry_timestamp=1328055286.51, \ - sentry_client=raven-python/42, \ - sentry_version=6, \ - sentry_key=public, \ - sentry_secret=secret" - .parse() - .unwrap(); - assert_eq!(auth.timestamp(), Some(1328055286.51)); - assert_eq!(auth.client_relay(), Some("raven-python/42")); - assert_eq!(auth.version(), 6); - assert_eq!(auth.public_key(), "public"); - assert_eq!(auth.secret_key(), Some("secret")); - - assert_eq!( - auth.to_string(), - "Sentry sentry_key=public, \ - sentry_version=6, \ - sentry_timestamp=1328055286.51, \ - sentry_client=raven-python/42, \ - sentry_secret=secret" - ); +pub(crate) fn auth_from_dsn_and_client(dsn: &Dsn, client: Option<&str>) -> Auth { + Auth { + timestamp: Some(Utc::now()), + client: client.map(|x| x.to_string()), + version: protocol::LATEST, + key: dsn.public_key().to_string(), + secret: dsn.secret_key().map(|x| x.to_string()), + } } diff --git a/src/dsn.rs b/src/dsn.rs index d81c8e2d..abedc511 100644 --- a/src/dsn.rs +++ b/src/dsn.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use url::Url; use project_id::{ProjectId, ProjectIdParseError}; +use auth::{Auth, auth_from_dsn_and_client}; /// Represents a dsn url parsing error. #[derive(Debug, Fail)] @@ -71,6 +72,11 @@ pub struct Dsn { } impl Dsn { + /// Converts the dsn into an auth object. + pub fn to_auth(&self, client_agent: Option<&str>) -> Auth { + auth_from_dsn_and_client(self, client_agent) + } + /// Returns the scheme pub fn scheme(&self) -> Scheme { self.scheme diff --git a/src/lib.rs b/src/lib.rs index 573a7a1d..fc611f66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,7 @@ mod macros; mod auth; mod dsn; mod project_id; +mod utils; pub mod protocol; pub use auth::*; diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 7f253ccd..b52241e2 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -2,7 +2,8 @@ pub mod v7; -mod utils; +/// The latest version of the protocol. +pub const LATEST: u16 = 7; /// the always latest sentry protocol version pub mod latest { diff --git a/src/protocol/v7.rs b/src/protocol/v7.rs index 55e9c546..6dee114a 100644 --- a/src/protocol/v7.rs +++ b/src/protocol/v7.rs @@ -15,7 +15,7 @@ use serde::de::{Deserialize, Deserializer, Error as DeError}; use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer}; use serde_json::{from_value, to_value}; -use protocol::utils::ts_seconds_float; +use utils::ts_seconds_float; /// An arbitrary (JSON) value (`serde_json::value::Value`) pub mod value { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..4756c3b5 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,75 @@ +use chrono::{DateTime, Utc, TimeZone}; + +/// Converts a datetime object into a float timestamp. +pub fn datetime_to_timestamp(dt: &DateTime) -> f64 { + if dt.timestamp_subsec_nanos() == 0 { + dt.timestamp() as f64 + } else { + (dt.timestamp() as f64) + + ((dt.timestamp_subsec_micros() as f64) / 1_000_000f64) + } +} + +pub fn timestamp_to_datetime(ts: f64) -> DateTime { + let secs = ts as i64; + let micros = (ts.fract() * 1_000_000f64) as u32; + Utc.timestamp_opt(secs, micros * 1000).unwrap() +} + + +pub mod ts_seconds_float { + use std::fmt; + use serde::{ser, de}; + use chrono::{DateTime, Utc, TimeZone}; + + use super::timestamp_to_datetime; + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where D: de::Deserializer<'de> + { + Ok(d.deserialize_any(SecondsTimestampVisitor) + .map(|dt| dt.with_timezone(&Utc))?) + } + + pub fn serialize(dt: &DateTime, serializer: S) -> Result + where S: ser::Serializer + { + if dt.timestamp_subsec_nanos() == 0 { + serializer.serialize_i64(dt.timestamp()) + } else { + serializer.serialize_f64( + (dt.timestamp() as f64) + + ((dt.timestamp_subsec_micros() as f64) / 1_000_000f64) + ) + } + } + + struct SecondsTimestampVisitor; + + impl<'de> de::Visitor<'de> for SecondsTimestampVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + { + write!(formatter, "a unix timestamp") + } + + fn visit_f64(self, value: f64) -> Result, E> + where E: de::Error + { + Ok(timestamp_to_datetime(value)) + } + + fn visit_i64(self, value: i64) -> Result, E> + where E: de::Error + { + Ok(Utc.timestamp_opt(value, 0).unwrap()) + } + + fn visit_u64(self, value: u64) -> Result, E> + where E: de::Error + { + Ok(Utc.timestamp_opt(value as i64, 0).unwrap()) + } + } +} diff --git a/tests/test_auth.rs b/tests/test_auth.rs new file mode 100644 index 00000000..84e217b9 --- /dev/null +++ b/tests/test_auth.rs @@ -0,0 +1,41 @@ +extern crate chrono; +extern crate sentry_types; +use chrono::{TimeZone, Utc}; +use sentry_types::{Auth, Dsn, protocol}; + + +#[test] +fn test_auth_parsing() { + let auth: Auth = "Sentry sentry_timestamp=1328055286.5, \ + sentry_client=raven-python/42, \ + sentry_version=6, \ + sentry_key=public, \ + sentry_secret=secret" + .parse() + .unwrap(); + assert_eq!(auth.timestamp(), Some(Utc.ymd(2012, 2, 1).and_hms_milli(0, 14, 46, 500))); + assert_eq!(auth.client_agent(), Some("raven-python/42")); + assert_eq!(auth.version(), 6); + assert_eq!(auth.public_key(), "public"); + assert_eq!(auth.secret_key(), Some("secret")); + + assert_eq!( + auth.to_string(), + "Sentry sentry_key=public, \ + sentry_version=6, \ + sentry_timestamp=1328055286.5, \ + sentry_client=raven-python/42, \ + sentry_secret=secret" + ); +} + +#[test] +fn auth_to_dsn() { + let url = "https://username:password@domain:8888/23"; + let dsn = url.parse::().unwrap(); + let auth = dsn.to_auth(Some("sentry-rust/1.0")); + assert_eq!(auth.client_agent(), Some("sentry-rust/1.0")); + assert_eq!(auth.version(), protocol::LATEST); + assert_eq!(auth.public_key(), "username"); + assert_eq!(auth.secret_key(), Some("password")); +}