From c41e14233b6b2264bb90968aebdcd6654c3de3af Mon Sep 17 00:00:00 2001 From: nazeh Date: Tue, 16 Jul 2024 12:36:28 +0300 Subject: [PATCH] feat(pubky): publish and resolve homeserver endpoint --- Cargo.lock | 217 ++++++++++++++++++++++++++++++- Cargo.toml | 2 +- pubky-common/Cargo.toml | 2 +- pubky-common/src/auth.rs | 9 +- pubky-homeserver/src/pkarr.rs | 4 +- pubky-homeserver/src/server.rs | 58 +++++++-- pubky/Cargo.toml | 16 +++ pubky/src/error.rs | 20 +++ pubky/src/lib.rs | 228 +++++++++++++++++++++++++++++++++ 9 files changed, 539 insertions(+), 17 deletions(-) create mode 100644 pubky/Cargo.toml create mode 100644 pubky/src/error.rs create mode 100644 pubky/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4d51cf4..5f3b96f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -231,6 +237,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -288,6 +303,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "document-features" version = "0.2.10" @@ -334,12 +370,24 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", "nanorand", "spin", ] @@ -563,6 +611,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "itoa" version = "1.0.11" @@ -590,6 +648,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "litrs" version = "0.4.1" @@ -798,9 +866,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f9e12544b00f5561253bbd3cb72a85ff3bc398483dc1bf82bdf095c774136b" +checksum = "4548c673cbf8c91b69f7a17d3a042710aa73cffe5e82351db5378f26c3be64d8" dependencies = [ "bytes", "document-features", @@ -847,6 +915,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pubky" +version = "0.1.0" +dependencies = [ + "pkarr", + "pubky-common", + "pubky_homeserver", + "thiserror", + "tokio", + "ureq", + "url", +] + [[package]] name = "pubky-common" version = "0.1.0" @@ -866,7 +947,9 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "dirs-next", "pkarr", + "pubky-common", "tokio", "tower-http", "tracing", @@ -921,6 +1004,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -965,6 +1059,21 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -980,6 +1089,38 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1238,6 +1379,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.38.0" @@ -1381,12 +1537,60 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1481,6 +1685,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index c60c23b..85e44a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["pubky-*"] +members = [ "pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 94ca15d..8f73315 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -10,6 +10,6 @@ base32 = "0.5.0" blake3 = "1.5.1" ed25519-dalek = "2.1.1" once_cell = "1.19.0" -pkarr = "2.0.3" +pkarr = "2.1.0" rand = "0.8.5" thiserror = "1.0.60" diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index a344be3..7fc2a02 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -1,3 +1,5 @@ +//! Client-server Authentication using signed timesteps + use std::sync::{Arc, Mutex}; use ed25519_dalek::ed25519::SignatureBytes; @@ -37,8 +39,9 @@ impl AuthnSignature { Self(bytes.into()) } - pub fn for_token(keypair: &Keypair, audience: &PublicKey, token: &[u8]) -> Self { - AuthnSignature::new(keypair, audience, Some(token)) + /// Sign a randomly generated nonce + pub fn generate(keypair: &Keypair, audience: &PublicKey) -> Self { + AuthnSignature::new(keypair, audience, None) } pub fn as_bytes(&self) -> &[u8] { @@ -192,7 +195,7 @@ mod tests { let verifier = AuthnVerifier::new(audience.clone()); - let authn_signature = AuthnSignature::new(&keypair, &audience, None); + let authn_signature = AuthnSignature::generate(&keypair, &audience); verifier .verify(authn_signature.as_bytes(), &signer) diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 5186273..113c598 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -19,6 +19,7 @@ pub async fn publish_server_packet( // assuming any other domain will point to a reverse proxy // at the conventional ports. if domain == "localhost" { + svcb.priority = 1; svcb.set_port(port); // TODO: Add more parameteres like the signer key! @@ -26,9 +27,10 @@ pub async fn publish_server_packet( }; // TODO: announce A/AAAA records as well for Noise connections? + // Or maybe Iroh's magic socket packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), + "@".try_into().unwrap(), pkarr::dns::CLASS::IN, 60 * 60, pkarr::dns::rdata::RData::SVCB(svcb), diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 506d5ac..215d67c 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -4,26 +4,31 @@ use anyhow::{Error, Result}; use tokio::{net::TcpListener, signal, task::JoinSet}; use tracing::{info, warn}; +use pkarr::{ + mainline::dht::{DhtSettings, Testnet}, + PkarrClient, PublicKey, Settings, +}; + +use crate::{config::Config, pkarr::publish_server_packet}; + #[derive(Debug)] pub struct Homeserver { + pub(crate) config: Config, + port: u16, tasks: JoinSet>, } impl Homeserver { - pub async fn start() -> Result { + pub async fn start(config: Config) -> Result { let app = crate::routes::create_app(); let mut tasks = JoinSet::new(); let app = app.clone(); - let listener = TcpListener::bind(SocketAddr::from(( - [0, 0, 0, 0], - 6287, // config.port() - ))) - .await?; + let listener = TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], config.port()))).await?; - let bound_addr = listener.local_addr()?; + let port = listener.local_addr()?.port(); // Spawn http server task tasks.spawn( @@ -35,9 +40,44 @@ impl Homeserver { .into_future(), ); - info!("HTTP server listening on {bound_addr}"); + info!("Homeserver listening on http://localhost:{port}"); - Ok(Self { tasks }) + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + + publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; + + info!( + "Homeserver listening on pubky://{}", + config.keypair().public_key() + ); + + Ok(Self { + tasks, + config, + port, + }) + } + + /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. + pub async fn start_test(testnet: &Testnet) -> Result { + Homeserver::start(Config::test(testnet)).await + } + + // === Getters === + + pub fn port(&self) -> u16 { + self.port + } + + pub fn public_key(&self) -> PublicKey { + self.config.keypair().public_key() } // === Public Methods === diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml new file mode 100644 index 0000000..321bea7 --- /dev/null +++ b/pubky/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pubky" +version = "0.1.0" +edition = "2021" + +[dependencies] +pubky-common = { version = "0.1.0", path = "../pubky-common" } + +pkarr = "2.1.0" +ureq = "2.10.0" +thiserror = "1.0.62" +url = "2.5.2" + +[dev-dependencies] +pubky_homeserver = { path = "../pubky-homeserver" } +tokio = "1.37.0" diff --git a/pubky/src/error.rs b/pubky/src/error.rs new file mode 100644 index 0000000..47ec28e --- /dev/null +++ b/pubky/src/error.rs @@ -0,0 +1,20 @@ +//! Main Crate Error + +use pkarr::dns::SimpleDnsError; + +// Alias Result to be the crate Result. +pub type Result = core::result::Result; + +#[derive(thiserror::Error, Debug)] +/// Pk common Error +pub enum Error { + /// For starter, to remove as code matures. + #[error("Generic error: {0}")] + Generic(String), + + #[error(transparent)] + Dns(#[from] SimpleDnsError), + + #[error(transparent)] + Pkarr(#[from] pkarr::Error), +} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs new file mode 100644 index 0000000..74cfd41 --- /dev/null +++ b/pubky/src/lib.rs @@ -0,0 +1,228 @@ +#![allow(unused)] + +use std::{collections::HashMap, time::Duration}; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, PublicKey, Settings, SignedPacket, +}; +use ureq::{Agent, Response}; +use url::Url; + +use pubky_common::auth::AuthnSignature; + +mod error; + +use error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +#[derive(Debug)] +pub struct Client { + agent: Agent, + pkarr: PkarrClient, +} + +impl Client { + pub fn new() -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Default::default()).unwrap(), + } + } + + pub fn test(testnet: &Testnet) -> Self { + Self { + agent: Agent::new(), + pkarr: PkarrClient::new(Settings { + dht: DhtSettings { + request_timeout: Some(Duration::from_millis(10)), + bootstrap: Some(testnet.bootstrap.to_owned()), + ..DhtSettings::default() + }, + ..Settings::default() + }) + .unwrap(), + } + } + + // === Public Methods === + + // === Private Methods === + + /// Publish the SVCB record for `_pubky.`. + fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let mut svcb = SVCB::new(0, host.try_into()?); + + 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)?; + + self.pkarr.publish(&signed_packet)?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, String)> { + // TODO: cache the result of this function? + + let mut target = format!("_pubky.{}", pubky); + let mut homeserver_public_key = None; + let mut host = target.clone(); + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { + let mut prior = None; + + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key); + target = svcb.target.to_string(); + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + continue; + } + }; + + break; + } + + if let Some(homeserver) = homeserver_public_key { + return Ok((homeserver, host)); + } + + Err(Error::Generic("Could not resolve homeserver".to_string())) + } + + fn fetch_direct(&self, method: HttpMethod, url: &str) -> Result { + self.agent + .request(method.into(), url) + .call() + .map_err(|_| Error::Generic("ureq error".to_string())) + } +} + +impl Default for Client { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone)] +pub enum HttpMethod { + GET, + PUT, +} + +impl From for &str { + fn from(value: HttpMethod) -> Self { + match value { + HttpMethod::GET => "GET", + HttpMethod::PUT => "PUT", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + 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(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + tokio::task::spawn_blocking(move || { + let client = Client::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + + let (public_key, host) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert!(host.starts_with("localhost")); + assert!(host.ends_with(&server.port().to_string())); + }) + .await + .expect("task failed") + } +}