feat(pubky): accept url instead of public key and path

This commit is contained in:
nazeh
2024-07-31 17:46:59 +03:00
parent 7feef474d3
commit 65a6d776c3
7 changed files with 110 additions and 65 deletions

View File

@@ -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);
}
```

View File

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

View File

@@ -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<T: TryInto<Url>>(&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<Option<Bytes>> {
self.inner_get(pubky, path).await
pub async fn get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
self.inner_get(url).await
}
// /// Delete a file at a path relative to a pubky author.

View File

@@ -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<Option<Session>> {
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));

View File

@@ -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<Endpoint> {
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<Endpoint> {
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();

View File

@@ -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<T: TryInto<Url>>(&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<Option<Bytes>> {
let url = self.url(pubky, path).await?;
pub async fn inner_get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
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<T: TryInto<Url>>(&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<Url> {
let path = normalize_path(path)?;
async fn pubky_to_http<T: TryInto<Url>>(&self, url: T) -> Result<Url> {
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)) => {

View File

@@ -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<Option<js_sys::Uint8Array>, JsValue> {
self.inner_get(pubky.as_inner(), path)
pub async fn get(&self, url: &str) -> Result<Option<js_sys::Uint8Array>, JsValue> {
self.inner_get(url)
.await
.map(|b| b.map(|b| (&*b).into()))
.map_err(|e| e.into())