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 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};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue