add request signatures and GET/POST requests
This commit is contained in:
parent
0e71e9dc5f
commit
7a694623e5
24 changed files with 690 additions and 30 deletions
|
@ -6,8 +6,15 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
addr = "0.15.6"
|
||||
axum = "0.7.4"
|
||||
base64 = "0.21.7"
|
||||
chrono = "0.4.31"
|
||||
eyre = "0.6.11"
|
||||
once_cell = "1.19.0"
|
||||
rand = "0.8.5"
|
||||
reqwest = "0.11.23"
|
||||
rsa = { version = "0.9.6", features = ["sha2"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
sqlx = "0.7.3"
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone)]
|
||||
pub enum QueryError {
|
||||
pub enum FoxError {
|
||||
#[error("object not found")]
|
||||
NotFound,
|
||||
#[error("date for signature out of range")]
|
||||
SignatureDateOutOfRange(&'static str),
|
||||
#[error("non-200 response to federation request")]
|
||||
ResponseNotOk,
|
||||
#[error("server is invalid")]
|
||||
InvalidServer,
|
||||
}
|
||||
|
|
4
foxchat/src/fed/mod.rs
Normal file
4
foxchat/src/fed/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod request;
|
||||
pub mod signature;
|
||||
|
||||
pub use request::{get, post};
|
100
foxchat/src/fed/request.rs
Normal file
100
foxchat/src/fed/request.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use eyre::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Response, StatusCode};
|
||||
use rsa::RsaPrivateKey;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
use crate::{
|
||||
signature::{build_signature, format_date},
|
||||
FoxError,
|
||||
};
|
||||
|
||||
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub fn is_valid_domain(domain: &str) -> bool {
|
||||
addr::parse_domain_name(domain).is_ok()
|
||||
}
|
||||
|
||||
pub async fn get<R: DeserializeOwned>(
|
||||
private_key: &RsaPrivateKey,
|
||||
self_domain: String,
|
||||
host: String,
|
||||
path: String,
|
||||
user_id: Option<String>,
|
||||
) -> Result<R> {
|
||||
let (signature, date) = build_signature(
|
||||
private_key,
|
||||
host.clone(),
|
||||
path.clone(),
|
||||
None,
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
req = if let Some(id) = user_id {
|
||||
req.header("X-Foxchat-User", id)
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
||||
let resp = req.send().await?;
|
||||
handle_response(resp).await
|
||||
}
|
||||
|
||||
pub async fn post<T: Serialize, R: DeserializeOwned>(
|
||||
private_key: &RsaPrivateKey,
|
||||
self_domain: String,
|
||||
host: String,
|
||||
path: String,
|
||||
user_id: Option<String>,
|
||||
body: &T,
|
||||
) -> Result<R> {
|
||||
let body = serde_json::to_string(body)?;
|
||||
|
||||
let (signature, date) = build_signature(
|
||||
private_key,
|
||||
host.clone(),
|
||||
path.clone(),
|
||||
Some(body.len()),
|
||||
user_id.clone(),
|
||||
);
|
||||
|
||||
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())
|
||||
.body(body);
|
||||
|
||||
req = if let Some(id) = user_id {
|
||||
req.header("X-Foxchat-User", id)
|
||||
} else {
|
||||
req
|
||||
};
|
||||
|
||||
let resp = req.send().await?;
|
||||
handle_response(resp).await
|
||||
}
|
||||
|
||||
async fn handle_response<R: DeserializeOwned>(resp: Response) -> Result<R> {
|
||||
if resp.status() != StatusCode::OK {
|
||||
return Err(FoxError::ResponseNotOk.into());
|
||||
}
|
||||
|
||||
let parsed = resp.json::<R>().await?;
|
||||
Ok(parsed)
|
||||
}
|
78
foxchat/src/fed/signature.rs
Normal file
78
foxchat/src/fed/signature.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use base64::prelude::{Engine, BASE64_URL_SAFE};
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use eyre::Result;
|
||||
use rsa::pkcs1v15::{SigningKey, VerifyingKey, Signature};
|
||||
use rsa::sha2::Sha256;
|
||||
use rsa::signature::{RandomizedSigner, SignatureEncoding, Verifier};
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
|
||||
use crate::FoxError;
|
||||
|
||||
pub fn build_signature(
|
||||
private_key: &RsaPrivateKey,
|
||||
host: String,
|
||||
request_path: String,
|
||||
content_length: Option<usize>,
|
||||
user_id: Option<String>,
|
||||
) -> (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 signature = signing_key.sign_with_rng(&mut rng, plaintext.as_bytes());
|
||||
|
||||
let str = BASE64_URL_SAFE.encode(signature.to_bytes());
|
||||
|
||||
(str, time)
|
||||
}
|
||||
|
||||
fn plaintext_string(
|
||||
time: DateTime<Utc>,
|
||||
host: String,
|
||||
request_path: String,
|
||||
content_length: Option<usize>,
|
||||
user_id: Option<String>,
|
||||
) -> 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());
|
||||
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}",
|
||||
raw_time, host, request_path, raw_content_length, raw_user_id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn format_date(time: DateTime<Utc>) -> String {
|
||||
time.format("%a, %d %b %Y %T GMT").to_string()
|
||||
}
|
||||
|
||||
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
|
||||
content_length: Option<usize>, // from Content-Length header
|
||||
user_id: Option<String>, // from X-Foxchat-User header
|
||||
) -> Result<bool> {
|
||||
let verifying_key = VerifyingKey::<Sha256>::new(public_key.clone());
|
||||
|
||||
let now = Utc::now();
|
||||
if (now - Duration::minutes(1)) < time {
|
||||
return Err(FoxError::SignatureDateOutOfRange("request was made too long ago").into());
|
||||
} else if (now + Duration::minutes(1)) > time {
|
||||
return Err(FoxError::SignatureDateOutOfRange("request was made in the future").into());
|
||||
}
|
||||
|
||||
let plaintext = plaintext_string(time, host, request_path, content_length, user_id);
|
||||
|
||||
let hash = &BASE64_URL_SAFE.decode(encoded_signature)?;
|
||||
let signature = Signature::try_from(hash.as_ref())?;
|
||||
|
||||
verifying_key.verify(plaintext.as_bytes(), &signature)?;
|
||||
Ok(true)
|
||||
}
|
|
@ -8,7 +8,7 @@ use serde::Serialize;
|
|||
use serde_json::json;
|
||||
use tracing::error;
|
||||
|
||||
use crate::QueryError;
|
||||
use crate::FoxError;
|
||||
|
||||
pub struct ApiError {
|
||||
status: StatusCode,
|
||||
|
@ -35,6 +35,7 @@ impl IntoResponse for ApiError {
|
|||
pub enum ErrorCode {
|
||||
InternalServerError,
|
||||
ObjectNotFound,
|
||||
InvalidServer,
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for ApiError {
|
||||
|
@ -53,7 +54,7 @@ impl From<sqlx::Error> for ApiError {
|
|||
|
||||
impl From<Report> for ApiError {
|
||||
fn from(err: Report) -> Self {
|
||||
match err.downcast_ref::<QueryError>() {
|
||||
match err.downcast_ref::<FoxError>() {
|
||||
Some(e) => return (*e).into(),
|
||||
None => {}
|
||||
};
|
||||
|
@ -71,14 +72,24 @@ impl From<Report> for ApiError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<QueryError> for ApiError {
|
||||
fn from(err: QueryError) -> Self {
|
||||
impl From<FoxError> for ApiError {
|
||||
fn from(err: FoxError) -> Self {
|
||||
match err {
|
||||
QueryError::NotFound => ApiError {
|
||||
FoxError::NotFound => ApiError {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
code: ErrorCode::ObjectNotFound,
|
||||
message: "Object not found".into(),
|
||||
},
|
||||
FoxError::InvalidServer => ApiError {
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
code: ErrorCode::InvalidServer,
|
||||
message: "Invalid domain or server".into(),
|
||||
},
|
||||
_ => ApiError {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: ErrorCode::InternalServerError,
|
||||
message: "Internal server error".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
pub mod error;
|
||||
pub mod http;
|
||||
pub mod s2s;
|
||||
pub mod fed;
|
||||
|
||||
pub use error::QueryError;
|
||||
pub use error::FoxError;
|
||||
pub use fed::signature;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue