chore(homeserver): Refactor config (#91)

* first draft

* config2 for the time being

* more refactoring

* write default config if it doesnt exist

* added relays to config

* some refactor

* proper bootstrap nodes and relay config validation

* small comments

* rename module

* renamings

* turn listen_ports to listen_socket

* connected config with homeserver

* cleaned up old config

* cleaned up config_old

* removed old config.example.toml

* cleanup tryfrom conversions

* removed dirs-next

* review cleanup

* extracted default config to its own toml file

* use hostname_validator for rfc1123 domain verification

* Domain struct

* fmt

* small config restructure

* use SignupMode in config and moved it to config dir
This commit is contained in:
Severin Alexander Bühler
2025-03-25 09:44:05 +02:00
committed by GitHub
parent 6f94333101
commit 5eb61d589b
19 changed files with 1032 additions and 477 deletions

51
Cargo.lock generated
View File

@@ -713,6 +713,15 @@ dependencies = [
"subtle",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -723,6 +732,18 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users 0.5.0",
"windows-sys 0.59.0",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -730,7 +751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"redox_users 0.4.6",
"winapi",
]
@@ -1220,6 +1241,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hostname-validator"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
[[package]]
name = "http"
version = "1.2.0"
@@ -1872,6 +1899,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "overload"
version = "0.1.1"
@@ -2232,11 +2265,12 @@ dependencies = [
"base32",
"bytes",
"clap",
"dirs-next",
"dirs",
"flume",
"futures-util",
"heed",
"hex",
"hostname-validator",
"httpdate",
"page_size",
"pkarr",
@@ -2244,6 +2278,7 @@ dependencies = [
"postcard",
"pubky-common",
"serde",
"tempfile",
"thiserror 2.0.12",
"tokio",
"toml",
@@ -2462,6 +2497,17 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -3448,6 +3494,7 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]

View File

@@ -25,7 +25,6 @@ axum-extra = { version = "0.10.0", features = [
base32 = "0.5.1"
bytes = "^1.10.0"
clap = { version = "4.5.29", features = ["derive"] }
dirs-next = "2.0.0"
flume = "0.11.1"
futures-util = "0.3.31"
heed = "0.21.0"
@@ -41,9 +40,15 @@ tower-cookies = "0.11.0"
tower-http = { version = "0.6.2", features = ["cors", "trace"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
url = "2.5.4"
url = { version = "2.5.4", features = ["serde"] }
axum-server = { version = "0.7.1", features = ["tls-rustls-no-provider"] }
tower = "0.5.2"
page_size = "0.6.0"
pkarr-republisher = { version = "0.1.0", path = "../pkarr-republisher" }
thiserror = "2.0.12"
dirs = "6.0.0"
hostname-validator = "1.1.1"
[dev-dependencies]
tempfile = "3.10.1"

View File

@@ -0,0 +1,60 @@
[general]
# The mode for the signup. Default: "token_required" Options:
# "open" - anyone can signup.
# "token_required" - a signup token is required to signup.
signup_mode = "token_required"
[drive]
# The port number to run an HTTPS (Pkarr TLS) server on.
# Pkarr TLS is a TLS implementation that is compatible with the Pkarr protocol.
# No need to provide a ICANN TLS certificate.
pubky_listen_socket = "127.0.0.1:6287"
# The port number to run an HTTP (clear text) server on.
# Used for http requests from regular browsers.
# May be put behind a reverse proxy with TLS enabled.
icann_listen_socket = "127.0.0.1:6286"
# An ICANN domain name is necessary to support legacy browsers
#
# Make sure to setup a domain name and point it the IP
# address of this machine where you are running this server.
#
# This domain should point to the `<public_ip>:<public_port>`.
#
# ICANN TLS is not natively supported, so you should be running
# a reverse proxy and managing certificates yourself.
icann_domain = "example.com"
[admin]
# The port number to run the admin HTTP (clear text) server on.
# Used for admin requests from the admin UI.
listen_socket = "127.0.0.1:6288"
# The password for the admin user to access the admin UI.
admin_password = "admin"
[pkdns]
# The public IP address and port of the homeserver pubky_drive_api to be advertised on the DHT.
# Must be set to be reachable from the outside.
public_socket = "127.0.0.1:6286"
# The interval at which user keys are republished to the DHT.
user_keys_republisher_interval = 14400 # 4 hours in seconds
# List of bootstrap nodes for the DHT.
# domain:port format.
dht_bootstrap_nodes = [
"router.bittorrent.com:6881",
"dht.transmissionbt.com:6881",
"dht.libtorrent.org:25401",
"relay.pkarr.org:6881"
]
# Relay node urls for the DHT.
# Improves the availability of pkarr packets.
dht_relay_nodes = [
"https://relay.pkarr.org",
"https://pkarr.pubky.org"
]

View File

@@ -1,52 +0,0 @@
# Secret key (in hex) to generate the Homeserver's Keypair
# secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
# The interval at which user keys are republished to the DHT.
user_keys_republisher_interval = 14400 # 4 hour in seconds
[admin]
# Set an admin password to protect admin endpoints.
# If no password is set, the admin endpoints will not be accessible.
password = "admin"
# Set signup_mode to "open" to allow anyone to signup a new user,
# otherwise, "token_required" (the default) to require a valid invite token on signup;
signup_mode = "token_required"
[database]
# Storage directory Defaults to <System's Data Directory>
#
# Storage path can be relative or absolute.
storage = "./storage/"
[io]
# The port number to run an HTTP (clear text) server on.
http_port = 6286
# The port number to run an HTTPs (Pkarr TLS) server on.
https_port = 6287
# The public IP of this server.
#
# This address will be mentioned in the Pkarr records of this
# Homeserver that is published on its public key (derivde from `secret_key`)
public_ip = "127.0.0.1"
# If you are running this server behind a reverse proxy,
# you need to provide some extra configurations.
[io.reverse_proxy]
# The public port should be mapped to the `io::https_port`
# and you should setup tcp forwarding (don't terminate TLS on that port).
public_port = 6287
# If you want your server to be accessible from legacy browsers,
# you need to provide some extra configurations.
[io.legacy_browsers]
# An ICANN domain name is necessary to support legacy browsers
#
# Make sure to setup a domain name and point it the IP
# address of this machine where you are running this server.
#
# This domain should point to the `<public_ip>:<http_port>`.
#
# Currently we don't support ICANN TLS, so you should be running
# a reverse proxy and managing certificates there for this endpoint.
domain = "example.com"

View File

@@ -1,349 +0,0 @@
//! Configuration for the server
use anyhow::{anyhow, Context, Result};
use pkarr::Keypair;
use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
fs,
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
};
use crate::{
core::{AdminConfig, CoreConfig, SignupMode},
io::IoConfig,
};
pub const DEFAULT_REPUBLISHER_INTERVAL: u64 = 4 * 60 * 60; // 4 hours in seconds
// === Core ==
pub const DEFAULT_STORAGE_DIR: &str = "pubky";
pub const DEFAULT_MAP_SIZE: usize = 10995116277760; // 10TB (not = disk-space used)
pub const DEFAULT_LIST_LIMIT: u16 = 100;
pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000;
// === IO ===
pub const DEFAULT_HTTP_PORT: u16 = 6286;
pub const DEFAULT_HTTPS_PORT: u16 = 6287;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct DatabaseToml {
storage: Option<PathBuf>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
struct ReverseProxyToml {
pub public_port: Option<u16>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct LegacyBrowsersTompl {
pub domain: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
struct AdminToml {
pub password: Option<String>,
/// "open" or "token_required" (defaults to "token_required", i.e., a signup token is required)
pub signup_mode: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
struct IoToml {
pub http_port: Option<u16>,
pub https_port: Option<u16>,
pub public_ip: Option<IpAddr>,
pub reverse_proxy: Option<ReverseProxyToml>,
pub legacy_browsers: Option<LegacyBrowsersTompl>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct ConfigToml {
secret_key: Option<String>,
database: Option<DatabaseToml>,
io: Option<IoToml>,
admin: Option<AdminToml>,
}
/// Server configuration
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
/// Server keypair.
///
/// Defaults to a random keypair.
pub keypair: Keypair,
pub io: IoConfig,
pub core: CoreConfig,
pub admin: AdminConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
keypair: Keypair::random(),
io: IoConfig::default(),
core: CoreConfig::default(),
admin: AdminConfig::default(),
}
}
}
impl Config {
fn try_from_str(value: &str) -> Result<Self> {
let config_toml: ConfigToml = toml::from_str(value)?;
config_toml.try_into()
}
/// Load the config from a file.
pub async fn load(path: impl AsRef<Path>) -> Result<Config> {
let config_file_path = path.as_ref();
let s = tokio::fs::read_to_string(config_file_path)
.await
.with_context(|| format!("failed to read {}", path.as_ref().to_string_lossy()))?;
let mut config = Config::try_from_str(&s)?;
// support relative path.
if config.core.storage.is_relative() {
config.core.storage = config_file_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(config.core.storage.clone());
}
fs::create_dir_all(&config.core.storage)?;
config.core.storage = config.core.storage.canonicalize()?;
Ok(config)
}
/// Create test configurations
pub fn test(bootstrap: &[String]) -> Self {
let bootstrap = Some(bootstrap.to_vec());
Self {
io: IoConfig {
bootstrap,
http_port: 0,
https_port: 0,
..Default::default()
},
core: CoreConfig::test(),
admin: AdminConfig::test(),
..Default::default()
}
}
}
impl TryFrom<ConfigToml> for Config {
type Error = anyhow::Error;
fn try_from(value: ConfigToml) -> std::result::Result<Self, Self::Error> {
let keypair = if let Some(secret_key) = value.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) = value.database.as_ref().and_then(|db| db.storage.clone()) {
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 io = if let Some(io) = value.io {
IoConfig {
http_port: io.http_port.unwrap_or(DEFAULT_HTTP_PORT),
https_port: io.https_port.unwrap_or(DEFAULT_HTTPS_PORT),
domain: io.legacy_browsers.and_then(|l| l.domain),
public_addr: io.public_ip.map(|ip| {
SocketAddr::from((
ip,
io.reverse_proxy
.and_then(|r| r.public_port)
.unwrap_or(io.https_port.unwrap_or(0)),
))
}),
..Default::default()
}
} else {
IoConfig {
http_port: DEFAULT_HTTP_PORT,
https_port: DEFAULT_HTTPS_PORT,
..Default::default()
}
};
let admin = if let Some(admin_toml) = value.admin {
AdminConfig {
password: admin_toml.password.clone(),
signup_mode: match admin_toml.signup_mode.as_deref() {
Some("open") => SignupMode::Open,
_ => SignupMode::TokenRequired,
},
}
} else {
AdminConfig::default()
};
Ok(Config {
keypair,
io,
core: CoreConfig {
storage,
..Default::default()
},
admin,
})
}
}
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"))?;
if bytes.len() != 32 {
return Err(anyhow!(format!(
"secret_key in config.toml should be 32 bytes in hex (64 characters), got: {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty() {
let config = Config::try_from_str("").unwrap();
assert_eq!(
config,
Config {
keypair: config.keypair.clone(),
..Default::default()
}
)
}
#[tokio::test]
async fn config_load() {
let crate_dir = std::env::current_dir().unwrap();
let config_file_path = crate_dir.join("./src/config.example.toml");
let canonical_file_path = config_file_path.canonicalize().unwrap();
let config = Config::load(canonical_file_path).await.unwrap();
assert!(config
.core
.storage
.ends_with("pubky-homeserver/src/storage/homeserver"));
}
#[test]
fn config_test() {
let config = Config::test(&[]);
assert_eq!(
config,
Config {
keypair: config.keypair.clone(),
io: IoConfig {
bootstrap: Some(vec![]),
http_port: 0,
https_port: 0,
..Default::default()
},
core: CoreConfig {
db_map_size: 10485760,
storage: config.core.storage.clone(),
..Default::default()
},
admin: AdminConfig {
password: Some("admin".to_string()),
signup_mode: SignupMode::Open
}
}
)
}
#[test]
fn parse() {
let config = Config::try_from_str(
r#"
# Secret key (in hex) to generate the Homeserver's Keypair
secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
[database]
# Storage directory Defaults to <System's Data Directory>
# storage = ""
[io]
# The port number to run an HTTP (clear text) server on.
http_port = 6286
# The port number to run an HTTPs (Pkarr TLS) server on.
https_port = 6287
# The public IP of this server.
#
# This address will be mentioned in the Pkarr records of this
# Homeserver that is published on its public key (derivde from `secret_key`)
public_ip = "127.0.0.1"
# If you are running this server behind a reverse proxy,
# you need to provide some extra configurations.
[io.reverse_proxy]
# The public port should be mapped to the `io::https_port`
# and you should setup tcp forwarding (don't terminate TLS on that port).
public_port = 6287
# If you want your server to be accessible from legacy browsers,
# you need to provide some extra configurations.
[io.legacy_browsers]
# An ICANN domain name is necessary to support legacy browsers
#
# Make sure to setup a domain name and point it the IP
# address of this machine where you are running this server.
#
# This domain should point to the `<public_ip>:<http_port>`.
#
# Currently we don't support ICANN TLS, so you should be running
# a reverse proxy and managing certificates there for this endpoint.
domain = "example.com"
"#,
)
.unwrap();
assert_eq!(config.keypair, Keypair::from_secret_key(&[0; 32]));
assert_eq!(config.io.https_port, 6287);
assert_eq!(
config.io.public_addr,
Some(SocketAddr::from(([127, 0, 0, 1], 6287)))
);
assert_eq!(config.io.domain, Some("example.com".to_string()));
}
}

View File

@@ -1,17 +1,20 @@
use std::{path::PathBuf, time::Duration};
use crate::core::database::DB;
use crate::core::user_keys_republisher::UserKeysRepublisher;
use crate::SignupMode;
use anyhow::Result;
use axum::Router;
use pubky_common::auth::AuthVerifier;
use tokio::time::sleep;
use crate::config::{
DEFAULT_LIST_LIMIT, DEFAULT_MAP_SIZE, DEFAULT_MAX_LIST_LIMIT, DEFAULT_REPUBLISHER_INTERVAL,
DEFAULT_STORAGE_DIR,
};
pub const DEFAULT_REPUBLISHER_INTERVAL: u64 = 4 * 60 * 60; // 4 hours in seconds
use crate::core::database::DB;
pub const DEFAULT_STORAGE_DIR: &str = "pubky";
pub const DEFAULT_MAP_SIZE: usize = 10995116277760; // 10TB (not = disk-space used)
pub const DEFAULT_LIST_LIMIT: u16 = 100;
pub const DEFAULT_MAX_LIST_LIMIT: u16 = 1000;
#[derive(Clone, Debug)]
pub(crate) struct AppState {
@@ -74,13 +77,6 @@ impl HomeserverCore {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SignupMode {
Open,
#[default]
TokenRequired,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AdminConfig {
/// The password used to authorize admin endpoints.
@@ -156,17 +152,16 @@ impl CoreConfig {
}
}
pub fn storage(storage: Option<String>) -> Result<PathBuf> {
let dir = if let Some(storage) = storage {
PathBuf::from(storage)
pub fn storage(storage: Option<String>) -> anyhow::Result<PathBuf> {
if let Some(storage) = storage {
Ok(PathBuf::from(storage))
} else {
let path = dirs_next::data_dir().ok_or_else(|| {
anyhow::anyhow!("operating environment provides no directory for application data")
})?;
path.join(DEFAULT_STORAGE_DIR)
};
Ok(dir.join("homeserver"))
dirs::home_dir()
.map(|dir| dir.join(".pubky/data/lmdb"))
.ok_or_else(|| {
anyhow::anyhow!("operating environment provides no directory for application data")
})
}
}
#[cfg(test)]

View File

@@ -1,7 +1,10 @@
use crate::core::{
database::tables::users::User,
error::{Error, Result},
AppState, SignupMode,
use crate::{
core::{
database::tables::users::User,
error::{Error, Result},
AppState,
},
SignupMode,
};
use axum::{
extract::{Query, State},

View File

@@ -0,0 +1,251 @@
//!
//! Configuration file for the homeserver.
//!
use super::{domain_port::DomainPort, SignupMode};
use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
net::{IpAddr, Ipv4Addr, SocketAddr},
num::NonZeroU64,
str::FromStr,
};
use url::Url;
/// Default TOML configuration for the homeserver.
/// This is used to create a default config file if it doesn't exist.
/// Why not use the Default trait? The `toml` crate doesn't support adding comments.
/// So we maintain this default manually.
pub const DEFAULT_CONFIG: &str = include_str!("../../config.default.toml");
/// All configuration related to the DHT
/// and /pkarr.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct PkdnsToml {
/// The public IP address and port of the server to be advertised in the DHT.
#[serde(default = "default_public_socket")]
pub public_socket: SocketAddr,
/// The interval at which the user keys are republished in the DHT.
#[serde(default = "default_user_keys_republisher_interval")]
pub user_keys_republisher_interval: NonZeroU64,
/// The list of bootstrap nodes for the DHT. If None, the default pkarr bootstrap nodes will be used.
#[serde(default = "default_dht_bootstrap_nodes")]
pub dht_bootstrap_nodes: Option<Vec<DomainPort>>,
/// The list of relay nodes for the DHT. If None, the default pkarr relay nodes will be used.
#[serde(default = "default_dht_relay_nodes")]
pub dht_relay_nodes: Option<Vec<Url>>,
}
fn default_public_socket() -> SocketAddr {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let port = 6286;
SocketAddr::from((ip, port))
}
fn default_dht_bootstrap_nodes() -> Option<Vec<DomainPort>> {
None
}
fn default_dht_relay_nodes() -> Option<Vec<Url>> {
None
}
fn default_user_keys_republisher_interval() -> NonZeroU64 {
// 4 hours
NonZeroU64::new(14400).expect("14400 is a valid non-zero u64")
}
fn default_pubky_drive_listen_socket() -> SocketAddr {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let port = 6287;
SocketAddr::from((ip, port))
}
/// All configuration related to file drive
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct DriveToml {
/// The port on which the Pubky TLS Drive API will listen.
#[serde(default = "default_pubky_drive_listen_socket")]
pub pubky_listen_socket: SocketAddr,
/// The port on which the regular http API will listen.
#[serde(default = "default_icann_drive_listen_socket")]
pub icann_listen_socket: SocketAddr,
/// Optional domain name of the regular http API.
#[serde(default = "default_icann_drive_domain")]
pub icann_domain: Option<String>,
}
fn default_icann_drive_listen_socket() -> SocketAddr {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let port = 6286;
SocketAddr::from((ip, port))
}
fn default_icann_drive_domain() -> Option<String> {
None
}
/// All configuration related to the admin API
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct AdminToml {
/// The socket on which the admin API will listen.
#[serde(default = "default_admin_listen_socket")]
pub listen_socket: SocketAddr,
/// The password for the admin API.
#[serde(default = "default_admin_password")]
pub admin_password: String,
}
fn default_admin_password() -> String {
"admin".to_string()
}
fn default_admin_listen_socket() -> SocketAddr {
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let port = 6288;
SocketAddr::from((ip, port))
}
/// All configuration related to the admin API
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct GeneralToml {
/// The mode of the signup.
#[serde(default = "default_signup_mode")]
pub signup_mode: SignupMode,
}
fn default_signup_mode() -> SignupMode {
SignupMode::TokenRequired
}
/// The error that can occur when reading the config file
#[derive(Debug, thiserror::Error)]
pub enum ConfigReadError {
/// The config file not found
#[error("Config file not found. {0}")]
ConfigFileNotFound(#[from] std::io::Error),
/// The config file is not valid
#[error("Config file is not valid. {0}")]
ConfigFileNotValid(#[from] toml::de::Error),
}
/// The main server configuration
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ConfigToml {
/// The configuration for the general settings.
pub general: GeneralToml,
/// The configuration for the drive files.
pub drive: DriveToml,
/// The configuration for the admin API.
pub admin: AdminToml,
/// The configuration for the pkdns.
pub pkdns: PkdnsToml,
}
impl ConfigToml {
/// Reads the configuration from a TOML file at the specified path.
///
/// # Arguments
/// * `path` - The path to the TOML configuration file
///
/// # Returns
/// * `Result<ConfigToml>` - The parsed configuration or an error if reading/parsing fails
pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigReadError> {
let contents = std::fs::read_to_string(path)?;
let config: ConfigToml = contents.parse()?;
Ok(config)
}
/// Returns the default config with all variables commented out.
pub fn default_string() -> String {
// Comment out all variables so they are not fixed by default.
DEFAULT_CONFIG
.split("\n")
.map(|line| {
let is_not_commented_variable =
!line.starts_with("#") && !line.starts_with("[") && line.is_empty();
if is_not_commented_variable {
format!("# {}", line)
} else {
line.to_string()
}
})
.collect::<Vec<String>>()
.join("\n")
}
}
impl Default for ConfigToml {
fn default() -> Self {
DEFAULT_CONFIG
.parse()
.expect("Default config is always valid")
}
}
impl FromStr for ConfigToml {
type Err = toml::de::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let config: ConfigToml = toml::from_str(s)?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let c: ConfigToml = ConfigToml::default();
assert_eq!(c.general.signup_mode, SignupMode::TokenRequired);
assert_eq!(
c.drive.icann_listen_socket,
default_icann_drive_listen_socket()
);
assert_eq!(c.drive.icann_domain, Some("example.com".to_string()));
assert_eq!(
c.drive.pubky_listen_socket,
default_pubky_drive_listen_socket()
);
assert_eq!(c.admin.listen_socket, default_admin_listen_socket());
assert_eq!(c.admin.admin_password, default_admin_password());
// Verify pkdns config
assert_eq!(c.pkdns.public_socket, default_public_socket());
assert_eq!(
c.pkdns.user_keys_republisher_interval,
default_user_keys_republisher_interval()
);
assert_eq!(
c.pkdns.dht_bootstrap_nodes,
Some(vec![
DomainPort::from_str("router.bittorrent.com:6881").unwrap(),
DomainPort::from_str("dht.transmissionbt.com:6881").unwrap(),
DomainPort::from_str("dht.libtorrent.org:25401").unwrap(),
DomainPort::from_str("relay.pkarr.org:6881").unwrap(),
])
);
assert_eq!(
c.pkdns.dht_relay_nodes,
Some(vec![
Url::parse("https://relay.pkarr.org").unwrap(),
Url::parse("https://pkarr.pubky.org").unwrap(),
])
);
}
#[test]
fn test_default_config_commented_out() {
// Sanity check that the default config is valid
// even when the variables are commented out.
let s = ConfigToml::default_string();
let _: ConfigToml = s.parse().expect("Failed to parse config");
}
}

View File

@@ -0,0 +1,227 @@
use super::ConfigToml;
use std::{io::Write, os::unix::fs::PermissionsExt, path::PathBuf};
/// The data directory for the homeserver.
///
/// This is the directory that will store the homeservers data.
///
#[derive(Debug, Clone)]
pub struct DataDir {
expanded_path: PathBuf,
}
impl DataDir {
/// Creates a new data directory.
/// `path` will be expanded to the home directory if it starts with "~".
pub fn new(path: PathBuf) -> Self {
Self {
expanded_path: Self::expand_home_dir(path),
}
}
/// Returns the full path to the data directory.
pub fn path(&self) -> &PathBuf {
&self.expanded_path
}
/// Expands the data directory to the home directory if it starts with "~".
/// Return the full path to the data directory.
fn expand_home_dir(path: PathBuf) -> PathBuf {
let path = match path.to_str() {
Some(path) => path,
None => {
// Path not valid utf-8 so we can't expand it.
return path;
}
};
if path.starts_with("~/") {
if let Some(home) = dirs::home_dir() {
let without_home = path.strip_prefix("~/").expect("Invalid ~ prefix");
let joined = home.join(without_home);
return joined;
}
}
PathBuf::from(path)
}
/// Makes sure the data directory exists.
/// Create the directory if it doesn't exist.
pub fn ensure_data_dir_exists_and_is_writable(&self) -> anyhow::Result<()> {
std::fs::create_dir_all(&self.expanded_path)?;
// Check if we can write to the data directory
let test_file_path = self
.expanded_path
.join("test_write_f2d560932f9b437fa9ef430ba436d611"); // random file name to not conflict with anything
std::fs::write(test_file_path.clone(), b"test")
.map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
std::fs::remove_file(test_file_path)
.map_err(|err| anyhow::anyhow!("Failed to write to data directory: {}", err))?;
Ok(())
}
/// Returns the config file path in this directory.
pub fn get_config_file_path(&self) -> PathBuf {
self.expanded_path.join("config.toml")
}
/// Reads the config file from the data directory.
/// Creates a default config file if it doesn't exist.
pub fn read_or_create_config_file(&self) -> anyhow::Result<ConfigToml> {
let config_file_path = self.get_config_file_path();
if !config_file_path.exists() {
self.write_default_config_file()?;
}
let config = ConfigToml::from_file(config_file_path)?;
Ok(config)
}
fn write_default_config_file(&self) -> anyhow::Result<()> {
let config_string = ConfigToml::default_string();
let config_file_path = self.get_config_file_path();
let mut config_file = std::fs::File::create(config_file_path)?;
config_file.write_all(config_string.as_bytes())?;
Ok(())
}
/// Returns the path to the secret file.
pub fn get_secret_file_path(&self) -> PathBuf {
self.expanded_path.join("secret")
}
/// Reads the secret file. Creates a new secret file if it doesn't exist.
pub fn read_or_create_keypair(&self) -> anyhow::Result<pkarr::Keypair> {
let secret_file_path = self.get_secret_file_path();
if !secret_file_path.exists() {
// Create a new secret file
let keypair = pkarr::Keypair::random();
let secret = keypair.secret_key();
let hex_string = hex::encode(secret);
std::fs::write(secret_file_path.clone(), hex_string)?;
std::fs::set_permissions(&secret_file_path, std::fs::Permissions::from_mode(0o600))?;
tracing::info!("Secret file created at {}", secret_file_path.display());
}
// Read the secret file
let secret = std::fs::read(secret_file_path)?;
let secret_bytes = hex::decode(secret)?;
let secret_bytes: [u8; 32] = secret_bytes.try_into().map_err(|_| {
anyhow::anyhow!("Failed to convert secret bytes into array of length 32")
})?;
let keypair = pkarr::Keypair::from_secret_key(&secret_bytes);
Ok(keypair)
}
}
impl Default for DataDir {
fn default() -> Self {
Self::new(PathBuf::from("~/.pubky"))
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use super::*;
use tempfile::TempDir;
/// Test that the home directory is expanded correctly.
#[test]
pub fn test_expand_home_dir() {
let data_dir = DataDir::new(PathBuf::from("~/.pubky"));
let homedir = dirs::home_dir().unwrap();
let expanded_path = homedir.join(".pubky");
assert_eq!(data_dir.expanded_path, expanded_path);
}
/// Test that the data directory is created if it doesn't exist.
#[test]
pub fn test_ensure_data_dir_exists_and_is_accessible() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
assert!(test_path.exists());
// temp_dir will be automatically cleaned up when it goes out of scope
}
#[test]
pub fn test_get_default_config_file_path_exists() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let config_file_path = data_dir.get_config_file_path();
assert!(!config_file_path.exists()); // Should not exist yet
let mut config_file = std::fs::File::create(config_file_path.clone()).unwrap();
config_file.write_all(b"test").unwrap();
assert!(config_file_path.exists()); // Should exist now
// temp_dir will be automatically cleaned up when it goes out of scope
}
#[test]
pub fn test_read_or_create_config_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let _ = data_dir.read_or_create_config_file().unwrap(); // Should create a default config file
assert!(data_dir.get_config_file_path().exists());
let _ = data_dir.read_or_create_config_file().unwrap(); // Should read the existing file
assert!(data_dir.get_config_file_path().exists());
}
#[test]
pub fn test_read_or_create_config_file_dont_override_existing_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
// Write a broken config file
let config_file_path = data_dir.get_config_file_path();
std::fs::write(config_file_path.clone(), b"test").unwrap();
assert!(config_file_path.exists()); // Should exist now
// Try to read the config file and fail because config is broken
let read_result = data_dir.read_or_create_config_file();
assert!(read_result.is_err());
// Make sure the broken config file is still there
let content = std::fs::read_to_string(config_file_path).unwrap();
assert_eq!(content, "test");
}
#[test]
pub fn test_create_secret_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
let _ = data_dir.read_or_create_keypair().unwrap();
assert!(data_dir.get_secret_file_path().exists());
}
#[test]
pub fn test_dont_override_existing_secret_file() {
let temp_dir = TempDir::new().unwrap();
let test_path = temp_dir.path().join(".pubky");
let data_dir = DataDir::new(test_path.clone());
data_dir.ensure_data_dir_exists_and_is_writable().unwrap();
// Create a secret file
let secret_file_path = data_dir.get_secret_file_path();
std::fs::write(secret_file_path.clone(), b"test").unwrap();
let result = data_dir.read_or_create_keypair();
assert!(result.is_err());
assert!(data_dir.get_secret_file_path().exists());
let content = std::fs::read_to_string(secret_file_path).unwrap();
assert_eq!(content, "test");
}
}

View File

@@ -0,0 +1,106 @@
use std::fmt::{self, Display};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
/// Validated domain name according to RFC 1123.
#[derive(Debug, Clone, PartialEq)]
pub struct Domain(pub String);
impl Domain {
/// Create a new domain from a string.
pub fn new(domain: String) -> Result<Self, anyhow::Error> {
Self::is_valid_domain(&domain)?;
Ok(Self(domain))
}
/// Validate a domain name according to RFC 1123
pub fn is_valid_domain(domain: &str) -> anyhow::Result<()> {
// Check if it's a valid hostname according to RFC 1123
if !hostname_validator::is_valid(domain) {
return Err(anyhow::anyhow!(
"Invalid domain '{}': is not a valid RFC 1123 hostname",
domain
));
}
Ok(())
}
}
impl FromStr for Domain {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::is_valid_domain(s)?;
Ok(Self(s.to_string()))
}
}
impl Display for Domain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for Domain {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Domain {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_domain_validation() {
// Test valid domains
let valid_domains = [
"example.com",
"sub.example.com",
"a.b.c.d",
"valid-domain.com",
"valid.domain-name.com",
"localhost",
"test.local",
];
for domain in valid_domains {
let result: anyhow::Result<Domain> = domain.parse();
assert!(result.is_ok(), "Domain '{}' should be valid", domain);
}
// Test invalid domains
let invalid_domains = [
("invalid@domain.com", "contains invalid characters"),
("domain..com", "contains consecutive dots"),
(".domain.com", "starts with a dot"),
("domain.com.", "ends with a dot"),
("-domain.com", "starts with a hyphen"),
("domain.com-", "ends with a hyphen"),
];
for (domain, reason) in invalid_domains {
let result: anyhow::Result<Domain> = domain.parse();
assert!(
result.is_err(),
"Domain '{}' should be invalid: {}",
domain,
reason
);
}
}
}

View File

@@ -0,0 +1,91 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::result::Result;
use std::str::FromStr;
use super::domain::Domain;
/// A domain and port pair.
#[derive(Debug, Clone, PartialEq)]
pub struct DomainPort {
/// The domain name.
pub domain: Domain,
/// The port number.
pub port: u16,
}
impl TryFrom<&str> for DomainPort {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::from_str(s)
}
}
impl fmt::Display for DomainPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.domain, self.port)
}
}
impl FromStr for DomainPort {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!(
"Invalid domain:port format. Expected 'domain:port'"
));
}
let part0 = parts[0];
let domain = part0.parse::<Domain>()?;
let port = parts[1].parse::<u16>()?;
Ok(Self { domain, port })
}
}
impl Serialize for DomainPort {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for DomainPort {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_domain_port_from_str() {
let domain_port = DomainPort::from_str("example.com:6286").unwrap();
assert_eq!(domain_port.domain.to_string(), "example.com");
assert_eq!(domain_port.port, 6286);
}
#[test]
fn test_domain_port_from_str_invalid1() {
let domain_port = DomainPort::from_str("example.com");
assert!(domain_port.is_err());
}
#[test]
fn test_domain_port_from_str_invalid2() {
let domain_port = DomainPort::from_str("example..com:80");
assert!(domain_port.is_err());
}
}

View File

@@ -0,0 +1,11 @@
mod config_toml;
mod data_dir;
mod domain;
mod domain_port;
mod signup_mode;
pub use config_toml::{ConfigReadError, ConfigToml};
pub use data_dir::DataDir;
pub use domain::Domain;
pub use domain_port::DomainPort;
pub use signup_mode::SignupMode;

View File

@@ -0,0 +1,74 @@
use core::fmt;
use std::{fmt::Display, str::FromStr};
use serde::{Deserialize, Serialize};
/// The mode of signup.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SignupMode {
/// Everybody can signup.
Open,
/// Only users with a valid token can signup.
#[default]
TokenRequired,
}
impl Display for SignupMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Open => write!(f, "open"),
Self::TokenRequired => write!(f, "token_required"),
}
}
}
impl FromStr for SignupMode {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"open" => Self::Open,
"token_required" => Self::TokenRequired,
_ => return Err(anyhow::anyhow!("Invalid signup mode: {}", s)),
})
}
}
impl Serialize for SignupMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
impl<'de> Deserialize<'de> for SignupMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::from_str(&s).unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signup_mode_from_str() {
assert_eq!(SignupMode::from_str("open").unwrap(), SignupMode::Open);
assert_eq!(
SignupMode::from_str("token_required").unwrap(),
SignupMode::TokenRequired
);
}
#[test]
fn test_signup_mode_display() {
assert_eq!(SignupMode::Open.to_string(), "open");
assert_eq!(SignupMode::TokenRequired.to_string(), "token_required");
}
}

