diff --git a/Cargo.lock b/Cargo.lock index 61051c5..8d6ec73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,6 @@ checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", "idna 0.5.0", - "indexmap", "log", "publicsuffix", "serde", @@ -463,15 +462,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - [[package]] name = "critical-section" version = "1.1.2" @@ -672,16 +662,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.0" @@ -1600,7 +1580,6 @@ dependencies = [ "reqwest", "thiserror", "tokio", - "ureq", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1615,6 +1594,7 @@ dependencies = [ "base32", "blake3", "ed25519-dalek", + "js-sys", "once_cell", "pkarr", "postcard", @@ -1637,7 +1617,6 @@ dependencies = [ "flume", "futures-util", "heed", - "once_cell", "pkarr", "postcard", "pubky-common", @@ -1856,9 +1835,7 @@ version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ - "log", "once_cell", - "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2532,24 +2509,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" -dependencies = [ - "base64 0.22.1", - "cookie", - "cookie_store", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - [[package]] name = "url" version = "2.5.2" @@ -2701,15 +2660,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/pubky-common/Cargo.toml b/pubky-common/Cargo.toml index 1b7111c..6855f97 100644 --- a/pubky-common/Cargo.toml +++ b/pubky-common/Cargo.toml @@ -15,3 +15,6 @@ rand = "0.8.5" thiserror = "1.0.60" postcard = { version = "1.0.8", features = ["alloc"] } serde = { version = "1.0.204", features = ["derive"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.69" diff --git a/pubky-common/src/timestamp.rs b/pubky-common/src/timestamp.rs index f850661..4c546d5 100644 --- a/pubky-common/src/timestamp.rs +++ b/pubky-common/src/timestamp.rs @@ -1,7 +1,6 @@ //! Monotonic unix timestamp in microseconds use std::fmt::Display; -use std::time::SystemTime; use std::{ ops::{Add, Sub}, sync::Mutex, @@ -10,6 +9,9 @@ use std::{ use once_cell::sync::Lazy; use rand::Rng; +#[cfg(not(target_arch = "wasm32"))] +use std::time::SystemTime; + /// ~4% chance of none of 10 clocks have matching id. const CLOCK_MASK: u64 = (1 << 8) - 1; const TIME_MASK: u64 = !0 >> 8; @@ -162,6 +164,15 @@ fn system_time() -> u64 { .as_micros() as u64 } +#[cfg(target_arch = "wasm32")] +/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] +pub fn system_time() -> u64 { + // Won't be an issue for more than 5000 years! + (js_sys::Date::now() as u64 ) + // Turn miliseconds to microseconds + * 1000 +} + #[derive(thiserror::Error, Debug)] pub enum TimestampError { #[error("Invalid bytes length, Timestamp should be encoded as 8 bytes, got {0}")] diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 68e30f0..698a3e6 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -14,7 +14,6 @@ dirs-next = "2.0.0" flume = "0.11.0" futures-util = "0.3.30" heed = "0.20.3" -once_cell = "1.19.0" pkarr = { version = "2.1.0", features = ["async"] } postcard = { version = "1.0.8", features = ["alloc"] } pubky-common = { version = "0.1.0", path = "../pubky-common" } diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 3657ecd..6949b09 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -1,9 +1,9 @@ //! Configuration for the server use anyhow::{anyhow, Result}; -use pkarr::Keypair; +use pkarr::{mainline::dht::DhtSettings, Keypair}; // use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf}; +use std::{fmt::Debug, path::PathBuf, time::Duration}; use pubky_common::timestamp::Timestamp; @@ -18,14 +18,15 @@ const DEFAULT_STORAGE_DIR: &str = "pubky"; Clone, )] pub struct Config { - port: Option, - bootstrap: Option>, - domain: String, + pub port: Option, + pub bootstrap: Option>, + pub domain: String, /// Path to the storage directory /// /// Defaults to a directory in the OS data directory - storage: Option, - keypair: Keypair, + pub storage: Option, + pub keypair: Keypair, + pub request_timeout: Option, } impl Config { @@ -50,6 +51,7 @@ impl Config { Self { bootstrap, storage, + request_timeout: Some(Duration::from_millis(10)), ..Default::default() } } @@ -93,6 +95,7 @@ impl Default for Config { domain: "localhost".to_string(), storage: None, keypair: Keypair::random(), + request_timeout: None, } } } diff --git a/pubky-homeserver/src/main.rs b/pubky-homeserver/src/main.rs index 0d22194..77b3382 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use pkarr::mainline::Testnet; -use pubky_homeserver::Homeserver; +use pkarr::{mainline::Testnet, Keypair}; +use pubky_homeserver::{config::Config, Homeserver}; use clap::Parser; @@ -27,7 +27,12 @@ async fn main() -> Result<()> { let server = if args.testnet { let testnet = Testnet::new(3); - Homeserver::start_test(&testnet).await? + Homeserver::start(Config { + port: Some(15411), + keypair: Keypair::from_secret_key(&[0_u8; 32]), + ..Config::test(&testnet) + }) + .await? } else { Homeserver::start(Default::default()).await? }; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index e1a4fef..3f53d9b 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -1,10 +1,16 @@ +use std::sync::Arc; + use axum::{ extract::DefaultBodyLimit, + http::Method, routing::{delete, get, post, put}, Router, }; use tower_cookies::CookieManagerLayer; -use tower_http::trace::TraceLayer; +use tower_http::{ + cors::{self, CorsLayer}, + trace::TraceLayer, +}; use crate::server::AppState; @@ -24,7 +30,6 @@ fn base(state: AppState) -> Router { .route("/:pubky/session", delete(auth::signout)) .route("/:pubky/*path", put(public::put)) .route("/:pubky/*path", get(public::get)) - .layer(TraceLayer::new_for_http()) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads // TODO: maybe add to a separate router (drive router?). @@ -33,5 +38,13 @@ fn base(state: AppState) -> Router { } pub fn create_app(state: AppState) -> Router { - base(state).merge(pkarr_router()) + base(state.clone()) + // TODO: Only enable this for test environments? + .nest("/pkarr", pkarr_router(state)) + .layer( + CorsLayer::new() + .allow_methods([Method::GET, Method::PUT, Method::POST, Method::DELETE]) + .allow_origin(cors::Any), + ) + .layer(TraceLayer::new_for_http()) } diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs index 9c9253b..977a129 100644 --- a/pubky-homeserver/src/routes/pkarr.rs +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -2,35 +2,39 @@ use std::{collections::HashMap, sync::RwLock}; use axum::{ body::{Body, Bytes}, + extract::State, http::StatusCode, response::IntoResponse, routing::{get, put}, Router, }; use futures_util::stream::StreamExt; -use once_cell::sync::OnceCell; use pkarr::{PublicKey, SignedPacket}; +use tracing::debug; use crate::{ error::{Error, Result}, extractors::Pubky, + server::AppState, }; -// TODO: maybe replace after we have local storage of users packets? -static IN_MEMORY: OnceCell>> = OnceCell::new(); - /// Pkarr relay, helpful for testing. /// /// For real productioin, you should use a [production ready /// relay](https://github.com/pubky/pkarr/server). -pub fn pkarr_router() -> Router { +pub fn pkarr_router(state: AppState) -> Router { Router::new() - .route("/pkarr/:pubky", put(pkarr_put)) - .route("/pkarr/:pubky", get(pkarr_get)) + .route("/:pubky", put(pkarr_put)) + .route("/:pubky", get(pkarr_get)) + .with_state(state) } -pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { +pub async fn pkarr_put( + State(mut state): State, + pubky: Pubky, + body: Body, +) -> Result { let mut bytes = Vec::with_capacity(1104); let mut stream = body.into_data_stream(); @@ -43,25 +47,13 @@ pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { let signed_packet = SignedPacket::from_relay_payload(&public_key, &Bytes::from(bytes))?; - let mut store = IN_MEMORY - .get() - .expect("In memory pkarr store is not initialized") - .write() - .unwrap(); - - store.insert(public_key, signed_packet); + state.pkarr_client.publish(&signed_packet).await?; Ok(()) } -pub async fn pkarr_get(pubky: Pubky) -> Result { - let store = IN_MEMORY - .get() - .expect("In memory pkarr store is not initialized") - .read() - .unwrap(); - - if let Some(signed_packet) = store.get(pubky.public_key()) { +pub async fn pkarr_get(State(state): State, pubky: Pubky) -> Result { + if let Some(signed_packet) = state.pkarr_client.resolve(pubky.public_key()).await? { return Ok(signed_packet.to_relay_payload()); } diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 7d37baf..f943203 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -1,13 +1,16 @@ -use std::{future::IntoFuture, net::SocketAddr}; +use std::{ + collections::HashMap, future::IntoFuture, net::SocketAddr, num::NonZeroUsize, sync::Arc, +}; use anyhow::{Error, Result}; +use lru::LruCache; use pubky_common::auth::AuthnVerifier; -use tokio::{net::TcpListener, signal, task::JoinSet}; +use tokio::{net::TcpListener, signal, sync::Mutex, task::JoinSet}; use tracing::{debug, info, warn}; use pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PublicKey, Settings, + PkarrClient, PkarrClientAsync, PublicKey, Settings, SignedPacket, }; use crate::{config::Config, database::DB, pkarr::publish_server_packet}; @@ -23,6 +26,7 @@ pub struct Homeserver { pub(crate) struct AppState { pub verifier: AuthnVerifier, pub db: DB, + pub pkarr_client: PkarrClientAsync, } impl Homeserver { @@ -33,9 +37,20 @@ impl Homeserver { let db = DB::open(&config.storage()?)?; + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: config.bootstsrap(), + request_timeout: config.request_timeout, + ..Default::default() + }, + ..Default::default() + })? + .as_async(); + let state = AppState { verifier: AuthnVerifier::new(public_key.clone()), db, + pkarr_client: pkarr_client.clone(), }; let app = crate::routes::create_app(state); @@ -60,15 +75,6 @@ impl Homeserver { info!("Homeserver listening on http://localhost:{port}"); - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: config.bootstsrap(), - ..Default::default() - }, - ..Default::default() - })? - .as_async(); - publish_server_packet(pkarr_client, config.keypair(), config.domain(), port).await?; info!("Homeserver listening on pubky://{public_key}"); diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 758af8e..34e1d5c 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -11,20 +11,22 @@ keywords = ["web", "dht", "dns", "decentralized", "identity"] crate-type = ["cdylib", "rlib"] [dependencies] -pkarr = "2.1.0" thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" reqwest = { version = "0.12.5", features = ["cookies"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -pubky-common = { version = "0.1.0", path = "../pubky-common" } - -ureq = { version = "2.10.0", features = ["cookies"] } -flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" +pubky-common = { version = "0.1.0", path = "../pubky-common" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +pkarr = { version="2.1.0", features = ["async"] } + +flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] +pkarr = { version = "2.1.0", default-features = false } + futures = "0.3.29" js-sys = "0.3.69" wasm-bindgen = "0.2.92" diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 0bc1b52..23cfe8f 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -1,12 +1,15 @@ import test from 'brittle' -import z32 from 'z32' -import App from '@pubky/homeserver/test/helper/app.js' - -import Client from '../src/index.js' +import { PubkyClient, Keypair, PublicKey } from '../index.js' test('seed auth', async (t) => { - // const homeserver = await App(t) + + let client = new PubkyClient(); + + let keypair = Keypair.random(); + let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + + await client.signup(keypair, homeserver); // const client = new Client( // homeserver.homeserver.pkarr.serverPkarr.publicKey(), diff --git a/pubky/src/bin/bundle_pubky_npm.rs b/pubky/src/bin/bundle_pubky_npm.rs index b3305d3..40e9b90 100644 --- a/pubky/src/bin/bundle_pubky_npm.rs +++ b/pubky/src/bin/bundle_pubky_npm.rs @@ -5,7 +5,7 @@ use std::process::{Command, ExitStatus}; // If the process hangs, try `cargo clean` to remove all locks. fn main() { - println!("cargo:rerun-if-changed=client/"); + println!("Building wasm for pubky..."); build_wasm("nodejs").unwrap(); patch().unwrap(); diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 398cb7b..18be027 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -35,4 +35,18 @@ pub enum Error { #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), + + #[error("Could not resolve endpoint for {0}")] + ResolveEndpoint(String), +} + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[cfg(target_arch = "wasm32")] +impl From for JsValue { + fn from(error: Error) -> JsValue { + let error_message = error.to_string(); + js_sys::Error::new(&error_message).into() + } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 36ff2c0..1c35ff8 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,33 +1,24 @@ #![allow(unused)] -macro_rules! if_not_wasm { - ($($item:item)*) => {$( - #[cfg(not(target_arch = "wasm32"))] - $item - )*} -} - -macro_rules! if_wasm { - ($($item:item)*) => {$( - #[cfg(target_arch = "wasm32")] - $item - )*} -} - -if_not_wasm! { - mod client; - - use client::PubkyClient; -} - -if_wasm! { - mod wasm; - - pub use wasm::keys::Keypair; - pub use wasm::PubkyClient; -} - mod error; mod shared; +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(target_arch = "wasm32")] +mod wasm; +use wasm_bindgen::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +use ::pkarr::PkarrClientAsync; + pub use error::Error; + +#[derive(Debug, Clone)] +#[wasm_bindgen] +pub struct PubkyClient { + http: reqwest::Client, + #[cfg(not(target_arch = "wasm32"))] + pkarr: PkarrClientAsync, +} diff --git a/pubky/src/client.rs b/pubky/src/native.rs similarity index 76% rename from pubky/src/client.rs rename to pubky/src/native.rs index d114a72..90a1930 100644 --- a/pubky/src/client.rs +++ b/pubky/src/native.rs @@ -1,24 +1,18 @@ -mod auth; -mod pkarr; -mod public; +pub mod auth; +pub mod pkarr; +pub mod public; -use std::{collections::HashMap, fmt::format, time::Duration}; +use std::time::Duration; -use ::pkarr::PkarrClientAsync; -use url::Url; +use ::pkarr::{ + mainline::dht::{DhtSettings, Testnet}, + PkarrClient, PkarrClientAsync, Settings, +}; -use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; - -use crate::error::{Error, Result}; +use crate::PubkyClient; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -#[derive(Debug, Clone)] -pub struct PubkyClient { - http: reqwest::Client, - pkarr: PkarrClientAsync, -} - impl PubkyClient { pub fn new() -> Self { Self { @@ -26,10 +20,12 @@ impl PubkyClient { .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), + #[cfg(not(target_arch = "wasm32"))] pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), } } + #[cfg(not(target_arch = "wasm32"))] pub fn test(testnet: &Testnet) -> Self { Self { http: reqwest::Client::builder() diff --git a/pubky/src/client/auth.rs b/pubky/src/native/auth.rs similarity index 88% rename from pubky/src/client/auth.rs rename to pubky/src/native/auth.rs index c99a451..76ef81e 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/native/auth.rs @@ -3,15 +3,20 @@ use reqwest::StatusCode; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, PubkyClient, Result}; +use crate::{ + error::{Error, Result}, + 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: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver).await?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> { + let homeserver = homeserver.to_string(); + + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); @@ -21,7 +26,7 @@ impl PubkyClient { self.http.put(url).body(body).send().await?; - self.publish_pubky_homeserver(keypair, homeserver).await?; + self.publish_pubky_homeserver(keypair, &homeserver).await?; Ok(()) } @@ -96,10 +101,7 @@ mod tests { let keypair = Keypair::random(); - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); + client.signup(&keypair, &server.public_key()).await.unwrap(); let session = client.session(&keypair.public_key()).await.unwrap(); diff --git a/pubky/src/client/pkarr.rs b/pubky/src/native/pkarr.rs similarity index 85% rename from pubky/src/client/pkarr.rs rename to pubky/src/native/pkarr.rs index f22ae43..5cced4b 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/native/pkarr.rs @@ -1,12 +1,16 @@ -pub use pkarr::{ +use url::Url; + +use pkarr::{ dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, PublicKey, Settings, SignedPacket, + Keypair, PublicKey, SignedPacket, }; use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -use super::{Error, PubkyClient, Result, Url}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -38,6 +42,7 @@ 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)> { + let original_target = target; // TODO: cache the result of this function? let mut target = target.to_string(); @@ -48,7 +53,11 @@ impl PubkyClient { // PublicKey is very good at extracting the Pkarr TLD from a string. while let Ok(public_key) = PublicKey::try_from(target.clone()) { - let response = self.pkarr.resolve(&public_key).await?; + let response = self + .pkarr + .resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; let done = parse_pubky_svcb( response, @@ -64,7 +73,7 @@ impl PubkyClient { } } - format_url(homeserver_public_key, host) + format_url(original_target, homeserver_public_key, host) } } @@ -79,6 +88,15 @@ mod tests { }; use pubky_homeserver::Homeserver; + #[tokio::test] + async fn resolve_endpoint() { + let target = "oc9tdmh8c4pmy3tk946oqqfkhic18xdiytadspiy55qhbnja5w9o"; + + let client = PubkyClient::new(); + + client.resolve_endpoint(target).await.unwrap(); + } + #[tokio::test] async fn resolve_homeserver() { let testnet = Testnet::new(3); diff --git a/pubky/src/client/public.rs b/pubky/src/native/public.rs similarity index 92% rename from pubky/src/client/public.rs rename to pubky/src/native/public.rs index 2c1b39f..ce2613e 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/native/public.rs @@ -2,7 +2,7 @@ use bytes::Bytes; use pkarr::PublicKey; -use super::{PubkyClient, Result}; +use crate::{error::Result, PubkyClient}; impl PubkyClient { pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { @@ -67,10 +67,7 @@ mod tests { let keypair = Keypair::random(); - client - .signup(&keypair, &server.public_key().to_string()) - .await - .unwrap(); + client.signup(&keypair, &server.public_key()).await.unwrap(); let response = client .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index e0d87cf..a5d35fc 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -87,6 +87,7 @@ pub fn parse_pubky_svcb( } pub fn format_url( + original_target: &str, homeserver_public_key: Option, host: String, ) -> Result<(PublicKey, Url)> { @@ -100,5 +101,5 @@ pub fn format_url( return Ok((homeserver, Url::parse(&url)?)); } - Err(Error::Generic("Could not resolve endpoint".to_string())) + Err(Error::ResolveEndpoint(original_target.into())) } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index a8d438f..554a4e8 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -4,11 +4,7 @@ pub mod auth; pub mod keys; pub mod pkarr; -#[wasm_bindgen] -pub struct PubkyClient { - pub(crate) http: reqwest::Client, - pub(crate) pkarr: pkarr::PkarrRelayClient, -} +use crate::PubkyClient; #[wasm_bindgen] impl PubkyClient { @@ -16,7 +12,7 @@ impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::new(), - pkarr: pkarr::PkarrRelayClient::default(), + // pkarr: pkarr::PkarrRelayClient::default(), } } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 08b802d..6c9d4ca 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -1,10 +1,14 @@ +use pubky_common::auth::AuthnSignature; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::RequestMode; use pkarr::PkarrRelayClient; -use super::{keys::Keypair, PubkyClient}; +use super::{ + keys::{Keypair, PublicKey}, + PubkyClient, +}; #[wasm_bindgen] impl PubkyClient { @@ -13,16 +17,21 @@ impl PubkyClient { /// 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: &str) -> Result { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<(), JsValue> { + let keypair = keypair.as_inner(); + let homeserver = homeserver.as_inner().to_string(); + + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); - self.http - .put(&url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; + let body = AuthnSignature::generate(keypair, &audience) + .as_bytes() + .to_owned(); - self.publish_pubky_homeserver(keypair, homeserver).await; + self.http.put(url).body(body).send().await?; + + self.publish_pubky_homeserver(keypair, &homeserver).await?; Ok(()) } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 65beae8..f54f7cb 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -1,5 +1,7 @@ use wasm_bindgen::prelude::*; +use crate::Error; + #[wasm_bindgen] pub struct Keypair(pkarr::Keypair); @@ -48,6 +50,17 @@ impl PublicKey { pub fn to_string(&self) -> String { self.0.to_string() } + + #[wasm_bindgen] + 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(), + ))?; + + Ok(PublicKey( + pkarr::PublicKey::try_from(string).map_err(|e| Error::Pkarr(e))?, + )) + } } impl PublicKey { diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index b000528..4af3e7d 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -1,14 +1,24 @@ +use reqwest::StatusCode; +use url::Url; use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - PkarrRelayClient, PublicKey, SignedPacket, + Keypair, PublicKey, SignedPacket, }; -use crate::error::Result; +use crate::error::{Error, Result}; use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; +use crate::PubkyClient; -use super::{keys::Keypair, PubkyClient}; +const TEST_RELAY: &str = "http://localhost:15411/pkarr"; + +#[macro_export] +macro_rules! log { + ($($arg:expr),*) => { + web_sys::console::debug_1(&format!($($arg),*).into()); + }; +} impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -17,11 +27,91 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key().as_inner()).await?; + // let existing = self.pkarr.resolve(&keypair.public_key()).await?; + let existing = self.pkarr_resolve(&keypair.public_key()).await?; - let signed_packet = prepare_packet_for_signup(keypair.as_inner(), host, existing)?; + let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - self.pkarr.publish(&signed_packet).await?; + // self.pkarr.publish(&signed_packet).await?; + self.pkarr_publish(&signed_packet).await?; + + Ok(()) + } + + /// Resolve the homeserver for a pubky. + pub(crate) async fn resolve_pubky_homeserver( + &self, + pubky: &PublicKey, + ) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); + + self.resolve_endpoint(&target) + .await + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } + + /// 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)> { + 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 step = 0; + + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + let response = self + .pkarr_resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + + let done = parse_pubky_svcb( + response, + &public_key, + &mut target, + &mut homeserver_public_key, + &mut host, + &mut step, + ); + + if done { + break; + } + } + + format_url(original_target, homeserver_public_key, host) + } + + //TODO: Allow multiple relays in parallel + //TODO: migrate to pkarr::PkarrRelayClient + async fn pkarr_resolve(&self, public_key: &PublicKey) -> Result> { + let res = self + .http + .get(format!("{TEST_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)) + } + + async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + self.http + .put(format!("{TEST_RELAY}/{}", signed_packet.public_key())) + .body(signed_packet.to_relay_payload()) + .send() + .await?; Ok(()) }