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;