From f42797a234325bf8c4e7d7e2c7da7376485d0077 Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 18 Dec 2024 00:16:49 +0300 Subject: [PATCH] feat(js): pass all unit tests --- pubky/pkg/README.md | 40 ++++------ pubky/pkg/test/public.js | 84 ++++++++++++-------- pubky/src/native.rs | 1 - pubky/src/native/http.rs | 6 ++ pubky/src/native/internal.rs | 12 --- pubky/src/wasm.rs | 1 - pubky/src/wasm/http.rs | 150 +++++++++++++++++++++++------------ pubky/src/wasm/internals.rs | 24 ------ 8 files changed, 170 insertions(+), 148 deletions(-) delete mode 100644 pubky/src/native/internal.rs delete mode 100644 pubky/src/wasm/internals.rs diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 1c75e00..a32438d 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -42,20 +42,22 @@ 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(url, body); +await client.fetch(url, { + method: "PUT", + body: JSON.stringify({foo: "bar"}), + credentials: "include" +}); // GET public data without signup or signin { const client = new Client(); - let response = await client.get(url); + let response = await client.fetch(url); } // Delete public data, by authorized client -await client.delete(url); +await client.fetch(url, { method: "DELETE", credentials: "include "}); ``` ## API @@ -67,6 +69,13 @@ await client.delete(url); let client = new Client() ``` +#### fetch +```js +let response = await client.fetch(url, opts); +``` + +Just like normal Fetch API, but it can handle `pubky://` urls and `http(s)://` urls with Pkarr domains. + #### signup ```js await client.signup(keypair, homeserver) @@ -127,27 +136,6 @@ let session = await client.session(publicKey) - publicKey: An instance of [PublicKey](#publickey). - Returns: A [Session](#session) object if signed in, or undefined if not. -#### put -```js -let response = await client.put(url, body); -``` -- url: A string representing the Pubky URL. -- body: A Buffer containing the data to be stored. - -### get -```js -let response = await client.get(url) -``` -- url: A string representing the Pubky URL. -- Returns: A Uint8Array object containing the requested data, or `undefined` if `NOT_FOUND`. - -### delete - -```js -let response = await client.delete(url); -``` -- url: A string representing the Pubky URL. - ### list ```js let response = await client.list(url, cursor, reverse, limit) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index a2cf823..d7c3db9 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -1,10 +1,10 @@ import test from 'tape' -import { Client, Keypair, PublicKey } from '../index.cjs' +import { Client, Keypair, PublicKey ,setLogLevel} from '../index.cjs' const HOMESERVER_PUBLICKEY = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') -test.skip('public: put/get', async (t) => { +test('public: put/get', async (t) => { const client = Client.testnet(); const keypair = Keypair.random(); @@ -15,33 +15,43 @@ test.skip('public: put/get', async (t) => { let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; - const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + const json = { foo: 'bar' } // PUT public data, by authorized client - await client.put(url, body); + await client.fetch(url, { + method:"PUT", + body: JSON.stringify(json), + contentType: "json", + credentials: "include" + }); const otherClient = Client.testnet(); // GET public data without signup or signin { - let response = await otherClient.get(url); + let response = await otherClient.fetch(url) - t.ok(Buffer.from(response).equals(body)) + t.is(response.status, 200); + + t.deepEquals(await response.json(), {foo: "bar"}) } // DELETE public data, by authorized client - await client.delete(url); + await client.fetch(url, { + method:"DELETE", + credentials: "include" + }); // GET public data without signup or signin { - let response = await otherClient.get(url); + let response = await otherClient.fetch(url); - t.notOk(response) + t.is(response.status, 404) } }) -test.skip("not found", async (t) => { +test("not found", async (t) => { const client = Client.testnet(); @@ -53,12 +63,12 @@ test.skip("not found", async (t) => { let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; - let result = await client.get(url).catch(e => e); + let result = await client.fetch(url); - t.notOk(result); + t.is(result.status, 404); }) -test.skip("unauthorized", async (t) => { +test("unauthorized", async (t) => { const client = Client.testnet(); const keypair = Keypair.random() @@ -71,21 +81,20 @@ test.skip("unauthorized", async (t) => { await client.signout(publicKey) - const body = Buffer.from(JSON.stringify({ foo: 'bar' })) - let url = `pubky://${publicKey.z32()}/pub/example.com/arbitrary`; // PUT public data, by authorized client - let result = await client.put(url, body).catch(e => e); + let response = await client.fetch(url, { + method: "PUT", + body: JSON.stringify({ foo: 'bar' }), + contentType: "json", + credentials: "include" + }); - t.ok(result instanceof Error); - t.is( - result.message, - `HTTP status client error (401 Unauthorized) for url (http://localhost:15411/${publicKey.z32()}/pub/example.com/arbitrary)` - ) + t.equals(response.status,401); }) -test.skip("forbidden", async (t) => { +test("forbidden", async (t) => { const client = Client.testnet(); const keypair = Keypair.random() @@ -96,21 +105,22 @@ test.skip("forbidden", async (t) => { const session = await client.session(publicKey) t.ok(session, "signup") - const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + const body = (JSON.stringify({ foo: 'bar' })) let url = `pubky://${publicKey.z32()}/priv/example.com/arbitrary`; // PUT public data, by authorized client - let result = await client.put(url, body).catch(e => e); + let response = await client.fetch(url, { + method: "PUT", + body: JSON.stringify({ foo: 'bar' }), + credentials: "include" + }); - t.ok(result instanceof Error); - t.is( - result.message, - `HTTP status client error (403 Forbidden) for url (http://localhost:15411/${publicKey.z32()}/priv/example.com/arbitrary)` - ) + t.is(response.status, 403) + t.is(await response.text(), 'Writing to directories other than \'/pub/\' is forbidden') }) -test.skip("list", async (t) => { +test("list", async (t) => { const client = Client.testnet(); const keypair = Keypair.random() @@ -132,7 +142,11 @@ test.skip("list", async (t) => { ] for (let url of urls) { - await client.put(url, Buffer.from("")); + await client.fetch(url, { + method: "PUT", + body:Buffer.from(""), + credentials: "include" + }); } let url = `pubky://${pubky}/pub/example.com/`; @@ -241,7 +255,7 @@ test.skip("list", async (t) => { } }) -test.skip('list shallow', async (t) => { +test('list shallow', async (t) => { const client = Client.testnet(); const keypair = Keypair.random() @@ -264,7 +278,11 @@ test.skip('list shallow', async (t) => { ] for (let url of urls) { - await client.put(url, Buffer.from("")); + await client.fetch(url, { + method: "PUT", + body: Buffer.from(""), + credentials: "include" + }); } let url = `pubky://${pubky}/pub/`; diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 2d98464..aa0dcd1 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -7,7 +7,6 @@ use crate::Client; mod api; mod cookies; mod http; -mod internal; pub(crate) use cookies::CookieJar; diff --git a/pubky/src/native/http.rs b/pubky/src/native/http.rs index 168e43e..f3474e8 100644 --- a/pubky/src/native/http.rs +++ b/pubky/src/native/http.rs @@ -123,6 +123,12 @@ impl Client { pub fn head(&self, url: U) -> RequestBuilder { self.request(Method::HEAD, url) } + + // === Private Methods === + + pub(crate) async fn inner_request(&self, method: Method, url: T) -> RequestBuilder { + self.request(method, url) + } } #[cfg(test)] diff --git a/pubky/src/native/internal.rs b/pubky/src/native/internal.rs deleted file mode 100644 index b6d1990..0000000 --- a/pubky/src/native/internal.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Native specific implementation of methods used in the shared module -//! - -use reqwest::{IntoUrl, Method, RequestBuilder}; - -use crate::Client; - -impl Client { - pub(crate) async fn inner_request(&self, method: Method, url: T) -> RequestBuilder { - self.request(method, url) - } -} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 8322694..5f364d9 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -4,7 +4,6 @@ use crate::Client; mod api; mod http; -mod internals; mod wrappers; impl Default for Client { diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs index 9729c21..4fab9d8 100644 --- a/pubky/src/wasm/http.rs +++ b/pubky/src/wasm/http.rs @@ -2,8 +2,9 @@ use js_sys::Promise; use wasm_bindgen::prelude::*; +use web_sys::{Headers, RequestInit}; -use reqwest::Url; +use reqwest::{IntoUrl, Method, RequestBuilder, Url}; use futures_lite::StreamExt; @@ -18,70 +19,40 @@ impl Client { pub async fn fetch( &self, url: &str, - init: &web_sys::RequestInit, + request_init: Option, ) -> Result { let mut url: Url = url.try_into().map_err(|err| { JsValue::from_str(&format!("pubky::Client::fetch(): Invalid `url`; {:?}", err)) })?; - self.transform_url(&mut url).await; + let request_init = request_init.unwrap_or_default(); - let js_req = - web_sys::Request::new_with_str_and_init(url.as_str(), init).map_err(|err| { + if let Some(pkarr_host) = self.prepare_request(&mut url).await { + let headers = request_init.get_headers(); + + let headers = if headers.is_null() || headers.is_undefined() { + Headers::new()? + } else { + Headers::from(headers) + }; + + headers.append("pkarr-host", &pkarr_host)?; + + request_init.set_headers(&headers.into()); + } + + let js_req = web_sys::Request::new_with_str_and_init(url.as_str(), &request_init).map_err( + |err| { JsValue::from_str(&format!( "pubky::Client::fetch(): Invalid `init`; {:?}", err )) - })?; + }, + )?; Ok(js_fetch(&js_req)) } - - pub(super) async fn transform_url(&self, url: &mut Url) { - if url.scheme() == "pubky" { - *url = Url::parse(&format!("https{}", &url.as_str()[5..])) - .expect("couldn't replace pubky:// with https://"); - url.set_host(Some(&format!("_pubky.{}", url.host_str().unwrap_or("")))) - .expect("couldn't map pubk:// to https://_pubky."); - } - - let qname = url.host_str().unwrap_or("").to_string(); - - if PublicKey::try_from(qname.to_string()).is_ok() { - let mut stream = self.pkarr.resolve_https_endpoints(&qname); - - let mut so_far: Option = None; - - // TODO: currently we return the first thing we can see, - // in the future we might want to failover to other endpoints - while so_far.is_none() { - while let Some(endpoint) = stream.next().await { - if endpoint.domain() != "." { - so_far = Some(endpoint); - } - } - } - - if let Some(e) = so_far { - // TODO: detect loopback IPs and other equivilants to localhost - if self.testnet && e.domain() == "localhost" { - url.set_scheme("http") - .expect("couldn't replace pubky:// with http://"); - } - - url.set_host(Some(e.domain())) - .expect("coultdn't use the resolved endpoint's domain"); - url.set_port(Some(e.port())) - .expect("coultdn't use the resolved endpoint's port"); - } else { - // TODO: didn't find any domain, what to do? - } - } - - log::debug!("Transformed URL to: {}", url.as_str()); - } } - #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_name = fetch)] @@ -102,3 +73,80 @@ fn js_fetch(req: &web_sys::Request) -> Promise { fetch_with_request(req) } } + +impl Client { + /// A wrapper around [reqwest::Client::request], with the same signature between native and wasm. + pub(crate) async fn inner_request(&self, method: Method, url: T) -> RequestBuilder { + let original_url = url.as_str(); + let mut url = Url::parse(original_url).expect("Invalid url in inner_request"); + + if let Some(pkarr_host) = self.prepare_request(&mut url).await { + self.http + .request(method, url.clone()) + .header::<&str, &str>("pkarr-host", &pkarr_host) + .fetch_credentials_include() + } else { + self.http + .request(method, url.clone()) + .fetch_credentials_include() + } + } + + /// - Transforms pubky:// url to http(s):// urls + /// - Resolves a clearnet host to call with fetch + /// - Returns the `pkarr-host` value if available + pub(super) async fn prepare_request(&self, url: &mut Url) -> Option { + if url.scheme() == "pubky" { + *url = Url::parse(&format!("https{}", &url.as_str()[5..])) + .expect("couldn't replace pubky:// with https://"); + url.set_host(Some(&format!("_pubky.{}", url.host_str().unwrap_or("")))) + .expect("couldn't map pubk:// to https://_pubky."); + } + + // TODO: move at the begining of the method. + let host = url.host_str().unwrap_or("").to_string(); + + let mut pkarr_host = None; + + if PublicKey::try_from(host.clone()).is_ok() { + self.transform_url(url, &host).await; + + pkarr_host = Some(host); + + log::debug!("Transformed URL to: {}", url.as_str()); + }; + + pkarr_host + } + + pub async fn transform_url(&self, url: &mut Url, qname: &str) { + let mut stream = self.pkarr.resolve_https_endpoints(qname); + + let mut so_far: Option = None; + + // TODO: currently we return the first thing we can see, + // in the future we might want to failover to other endpoints + while so_far.is_none() { + while let Some(endpoint) = stream.next().await { + if endpoint.domain() != "." { + so_far = Some(endpoint); + } + } + } + + if let Some(e) = so_far { + // TODO: detect loopback IPs and other equivilants to localhost + if self.testnet && e.domain() == "localhost" { + url.set_scheme("http") + .expect("couldn't replace pubky:// with http://"); + } + + url.set_host(Some(e.domain())) + .expect("coultdn't use the resolved endpoint's domain"); + url.set_port(Some(e.port())) + .expect("coultdn't use the resolved endpoint's port"); + } else { + // TODO: didn't find any domain, what to do? + } + } +} diff --git a/pubky/src/wasm/internals.rs b/pubky/src/wasm/internals.rs deleted file mode 100644 index 024e951..0000000 --- a/pubky/src/wasm/internals.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Wasm specific implementation of methods used in the shared module -//! - -use reqwest::{IntoUrl, Method, RequestBuilder}; -use url::Url; - -use crate::Client; - -impl Client { - /// A wrapper around [reqwest::Client::request], with the same signature between native and wasm. - pub(crate) async fn inner_request(&self, method: Method, url: T) -> RequestBuilder { - let original_url = url.as_str(); - let mut url = Url::parse(original_url).expect("Invalid url in inner_request"); - - let original_host = url.host_str().unwrap_or("").to_string(); - - self.transform_url(&mut url).await; - - self.http - .request(method, url) - .header::<&str, &str>("pkarr-host", &original_host) - .fetch_credentials_include() - } -}