Compare commits
No commits in common. "be6d9bf28b6b734b05b670d0ae03207725a9f696" and "c0cebcca96b695998a10fee3e267475a5c4f18a3" have entirely different histories.
be6d9bf28b
...
c0cebcca96
12 changed files with 184 additions and 327 deletions
|
@ -10,8 +10,8 @@ pub use faker::*;
|
||||||
mod relay;
|
mod relay;
|
||||||
pub use relay::*;
|
pub use relay::*;
|
||||||
|
|
||||||
mod register;
|
//mod register;
|
||||||
pub use register::*;
|
//pub use register::*;
|
||||||
|
|
||||||
mod update;
|
mod update;
|
||||||
pub use update::*;
|
pub use update::*;
|
||||||
|
@ -64,32 +64,6 @@ pub enum CliCommand {
|
||||||
#[arg(long, short, default_value_t = 7)]
|
#[arg(long, short, default_value_t = 7)]
|
||||||
/// number of days after which users should get updated
|
/// number of days after which users should get updated
|
||||||
days: i64,
|
days: i64,
|
||||||
},
|
|
||||||
|
|
||||||
/// register a new local user
|
|
||||||
Register {
|
|
||||||
/// username for new user, must be unique locally and cannot be changed
|
|
||||||
username: String,
|
|
||||||
|
|
||||||
/// password for new user
|
|
||||||
// TODO get this with getpass rather than argv!!!!
|
|
||||||
password: String,
|
|
||||||
|
|
||||||
/// display name for new user
|
|
||||||
#[arg(long = "name")]
|
|
||||||
display_name: Option<String>,
|
|
||||||
|
|
||||||
/// summary text for new user
|
|
||||||
#[arg(long = "summary")]
|
|
||||||
summary: Option<String>,
|
|
||||||
|
|
||||||
/// url for avatar image of new user
|
|
||||||
#[arg(long = "avatar")]
|
|
||||||
avatar_url: Option<String>,
|
|
||||||
|
|
||||||
/// url for banner image of new user
|
|
||||||
#[arg(long = "banner")]
|
|
||||||
banner_url: Option<String>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +87,5 @@ pub async fn run(
|
||||||
Ok(fix(ctx, likes, shares, replies).await?),
|
Ok(fix(ctx, likes, shares, replies).await?),
|
||||||
CliCommand::Update { days } =>
|
CliCommand::Update { days } =>
|
||||||
Ok(update_users(ctx, days).await?),
|
Ok(update_users(ctx, days).await?),
|
||||||
CliCommand::Register { username, password, display_name, summary, avatar_url, banner_url } =>
|
|
||||||
Ok(register(ctx, username, password, display_name, summary, avatar_url, banner_url).await?),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,37 @@ use openssl::rsa::Rsa;
|
||||||
use sea_orm::{EntityTrait, IntoActiveModel};
|
use sea_orm::{EntityTrait, IntoActiveModel};
|
||||||
|
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
ctx: crate::server::Context,
|
db: sea_orm::DatabaseConnection,
|
||||||
username: String,
|
domain: String,
|
||||||
password: String,
|
|
||||||
display_name: Option<String>,
|
|
||||||
summary: Option<String>,
|
|
||||||
avatar_url: Option<String>,
|
|
||||||
banner_url: Option<String>,
|
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
ctx.register_user(
|
let key = Rsa::generate(2048).unwrap();
|
||||||
username.clone(),
|
let test_user = crate::model::user::Model {
|
||||||
password,
|
id: format!("{domain}/users/test"),
|
||||||
display_name,
|
name: Some("μpub".into()),
|
||||||
summary,
|
domain: clean_domain(&domain),
|
||||||
avatar_url,
|
preferred_username: "test".to_string(),
|
||||||
banner_url,
|
summary: Some("hello world! i'm manually generated but served dynamically from db! check progress at https://git.alemi.dev/upub.git".to_string()),
|
||||||
).await?;
|
following: None,
|
||||||
|
following_count: 0,
|
||||||
|
followers: None,
|
||||||
|
followers_count: 0,
|
||||||
|
statuses_count: 0,
|
||||||
|
icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()),
|
||||||
|
image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()),
|
||||||
|
inbox: None,
|
||||||
|
shared_inbox: None,
|
||||||
|
outbox: None,
|
||||||
|
actor_type: apb::ActorType::Person,
|
||||||
|
created: chrono::Utc::now(),
|
||||||
|
updated: chrono::Utc::now(),
|
||||||
|
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()),
|
||||||
|
// TODO generate a fresh one every time
|
||||||
|
public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!("registered new user: {username}");
|
crate::model::user::Entity::insert(test_user.clone().into_active_model()).exec(&db).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO duplicated, make an util?? idk
|
// TODO duplicated, make an util?? idk
|
||||||
|
|
|
@ -9,31 +9,9 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub datasource: DatasourceConfig,
|
pub datasource: DatasourceConfig,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub security: SecurityConfig,
|
|
||||||
|
|
||||||
// TODO should i move app keys here?
|
// TODO should i move app keys here?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_inline_default::serde_inline_default]
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
|
|
||||||
pub struct InstanceConfig {
|
|
||||||
#[serde_inline_default("μpub".into())]
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
#[serde_inline_default("micro social network, federated".into())]
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
#[serde_inline_default("upub.social".into())]
|
|
||||||
pub domain: String,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub contact: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub frontend: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serde_inline_default::serde_inline_default]
|
#[serde_inline_default::serde_inline_default]
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
|
||||||
pub struct DatasourceConfig {
|
pub struct DatasourceConfig {
|
||||||
|
@ -61,12 +39,26 @@ pub struct DatasourceConfig {
|
||||||
|
|
||||||
#[serde_inline_default::serde_inline_default]
|
#[serde_inline_default::serde_inline_default]
|
||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, serde_default::DefaultFromSerde)]
|
||||||
pub struct SecurityConfig {
|
pub struct InstanceConfig {
|
||||||
|
#[serde_inline_default("μpub".into())]
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde_inline_default("micro social network, federated".into())]
|
||||||
|
pub description: String,
|
||||||
|
|
||||||
|
#[serde_inline_default("upub.social".into())]
|
||||||
|
pub domain: String,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_registration: bool,
|
pub contact: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub frontend: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(path: Option<std::path::PathBuf>) -> Self {
|
pub fn load(path: Option<std::path::PathBuf>) -> Self {
|
||||||
let Some(cfg_path) = path else { return Config::default() };
|
let Some(cfg_path) = path else { return Config::default() };
|
||||||
|
|
|
@ -2,7 +2,7 @@ use axum::{http::StatusCode, extract::State, Json};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
use crate::{errors::UpubError, model, server::{admin::Administrable, Context}};
|
use crate::{errors::UpubError, model, server::Context};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
@ -18,10 +18,7 @@ pub struct AuthSuccess {
|
||||||
expires: chrono::DateTime<chrono::Utc>,
|
expires: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn login(State(ctx): State<Context>, Json(login): Json<LoginForm>) -> crate::Result<Json<AuthSuccess>> {
|
||||||
State(ctx): State<Context>,
|
|
||||||
Json(login): Json<LoginForm>
|
|
||||||
) -> crate::Result<Json<AuthSuccess>> {
|
|
||||||
// TODO salt the pwd
|
// TODO salt the pwd
|
||||||
match model::credential::Entity::find()
|
match model::credential::Entity::find()
|
||||||
.filter(Condition::all()
|
.filter(Condition::all()
|
||||||
|
@ -56,33 +53,3 @@ pub async fn login(
|
||||||
None => Err(UpubError::unauthorized()),
|
None => Err(UpubError::unauthorized()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
|
||||||
pub struct RegisterForm {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
display_name: Option<String>,
|
|
||||||
summary: Option<String>,
|
|
||||||
avatar_url: Option<String>,
|
|
||||||
banner_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn register(
|
|
||||||
State(ctx): State<Context>,
|
|
||||||
Json(registration): Json<RegisterForm>
|
|
||||||
) -> crate::Result<Json<String>> {
|
|
||||||
if !ctx.cfg().security.allow_registration {
|
|
||||||
return Err(UpubError::forbidden());
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.register_user(
|
|
||||||
registration.username.clone(),
|
|
||||||
registration.password,
|
|
||||||
registration.display_name,
|
|
||||||
registration.summary,
|
|
||||||
registration.avatar_url,
|
|
||||||
registration.banner_url
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
Ok(Json(ctx.uid(registration.username)))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use apb::{server::Inbox, Activity, ActivityType};
|
use apb::{server::Inbox, Activity, ActivityType};
|
||||||
use axum::{extract::{Query, State}, http::StatusCode, Json};
|
use axum::{extract::{Query, State}, http::StatusCode, Json};
|
||||||
use sea_orm::{sea_query::IntoCondition, ColumnTrait};
|
|
||||||
|
|
||||||
use crate::{errors::UpubError, server::{auth::{AuthIdentity, Identity}, Context}, url};
|
use crate::{errors::UpubError, server::{auth::{AuthIdentity, Identity}, Context}, url};
|
||||||
|
|
||||||
|
@ -20,8 +19,7 @@ pub async fn page(
|
||||||
) -> crate::Result<JsonLD<serde_json::Value>> {
|
) -> crate::Result<JsonLD<serde_json::Value>> {
|
||||||
crate::server::builders::paginate(
|
crate::server::builders::paginate(
|
||||||
url!(ctx, "/inbox/page"),
|
url!(ctx, "/inbox/page"),
|
||||||
crate::model::addressing::Column::Actor.eq(apb::target::PUBLIC)
|
auth.filter_condition(),
|
||||||
.into_condition(),
|
|
||||||
ctx.db(),
|
ctx.db(),
|
||||||
page,
|
page,
|
||||||
auth.my_id(),
|
auth.my_id(),
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub mod well_known;
|
||||||
pub mod jsonld;
|
pub mod jsonld;
|
||||||
pub use jsonld::JsonLD;
|
pub use jsonld::JsonLD;
|
||||||
|
|
||||||
use axum::{http::StatusCode, response::IntoResponse, routing::{get, post, put}, Router};
|
use axum::{http::StatusCode, response::IntoResponse, routing::{get, post}, Router};
|
||||||
|
|
||||||
pub trait ActivityPubRouter {
|
pub trait ActivityPubRouter {
|
||||||
fn ap_routes(self) -> Self;
|
fn ap_routes(self) -> Self;
|
||||||
|
@ -35,7 +35,6 @@ impl ActivityPubRouter for Router<crate::server::Context> {
|
||||||
.route("/outbox/page", get(ap::outbox::page))
|
.route("/outbox/page", get(ap::outbox::page))
|
||||||
// AUTH routes
|
// AUTH routes
|
||||||
.route("/auth", post(ap::auth::login))
|
.route("/auth", post(ap::auth::login))
|
||||||
.route("/auth", put(ap::auth::register))
|
|
||||||
// .well-known and discovery
|
// .well-known and discovery
|
||||||
.route("/.well-known/webfinger", get(ap::well_known::webfinger))
|
.route("/.well-known/webfinger", get(ap::well_known::webfinger))
|
||||||
.route("/.well-known/host-meta", get(ap::well_known::host_meta))
|
.route("/.well-known/host-meta", get(ap::well_known::host_meta))
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
use sea_orm::{EntityTrait, IntoActiveModel};
|
|
||||||
|
|
||||||
#[axum::async_trait]
|
|
||||||
pub trait Administrable {
|
|
||||||
async fn register_user(
|
|
||||||
&self,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
display_name: Option<String>,
|
|
||||||
summary: Option<String>,
|
|
||||||
avatar_url: Option<String>,
|
|
||||||
banner_url: Option<String>,
|
|
||||||
) -> crate::Result<()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[axum::async_trait]
|
|
||||||
impl Administrable for super::Context {
|
|
||||||
async fn register_user(
|
|
||||||
&self,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
display_name: Option<String>,
|
|
||||||
summary: Option<String>,
|
|
||||||
avatar_url: Option<String>,
|
|
||||||
banner_url: Option<String>,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
let key = openssl::rsa::Rsa::generate(2048).unwrap();
|
|
||||||
let ap_id = self.uid(username.clone());
|
|
||||||
let db = self.db();
|
|
||||||
let domain = self.domain().to_string();
|
|
||||||
let user_model = crate::model::user::Model {
|
|
||||||
id: ap_id.clone(),
|
|
||||||
name: display_name,
|
|
||||||
domain, summary,
|
|
||||||
preferred_username: username.clone(),
|
|
||||||
following: None,
|
|
||||||
following_count: 0,
|
|
||||||
followers: None,
|
|
||||||
followers_count: 0,
|
|
||||||
statuses_count: 0,
|
|
||||||
icon: avatar_url,
|
|
||||||
image: banner_url,
|
|
||||||
inbox: None,
|
|
||||||
shared_inbox: None,
|
|
||||||
outbox: None,
|
|
||||||
actor_type: apb::ActorType::Person,
|
|
||||||
created: chrono::Utc::now(),
|
|
||||||
updated: chrono::Utc::now(),
|
|
||||||
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()),
|
|
||||||
public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
crate::model::user::Entity::insert(user_model.into_active_model())
|
|
||||||
.exec(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let config_model = crate::model::config::Model {
|
|
||||||
id: ap_id.clone(),
|
|
||||||
accept_follow_requests: true,
|
|
||||||
show_followers_count: true,
|
|
||||||
show_following_count: true,
|
|
||||||
show_followers: false,
|
|
||||||
show_following: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
crate::model::config::Entity::insert(config_model.into_active_model())
|
|
||||||
.exec(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let credentials_model = crate::model::credential::Model {
|
|
||||||
id: ap_id,
|
|
||||||
email: username,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
|
|
||||||
crate::model::credential::Entity::insert(credentials_model.into_active_model())
|
|
||||||
.exec(db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts}};
|
use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts}};
|
||||||
|
use base64::Engine;
|
||||||
|
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
use crate::{errors::UpubError, model, server::Context};
|
use crate::{errors::UpubError, model, server::Context};
|
||||||
|
|
||||||
use super::{fetcher::Fetcher, httpsign::HttpSignature};
|
use super::fetcher::Fetcher;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Identity {
|
pub enum Identity {
|
||||||
|
@ -143,3 +147,129 @@ where
|
||||||
Ok(AuthIdentity(identity))
|
Ok(AuthIdentity(identity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct HttpSignature {
|
||||||
|
pub key_id: String,
|
||||||
|
pub algorithm: String,
|
||||||
|
pub headers: Vec<String>,
|
||||||
|
pub signature: String,
|
||||||
|
pub control: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpSignature {
|
||||||
|
pub fn new(key_id: String, algorithm: String, headers: &[&str]) -> Self {
|
||||||
|
HttpSignature {
|
||||||
|
key_id, algorithm,
|
||||||
|
headers: headers.iter().map(|x| x.to_string()).collect(),
|
||||||
|
signature: String::new(),
|
||||||
|
control: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(header: &str) -> Self {
|
||||||
|
let mut sig = HttpSignature::default();
|
||||||
|
header.split(',')
|
||||||
|
.filter_map(|x| x.split_once('='))
|
||||||
|
.map(|(k, v)| (k, v.trim_end_matches('"').trim_matches('"')))
|
||||||
|
.for_each(|(k, v)| match k {
|
||||||
|
"keyId" => sig.key_id = v.to_string(),
|
||||||
|
"algorithm" => sig.algorithm = v.to_string(),
|
||||||
|
"signature" => sig.signature = v.to_string(),
|
||||||
|
"headers" => sig.headers = v.split(' ').map(|x| x.to_string()).collect(),
|
||||||
|
_ => tracing::warn!("unexpected field in http signature: '{k}=\"{v}\"'"),
|
||||||
|
});
|
||||||
|
sig
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"keyId=\"{}\",algorithm=\"{}\",headers=\"{}\",signature=\"{}\"",
|
||||||
|
self.key_id, self.algorithm, self.headers.join(" "), self.signature,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_manually(&mut self, method: &str, target: &str, mut headers: BTreeMap<String, String>) -> &mut Self {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for header in &self.headers {
|
||||||
|
match header.as_str() {
|
||||||
|
"(request-target)" => out.push(format!("(request-target): {method} {target}")),
|
||||||
|
// TODO other pseudo-headers
|
||||||
|
_ => out.push(
|
||||||
|
format!("{header}: {}", headers.remove(header).unwrap_or_default())
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.control = out.join("\n");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_from_parts(&mut self, parts: &Parts) -> &mut Self {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for header in self.headers.iter() {
|
||||||
|
match header.as_str() {
|
||||||
|
"(request-target)" => out.push(
|
||||||
|
format!(
|
||||||
|
"(request-target): {} {}",
|
||||||
|
parts.method.to_string().to_lowercase(),
|
||||||
|
parts.uri.path_and_query().map(|x| x.as_str()).unwrap_or("/")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
// TODO other pseudo-headers,
|
||||||
|
_ => out.push(format!("{}: {}",
|
||||||
|
header.to_lowercase(),
|
||||||
|
parts.headers.get(header).map(|x| x.to_str().unwrap_or("")).unwrap_or("")
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.control = out.join("\n");
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, key: &str) -> crate::Result<bool> {
|
||||||
|
let pubkey = PKey::public_key_from_pem(key.as_bytes())?;
|
||||||
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey)?;
|
||||||
|
let signature = base64::prelude::BASE64_STANDARD.decode(&self.signature)?;
|
||||||
|
Ok(verifier.verify_oneshot(&signature, self.control.as_bytes())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign(&mut self, key: &str) -> crate::Result<&str> {
|
||||||
|
let privkey = PKey::private_key_from_pem(key.as_bytes())?;
|
||||||
|
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &privkey)?;
|
||||||
|
signer.update(self.control.as_bytes())?;
|
||||||
|
self.signature = base64::prelude::BASE64_STANDARD.encode(signer.sign_to_vec()?);
|
||||||
|
Ok(&self.signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn http_signature_signs_and_verifies() {
|
||||||
|
let key = openssl::rsa::Rsa::generate(2048).unwrap();
|
||||||
|
let private_key = std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string();
|
||||||
|
let public_key = std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string();
|
||||||
|
let mut signer = super::HttpSignature {
|
||||||
|
key_id: "test".to_string(),
|
||||||
|
algorithm: "rsa-sha256".to_string(),
|
||||||
|
headers: vec![
|
||||||
|
"(request-target)".to_string(),
|
||||||
|
"host".to_string(),
|
||||||
|
"date".to_string(),
|
||||||
|
],
|
||||||
|
signature: String::new(),
|
||||||
|
control: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
signer
|
||||||
|
.build_manually("get", "/actor/inbox", [("host".into(), "example.net".into()), ("date".into(), "Sat, 13 Apr 2024 13:36:23 GMT".into())].into())
|
||||||
|
.sign(&private_key)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut verifier = super::HttpSignature::parse(&signer.header());
|
||||||
|
verifier.build_manually("get", "/actor/inbox", [("host".into(), "example.net".into()), ("date".into(), "Sat, 13 Apr 2024 13:36:23 GMT".into())].into());
|
||||||
|
|
||||||
|
assert!(verifier.verify(&public_key).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ impl Context {
|
||||||
.replace("///", "/@/") // omg wordpress PLEASE AAAAAAAAAAAAAAAAAAAA
|
.replace("///", "/@/") // omg wordpress PLEASE AAAAAAAAAAAAAAAAAAAA
|
||||||
.replace("//", "/@") // oops my method sucks!! TODO
|
.replace("//", "/@") // oops my method sucks!! TODO
|
||||||
.replacen('+', "https://", 1)
|
.replacen('+', "https://", 1)
|
||||||
.replace(' ', "%20") // omg wordpress
|
.replace(" ", "%20") // omg wordpress
|
||||||
} else { // bare local id
|
} else { // bare local id
|
||||||
format!("{}{}/{}/{}", self.0.protocol, self.0.domain, entity, id)
|
format!("{}{}/{}/{}", self.0.protocol, self.0.domain, entity, id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryF
|
||||||
|
|
||||||
use crate::{errors::UpubError, model, VERSION};
|
use crate::{errors::UpubError, model, VERSION};
|
||||||
|
|
||||||
use super::{httpsign::HttpSignature, Context};
|
use super::{auth::HttpSignature, Context};
|
||||||
|
|
||||||
#[axum::async_trait]
|
#[axum::async_trait]
|
||||||
pub trait Fetcher {
|
pub trait Fetcher {
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use axum::http::request::Parts;
|
|
||||||
use base64::Engine;
|
|
||||||
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct HttpSignature {
|
|
||||||
pub key_id: String,
|
|
||||||
pub algorithm: String,
|
|
||||||
pub headers: Vec<String>,
|
|
||||||
pub signature: String,
|
|
||||||
pub control: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpSignature {
|
|
||||||
pub fn new(key_id: String, algorithm: String, headers: &[&str]) -> Self {
|
|
||||||
HttpSignature {
|
|
||||||
key_id, algorithm,
|
|
||||||
headers: headers.iter().map(|x| x.to_string()).collect(),
|
|
||||||
signature: String::new(),
|
|
||||||
control: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(header: &str) -> Self {
|
|
||||||
let mut sig = HttpSignature::default();
|
|
||||||
header.split(',')
|
|
||||||
.filter_map(|x| x.split_once('='))
|
|
||||||
.map(|(k, v)| (k, v.trim_end_matches('"').trim_matches('"')))
|
|
||||||
.for_each(|(k, v)| match k {
|
|
||||||
"keyId" => sig.key_id = v.to_string(),
|
|
||||||
"algorithm" => sig.algorithm = v.to_string(),
|
|
||||||
"signature" => sig.signature = v.to_string(),
|
|
||||||
"headers" => sig.headers = v.split(' ').map(|x| x.to_string()).collect(),
|
|
||||||
_ => tracing::warn!("unexpected field in http signature: '{k}=\"{v}\"'"),
|
|
||||||
});
|
|
||||||
sig
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn header(&self) -> String {
|
|
||||||
format!(
|
|
||||||
"keyId=\"{}\",algorithm=\"{}\",headers=\"{}\",signature=\"{}\"",
|
|
||||||
self.key_id, self.algorithm, self.headers.join(" "), self.signature,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_manually(&mut self, method: &str, target: &str, mut headers: BTreeMap<String, String>) -> &mut Self {
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for header in &self.headers {
|
|
||||||
match header.as_str() {
|
|
||||||
"(request-target)" => out.push(format!("(request-target): {method} {target}")),
|
|
||||||
// TODO other pseudo-headers
|
|
||||||
_ => out.push(
|
|
||||||
format!("{header}: {}", headers.remove(header).unwrap_or_default())
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.control = out.join("\n");
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_from_parts(&mut self, parts: &Parts) -> &mut Self {
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for header in self.headers.iter() {
|
|
||||||
match header.as_str() {
|
|
||||||
"(request-target)" => out.push(
|
|
||||||
format!(
|
|
||||||
"(request-target): {} {}",
|
|
||||||
parts.method.to_string().to_lowercase(),
|
|
||||||
parts.uri.path_and_query().map(|x| x.as_str()).unwrap_or("/")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// TODO other pseudo-headers,
|
|
||||||
_ => out.push(format!("{}: {}",
|
|
||||||
header.to_lowercase(),
|
|
||||||
parts.headers.get(header).map(|x| x.to_str().unwrap_or("")).unwrap_or("")
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.control = out.join("\n");
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self, key: &str) -> crate::Result<bool> {
|
|
||||||
let pubkey = PKey::public_key_from_pem(key.as_bytes())?;
|
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey)?;
|
|
||||||
let signature = base64::prelude::BASE64_STANDARD.decode(&self.signature)?;
|
|
||||||
Ok(verifier.verify_oneshot(&signature, self.control.as_bytes())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign(&mut self, key: &str) -> crate::Result<&str> {
|
|
||||||
let privkey = PKey::private_key_from_pem(key.as_bytes())?;
|
|
||||||
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &privkey)?;
|
|
||||||
signer.update(self.control.as_bytes())?;
|
|
||||||
self.signature = base64::prelude::BASE64_STANDARD.encode(signer.sign_to_vec()?);
|
|
||||||
Ok(&self.signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
#[test]
|
|
||||||
fn http_signature_signs_and_verifies() {
|
|
||||||
let key = openssl::rsa::Rsa::generate(2048).unwrap();
|
|
||||||
let private_key = std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string();
|
|
||||||
let public_key = std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string();
|
|
||||||
let mut signer = super::HttpSignature {
|
|
||||||
key_id: "test".to_string(),
|
|
||||||
algorithm: "rsa-sha256".to_string(),
|
|
||||||
headers: vec![
|
|
||||||
"(request-target)".to_string(),
|
|
||||||
"host".to_string(),
|
|
||||||
"date".to_string(),
|
|
||||||
],
|
|
||||||
signature: String::new(),
|
|
||||||
control: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
signer
|
|
||||||
.build_manually("get", "/actor/inbox", [("host".into(), "example.net".into()), ("date".into(), "Sat, 13 Apr 2024 13:36:23 GMT".into())].into())
|
|
||||||
.sign(&private_key)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut verifier = super::HttpSignature::parse(&signer.header());
|
|
||||||
verifier.build_manually("get", "/actor/inbox", [("host".into(), "example.net".into()), ("date".into(), "Sat, 13 Apr 2024 13:36:23 GMT".into())].into());
|
|
||||||
|
|
||||||
assert!(verifier.verify(&public_key).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod admin;
|
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod dispatcher;
|
pub mod dispatcher;
|
||||||
pub mod fetcher;
|
pub mod fetcher;
|
||||||
|
@ -6,6 +5,5 @@ pub mod inbox;
|
||||||
pub mod outbox;
|
pub mod outbox;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod builders;
|
pub mod builders;
|
||||||
pub mod httpsign;
|
|
||||||
|
|
||||||
pub use context::Context;
|
pub use context::Context;
|
||||||
|
|
Loading…
Reference in a new issue