feat(pubky): publish and resolve homeserver endpoint

This commit is contained in:
nazeh
2024-07-16 12:36:28 +03:00
parent 6363a174e4
commit c41e14233b
9 changed files with 539 additions and 17 deletions

217
Cargo.lock generated
View File

@@ -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"

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["pubky-*"]
members = [ "pubky","pubky-*"]
# See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352
resolver = "2"

View File

@@ -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"

View File

@@ -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)

View File

@@ -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),

View File

@@ -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<std::io::Result<()>>,
}
impl Homeserver {
pub async fn start() -> Result<Self> {
pub async fn start(config: Config) -> Result<Self> {
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<Self> {
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 ===

16
pubky/Cargo.toml Normal file
View File

@@ -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"

20
pubky/src/error.rs Normal file
View File

@@ -0,0 +1,20 @@
//! Main Crate Error
use pkarr::dns::SimpleDnsError;
// Alias Result to be the crate Result.
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[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),
}

228
pubky/src/lib.rs Normal file
View File

@@ -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.<public_key>`.
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<Response> {
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<HttpMethod> 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")
}
}