Merge branch 'main' into feat/feed

This commit is contained in:
nazeh
2024-08-23 10:45:52 +03:00
12 changed files with 329 additions and 76 deletions

82
Cargo.lock generated
View File

@@ -659,6 +659,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
@@ -820,6 +826,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "headers"
version = "0.4.0"
@@ -914,6 +926,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "1.1.0"
@@ -1020,6 +1038,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
@@ -1505,11 +1533,13 @@ dependencies = [
"flume",
"futures-util",
"heed",
"hex",
"pkarr",
"postcard",
"pubky-common",
"serde",
"tokio",
"toml",
"tower-cookies",
"tower-http",
"tracing",
@@ -1780,6 +1810,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -2075,6 +2114,40 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
@@ -2523,6 +2596,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"

View File

@@ -14,11 +14,13 @@ dirs-next = "2.0.0"
flume = "0.11.0"
futures-util = "0.3.30"
heed = "0.20.3"
hex = "0.4.3"
pkarr = { version = "2.1.0", features = ["async"] }
postcard = { version = "1.0.8", features = ["alloc"] }
pubky-common = { version = "0.1.0", path = "../pubky-common" }
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.37.0", features = ["full"] }
toml = "0.8.19"
tower-cookies = "0.10.0"
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
tracing = "0.1.40"

View File

@@ -0,0 +1,23 @@
# Pubky Homeserver
## Usage
Use `cargo run`
```bash
cargo run -- --config=./src/config.toml
```
Or Build first then run from target.
Build
```bash
cargo build --release
```
Run with an optional config file
```bash
../target/release/pubky-homeserver --config=./src/config.toml
```

View File

@@ -1,10 +1,14 @@
//! Configuration for the server
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use pkarr::Keypair;
use serde::{Deserialize, Deserializer, Serialize};
use std::{
fmt::Debug,
path::{Path, PathBuf},
time::Duration,
};
use tracing::info;
// use serde::{Deserialize, Serialize};
use std::{fmt::Debug, path::PathBuf, time::Duration};
use pubky_common::timestamp::Timestamp;
@@ -12,10 +16,7 @@ const DEFAULT_HOMESERVER_PORT: u16 = 6287;
const DEFAULT_STORAGE_DIR: &str = "pubky";
/// Server configuration
#[derive(
// Serialize, Deserialize,
Clone,
)]
#[derive(Serialize, Deserialize, Clone)]
pub struct Config {
port: Option<u16>,
bootstrap: Option<Vec<String>>,
@@ -24,20 +25,22 @@ pub struct Config {
///
/// Defaults to a directory in the OS data directory
storage: Option<PathBuf>,
keypair: Keypair,
#[serde(deserialize_with = "secret_key_deserialize")]
secret_key: Option<[u8; 32]>,
dht_request_timeout: Option<Duration>,
}
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()))?;
// let config: Config = toml::from_str(&s)?;
// 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()))?;
let config: Config = toml::from_str(&s)?;
Ok(config)
}
/// Testnet configurations
pub fn testnet() -> Self {
@@ -55,7 +58,6 @@ impl Config {
bootstrap,
storage,
port: Some(15411),
keypair: Keypair::from_secret_key(&[0_u8; 32]),
dht_request_timeout: Some(Duration::from_millis(10)),
..Default::default()
}
@@ -103,8 +105,8 @@ impl Config {
Ok(dir.join("homeserver"))
}
pub fn keypair(&self) -> &Keypair {
&self.keypair
pub fn keypair(&self) -> Keypair {
Keypair::from_secret_key(&self.secret_key.unwrap_or_default())
}
pub(crate) fn dht_request_timeout(&self) -> Option<Duration> {
@@ -119,12 +121,34 @@ impl Default for Config {
bootstrap: None,
domain: "localhost".to_string(),
storage: None,
keypair: Keypair::random(),
secret_key: None,
dht_request_timeout: None,
}
}
}
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)?;
match opt {
Some(s) => {
let bytes = hex::decode(s).map_err(serde::de::Error::custom)?;
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);
Ok(Some(arr))
}
None => Ok(None),
}
}
impl Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()

View File

@@ -0,0 +1,8 @@
# Secret key (in hex) to generate the Homeserver's Keypair
secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
# Domain to be published in Pkarr records for this server to be accessible by.
domain = "localhost"
# Port for the Homeserver to listen on.
port = 6287
# Storage directory Defaults to <System's Data Directory>
# storage = ""

View File

