Skip to content

Commit

Permalink
refactor(pubky): separate PubkyClient implementation into modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuhvi committed Jul 23, 2024
1 parent 401872a commit 62cc13b
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 292 deletions.
245 changes: 4 additions & 241 deletions pubky/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
mod auth;
mod pkarr;

use std::{collections::HashMap, fmt::format, 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, session::Session};

use crate::error::{Error, Result};

const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3;
use pkarr::{DhtSettings, PkarrClient, Settings, Testnet};

#[derive(Debug, Clone)]
pub struct PubkyClient {
Expand Down Expand Up @@ -45,174 +41,8 @@ impl PubkyClient {

// === Public Methods ===

/// 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"
pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> {
let (audience, mut url) = self.resolve_endpoint(homeserver)?;

url.set_path(&format!("/{}", keypair.public_key()));

self.request(HttpMethod::Put, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())
.map_err(Box::new)?;

self.publish_pubky_homeserver(keypair, homeserver);

Ok(())
}

/// Check the current sesison for a given Pubky in its homeserver.
pub fn session(&self, pubky: &PublicKey) -> Result<Session> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;

url.set_path(&format!("/{}/session", pubky));

let mut bytes = vec![];

let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new);

if let Ok(reader) = result {
reader.into_reader().read_to_end(&mut bytes);
} else {
return Err(Error::NotSignedIn);
}

Ok(Session::deserialize(&bytes)?)
}

/// Signout from a homeserver.
pub fn signout(&self, pubky: &PublicKey) -> Result<()> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;

url.set_path(&format!("/{}/session", pubky));

self.request(HttpMethod::Delete, &url)
.call()
.map_err(Box::new)?;

Ok(())
}

/// Signin to a homeserver.
pub fn signin(&self, keypair: &Keypair) -> Result<()> {
let pubky = keypair.public_key();

let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?;

url.set_path(&format!("/{}/session", &pubky));

self.request(HttpMethod::Post, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())
.map_err(Box::new)?;

Ok(())
}

// === Private Methods ===

/// Publish the SVCB record for `_pubky.<public_key>`.
pub(crate) 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 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.
pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> {
let target = format!("_pubky.{}", pubky);

self.resolve_endpoint(&target)
.map_err(|_| Error::Generic("Could not resolve homeserver".to_string()))
}

/// Resolve a service's public_key and clearnet url from a Pubky domain
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();

// 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 {
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()))
}

fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request {
self.agent.request_url(method.into(), url)
}
Expand Down Expand Up @@ -242,70 +72,3 @@ impl From<HttpMethod> for &str {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

use pkarr::{
dns::{rdata::SVCB, Packet},
mainline::{dht::DhtSettings, Testnet},
Keypair, PkarrClient, Settings, SignedPacket,
};
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 = PubkyClient::test(&testnet);

let pubky = Keypair::random();

client
.publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key()));

let (public_key, url) = client
.resolve_pubky_homeserver(&pubky.public_key())
.unwrap();

assert_eq!(public_key, server.public_key());
assert_eq!(url.host_str(), Some("localhost"));
assert_eq!(url.port(), Some(server.port()));
})
.await
.expect("task failed")
}
}
122 changes: 122 additions & 0 deletions pubky/src/client/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::PubkyClient;

use pubky_common::{auth::AuthnSignature, session::Session};

use super::{Error, HttpMethod, Result};
use pkarr::{Keypair, PublicKey};

impl PubkyClient {
/// 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"
pub fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> {
let (audience, mut url) = self.resolve_endpoint(homeserver)?;

url.set_path(&format!("/{}", keypair.public_key()));

self.request(HttpMethod::Put, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())
.map_err(Box::new)?;

self.publish_pubky_homeserver(keypair, homeserver);

Ok(())
}

/// Check the current sesison for a given Pubky in its homeserver.
pub fn session(&self, pubky: &PublicKey) -> Result<Session> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;

url.set_path(&format!("/{}/session", pubky));

let mut bytes = vec![];

let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new);

if let Ok(reader) = result {
reader.into_reader().read_to_end(&mut bytes);
} else {
return Err(Error::NotSignedIn);
}

Ok(Session::deserialize(&bytes)?)
}

/// Signout from a homeserver.
pub fn signout(&self, pubky: &PublicKey) -> Result<()> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;

url.set_path(&format!("/{}/session", pubky));

self.request(HttpMethod::Delete, &url)
.call()
.map_err(Box::new)?;

Ok(())
}

/// Signin to a homeserver.
pub fn signin(&self, keypair: &Keypair) -> Result<()> {
let pubky = keypair.public_key();

let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?;

url.set_path(&format!("/{}/session", &pubky));

self.request(HttpMethod::Post, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())
.map_err(Box::new)?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::*;

use pkarr::{mainline::Testnet, Keypair};
use pubky_common::session::Session;
use pubky_homeserver::Homeserver;

#[tokio::test]
async fn basic_authn() {
let testnet = Testnet::new(3);
let server = Homeserver::start_test(&testnet).await.unwrap();

let client = PubkyClient::test(&testnet).as_async();

let keypair = Keypair::random();

client
.signup(&keypair, &server.public_key().to_string())
.await
.unwrap();

let session = client.session(&keypair.public_key()).await.unwrap();

assert_eq!(session, Session { ..session.clone() });

client.signout(&keypair.public_key()).await.unwrap();

{
let session = client.session(&keypair.public_key()).await;

assert!(session.is_err());

match session {
Err(Error::NotSignedIn) => {}
_ => assert!(false, "expected NotSignedInt error"),
}
}

client.signin(&keypair).await.unwrap();

{
let session = client.session(&keypair.public_key()).await.unwrap();

assert_eq!(session, Session { ..session.clone() });
}
}
}
Loading

0 comments on commit 62cc13b

Please sign in to comment.