From 65a6d776c379ff465a7da5d48dc836f6dc0b7012 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 31 Jul 2024 17:46:59 +0300 Subject: [PATCH] feat(pubky): accept url instead of public key and path --- pubky/pkg/README.md | 7 ++-- pubky/pkg/test/public.js | 6 ++-- pubky/src/native.rs | 8 ++--- pubky/src/shared/auth.rs | 26 +++++++++++--- pubky/src/shared/pkarr.rs | 42 +++++++++++++---------- pubky/src/shared/public.rs | 70 +++++++++++++++++++++++++------------- pubky/src/wasm.rs | 16 +++------ 7 files changed, 110 insertions(+), 65 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index e3aa3f5..3852307 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -26,19 +26,22 @@ await client.signup(keypair, homeserver) const publicKey = keypair.public_key(); +// Pubky URL +let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + // Verify that you are signed in. const session = await client.session(publicKey) const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client -await client.put(publicKey, "/pub/example.com/arbitrary", body); +await client.put(url, body); // GET public data without signup or signin { const client = new PubkyClient(); - let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + let response = await client.get(url); } ``` diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index 624f7dc..7fb4111 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -12,17 +12,19 @@ test('public: put/get', async (t) => { const publicKey = keypair.public_key(); + let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) // PUT public data, by authorized client - await client.put(publicKey, "/pub/example.com/arbitrary", body); + await client.put(url, body); // GET public data without signup or signin { const client = PubkyClient.testnet(); - let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + let response = await client.get(url); t.ok(Buffer.from(response).equals(body)) } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index a2afea0..95f4a7d 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -86,13 +86,13 @@ impl PubkyClient { // === Public data === /// Upload a small payload to a given path. - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - self.inner_put(pubky, path, content).await + pub async fn put>(&self, url: T, content: &[u8]) -> Result<()> { + self.inner_put(url, content).await } /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result> { - self.inner_get(pubky, path).await + pub async fn get>(&self, url: T) -> Result> { + self.inner_get(url).await } // /// Delete a file at a path relative to a pubky author. diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 0985d34..3f900ce 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -2,12 +2,15 @@ use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; +use url::Url; use crate::{ error::{Error, Result}, PubkyClient, }; +use super::pkarr::Endpoint; + impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. /// @@ -22,7 +25,10 @@ impl PubkyClient { let public_key = &keypair.public_key(); - let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; + let Endpoint { + public_key: audience, + mut url, + } = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", public_key)); @@ -30,7 +36,11 @@ impl PubkyClient { .as_bytes() .to_owned(); - let response = self.request(Method::PUT, url).body(body).send().await?; + let response = self + .request(Method::PUT, url.clone()) + .body(body) + .send() + .await?; self.store_session(response); @@ -44,7 +54,7 @@ impl PubkyClient { /// Returns None if not signed in, or [reqwest::Error] /// if the response has any other `>=404` status code. pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + let Endpoint { mut url, .. } = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); @@ -65,7 +75,10 @@ impl PubkyClient { /// Signout from a homeserver. pub async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + let Endpoint { + public_key, + mut url, + } = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); @@ -80,7 +93,10 @@ impl PubkyClient { pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; + let Endpoint { + public_key: audience, + mut url, + } = self.resolve_pubky_homeserver(&pubky).await?; url.set_path(&format!("/{}/session", &pubky)); diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index 92f7bc8..f615de0 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -1,4 +1,4 @@ -use url::Url; +use url::{Origin, Url}; use pkarr::{ dns::{rdata::SVCB, Packet}, @@ -48,11 +48,8 @@ impl PubkyClient { } /// Resolve the homeserver for a pubky. - pub(crate) async fn resolve_pubky_homeserver( - &self, - pubky: &PublicKey, - ) -> Result<(PublicKey, Url)> { - let target = format!("_pubky.{}", pubky); + pub(crate) async fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result { + let target = format!("_pubky.{pubky}"); self.resolve_endpoint(&target) .await @@ -60,14 +57,14 @@ impl PubkyClient { } /// Resolve a service's public_key and clearnet url from a Pubky domain - pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result { 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 host = target.clone(); + let mut origin = target.clone(); let mut step = 0; @@ -118,9 +115,9 @@ impl PubkyClient { } let port = u16::from_be_bytes([port[0], port[1]]); - host = format!("{target}:{port}"); + origin = format!("{target}:{port}"); } else { - host.clone_from(&target); + origin.clone_from(&target); }; if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { @@ -130,20 +127,29 @@ impl PubkyClient { } } - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; + if let Some(public_key) = homeserver_public_key { + let mut url = Url::parse(&format!( + "{}://{}", + if origin.starts_with("localhost") { + "http" + } else { + "https" + }, + origin + ))?; - return Ok((homeserver, Url::parse(&url)?)); + return Ok(Endpoint { public_key, url }); } Err(Error::ResolveEndpoint(original_target.into())) } } +pub(crate) struct Endpoint { + pub public_key: PublicKey, + pub url: Url, +} + #[cfg(test)] mod tests { use super::*; @@ -200,7 +206,7 @@ mod tests { .await .unwrap(); - let (public_key, url) = client + let Endpoint { public_key, url } = client .resolve_pubky_homeserver(&pubky.public_key()) .await .unwrap(); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index b78554a..d3a776c 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -4,11 +4,16 @@ use pkarr::PublicKey; use reqwest::{Method, Response, StatusCode}; use url::Url; -use crate::{error::Result, PubkyClient}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; + +use super::pkarr::Endpoint; impl PubkyClient { - pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let url = self.url(pubky, path).await?; + pub async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { + let url = self.pubky_to_http(url).await?; let response = self .request(Method::PUT, url) @@ -21,8 +26,8 @@ impl PubkyClient { Ok(()) } - pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result> { - let url = self.url(pubky, path).await?; + pub async fn inner_get>(&self, url: T) -> Result> { + let url = self.pubky_to_http(url).await?; let response = self.request(Method::GET, url).send().await?; @@ -38,8 +43,8 @@ impl PubkyClient { Ok(Some(bytes)) } - pub async fn inner_delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { - let url = self.url(pubky, path).await?; + pub async fn inner_delete>(&self, url: T) -> Result<()> { + let url = self.pubky_to_http(url).await?; let response = self.request(Method::DELETE, url).send().await?; @@ -48,12 +53,35 @@ impl PubkyClient { Ok(()) } - async fn url(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path)?; + async fn pubky_to_http>(&self, url: T) -> Result { + let mut original_url: Url = url + .try_into() + .map_err(|e| Error::Generic("Invalid Url".to_string()))?; - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + if original_url.scheme() != "pubky" { + return Ok(original_url); + } - url.set_path(&format!("/{pubky}/{path}")); + let pubky = original_url + .host_str() + .ok_or(Error::Generic("Missing Pubky Url host".to_string()))? + .to_string(); + + let Endpoint { mut url, .. } = self + .resolve_pubky_homeserver(&PublicKey::try_from(pubky.clone())?) + .await?; + + 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); + } + } + drop(split); Ok(url) } @@ -96,16 +124,11 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - client - .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await - .unwrap(); + let url = format!("pubky://{}/pub/foo.txt", keypair.public_key()); - let response = client - .get(&keypair.public_key(), "/pub/foo.txt") - .await - .unwrap() - .unwrap(); + client.put(url.as_str(), &[0, 1, 2, 3, 4]).await.unwrap(); + + let response = client.get(url.as_str()).await.unwrap().unwrap(); assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); @@ -135,18 +158,19 @@ mod tests { let public_key = keypair.public_key(); + let url = format!("pubky://{public_key}/pub/foo.txt"); + let other_client = PubkyClient::test(&testnet); { let other = Keypair::random(); + // TODO: remove extra client after switching to subdomains. other_client .signup(&other, &server.public_key()) .await .unwrap(); - let response = other_client - .put(&public_key, "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await; + let response = other_client.put(url.as_str(), &[0, 1, 2, 3, 4]).await; match response { Err(Error::Reqwest(error)) => { diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index d22a6ed..782a5b0 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -5,7 +5,7 @@ use std::{ use wasm_bindgen::prelude::*; -use reqwest::{Method, RequestBuilder, Response}; +use reqwest::{IntoUrl, Method, RequestBuilder, Response}; use url::Url; use crate::PubkyClient; @@ -116,20 +116,14 @@ impl PubkyClient { #[wasm_bindgen] /// Upload a small payload to a given path. - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<(), JsValue> { - self.inner_put(pubky.as_inner(), path, content) - .await - .map_err(|e| e.into()) + pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(url, content).await.map_err(|e| e.into()) } #[wasm_bindgen] /// Download a small payload from a given path relative to a pubky author. - pub async fn get( - &self, - pubky: &PublicKey, - path: &str, - ) -> Result, JsValue> { - self.inner_get(pubky.as_inner(), path) + pub async fn get(&self, url: &str) -> Result, JsValue> { + self.inner_get(url) .await .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into())