View File

@@ -0,0 +1,5 @@
mod http;
mod key_republisher;
mod server;
pub use server::*;

View File

@@ -1,22 +1,16 @@
use std::{
net::SocketAddr,
path::{Path, PathBuf},
time::Duration,
};
use std::{net::SocketAddr, path::PathBuf, time::Duration};
use ::pkarr::{Keypair, PublicKey};
use super::http::HttpServers;
use super::key_republisher::HomeserverKeyRepublisher;
use crate::{data_directory::DataDir, SignupMode};
use anyhow::Result;
use homeserver_key_republisher::HomeserverKeyRepublisher;
use http::HttpServers;
use pkarr::{Keypair, PublicKey};
use tracing::info;
use crate::{
config::{Config, DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT},
core::{HomeserverCore, SignupMode},
};
use crate::core::{AdminConfig, CoreConfig, HomeserverCore};
mod homeserver_key_republisher;
mod http;
pub const DEFAULT_HTTP_PORT: u16 = 6286;
pub const DEFAULT_HTTPS_PORT: u16 = 6287;
#[derive(Debug, Default)]
/// Builder for [Homeserver].
@@ -88,7 +82,7 @@ impl HomeserverBuilder {
pub struct Homeserver {
http_servers: HttpServers,
keypair: Keypair,
pkarr_server: HomeserverKeyRepublisher,
key_republisher: HomeserverKeyRepublisher,
}
impl Homeserver {
@@ -97,13 +91,11 @@ impl Homeserver {
HomeserverBuilder::default()
}
/// Run a Homeserver with a configuration file path.
///
/// # Safety
/// Homeserver uses LMDB, [opening][heed::EnvOpenOptions::open] which is marked unsafe,
/// because the possible Undefined Behavior (UB) if the lock file is broken.
pub async fn run_with_config_file(config_path: impl AsRef<Path>) -> Result<Self> {
unsafe { Self::run(Config::load(config_path).await?) }.await
/// Run the homeserver with configurations from a data directory.
pub async fn run_with_data_dir(dir_path: PathBuf) -> Result<Self> {
let data_dir = DataDir::new(dir_path);
let config = Config::try_from(data_dir)?;
unsafe { Self::run(config) }.await
}
/// Run a Homeserver with configurations suitable for ephemeral tests.
@@ -152,7 +144,7 @@ impl Homeserver {
Ok(Self {
http_servers,
keypair,
pkarr_server: dht_republisher,
key_republisher: dht_republisher,
})
}
@@ -173,7 +165,7 @@ impl Homeserver {
/// Send a shutdown signal to all open resources
pub async fn shutdown(&self) {
self.http_servers.shutdown();
self.pkarr_server.stop_periodic_republish().await;
self.key_republisher.stop_periodic_republish().await;
}
}
@@ -203,3 +195,85 @@ impl Default for IoConfig {
}
}
}
/// Server configuration
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
/// Server keypair.
///
/// Defaults to a random keypair.
pub keypair: Keypair,
pub io: IoConfig,
pub core: CoreConfig,
pub admin: AdminConfig,
}
impl Config {
/// Create test configurations
pub fn test(bootstrap: &[String]) -> Self {
let bootstrap = Some(bootstrap.to_vec());
Self {
io: IoConfig {
bootstrap,
http_port: 0,
https_port: 0,
..Default::default()
},
core: CoreConfig::test(),
admin: AdminConfig::test(),
..Default::default()
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
keypair: Keypair::random(),
io: IoConfig::default(),
core: CoreConfig::default(),
admin: AdminConfig::default(),
}
}
}
impl TryFrom<DataDir> for Config {
type Error = anyhow::Error;
fn try_from(dir: DataDir) -> Result<Self, Self::Error> {
dir.ensure_data_dir_exists_and_is_writable()?;
let conf = dir.read_or_create_config_file()?;
let keypair = dir.read_or_create_keypair()?;
// TODO: Needs refactoring of the Homeserver Config struct. I am not doing
// it yet because I am concentrating on the config currently.
let io = IoConfig {
http_port: conf.drive.icann_listen_socket.port(),
https_port: conf.drive.pubky_listen_socket.port(),
domain: conf.drive.icann_domain,
public_addr: Some(conf.pkdns.public_socket),
..Default::default()
};
let core = CoreConfig {
storage: dir.path().join("data/lmdb"),
user_keys_republisher_interval: Some(Duration::from_secs(
conf.pkdns.user_keys_republisher_interval.into(),
)),
..Default::default()
};
let admin = AdminConfig {
signup_mode: conf.general.signup_mode,
password: Some(conf.admin.admin_password),
};
Ok(Config {
keypair,
io,
core,
admin,
})
}
}

