feat: added basic fetcher, get keys at startup

This commit is contained in:
əlemi 2024-03-25 21:19:02 +01:00
parent 88808f020c
commit e1b93e8a93
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 109 additions and 6 deletions

52
src/fetcher.rs Normal file
View file

@ -0,0 +1,52 @@
use reqwest::header::USER_AGENT;
use sea_orm::{DatabaseConnection, EntityTrait, IntoActiveModel};
use crate::{VERSION, model};
#[derive(Debug, thiserror::Error)]
pub enum FetchError {
#[error("could not dereference resource: {0}")]
Network(#[from] reqwest::Error),
#[error("error operating on database: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("missing field when constructing object: {0}")]
Field(#[from] model::FieldError),
}
pub struct Fetcher {
db: DatabaseConnection,
key: String, // TODO store pre-parsed
domain: String, // TODO merge directly with Context so we don't need to copy this
}
impl Fetcher {
pub fn new(db: DatabaseConnection, domain: String, key: String) -> Self {
Fetcher { db, domain, key }
}
pub async fn user(&self, id: &str) -> Result<model::user::Model, FetchError> {
if let Some(x) = model::user::Entity::find_by_id(id).one(&self.db).await? {
return Ok(x); // already in db, easy
}
// TODO sign http fetches, we got the app key and db to get user keys just in case
let user = reqwest::Client::new()
.get(id)
.header(USER_AGENT, format!("upub+{VERSION} ({})", self.domain)) // TODO put instance admin email
.send()
.await?
.json::<serde_json::Value>()
.await?;
let user_model = model::user::Model::new(&user)?;
model::user::Entity::insert(user_model.clone().into_active_model())
.exec(&self.db).await?;
Ok(user_model)
}
}

View file

@ -8,6 +8,7 @@ mod router;
mod errors; mod errors;
mod auth; mod auth;
mod dispatcher; mod dispatcher;
mod fetcher;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use sea_orm::{ConnectOptions, Database, EntityTrait, IntoActiveModel}; use sea_orm::{ConnectOptions, Database, EntityTrait, IntoActiveModel};

View file

@ -30,7 +30,9 @@ pub async fn serve(db: DatabaseConnection, domain: String) {
// specific object routes // specific object routes
.route("/activities/:id", get(ap::activity::view)) .route("/activities/:id", get(ap::activity::view))
.route("/objects/:id", get(ap::object::view)) .route("/objects/:id", get(ap::object::view))
.with_state(crate::server::Context::new(db, domain)); .with_state(
crate::server::Context::new(db, domain).await.expect("could not create server state")
);
// run our app with hyper, listening locally on port 3000 // run our app with hyper, listening locally on port 3000
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();

View file

@ -1,8 +1,9 @@
use std::sync::Arc; use std::{str::Utf8Error, sync::Arc};
use sea_orm::DatabaseConnection; use openssl::rsa::Rsa;
use sea_orm::{DatabaseConnection, DbErr, EntityTrait, QuerySelect, SelectColumns};
use crate::dispatcher::Dispatcher; use crate::{dispatcher::Dispatcher, fetcher::Fetcher, model};
#[derive(Clone)] #[derive(Clone)]
pub struct Context(Arc<ContextInner>); pub struct Context(Arc<ContextInner>);
@ -10,6 +11,10 @@ struct ContextInner {
db: DatabaseConnection, db: DatabaseConnection,
domain: String, domain: String,
protocol: String, protocol: String,
fetcher: Fetcher,
// TODO keep these pre-parsed
public_key: String,
private_key: String,
} }
#[macro_export] #[macro_export]
@ -19,8 +24,20 @@ macro_rules! url {
}; };
} }
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("database error: {0}")]
Db(#[from] DbErr),
#[error("openssl error: {0}")]
OpenSSL(#[from] openssl::error::ErrorStack),
#[error("invalid UTF8 PEM key: {0}")]
UTF8Error(#[from] Utf8Error)
}
impl Context { impl Context {
pub fn new(db: DatabaseConnection, mut domain: String) -> Self { pub async fn new(db: DatabaseConnection, mut domain: String) -> Result<Self, ContextError> {
let protocol = if domain.starts_with("http://") let protocol = if domain.starts_with("http://")
{ "http://" } else { "https://" }.to_string(); { "http://" } else { "https://" }.to_string();
if domain.ends_with('/') { if domain.ends_with('/') {
@ -32,7 +49,34 @@ impl Context {
for _ in 0..1 { // TODO customize delivery workers amount for _ in 0..1 { // TODO customize delivery workers amount
Dispatcher::spawn(db.clone(), domain.clone(), 30); // TODO ew don't do it this deep and secretly!! Dispatcher::spawn(db.clone(), domain.clone(), 30); // TODO ew don't do it this deep and secretly!!
} }
Context(Arc::new(ContextInner { db, domain, protocol })) let (public_key, private_key) = match model::application::Entity::find()
.select_only()
.select_column(model::application::Column::PublicKey)
.select_column(model::application::Column::PrivateKey)
.one(&db)
.await?
{
Some(model) => (model.public_key, model.private_key),
None => {
tracing::info!("generating application keys");
let rsa = Rsa::generate(2048)?;
let privk = std::str::from_utf8(&rsa.private_key_to_pem()?)?.to_string();
let pubk = std::str::from_utf8(&rsa.public_key_to_pem()?)?.to_string();
let system = model::application::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
private_key: sea_orm::ActiveValue::Set(privk.clone()),
public_key: sea_orm::ActiveValue::Set(pubk.clone()),
};
model::application::Entity::insert(system).exec(&db).await?;
(pubk, privk)
}
};
let fetcher = Fetcher::new(db.clone(), domain.clone(), private_key.clone());
Ok(Context(Arc::new(ContextInner {
db, domain, protocol, private_key, public_key, fetcher,
})))
} }
pub fn db(&self) -> &DatabaseConnection { pub fn db(&self) -> &DatabaseConnection {
@ -49,6 +93,10 @@ impl Context {
} }
} }
pub fn fetch(&self) -> &Fetcher {
&self.0.fetcher
}
/// get full user id uri /// get full user id uri
pub fn uid(&self, id: String) -> String { pub fn uid(&self, id: String) -> String {
self.uri("users", id) self.uri("users", id)