add request proxying and basic user auth
This commit is contained in:
parent
bfb0a1d1b0
commit
274c527ade
31 changed files with 541 additions and 36 deletions
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
foxchat = { path = "../foxchat" }
|
||||
axum = { version = "0.7.4", features = ["macros", "query", "tracing", "ws"] }
|
||||
axum = { version = "0.7.4", features = ["macros", "query", "tracing", "ws", "original-uri"] }
|
||||
clap = { version = "4.4.16", features = ["env", "derive"] }
|
||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "tls-rustls", "postgres", "migrate", "chrono", "json"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::config::Config;
|
||||
|
@ -5,4 +6,6 @@ use crate::config::Config;
|
|||
pub struct AppState {
|
||||
pub pool: Pool<Postgres>,
|
||||
pub config: Config,
|
||||
pub private_key: RsaPrivateKey,
|
||||
pub public_key: RsaPublicKey,
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ pub async fn check_token(pool: &Pool<Postgres>, token: String) -> Result<Account
|
|||
a.id, a.username, a.email, a.password, a.role as "role: Role", a.avatar
|
||||
from accounts a
|
||||
join tokens t on t.account_id = a.id
|
||||
where t.account_id = $1"#,
|
||||
where t.token = $1"#,
|
||||
hash
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
|
|
136
identity/src/fed/mod.rs
Normal file
136
identity/src/fed/mod.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
mod proxy_header;
|
||||
|
||||
pub use proxy_header::ProxyServerHeader;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::FromRequestParts,
|
||||
http::{
|
||||
header::{CONTENT_LENGTH, DATE, HOST},
|
||||
request::Parts,
|
||||
},
|
||||
Extension,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use foxchat::{
|
||||
fed::{SERVER_HEADER, SIGNATURE_HEADER},
|
||||
http::ApiError,
|
||||
signature::{parse_date, verify_signature},
|
||||
FoxError,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{app_state::AppState, model::chat_instance::ChatInstance};
|
||||
|
||||
/// A parsed and validated federation signature.
|
||||
pub struct FoxRequestData {
|
||||
pub instance: ChatInstance,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for FoxRequestData
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let signature = FoxSignatureData::from_request_parts(parts, state).await?;
|
||||
let state: Extension<Arc<AppState>> = Extension::from_request_parts(parts, state)
|
||||
.await
|
||||
.expect("AppState was not added as an extension");
|
||||
|
||||
let instance = ChatInstance::get(state.0.clone(), &signature.domain).await?;
|
||||
let public_key = instance.parse_public_key()?;
|
||||
if state.config.domain.clone() != signature.host {
|
||||
return Err(FoxError::InvalidHeader.into());
|
||||
}
|
||||
|
||||
if let Err(e) = verify_signature(
|
||||
&public_key,
|
||||
signature.signature,
|
||||
signature.date,
|
||||
&signature.host,
|
||||
&signature.request_path,
|
||||
signature.content_length,
|
||||
None,
|
||||
) {
|
||||
error!(
|
||||
"Verifying signature from request for {} from {}: {}",
|
||||
parts.uri.path(),
|
||||
signature.domain,
|
||||
e
|
||||
);
|
||||
|
||||
return Err(FoxError::InvalidSignature.into());
|
||||
}
|
||||
|
||||
Ok(FoxRequestData {
|
||||
instance,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An unvalidated federation signature. Use with caution, you should almost always use `FoxRequestData` instead.
|
||||
pub struct FoxSignatureData {
|
||||
pub domain: String,
|
||||
pub signature: String,
|
||||
pub date: DateTime<Utc>,
|
||||
pub host: String,
|
||||
pub request_path: String,
|
||||
pub content_length: Option<usize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for FoxSignatureData
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let domain = parts
|
||||
.headers
|
||||
.get(SERVER_HEADER)
|
||||
.ok_or(FoxError::InvalidHeader)?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
|
||||
let date = parse_date(
|
||||
parts
|
||||
.headers
|
||||
.get(DATE)
|
||||
.ok_or(FoxError::InvalidHeader)?
|
||||
.to_str()?,
|
||||
)?;
|
||||
let signature = parts
|
||||
.headers
|
||||
.get(SIGNATURE_HEADER)
|
||||
.ok_or(FoxError::MissingSignature)?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
let host = parts
|
||||
.headers
|
||||
.get(HOST)
|
||||
.ok_or(FoxError::InvalidHeader)?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
|
||||
let content_length = if let Some(raw_length) = parts.headers.get(CONTENT_LENGTH) {
|
||||
Some(raw_length.to_str()?.parse::<usize>()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(FoxSignatureData {
|
||||
domain,
|
||||
date,
|
||||
signature,
|
||||
host,
|
||||
request_path: parts.uri.path().to_string(),
|
||||
content_length,
|
||||
})
|
||||
}
|
||||
}
|
31
identity/src/fed/proxy_header.rs
Normal file
31
identity/src/fed/proxy_header.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use axum::{async_trait, extract::FromRequestParts, http::request::Parts, Extension};
|
||||
use foxchat::{http::ApiError, fed::SERVER_HEADER, FoxError};
|
||||
|
||||
use crate::{app_state::AppState, model::chat_instance::ChatInstance};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ProxyServerHeader(pub ChatInstance);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for ProxyServerHeader
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let state: Extension<Arc<AppState>> = Extension::from_request_parts(parts, state)
|
||||
.await
|
||||
.expect("AppState was not added as an extension");
|
||||
|
||||
let domain = parts
|
||||
.headers
|
||||
.get(SERVER_HEADER)
|
||||
.ok_or(FoxError::InvalidHeader)?
|
||||
.to_str()?;
|
||||
|
||||
let instance = ChatInstance::get(state.0.clone(), domain).await?;
|
||||
|
||||
Ok(ProxyServerHeader(instance))
|
||||
}
|
||||
}
|
0
identity/src/http/account/get_user.rs
Normal file
0
identity/src/http/account/get_user.rs
Normal file
|
@ -1,7 +1,38 @@
|
|||
mod create_user;
|
||||
|
||||
use axum::{routing::post, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
routing::{get, post},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use foxchat::{http::ApiError, model::User};
|
||||
|
||||
use crate::{app_state::AppState, fed::FoxRequestData};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/_fox/ident/accounts", post(create_user::create_user))
|
||||
Router::new()
|
||||
.route("/_fox/ident/users", post(create_user::create_user))
|
||||
.route("/_fox/ident/users/:id", get(get_user))
|
||||
}
|
||||
|
||||
pub async fn get_user(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
_data: FoxRequestData,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<User>, ApiError> {
|
||||
let user = sqlx::query!(
|
||||
"select id, username, avatar from accounts where id = $1",
|
||||
id
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(User {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
instance: state.config.domain.clone(),
|
||||
avatar_url: None,
|
||||
}))
|
||||
}
|
||||
|
|
39
identity/src/http/auth.rs
Normal file
39
identity/src/http/auth.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{app_state::AppState, db::check_token, model::account::Account};
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::FromRequestParts,
|
||||
http::{header::AUTHORIZATION, request::Parts},
|
||||
Extension,
|
||||
};
|
||||
use foxchat::{http::ApiError, FoxError};
|
||||
|
||||
pub struct AuthUser(pub Account);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let state: Extension<Arc<AppState>> = Extension::from_request_parts(parts, state)
|
||||
.await
|
||||
.expect("AppState was not added as an extension");
|
||||
|
||||
let token = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(FoxError::Unauthorized)?
|
||||
.to_str()?
|
||||
.to_string();
|
||||
|
||||
if let Ok(user) = check_token(&state.pool, token).await {
|
||||
return Ok(AuthUser(user));
|
||||
}
|
||||
|
||||
Err(FoxError::Unauthorized.into())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
mod account;
|
||||
mod auth;
|
||||
mod node;
|
||||
mod proxy;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -7,13 +9,19 @@ use axum::{routing::get, Extension, Router};
|
|||
use sqlx::{Pool, Postgres};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::{app_state::AppState, config::Config};
|
||||
use crate::{app_state::AppState, config::Config, model::instance::Instance};
|
||||
|
||||
pub fn new(pool: Pool<Postgres>, config: Config) -> Router {
|
||||
let app_state = Arc::new(AppState { pool, config });
|
||||
pub fn new(pool: Pool<Postgres>, config: Config, instance: Instance) -> Router {
|
||||
let app_state = Arc::new(AppState {
|
||||
pool,
|
||||
config,
|
||||
public_key: instance.public_key,
|
||||
private_key: instance.private_key,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.merge(account::router())
|
||||
.nest("/_fox/proxy", proxy::router())
|
||||
.route("/_fox/ident/node", get(node::get_node))
|
||||
.route("/_fox/ident/node/:domain", get(node::get_chat_node))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
|
|
|
@ -5,14 +5,12 @@ use eyre::{Context, Result};
|
|||
use foxchat::{http::ApiError, s2s::http::NodeResponse};
|
||||
use rsa::pkcs1::EncodeRsaPublicKey;
|
||||
|
||||
use crate::{app_state::AppState, model::{instance::Instance, chat_instance::ChatInstance}};
|
||||
use crate::{app_state::AppState, model::chat_instance::ChatInstance};
|
||||
|
||||
pub async fn get_node(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<NodeResponse>, ApiError> {
|
||||
let instance = Instance::get(&state.pool).await?;
|
||||
|
||||
let public_key = instance
|
||||
let public_key: String = state
|
||||
.public_key
|
||||
.to_pkcs1_pem(rsa::pkcs8::LineEnding::LF)
|
||||
.wrap_err("serializing public key")?;
|
||||
|
@ -29,7 +27,7 @@ pub async fn get_chat_node(
|
|||
Extension(state): Extension<Arc<AppState>>,
|
||||
Path(domain): Path<String>,
|
||||
) -> Result<Json<ChatInstance>, ApiError> {
|
||||
let instance = ChatInstance::get(state, domain).await?;
|
||||
let instance = ChatInstance::get(state, &domain).await?;
|
||||
|
||||
Ok(Json(instance))
|
||||
}
|
||||
|
|
69
identity/src/http/proxy/mod.rs
Normal file
69
identity/src/http/proxy/mod.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::OriginalUri, routing::post, Extension, Json, Router};
|
||||
use eyre::ContextCompat;
|
||||
use foxchat::{
|
||||
fed,
|
||||
http::ApiError,
|
||||
model::{http::guild::CreateGuildParams, Guild},
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{app_state::AppState, fed::ProxyServerHeader};
|
||||
|
||||
use super::auth::AuthUser;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new().route("/guilds", post(proxy_post::<CreateGuildParams, Guild>))
|
||||
}
|
||||
|
||||
async fn proxy_get<R: Serialize + DeserializeOwned>(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
ProxyServerHeader(remote): ProxyServerHeader,
|
||||
AuthUser(user): AuthUser,
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
) -> Result<Json<R>, ApiError> {
|
||||
let original = original_uri.to_string();
|
||||
let proxy_path = original
|
||||
.strip_prefix("/_fox/proxy/")
|
||||
.wrap_err("invalid url")?;
|
||||
println!("{}", proxy_path);
|
||||
|
||||
let resp = fed::get::<R>(
|
||||
&state.private_key,
|
||||
&state.config.domain,
|
||||
&remote.domain,
|
||||
&format!("/_fox/chat/{}", proxy_path),
|
||||
Some(user.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(resp))
|
||||
}
|
||||
|
||||
async fn proxy_post<B: Serialize, R: Serialize + DeserializeOwned>(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
ProxyServerHeader(remote): ProxyServerHeader,
|
||||
AuthUser(user): AuthUser,
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
Json(body): Json<B>,
|
||||
) -> Result<Json<R>, ApiError> {
|
||||
let original = original_uri.to_string();
|
||||
let proxy_path = original
|
||||
.strip_prefix("/_fox/proxy/")
|
||||
.wrap_err("invalid url")?;
|
||||
debug!("Proxying request to {}", proxy_path);
|
||||
|
||||
let resp = fed::post::<B, R>(
|
||||
&state.private_key,
|
||||
&state.config.domain,
|
||||
&remote.domain,
|
||||
&format!("/_fox/chat/{}", proxy_path),
|
||||
Some(user.id),
|
||||
&body,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(resp))
|
||||
}
|
|
@ -3,8 +3,9 @@ mod config;
|
|||
mod db;
|
||||
mod http;
|
||||
mod model;
|
||||
mod fed;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::{config::Config, model::instance::Instance};
|
||||
use clap::{Parser, Subcommand};
|
||||
use color_eyre::eyre::Result;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
|
@ -63,8 +64,9 @@ async fn main_web(config: Config) -> Result<()> {
|
|||
db::init_instance(&pool).await?;
|
||||
info!("Initialized instance data!");
|
||||
|
||||
let instance = Instance::get(&pool).await?;
|
||||
let port = config.port;
|
||||
let app = http::new(pool, config);
|
||||
let app = http::new(pool, config, instance);
|
||||
|
||||
let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), port)).await?;
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use eyre::Result;
|
||||
use eyre::{Context, Result};
|
||||
use foxchat::{
|
||||
fed::{self, request::is_valid_domain},
|
||||
s2s::http::{HelloRequest, HelloResponse},
|
||||
FoxError,
|
||||
};
|
||||
use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{app_state::AppState, model::instance::Instance};
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ChatInstance {
|
||||
|
@ -29,7 +30,7 @@ pub enum InstanceStatus {
|
|||
}
|
||||
|
||||
impl ChatInstance {
|
||||
pub async fn get(state: Arc<AppState>, domain: String) -> Result<Self> {
|
||||
pub async fn get(state: Arc<AppState>, domain: &str) -> Result<Self> {
|
||||
if !is_valid_domain(&domain) {
|
||||
return Err(FoxError::InvalidServer.into());
|
||||
}
|
||||
|
@ -47,10 +48,8 @@ impl ChatInstance {
|
|||
return Ok(instance);
|
||||
}
|
||||
|
||||
let current_instance = Instance::get(&state.pool).await?;
|
||||
|
||||
let resp: HelloResponse = fed::post(
|
||||
¤t_instance.private_key,
|
||||
&state.private_key,
|
||||
&state.config.domain,
|
||||
&domain,
|
||||
"/_fox/chat/hello",
|
||||
|
@ -61,7 +60,7 @@ impl ChatInstance {
|
|||
)
|
||||
.await?;
|
||||
|
||||
if resp.host != domain.clone() {
|
||||
if resp.host != domain {
|
||||
return Err(FoxError::InvalidServer.into());
|
||||
}
|
||||
|
||||
|
@ -72,7 +71,7 @@ impl ChatInstance {
|
|||
returning id, domain, base_url, public_key,
|
||||
status as "status: InstanceStatus", reason"#,
|
||||
Ulid::new().to_string(),
|
||||
domain.clone(),
|
||||
domain,
|
||||
format!("https://{domain}"),
|
||||
resp.public_key
|
||||
)
|
||||
|
@ -81,4 +80,9 @@ impl ChatInstance {
|
|||
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
pub fn parse_public_key(&self) -> Result<RsaPublicKey> {
|
||||
RsaPublicKey::from_pkcs1_pem(&self.public_key)
|
||||
.wrap_err("parsing identity instance public key")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue