Add ruma-server-util crate with X-Matrix auth header parsing
This commit is contained in:
parent
6b04b6c567
commit
8b21519d25
5
crates/ruma-server-util/CHANGELOG.md
Normal file
5
crates/ruma-server-util/CHANGELOG.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# [unreleased]
|
||||||
|
|
||||||
|
Improvements:
|
||||||
|
|
||||||
|
* Provide `XMatrix` type for Matrix federation authorization headers.
|
25
crates/ruma-server-util/Cargo.toml
Normal file
25
crates/ruma-server-util/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
categories = ["api-bindings", "web-programming"]
|
||||||
|
description = "Utilities for implementing Matrix server applications."
|
||||||
|
homepage = "https://www.ruma.io/"
|
||||||
|
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||||
|
license = "MIT"
|
||||||
|
name = "ruma-server-util"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/ruma/ruma"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.60"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
headers = "0.3"
|
||||||
|
ruma-common = { version = "0.9.2", path = "../ruma-common" }
|
||||||
|
tracing = "0.1.25"
|
||||||
|
yap = "0.7.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tracing-subscriber = "0.3.3"
|
7
crates/ruma-server-util/README.md
Normal file
7
crates/ruma-server-util/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ruma-server-util
|
||||||
|
|
||||||
|
[](https://crates.io/crates/ruma-server-util)
|
||||||
|
[](https://docs.rs/ruma-server-util/)
|
||||||
|

|
||||||
|
|
||||||
|
**ruma-server-util** contains types and trait implementations useful for implementing a Matrix homeserver.
|
284
crates/ruma-server-util/src/authorization.rs
Normal file
284
crates/ruma-server-util/src/authorization.rs
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
//! Common types for implementing federation authorization.
|
||||||
|
|
||||||
|
use headers::{authorization::Credentials, HeaderValue};
|
||||||
|
|
||||||
|
use ruma_common::{OwnedServerName, OwnedServerSigningKeyId};
|
||||||
|
use tracing::debug;
|
||||||
|
use yap::{IntoTokens, TokenLocation, Tokens};
|
||||||
|
|
||||||
|
/// Typed representation of an `Authorization` header of scheme `X-Matrix`, as defined in the
|
||||||
|
/// [Matrix Server-Server API][spec]. Includes an implementation of
|
||||||
|
/// [`headers::authorization::Credentials`] for automatically handling the encoding and decoding
|
||||||
|
/// when using a web framework that supports typed headers.
|
||||||
|
///
|
||||||
|
/// [spec]: https://spec.matrix.org/v1.3/server-server-api/#request-authentication
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct XMatrix {
|
||||||
|
/// The server name of the sending server.
|
||||||
|
pub origin: OwnedServerName,
|
||||||
|
/// The server name of the receiving sender. For compatibility with older servers, recipients
|
||||||
|
/// should accept requests without this parameter, but MUST always send it. If this property is
|
||||||
|
/// included, but the value does not match the receiving server's name, the receiving server
|
||||||
|
/// must deny the request with an HTTP status code 401 Unauthorized.
|
||||||
|
pub destination: Option<OwnedServerName>,
|
||||||
|
/// The ID - including the algorithm name - of the sending server's key that was used to sign
|
||||||
|
/// the request.
|
||||||
|
pub key: OwnedServerSigningKeyId,
|
||||||
|
/// The signature of the JSON.
|
||||||
|
pub sig: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XMatrix {
|
||||||
|
/// Construct a new X-Matrix Authorization header.
|
||||||
|
pub fn new(
|
||||||
|
origin: OwnedServerName,
|
||||||
|
destination: Option<OwnedServerName>,
|
||||||
|
key: OwnedServerSigningKeyId,
|
||||||
|
sig: String,
|
||||||
|
) -> Self {
|
||||||
|
Self { origin, destination, key, sig }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_token<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||||
|
tokens.optional(|t| {
|
||||||
|
let token: Vec<u8> = t.tokens_while(|c| is_tchar(**c)).copied().collect();
|
||||||
|
if !token.is_empty() {
|
||||||
|
Some(token)
|
||||||
|
} else {
|
||||||
|
debug!("Returning early because of empty token at {}", t.location().offset());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_token_with_colons<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||||
|
tokens.optional(|t| {
|
||||||
|
let token: Vec<u8> = t.tokens_while(|c| is_tchar(**c) || **c == b':').copied().collect();
|
||||||
|
if !token.is_empty() {
|
||||||
|
Some(token)
|
||||||
|
} else {
|
||||||
|
debug!("Returning early because of empty token at {}", t.location().offset());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_quoted<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<Vec<u8>> {
|
||||||
|
tokens.optional(|t| {
|
||||||
|
if !(t.token(&b'"')) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
match t.next()? {
|
||||||
|
// quoted pair
|
||||||
|
b'\\' => {
|
||||||
|
let escaped = t.next().filter(|c| {
|
||||||
|
if is_quoted_pair(**c) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"Encountered an illegal character {} at location {}",
|
||||||
|
**c as char,
|
||||||
|
t.location().offset()
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
buffer.push(*escaped)
|
||||||
|
}
|
||||||
|
// end of quote
|
||||||
|
b'"' => break,
|
||||||
|
// regular character
|
||||||
|
c if is_qdtext(*c) => buffer.push(*c),
|
||||||
|
// Invalid character
|
||||||
|
c => {
|
||||||
|
debug!(
|
||||||
|
"Encountered an illegal character {} at location {}",
|
||||||
|
*c as char,
|
||||||
|
t.location().offset()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(buffer)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_xmatrix_field<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<(String, Vec<u8>)> {
|
||||||
|
tokens.optional(|t| {
|
||||||
|
let name = parse_token(t).and_then(|name| {
|
||||||
|
let name = std::str::from_utf8(&name).ok()?.to_ascii_lowercase();
|
||||||
|
match name.as_str() {
|
||||||
|
"origin" | "destination" | "key" | "sig" => Some(name),
|
||||||
|
name => {
|
||||||
|
debug!(
|
||||||
|
"Encountered an invalid field name {} at location {}",
|
||||||
|
name,
|
||||||
|
t.location().offset()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !t.token(&b'=') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = parse_quoted(t).or_else(|| parse_token_with_colons(t))?;
|
||||||
|
|
||||||
|
Some((name, value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_xmatrix<'a>(tokens: &mut impl Tokens<Item = &'a u8>) -> Option<XMatrix> {
|
||||||
|
tokens.optional(|t| {
|
||||||
|
if !t.tokens(b"X-Matrix ".into_tokens()) {
|
||||||
|
debug!("Failed to parse X-Matrix credentials, didn't start with 'X-Matrix '");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut origin = None;
|
||||||
|
let mut destination = None;
|
||||||
|
let mut key = None;
|
||||||
|
let mut sig = None;
|
||||||
|
|
||||||
|
for (name, value) in t.sep_by(|t| parse_xmatrix_field(t), |t| t.token(&b',')) {
|
||||||
|
match name.as_str() {
|
||||||
|
"origin" => {
|
||||||
|
if origin.is_some() {
|
||||||
|
debug!("Field origin duplicated in X-Matrix Authorization header");
|
||||||
|
}
|
||||||
|
origin = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||||
|
}
|
||||||
|
"destination" => {
|
||||||
|
if destination.is_some() {
|
||||||
|
debug!("Field destination duplicated in X-Matrix Authorization header");
|
||||||
|
}
|
||||||
|
destination = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||||
|
}
|
||||||
|
"key" => {
|
||||||
|
if key.is_some() {
|
||||||
|
debug!("Field key duplicated in X-Matrix Authorization header");
|
||||||
|
}
|
||||||
|
key = Some(std::str::from_utf8(&value).ok()?.try_into().ok()?);
|
||||||
|
}
|
||||||
|
"sig" => {
|
||||||
|
if sig.is_some() {
|
||||||
|
debug!("Field sig duplicated in X-Matrix Authorization header");
|
||||||
|
}
|
||||||
|
sig = Some(std::str::from_utf8(&value).ok()?.to_owned());
|
||||||
|
}
|
||||||
|
name => {
|
||||||
|
debug!("Unknown field {} found in X-Matrix Authorization header", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(XMatrix { origin: origin?, destination, key: key?, sig: sig? })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_alpha(c: u8) -> bool {
|
||||||
|
(0x41..=0x5A).contains(&c) || (0x61..=0x7A).contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_digit(c: u8) -> bool {
|
||||||
|
(0x30..=0x39).contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_tchar(c: u8) -> bool {
|
||||||
|
const TOKEN_CHARS: [u8; 15] =
|
||||||
|
[b'!', b'#', b'$', b'%', b'&', b'\'', b'*', b'+', b'-', b'.', b'^', b'_', b'`', b'|', b'~'];
|
||||||
|
is_alpha(c) || is_digit(c) || TOKEN_CHARS.contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_qdtext(c: u8) -> bool {
|
||||||
|
c == b'\t'
|
||||||
|
|| c == b' '
|
||||||
|
|| c == 0x21
|
||||||
|
|| (0x23..=0x5B).contains(&c)
|
||||||
|
|| (0x5D..=0x7E).contains(&c)
|
||||||
|
|| is_obs_text(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_obs_text(c: u8) -> bool {
|
||||||
|
c >= 0x80 // The spec does contain an upper limit of 0xFF here, but that's enforced by the type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_vchar(c: u8) -> bool {
|
||||||
|
(0x21..=0x7E).contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_quoted_pair(c: u8) -> bool {
|
||||||
|
c == b'\t' || c == b' ' || is_vchar(c) || is_obs_text(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Credentials for XMatrix {
|
||||||
|
const SCHEME: &'static str = "X-Matrix";
|
||||||
|
|
||||||
|
fn decode(value: &HeaderValue) -> Option<Self> {
|
||||||
|
let value: Vec<u8> = value.as_bytes().to_vec();
|
||||||
|
parse_xmatrix(&mut value.into_tokens())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self) -> HeaderValue {
|
||||||
|
if let Some(destination) = &self.destination {
|
||||||
|
format!(
|
||||||
|
"X-Matrix origin=\"{}\",destination=\"{}\",key=\"{}\",sig=\"{}\"",
|
||||||
|
self.origin, destination, self.key, self.sig
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("X-Matrix origin=\"{}\",key=\"{}\",sig=\"{}\"", self.origin, self.key, self.sig)
|
||||||
|
}
|
||||||
|
.try_into()
|
||||||
|
.expect("header format is static")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use headers::{authorization::Credentials, HeaderValue};
|
||||||
|
use ruma_common::OwnedServerName;
|
||||||
|
|
||||||
|
use super::XMatrix;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xmatrix_auth_pre_1_3() {
|
||||||
|
let header = HeaderValue::from_static(
|
||||||
|
"X-Matrix origin=\"origin.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\"",
|
||||||
|
);
|
||||||
|
let origin = "origin.hs.example.com".try_into().unwrap();
|
||||||
|
let key = "ed25519:key1".try_into().unwrap();
|
||||||
|
let sig = "ABCDEF...".to_string();
|
||||||
|
let credentials: XMatrix = Credentials::decode(&header).unwrap();
|
||||||
|
assert_eq!(credentials.origin, origin);
|
||||||
|
assert_eq!(credentials.destination, None);
|
||||||
|
assert_eq!(credentials.key, key);
|
||||||
|
assert_eq!(credentials.sig, sig);
|
||||||
|
|
||||||
|
let credentials = XMatrix::new(origin, None, key, sig);
|
||||||
|
|
||||||
|
assert_eq!(credentials.encode(), header);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xmatrix_auth_1_3() {
|
||||||
|
let header = HeaderValue::from_static("X-Matrix origin=\"origin.hs.example.com\",destination=\"destination.hs.example.com\",key=\"ed25519:key1\",sig=\"ABCDEF...\"");
|
||||||
|
let origin: OwnedServerName = "origin.hs.example.com".try_into().unwrap();
|
||||||
|
let destination: OwnedServerName = "destination.hs.example.com".try_into().unwrap();
|
||||||
|
let key = "ed25519:key1".try_into().unwrap();
|
||||||
|
let sig = "ABCDEF...".to_string();
|
||||||
|
let credentials: XMatrix = Credentials::decode(&header).unwrap();
|
||||||
|
assert_eq!(credentials.origin, origin);
|
||||||
|
assert_eq!(credentials.destination, Some(destination.clone()));
|
||||||
|
assert_eq!(credentials.key, key);
|
||||||
|
assert_eq!(credentials.sig, sig);
|
||||||
|
|
||||||
|
let credentials = XMatrix::new(origin, Some(destination), key, sig);
|
||||||
|
|
||||||
|
assert_eq!(credentials.encode(), header);
|
||||||
|
}
|
||||||
|
}
|
6
crates/ruma-server-util/src/lib.rs
Normal file
6
crates/ruma-server-util/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#![doc(html_favicon_url = "https://www.ruma.io/favicon.ico")]
|
||||||
|
#![doc(html_logo_url = "https://www.ruma.io/images/logo.png")]
|
||||||
|
//! Collection of helpers for implementing Matrix homeservers using Ruma.
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
pub mod authorization;
|
Loading…
x
Reference in New Issue
Block a user