Skip to content

Commit

Permalink
feat(homeserver): allow empty config file
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuhvi committed Sep 27, 2024
1 parent d129e4b commit 6897671
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 75 deletions.
186 changes: 120 additions & 66 deletions pubky-homeserver/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use anyhow::{anyhow, Context, Result};
use pkarr::Keypair;
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
path::{Path, PathBuf},
Expand All @@ -18,20 +18,41 @@ const DEFAULT_STORAGE_DIR: &str = "pubky";
pub const DEFAULT_LIST_LIMIT: u16 = 100;
pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000;

#[derive(Serialize, Deserialize, Clone, PartialEq)]
struct ConfigToml {
testnet: Option<bool>,
port: Option<u16>,
bootstrap: Option<Vec<String>>,
domain: Option<String>,
storage: Option<PathBuf>,
secret_key: Option<String>,
dht_request_timeout: Option<Duration>,
default_list_limit: Option<u16>,
max_list_limit: Option<u16>,
}

/// Server configuration
#[derive(Serialize, Deserialize, Clone)]
#[derive(Debug, Clone)]
pub struct Config {
/// Whether or not this server is running in a testnet.
testnet: bool,
/// The configured port for this server.
port: Option<u16>,
/// Bootstrapping DHT nodes.
///
/// Helpful to run the server locally or in testnet.
bootstrap: Option<Vec<String>>,
domain: String,
/// Path to the storage directory
/// A public domain for this server
/// necessary for web browsers running in https environment.
domain: Option<String>,
/// Path to the storage directory.
///
/// Defaults to a directory in the OS data directory
storage: Option<PathBuf>,
#[serde(deserialize_with = "secret_key_deserialize")]
secret_key: Option<[u8; 32]>,

storage: PathBuf,
/// Server keypair.
///
/// Defaults to a random keypair.
keypair: Keypair,
dht_request_timeout: Option<Duration>,
/// The default limit of a list api if no `limit` query parameter is provided.
///
Expand All @@ -44,13 +65,42 @@ pub struct Config {
}

impl Config {
/// Load the config from a file.
pub async fn load(path: impl AsRef<Path>) -> Result<Config> {
let s = tokio::fs::read_to_string(path.as_ref())
.await
.with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
fn try_from_str(value: &str) -> Result<Self> {
let config_toml: ConfigToml = toml::from_str(value)?;

let config: Config = toml::from_str(&s)?;
let keypair = if let Some(secret_key) = config_toml.secret_key {
let secret_key = deserialize_secret_key(secret_key)?;
Keypair::from_secret_key(&secret_key)
} else {
Keypair::random()
};

let storage = {
let dir = if let Some(storage) = config_toml.storage {
storage
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};

dir.join("homeserver")
};

let config = Config {
testnet: config_toml.testnet.unwrap_or(false),
port: config_toml.port,
bootstrap: config_toml.bootstrap,
domain: config_toml.domain,
keypair,
storage,
dht_request_timeout: config_toml.dht_request_timeout,
default_list_limit: config_toml.default_list_limit.unwrap_or(DEFAULT_LIST_LIMIT),
max_list_limit: config_toml
.default_list_limit
.unwrap_or(DEFAULT_MAX_LIST_LIMIT),
};

if config.testnet {
let testnet_config = Config::testnet();
Expand All @@ -64,17 +114,24 @@ impl Config {
Ok(config)
}

/// Load the config from a file.
pub async fn load(path: impl AsRef<Path>) -> Result<Config> {
let s = tokio::fs::read_to_string(path.as_ref())
.await
.with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;

Config::try_from_str(&s)
}

/// Testnet configurations
pub fn testnet() -> Self {
let testnet = pkarr::mainline::Testnet::new(10);
info!(?testnet.bootstrap, "Testnet bootstrap nodes");

let bootstrap = Some(testnet.bootstrap.to_owned());
let storage = Some(
std::env::temp_dir()
.join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR),
);
let storage = std::env::temp_dir()
.join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR);

Self {
bootstrap,
Expand All @@ -88,11 +145,9 @@ impl Config {
/// Test configurations
pub fn test(testnet: &pkarr::mainline::Testnet) -> Self {
let bootstrap = Some(testnet.bootstrap.to_owned());
let storage = Some(
std::env::temp_dir()
.join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR),
);
let storage = std::env::temp_dir()
.join(Timestamp::now().to_string())
.join(DEFAULT_STORAGE_DIR);

Self {
bootstrap,
Expand All @@ -109,12 +164,12 @@ impl Config {
self.bootstrap.to_owned()
}

pub fn domain(&self) -> &str {
pub fn domain(&self) -> &Option<String> {
&self.domain
}

pub fn keypair(&self) -> Keypair {
Keypair::from_secret_key(&self.secret_key.unwrap_or_default())
pub fn keypair(&self) -> &Keypair {
&self.keypair
}

pub fn default_list_limit(&self) -> u16 {
Expand All @@ -126,17 +181,8 @@ impl Config {
}

/// Get the path to the storage directory
pub fn storage(&self) -> Result<PathBuf> {
let dir = if let Some(storage) = &self.storage {
PathBuf::from(storage)
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};

Ok(dir.join("homeserver"))
pub fn storage(&self) -> &PathBuf {
&self.storage
}

pub(crate) fn dht_request_timeout(&self) -> Option<Duration> {
Expand All @@ -150,45 +196,53 @@ impl Default for Config {
testnet: false,
port: Some(0),
bootstrap: None,
domain: "localhost".to_string(),
storage: None,
secret_key: None,
domain: None,
storage: storage(None)
.expect("operating environment provides no directory for application data"),
keypair: Keypair::random(),
dht_request_timeout: None,
default_list_limit: DEFAULT_LIST_LIMIT,
max_list_limit: DEFAULT_MAX_LIST_LIMIT,
}
}
}

fn secret_key_deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
fn deserialize_secret_key(s: String) -> anyhow::Result<[u8; 32]> {
let bytes =
hex::decode(s).map_err(|_| anyhow!("secret_key in config.toml should hex encoded"))?;

match opt {
Some(s) => {
let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(anyhow!(format!(
"secret_key in config.toml should be 32 bytes in hex (64 characters), got: {}",
bytes.len()
)));
}

if bytes.len() != 32 {
return Err(serde::de::Error::custom("Expected a 32-byte array"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);

let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Some(arr))
}
None => Ok(None),
}
Ok(arr)
}

fn storage(storage: Option<String>) -> Result<PathBuf> {
let dir = if let Some(storage) = storage {
PathBuf::from(storage)
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};

Ok(dir.join("homeserver"))
}

impl Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entry(&"testnet", &self.testnet)
.entry(&"port", &self.port())
.entry(&"storage", &self.storage())
.entry(&"public_key", &self.keypair().public_key())
.finish()
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_empty() {
Config::try_from_str("").unwrap();
}
}
6 changes: 2 additions & 4 deletions pubky-homeserver/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@ pub struct DB {

impl DB {
pub fn open(config: Config) -> anyhow::Result<Self> {
let storage = config.storage()?;

fs::create_dir_all(&storage).unwrap();
fs::create_dir_all(config.storage())?;

let env = unsafe {
EnvOpenOptions::new()
.max_dbs(TABLES_COUNT)
// TODO: Add a configuration option?
.map_size(DEFAULT_MAP_SIZE)
.open(storage)
.open(config.storage())
}?;

let tables = migrations::run(&env)?;
Expand Down
21 changes: 16 additions & 5 deletions pubky-homeserver/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ impl Homeserver {
pub async fn start(config: Config) -> Result<Self> {
debug!(?config);

let keypair = config.keypair();

let db = DB::open(config.clone())?;

let pkarr_client = PkarrClient::new(Settings {
Expand All @@ -55,7 +53,7 @@ impl Homeserver {
verifier: AuthVerifier::default(),
db,
pkarr_client,
config,
config: config.clone(),
port,
};

Expand All @@ -73,9 +71,22 @@ impl Homeserver {

info!("Homeserver listening on http://localhost:{port}");

publish_server_packet(&state.pkarr_client, &keypair, state.config.domain(), port).await?;
publish_server_packet(
&state.pkarr_client,
config.keypair(),
&state
.config
.domain()
.clone()
.unwrap_or("localhost".to_string()),
port,
)
.await?;

info!("Homeserver listening on pubky://{}", keypair.public_key());
info!(
"Homeserver listening on pubky://{}",
config.keypair().public_key()
);

Ok(Self { tasks, state })
}
Expand Down

0 comments on commit 6897671

Please sign in to comment.