add bearcap utility
This commit is contained in:
parent
4e9a09f74b
commit
64e670480a
5 changed files with 205 additions and 4 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -1134,6 +1134,7 @@ dependencies = [
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_test",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -1445,6 +1446,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_test"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8f77be7305dac4f250891d2f7444276315f3c288176d35746b6a4ca786dacb3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
|
@ -16,6 +16,7 @@ log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_test = "1.0"
|
||||||
sqlx = { version = "0.6", features = [ "chrono", "runtime-actix-rustls", "postgres" ] }
|
sqlx = { version = "0.6", features = [ "chrono", "runtime-actix-rustls", "postgres" ] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = "1.23"
|
tokio = "1.23"
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::{fmt, io};
|
||||||
|
|
||||||
use crate::util::validate;
|
use crate::util::validate;
|
||||||
pub use log::{debug, error, info, trace, warn};
|
pub use log::{debug, error, info, trace, warn};
|
||||||
|
use serde::ser::SerializeMap;
|
||||||
|
|
||||||
pub use crate::util::validate::{Insane, Sane};
|
pub use crate::util::validate::{Insane, Sane};
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ pub enum Error {
|
||||||
MalformedHeader(header::ToStrError),
|
MalformedHeader(header::ToStrError),
|
||||||
BadToken(jsonwebtoken::errors::Error),
|
BadToken(jsonwebtoken::errors::Error),
|
||||||
BadCredentials,
|
BadCredentials,
|
||||||
|
BadBearcap,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn utc_now() -> NaiveDateTime {
|
pub fn utc_now() -> NaiveDateTime {
|
||||||
|
@ -33,9 +35,7 @@ pub fn utc_now() -> NaiveDateTime {
|
||||||
pub fn unix_now() -> i64 {
|
pub fn unix_now() -> i64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect(
|
.expect("You've either broken spacetime, or your system clock is a bit off.")
|
||||||
"You've either broken spacetime, or your system clock is a bit off.",
|
|
||||||
)
|
|
||||||
.as_secs() as i64
|
.as_secs() as i64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ impl ResponseError for Error {
|
||||||
Error::MalformedHeader(_) => StatusCode::BAD_REQUEST,
|
Error::MalformedHeader(_) => StatusCode::BAD_REQUEST,
|
||||||
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
|
Error::BadToken(_) => StatusCode::UNAUTHORIZED,
|
||||||
Error::BadCredentials => StatusCode::UNAUTHORIZED,
|
Error::BadCredentials => StatusCode::UNAUTHORIZED,
|
||||||
|
Error::BadBearcap => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,6 +67,7 @@ impl fmt::Display for Error {
|
||||||
Error::MalformedHeader(to_str_error) => to_str_error.fmt(f),
|
Error::MalformedHeader(to_str_error) => to_str_error.fmt(f),
|
||||||
Error::BadToken(jwt_error) => jwt_error.fmt(f),
|
Error::BadToken(jwt_error) => jwt_error.fmt(f),
|
||||||
Error::BadCredentials => write!(f, "Invalid user name or password"),
|
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
|
where
|
||||||
S: Serializer,
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
185
src/util/bear.rs
Normal file
185
src/util/bear.rs
Normal file
|
@ -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 <https://docs.joinmastodon.org/spec/bearcaps/>
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct Bearcap {
|
||||||
|
pub url: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MaybeBearcap {
|
||||||
|
url: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Bearcap {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(v: &str) -> Result<Self> {
|
||||||
|
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<Chars>) -> 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<Chars>) -> Result<String> {
|
||||||
|
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<MaybeBearcap> for Bearcap {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(v: MaybeBearcap) -> Result<Self> {
|
||||||
|
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
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<E>(self, value: &str) -> std::result::Result<Self::Value, E>
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod bear;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
|
|
Loading…
Reference in a new issue