forked from alemi/upub
feat: added basic fetcher, get keys at startup
This commit is contained in:
parent
88808f020c
commit
e1b93e8a93
4 changed files with 109 additions and 6 deletions
52
src/fetcher.rs
Normal file
52
src/fetcher.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<ContextInner>);
|
||||
|
@ -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<Self, ContextError> {
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue