diff --git a/Cargo.lock b/Cargo.lock index fb0348a..8087df4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1066,9 +1066,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1689,6 +1689,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -1899,9 +1900,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "base64 0.22.1", "bytes", diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index c3c9846..4317484 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,4 +1,4 @@ -//! Strictly monotonic unix timestamp in microseconds +//! Absolutely monotonic unix timestamp in microseconds use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -31,7 +31,7 @@ impl TimestampFactory { } pub fn now(&mut self) -> Timestamp { - // Ensure strict monotonicity. + // Ensure absolute monotonicity. self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1); // Add clock_id to the end of the timestamp @@ -48,13 +48,13 @@ impl Default for TimestampFactory { static DEFAULT_FACTORY: Lazy> = Lazy::new(|| Mutex::new(TimestampFactory::default())); -/// STrictly monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64. +/// Absolutely monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64. /// /// The purpose of this timestamp is to unique per "user", not globally, /// it achieves this by: /// 1. Override the last byte with a random `clock_id`, reducing the probability /// of two matching timestamps across multiple machines/threads. -/// 2. Gurantee that the remaining 3 bytes are ever increasing (strictly monotonic) within +/// 2. Gurantee that the remaining 3 bytes are ever increasing (absolutely monotonic) within /// the same thread regardless of the wall clock value /// /// This timestamp is also serialized as BE bytes to remain sortable. @@ -215,7 +215,7 @@ mod tests { use super::*; #[test] - fn strictly_monotonic() { + fn absolutely_monotonic() { const COUNT: usize = 100; let mut set = HashSet::with_capacity(COUNT); diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 6e3dea5..ba699b7 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -168,8 +168,8 @@ impl Config { self.bootstrap.to_owned() } - pub fn domain(&self) -> &Option { - &self.domain + pub fn domain(&self) -> Option<&String> { + self.domain.as_ref() } pub fn keypair(&self) -> &Keypair { diff --git a/pubky-homeserver/src/config.toml b/pubky-homeserver/src/config.toml index 2012efc..cb47003 100644 --- a/pubky-homeserver/src/config.toml +++ b/pubky-homeserver/src/config.toml @@ -1,10 +1,14 @@ # Use testnet network (local DHT) for testing. -testnet = false +# testnet = false + # Secret key (in hex) to generate the Homeserver's Keypair -secret_key = "0000000000000000000000000000000000000000000000000000000000000000" -# Domain to be published in Pkarr records for this server to be accessible by. -domain = "localhost" +# secret_key = "0000000000000000000000000000000000000000000000000000000000000000" + +# ICANN domain pointing to this server to allow browsers to connect to it. +# domain = "example.com" + # Port for the Homeserver to listen on. port = 6287 + # Storage directory Defaults to # storage = "" diff --git a/pubky-homeserver/src/pkarr.rs b/pubky-homeserver/src/pkarr.rs index 415baa0..26c6b66 100644 --- a/pubky-homeserver/src/pkarr.rs +++ b/pubky-homeserver/src/pkarr.rs @@ -1,44 +1,63 @@ //! Pkarr related task +use std::net::Ipv4Addr; + use pkarr::{ - dns::{rdata::SVCB, Packet}, - Keypair, SignedPacket, + dns::{ + rdata::{RData, A, SVCB}, + Packet, + }, + SignedPacket, }; +use crate::config::Config; + pub async fn publish_server_packet( pkarr_client: &pkarr::Client, - keypair: &Keypair, - domain: &str, + config: &Config, port: u16, ) -> anyhow::Result<()> { // TODO: Try to resolve first before publishing. let mut packet = Packet::new_reply(0); - let mut svcb = SVCB::new(0, domain.try_into()?); + let default = ".".to_string(); + let target = config.domain().unwrap_or(&default); + let mut svcb = SVCB::new(0, target.as_str().try_into()?); - // Publishing port only for localhost domain, - // assuming any other domain will point to a reverse proxy - // at the conventional ports. - if domain == "localhost" { - svcb.priority = 1; - svcb.set_port(port); - - // TODO: Add more parameteres like the signer key! - // svcb.set_param(key, value) - }; - - // TODO: announce A/AAAA records as well for Noise connections? - // Or maybe Iroh's magic socket + svcb.priority = 1; + svcb.set_port(port); packet.answers.push(pkarr::dns::ResourceRecord::new( "@".try_into().unwrap(), pkarr::dns::CLASS::IN, 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), + RData::HTTPS(svcb.clone().into()), )); - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + if config.domain().is_none() { + // TODO: remove after remvoing Pubky shared/public + // and add local host IP address instead. + svcb.target = "localhost".try_into().unwrap(); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "@".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + RData::HTTPS(svcb.clone().into()), + )); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "@".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + RData::A(A::from(Ipv4Addr::from([127, 0, 0, 1]))), + )); + } + + // TODO: announce A/AAAA records as well for TLS connections? + + let signed_packet = SignedPacket::from_packet(config.keypair(), &packet)?; pkarr_client.publish(&signed_packet).await?; diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 89d9dd6..15c4b3f 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -51,7 +51,7 @@ impl Homeserver { let state = AppState { verifier: AuthVerifier::default(), db, - pkarr_client, + pkarr_client: pkarr_client.clone(), config: config.clone(), port, }; @@ -70,20 +70,10 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - publish_server_packet( - &state.pkarr_client, - config.keypair(), - &state - .config - .domain() - .clone() - .unwrap_or("localhost".to_string()), - port, - ) - .await?; + publish_server_packet(&pkarr_client, &config, port).await?; info!( - "Homeserver listening on pubky://{}", + "Homeserver listening on http://{}", config.keypair().public_key() ); diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 9caaf33..31d15d2 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -18,19 +18,23 @@ bytes = "^1.7.1" base64 = "0.22.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } -pkarr = { workspace = true } +pkarr = { workspace = true, features = ["endpoints"] } +# Native dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false } tokio = { version = "1.37.0", features = ["full"] } +# Wasm dependencies [target.'cfg(target_arch = "wasm32")'.dependencies] reqwest = { version = "0.12.5", default-features = false } -js-sys = "0.3.69" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" +js-sys = "0.3.69" +web-sys = "0.3.70" + [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore index 7355b75..d2a005f 100644 --- a/pubky/pkg/.gitignore +++ b/pubky/pkg/.gitignore @@ -1,4 +1,3 @@ -index.cjs browser.js coverage node_modules diff --git a/pubky/pkg/index.cjs b/pubky/pkg/index.cjs new file mode 100644 index 0000000..bd8a6a2 --- /dev/null +++ b/pubky/pkg/index.cjs @@ -0,0 +1,6 @@ +const makeFetchCookie = require("fetch-cookie").default; + +let originalFetch = globalThis.fetch; +globalThis.fetch = makeFetchCookie(originalFetch); + +module.exports = require('./nodejs/pubky') diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index be3e6df..1c08264 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -37,5 +37,8 @@ "esmify": "^2.1.1", "tape": "^5.8.1", "tape-run": "^11.0.0" + }, + "dependencies": { + "fetch-cookie": "^3.0.1" } } diff --git a/pubky/pkg/test/http.js b/pubky/pkg/test/http.js new file mode 100644 index 0000000..a11a63d --- /dev/null +++ b/pubky/pkg/test/http.js @@ -0,0 +1,26 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.cjs' + +const TLD = '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'; + +// TODO: test HTTPs too somehow. + +test("basic fetch", async (t) => { + let client = PubkyClient.testnet(); + + // Normal TLD + { + + let response = await client.fetch(`http://relay.pkarr.org/`); + + t.equal(response.status, 200); + } + + + // Pubky + let response = await client.fetch(`http://${TLD}/`); + + t.equal(response.status, 200); +}) + diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index a8ed503..86983b6 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -55,12 +55,3 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `. ); await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched + "\nglobalThis['pubky'] = imports"); - -// Move outside of nodejs - -await Promise.all([".js", ".d.ts", "_bg.wasm"].map(suffix => - rename( - path.join(__dirname, `../../pkg/nodejs/${name}${suffix}`), - path.join(__dirname, `../../pkg/${suffix === '.js' ? "index.cjs" : (name + suffix)}`), - )) -) diff --git a/pubky/src/error.rs b/pubky/src/error.rs index c8d80e1..ec6d125 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -12,9 +12,6 @@ pub enum Error { #[error("Generic error: {0}")] Generic(String), - #[error("Could not resolve endpoint for {0}")] - ResolveEndpoint(String), - #[error("Could not convert the passed type into a Url")] InvalidUrl, @@ -42,6 +39,9 @@ pub enum Error { #[error(transparent)] AuthToken(#[from] pubky_common::auth::Error), + + #[error("Could not resolve Endpoint for domain: {0}")] + ResolveEndpoint(String), } #[cfg(target_arch = "wasm32")] diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 9e22a3b..b251d3e 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -6,11 +6,6 @@ mod native; #[cfg(target_arch = "wasm32")] mod wasm; -#[cfg(target_arch = "wasm32")] -use std::{ - collections::HashSet, - sync::{Arc, RwLock}, -}; use wasm_bindgen::prelude::*; @@ -23,11 +18,5 @@ pub use crate::shared::list_builder::ListBuilder; #[wasm_bindgen] pub struct PubkyClient { http: reqwest::Client, - #[cfg(not(target_arch = "wasm32"))] pub(crate) pkarr: pkarr::Client, - /// A cookie jar for nodejs fetch. - #[cfg(target_arch = "wasm32")] - pub(crate) session_cookies: Arc>>, - #[cfg(target_arch = "wasm32")] - pub(crate) pkarr_relays: Vec, } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 85b6edb..83406f6 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,25 +1,14 @@ -use std::net::ToSocketAddrs; use std::time::Duration; +use std::{net::ToSocketAddrs, sync::Arc}; -use bytes::Bytes; -use pubky_common::{ - capabilities::Capabilities, - recovery_file::{create_recovery_file, decrypt_recovery_file}, - session::Session, -}; -use reqwest::{RequestBuilder, Response}; -use tokio::sync::oneshot; -use url::Url; +use ::pkarr::mainline::dht::Testnet; -use pkarr::Keypair; +use crate::PubkyClient; -use ::pkarr::{mainline::dht::Testnet, PublicKey, SignedPacket}; +mod api; +mod internals; -use crate::{ - error::{Error, Result}, - shared::list_builder::ListBuilder, - PubkyClient, -}; +use internals::PkarrResolver; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -63,13 +52,19 @@ impl PubkyClientBuilder { /// Build [PubkyClient] pub fn build(self) -> PubkyClient { + // TODO: convert to Result + + let pkarr = pkarr::Client::new(self.pkarr_settings).unwrap(); + let dns_resolver: PkarrResolver = (&pkarr).into(); + PubkyClient { http: reqwest::Client::builder() .cookie_store(true) + .dns_resolver(Arc::new(dns_resolver)) .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), - pkarr: pkarr::Client::new(self.pkarr_settings).unwrap(), + pkarr, } } } @@ -80,8 +75,6 @@ impl Default for PubkyClient { } } -// === Public API === - impl PubkyClient { /// Returns a builder to edit settings before creating [PubkyClient]. pub fn builder() -> PubkyClientBuilder { @@ -111,148 +104,4 @@ impl PubkyClient { builder.build() } - - // === Getters === - - /// Returns a reference to the internal [pkarr] Client. - pub fn pkarr(&self) -> &pkarr::Client { - &self.pkarr - } - - // === Auth === - - /// 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 async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result { - self.inner_signup(keypair, homeserver).await - } - - /// Check the current sesison for a given Pubky in its homeserver. - /// - /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), - /// or [reqwest::Error] if the response has any other `>=400` status code. - pub async fn session(&self, pubky: &PublicKey) -> Result> { - self.inner_session(pubky).await - } - - /// Signout from a homeserver. - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - self.inner_signout(pubky).await - } - - /// Signin to a homeserver. - pub async fn signin(&self, keypair: &Keypair) -> Result { - self.inner_signin(keypair).await - } - - // === Public data === - - /// Upload a small payload to a given path. - 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, url: T) -> Result> { - self.inner_get(url).await - } - - /// Delete a file at a path relative to a pubky author. - pub async fn delete>(&self, url: T) -> Result<()> { - self.inner_delete(url).await - } - - /// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send]. - /// - /// `url` sets the path you want to lest within. - pub fn list>(&self, url: T) -> Result { - self.inner_list(url) - } - - // === Helpers === - - /// Create a recovery file of the `keypair`, containing the secret key encrypted - /// using the `passphrase`. - pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { - Ok(create_recovery_file(keypair, passphrase)?) - } - - /// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`. - pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { - Ok(decrypt_recovery_file(recovery_file, passphrase)?) - } - - /// Return `pubkyauth://` url and wait for the incoming [AuthToken] - /// verifying that AuthToken, and if capabilities were requested, signing in to - /// the Pubky's homeserver and returning the [Session] information. - pub fn auth_request( - &self, - relay: impl TryInto, - capabilities: &Capabilities, - ) -> Result<(Url, tokio::sync::oneshot::Receiver)> { - let mut relay: Url = relay - .try_into() - .map_err(|_| Error::Generic("Invalid relay Url".into()))?; - - let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?; - - let (tx, rx) = oneshot::channel::(); - - let this = self.clone(); - - tokio::spawn(async move { - let to_send = this - .subscribe_to_auth_response(relay, &client_secret) - .await?; - - tx.send(to_send) - .map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?; - - Ok::<(), Error>(()) - }); - - Ok((pubkyauth_url, rx)) - } - - /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the - /// source of the pubkyauth request url. - pub async fn send_auth_token>( - &self, - keypair: &Keypair, - pubkyauth_url: T, - ) -> Result<()> { - let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?; - - self.inner_send_auth_token(keypair, url).await?; - - Ok(()) - } -} - -// === Internals === - -impl PubkyClient { - // === Pkarr === - - pub(crate) async fn pkarr_resolve( - &self, - public_key: &PublicKey, - ) -> Result> { - Ok(self.pkarr.resolve(public_key).await?) - } - - pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { - Ok(self.pkarr.publish(signed_packet).await?) - } - - // === HTTP === - - pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { - self.http.request(method, url) - } - - pub(crate) fn store_session(&self, _: &Response) {} - pub(crate) fn remove_session(&self, _: &PublicKey) {} } diff --git a/pubky/src/native/api/auth.rs b/pubky/src/native/api/auth.rs new file mode 100644 index 0000000..e9ea098 --- /dev/null +++ b/pubky/src/native/api/auth.rs @@ -0,0 +1,85 @@ +use pkarr::Keypair; +use pubky_common::session::Session; +use tokio::sync::oneshot; +use url::Url; + +use pkarr::PublicKey; + +use pubky_common::capabilities::Capabilities; + +use crate::error::{Error, Result}; +use crate::PubkyClient; + +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 async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result { + self.inner_signup(keypair, homeserver).await + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or [reqwest::Error] if the response has any other `>=400` status code. + pub async fn session(&self, pubky: &PublicKey) -> Result> { + self.inner_session(pubky).await + } + + /// Signout from a homeserver. + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + self.inner_signout(pubky).await + } + + /// Signin to a homeserver. + pub async fn signin(&self, keypair: &Keypair) -> Result { + self.inner_signin(keypair).await + } + + /// Return `pubkyauth://` url and wait for the incoming [AuthToken] + /// verifying that AuthToken, and if capabilities were requested, signing in to + /// the Pubky's homeserver and returning the [Session] information. + pub fn auth_request( + &self, + relay: impl TryInto, + capabilities: &Capabilities, + ) -> Result<(Url, tokio::sync::oneshot::Receiver)> { + let mut relay: Url = relay + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?; + + let (tx, rx) = oneshot::channel::(); + + let this = self.clone(); + + tokio::spawn(async move { + let to_send = this + .subscribe_to_auth_response(relay, &client_secret) + .await?; + + tx.send(to_send) + .map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?; + + Ok::<(), Error>(()) + }); + + Ok((pubkyauth_url, rx)) + } + + /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the + /// source of the pubkyauth request url. + pub async fn send_auth_token>( + &self, + keypair: &Keypair, + pubkyauth_url: T, + ) -> Result<()> { + let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?; + + self.inner_send_auth_token(keypair, url).await?; + + Ok(()) + } +} diff --git a/pubky/src/native/api/http.rs b/pubky/src/native/api/http.rs new file mode 100644 index 0000000..8ba46d5 --- /dev/null +++ b/pubky/src/native/api/http.rs @@ -0,0 +1,65 @@ +use reqwest::{IntoUrl, Method, RequestBuilder}; + +use crate::PubkyClient; + +impl PubkyClient { + /// Start building a `Request` with the `Method` and `Url`. + /// + /// Returns a `RequestBuilder`, which will allow setting headers and + /// the request body before sending. + /// + /// Differs from [reqwest::Client::request], in that it can make requests + /// to URLs with a [pkarr::PublicKey] as Top Level Domain, by resolving + /// corresponding endpoints, and verifying TLS certificates accordingly. + /// + /// # Errors + /// + /// This method fails whenever the supplied `Url` cannot be parsed. + pub fn request(&self, method: Method, url: U) -> RequestBuilder { + self.http.request(method, url) + } +} + +#[cfg(test)] +mod tests { + use pkarr::mainline::Testnet; + use pubky_homeserver::Homeserver; + + use crate::*; + + #[tokio::test] + async fn http_get_pubky() { + let testnet = Testnet::new(10).unwrap(); + + let homeserver = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::builder().testnet(&testnet).build(); + + let url = format!("http://{}/", homeserver.public_key()); + + let response = client + .request(Default::default(), url) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 200) + } + + #[tokio::test] + async fn http_get_icann() { + let testnet = Testnet::new(10).unwrap(); + + let client = PubkyClient::builder().testnet(&testnet).build(); + + let url = format!("http://example.com/"); + + let response = client + .request(Default::default(), url) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), 200); + } +} diff --git a/pubky/src/native/api/mod.rs b/pubky/src/native/api/mod.rs new file mode 100644 index 0000000..a502667 --- /dev/null +++ b/pubky/src/native/api/mod.rs @@ -0,0 +1,6 @@ +pub mod http; +pub mod recovery_file; + +// TODO: put the Homeserver API behind a feature flag +pub mod auth; +pub mod public; diff --git a/pubky/src/native/api/public.rs b/pubky/src/native/api/public.rs new file mode 100644 index 0000000..dfee053 --- /dev/null +++ b/pubky/src/native/api/public.rs @@ -0,0 +1,28 @@ +use bytes::Bytes; +use url::Url; + +use crate::{error::Result, shared::list_builder::ListBuilder, PubkyClient}; + +impl PubkyClient { + /// Upload a small payload to a given path. + 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, url: T) -> Result> { + self.inner_get(url).await + } + + /// Delete a file at a path relative to a pubky author. + pub async fn delete>(&self, url: T) -> Result<()> { + self.inner_delete(url).await + } + + /// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send]. + /// + /// `url` sets the path you want to lest within. + pub fn list>(&self, url: T) -> Result { + self.inner_list(url) + } +} diff --git a/pubky/src/native/api/recovery_file.rs b/pubky/src/native/api/recovery_file.rs new file mode 100644 index 0000000..0eace09 --- /dev/null +++ b/pubky/src/native/api/recovery_file.rs @@ -0,0 +1,19 @@ +use pubky_common::{ + crypto::Keypair, + recovery_file::{create_recovery_file, decrypt_recovery_file}, +}; + +use crate::{error::Result, PubkyClient}; + +impl PubkyClient { + /// Create a recovery file of the `keypair`, containing the secret key encrypted + /// using the `passphrase`. + pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result> { + Ok(create_recovery_file(keypair, passphrase)?) + } + + /// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`. + pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result { + Ok(decrypt_recovery_file(recovery_file, passphrase)?) + } +} diff --git a/pubky/src/native/internals.rs b/pubky/src/native/internals.rs new file mode 100644 index 0000000..4e9f593 --- /dev/null +++ b/pubky/src/native/internals.rs @@ -0,0 +1,45 @@ +use reqwest::RequestBuilder; +use url::Url; + +use crate::PubkyClient; + +use std::net::ToSocketAddrs; + +use pkarr::{Client, EndpointResolver, PublicKey}; +use reqwest::dns::{Addrs, Resolve}; + +pub struct PkarrResolver(Client); + +impl Resolve for PkarrResolver { + fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { + let client = self.0.clone(); + + Box::pin(async move { + let name = name.as_str(); + + if PublicKey::try_from(name).is_ok() { + let endpoint = client.resolve_endpoint(name).await?; + + let addrs: Addrs = Box::new(endpoint.to_socket_addrs().into_iter()); + return Ok(addrs); + }; + + Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap())) + }) + } +} + +impl From<&pkarr::Client> for PkarrResolver { + fn from(pkarr: &pkarr::Client) -> Self { + PkarrResolver(pkarr.clone()) + } +} + +impl PubkyClient { + // === HTTP === + + /// A wrapper around [reqwest::Client::request], with the same signature between native and wasm. + pub(crate) async fn inner_request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + self.http.request(method, url) + } +} diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index d36e849..c545adc 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -38,13 +38,12 @@ impl PubkyClient { let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize(); let response = self - .request(Method::POST, url.clone()) + .inner_request(Method::POST, url.clone()) + .await .body(body) .send() .await?; - self.store_session(&response); - self.publish_pubky_homeserver(keypair, &homeserver).await?; let bytes = response.bytes().await?; @@ -61,7 +60,7 @@ impl PubkyClient { url.set_path(&format!("/{}/session", pubky)); - let res = self.request(Method::GET, url).send().await?; + let res = self.inner_request(Method::GET, url).await.send().await?; if res.status() == StatusCode::NOT_FOUND { return Ok(None); @@ -82,9 +81,7 @@ impl PubkyClient { url.set_path(&format!("/{}/session", pubky)); - self.request(Method::DELETE, url).send().await?; - - self.remove_session(pubky); + self.inner_request(Method::DELETE, url).await.send().await?; Ok(()) } @@ -143,7 +140,8 @@ impl PubkyClient { path_segments.push(&channel_id); drop(path_segments); - self.request(Method::POST, callback) + self.inner_request(Method::POST, callback) + .await .body(encrypted_token) .send() .await?; @@ -170,13 +168,12 @@ impl PubkyClient { self.resolve_url(&mut url).await?; let response = self - .request(Method::POST, url) + .inner_request(Method::POST, url) + .await .body(token.serialize()) .send() .await?; - self.store_session(&response); - let bytes = response.bytes().await?; Ok(Session::deserialize(&bytes)?) @@ -309,6 +306,9 @@ mod tests { assert_eq!(&public_key, &pubky); + let session = client.session(&pubky).await.unwrap().unwrap(); + assert_eq!(session.capabilities(), &capabilities.0); + // Test access control enforcement client diff --git a/pubky/src/shared/list_builder.rs b/pubky/src/shared/list_builder.rs index 0eaec77..b76fee4 100644 --- a/pubky/src/shared/list_builder.rs +++ b/pubky/src/shared/list_builder.rs @@ -90,7 +90,12 @@ impl<'a> ListBuilder<'a> { drop(query); - let response = self.client.request(Method::GET, url).send().await?; + let response = self + .client + .inner_request(Method::GET, url) + .await + .send() + .await?; response.error_for_status_ref()?; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index 82f3adf..9e2fba7 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -19,7 +19,7 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let existing = self.pkarr_resolve(&keypair.public_key()).await?; + let existing = self.pkarr.resolve(&keypair.public_key()).await?; let mut packet = Packet::new_reply(0); @@ -42,7 +42,7 @@ impl PubkyClient { let signed_packet = SignedPacket::from_packet(keypair, &packet)?; - self.pkarr_publish(&signed_packet).await?; + self.pkarr.publish(&signed_packet).await?; Ok(()) } @@ -81,7 +81,8 @@ impl PubkyClient { step += 1; if let Some(signed_packet) = self - .pkarr_resolve(&public_key) + .pkarr + .resolve(&public_key) .await .map_err(|_| Error::ResolveEndpoint(original_target.into()))? { @@ -185,8 +186,8 @@ mod tests { rdata::{HTTPS, SVCB}, Packet, }, - mainline::{dht::DhtSettings, Testnet}, - Keypair, Settings, SignedPacket, + mainline::Testnet, + Keypair, SignedPacket, }; use pubky_homeserver::Homeserver; @@ -194,14 +195,7 @@ mod tests { async fn resolve_endpoint_https() { let testnet = Testnet::new(10).unwrap(); - let pkarr_client = pkarr::Client::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap(); + let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap(); let domain = "example.com"; let mut target; @@ -284,14 +278,7 @@ mod tests { let server = Homeserver::start_test(&testnet).await.unwrap(); // Publish an intermediate controller of the homeserver - let pkarr_client = pkarr::Client::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap(); + let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap(); let intermediate = Keypair::random(); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 43973d8..6ded72a 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -16,7 +16,8 @@ impl PubkyClient { let url = self.pubky_to_http(url).await?; let response = self - .request(Method::PUT, url) + .inner_request(Method::PUT, url) + .await .body(content.to_owned()) .send() .await?; @@ -29,7 +30,7 @@ impl PubkyClient { pub(crate) 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?; + let response = self.inner_request(Method::GET, url).await.send().await?; if response.status() == StatusCode::NOT_FOUND { return Ok(None); @@ -46,7 +47,7 @@ impl PubkyClient { pub(crate) 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?; + let response = self.inner_request(Method::DELETE, url).await.send().await?; response.error_for_status_ref()?; @@ -650,10 +651,7 @@ mod tests { { let response = client - .request( - Method::GET, - format!("{feed_url}?limit=10").as_str().try_into().unwrap(), - ) + .request(Method::GET, format!("{feed_url}?limit=10")) .send() .await .unwrap(); @@ -683,13 +681,7 @@ mod tests { { let response = client - .request( - Method::GET, - format!("{feed_url}?limit=10&cursor={cursor}") - .as_str() - .try_into() - .unwrap(), - ) + .request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}")) .send() .await .unwrap(); @@ -740,10 +732,7 @@ mod tests { { let response = client - .request( - Method::GET, - format!("{feed_url}?limit=10").as_str().try_into().unwrap(), - ) + .request(Method::GET, format!("{feed_url}?limit=10")) .send() .await .unwrap(); @@ -762,8 +751,9 @@ mod tests { ); } - let get = client.get(url.as_str()).await.unwrap(); - dbg!(get); + let get = client.get(url.as_str()).await.unwrap().unwrap(); + + assert_eq!(get.as_ref(), &[0]); } #[tokio::test] @@ -800,10 +790,7 @@ mod tests { let feed_url = format!("http://localhost:{}/events/", homeserver.port()); let response = client - .request( - Method::GET, - format!("{feed_url}").as_str().try_into().unwrap(), - ) + .request(Method::GET, format!("{feed_url}")) .send() .await .unwrap(); diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index cbbf71b..e983700 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,26 +1,10 @@ -use std::{ - collections::HashSet, - sync::{Arc, RwLock}, -}; - -use js_sys::{Array, Uint8Array}; use wasm_bindgen::prelude::*; -use url::Url; - -use pubky_common::capabilities::Capabilities; - -use crate::error::Error; use crate::PubkyClient; -mod http; -mod keys; -mod pkarr; -mod recovery_file; -mod session; - -use keys::{Keypair, PublicKey}; -use session::Session; +mod api; +mod internals; +mod wrappers; impl Default for PubkyClient { fn default() -> Self { @@ -28,7 +12,6 @@ impl Default for PubkyClient { } } -static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/pkarr"]; #[wasm_bindgen] @@ -37,8 +20,7 @@ impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::builder().build().unwrap(), - session_cookies: Arc::new(RwLock::new(HashSet::new())), - pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(), + pkarr: pkarr::Client::builder().build().unwrap(), } } @@ -48,203 +30,10 @@ impl PubkyClient { pub fn testnet() -> Self { Self { http: reqwest::Client::builder().build().unwrap(), - session_cookies: Arc::new(RwLock::new(HashSet::new())), - pkarr_relays: TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect(), + pkarr: pkarr::Client::builder() + .relays(TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect()) + .build() + .unwrap(), } } - - /// Set Pkarr relays used for publishing and resolving Pkarr packets. - /// - /// By default, [PubkyClient] will use `["https://relay.pkarr.org"]` - #[wasm_bindgen(js_name = "setPkarrRelays")] - pub fn set_pkarr_relays(mut self, relays: Vec) -> Self { - self.pkarr_relays = relays; - self - } - - // Read the set of pkarr relays used by this client. - #[wasm_bindgen(js_name = "getPkarrRelays")] - pub fn get_pkarr_relays(&self) -> Vec { - self.pkarr_relays.clone() - } - - /// 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" - #[wasm_bindgen] - pub async fn signup( - &self, - keypair: &Keypair, - homeserver: &PublicKey, - ) -> Result { - Ok(Session( - self.inner_signup(keypair.as_inner(), homeserver.as_inner()) - .await - .map_err(JsValue::from)?, - )) - } - - /// Check the current sesison for a given Pubky in its homeserver. - /// - /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), - /// or throws the recieved error if the response has any other `>=400` status code. - #[wasm_bindgen] - pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { - self.inner_session(pubky.as_inner()) - .await - .map(|s| s.map(Session)) - .map_err(|e| e.into()) - } - - /// Signout from a homeserver. - #[wasm_bindgen] - pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> { - self.inner_signout(pubky.as_inner()) - .await - .map_err(|e| e.into()) - } - - /// Signin to a homeserver using the root Keypair. - #[wasm_bindgen] - pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> { - self.inner_signin(keypair.as_inner()) - .await - .map(|_| ()) - .map_err(|e| e.into()) - } - - /// Return `pubkyauth://` url and wait for the incoming [AuthToken] - /// verifying that AuthToken, and if capabilities were requested, signing in to - /// the Pubky's homeserver and returning the [Session] information. - /// - /// Returns a tuple of [pubkyAuthUrl, Promise] - #[wasm_bindgen(js_name = "authRequest")] - pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result { - let mut relay: Url = relay - .try_into() - .map_err(|_| Error::Generic("Invalid relay Url".into()))?; - - let (pubkyauth_url, client_secret) = self.create_auth_request( - &mut relay, - &Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?, - )?; - - let this = self.clone(); - - let future = async move { - this.subscribe_to_auth_response(relay, &client_secret) - .await - .map(|pubky| JsValue::from(PublicKey(pubky))) - .map_err(|err| JsValue::from_str(&format!("{:?}", err))) - }; - - let promise = wasm_bindgen_futures::future_to_promise(future); - - // Return the URL and the promise - let js_tuple = js_sys::Array::new(); - js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref())); - js_tuple.push(&promise); - - Ok(js_tuple) - } - - /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the - /// source of the pubkyauth request url. - #[wasm_bindgen(js_name = "sendAuthToken")] - pub async fn send_auth_token( - &self, - keypair: &Keypair, - pubkyauth_url: &str, - ) -> Result<(), JsValue> { - let pubkyauth_url: Url = pubkyauth_url - .try_into() - .map_err(|_| Error::Generic("Invalid relay Url".into()))?; - - self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url) - .await?; - - Ok(()) - } - - // === Public data === - - #[wasm_bindgen] - /// Upload a small payload to a given path. - pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> { - self.inner_put(url, content).await.map_err(|e| e.into()) - } - - /// Download a small payload from a given path relative to a pubky author. - #[wasm_bindgen] - 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()) - } - - /// Delete a file at a path relative to a pubky author. - #[wasm_bindgen] - pub async fn delete(&self, url: &str) -> Result<(), JsValue> { - self.inner_delete(url).await.map_err(|e| e.into()) - } - - /// Returns a list of Pubky urls (as strings). - /// - /// - `url`: The Pubky url (string) to the directory you want to list its content. - /// - `cursor`: Either a full `pubky://` Url (from previous list response), - /// or a path (to a file or directory) relative to the `url` - /// - `reverse`: List in reverse order - /// - `limit` Limit the number of urls in the response - /// - `shallow`: List directories and files, instead of flat list of files. - #[wasm_bindgen] - pub async fn list( - &self, - url: &str, - cursor: Option, - reverse: Option, - limit: Option, - shallow: Option, - ) -> Result { - // TODO: try later to return Vec from async function. - - if let Some(cursor) = cursor { - return self - .inner_list(url)? - .reverse(reverse.unwrap_or(false)) - .limit(limit.unwrap_or(u16::MAX)) - .cursor(&cursor) - .shallow(shallow.unwrap_or(false)) - .send() - .await - .map(|urls| { - let js_array = Array::new(); - - for url in urls { - js_array.push(&JsValue::from_str(&url)); - } - - js_array - }) - .map_err(|e| e.into()); - } - - self.inner_list(url)? - .reverse(reverse.unwrap_or(false)) - .limit(limit.unwrap_or(u16::MAX)) - .shallow(shallow.unwrap_or(false)) - .send() - .await - .map(|urls| { - let js_array = Array::new(); - - for url in urls { - js_array.push(&JsValue::from_str(&url)); - } - - js_array - }) - .map_err(|e| e.into()) - } } diff --git a/pubky/src/wasm/api/auth.rs b/pubky/src/wasm/api/auth.rs new file mode 100644 index 0000000..d75c716 --- /dev/null +++ b/pubky/src/wasm/api/auth.rs @@ -0,0 +1,113 @@ +use url::Url; + +use pubky_common::capabilities::Capabilities; + +use crate::Error; +use crate::PubkyClient; + +use crate::wasm::wrappers::keys::{Keypair, PublicKey}; +use crate::wasm::wrappers::session::Session; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +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" + #[wasm_bindgen] + pub async fn signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result { + Ok(Session( + self.inner_signup(keypair.as_inner(), homeserver.as_inner()) + .await + .map_err(JsValue::from)?, + )) + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns [Session] or `None` (if recieved `404 NOT_FOUND`), + /// or throws the recieved error if the response has any other `>=400` status code. + #[wasm_bindgen] + pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { + self.inner_session(pubky.as_inner()) + .await + .map(|s| s.map(Session)) + .map_err(|e| e.into()) + } + + /// Signout from a homeserver. + #[wasm_bindgen] + pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> { + self.inner_signout(pubky.as_inner()) + .await + .map_err(|e| e.into()) + } + + /// Signin to a homeserver using the root Keypair. + #[wasm_bindgen] + pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> { + self.inner_signin(keypair.as_inner()) + .await + .map(|_| ()) + .map_err(|e| e.into()) + } + + /// Return `pubkyauth://` url and wait for the incoming [AuthToken] + /// verifying that AuthToken, and if capabilities were requested, signing in to + /// the Pubky's homeserver and returning the [Session] information. + /// + /// Returns a tuple of [pubkyAuthUrl, Promise] + #[wasm_bindgen(js_name = "authRequest")] + pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result { + let mut relay: Url = relay + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + let (pubkyauth_url, client_secret) = self.create_auth_request( + &mut relay, + &Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?, + )?; + + let this = self.clone(); + + let future = async move { + this.subscribe_to_auth_response(relay, &client_secret) + .await + .map(|pubky| JsValue::from(PublicKey(pubky))) + .map_err(|err| JsValue::from_str(&format!("{:?}", err))) + }; + + let promise = wasm_bindgen_futures::future_to_promise(future); + + // Return the URL and the promise + let js_tuple = js_sys::Array::new(); + js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref())); + js_tuple.push(&promise); + + Ok(js_tuple) + } + + /// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the + /// source of the pubkyauth request url. + #[wasm_bindgen(js_name = "sendAuthToken")] + pub async fn send_auth_token( + &self, + keypair: &Keypair, + pubkyauth_url: &str, + ) -> Result<(), JsValue> { + let pubkyauth_url: Url = pubkyauth_url + .try_into() + .map_err(|_| Error::Generic("Invalid relay Url".into()))?; + + self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url) + .await?; + + Ok(()) + } +} diff --git a/pubky/src/wasm/api/http.rs b/pubky/src/wasm/api/http.rs new file mode 100644 index 0000000..46c5733 --- /dev/null +++ b/pubky/src/wasm/api/http.rs @@ -0,0 +1,56 @@ +//! Fetch method handling HTTP and Pubky urls with Pkarr TLD. + +use js_sys::Promise; +use wasm_bindgen::prelude::*; + +use reqwest::Url; + +use crate::PubkyClient; + +use super::super::internals::resolve; + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen] + pub async fn fetch( + &self, + url: &str, + init: &web_sys::RequestInit, + ) -> Result { + let mut url: Url = url.try_into().map_err(|err| { + JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `url`; {:?}", err)) + })?; + + resolve(&self.pkarr, &mut url) + .await + .map_err(|err| JsValue::from_str(&format!("PubkyClient::fetch(): {:?}", err)))?; + + let js_req = + web_sys::Request::new_with_str_and_init(url.as_str(), init).map_err(|err| { + JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `init`; {:?}", err)) + })?; + + Ok(js_fetch(&js_req)) + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = fetch)] + fn fetch_with_request(input: &web_sys::Request) -> Promise; +} + +fn js_fetch(req: &web_sys::Request) -> Promise { + use wasm_bindgen::{JsCast, JsValue}; + let global = js_sys::global(); + + if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope")) + { + global + .unchecked_into::() + .fetch_with_request(req) + } else { + // browser + fetch_with_request(req) + } +} diff --git a/pubky/src/wasm/api/mod.rs b/pubky/src/wasm/api/mod.rs new file mode 100644 index 0000000..a502667 --- /dev/null +++ b/pubky/src/wasm/api/mod.rs @@ -0,0 +1,6 @@ +pub mod http; +pub mod recovery_file; + +// TODO: put the Homeserver API behind a feature flag +pub mod auth; +pub mod public; diff --git a/pubky/src/wasm/api/public.rs b/pubky/src/wasm/api/public.rs new file mode 100644 index 0000000..9bd8a17 --- /dev/null +++ b/pubky/src/wasm/api/public.rs @@ -0,0 +1,87 @@ +use wasm_bindgen::prelude::*; + +use js_sys::{Array, Uint8Array}; + +use crate::PubkyClient; + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen] + /// Upload a small payload to a given path. + pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(url, content).await.map_err(|e| e.into()) + } + + /// Download a small payload from a given path relative to a pubky author. + #[wasm_bindgen] + 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()) + } + + /// Delete a file at a path relative to a pubky author. + #[wasm_bindgen] + pub async fn delete(&self, url: &str) -> Result<(), JsValue> { + self.inner_delete(url).await.map_err(|e| e.into()) + } + + /// Returns a list of Pubky urls (as strings). + /// + /// - `url`: The Pubky url (string) to the directory you want to list its content. + /// - `cursor`: Either a full `pubky://` Url (from previous list response), + /// or a path (to a file or directory) relative to the `url` + /// - `reverse`: List in reverse order + /// - `limit` Limit the number of urls in the response + /// - `shallow`: List directories and files, instead of flat list of files. + #[wasm_bindgen] + pub async fn list( + &self, + url: &str, + cursor: Option, + reverse: Option, + limit: Option, + shallow: Option, + ) -> Result { + // TODO: try later to return Vec from async function. + + if let Some(cursor) = cursor { + return self + .inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .cursor(&cursor) + .shallow(shallow.unwrap_or(false)) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()); + } + + self.inner_list(url)? + .reverse(reverse.unwrap_or(false)) + .limit(limit.unwrap_or(u16::MAX)) + .shallow(shallow.unwrap_or(false)) + .send() + .await + .map(|urls| { + let js_array = Array::new(); + + for url in urls { + js_array.push(&JsValue::from_str(&url)); + } + + js_array + }) + .map_err(|e| e.into()) + } +} diff --git a/pubky/src/wasm/recovery_file.rs b/pubky/src/wasm/api/recovery_file.rs similarity index 95% rename from pubky/src/wasm/recovery_file.rs rename to pubky/src/wasm/api/recovery_file.rs index 7b85178..89cff37 100644 --- a/pubky/src/wasm/recovery_file.rs +++ b/pubky/src/wasm/api/recovery_file.rs @@ -3,7 +3,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; use crate::error::Error; -use super::keys::Keypair; +use crate::wasm::wrappers::keys::Keypair; /// Create a recovery file of the `keypair`, containing the secret key encrypted /// using the `passphrase`. diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs deleted file mode 100644 index 61fee29..0000000 --- a/pubky/src/wasm/http.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::PubkyClient; - -use reqwest::{Method, RequestBuilder, Response}; -use url::Url; - -impl PubkyClient { - pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { - let mut request = self.http.request(method, url).fetch_credentials_include(); - - for cookie in self.session_cookies.read().unwrap().iter() { - request = 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/internals.rs b/pubky/src/wasm/internals.rs new file mode 100644 index 0000000..95b625d --- /dev/null +++ b/pubky/src/wasm/internals.rs @@ -0,0 +1,33 @@ +use reqwest::{Method, RequestBuilder}; +use url::Url; + +use pkarr::{EndpointResolver, PublicKey}; + +use crate::{error::Result, PubkyClient}; + +// TODO: remove expect +pub async fn resolve(pkarr: &pkarr::Client, url: &mut Url) -> Result<()> { + let qname = url.host_str().expect("URL TO HAVE A HOST!").to_string(); + + // If http and has a Pubky TLD, switch to socket addresses. + if url.scheme() == "http" && PublicKey::try_from(qname.as_str()).is_ok() { + let endpoint = pkarr.resolve_endpoint(&qname).await?; + + if let Some(socket_address) = endpoint.to_socket_addrs().into_iter().next() { + url.set_host(Some(&socket_address.to_string()))?; + let _ = url.set_port(Some(socket_address.port())); + } else if let Some(port) = endpoint.port() { + url.set_host(Some(endpoint.target()))?; + let _ = url.set_port(Some(port)); + } + }; + + Ok(()) +} + +impl PubkyClient { + /// A wrapper around [reqwest::Client::request], with the same signature between native and wasm. + pub(crate) async fn inner_request(&self, method: Method, url: Url) -> RequestBuilder { + self.http.request(method, url).fetch_credentials_include() + } +} diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs deleted file mode 100644 index 49726f6..0000000 --- a/pubky/src/wasm/pkarr.rs +++ /dev/null @@ -1,48 +0,0 @@ -use reqwest::StatusCode; - -pub use pkarr::{PublicKey, SignedPacket}; - -use crate::error::Result; -use crate::PubkyClient; - -// TODO: Add an in memory cache of packets - -impl PubkyClient { - //TODO: migrate to pkarr::PkarrRelayClient - pub(crate) async fn pkarr_resolve( - &self, - public_key: &PublicKey, - ) -> Result> { - //TODO: Allow multiple relays in parallel - let relay = self.pkarr_relays.first().expect("initialized with relays"); - - let res = self - .http - .get(format!("{relay}/{}", public_key)) - .send() - .await?; - - if res.status() == StatusCode::NOT_FOUND { - return Ok(None); - }; - - // TODO: guard against too large responses. - let bytes = res.bytes().await?; - - let existing = SignedPacket::from_relay_payload(public_key, &bytes)?; - - Ok(Some(existing)) - } - - pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { - let relay = self.pkarr_relays.first().expect("initialized with relays"); - - self.http - .put(format!("{relay}/{}", signed_packet.public_key())) - .body(signed_packet.to_relay_payload()) - .send() - .await?; - - Ok(()) - } -} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/wrappers/keys.rs similarity index 100% rename from pubky/src/wasm/keys.rs rename to pubky/src/wasm/wrappers/keys.rs diff --git a/pubky/src/wasm/wrappers/mod.rs b/pubky/src/wasm/wrappers/mod.rs new file mode 100644 index 0000000..f632a1f --- /dev/null +++ b/pubky/src/wasm/wrappers/mod.rs @@ -0,0 +1,5 @@ +//! Wasm wrappers around structs that we need to be turned into Classes +//! in JavaScript. + +pub mod keys; +pub mod session; diff --git a/pubky/src/wasm/session.rs b/pubky/src/wasm/wrappers/session.rs similarity index 100% rename from pubky/src/wasm/session.rs rename to pubky/src/wasm/wrappers/session.rs