View File

@@ -5,9 +5,10 @@
#![deny(rustdoc::broken_intra_doc_links)]
#![cfg_attr(any(), deny(clippy::unwrap_used))]
mod config;
mod core;
mod io;
mod data_directory;
mod homeserver;
pub use io::Homeserver;
pub use io::HomeserverBuilder;
pub use data_directory::*;
pub use homeserver::Homeserver;
pub use homeserver::HomeserverBuilder;

View File

@@ -1,19 +1,29 @@
use std::path::PathBuf;
use anyhow::Result;
use pubky_homeserver::Homeserver;
use clap::Parser;
use pubky_homeserver::Homeserver;
use tracing_subscriber::EnvFilter;
fn default_config_dir_path() -> PathBuf {
dirs::home_dir().unwrap_or_default().join(".pubky")
}
/// Validate that the data_dir path is a directory.
/// It doesnt need to exist, but if it does, it needs to be a directory.
fn validate_config_dir_path(path: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(path);
if path.exists() && path.is_file() {
return Err(format!("Given path is not a directory: {}", path.display()));
}
Ok(path)
}
#[derive(Parser, Debug)]
struct Cli {
/// [tracing_subscriber::EnvFilter]
#[clap(short, long)]
tracing_env_filter: Option<String>,
/// Optional Path to config file.
#[clap(short, long)]
config: Option<PathBuf>,
/// Path to config file. Defaults to ~/.pubky/config.toml
#[clap(short, long, default_value_os_t = default_config_dir_path(), value_parser = validate_config_dir_path)]
data_dir: PathBuf,
}
#[tokio::main]
@@ -22,18 +32,14 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
args.tracing_env_filter
.unwrap_or("pubky_homeserver=debug,tower_http=debug".to_string()),
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("pubky_homeserver=debug,tower_http=debug")),
)
.init();
let server = unsafe {
if let Some(config_path) = args.config {
Homeserver::run_with_config_file(config_path).await?
} else {
Homeserver::builder().run().await?
}
};
tracing::debug!("Using data dir: {}", args.data_dir.display());
let server = Homeserver::run_with_data_dir(args.data_dir).await?;
tokio::signal::ctrl_c().await?;