diff --git a/src/fetcher.rs b/src/fetcher.rs new file mode 100644 index 00000000..74f0bb3a --- /dev/null +++ b/src/fetcher.rs @@ -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 { + 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::() + .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) + } +} diff --git a/src/main.rs b/src/main.rs index 76215d02..66041112 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod router; mod errors; mod auth; mod dispatcher; +mod fetcher; use clap::{Parser, Subcommand}; use sea_orm::{ConnectOptions, Database, EntityTrait, IntoActiveModel}; diff --git a/src/router.rs b/src/router.rs index 60177d8a..5db25d47 100644 --- a/src/router.rs +++ b/src/router.rs @@ -30,7 +30,9 @@ pub async fn serve(db: DatabaseConnection, domain: String) { // specific object routes .route("/activities/:id", get(ap::activity::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 let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); diff --git a/src/server.rs b/src/server.rs index eaa9a1f0..d19f637d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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)] pub struct Context(Arc); @@ -10,6 +11,10 @@ struct ContextInner { db: DatabaseConnection, domain: String, protocol: String, + fetcher: Fetcher, + // TODO keep these pre-parsed + public_key: String, + private_key: String, } #[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 { - pub fn new(db: DatabaseConnection, mut domain: String) -> Self { + pub async fn new(db: DatabaseConnection, mut domain: String) -> Result { let protocol = if domain.starts_with("http://") { "http://" } else { "https://" }.to_string(); if domain.ends_with('/') { @@ -32,7 +49,34 @@ impl Context { 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!! } - 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 { @@ -49,6 +93,10 @@ impl Context { } } + pub fn fetch(&self) -> &Fetcher { + &self.0.fetcher + } + /// get full user id uri pub fn uid(&self, id: String) -> String { self.uri("users", id)