diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 8396f3b..8408c39 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -13,12 +13,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] pkarr = "2.1.0" thiserror = "1.0.62" +wasm-bindgen = "0.2.92" +url = "2.5.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } ureq = { version = "2.10.0", features = ["cookies"] } -url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs index c036527..a568c4c 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/client/pkarr.rs @@ -4,33 +4,16 @@ pub use pkarr::{ Keypair, PkarrClient, PublicKey, Settings, SignedPacket, }; -use super::{Error, PubkyClient, Result, Url}; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; +use super::{Error, PubkyClient, Result, Url}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key())?; - 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 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)?; + let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; self.pkarr.publish(&signed_packet)?; @@ -48,66 +31,32 @@ impl PubkyClient { /// Resolve a service's public_key and clearnet url from a Pubky domain pub(crate) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { // TODO: cache the result of this function? - // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION - // TODO: move to common? let mut target = target.to_string(); let mut homeserver_public_key = None; let mut host = 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 let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; + let response = self.pkarr.resolve(&public_key)?; - 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) - } - } - } + let done = parse_pubky_svcb( + response, + &public_key, + &mut target, + &mut homeserver_public_key, + &mut host, + &mut step, + ); - 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 done { + break; + } } - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::Generic("Could not resolve endpoint".to_string())) + format_url(homeserver_public_key, host) } } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index f051d14..2b5320e 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -22,6 +22,9 @@ pub enum Error { #[error(transparent)] Pkarr(#[from] pkarr::Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Flume(#[from] flume::RecvError), @@ -30,10 +33,6 @@ pub enum Error { #[cfg(not(target_arch = "wasm32"))] Ureq(#[from] Box), - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Url(#[from] url::ParseError), - #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index fab446f..91a117d 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -29,4 +29,6 @@ if_wasm! { } mod error; +mod shared; + pub use error::Error; diff --git a/pubky/src/shared.rs b/pubky/src/shared.rs new file mode 100644 index 0000000..ad39b70 --- /dev/null +++ b/pubky/src/shared.rs @@ -0,0 +1 @@ +pub mod pkarr; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs new file mode 100644 index 0000000..e0d87cf --- /dev/null +++ b/pubky/src/shared/pkarr.rs @@ -0,0 +1,104 @@ +use url::Url; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +pub fn prepare_packet_for_signup( + keypair: &Keypair, + host: &str, + existing: Option, +) -> Result { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let 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), + )); + + Ok(SignedPacket::from_packet(keypair, &packet)?) +} + +pub fn parse_pubky_svcb( + signed_packet: Option, + public_key: &PublicKey, + target: &mut String, + homeserver_public_key: &mut Option, + host: &mut String, + step: &mut u8, +) -> bool { + *step += 1; + + let mut prior = None; + + if let Some(signed_packet) = signed_packet { + 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.clone()); + *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); + }; + + return *step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION; + } + } + + true +} + +pub fn format_url( + homeserver_public_key: Option, + host: String, +) -> Result<(PublicKey, Url)> { + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") + } else { + format!("https://{host}") + }; + + return Ok((homeserver, Url::parse(&url)?)); + } + + Err(Error::Generic("Could not resolve endpoint".to_string())) +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index a5589d3..2414ba4 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -8,3 +8,13 @@ pub mod pkarr; pub struct PubkyClient { pub(crate) pkarr: pkarr::PkarrRelayClient, } + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + pkarr: pkarr::PkarrRelayClient::default(), + } + } +} diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index e8bc842..6f03834 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -8,19 +8,12 @@ use super::{keys::Keypair, PubkyClient}; #[wasm_bindgen] impl PubkyClient { - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - Self { - pkarr: PkarrRelayClient::default(), - } - } - /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key /// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy" #[wasm_bindgen] - pub fn signup(&self, secret_key: Keypair, homeserver: &str) -> Result<(), JsValue> { + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<(), JsError> { // let (audience, mut url) = self.resolve_endpoint(homeserver)?; // url.set_path(&format!("/{}", keypair.public_key())); @@ -29,7 +22,7 @@ impl PubkyClient { // fetch_base(url.to_string(), "PUT", body).await?; - // self.publish_pubky_homeserver(keypair, homeserver); + self.publish_pubky_homeserver(keypair, homeserver).await?; Ok(()) } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index f73489d..65beae8 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -27,6 +27,12 @@ impl Keypair { } } +impl Keypair { + pub fn as_inner(&self) -> &pkarr::Keypair { + &self.0 + } +} + #[wasm_bindgen] pub struct PublicKey(pkarr::PublicKey); @@ -43,3 +49,9 @@ impl PublicKey { self.0.to_string() } } + +impl PublicKey { + pub fn as_inner(&self) -> &pkarr::PublicKey { + &self.0 + } +} diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 97d39a1..b000528 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -2,14 +2,13 @@ use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - Keypair, PkarrRelayClient, PublicKey, SignedPacket, + PkarrRelayClient, PublicKey, SignedPacket, }; use crate::error::Result; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -use super::PubkyClient; - -// TODO: Share more code with the non-wasm client. +use super::{keys::Keypair, PubkyClient}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -18,26 +17,9 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key().as_inner()).await?; - if let Some(existing) = self.pkarr.resolve(&keypair.public_key()).await? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let 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)?; + let signed_packet = prepare_packet_for_signup(keypair.as_inner(), host, existing)?; self.pkarr.publish(&signed_packet).await?;