@@ -1,7 +1,7 @@
use pkarr::PublicKey;
use postcard::{from_bytes, to_allocvec};
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument};
use tracing::instrument;
use heed::{
types::{Bytes, Str},
@@ -197,8 +197,6 @@ fn next_threshold(
reverse: bool,
shallow: bool,
) -> String {
debug!("Fuck me!");
format!(
"{path}{file_or_directory}{}",
if file_or_directory.is_empty() {

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use anyhow::Result;
use pubky_homeserver::{config::Config, Homeserver};
@@ -8,8 +10,14 @@ struct Cli {
/// [tracing_subscriber::EnvFilter]
#[clap(short, long)]
tracing_env_filter: Option<String>,
/// Run Homeserver in a local testnet
#[clap(long)]
testnet: bool,
/// Optional Path to config file.
#[clap(short, long)]
config: Option<PathBuf>,
}
#[tokio::main]
@@ -25,8 +33,10 @@ async fn main() -> Result<()> {
let server = Homeserver::start(if args.testnet {
Config::testnet()
} else if let Some(config_path) = args.config {
Config::load(config_path).await?
} else {
Default::default()
Config::default()
})
.await?;

View File

@@ -11,6 +11,8 @@ pub async fn publish_server_packet(
domain: &str,
port: u16,
) -> anyhow::Result<()> {
// TODO: Try to resolve first before publishing.
let mut packet = Packet::new_reply(0);
let mut svcb = SVCB::new(0, domain.try_into()?);

View File

@@ -30,7 +30,8 @@ impl Homeserver {
pub async fn start(config: Config) -> Result<Self> {
debug!(?config);
let public_key = config.keypair().public_key();
let keypair = config.keypair();
let public_key = keypair.public_key();
let db = DB::open(&config.storage()?)?;
@@ -72,7 +73,7 @@ impl Homeserver {
info!("Homeserver listening on http://localhost:{port}");
publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?;
publish_server_packet(pkarr_client, &keypair, config.domain(), port).await?;
info!("Homeserver listening on pubky://{public_key}");

View File

@@ -10,7 +10,7 @@ use crate::{
PubkyClient,
};
const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3;
const MAX_ENDPOINT_RESOLUTION_RECURSION: u8 = 3;
impl PubkyClient {
/// Publish the SVCB record for `_pubky.<public_key>`.
@@ -56,24 +56,28 @@ impl PubkyClient {
.map_err(|_| Error::Generic("Could not resolve homeserver".to_string()))
}
/// Resolve a service's public_key and clearnet url from a Pubky domain
/// Resolve a service's public_key and "non-pkarr url" from a Pubky domain
///
/// "non-pkarr" url is any URL where the hostname isn't a 52 z-base32 character,
/// usually an IPv4, IPv6 or ICANN domain, but could also be any other unknown hostname.
///
/// Recursively resolve SVCB and HTTPS endpoints, with [MAX_ENDPOINT_RESOLUTION_RECURSION] limit.
pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<Endpoint> {
let original_target = target;
// TODO: cache the result of this function?
let mut target = target.to_string();
let mut homeserver_public_key = None;
let mut endpoint_public_key = None;
let mut origin = target.clone();
let mut step = 0;
// PublicKey is very good at extracting the Pkarr TLD from a string.
while let Ok(public_key) = PublicKey::try_from(target.clone()) {
if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION {
if step >= MAX_ENDPOINT_RESOLUTION_RECURSION {
break;
};
step += 1;
if let Some(signed_packet) = self
@@ -85,8 +89,12 @@ impl PubkyClient {
let svcb = signed_packet.resource_records(&target).fold(
None,
|prev: Option<SVCB>, answer| {
if let pkarr::dns::rdata::RData::SVCB(curr) = &answer.rdata {
let curr = curr.clone();
if let Some(svcb) = match &answer.rdata {
pkarr::dns::rdata::RData::SVCB(svcb) => Some(svcb),
pkarr::dns::rdata::RData::HTTPS(curr) => Some(&curr.0),
_ => None,
} {
let curr = svcb.clone();
if curr.priority == 0 {
return Some(curr);
@@ -106,7 +114,7 @@ impl PubkyClient {
);
if let Some(svcb) = svcb {
homeserver_public_key = Some(public_key.clone());
endpoint_public_key = Some(public_key.clone());
target = svcb.target.to_string();
if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) {
@@ -120,14 +128,14 @@ impl PubkyClient {
origin.clone_from(&target);
};
if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION {
if step >= MAX_ENDPOINT_RESOLUTION_RECURSION {
continue;
};
}
}
}
if let Some(public_key) = homeserver_public_key {
if let Some(public_key) = endpoint_public_key {
let url = Url::parse(&format!(
"{}://{}",
if origin.starts_with("localhost") {
@@ -145,6 +153,7 @@ impl PubkyClient {
}
}
#[derive(Debug)]
pub(crate) struct Endpoint {
pub public_key: PublicKey,
pub url: Url,
@@ -155,12 +164,104 @@ mod tests {
use super::*;
use pkarr::{
dns::{rdata::SVCB, Packet},
dns::{
rdata::{HTTPS, SVCB},
Packet,
},
mainline::{dht::DhtSettings, Testnet},
Keypair, PkarrClient, Settings, SignedPacket,
};
use pubky_homeserver::Homeserver;
#[tokio::test]
async fn resolve_endpoint_https() {
let testnet = Testnet::new(10);
let pkarr_client = PkarrClient::new(Settings {
dht: DhtSettings {
bootstrap: Some(testnet.bootstrap.clone()),
..Default::default()
},
..Default::default()
})
.unwrap()
.as_async();
let domain = "example.com";
let mut target;
// Server
{
let keypair = Keypair::random();
let https = HTTPS(SVCB::new(0, domain.try_into().unwrap()));
let mut packet = Packet::new_reply(0);
packet.answers.push(pkarr::dns::ResourceRecord::new(
"foo".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
pkarr::dns::rdata::RData::HTTPS(https),
));
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
pkarr_client.publish(&signed_packet).await.unwrap();
target = format!("foo.{}", keypair.public_key());
}
// intermediate
{
let keypair = Keypair::random();
let svcb = SVCB::new(0, target.as_str().try_into().unwrap());
let mut packet = Packet::new_reply(0);
packet.answers.push(pkarr::dns::ResourceRecord::new(
"bar".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
pkarr::dns::rdata::RData::SVCB(svcb),
));
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
pkarr_client.publish(&signed_packet).await.unwrap();
target = format!("bar.{}", keypair.public_key())
}
{
let keypair = Keypair::random();
let svcb = SVCB::new(0, target.as_str().try_into().unwrap());
let mut packet = Packet::new_reply(0);
packet.answers.push(pkarr::dns::ResourceRecord::new(
"pubky".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
pkarr::dns::rdata::RData::SVCB(svcb),
));
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
pkarr_client.publish(&signed_packet).await.unwrap();
target = format!("pubky.{}", keypair.public_key())
}
let client = PubkyClient::test(&testnet);
let endpoint = client.resolve_endpoint(&target).await.unwrap();
assert_eq!(endpoint.url.host_str().unwrap(), domain);
}
#[tokio::test]
async fn resolve_homeserver() {
let testnet = Testnet::new(10);

View File

@@ -63,32 +63,31 @@ impl PubkyClient {
pub(crate) async fn pubky_to_http<T: TryInto<Url>>(&self, url: T) -> Result<Url> {
let original_url: Url = url.try_into().map_err(|_| Error::InvalidUrl)?;
if original_url.scheme() != "pubky" {
return Ok(original_url);
}
let pubky = original_url
.host_str()
.ok_or(Error::Generic("Missing Pubky Url host".to_string()))?
.to_string();
.ok_or(Error::Generic("Missing Pubky Url host".to_string()))?;
let Endpoint { mut url, .. } = self
.resolve_pubky_homeserver(&PublicKey::try_from(pubky.clone())?)
.await?;
if let Ok(public_key) = PublicKey::try_from(pubky) {
let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(&public_key).await?;
let path = original_url.path_segments();
// TODO: remove if we move to subdomains instead of paths.
if original_url.scheme() == "pubky" {
let path = original_url.path_segments();
// TODO: replace if we move to subdomains instead of paths.
let mut split = url.path_segments_mut().unwrap();
split.push(&pubky);
if let Some(segments) = path {
for segment in segments {
split.push(segment);
let mut split = url.path_segments_mut().unwrap();
split.push(pubky);
if let Some(segments) = path {
for segment in segments {
split.push(segment);
}
}
drop(split);
}
}
drop(split);
Ok(url)
return Ok(url);
}
Ok(original_url)
}
}

View File

@@ -10,30 +10,33 @@ static SPEC_LINE: &str = "pubky.org/recovery";
pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result<Keypair> {
let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase)?;
let mut split = recovery_file.split(|byte| byte == &10);
let newline_index = recovery_file
.iter()
.position(|&r| r == 10)
.ok_or(())
.map_err(|_| Error::RecoveryFileMissingSpecLine)?;
match split.next() {
Some(bytes) => {
if !(bytes.starts_with(SPEC_LINE.as_bytes())
|| bytes.starts_with(b"pkarr.org/recovery"))
{
return Err(Error::RecoveryFileVersionNotSupported);
}
}
None => return Err(Error::RecoveryFileMissingSpecLine),
let spec_line = &recovery_file[..newline_index];
if !(spec_line.starts_with(SPEC_LINE.as_bytes())
|| spec_line.starts_with(b"pkarr.org/recovery"))
{
return Err(Error::RecoveryFileVersionNotSupported);
}
let encrypted = &recovery_file[newline_index + 1..];
if encrypted.is_empty() {
return Err(Error::RecoverFileMissingEncryptedSecretKey);
};
if let Some(encrypted) = split.next() {
let decrypted = decrypt(encrypted, &encryption_key)?;
let length = decrypted.len();
let secret_key: [u8; 32] = decrypted
.try_into()
.map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?;
let decrypted = decrypt(encrypted, &encryption_key)?;
let length = decrypted.len();
let secret_key: [u8; 32] = decrypted
.try_into()
.map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?;
return Ok(Keypair::from_secret_key(&secret_key));
};
Err(Error::RecoverFileMissingEncryptedSecretKey)
Ok(Keypair::from_secret_key(&secret_key))
}
pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result<Vec<u8>> {