diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 55f015cd..362db0b5 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -15,6 +15,9 @@ use pubky_common::timestamp::Timestamp; const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_STORAGE_DIR: &str = "pubky"; +pub const DEFAULT_LIST_LIMIT: u16 = 100; +pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000; + /// Server configuration #[derive(Serialize, Deserialize, Clone)] pub struct Config { @@ -30,6 +33,14 @@ pub struct Config { secret_key: Option<[u8; 32]>, dht_request_timeout: Option, + /// The default limit of a list api if no `limit` query parameter is provided. + /// + /// Defaults to `100` + default_list_limit: u16, + /// The maximum limit of a list api, even if a `limit` query parameter is provided. + /// + /// Defaults to `1000` + max_list_limit: u16, } impl Config { @@ -102,6 +113,18 @@ impl Config { &self.domain } + pub fn keypair(&self) -> Keypair { + Keypair::from_secret_key(&self.secret_key.unwrap_or_default()) + } + + pub fn default_list_limit(&self) -> u16 { + self.default_list_limit + } + + pub fn max_list_limit(&self) -> u16 { + self.max_list_limit + } + /// Get the path to the storage directory pub fn storage(&self) -> Result { let dir = if let Some(storage) = &self.storage { @@ -116,10 +139,6 @@ impl Config { Ok(dir.join("homeserver")) } - pub fn keypair(&self) -> Keypair { - Keypair::from_secret_key(&self.secret_key.unwrap_or_default()) - } - pub(crate) fn dht_request_timeout(&self) -> Option { self.dht_request_timeout } @@ -135,6 +154,8 @@ impl Default for Config { storage: None, secret_key: None, dht_request_timeout: None, + default_list_limit: DEFAULT_LIST_LIMIT, + max_list_limit: DEFAULT_MAX_LIST_LIMIT, } } } diff --git a/pubky-homeserver/src/database.rs b/pubky-homeserver/src/database.rs index 9eb291f4..4b282a14 100644 --- a/pubky-homeserver/src/database.rs +++ b/pubky-homeserver/src/database.rs @@ -1,12 +1,12 @@ use std::fs; -use std::path::Path; - use heed::{Env, EnvOpenOptions}; mod migrations; pub mod tables; +use crate::config::Config; + use tables::{Tables, TABLES_COUNT}; pub const DEFAULT_MAP_SIZE: usize = 10995116277760; // 10TB (not = disk-space used) @@ -15,11 +15,14 @@ pub const DEFAULT_MAP_SIZE: usize = 10995116277760; // 10TB (not = disk-space us pub struct DB { pub(crate) env: Env, pub(crate) tables: Tables, + pub(crate) config: Config, } impl DB { - pub fn open(storage: &Path) -> anyhow::Result { - fs::create_dir_all(storage).unwrap(); + pub fn open(config: Config) -> anyhow::Result { + let storage = config.storage()?; + + fs::create_dir_all(&storage).unwrap(); let env = unsafe { EnvOpenOptions::new() @@ -31,7 +34,11 @@ impl DB { let tables = migrations::run(&env)?; - let db = DB { env, tables }; + let db = DB { + env, + tables, + config, + }; Ok(db) } @@ -40,18 +47,15 @@ impl DB { #[cfg(test)] mod tests { use bytes::Bytes; - use pkarr::Keypair; - use pubky_common::timestamp::Timestamp; + use pkarr::{mainline::Testnet, Keypair}; + + use crate::config::Config; use super::DB; #[tokio::test] async fn entries() { - let storage = std::env::temp_dir() - .join(Timestamp::now().to_string()) - .join("pubky"); - - let db = DB::open(&storage).unwrap(); + let db = DB::open(Config::test(&Testnet::new(0))).unwrap(); let keypair = Keypair::random(); let path = "/pub/foo.txt"; diff --git a/pubky-homeserver/src/database/tables/entries.rs b/pubky-homeserver/src/database/tables/entries.rs index 081f606e..b1c70392 100644 --- a/pubky-homeserver/src/database/tables/entries.rs +++ b/pubky-homeserver/src/database/tables/entries.rs @@ -13,7 +13,7 @@ use pubky_common::{ timestamp::Timestamp, }; -use crate::database::{DB, MAX_LIST_LIMIT}; +use crate::database::DB; use super::events::Event; @@ -157,7 +157,7 @@ impl DB { /// Return a list of pubky urls. /// - /// - limit defaults to and capped by [MAX_LIST_LIMIT] + /// - limit defaults to [Config::default_list_limit] and capped by [Config::max_list_limit] pub fn list( &self, txn: &RoTxn, @@ -170,7 +170,9 @@ impl DB { // Vector to store results let mut results = Vec::new(); - let limit = limit.unwrap_or(MAX_LIST_LIMIT).min(MAX_LIST_LIMIT); + let limit = limit + .unwrap_or(self.config.default_list_limit()) + .min(self.config.max_list_limit()); // TODO: make this more performant than split and allocations? diff --git a/pubky-homeserver/src/database/tables/events.rs b/pubky-homeserver/src/database/tables/events.rs index c04554b8..18297655 100644 --- a/pubky-homeserver/src/database/tables/events.rs +++ b/pubky-homeserver/src/database/tables/events.rs @@ -59,10 +59,11 @@ impl Event { } } -const MAX_LIST_LIMIT: u16 = 1000; -const DEFAULT_LIST_LIMIT: u16 = 100; - impl DB { + /// Returns a list of events formatted as ` `. + /// + /// - limit defaults to [Config::default_list_limit] and capped by [Config::max_list_limit] + /// - cursor is a 13 character string encoding of a timestamp pub fn list_events( &self, limit: Option, @@ -70,15 +71,11 @@ impl DB { ) -> anyhow::Result> { let txn = self.env.read_txn()?; - let limit = limit.unwrap_or(DEFAULT_LIST_LIMIT).min(MAX_LIST_LIMIT); - - let mut cursor = cursor.unwrap_or("0000000000000"); + let limit = limit + .unwrap_or(self.config.default_list_limit()) + .min(self.config.max_list_limit()); - // Cursor smaller than 13 character is invalid - // TODO: should we send an error instead? - if cursor.len() < 13 { - cursor = "0000000000000" - } + let cursor = cursor.unwrap_or("0000000000000"); let mut result: Vec = vec![]; let mut next_cursor = cursor.to_string(); diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index cf4d7b7e..c23755ea 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -5,8 +5,8 @@ use pkarr::{ Keypair, PkarrClientAsync, SignedPacket, }; -pub async fn publish_server_packet( - pkarr_client: PkarrClientAsync, +pub(crate) async fn publish_server_packet( + pkarr_client: &PkarrClientAsync, keypair: &Keypair, domain: &str, port: u16, diff --git a/pubky-homeserver/src/routes/feed.rs b/pubky-homeserver/src/routes/feed.rs index 0afe5040..627eb3e7 100644 --- a/pubky-homeserver/src/routes/feed.rs +++ b/pubky-homeserver/src/routes/feed.rs @@ -6,8 +6,12 @@ use axum::{ http::{header, Response, StatusCode}, response::IntoResponse, }; +use pubky_common::timestamp::{Timestamp, TimestampError}; -use crate::{error::Result, server::AppState}; +use crate::{ + error::{Error, Result}, + server::AppState, +}; pub async fn feed( State(state): State, @@ -16,6 +20,21 @@ pub async fn feed( let limit = params.get("limit").and_then(|l| l.parse::().ok()); let cursor = params.get("cursor").map(|c| c.as_str()); + if let Some(cursor) = cursor { + if let Err(timestmap_error) = Timestamp::try_from(cursor.to_string()) { + let cause = match timestmap_error { + TimestampError::InvalidEncoding => { + "Cursor should be valid base32 Crockford encoding of a timestamp" + } + TimestampError::InvalidBytesLength(size) => { + &format!("Cursor should be 13 characters long, got: {size}") + } + }; + + Err(Error::new(StatusCode::BAD_REQUEST, cause.into()))? + } + } + let result = state.db.list_events(limit, cursor)?; Ok(Response::builder() diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index c94a803f..44716f7e 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -14,16 +14,17 @@ use crate::{config::Config, database::DB, pkarr::publish_server_packet}; #[derive(Debug)] pub struct Homeserver { - port: u16, - config: Config, + state: AppState, tasks: JoinSet>, } #[derive(Clone, Debug)] pub(crate) struct AppState { - pub verifier: AuthVerifier, - pub db: DB, - pub pkarr_client: PkarrClientAsync, + pub(crate) verifier: AuthVerifier, + pub(crate) db: DB, + pub(crate) pkarr_client: PkarrClientAsync, + pub(crate) config: Config, + pub(crate) port: u16, } impl Homeserver { @@ -32,7 +33,7 @@ impl Homeserver { let keypair = config.keypair(); - let db = DB::open(&config.storage()?)?; + let db = DB::open(config.clone())?; let pkarr_client = PkarrClient::new(Settings { dht: DhtSettings { @@ -44,22 +45,22 @@ impl Homeserver { })? .as_async(); - let state = AppState { - verifier: AuthVerifier::default(), - db, - pkarr_client: pkarr_client.clone(), - }; - - let app = crate::routes::create_app(state); - let mut tasks = JoinSet::new(); - let app = app.clone(); - let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; let port = listener.local_addr()?.port(); + let state = AppState { + verifier: AuthVerifier::default(), + db, + pkarr_client, + config, + port, + }; + + let app = crate::routes::create_app(state.clone()); + // Spawn http server task tasks.spawn( axum::serve( @@ -72,15 +73,11 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - publish_server_packet(pkarr_client, &keypair, config.domain(), port).await?; + publish_server_packet(&state.pkarr_client, &keypair, state.config.domain(), port).await?; info!("Homeserver listening on pubky://{}", keypair.public_key()); - Ok(Self { - tasks, - config, - port, - }) + Ok(Self { tasks, state }) } /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. @@ -93,11 +90,11 @@ impl Homeserver { // === Getters === pub fn port(&self) -> u16 { - self.port + self.state.port } pub fn public_key(&self) -> PublicKey { - self.config.keypair().public_key() + self.state.config.keypair().public_key() } // === Public Methods ===