add request verification extractor
This commit is contained in:
parent
7a694623e5
commit
1e53661b0a
18 changed files with 482 additions and 32 deletions
|
@ -1,3 +1,4 @@
|
|||
use axum::http::header::ToStrError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone)]
|
||||
|
@ -10,4 +11,24 @@ pub enum FoxError {
|
|||
ResponseNotOk,
|
||||
#[error("server is invalid")]
|
||||
InvalidServer,
|
||||
#[error("invalid header")]
|
||||
InvalidHeader,
|
||||
#[error("invalid date format")]
|
||||
InvalidDate,
|
||||
#[error("missing signature")]
|
||||
MissingSignature,
|
||||
#[error("invalid signature")]
|
||||
InvalidSignature,
|
||||
}
|
||||
|
||||
impl From<ToStrError> for FoxError {
|
||||
fn from(_: ToStrError) -> Self {
|
||||
Self::InvalidHeader
|
||||
}
|
||||
}
|
||||
|
||||
impl From<chrono::ParseError> for FoxError {
|
||||
fn from(_: chrono::ParseError) -> Self {
|
||||
Self::InvalidDate
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,3 +2,7 @@ pub mod request;
|
|||
pub mod signature;
|
||||
|
||||
pub use request::{get, post};
|
||||
|
||||
pub const SERVER_HEADER: &'static str = "X-Foxchat-Server";
|
||||
pub const SIGNATURE_HEADER: &'static str = "X-Foxchat-Signature";
|
||||
pub const USER_HEADER: &'static str = "X-Foxchat-User";
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use eyre::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Response, StatusCode};
|
||||
use reqwest::{Response, StatusCode, header::{CONTENT_TYPE, CONTENT_LENGTH, DATE}};
|
||||
use rsa::RsaPrivateKey;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
signature::{build_signature, format_date},
|
||||
FoxError,
|
||||
};
|
||||
|
||||
use super::{SIGNATURE_HEADER, USER_HEADER, SERVER_HEADER};
|
||||
|
||||
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
|
@ -27,7 +30,7 @@ pub async fn get<R: DeserializeOwned>(
|
|||
self_domain: String,
|
||||
host: String,
|
||||
path: String,
|
||||
user_id: Option<String>,
|
||||
user_id: Option<&str>,
|
||||
) -> Result<R> {
|
||||
let (signature, date) = build_signature(
|
||||
private_key,
|
||||
|
@ -39,12 +42,12 @@ pub async fn get<R: DeserializeOwned>(
|
|||
|
||||
let mut req = CLIENT
|
||||
.get(format!("https://{host}{path}"))
|
||||
.header("Date", format_date(date))
|
||||
.header("X-Foxchat-Signature", signature)
|
||||
.header("X-Foxchat-Server", self_domain);
|
||||
.header(DATE, format_date(date))
|
||||
.header(SIGNATURE_HEADER, signature)
|
||||
.header(SERVER_HEADER, self_domain);
|
||||
|
||||
req = if let Some(id) = user_id {
|
||||
req.header("X-Foxchat-User", id)
|
||||
req.header(USER_HEADER, id)
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
@ -58,7 +61,7 @@ pub async fn post<T: Serialize, R: DeserializeOwned>(
|
|||
self_domain: String,
|
||||
host: String,
|
||||
path: String,
|
||||
user_id: Option<String>,
|
||||
user_id: Option<&str>,
|
||||
body: &T,
|
||||
) -> Result<R> {
|
||||
let body = serde_json::to_string(body)?;
|
||||
|
@ -73,15 +76,15 @@ pub async fn post<T: Serialize, R: DeserializeOwned>(
|
|||
|
||||
let mut req = CLIENT
|
||||
.post(format!("https://{host}{path}"))
|
||||
.header("Date", format_date(date))
|
||||
.header("X-Foxchat-Signature", signature)
|
||||
.header("X-Foxchat-Server", self_domain)
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Content-Length", body.len().to_string())
|
||||
.header(DATE, format_date(date))
|
||||
.header(SIGNATURE_HEADER, signature)
|
||||
.header(SERVER_HEADER, self_domain)
|
||||
.header(CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.header(CONTENT_LENGTH, body.len().to_string())
|
||||
.body(body);
|
||||
|
||||
req = if let Some(id) = user_id {
|
||||
req.header("X-Foxchat-User", id)
|
||||
req.header(USER_HEADER, id)
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
@ -92,6 +95,7 @@ pub async fn post<T: Serialize, R: DeserializeOwned>(
|
|||
|
||||
async fn handle_response<R: DeserializeOwned>(resp: Response) -> Result<R> {
|
||||
if resp.status() != StatusCode::OK {
|
||||
error!("federation request failed with status code {}", resp.status());
|
||||
return Err(FoxError::ResponseNotOk.into());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use base64::prelude::{Engine, BASE64_URL_SAFE};
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use chrono::{DateTime, Utc, Duration, NaiveDateTime};
|
||||
use eyre::Result;
|
||||
use rsa::pkcs1v15::{SigningKey, VerifyingKey, Signature};
|
||||
use rsa::sha2::Sha256;
|
||||
|
@ -13,13 +13,13 @@ pub fn build_signature(
|
|||
host: String,
|
||||
request_path: String,
|
||||
content_length: Option<usize>,
|
||||
user_id: Option<String>,
|
||||
user_id: Option<&str>,
|
||||
) -> (String, DateTime<Utc>) {
|
||||
let mut rng = rand::thread_rng();
|
||||
let signing_key = SigningKey::<Sha256>::new(private_key.clone());
|
||||
let time = Utc::now();
|
||||
|
||||
let plaintext = plaintext_string(time, host, request_path, content_length, user_id);
|
||||
let plaintext = plaintext_string(time, &host, &request_path, content_length, user_id);
|
||||
let signature = signing_key.sign_with_rng(&mut rng, plaintext.as_bytes());
|
||||
|
||||
let str = BASE64_URL_SAFE.encode(signature.to_bytes());
|
||||
|
@ -29,16 +29,16 @@ pub fn build_signature(
|
|||
|
||||
fn plaintext_string(
|
||||
time: DateTime<Utc>,
|
||||
host: String,
|
||||
request_path: String,
|
||||
host: &str,
|
||||
request_path: &str,
|
||||
content_length: Option<usize>,
|
||||
user_id: Option<String>,
|
||||
user_id: Option<&str>,
|
||||
) -> String {
|
||||
let raw_time = format_date(time);
|
||||
let raw_content_length = content_length
|
||||
.map(|i| i.to_string())
|
||||
.unwrap_or("".to_owned());
|
||||
let raw_user_id = user_id.unwrap_or("".to_owned());
|
||||
let raw_user_id = user_id.unwrap_or("");
|
||||
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}",
|
||||
|
@ -50,14 +50,18 @@ pub fn format_date(time: DateTime<Utc>) -> String {
|
|||
time.format("%a, %d %b %Y %T GMT").to_string()
|
||||
}
|
||||
|
||||
pub fn parse_date(input: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
|
||||
Ok(NaiveDateTime::parse_from_str(input, "%a, %d %b %Y %T GMT")?.and_utc())
|
||||
}
|
||||
|
||||
pub fn verify_signature(
|
||||
public_key: &RsaPublicKey,
|
||||
encoded_signature: String,
|
||||
time: DateTime<Utc>, // from Date header
|
||||
host: String, // from Host header, verify that it's actually your host
|
||||
request_path: String, // from router
|
||||
host: &str, // from Host header, verify that it's actually your host
|
||||
request_path: &str, // from router
|
||||
content_length: Option<usize>, // from Content-Length header
|
||||
user_id: Option<String>, // from X-Foxchat-User header
|
||||
user_id: Option<&str>, // from X-Foxchat-User header
|
||||
) -> Result<bool> {
|
||||
let verifying_key = VerifyingKey::<Sha256>::new(public_key.clone());
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::num::ParseIntError;
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
http::{StatusCode, header::ToStrError},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
@ -36,6 +38,10 @@ pub enum ErrorCode {
|
|||
InternalServerError,
|
||||
ObjectNotFound,
|
||||
InvalidServer,
|
||||
InvalidHeader,
|
||||
InvalidDate,
|
||||
InvalidSignature,
|
||||
MissingSignature,
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for ApiError {
|
||||
|
@ -85,6 +91,16 @@ impl From<FoxError> for ApiError {
|
|||
code: ErrorCode::InvalidServer,
|
||||
message: "Invalid domain or server".into(),
|
||||
},
|
||||
FoxError::MissingSignature => ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::MissingSignature,
|
||||
message: "Missing signature".into(),
|
||||
},
|
||||
FoxError::InvalidSignature => ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::InvalidSignature,
|
||||
message: "Invalid signature".into(),
|
||||
},
|
||||
_ => ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: ErrorCode::InternalServerError,
|
||||
|
@ -93,3 +109,33 @@ impl From<FoxError> for ApiError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToStrError> for ApiError {
|
||||
fn from(_: ToStrError) -> Self {
|
||||
ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::InvalidHeader,
|
||||
message: "Invalid header value".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<chrono::ParseError> for ApiError {
|
||||
fn from(_: chrono::ParseError) -> Self {
|
||||
ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::InvalidDate,
|
||||
message: "Invalid date header value".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for ApiError {
|
||||
fn from(_: ParseIntError) -> Self {
|
||||
ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::InvalidHeader,
|
||||
message: "Invalid content length value".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue