diff --git a/Cargo.lock b/Cargo.lock index 7300ec0..73502cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,6 +1134,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "serde_test", "sqlx", "thiserror", "tokio", @@ -1445,6 +1446,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_test" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f77be7305dac4f250891d2f7444276315f3c288176d35746b6a4ca786dacb3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index ba2ee36..7aaa32c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ log = "0.4" pretty_env_logger = "0.4" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" +serde_test = "1.0" sqlx = { version = "0.6", features = [ "chrono", "runtime-actix-rustls", "postgres" ] } thiserror = "1.0" tokio = "1.23" diff --git a/src/core/mod.rs b/src/core/mod.rs index a3a1c93..e9ee5e0 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -8,6 +8,7 @@ use std::{fmt, io}; use crate::util::validate; pub use log::{debug, error, info, trace, warn}; +use serde::ser::SerializeMap; pub use crate::util::validate::{Insane, Sane}; @@ -24,6 +25,7 @@ pub enum Error { MalformedHeader(header::ToStrError), BadToken(jsonwebtoken::errors::Error), BadCredentials, + BadBearcap, } pub fn utc_now() -> NaiveDateTime { @@ -33,9 +35,7 @@ pub fn utc_now() -> NaiveDateTime { pub fn unix_now() -> i64 { SystemTime::now() .duration_since(UNIX_EPOCH) - .expect( - "You've either broken spacetime, or your system clock is a bit off.", - ) + .expect("You've either broken spacetime, or your system clock is a bit off.") .as_secs() as i64 } @@ -47,6 +47,7 @@ impl ResponseError for Error { Error::MalformedHeader(_) => StatusCode::BAD_REQUEST, Error::BadToken(_) => StatusCode::UNAUTHORIZED, Error::BadCredentials => StatusCode::UNAUTHORIZED, + Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY, _ => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -66,6 +67,7 @@ impl fmt::Display for Error { Error::MalformedHeader(to_str_error) => to_str_error.fmt(f), Error::BadToken(jwt_error) => jwt_error.fmt(f), Error::BadCredentials => write!(f, "Invalid user name or password"), + Error::BadBearcap => write!(f, "Invalid bearcap URL"), } } } @@ -108,6 +110,8 @@ impl Serialize for Error { where S: Serializer, { - serializer.serialize_str(format!("{}", self).as_str()) + let mut fields = serializer.serialize_map(Some(1))?; + fields.serialize_entry("msg", format!("{}", self).as_str())?; + fields.end() } } diff --git a/src/util/bear.rs b/src/util/bear.rs new file mode 100644 index 0000000..5383d84 --- /dev/null +++ b/src/util/bear.rs @@ -0,0 +1,185 @@ +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; +use std::iter::Peekable; +use std::str::Chars; + +use crate::core::*; + +const MAX_PARAM_LEN: usize = 16384; + +/// Bearer Capabilities URI, see +#[derive(PartialEq, Debug)] +pub struct Bearcap { + pub url: String, + pub token: String, +} + +struct MaybeBearcap { + url: Option, + token: Option, +} + +impl TryFrom<&str> for Bearcap { + type Error = Error; + + fn try_from(v: &str) -> Result { + const BEAR_PREFIX: &'static str = "bear:"; + if !v.starts_with(BEAR_PREFIX) { + return Err(Error::BadBearcap); + } + let v = &v[BEAR_PREFIX.len()..]; + + let mut params = MaybeBearcap { + url: None, + token: None, + }; + let mut chars = v.chars().peekable(); + for _ in 0..2 { + match chars.next() { + Some('?' | '&') => parse_param(&mut params, &mut chars)?, + None => break, + _ => return Err(Error::BadBearcap), + } + } + + params.try_into() + } +} + +fn parse_param(params: &mut MaybeBearcap, chars: &mut Peekable) -> Result<()> { + match chars.next() { + Some('t') => params.token = Some(parse_param_val(chars)?), + Some('u') => params.url = Some(parse_param_val(chars)?), + _ => return Err(Error::BadBearcap), + } + + Ok(()) +} + +fn parse_param_val(chars: &mut Peekable) -> Result { + if chars.next() != Some('=') { + return Err(Error::BadBearcap); + } + let mut val = String::new(); + + for _ in 0..MAX_PARAM_LEN { + let c = chars.peek().map(|c| *c); + match c { + Some('&') | None => return Ok(val), + Some(c) => { + chars.next(); + val.push(c); + } + } + } + + warn!( + "Rejecting bearcap with param longer than {} chars", + MAX_PARAM_LEN + ); + Err(Error::BadBearcap) +} + +impl TryFrom for Bearcap { + type Error = Error; + + fn try_from(v: MaybeBearcap) -> Result { + if let (Some(token), Some(url)) = (v.token, v.url) { + if token.len() > 0 && url.len() > 0 { + return Ok(Bearcap { token, url }); + } + } + + Err(Error::BadBearcap) + } +} + +impl Serialize for Bearcap { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let s = format!("bear:?t={}&u={}", self.token, self.url); + serializer.serialize_str(s.as_str()) + } +} + +impl<'de> Deserialize<'de> for Bearcap { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(BearcapVisitor) + } +} + +struct BearcapVisitor; + +impl<'de> de::Visitor<'de> for BearcapVisitor { + type Value = Bearcap; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a valid bearcap URI") + } + + fn visit_str(self, value: &str) -> std::result::Result + where + E: de::Error, + { + match Bearcap::try_from(value) { + Ok(bc) => Ok(bc), + Err(_) => Err(E::custom("Expected a valid bearcap URI")), + } + } +} + +#[cfg(test)] +mod tests { + use crate::util::bear::Bearcap; + use serde_test::{assert_tokens, Token}; + + #[test] + fn parse_vali() { + let bc = Bearcap::try_from("bear:?t=asdf&u=https://example.com/test") + .expect("Doesn't parse valid bearcap"); + assert_eq!(bc.url, String::from("https://example.com/test")); + assert_eq!(bc.token, String::from("asdf")); + } + + #[test] + fn serialize() { + let bc = Bearcap { + token: String::from("asdf"), + url: String::from("https://example.com/test"), + }; + + assert_tokens( + &bc, + &[Token::Str("bear:?t=asdf&u=https://example.com/test")], + ); + } + + #[test] + fn reject_without_url() { + let result = Bearcap::try_from("bear:?t=asdf"); + assert!(result.is_err()); + } + + #[test] + fn reject_without_token() { + let result = Bearcap::try_from("bear:?u=https://example.com/test"); + assert!(result.is_err()); + } + + #[test] + fn reject_with_empty_token() { + let result = Bearcap::try_from("bear:?t=&u=https://example.com/test"); + assert!(result.is_err()); + } + + #[test] + fn reject_with_empty_url() { + let result = Bearcap::try_from("bear:?t=asdf&u="); + assert!(result.is_err()); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index a1c6188..2f8eef5 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod bear; pub mod password; pub mod token; pub mod validate;