From bd5b44e54454a3e7812e0cadd9bdbeaf694f33a0 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 13:13:56 +0300 Subject: [PATCH] feat(pubky): add put/get methods for js --- pubky/Cargo.toml | 13 +----- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/public.js | 46 ++++++++++++++++++++ pubky/src/native.rs | 27 +++++++++++- pubky/src/shared/mod.rs | 1 + pubky/src/shared/public.rs | 86 ++++++++++++++++++++++++++++++++++++++ pubky/src/wasm.rs | 44 +++++++------------ pubky/src/wasm/http.rs | 42 +++++++++++++++++++ pubky/src/wasm/keys.rs | 3 +- 9 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 pubky/pkg/test/public.js create mode 100644 pubky/src/shared/public.rs create mode 100644 pubky/src/wasm/http.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index d71c0bb..392402e 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -14,30 +14,21 @@ crate-type = ["cdylib", "rlib"] thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" -reqwest = { version = "0.12.5", features = ["cookies"] } bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } -flume = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } +reqwest = { version = "0.12.5", features = ["cookies"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] pkarr = { version = "2.1.0", default-features = false } +reqwest = { version = "0.12.5", default-features = false } -futures = "0.3.29" js-sys = "0.3.69" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" -web-sys = { version = "0.3.69", features = [ - "console", - "Request", - "RequestInit", - "RequestMode", - "Response", - "Window", -] } [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index a660d99..60f9b4c 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -8,7 +8,7 @@ test('seed auth', async (t) => { const keypair = Keypair.random() const publicKey = keypair.public_key() - const homeserver = PublicKey.try_from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) const session = await client.session(publicKey) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js new file mode 100644 index 0000000..b5ae7a9 --- /dev/null +++ b/pubky/pkg/test/public.js @@ -0,0 +1,46 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.js' + +test('public: put/get', async (t) => { + const client = new PubkyClient(); + + const keypair = Keypair.random(); + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + await client.signup(keypair, homeserver); + + const publicKey = keypair.public_key(); + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + // PUT public data, by authorized client + await client.put(publicKey, "/pub/example.com/arbitrary", body); + + + // GET public data without signup or signin + { + const client = new PubkyClient(); + + let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + + t.ok(Buffer.from(response).equals(body)) + } + + // // DELETE public data, by authorized client + // await client.delete(publicKey, "/pub/example.com/arbitrary"); + // + // + // // GET public data without signup or signin + // { + // const client = new PubkyClient(); + // + // let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + // + // t.notOk(response) + // } +}) + +test.skip("not found") + +test.skip("unauthorized") diff --git a/pubky/src/native.rs b/pubky/src/native.rs index e6627cc..b350892 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,11 +1,10 @@ -pub mod public; - use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, PkarrClient, PublicKey, Settings, SignedPacket, }; +use bytes::Bytes; use pkarr::Keypair; use pubky_common::session::Session; use reqwest::{Method, RequestBuilder, Response}; @@ -21,6 +20,8 @@ impl Default for PubkyClient { } } +// === Public API === + impl PubkyClient { pub fn new() -> Self { Self { @@ -54,6 +55,8 @@ impl PubkyClient { } } + // === Auth === + /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key @@ -80,6 +83,24 @@ impl PubkyClient { self.inner_signin(keypair).await } + // === 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 + } + + /// 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 + } +} + +// === Internals === + +impl PubkyClient { + // === Pkarr === + pub(crate) async fn pkarr_resolve( &self, public_key: &PublicKey, @@ -91,6 +112,8 @@ impl PubkyClient { Ok(self.pkarr.publish(signed_packet).await?) } + // === HTTP === + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { self.http.request(method, url) } diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index c61bbfe..ec9bd27 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod pkarr; +pub mod public; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs new file mode 100644 index 0000000..ed2da36 --- /dev/null +++ b/pubky/src/shared/public.rs @@ -0,0 +1,86 @@ +use bytes::Bytes; + +use pkarr::PublicKey; +use reqwest::Method; + +use crate::{error::Result, PubkyClient}; + +impl PubkyClient { + pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + self.request(Method::PUT, url) + .body(content.to_owned()) + .send() + .await?; + + Ok(()) + } + + pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + let response = self.request(Method::GET, url).send().await?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(bytes) + } +} + +fn normalize_path(path: &str) -> String { + let mut path = path.to_string(); + + if path.starts_with('/') { + path = path[1..].to_string() + } + + // TODO: should we return error instead? + if path.ends_with('/') { + path = path[..path.len()].to_string() + } + + path +} + +#[cfg(test)] +mod tests { + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn put_get() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + client + .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) + .await + .unwrap(); + + let response = client + .get(&keypair.public_key(), "/pub/foo.txt") + .await + .unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index e572670..cdff018 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -10,6 +10,7 @@ use url::Url; use crate::PubkyClient; +mod http; mod keys; mod pkarr; mod session; @@ -66,37 +67,22 @@ impl PubkyClient { .map_err(|e| e.into()) } - pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { - let request = self.http.request(method, url).fetch_credentials_include(); + // === Public data === - for cookie in self.session_cookies.read().unwrap().iter() { - return request.header("Cookie", cookie); - } - - request + #[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()) } - // Support cookies for nodejs - - pub(crate) fn store_session(&self, response: Response) { - if let Some(cookie) = response - .headers() - .get("set-cookie") - .and_then(|h| h.to_str().ok()) - .and_then(|s| s.split(';').next()) - { - self.session_cookies - .write() - .unwrap() - .insert(cookie.to_string()); - } - } - pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { - let key = pubky.to_string(); - - self.session_cookies - .write() - .unwrap() - .retain(|cookie| !cookie.starts_with(&key)); + #[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 { + self.inner_get(pubky.as_inner(), path) + .await + .map(|b| (*b).into()) + .map_err(|e| e.into()) } } diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs new file mode 100644 index 0000000..52a0c9d --- /dev/null +++ b/pubky/src/wasm/http.rs @@ -0,0 +1,42 @@ +use crate::PubkyClient; + +use reqwest::{Method, RequestBuilder, Response}; +use url::Url; + +use ::pkarr::PublicKey; + +impl PubkyClient { + pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { + let request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + return request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); + } +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index cbd6ec7..b5043bb 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -51,7 +51,8 @@ impl PublicKey { self.0.to_string() } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "from")] + /// @throws pub fn try_from(value: JsValue) -> Result { let string = value.as_string().ok_or(Error::Generic( "Couldn't create a PublicKey from this type of value".to_string(),