From 979882b44326183f8bbb4cf5bfedccb189c6741e Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 24 Jul 2024 18:02:41 +0300 Subject: [PATCH 01/26] feat(js): add common modules --- js/pubky/.gitignore | 6 + js/pubky/package.json | 51 ++++++++ js/pubky/src/common/auth.js | 194 ++++++++++++++++++++++++++++++ js/pubky/src/common/crypto.js | 131 ++++++++++++++++++++ js/pubky/src/common/index.js | 8 ++ js/pubky/src/common/namespaces.js | 1 + js/pubky/src/common/timestamp.js | 53 ++++++++ js/pubky/tsconfig.json | 32 +++++ 8 files changed, 476 insertions(+) create mode 100644 js/pubky/.gitignore create mode 100644 js/pubky/package.json create mode 100644 js/pubky/src/common/auth.js create mode 100644 js/pubky/src/common/crypto.js create mode 100644 js/pubky/src/common/index.js create mode 100644 js/pubky/src/common/namespaces.js create mode 100644 js/pubky/src/common/timestamp.js create mode 100644 js/pubky/tsconfig.json diff --git a/js/pubky/.gitignore b/js/pubky/.gitignore new file mode 100644 index 0000000..6da14b1 --- /dev/null +++ b/js/pubky/.gitignore @@ -0,0 +1,6 @@ +coverage +node_modules +types +.storage +.env +package-lock.json diff --git a/js/pubky/package.json b/js/pubky/package.json new file mode 100644 index 0000000..08a46cf --- /dev/null +++ b/js/pubky/package.json @@ -0,0 +1,51 @@ +{ + "name": "@synonymdev/pubky", + "version": "0.1.0", + "description": "Pubky client library", + "type": "module", + "main": "src/index.js", + "types": "types/src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/slashtags/skunk-works.git" + }, + "scripts": { + "build": "tsc", + "clean": "rm -rf types", + "lint": "standard --fix", + "test": "brittle test/*.js -cov", + "depcheck": "npx depcheck --ignore-dirs=test", + "fullcheck": "npm run lint && npm run clean && npm run build && npm run test && npm run depcheck", + "prepublishOnly": "npm run fullcheck" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/pubky/pubky/issues" + }, + "homepage": "https://github.com/pubky/pubky/tree/master/js/pubky/#readme", + "files": [ + "src", + "types", + "!**/*.tsbuildinfo" + ], + "dependencies": { + "blake3-wasm": "^3.0.0", + "crockford-base32": "^2.0.0", + "eventsource": "^2.0.2", + "hash-wasm": "^4.11.0", + "node-fetch-cache": "^4.1.2", + "pkarr": "^1.4.1", + "z32": "^1.1.0" + }, + "browser": { + "./src/lib/fetch.js": "./src/lib/fetch-browser.js" + }, + "devDependencies": { + "standard": "^17.1.0", + "typescript": "^5.5.4" + }, + "overrides": { + "blake3-wasm@2.1.7": "^3.0.0", + "@c4312/blake3-internal": "^3.0.0" + } +} diff --git a/js/pubky/src/common/auth.js b/js/pubky/src/common/auth.js new file mode 100644 index 0000000..f48d387 --- /dev/null +++ b/js/pubky/src/common/auth.js @@ -0,0 +1,194 @@ +import { Timestamp } from './timestamp.js' +import * as namespaces from './namespaces.js' +import * as crypto from './crypto.js' + +// 30 seconds +const TIME_INTERVAL = 30 * 1000000 + +export class AuthnSignature { + /** + * @param {number} time + * @param {import ('./crypto.js').KeyPair} signer + * @param {import ('./crypto.js').PublicKey} audience + * @param {Buffer} [token] + */ + constructor (time, signer, audience, token = crypto.randomBytes()) { + const timeStep = Math.floor(time / TIME_INTERVAL) + + const tokenHash = crypto.hash(token) + + const timeStepBytes = Buffer.allocUnsafe(8) + timeStepBytes.writeBigUint64BE(BigInt(timeStep)) + + const signature = signer.sign(signable( + signer.publicKey().bytes, + audience.bytes, + timeStepBytes, + tokenHash + )) + + this.bytes = Buffer.concat([ + signature, + tokenHash + ]) + } + + /** + * @param {import ('./crypto.js').KeyPair} signer + * @param {import ('./crypto.js').PublicKey} audience + * @param {Buffer} [token] + */ + static sign (signer, audience, token = crypto.randomBytes()) { + const time = Timestamp.now().microseconds + + return new AuthnSignature(time, signer, audience, token) + } + + asBytes () { + return new Uint8Array(this.bytes) + } +} + +export class AuthnVerifier { + #audience + /** @type {Array} */ + #seen + + /** + * @param {crypto.PublicKey} audience + */ + constructor (audience) { + this.#audience = audience + + this.#seen = [] + } + + #gc () { + const threshold = Timestamp.now().microseconds + const threshouldStep = Math.floor(threshold / TIME_INTERVAL) - 2 + + const thresholdBytes = Buffer.allocUnsafe(8) + thresholdBytes.writeBigUint64BE(BigInt(threshouldStep)) + + let count = 0 + + for (let i = 0; i < this.#seen.length; i++) { + if (this.#seen[i].subarray(0, 8).compare(thresholdBytes) > 0) { + break + } + count = i + } + + this.#seen.splice(0, count) + } + + /** + * @param {Buffer} bytes + * @param {crypto.PublicKey} signer + * + * @returns {true | Error} + */ + verify (bytes, signer) { + this.#gc() + + if (bytes.length !== 96) { + throw new Error(`InvalidLength: ${bytes.length}`) + } + + const signature = bytes.subarray(0, 64) + const tokenHash = bytes.subarray(64) + + const now = Timestamp.now().microseconds + const past = now - TIME_INTERVAL + const future = now + TIME_INTERVAL + + let result = verifyAt.call(this, now) + + if (!(result instanceof Error)) { + return result + } else if (result.toString() === 'Error: AuthnSignature already used') { + return result + } + + result = verifyAt.call(this, past) + + if (!(result instanceof Error)) { + return result + } else if (result.toString() === 'Error: AuthnSignature already used') { + return result + } + + return verifyAt.call(this, future) + + /** + * @param {number} time + */ + function verifyAt (time) { + const timeStep = Math.floor(time / TIME_INTERVAL) + + const timeStepBytes = Buffer.allocUnsafe(8) + timeStepBytes.writeBigUint64BE(BigInt(timeStep)) + + const result = signer.verify(signature, signable(signer.bytes, this.#audience.bytes, timeStepBytes, tokenHash)) + + const candidate = Buffer.concat([ + timeStepBytes, + tokenHash + ]) + + if (!(result instanceof Error)) { + const index = binarySearch(this.#seen, timeStepBytes) + + if (this.#seen[index]?.equals(candidate)) { + return new Error('AuthnSignature already used') + } + + this.#seen.splice(~index, 0, candidate) + + return + } + + return result + } + } +} + +/** + * @param {Array} arr + */ +function binarySearch (arr, element) { + let left = 0 + let right = arr.length - 1 + + while (left <= right) { + const mid = Math.floor((left + right) / 2) + + const comparison = arr[mid].subarray(0, 8).compare(element.subarray(0, 8)) + + if (comparison === 0) { + return mid + } else if (comparison < 0) { + left = mid + 1 + } else { + right = mid - 1 + } + } + + return left // Element not found, return the index where it should be inserted +} + +/** + * @param {Buffer} signer + * @param {Buffer} audience + * @param {Buffer} timeStepBytes + * @param {Buffer} tokenHash + */ +function signable (signer, audience, timeStepBytes, tokenHash) { + return Buffer.concat([ + namespaces.PUBKY_AUTHN, + timeStepBytes, + signer, + audience, + tokenHash + ]) +} diff --git a/js/pubky/src/common/crypto.js b/js/pubky/src/common/crypto.js new file mode 100644 index 0000000..c1c81b0 --- /dev/null +++ b/js/pubky/src/common/crypto.js @@ -0,0 +1,131 @@ +//! Crypeo functions + +import sodium from 'sodium-universal' +import z32 from 'z32' + +// Blake3 + +/** @type {import('blake3-wasm')} */ +let loadedBlake3 + +const loadBlake3 = async () => { + if (loadedBlake3) return loadedBlake3 + // @ts-ignore + loadedBlake3 = await import('blake3-wasm').then(b3 => b3.load().then(() => b3)) + + return loadedBlake3 +} + +loadBlake3() + +/** + * It will return null if blake3 is not loaded yet! + * + * @param {Buffer} message + * + * @returns {Buffer | null} + */ +export const hash = (message) => { + return loadedBlake3?.createHash().update(message).digest() +} + +// Random +export const randomBytes = (n = 32) => { + const buf = Buffer.alloc(n) + sodium.randombytes_buf(buf) + return buf +} + +/// Keypairs + +/** + * @param {Buffer} buf + */ +export const zeroize = (buf) => { + buf.fill(0) +} + +export class KeyPair { + #publicKey + #secretKey + + /** + * @param {Buffer} seed + */ + constructor (seed) { + this.#publicKey = Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES) + this.#secretKey = Buffer.allocUnsafe(sodium.crypto_sign_SECRETKEYBYTES) + + if (seed) sodium.crypto_sign_seed_keypair(this.#publicKey, this.#secretKey, seed) + else sodium.crypto_sign_keypair(this.#publicKey, this.#secretKey) + } + + static random () { + const seed = randomBytes(32) + + return new KeyPair(seed) + } + + zeroize () { + zeroize(this.#secretKey) + this.secretKey = null + } + + publicKey () { + return new PublicKey(this.#publicKey) + } + + secretKey () { + return this.#secretKey + } + + /** + * @param {Uint8Array} message + */ + sign (message) { + const signature = Buffer.alloc(sodium.crypto_sign_BYTES) + sodium.crypto_sign_detached(signature, message, this.#secretKey) + + return signature + } +} + +export class PublicKey { + /** + * @param {Buffer} bytes + */ + constructor (bytes) { + this.bytes = bytes + } + + /** + * @param {string} string + * @returns {Error | PublicKey} + */ + static fromString (string) { + if (string.length !== 52) { + return new Error('Invalid PublicKey string, expected 52 characters, got: ' + string.length) + } + + try { + return new PublicKey(z32.decode(string)) + } catch (error) { + return error + } + } + + /** + * @param {Buffer} signature + * @param {Buffer} message + */ + verify (signature, message) { + const valid = sodium.crypto_sign_verify_detached(signature, message, this.bytes) + if (!valid) return new Error('Invalid signature') + + return true + } + + toString () { + return z32.encode(this.bytes) + } +} diff --git a/js/pubky/src/common/index.js b/js/pubky/src/common/index.js new file mode 100644 index 0000000..71ccb60 --- /dev/null +++ b/js/pubky/src/common/index.js @@ -0,0 +1,8 @@ +export * as crypto from './crypto.js' +export { Timestamp } from './timestamp.js' +export { AuthnSignature, AuthnVerifier } from './auth.js' + +/** + * @typedef {string | number | boolean | null} JSONValue + * @typedef {{[key: string]: JSONValue | Array}} JSONObject + */ diff --git a/js/pubky/src/common/namespaces.js b/js/pubky/src/common/namespaces.js new file mode 100644 index 0000000..156d3af --- /dev/null +++ b/js/pubky/src/common/namespaces.js @@ -0,0 +1 @@ +export const PUBKY_AUTHN = Buffer.from('PUBKY:AUTHN') diff --git a/js/pubky/src/common/timestamp.js b/js/pubky/src/common/timestamp.js new file mode 100644 index 0000000..eeeadec --- /dev/null +++ b/js/pubky/src/common/timestamp.js @@ -0,0 +1,53 @@ +import { CrockfordBase32 } from 'crockford-base32' +import { randomBytes } from './crypto.js' + +const clockId = randomBytes(1).readUintBE(0, 1) +let latest = 0 + +export class Timestamp { + /** + * @param {number} microseconds - u64 microseconds + */ + constructor (microseconds) { + /** microseconds as u64 */ + this.microseconds = microseconds + } + + static now () { + const now = Date.now() + latest = Math.max(now, latest + 1) + + return new Timestamp((latest * 1000) + clockId) + } + + /** + * @param {string} string + */ + static fromString (string) { + const microseconds = Number(CrockfordBase32.decode(string, { asNumber: true })) + return new Timestamp(microseconds) + } + + /** + * @param {Date} date + */ + static fromDate (date) { + const microseconds = Number(date) * 1000 + return new Timestamp(microseconds) + } + + toString () { + return CrockfordBase32.encode(this.microseconds) + } + + toDate () { + return new Date(this.microseconds / 1000) + } + + intoBytes () { + const buffer = Buffer.allocUnsafe(8) + buffer.writeBigUint64BE(BigInt(this.microseconds), 0) + + return buffer + } +} diff --git a/js/pubky/tsconfig.json b/js/pubky/tsconfig.json new file mode 100644 index 0000000..63d7a71 --- /dev/null +++ b/js/pubky/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + // Declarations control + "target": "esnext", + "module": "esnext", + + "noEmitOnError": true, + "emitDeclarationOnly": true, + "declarationMap": true, + "isolatedModules": true, + + "incremental": true, + "composite": true, + + // Check control + "strict": false, + "allowJs": true, + "checkJs": true, + + // module resolution + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + + // advanced + "verbatimModuleSyntax": true, + "skipLibCheck": true, + + "outDir": "types" + }, + "include": ["src", "lib"] +} From ae4a34c511b04a9d266ab344cbef2fa658464d1a Mon Sep 17 00:00:00 2001 From: nazeh Date: Wed, 24 Jul 2024 22:26:32 +0300 Subject: [PATCH 02/26] feat(pubky): add wasm mod --- Cargo.lock | 5 ++ js/pubky/README.md | 3 + js/pubky/examples/basic.js | 9 +++ js/pubky/src/index.js | 5 ++ js/pubky/src/lib/client.js | 102 ++++++++++++++++++++++++++++++ js/pubky/src/lib/error.js | 1 + js/pubky/src/lib/fetch-browser.js | 11 ++++ js/pubky/src/lib/fetch.js | 3 + pubky/Cargo.toml | 21 +++++- pubky/src/lib.rs | 32 ++++++++-- pubky/src/wasm.rs | 6 ++ pubky/src/wasm/client.rs | 73 +++++++++++++++++++++ pubky/src/wasm/keys.rs | 15 +++++ 13 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 js/pubky/README.md create mode 100644 js/pubky/examples/basic.js create mode 100644 js/pubky/src/index.js create mode 100644 js/pubky/src/lib/client.js create mode 100644 js/pubky/src/lib/error.js create mode 100644 js/pubky/src/lib/fetch-browser.js create mode 100644 js/pubky/src/lib/fetch.js create mode 100644 pubky/src/wasm.rs create mode 100644 pubky/src/wasm/client.rs create mode 100644 pubky/src/wasm/keys.rs diff --git a/Cargo.lock b/Cargo.lock index 613c9c4..910eaa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,6 +1252,8 @@ version = "0.1.0" dependencies = [ "bytes", "flume", + "futures", + "js-sys", "pkarr", "pubky-common", "pubky_homeserver", @@ -1259,6 +1261,9 @@ dependencies = [ "tokio", "ureq", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/js/pubky/README.md b/js/pubky/README.md new file mode 100644 index 0000000..c57a08a --- /dev/null +++ b/js/pubky/README.md @@ -0,0 +1,3 @@ +# Pubky + +This is a JavaScript implementation of a Pubky protocol client-side tools. diff --git a/js/pubky/examples/basic.js b/js/pubky/examples/basic.js new file mode 100644 index 0000000..6b40582 --- /dev/null +++ b/js/pubky/examples/basic.js @@ -0,0 +1,9 @@ +import { PubkyClient } from '../src/index.js' + +main() + +async function main() { + let client = new PubkyClient() + + console.log(client) +} diff --git a/js/pubky/src/index.js b/js/pubky/src/index.js new file mode 100644 index 0000000..e3e7548 --- /dev/null +++ b/js/pubky/src/index.js @@ -0,0 +1,5 @@ +// import { PubkyClient as _PubkyClient } from './lib/client.js' +// import { PubkyError as _PubkyError } from './lib/error.js' +// +// export const PubkyClient = _PubkyClient; +// export const PubkyError = _PubkyError; diff --git a/js/pubky/src/lib/client.js b/js/pubky/src/lib/client.js new file mode 100644 index 0000000..742262d --- /dev/null +++ b/js/pubky/src/lib/client.js @@ -0,0 +1,102 @@ +import Pkarr, { SignedPacket } from 'pkarr' +import { URL } from 'url' + +// import { AuthnSignature, crypto } from '@pubky/common' + +import * as crypto from '../common/crypto.js' + +// import { Pubky } from './pubky.js' +import fetch from './fetch.js' + + +const DEFAULT_PKARR_RELAY = new URL('https://relay.pkarr.org') + +export class PubkyClient { + // TODO: use DHT in nodejs + #pkarrRelay + + crypto = crypto + static crypto = crypto + + /** + * @param {object} [options={}] + * @param {URL} [options.pkarrRelay] + * + * @param {{relay: string, bootstrap: Array<{host: string, port: number}>}} [options.testnet] + */ + constructor(options = {}) { + this.#pkarrRelay = options.pkarrRelay || DEFAULT_PKARR_RELAY + } + + /** + * Publish the SVCB record for `_pubky.`. + * @param {crypto.KeyPair} keypair + * @param {String} host + */ + async publishPubkyHomeserver(keypair, host) { + + let existing = await (async () => { + try { + return (await Pkarr.relayGet(this.#pkarrRelay.toString(), keypair.publicKey().bytes)).packet() + } catch (error) { + return { + id: 0, + type: 'response', + flags: 0, + answers: [] + } + } + })(); + + let answers = [ + ]; + + for (let answer of existing.answers) { + if (!answer.name.startsWith("_pubky")) { + answers.push(answer) + } + } + + let signedPacket = SignedPacket.fromPacket(keypair, { + id: 0, + type: 'response', + flags: 0, + answers: [ + ...answers, + { + name: '_pubky.', type: 'SVCB', ttl: 7200, data: + + Buffer.from( + + ) + + } + ] + }) + + // let mut packet = Packet:: new_reply(0); + // + // if let Some(existing) = self.pkarr.resolve(& keypair.public_key()) ? { + // for answer in existing.packet().answers.iter().cloned() { + // if !answer.name.to_string().starts_with("_pubky") { + // packet.answers.push(answer.into_owned()) + // } + // } + // } + // + // let svcb = SVCB::new (0, host.try_into() ?); + // + // packet.answers.push(pkarr:: dns:: ResourceRecord:: new ( + // "_pubky".try_into().unwrap(), + // pkarr:: dns:: CLASS:: IN, + // 60 * 60, + // pkarr:: dns:: rdata:: RData:: SVCB(svcb), + // )); + // + // let signed_packet = SignedPacket:: from_packet(keypair, & packet) ?; + // + // self.pkarr.publish(& signed_packet) ?; + } +} + +export default PubkyClient diff --git a/js/pubky/src/lib/error.js b/js/pubky/src/lib/error.js new file mode 100644 index 0000000..29d4146 --- /dev/null +++ b/js/pubky/src/lib/error.js @@ -0,0 +1 @@ +export class PubkyError extends Error { } diff --git a/js/pubky/src/lib/fetch-browser.js b/js/pubky/src/lib/fetch-browser.js new file mode 100644 index 0000000..30b547c --- /dev/null +++ b/js/pubky/src/lib/fetch-browser.js @@ -0,0 +1,11 @@ +/* eslint-disable no-prototype-builtins */ +const g = + (typeof globalThis !== 'undefined' && globalThis) || + // eslint-disable-next-line no-undef + (typeof self !== 'undefined' && self) || + // eslint-disable-next-line no-undef + (typeof global !== 'undefined' && global) || + {} + +// @ts-ignore +export default g.fetch diff --git a/js/pubky/src/lib/fetch.js b/js/pubky/src/lib/fetch.js new file mode 100644 index 0000000..7c24a8b --- /dev/null +++ b/js/pubky/src/lib/fetch.js @@ -0,0 +1,3 @@ +import fetch from 'node-fetch-cache' + +export default fetch diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index c40cd9c..57a8b5c 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -3,16 +3,35 @@ name = "pubky" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] +pkarr = "2.1.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } -pkarr = "2.1.0" ureq = { version = "2.10.0", features = ["cookies"] } thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +futures = "0.3.29" +js-sys = "0.3.69" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +web-sys = { version = "0.3.69", features = [ + "console", + "Request", + "RequestInit", + "RequestMode", + "Response", + "Window", +] } + [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 7125ca1..4ed3714 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -1,8 +1,30 @@ #![allow(unused)] -mod client; -mod client_async; -mod error; +macro_rules! if_not_wasm { + ($($item:item)*) => {$( + #[cfg(not(target_arch = "wasm32"))] + $item + )*} +} -pub use client::PubkyClient; -pub use error::Error; +macro_rules! if_wasm { + ($($item:item)*) => {$( + #[cfg(target_arch = "wasm32")] + $item + )*} +} + +if_not_wasm! { + mod client; + mod client_async; + mod error; + + pub use client::PubkyClient; + pub use error::Error; +} + +if_wasm! { +mod wasm; + +pub use wasm::{PubkyClient, Keypair}; +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs new file mode 100644 index 0000000..faddf5b --- /dev/null +++ b/pubky/src/wasm.rs @@ -0,0 +1,6 @@ +mod keys; + +mod client; + +pub use client::PubkyClient; +pub use keys::Keypair; diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs new file mode 100644 index 0000000..7504789 --- /dev/null +++ b/pubky/src/wasm/client.rs @@ -0,0 +1,73 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::RequestMode; + +use pkarr::PkarrRelayClient; + +use super::Keypair; + +#[wasm_bindgen] +pub struct Error {} + +#[wasm_bindgen] +pub struct PubkyClient { + pkarr: PkarrRelayClient, +} + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + pkarr: PkarrRelayClient::default(), + } + } + + /// 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 fn signup(&self, secret_key: Keypair, homeserver: &str) -> Result<(), JsValue> { + // let (audience, mut url) = self.resolve_endpoint(homeserver)?; + + // url.set_path(&format!("/{}", keypair.public_key())); + + // let body = AuthnSignature::generate(keypair, &audience).as_bytes(); + + // fetch_base(url.to_string(), "PUT", body).await?; + + // self.publish_pubky_homeserver(keypair, homeserver); + + Ok(()) + } +} + +async fn fetch_base( + url: &String, + method: &str, + body: Option>, +) -> Result { + let mut opts = web_sys::RequestInit::new(); + opts.method(method); + opts.mode(RequestMode::Cors); + + if let Some(body) = body { + let body_bytes: &[u8] = &body; + let body_array: js_sys::Uint8Array = body_bytes.into(); + let js_value: &JsValue = body_array.as_ref(); + opts.body(Some(js_value)); + } + + let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; + // .map_err(|error| Error::JsError(error))?; + + let window = web_sys::window().unwrap(); + let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; + // .map_err(|error| Error::JsError(error))?; + + let response: web_sys::Response = response.dyn_into()?; + // .map_err(|error| Error::JsError(error))? + + Ok(response) +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs new file mode 100644 index 0000000..3859511 --- /dev/null +++ b/pubky/src/wasm/keys.rs @@ -0,0 +1,15 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Keypair(pkarr::Keypair); + +#[wasm_bindgen] +impl Keypair { + #[wasm_bindgen] + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { + let mut bytes = [0; 32]; + secret_key.copy_to(&mut bytes); + + Self(pkarr::Keypair::from_secret_key(&bytes)) + } +} From b03602045f212d81510a55e0e125354e34c629ea Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 25 Jul 2024 14:05:42 +0300 Subject: [PATCH 03/26] feat(js): successful _initial_ test of wasm in nodejs and browser --- Cargo.lock | 42 ++++++ examples/nodejs/.gitignore | 5 + examples/nodejs/README.md | 3 + examples/nodejs/index.js | 10 ++ examples/nodejs/package.json | 7 + examples/web/auth/.gitignore | 5 + examples/web/auth/index.html | 38 +++++ examples/web/auth/package.json | 23 +++ examples/web/auth/package.json0 | 30 ++++ .../auth/src/components/RecoveryFileUpload.js | 78 ++++++++++ examples/web/auth/src/components/layout.jsx | 21 +++ examples/web/auth/src/index.jsx | 21 +++ examples/web/auth/src/pages/Home.jsx | 43 ++++++ examples/web/auth/src/store.js | 90 ++++++++++++ examples/web/auth/src/style.css | 137 ++++++++++++++++++ examples/web/auth/vite.config.js | 12 ++ examples/web/no-bundler/.gitignore | 3 + examples/web/no-bundler/README.md | 3 + examples/web/no-bundler/index.html | 46 ++++++ examples/web/no-bundler/package.json | 17 +++ examples/web/package-lock.json | 6 + pubky/Cargo.toml | 10 ++ pubky/pkg/.gitignore | 2 + pubky/pkg/LICENSE | 21 +++ pubky/pkg/README.md | 3 + pubky/pkg/package.json | 28 ++++ pubky/src/bin/bundle_wasm.rs | 58 ++++++++ pubky/src/bin/patch.mjs | 54 +++++++ pubky/src/lib.rs | 4 +- pubky/src/wasm/client.rs | 17 ++- 30 files changed, 832 insertions(+), 5 deletions(-) create mode 100644 examples/nodejs/.gitignore create mode 100644 examples/nodejs/README.md create mode 100644 examples/nodejs/index.js create mode 100644 examples/nodejs/package.json create mode 100644 examples/web/auth/.gitignore create mode 100644 examples/web/auth/index.html create mode 100644 examples/web/auth/package.json create mode 100644 examples/web/auth/package.json0 create mode 100644 examples/web/auth/src/components/RecoveryFileUpload.js create mode 100644 examples/web/auth/src/components/layout.jsx create mode 100644 examples/web/auth/src/index.jsx create mode 100644 examples/web/auth/src/pages/Home.jsx create mode 100644 examples/web/auth/src/store.js create mode 100644 examples/web/auth/src/style.css create mode 100644 examples/web/auth/vite.config.js create mode 100644 examples/web/no-bundler/.gitignore create mode 100644 examples/web/no-bundler/README.md create mode 100644 examples/web/no-bundler/index.html create mode 100644 examples/web/no-bundler/package.json create mode 100644 examples/web/package-lock.json create mode 100644 pubky/pkg/.gitignore create mode 100644 pubky/pkg/LICENSE create mode 100644 pubky/pkg/README.md create mode 100644 pubky/pkg/package.json create mode 100644 pubky/src/bin/bundle_wasm.rs create mode 100644 pubky/src/bin/patch.mjs diff --git a/Cargo.lock b/Cargo.lock index 910eaa2..b8b2d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -265,6 +265,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1263,6 +1273,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-bindgen-test", "web-sys", ] @@ -1482,6 +1493,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2119,6 +2136,31 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-bindgen-test" +version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/examples/nodejs/.gitignore b/examples/nodejs/.gitignore new file mode 100644 index 0000000..fb59162 --- /dev/null +++ b/examples/nodejs/.gitignore @@ -0,0 +1,5 @@ +.netlify +.parcel* +dist +node_modules +package-lock.json diff --git a/examples/nodejs/README.md b/examples/nodejs/README.md new file mode 100644 index 0000000..c427745 --- /dev/null +++ b/examples/nodejs/README.md @@ -0,0 +1,3 @@ +# Nodejs + +An example of using Pubky wasm in nodejs diff --git a/examples/nodejs/index.js b/examples/nodejs/index.js new file mode 100644 index 0000000..06acbaa --- /dev/null +++ b/examples/nodejs/index.js @@ -0,0 +1,10 @@ +import { PubkyClient, Keypair } from '@synonymdev/pubky' + +let keypair = Keypair.from_secret_key(new Uint8Array(32).fill(0)) +console.log(keypair) + +const client = new PubkyClient() + +console.log(client) + +const x = client.signup(keypair, "foo.com") diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json new file mode 100644 index 0000000..5e17eb5 --- /dev/null +++ b/examples/nodejs/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "main": "index.js", + "dependencies": { + "@synonymdev/pubky":"file:../../pubky/pkg" + } +} diff --git a/examples/web/auth/.gitignore b/examples/web/auth/.gitignore new file mode 100644 index 0000000..fb59162 --- /dev/null +++ b/examples/web/auth/.gitignore @@ -0,0 +1,5 @@ +.netlify +.parcel* +dist +node_modules +package-lock.json diff --git a/examples/web/auth/index.html b/examples/web/auth/index.html new file mode 100644 index 0000000..ef33b63 --- /dev/null +++ b/examples/web/auth/index.html @@ -0,0 +1,38 @@ + + + + + + + + Pubky demo + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json new file mode 100644 index 0000000..6f1660b --- /dev/null +++ b/examples/web/auth/package.json @@ -0,0 +1,23 @@ +{ + "private": "true", + "main": "src/index.js", + "type": "module", + "scripts": { + "build": "vite build", + "start": "vite serve", + "preview": "vite preview", + "wasm": "wasm-pack build ../../../pubky/ --target web" + }, + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + + "@solidjs/router": "^0.10.9", + "solid-js": "^1.7.0", + "vite-plugin-solid": "^2.10.2" + }, + "devDependencies": { + "typescript": "^4.9.5", + "vite": "^4.1.4", + "vite-plugin-html": "^3.2.0" + } +} diff --git a/examples/web/auth/package.json0 b/examples/web/auth/package.json0 new file mode 100644 index 0000000..9bd267d --- /dev/null +++ b/examples/web/auth/package.json0 @@ -0,0 +1,30 @@ +{ + "private": "true", + "type": "module", + "source": "src/index.html", + "browserslist": "> 0.5%, last 2 versions, not dead", + "scripts": { + "start": "parcel -p 7251", + "serve": "npm run start", + "build": "parcel build", + "lint": "standard --fix" + }, + "standard": { + "ignore": [ + "src/pages", + "src/components", + "dist" + ] + }, + "dependencies": { + "@solidjs/router": "^0.10.9", + "parcel": "^2.0.1", + "@synonymdev/pubky": "file:../../../pubky/pkg", + "solid-js": "^1.7.0" + }, + "devDependencies": { + "@babel/core": "^7.24.9", + "babel-preset-solid": "^1.2.5", + "solid-refresh": "^0.2.2" + } +} diff --git a/examples/web/auth/src/components/RecoveryFileUpload.js b/examples/web/auth/src/components/RecoveryFileUpload.js new file mode 100644 index 0000000..06b3ad0 --- /dev/null +++ b/examples/web/auth/src/components/RecoveryFileUpload.js @@ -0,0 +1,78 @@ +import { useNavigate } from '@solidjs/router' +import { createSignal } from 'solid-js' +import { crypto } from '@pubky/common' + +import { decryptRecoveryFile } from '../sdk/recovery.js' +import store from '../store.js' + +/** + * @param {"login" | "signup"} type + */ +const RecoveryFileUpload = ({ type }) => { + const [valid, setValid] = createSignal(false) + + const navigate = useNavigate() + + const onSubmit = async (e) => { + e.preventDefault() + + const form = e.target + + const file = form.file.files[0] + const passphrase = form['import-passphrase'].value + + const reader = new FileReader() + + reader.onload = async function(event) { + const recoveryFile = event.target.result + + const seedResult = await decryptRecoveryFile(recoveryFile, passphrase) + if (seedResult.isErr()) return alert(seedResult.error.message) + + const action = type === 'signup' + ? store.pubkyClient.signup.bind(store.pubkyClient) + : store.pubkyClient.login.bind(store.pubkyClient); + + const result = await action(seedResult.value) + crypto.zeroize(seedResult.value) + + if (result.isErr()) return alert(result.error.message) + + store.setCurrentUser({ id: result.value }) + + navigate("/", { replace: true }) + } + + // Read the file as text + reader.readAsText(file) + } + + const onUpdate = (e) => { + const form = e.target.parentElement.parentElement + + const file = form.file.files[0] + const passphrase = form['import-passphrase'].value + + if (passphrase.length > 0 && file) { + setValid(true) + } else { + setValid(false) + } + } + + return ( +
+ + + +
+ ) +} + +export default RecoveryFileUpload diff --git a/examples/web/auth/src/components/layout.jsx b/examples/web/auth/src/components/layout.jsx new file mode 100644 index 0000000..1d0c03b --- /dev/null +++ b/examples/web/auth/src/components/layout.jsx @@ -0,0 +1,21 @@ +const Layout = ({ children }) => { + return ( + <> +
+
+

Pubky

+
+
+ +
+ {children} +
+ +
+

This is a proof of concept for demonstration purposes only.

+
+ + ) +} + +export default Layout diff --git a/examples/web/auth/src/index.jsx b/examples/web/auth/src/index.jsx new file mode 100644 index 0000000..1c78d71 --- /dev/null +++ b/examples/web/auth/src/index.jsx @@ -0,0 +1,21 @@ +import { render } from 'solid-js/web' +import { Router, Route } from '@solidjs/router' + +import { PubkyClient } from "@synonymdev/pubky"; + +let client = new PubkyClient() +console.log(client); + +import Home from './pages/Home.jsx' +// import Login from './pages/Login.js' +// import Signup from './pages/Signup.js' + +render(() => ( + + + +), document.getElementById('app')) + +// +// +// diff --git a/examples/web/auth/src/pages/Home.jsx b/examples/web/auth/src/pages/Home.jsx new file mode 100644 index 0000000..307d1a1 --- /dev/null +++ b/examples/web/auth/src/pages/Home.jsx @@ -0,0 +1,43 @@ +import { useNavigate } from '@solidjs/router' + +import store from '../store.js' +import Layout from '../components/Layout' + +const Home = () => { + const navigate = useNavigate() + + let currentUser = store.getCurrentUser() + + if (!currentUser) { + navigate('/login', { replace: true }) + } + + const logout = async () => { + // await store.pubkyClient.ready() + // + // const logoutResult = await store.pubkyClient.logout(currentUser.id) + // if (logoutResult.isErr()) { + // alert(logoutResult.error.message) + // return + // } + // + // store.removeCurrentUser() + // + // if (window.location.pathname === '/home') { + // navigate('/', { replace: true }) + // } else { + // navigate('/home', { replace: true }) + // } + } + + return ( + + Home.. +

Welcome {store.getCurrentUser()?.id}

+
+ +
+ ) +} + +export default Home diff --git a/examples/web/auth/src/store.js b/examples/web/auth/src/store.js new file mode 100644 index 0000000..910d6a5 --- /dev/null +++ b/examples/web/auth/src/store.js @@ -0,0 +1,90 @@ +import { createMutable } from 'solid-js/store' +// import z32 from 'z32' +// import { Result } from '@pubky/common' +// import { Level } from 'level' +// import Client from '@pubky/client' +// +// import { recoveryFile } from './sdk/recovery.js' +// +// // In real application it should be the server's Pkarr Id +// const DEFAULT_HOME_SERVER = 'http://localhost:7259' +// const DEFAULT_RELAY = 'https://relay.pkarr.org' + +class Store { + constructor() { + // this.db = new Level('app-db', { keyEncoding: 'utf8', valueEncoding: 'json' }) + // this.currentUser = null + // + // this.pubkyClient = new Client(DEFAULT_HOME_SERVER, { relay: DEFAULT_RELAY }) + // + // this.DEFAULT_HOME_SERVER = DEFAULT_HOME_SERVER + } + + // getUsers() { + // try { + // return JSON.parse(global.localStorage.getItem('users')) || [] + // } catch { + // return [] + // } + // } + + getCurrentUser() { + try { + return JSON.parse(global.localStorage.getItem('currentUser')) + } catch { + return null + } + } + + // setCurrentUser(user) { + // const users = this.getUsers() + // + // if (!users?.map(user => user.id).includes(user.id)) { + // global.localStorage.setItem('users', JSON.stringify([ + // ...users, + // user + // ])) + // } + // + // global.localStorage.setItem('currentUser', JSON.stringify(user)) + // } + // + // removeCurrentUser() { + // global.localStorage.removeItem('currentUser') + // } + // + // /** + // * @param {string} name + // * @param {string} passphrase + // * + // * @returns {Promise>} + // */ + // async createAccount(name, passphrase) { + // await this.pubkyClient.ready() + // + // const seed = Client.crypto.generateSeed() + // + // const keypair = Client.crypto.generateKeyPair(seed) + // Client.crypto.zeroize(keypair.secretKey) + // + // const userId = z32.encode(keypair.publicKey) + // + // const recoveryFileAndFilename = await recoveryFile(name, seed, passphrase) + // + // const signedUp = await this.pubkyClient.signup(seed) + // if (signedUp.isErr()) return signedUp + // + // Client.crypto.zeroize(seed) + // + // return Result.Ok({ + // userId, + // ...recoveryFileAndFilename + // }) + // } +} + +export default createMutable(new Store()) diff --git a/examples/web/auth/src/style.css b/examples/web/auth/src/style.css new file mode 100644 index 0000000..26c3d1d --- /dev/null +++ b/examples/web/auth/src/style.css @@ -0,0 +1,137 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + min-width: 320px; +} + +body, +#app { + display: flex; + flex-direction: column; + justify-content: space-between; + max-width: 600px; + margin: 0 auto; + padding: 2rem 1rem 0; + font-family: 'IBM Plex Sans', Helvetica, sans-serif; +} + +.button { + background: none; + border: none; + cursor: pointer; +} + +.button:disabled { + pointer-events: none; + opacity: 0.4; +} + +main { + height: 100%; + margin: 1rem 0; +} + +footer { + text-align: center; + color: #666; + padding: 1rem 0; + border-top: 1px solid #ddd; + margin-top: 1rem; +} + +footer p, +footer a { + font-size: 0.8rem !important; + padding-bottom: 0.3rem; +} + +.row { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +a { + color: #000; + font-size: 1rem; + font-weight: 700; +} + +h1 { + font-size: 2rem; + font-weight: 700; +} + +.small { + font-size: 0.8rem; +} + +.button.primary { + background: black; + color: white; + + min-width: 90px; + width: 100%; + + padding: 0.4rem 0.8rem; + border: 2px solid #000; +} + +.button.primary:hover { + background: #222; +} + +.divider { + background: #333; + height: 1px; + margin-top: 1rem; + margin-bottom: 1rem; +} + +form { + display: flex; + flex-direction: column; +} + +label { + font-size: 0.9rem; + font-weight: 700; + margin-bottom: 1rem; +} + +form input { + font-size: 1rem; + width: 100%; + height: 2rem; + border: none; + box-shadow: none; + border-radius: 0; + border-bottom: 1px solid #ddd; +} + +form input:focus { + outline: none; + border-bottom: 2px solid #000; +} + +label.checkbox { + display: flex; + font-weight: normal; + line-height: 1rem; +} + +label.checkbox input { + width: 1rem; + margin: 0 0.5rem 0 0; +} +label.checkbox:focus-within { + outline: 1px solid; +} diff --git a/examples/web/auth/vite.config.js b/examples/web/auth/vite.config.js new file mode 100644 index 0000000..6dba9ca --- /dev/null +++ b/examples/web/auth/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from "vite"; +// import wasmPack from "vite-plugin-wasm-pack"; +import solidPlugin from 'vite-plugin-solid'; +// import path from "node:path"; + +export default defineConfig({ + // pass your local crate path to the plugin + plugins: [ + // wasmPack(path.resolve("../../../pubky")), + solidPlugin() + ], +}); diff --git a/examples/web/no-bundler/.gitignore b/examples/web/no-bundler/.gitignore new file mode 100644 index 0000000..91a3983 --- /dev/null +++ b/examples/web/no-bundler/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +package-lock.json diff --git a/examples/web/no-bundler/README.md b/examples/web/no-bundler/README.md new file mode 100644 index 0000000..ddd6280 --- /dev/null +++ b/examples/web/no-bundler/README.md @@ -0,0 +1,3 @@ +# No bundler + +An example of using Pubky wasm immidiatly in an `index.html` with no bundlers. diff --git a/examples/web/no-bundler/index.html b/examples/web/no-bundler/index.html new file mode 100644 index 0000000..0770e09 --- /dev/null +++ b/examples/web/no-bundler/index.html @@ -0,0 +1,46 @@ + + + + + + + + Pubky demo + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json new file mode 100644 index 0000000..e6e1195 --- /dev/null +++ b/examples/web/no-bundler/package.json @@ -0,0 +1,17 @@ +{ + "scripts": { + "start": "vite serve" + }, + "dependencies": { + "@synonymdev/pubky": "file:../../../pubky/pkg", + + "@solidjs/router": "^0.10.9", + "solid-js": "^1.7.0" + }, + "devDependencies": { + "typescript": "^4.9.5", + "vite": "^4.1.4", + "vite-plugin-html": "^3.2.0", + "vite-plugin-solid": "^2.10.2" + } +} diff --git a/examples/web/package-lock.json b/examples/web/package-lock.json new file mode 100644 index 0000000..29993b6 --- /dev/null +++ b/examples/web/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "web", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 57a8b5c..e3f6e93 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -2,6 +2,10 @@ name = "pubky" version = "0.1.0" edition = "2021" +description = "Pubky client" +license = "MIT" +repository = "https://github.com/pubky/pubky" +keywords = ["web", "dht", "dns", "decentralized", "identity"] [lib] crate-type = ["cdylib", "rlib"] @@ -36,7 +40,13 @@ web-sys = { version = "0.3.69", features = [ pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3.42" + [features] async = ["flume/async"] default = ["async"] + +[package.metadata.docs.rs] +all-features = true diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore new file mode 100644 index 0000000..7a5774d --- /dev/null +++ b/pubky/pkg/.gitignore @@ -0,0 +1,2 @@ +nodejs/* +pubky.mjs diff --git a/pubky/pkg/LICENSE b/pubky/pkg/LICENSE new file mode 100644 index 0000000..a0e67c5 --- /dev/null +++ b/pubky/pkg/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md new file mode 100644 index 0000000..32080e5 --- /dev/null +++ b/pubky/pkg/README.md @@ -0,0 +1,3 @@ +# Pubky + +JavaScript implementation of [Pubky](https://github.com/pubky/pubky). diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json new file mode 100644 index 0000000..6398be1 --- /dev/null +++ b/pubky/pkg/package.json @@ -0,0 +1,28 @@ +{ + "name": "@synonymdev/pubky", + "type": "module", + "description": "Pubky client", + "version": "0.1.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/pubky/pubky" + }, + "files": [ + "nodejs/*", + "pupky.mjs" + ], + "main": "nodejs/pubky.js", + "browser": "pubky.mjs", + "types": "pubky.d.ts", + "sideEffects": [ + "./snippets/*" + ], + "keywords": [ + "web", + "dht", + "dns", + "decentralized", + "identity" + ] +} diff --git a/pubky/src/bin/bundle_wasm.rs b/pubky/src/bin/bundle_wasm.rs new file mode 100644 index 0000000..44cf304 --- /dev/null +++ b/pubky/src/bin/bundle_wasm.rs @@ -0,0 +1,58 @@ +use std::io; +use std::process::{Command, ExitStatus}; + +// If the process hangs, try `cargo clean` to remove all locks. + +fn main() { + println!("cargo:rerun-if-changed=client/"); + + build_wasm("nodejs").unwrap(); + patch().unwrap(); +} + +fn build_wasm(target: &str) -> io::Result { + let output = Command::new("wasm-pack") + .args([ + "build", + "--release", + "--target", + target, + "--out-dir", + &format!("pkg/{}", target), + ]) + .output()?; + + println!( + "wasm-pack {target} output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "wasm-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} + +fn patch() -> io::Result { + let output = Command::new("node") + .args(["./src/bin/patch.mjs"]) + .output()?; + + println!( + "patch.mjs output: {}", + String::from_utf8_lossy(&output.stdout) + ); + + if !output.status.success() { + eprintln!( + "wasm-pack failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output.status) +} diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs new file mode 100644 index 0000000..554c6c8 --- /dev/null +++ b/pubky/src/bin/patch.mjs @@ -0,0 +1,54 @@ +// This script is used to generate isomorphic code for web and nodejs +// +// Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) + +import { readFile, writeFile } from "node:fs/promises"; + +const cargoTomlContent = await readFile("./Cargo.toml", "utf8"); +const cargoPackageName = /\[package\]\nname = "(.*?)"/.exec(cargoTomlContent)[1] +const name = cargoPackageName.replace(/-/g, '_') + +const content = await readFile(`./pkg/nodejs/${name}.js`, "utf8"); + +const patched = content + // use global TextDecoder TextEncoder + .replace("require(`util`)", "globalThis") + // attach to `imports` instead of module.exports + .replace("= module.exports", "= imports") + + // add suffix Class + .replace(/\nclass (.*?) \{/g, "\nclass $1Class {") + .replace(/\nmodule\.exports\.(.*?) = (.*?);/g, "\nexport const $1 = imports.$1 = $1Class") + + // quick and dirty fix for a bug caused by the previous replace + .replace(/__wasmClass/g, "wasm") + + .replace(/\nmodule\.exports\.(.*?)\s+/g, "\nexport const $1 = imports.$1 ") + .replace(/$/, 'export default imports') + // inline bytes Uint8Array + .replace( + /\nconst path.*\nconst bytes.*\n/, + ` +var __toBinary = /* @__PURE__ */ (() => { + var table = new Uint8Array(128); + for (var i = 0; i < 64; i++) + table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i; + return (base64) => { + var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0); + for (var i2 = 0, j = 0; i2 < n; ) { + var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)]; + var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)]; + bytes[j++] = c0 << 2 | c1 >> 4; + bytes[j++] = c1 << 4 | c2 >> 2; + bytes[j++] = c2 << 6 | c3; + } + return bytes; + }; +})(); + +const bytes = __toBinary(${JSON.stringify(await readFile(`./pkg/nodejs/${name}_bg.wasm`, "base64")) + }); +`, + ); + +await writeFile(`./pkg/${name}.mjs`, patched); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 4ed3714..afa33b6 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -24,7 +24,7 @@ if_not_wasm! { } if_wasm! { -mod wasm; + mod wasm; -pub use wasm::{PubkyClient, Keypair}; + pub use wasm::{PubkyClient, Keypair}; } diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs index 7504789..550c8e5 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/client.rs @@ -60,14 +60,25 @@ async fn fetch_base( } let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; - // .map_err(|error| Error::JsError(error))?; let window = web_sys::window().unwrap(); let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; - // .map_err(|error| Error::JsError(error))?; let response: web_sys::Response = response.dyn_into()?; - // .map_err(|error| Error::JsError(error))? Ok(response) } + +#[cfg(test)] +mod tests { + use wasm_bindgen_test::*; + + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + use super::*; + + #[wasm_bindgen_test] + async fn basic() { + // let client = PubkyClient::new(); + } +} From e407461c0dcd70a6f69f488c91f3f48f0b317cb9 Mon Sep 17 00:00:00 2001 From: nazeh Date: Thu, 25 Jul 2024 14:36:39 +0300 Subject: [PATCH 04/26] examples: add preinstall script --- examples/nodejs/package.json | 3 +++ examples/web/auth/package.json | 2 +- examples/web/no-bundler/package.json | 3 ++- .../src/bin/{bundle_wasm.rs => bundle_pubky_npm.rs} | 11 +++++++++-- pubky/src/bin/patch.mjs | 13 +++++++++---- 5 files changed, 24 insertions(+), 8 deletions(-) rename pubky/src/bin/{bundle_wasm.rs => bundle_pubky_npm.rs} (76%) diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index 5e17eb5..a2359bf 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -1,6 +1,9 @@ { "type": "module", "main": "index.js", + "scripts": { + "preinstall": "cargo run --bin bundle_pubky_npm" + }, "dependencies": { "@synonymdev/pubky":"file:../../pubky/pkg" } diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json index 6f1660b..61e1abc 100644 --- a/examples/web/auth/package.json +++ b/examples/web/auth/package.json @@ -6,7 +6,7 @@ "build": "vite build", "start": "vite serve", "preview": "vite preview", - "wasm": "wasm-pack build ../../../pubky/ --target web" + "preinstall": "cargo run --bin bundle_pubky_npm" }, "dependencies": { "@synonymdev/pubky": "file:../../../pubky/pkg", diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json index e6e1195..68d5fbc 100644 --- a/examples/web/no-bundler/package.json +++ b/examples/web/no-bundler/package.json @@ -1,6 +1,7 @@ { "scripts": { - "start": "vite serve" + "start": "vite serve", + "preinstall": "cargo run --bin bundle_pubky_npm" }, "dependencies": { "@synonymdev/pubky": "file:../../../pubky/pkg", diff --git a/pubky/src/bin/bundle_wasm.rs b/pubky/src/bin/bundle_pubky_npm.rs similarity index 76% rename from pubky/src/bin/bundle_wasm.rs rename to pubky/src/bin/bundle_pubky_npm.rs index 44cf304..b3305d3 100644 --- a/pubky/src/bin/bundle_wasm.rs +++ b/pubky/src/bin/bundle_pubky_npm.rs @@ -1,3 +1,4 @@ +use std::env; use std::io; use std::process::{Command, ExitStatus}; @@ -11,9 +12,12 @@ fn main() { } fn build_wasm(target: &str) -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + let output = Command::new("wasm-pack") .args([ "build", + &manifest_dir, "--release", "--target", target, @@ -38,8 +42,11 @@ fn build_wasm(target: &str) -> io::Result { } fn patch() -> io::Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + + println!("{manifest_dir}/src/bin/patch.mjs"); let output = Command::new("node") - .args(["./src/bin/patch.mjs"]) + .args([format!("{manifest_dir}/src/bin/patch.mjs")]) .output()?; println!( @@ -49,7 +56,7 @@ fn patch() -> io::Result { if !output.status.success() { eprintln!( - "wasm-pack failed: {}", + "patch.mjs failed: {}", String::from_utf8_lossy(&output.stderr) ); } diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index 554c6c8..749b434 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -3,12 +3,17 @@ // Based on hacks from [this issue](https://github.com/rustwasm/wasm-pack/issues/1334) import { readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from 'node:url'; +import path, { dirname } from 'node:path'; -const cargoTomlContent = await readFile("./Cargo.toml", "utf8"); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const cargoTomlContent = await readFile(path.join(__dirname, "../../Cargo.toml"), "utf8"); const cargoPackageName = /\[package\]\nname = "(.*?)"/.exec(cargoTomlContent)[1] const name = cargoPackageName.replace(/-/g, '_') -const content = await readFile(`./pkg/nodejs/${name}.js`, "utf8"); +const content = await readFile(path.join(__dirname, `../../pkg/nodejs/${name}.js`), "utf8"); const patched = content // use global TextDecoder TextEncoder @@ -46,9 +51,9 @@ var __toBinary = /* @__PURE__ */ (() => { }; })(); -const bytes = __toBinary(${JSON.stringify(await readFile(`./pkg/nodejs/${name}_bg.wasm`, "base64")) +const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `../../pkg/nodejs/${name}_bg.wasm`), "base64")) }); `, ); -await writeFile(`./pkg/${name}.mjs`, patched); +await writeFile(path.join(__dirname, `../../pkg/${name}.mjs`), patched); From d81b6234d4a46885c372d5f7c5dd036919ccd87e Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 16:45:59 +0300 Subject: [PATCH 05/26] test: add JS unit tests instead of nodejs examples --- examples/nodejs/.gitignore | 5 - examples/nodejs/README.md | 3 - examples/nodejs/index.js | 10 -- examples/nodejs/package.json | 10 -- examples/web/package-lock.json | 6 - js/pubky/.gitignore | 6 - js/pubky/README.md | 3 - js/pubky/examples/basic.js | 9 -- js/pubky/package.json | 51 -------- js/pubky/src/common/auth.js | 194 ------------------------------ js/pubky/src/common/crypto.js | 131 -------------------- js/pubky/src/common/index.js | 8 -- js/pubky/src/common/namespaces.js | 1 - js/pubky/src/common/timestamp.js | 53 -------- js/pubky/src/index.js | 5 - js/pubky/src/lib/client.js | 102 ---------------- js/pubky/src/lib/error.js | 1 - js/pubky/src/lib/fetch-browser.js | 11 -- js/pubky/src/lib/fetch.js | 3 - js/pubky/tsconfig.json | 32 ----- pubky/pkg/.gitignore | 5 +- pubky/pkg/index.js | 1 + pubky/pkg/package.json | 18 ++- pubky/pkg/test/keys.js | 13 ++ pubky/src/bin/patch.mjs | 2 +- pubky/src/wasm/keys.rs | 32 ++++- 26 files changed, 64 insertions(+), 651 deletions(-) delete mode 100644 examples/nodejs/.gitignore delete mode 100644 examples/nodejs/README.md delete mode 100644 examples/nodejs/index.js delete mode 100644 examples/nodejs/package.json delete mode 100644 examples/web/package-lock.json delete mode 100644 js/pubky/.gitignore delete mode 100644 js/pubky/README.md delete mode 100644 js/pubky/examples/basic.js delete mode 100644 js/pubky/package.json delete mode 100644 js/pubky/src/common/auth.js delete mode 100644 js/pubky/src/common/crypto.js delete mode 100644 js/pubky/src/common/index.js delete mode 100644 js/pubky/src/common/namespaces.js delete mode 100644 js/pubky/src/common/timestamp.js delete mode 100644 js/pubky/src/index.js delete mode 100644 js/pubky/src/lib/client.js delete mode 100644 js/pubky/src/lib/error.js delete mode 100644 js/pubky/src/lib/fetch-browser.js delete mode 100644 js/pubky/src/lib/fetch.js delete mode 100644 js/pubky/tsconfig.json create mode 100644 pubky/pkg/index.js create mode 100644 pubky/pkg/test/keys.js diff --git a/examples/nodejs/.gitignore b/examples/nodejs/.gitignore deleted file mode 100644 index fb59162..0000000 --- a/examples/nodejs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.netlify -.parcel* -dist -node_modules -package-lock.json diff --git a/examples/nodejs/README.md b/examples/nodejs/README.md deleted file mode 100644 index c427745..0000000 --- a/examples/nodejs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Nodejs - -An example of using Pubky wasm in nodejs diff --git a/examples/nodejs/index.js b/examples/nodejs/index.js deleted file mode 100644 index 06acbaa..0000000 --- a/examples/nodejs/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { PubkyClient, Keypair } from '@synonymdev/pubky' - -let keypair = Keypair.from_secret_key(new Uint8Array(32).fill(0)) -console.log(keypair) - -const client = new PubkyClient() - -console.log(client) - -const x = client.signup(keypair, "foo.com") diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json deleted file mode 100644 index a2359bf..0000000 --- a/examples/nodejs/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "module", - "main": "index.js", - "scripts": { - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky":"file:../../pubky/pkg" - } -} diff --git a/examples/web/package-lock.json b/examples/web/package-lock.json deleted file mode 100644 index 29993b6..0000000 --- a/examples/web/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "web", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/js/pubky/.gitignore b/js/pubky/.gitignore deleted file mode 100644 index 6da14b1..0000000 --- a/js/pubky/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -coverage -node_modules -types -.storage -.env -package-lock.json diff --git a/js/pubky/README.md b/js/pubky/README.md deleted file mode 100644 index c57a08a..0000000 --- a/js/pubky/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Pubky - -This is a JavaScript implementation of a Pubky protocol client-side tools. diff --git a/js/pubky/examples/basic.js b/js/pubky/examples/basic.js deleted file mode 100644 index 6b40582..0000000 --- a/js/pubky/examples/basic.js +++ /dev/null @@ -1,9 +0,0 @@ -import { PubkyClient } from '../src/index.js' - -main() - -async function main() { - let client = new PubkyClient() - - console.log(client) -} diff --git a/js/pubky/package.json b/js/pubky/package.json deleted file mode 100644 index 08a46cf..0000000 --- a/js/pubky/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@synonymdev/pubky", - "version": "0.1.0", - "description": "Pubky client library", - "type": "module", - "main": "src/index.js", - "types": "types/src/index.d.ts", - "repository": { - "type": "git", - "url": "git+https://github.com/slashtags/skunk-works.git" - }, - "scripts": { - "build": "tsc", - "clean": "rm -rf types", - "lint": "standard --fix", - "test": "brittle test/*.js -cov", - "depcheck": "npx depcheck --ignore-dirs=test", - "fullcheck": "npm run lint && npm run clean && npm run build && npm run test && npm run depcheck", - "prepublishOnly": "npm run fullcheck" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/pubky/pubky/issues" - }, - "homepage": "https://github.com/pubky/pubky/tree/master/js/pubky/#readme", - "files": [ - "src", - "types", - "!**/*.tsbuildinfo" - ], - "dependencies": { - "blake3-wasm": "^3.0.0", - "crockford-base32": "^2.0.0", - "eventsource": "^2.0.2", - "hash-wasm": "^4.11.0", - "node-fetch-cache": "^4.1.2", - "pkarr": "^1.4.1", - "z32": "^1.1.0" - }, - "browser": { - "./src/lib/fetch.js": "./src/lib/fetch-browser.js" - }, - "devDependencies": { - "standard": "^17.1.0", - "typescript": "^5.5.4" - }, - "overrides": { - "blake3-wasm@2.1.7": "^3.0.0", - "@c4312/blake3-internal": "^3.0.0" - } -} diff --git a/js/pubky/src/common/auth.js b/js/pubky/src/common/auth.js deleted file mode 100644 index f48d387..0000000 --- a/js/pubky/src/common/auth.js +++ /dev/null @@ -1,194 +0,0 @@ -import { Timestamp } from './timestamp.js' -import * as namespaces from './namespaces.js' -import * as crypto from './crypto.js' - -// 30 seconds -const TIME_INTERVAL = 30 * 1000000 - -export class AuthnSignature { - /** - * @param {number} time - * @param {import ('./crypto.js').KeyPair} signer - * @param {import ('./crypto.js').PublicKey} audience - * @param {Buffer} [token] - */ - constructor (time, signer, audience, token = crypto.randomBytes()) { - const timeStep = Math.floor(time / TIME_INTERVAL) - - const tokenHash = crypto.hash(token) - - const timeStepBytes = Buffer.allocUnsafe(8) - timeStepBytes.writeBigUint64BE(BigInt(timeStep)) - - const signature = signer.sign(signable( - signer.publicKey().bytes, - audience.bytes, - timeStepBytes, - tokenHash - )) - - this.bytes = Buffer.concat([ - signature, - tokenHash - ]) - } - - /** - * @param {import ('./crypto.js').KeyPair} signer - * @param {import ('./crypto.js').PublicKey} audience - * @param {Buffer} [token] - */ - static sign (signer, audience, token = crypto.randomBytes()) { - const time = Timestamp.now().microseconds - - return new AuthnSignature(time, signer, audience, token) - } - - asBytes () { - return new Uint8Array(this.bytes) - } -} - -export class AuthnVerifier { - #audience - /** @type {Array} */ - #seen - - /** - * @param {crypto.PublicKey} audience - */ - constructor (audience) { - this.#audience = audience - - this.#seen = [] - } - - #gc () { - const threshold = Timestamp.now().microseconds - const threshouldStep = Math.floor(threshold / TIME_INTERVAL) - 2 - - const thresholdBytes = Buffer.allocUnsafe(8) - thresholdBytes.writeBigUint64BE(BigInt(threshouldStep)) - - let count = 0 - - for (let i = 0; i < this.#seen.length; i++) { - if (this.#seen[i].subarray(0, 8).compare(thresholdBytes) > 0) { - break - } - count = i - } - - this.#seen.splice(0, count) - } - - /** - * @param {Buffer} bytes - * @param {crypto.PublicKey} signer - * - * @returns {true | Error} - */ - verify (bytes, signer) { - this.#gc() - - if (bytes.length !== 96) { - throw new Error(`InvalidLength: ${bytes.length}`) - } - - const signature = bytes.subarray(0, 64) - const tokenHash = bytes.subarray(64) - - const now = Timestamp.now().microseconds - const past = now - TIME_INTERVAL - const future = now + TIME_INTERVAL - - let result = verifyAt.call(this, now) - - if (!(result instanceof Error)) { - return result - } else if (result.toString() === 'Error: AuthnSignature already used') { - return result - } - - result = verifyAt.call(this, past) - - if (!(result instanceof Error)) { - return result - } else if (result.toString() === 'Error: AuthnSignature already used') { - return result - } - - return verifyAt.call(this, future) - - /** - * @param {number} time - */ - function verifyAt (time) { - const timeStep = Math.floor(time / TIME_INTERVAL) - - const timeStepBytes = Buffer.allocUnsafe(8) - timeStepBytes.writeBigUint64BE(BigInt(timeStep)) - - const result = signer.verify(signature, signable(signer.bytes, this.#audience.bytes, timeStepBytes, tokenHash)) - - const candidate = Buffer.concat([ - timeStepBytes, - tokenHash - ]) - - if (!(result instanceof Error)) { - const index = binarySearch(this.#seen, timeStepBytes) - - if (this.#seen[index]?.equals(candidate)) { - return new Error('AuthnSignature already used') - } - - this.#seen.splice(~index, 0, candidate) - - return - } - - return result - } - } -} - -/** - * @param {Array} arr - */ -function binarySearch (arr, element) { - let left = 0 - let right = arr.length - 1 - - while (left <= right) { - const mid = Math.floor((left + right) / 2) - - const comparison = arr[mid].subarray(0, 8).compare(element.subarray(0, 8)) - - if (comparison === 0) { - return mid - } else if (comparison < 0) { - left = mid + 1 - } else { - right = mid - 1 - } - } - - return left // Element not found, return the index where it should be inserted -} - -/** - * @param {Buffer} signer - * @param {Buffer} audience - * @param {Buffer} timeStepBytes - * @param {Buffer} tokenHash - */ -function signable (signer, audience, timeStepBytes, tokenHash) { - return Buffer.concat([ - namespaces.PUBKY_AUTHN, - timeStepBytes, - signer, - audience, - tokenHash - ]) -} diff --git a/js/pubky/src/common/crypto.js b/js/pubky/src/common/crypto.js deleted file mode 100644 index c1c81b0..0000000 --- a/js/pubky/src/common/crypto.js +++ /dev/null @@ -1,131 +0,0 @@ -//! Crypeo functions - -import sodium from 'sodium-universal' -import z32 from 'z32' - -// Blake3 - -/** @type {import('blake3-wasm')} */ -let loadedBlake3 - -const loadBlake3 = async () => { - if (loadedBlake3) return loadedBlake3 - // @ts-ignore - loadedBlake3 = await import('blake3-wasm').then(b3 => b3.load().then(() => b3)) - - return loadedBlake3 -} - -loadBlake3() - -/** - * It will return null if blake3 is not loaded yet! - * - * @param {Buffer} message - * - * @returns {Buffer | null} - */ -export const hash = (message) => { - return loadedBlake3?.createHash().update(message).digest() -} - -// Random -export const randomBytes = (n = 32) => { - const buf = Buffer.alloc(n) - sodium.randombytes_buf(buf) - return buf -} - -/// Keypairs - -/** - * @param {Buffer} buf - */ -export const zeroize = (buf) => { - buf.fill(0) -} - -export class KeyPair { - #publicKey - #secretKey - - /** - * @param {Buffer} seed - */ - constructor (seed) { - this.#publicKey = Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES) - this.#secretKey = Buffer.allocUnsafe(sodium.crypto_sign_SECRETKEYBYTES) - - if (seed) sodium.crypto_sign_seed_keypair(this.#publicKey, this.#secretKey, seed) - else sodium.crypto_sign_keypair(this.#publicKey, this.#secretKey) - } - - static random () { - const seed = randomBytes(32) - - return new KeyPair(seed) - } - - zeroize () { - zeroize(this.#secretKey) - this.secretKey = null - } - - publicKey () { - return new PublicKey(this.#publicKey) - } - - secretKey () { - return this.#secretKey - } - - /** - * @param {Uint8Array} message - */ - sign (message) { - const signature = Buffer.alloc(sodium.crypto_sign_BYTES) - sodium.crypto_sign_detached(signature, message, this.#secretKey) - - return signature - } -} - -export class PublicKey { - /** - * @param {Buffer} bytes - */ - constructor (bytes) { - this.bytes = bytes - } - - /** - * @param {string} string - * @returns {Error | PublicKey} - */ - static fromString (string) { - if (string.length !== 52) { - return new Error('Invalid PublicKey string, expected 52 characters, got: ' + string.length) - } - - try { - return new PublicKey(z32.decode(string)) - } catch (error) { - return error - } - } - - /** - * @param {Buffer} signature - * @param {Buffer} message - */ - verify (signature, message) { - const valid = sodium.crypto_sign_verify_detached(signature, message, this.bytes) - if (!valid) return new Error('Invalid signature') - - return true - } - - toString () { - return z32.encode(this.bytes) - } -} diff --git a/js/pubky/src/common/index.js b/js/pubky/src/common/index.js deleted file mode 100644 index 71ccb60..0000000 --- a/js/pubky/src/common/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export * as crypto from './crypto.js' -export { Timestamp } from './timestamp.js' -export { AuthnSignature, AuthnVerifier } from './auth.js' - -/** - * @typedef {string | number | boolean | null} JSONValue - * @typedef {{[key: string]: JSONValue | Array}} JSONObject - */ diff --git a/js/pubky/src/common/namespaces.js b/js/pubky/src/common/namespaces.js deleted file mode 100644 index 156d3af..0000000 --- a/js/pubky/src/common/namespaces.js +++ /dev/null @@ -1 +0,0 @@ -export const PUBKY_AUTHN = Buffer.from('PUBKY:AUTHN') diff --git a/js/pubky/src/common/timestamp.js b/js/pubky/src/common/timestamp.js deleted file mode 100644 index eeeadec..0000000 --- a/js/pubky/src/common/timestamp.js +++ /dev/null @@ -1,53 +0,0 @@ -import { CrockfordBase32 } from 'crockford-base32' -import { randomBytes } from './crypto.js' - -const clockId = randomBytes(1).readUintBE(0, 1) -let latest = 0 - -export class Timestamp { - /** - * @param {number} microseconds - u64 microseconds - */ - constructor (microseconds) { - /** microseconds as u64 */ - this.microseconds = microseconds - } - - static now () { - const now = Date.now() - latest = Math.max(now, latest + 1) - - return new Timestamp((latest * 1000) + clockId) - } - - /** - * @param {string} string - */ - static fromString (string) { - const microseconds = Number(CrockfordBase32.decode(string, { asNumber: true })) - return new Timestamp(microseconds) - } - - /** - * @param {Date} date - */ - static fromDate (date) { - const microseconds = Number(date) * 1000 - return new Timestamp(microseconds) - } - - toString () { - return CrockfordBase32.encode(this.microseconds) - } - - toDate () { - return new Date(this.microseconds / 1000) - } - - intoBytes () { - const buffer = Buffer.allocUnsafe(8) - buffer.writeBigUint64BE(BigInt(this.microseconds), 0) - - return buffer - } -} diff --git a/js/pubky/src/index.js b/js/pubky/src/index.js deleted file mode 100644 index e3e7548..0000000 --- a/js/pubky/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// import { PubkyClient as _PubkyClient } from './lib/client.js' -// import { PubkyError as _PubkyError } from './lib/error.js' -// -// export const PubkyClient = _PubkyClient; -// export const PubkyError = _PubkyError; diff --git a/js/pubky/src/lib/client.js b/js/pubky/src/lib/client.js deleted file mode 100644 index 742262d..0000000 --- a/js/pubky/src/lib/client.js +++ /dev/null @@ -1,102 +0,0 @@ -import Pkarr, { SignedPacket } from 'pkarr' -import { URL } from 'url' - -// import { AuthnSignature, crypto } from '@pubky/common' - -import * as crypto from '../common/crypto.js' - -// import { Pubky } from './pubky.js' -import fetch from './fetch.js' - - -const DEFAULT_PKARR_RELAY = new URL('https://relay.pkarr.org') - -export class PubkyClient { - // TODO: use DHT in nodejs - #pkarrRelay - - crypto = crypto - static crypto = crypto - - /** - * @param {object} [options={}] - * @param {URL} [options.pkarrRelay] - * - * @param {{relay: string, bootstrap: Array<{host: string, port: number}>}} [options.testnet] - */ - constructor(options = {}) { - this.#pkarrRelay = options.pkarrRelay || DEFAULT_PKARR_RELAY - } - - /** - * Publish the SVCB record for `_pubky.`. - * @param {crypto.KeyPair} keypair - * @param {String} host - */ - async publishPubkyHomeserver(keypair, host) { - - let existing = await (async () => { - try { - return (await Pkarr.relayGet(this.#pkarrRelay.toString(), keypair.publicKey().bytes)).packet() - } catch (error) { - return { - id: 0, - type: 'response', - flags: 0, - answers: [] - } - } - })(); - - let answers = [ - ]; - - for (let answer of existing.answers) { - if (!answer.name.startsWith("_pubky")) { - answers.push(answer) - } - } - - let signedPacket = SignedPacket.fromPacket(keypair, { - id: 0, - type: 'response', - flags: 0, - answers: [ - ...answers, - { - name: '_pubky.', type: 'SVCB', ttl: 7200, data: - - Buffer.from( - - ) - - } - ] - }) - - // let mut packet = Packet:: new_reply(0); - // - // if let Some(existing) = self.pkarr.resolve(& keypair.public_key()) ? { - // for answer in existing.packet().answers.iter().cloned() { - // if !answer.name.to_string().starts_with("_pubky") { - // packet.answers.push(answer.into_owned()) - // } - // } - // } - // - // let svcb = SVCB::new (0, host.try_into() ?); - // - // packet.answers.push(pkarr:: dns:: ResourceRecord:: new ( - // "_pubky".try_into().unwrap(), - // pkarr:: dns:: CLASS:: IN, - // 60 * 60, - // pkarr:: dns:: rdata:: RData:: SVCB(svcb), - // )); - // - // let signed_packet = SignedPacket:: from_packet(keypair, & packet) ?; - // - // self.pkarr.publish(& signed_packet) ?; - } -} - -export default PubkyClient diff --git a/js/pubky/src/lib/error.js b/js/pubky/src/lib/error.js deleted file mode 100644 index 29d4146..0000000 --- a/js/pubky/src/lib/error.js +++ /dev/null @@ -1 +0,0 @@ -export class PubkyError extends Error { } diff --git a/js/pubky/src/lib/fetch-browser.js b/js/pubky/src/lib/fetch-browser.js deleted file mode 100644 index 30b547c..0000000 --- a/js/pubky/src/lib/fetch-browser.js +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -const g = - (typeof globalThis !== 'undefined' && globalThis) || - // eslint-disable-next-line no-undef - (typeof self !== 'undefined' && self) || - // eslint-disable-next-line no-undef - (typeof global !== 'undefined' && global) || - {} - -// @ts-ignore -export default g.fetch diff --git a/js/pubky/src/lib/fetch.js b/js/pubky/src/lib/fetch.js deleted file mode 100644 index 7c24a8b..0000000 --- a/js/pubky/src/lib/fetch.js +++ /dev/null @@ -1,3 +0,0 @@ -import fetch from 'node-fetch-cache' - -export default fetch diff --git a/js/pubky/tsconfig.json b/js/pubky/tsconfig.json deleted file mode 100644 index 63d7a71..0000000 --- a/js/pubky/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - // Declarations control - "target": "esnext", - "module": "esnext", - - "noEmitOnError": true, - "emitDeclarationOnly": true, - "declarationMap": true, - "isolatedModules": true, - - "incremental": true, - "composite": true, - - // Check control - "strict": false, - "allowJs": true, - "checkJs": true, - - // module resolution - "esModuleInterop": true, - "moduleResolution": "node", - "resolveJsonModule": true, - - // advanced - "verbatimModuleSyntax": true, - "skipLibCheck": true, - - "outDir": "types" - }, - "include": ["src", "lib"] -} diff --git a/pubky/pkg/.gitignore b/pubky/pkg/.gitignore index 7a5774d..bc0022f 100644 --- a/pubky/pkg/.gitignore +++ b/pubky/pkg/.gitignore @@ -1,2 +1,5 @@ nodejs/* -pubky.mjs +browser.js +coverage +node_modules +package-lock.json diff --git a/pubky/pkg/index.js b/pubky/pkg/index.js new file mode 100644 index 0000000..0af4fe0 --- /dev/null +++ b/pubky/pkg/index.js @@ -0,0 +1 @@ +export * from './nodejs/pubky.js' diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 6398be1..3c48852 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -8,12 +8,18 @@ "type": "git", "url": "https://github.com/pubky/pubky" }, + "scripts": { + "lint": "standard --fix", + "test": "brittle test/*.js -cov", + "prepublishOnly": "npm run lint && npm run test" + }, "files": [ "nodejs/*", - "pupky.mjs" + "index.js", + "browser.js" ], - "main": "nodejs/pubky.js", - "browser": "pubky.mjs", + "main": "index.js", + "browser": "browser.js", "types": "pubky.d.ts", "sideEffects": [ "./snippets/*" @@ -24,5 +30,9 @@ "dns", "decentralized", "identity" - ] + ], + "devDependencies": { + "brittle": "^3.6.1", + "standard": "^17.1.0" + } } diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js new file mode 100644 index 0000000..3564964 --- /dev/null +++ b/pubky/pkg/test/keys.js @@ -0,0 +1,13 @@ +import test from 'brittle' + +import { Keypair } from '../index.js' + +test('generate keys from a seed', async (t) => { + const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') + + const keypair = Keypair.fromSecretKey(secretkey) + + const publicKey = keypair.publicKey() + + t.is(publicKey.toString(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') +}) diff --git a/pubky/src/bin/patch.mjs b/pubky/src/bin/patch.mjs index 749b434..ebbf13b 100644 --- a/pubky/src/bin/patch.mjs +++ b/pubky/src/bin/patch.mjs @@ -56,4 +56,4 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `. `, ); -await writeFile(path.join(__dirname, `../../pkg/${name}.mjs`), patched); +await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched); diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 3859511..6f6c6dd 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -6,10 +6,40 @@ pub struct Keypair(pkarr::Keypair); #[wasm_bindgen] impl Keypair { #[wasm_bindgen] - pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { + /// Generate a random [Keypair] + pub fn random(secret_key: js_sys::Uint8Array) -> Self { + Self(pkarr::Keypair::random()) + } + + #[wasm_bindgen] + /// Generate a [Keypair] from a secret key. + pub fn fromSecretKey(secret_key: js_sys::Uint8Array) -> Self { let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); Self(pkarr::Keypair::from_secret_key(&bytes)) } + + #[wasm_bindgen] + /// Returns the [PublicKey] of this keypair. + pub fn publicKey(&self) -> PublicKey { + PublicKey(self.0.public_key()) + } +} + +#[wasm_bindgen] +pub struct PublicKey(pkarr::PublicKey); + +#[wasm_bindgen] +impl PublicKey { + #[wasm_bindgen] + /// Return the public key as Uint8Array + pub fn toBytes(&self) -> js_sys::Uint8Array { + js_sys::Uint8Array::from(self.0.as_bytes().as_slice()) + } + + #[wasm_bindgen] + pub fn toString(&self) -> String { + self.0.to_string() + } } From 42156b10f58f65a012a42da11956e25e3386fd69 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 20:10:50 +0300 Subject: [PATCH 06/26] feat(js): signup --- pubky/Cargo.toml | 2 +- pubky/pkg/package.json | 1 + pubky/pkg/test/auth.js | 45 +++++++++++++++++++++++++++++++++++++++ pubky/pkg/test/keys.js | 6 +++--- pubky/src/error.rs | 5 +++++ pubky/src/lib.rs | 5 +++-- pubky/src/wasm.rs | 4 ++-- pubky/src/wasm/client.rs | 2 +- pubky/src/wasm/keys.rs | 10 ++++----- pubky/src/wasm/pkarr.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 pubky/pkg/test/auth.js create mode 100644 pubky/src/wasm/pkarr.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index e3f6e93..8396f3b 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -12,12 +12,12 @@ crate-type = ["cdylib", "rlib"] [dependencies] pkarr = "2.1.0" +thiserror = "1.0.62" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } ureq = { version = "2.10.0", features = ["cookies"] } -thiserror = "1.0.62" url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 3c48852..b3e43e9 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -11,6 +11,7 @@ "scripts": { "lint": "standard --fix", "test": "brittle test/*.js -cov", + "preinstall": "cargo run --bin bundle_pubky_npm", "prepublishOnly": "npm run lint && npm run test" }, "files": [ diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js new file mode 100644 index 0000000..0bc1b52 --- /dev/null +++ b/pubky/pkg/test/auth.js @@ -0,0 +1,45 @@ +import test from 'brittle' +import z32 from 'z32' + +import App from '@pubky/homeserver/test/helper/app.js' + +import Client from '../src/index.js' + +test('seed auth', async (t) => { + // const homeserver = await App(t) + + // const client = new Client( + // homeserver.homeserver.pkarr.serverPkarr.publicKey(), + // { + // relay: homeserver.testnet.relay + // } + // ) + // await client.ready() + // + // const seed = Client.crypto.generateSeed() + // const keypair = Client.crypto.generateKeyPair(seed) + // const expectedUserId = keypair.public_key().to_string() + // + // const userIdResult = await client.signup(seed) + // t.ok(userIdResult.isOk(), userIdResult.error) + // + // const userId = userIdResult.value + // t.is(userId, expectedUserId) + // + // const session = await client.session() + // t.ok(session?.users[userId]) + // + // { + // await client.logout(userId) + // + // const session = await client.session() + // t.absent(session?.users?.[userId]) + // } + // + // { + // await client.login(seed) + // + // const session = await client.session() + // t.ok(session?.users[userId]) + // } +}) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 3564964..d01467a 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -5,9 +5,9 @@ import { Keypair } from '../index.js' test('generate keys from a seed', async (t) => { const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex') - const keypair = Keypair.fromSecretKey(secretkey) + const keypair = Keypair.from_secret_key(secretkey) - const publicKey = keypair.publicKey() + const publicKey = keypair.public_key() - t.is(publicKey.toString(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') + t.is(publicKey.to_string(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) diff --git a/pubky/src/error.rs b/pubky/src/error.rs index acbad4b..f051d14 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -23,18 +23,23 @@ pub enum Error { Pkarr(#[from] pkarr::Error), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Flume(#[from] flume::RecvError), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Ureq(#[from] Box), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Url(#[from] url::ParseError), #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), } +#[cfg(not(target_arch = "wasm32"))] impl From for Error { fn from(error: ureq::Error) -> Self { Error::Ureq(Box::new(error)) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index afa33b6..39955c4 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -14,13 +14,14 @@ macro_rules! if_wasm { )*} } +mod error; +pub use error::Error; + if_not_wasm! { mod client; mod client_async; - mod error; pub use client::PubkyClient; - pub use error::Error; } if_wasm! { diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index faddf5b..9610160 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,6 @@ -mod keys; - mod client; +mod keys; +mod pkarr; pub use client::PubkyClient; pub use keys::Keypair; diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/client.rs index 550c8e5..e692cb8 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/client.rs @@ -11,7 +11,7 @@ pub struct Error {} #[wasm_bindgen] pub struct PubkyClient { - pkarr: PkarrRelayClient, + pub(crate) pkarr: PkarrRelayClient, } #[wasm_bindgen] diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index 6f6c6dd..f73489d 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -13,7 +13,7 @@ impl Keypair { #[wasm_bindgen] /// Generate a [Keypair] from a secret key. - pub fn fromSecretKey(secret_key: js_sys::Uint8Array) -> Self { + pub fn from_secret_key(secret_key: js_sys::Uint8Array) -> Self { let mut bytes = [0; 32]; secret_key.copy_to(&mut bytes); @@ -22,7 +22,7 @@ impl Keypair { #[wasm_bindgen] /// Returns the [PublicKey] of this keypair. - pub fn publicKey(&self) -> PublicKey { + pub fn public_key(&self) -> PublicKey { PublicKey(self.0.public_key()) } } @@ -33,13 +33,13 @@ pub struct PublicKey(pkarr::PublicKey); #[wasm_bindgen] impl PublicKey { #[wasm_bindgen] - /// Return the public key as Uint8Array - pub fn toBytes(&self) -> js_sys::Uint8Array { + /// Convert the PublicKey to Uint8Array + pub fn to_uint8array(&self) -> js_sys::Uint8Array { js_sys::Uint8Array::from(self.0.as_bytes().as_slice()) } #[wasm_bindgen] - pub fn toString(&self) -> String { + pub fn to_string(&self) -> String { self.0.to_string() } } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs new file mode 100644 index 0000000..81a5685 --- /dev/null +++ b/pubky/src/wasm/pkarr.rs @@ -0,0 +1,46 @@ +use wasm_bindgen::prelude::*; + +pub use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::error::Result; + +use super::PubkyClient; + +// TODO: Share more code with the non-wasm client. + +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = self.pkarr.resolve(&keypair.public_key()).await? { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr.publish(&signed_packet).await?; + + Ok(()) + } +} From cdfd6c30ffb9940a02a38c8cdcde4d2aa5717a8b Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 21:10:19 +0300 Subject: [PATCH 07/26] refactor(pubky): refactor modules --- pubky/src/client.rs | 6 ++---- pubky/src/client/auth.rs | 5 ++--- pubky/src/client/public.rs | 4 +--- pubky/src/client_async.rs | 2 +- pubky/src/lib.rs | 13 ++++++++----- pubky/src/wasm.rs | 14 +++++++++----- pubky/src/wasm/{client.rs => auth.rs} | 10 +--------- pubky/src/wasm/pkarr.rs | 2 +- 8 files changed, 25 insertions(+), 31 deletions(-) rename pubky/src/wasm/{client.rs => auth.rs} (93%) diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 95742df..63fae96 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -7,10 +7,10 @@ use std::{collections::HashMap, fmt::format, time::Duration}; use ureq::{Agent, Response}; use url::Url; -use crate::error::{Error, Result}; - use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; +use crate::error::{Error, Result}; + #[derive(Debug, Clone)] pub struct PubkyClient { agent: Agent, @@ -40,8 +40,6 @@ impl PubkyClient { } } - // === Public Methods === - // === Private Methods === fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index 8445640..a9f3da1 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -1,9 +1,8 @@ -use crate::PubkyClient; +use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, HttpMethod, Result}; -use pkarr::{Keypair, PublicKey}; +use super::{Error, HttpMethod, PubkyClient, Result}; impl PubkyClient { /// Signup to a homeserver and update Pkarr accordingly. diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index b54cd21..68b8209 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -2,9 +2,7 @@ use bytes::Bytes; use pkarr::PublicKey; -use crate::PubkyClient; - -use super::Result; +use super::{PubkyClient, Result}; impl PubkyClient { pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs index 2fb7bd5..6fa063e 100644 --- a/pubky/src/client_async.rs +++ b/pubky/src/client_async.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use pkarr::{Keypair, PublicKey}; use pubky_common::session::Session; -use crate::{error::Result, PubkyClient}; +use crate::{client::PubkyClient, error::Result}; pub struct PubkyClientAsync(PubkyClient); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 39955c4..87c535d 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -14,18 +14,21 @@ macro_rules! if_wasm { )*} } -mod error; -pub use error::Error; - if_not_wasm! { mod client; mod client_async; - pub use client::PubkyClient; + use pkarr::{PkarrClient}; + use ureq::{Agent, Response}; + use url::Url; } if_wasm! { mod wasm; - pub use wasm::{PubkyClient, Keypair}; + pub use wasm::keys::Keypair; + pub use wasm::PubkyClient; } + +mod error; +pub use error::Error; diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 9610160..a5589d3 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,10 @@ -mod client; -mod keys; -mod pkarr; +use wasm_bindgen::prelude::*; -pub use client::PubkyClient; -pub use keys::Keypair; +pub mod auth; +pub mod keys; +pub mod pkarr; + +#[wasm_bindgen] +pub struct PubkyClient { + pub(crate) pkarr: pkarr::PkarrRelayClient, +} diff --git a/pubky/src/wasm/client.rs b/pubky/src/wasm/auth.rs similarity index 93% rename from pubky/src/wasm/client.rs rename to pubky/src/wasm/auth.rs index e692cb8..e8bc842 100644 --- a/pubky/src/wasm/client.rs +++ b/pubky/src/wasm/auth.rs @@ -4,15 +4,7 @@ use web_sys::RequestMode; use pkarr::PkarrRelayClient; -use super::Keypair; - -#[wasm_bindgen] -pub struct Error {} - -#[wasm_bindgen] -pub struct PubkyClient { - pub(crate) pkarr: PkarrRelayClient, -} +use super::{keys::Keypair, PubkyClient}; #[wasm_bindgen] impl PubkyClient { diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 81a5685..97d39a1 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, + Keypair, PkarrRelayClient, PublicKey, SignedPacket, }; use crate::error::Result; From e05a49cd04c1a6bc7fdd4242c2930a4d9be4f142 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 26 Jul 2024 21:27:18 +0300 Subject: [PATCH 08/26] fix: cargo clippy --- pubky-common/src/auth.rs | 4 ++-- pubky-common/src/session.rs | 7 ++++--- pubky/src/client/auth.rs | 2 +- pubky/src/lib.rs | 4 +--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pubky-common/src/auth.rs b/pubky-common/src/auth.rs index 7fc2a02..5d5ebba 100644 --- a/pubky-common/src/auth.rs +++ b/pubky-common/src/auth.rs @@ -206,7 +206,7 @@ mod tests { let mut invalid = authn_signature.as_bytes().to_vec(); invalid[64..].copy_from_slice(&[0; 32]); - assert!(!verifier.verify(&invalid, &signer).is_ok()) + assert!(verifier.verify(&invalid, &signer).is_err()) } { @@ -214,7 +214,7 @@ mod tests { let mut invalid = authn_signature.as_bytes().to_vec(); invalid[0..32].copy_from_slice(&[0; 32]); - assert!(!verifier.verify(&invalid, &signer).is_ok()) + assert!(verifier.verify(&invalid, &signer).is_err()) } } } diff --git a/pubky-common/src/session.rs b/pubky-common/src/session.rs index 9ef3c9d..5a35e14 100644 --- a/pubky-common/src/session.rs +++ b/pubky-common/src/session.rs @@ -69,9 +69,10 @@ mod tests { #[test] fn serialize() { - let mut session = Session::default(); - - session.user_agent = "foo".to_string(); + let session = Session { + user_agent: "foo".to_string(), + ..Default::default() + }; let serialized = session.serialize(); diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index a9f3da1..b1f4cc8 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -112,7 +112,7 @@ mod tests { match session { Err(Error::NotSignedIn) => {} - _ => assert!(false, "expected NotSignedInt error"), + _ => panic!("expected NotSignedInt error"), } } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 87c535d..fab446f 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -18,9 +18,7 @@ if_not_wasm! { mod client; mod client_async; - use pkarr::{PkarrClient}; - use ureq::{Agent, Response}; - use url::Url; + use client::PubkyClient; } if_wasm! { From c466ca5546e9bfd33963527c6ff781cac648d6ef Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 10:13:42 +0300 Subject: [PATCH 09/26] refactor(pubky): share helper functions between rust and wasm --- pubky/Cargo.toml | 3 +- pubky/src/client/pkarr.rs | 89 +++++++------------------------- pubky/src/error.rs | 7 ++- pubky/src/lib.rs | 2 + pubky/src/shared.rs | 1 + pubky/src/shared/pkarr.rs | 104 ++++++++++++++++++++++++++++++++++++++ pubky/src/wasm.rs | 10 ++++ pubky/src/wasm/auth.rs | 11 +--- pubky/src/wasm/keys.rs | 12 +++++ pubky/src/wasm/pkarr.rs | 28 ++-------- 10 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 pubky/src/shared.rs create mode 100644 pubky/src/shared/pkarr.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 8396f3b..8408c39 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -13,12 +13,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] pkarr = "2.1.0" thiserror = "1.0.62" +wasm-bindgen = "0.2.92" +url = "2.5.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pubky-common = { version = "0.1.0", path = "../pubky-common" } ureq = { version = "2.10.0", features = ["cookies"] } -url = "2.5.2" flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } bytes = "1.6.1" diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs index c036527..a568c4c 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/client/pkarr.rs @@ -4,33 +4,16 @@ pub use pkarr::{ Keypair, PkarrClient, PublicKey, Settings, SignedPacket, }; -use super::{Error, PubkyClient, Result, Url}; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; +use super::{Error, PubkyClient, Result, Url}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key())?; - if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; self.pkarr.publish(&signed_packet)?; @@ -48,66 +31,32 @@ impl PubkyClient { /// Resolve a service's public_key and clearnet url from a Pubky domain pub(crate) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { // TODO: cache the result of this function? - // TODO: use MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION - // TODO: move to common? 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()) { - if let Some(signed_packet) = self.pkarr.resolve(&public_key)? { - let mut prior = None; + let response = self.pkarr.resolve(&public_key)?; - for answer in signed_packet.resource_records(&target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) - } - // TODO return random if priority is the same - } else { - prior = Some(svcb) - } - } - } + let done = parse_pubky_svcb( + response, + &public_key, + &mut target, + &mut homeserver_public_key, + &mut host, + &mut step, + ); - if let Some(svcb) = prior { - homeserver_public_key = Some(public_key); - target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - host = format!("{target}:{port}"); - } else { - host.clone_from(&target); - }; - - continue; - } - }; - - break; + if done { + break; + } } - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::Generic("Could not resolve endpoint".to_string())) + format_url(homeserver_public_key, host) } } diff --git a/pubky/src/error.rs b/pubky/src/error.rs index f051d14..2b5320e 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -22,6 +22,9 @@ pub enum Error { #[error(transparent)] Pkarr(#[from] pkarr::Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Flume(#[from] flume::RecvError), @@ -30,10 +33,6 @@ pub enum Error { #[cfg(not(target_arch = "wasm32"))] Ureq(#[from] Box), - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Url(#[from] url::ParseError), - #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index fab446f..91a117d 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -29,4 +29,6 @@ if_wasm! { } mod error; +mod shared; + pub use error::Error; diff --git a/pubky/src/shared.rs b/pubky/src/shared.rs new file mode 100644 index 0000000..ad39b70 --- /dev/null +++ b/pubky/src/shared.rs @@ -0,0 +1 @@ +pub mod pkarr; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs new file mode 100644 index 0000000..e0d87cf --- /dev/null +++ b/pubky/src/shared/pkarr.rs @@ -0,0 +1,104 @@ +use url::Url; + +use pkarr::{ + dns::{rdata::SVCB, Packet}, + Keypair, PublicKey, SignedPacket, +}; + +use crate::error::{Error, Result}; + +const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; + +pub fn prepare_packet_for_signup( + keypair: &Keypair, + host: &str, + existing: Option, +) -> Result { + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } + } + } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + Ok(SignedPacket::from_packet(keypair, &packet)?) +} + +pub fn parse_pubky_svcb( + signed_packet: Option, + public_key: &PublicKey, + target: &mut String, + homeserver_public_key: &mut Option, + host: &mut String, + step: &mut u8, +) -> bool { + *step += 1; + + let mut prior = None; + + if let Some(signed_packet) = signed_packet { + for answer in signed_packet.resource_records(target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } + } + } + + if let Some(svcb) = prior { + *homeserver_public_key = Some(public_key.clone()); + *target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + *host = format!("{target}:{port}"); + } else { + host.clone_from(target); + }; + + return *step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION; + } + } + + true +} + +pub fn format_url( + homeserver_public_key: Option, + host: String, +) -> Result<(PublicKey, Url)> { + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") + } else { + format!("https://{host}") + }; + + return Ok((homeserver, Url::parse(&url)?)); + } + + Err(Error::Generic("Could not resolve endpoint".to_string())) +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index a5589d3..2414ba4 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -8,3 +8,13 @@ pub mod pkarr; pub struct PubkyClient { pub(crate) pkarr: pkarr::PkarrRelayClient, } + +#[wasm_bindgen] +impl PubkyClient { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + pkarr: pkarr::PkarrRelayClient::default(), + } + } +} diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index e8bc842..6f03834 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -8,19 +8,12 @@ use super::{keys::Keypair, PubkyClient}; #[wasm_bindgen] impl PubkyClient { - #[wasm_bindgen(constructor)] - pub fn new() -> Self { - Self { - pkarr: PkarrRelayClient::default(), - } - } - /// 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 fn signup(&self, secret_key: Keypair, homeserver: &str) -> Result<(), JsValue> { + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<(), JsError> { // let (audience, mut url) = self.resolve_endpoint(homeserver)?; // url.set_path(&format!("/{}", keypair.public_key())); @@ -29,7 +22,7 @@ impl PubkyClient { // fetch_base(url.to_string(), "PUT", body).await?; - // self.publish_pubky_homeserver(keypair, homeserver); + self.publish_pubky_homeserver(keypair, homeserver).await?; Ok(()) } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index f73489d..65beae8 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -27,6 +27,12 @@ impl Keypair { } } +impl Keypair { + pub fn as_inner(&self) -> &pkarr::Keypair { + &self.0 + } +} + #[wasm_bindgen] pub struct PublicKey(pkarr::PublicKey); @@ -43,3 +49,9 @@ impl PublicKey { self.0.to_string() } } + +impl PublicKey { + pub fn as_inner(&self) -> &pkarr::PublicKey { + &self.0 + } +} diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 97d39a1..b000528 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -2,14 +2,13 @@ use wasm_bindgen::prelude::*; pub use pkarr::{ dns::{rdata::SVCB, Packet}, - Keypair, PkarrRelayClient, PublicKey, SignedPacket, + PkarrRelayClient, PublicKey, SignedPacket, }; use crate::error::Result; +use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; -use super::PubkyClient; - -// TODO: Share more code with the non-wasm client. +use super::{keys::Keypair, PubkyClient}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. @@ -18,26 +17,9 @@ impl PubkyClient { keypair: &Keypair, host: &str, ) -> Result<()> { - let mut packet = Packet::new_reply(0); + let existing = self.pkarr.resolve(&keypair.public_key().as_inner()).await?; - if let Some(existing) = self.pkarr.resolve(&keypair.public_key()).await? { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) - } - } - } - - let svcb = SVCB::new(0, host.try_into()?); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + let signed_packet = prepare_packet_for_signup(keypair.as_inner(), host, existing)?; self.pkarr.publish(&signed_packet).await?; From ba50429b7ad46d8b79c6cc4f18a4536c2e971364 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 18:31:34 +0300 Subject: [PATCH 10/26] feat(pubky): use reqwest and async instead of ureq --- Cargo.lock | 433 ++++++++++++++++++++++++++++++++++++- pubky/Cargo.toml | 1 + pubky/src/client.rs | 51 ++--- pubky/src/client/auth.rs | 62 +++--- pubky/src/client/pkarr.rs | 31 ++- pubky/src/client/public.rs | 2 +- pubky/src/client_async.rs | 94 -------- pubky/src/error.rs | 10 +- pubky/src/lib.rs | 1 - pubky/src/wasm.rs | 2 + pubky/src/wasm/auth.rs | 42 +--- 11 files changed, 509 insertions(+), 220 deletions(-) delete mode 100644 pubky/src/client_async.rs diff --git a/Cargo.lock b/Cargo.lock index b8b2d2d..dbb725b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -198,6 +204,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -305,9 +317,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", - "idna", + "idna 0.5.0", "indexmap", "log", + "publicsuffix", "serde", "serde_derive", "serde_json", @@ -315,6 +328,22 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -511,12 +540,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -551,6 +605,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -678,6 +747,25 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.2.1" @@ -737,7 +825,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" dependencies = [ - "bitflags", + "bitflags 2.6.0", "byteorder", "heed-traits", "heed-types", @@ -830,6 +918,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -838,6 +927,40 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -847,12 +970,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -875,6 +1013,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.11" @@ -908,10 +1052,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "litrs" version = "0.4.1" @@ -1027,6 +1177,23 @@ dependencies = [ "getrandom", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1068,6 +1235,50 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1223,6 +1434,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "postcard" version = "1.0.8" @@ -1256,6 +1473,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + [[package]] name = "pubky" version = "0.1.0" @@ -1267,6 +1490,7 @@ dependencies = [ "pkarr", "pubky-common", "pubky_homeserver", + "reqwest", "thiserror", "tokio", "ureq", @@ -1316,6 +1540,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quote" version = "1.0.36" @@ -1361,7 +1595,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1419,6 +1653,51 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -1449,6 +1728,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.23.11" @@ -1464,6 +1756,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.7.0" @@ -1493,6 +1795,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1505,6 +1816,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.0.4" @@ -1650,7 +1984,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1747,6 +2081,39 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.62" @@ -1853,6 +2220,27 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -1905,7 +2293,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "http", "http-body", @@ -1990,6 +2378,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" @@ -2048,7 +2442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -2058,12 +2452,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2341,6 +2750,16 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "z32" version = "1.1.1" diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 8408c39..758af8e 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -15,6 +15,7 @@ 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" } diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 63fae96..2839110 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,33 +1,42 @@ mod auth; mod pkarr; -mod public; +// mod public; use std::{collections::HashMap, fmt::format, time::Duration}; -use ureq::{Agent, Response}; +use ::pkarr::PkarrClientAsync; use url::Url; use pkarr::{DhtSettings, PkarrClient, Settings, Testnet}; use crate::error::{Error, Result}; +static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + #[derive(Debug, Clone)] pub struct PubkyClient { - agent: Agent, - pkarr: PkarrClient, + http: reqwest::Client, + pkarr: PkarrClientAsync, } impl PubkyClient { pub fn new() -> Self { Self { - agent: Agent::new(), - pkarr: PkarrClient::new(Default::default()).unwrap(), + http: reqwest::Client::builder() + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), + pkarr: PkarrClient::new(Default::default()).unwrap().as_async(), } } pub fn test(testnet: &Testnet) -> Self { Self { - agent: Agent::new(), + http: reqwest::Client::builder() + .cookie_store(true) + .user_agent(DEFAULT_USER_AGENT) + .build() + .unwrap(), pkarr: PkarrClient::new(Settings { dht: DhtSettings { request_timeout: Some(Duration::from_millis(10)), @@ -36,15 +45,10 @@ impl PubkyClient { }, ..Settings::default() }) - .unwrap(), + .unwrap() + .as_async(), } } - - // === Private Methods === - - fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request { - self.agent.request_url(method.into(), url) - } } impl Default for PubkyClient { @@ -52,22 +56,3 @@ impl Default for PubkyClient { Self::new() } } - -#[derive(Debug, Clone)] -pub enum HttpMethod { - Get, - Put, - Post, - Delete, -} - -impl From for &str { - fn from(value: HttpMethod) -> Self { - match value { - HttpMethod::Get => "GET", - HttpMethod::Put => "PUT", - HttpMethod::Post => "POST", - HttpMethod::Delete => "DELETE", - } - } -} diff --git a/pubky/src/client/auth.rs b/pubky/src/client/auth.rs index b1f4cc8..c99a451 100644 --- a/pubky/src/client/auth.rs +++ b/pubky/src/client/auth.rs @@ -1,23 +1,27 @@ -use pkarr::{Keypair, PublicKey}; +use reqwest::StatusCode; +use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; -use super::{Error, HttpMethod, PubkyClient, Result}; +use super::{Error, PubkyClient, Result}; 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 fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { + let (audience, mut url) = self.resolve_endpoint(homeserver).await?; url.set_path(&format!("/{}", keypair.public_key())); - self.request(HttpMethod::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); + self.http.put(url).body(body).send().await?; + + self.publish_pubky_homeserver(keypair, homeserver).await?; Ok(()) } @@ -26,52 +30,50 @@ impl PubkyClient { /// /// Returns an [Error::NotSignedIn] if so, or [ureq::Error] if /// the response has any other `>=400` status code. - pub fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + pub async fn session(&self, pubky: &PublicKey) -> Result { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - let mut bytes = vec![]; + let res = self.http.get(url).send().await?; - let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new); + if res.status() == StatusCode::NOT_FOUND { + return Err(Error::NotSignedIn); + } - let reader = self.request(HttpMethod::Get, &url).call().map_err(|err| { - match err { - ureq::Error::Status(404, _) => Error::NotSignedIn, - // TODO: handle other types of errors - _ => err.into(), - } - })?; + if !res.status().is_success() { + res.error_for_status_ref()?; + }; - reader.into_reader().read_to_end(&mut bytes); + let bytes = res.bytes().await?; Ok(Session::deserialize(&bytes)?) } /// Signout from a homeserver. - pub fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?; + pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - self.request(HttpMethod::Delete, &url) - .call() - .map_err(Box::new)?; + self.http.delete(url).send().await?; Ok(()) } /// Signin to a homeserver. - pub fn signin(&self, keypair: &Keypair) -> Result<()> { + pub async fn signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); - let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?; + let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; url.set_path(&format!("/{}/session", &pubky)); - self.request(HttpMethod::Post, &url) - .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes()) - .map_err(Box::new)?; + let body = AuthnSignature::generate(keypair, &audience) + .as_bytes() + .to_owned(); + + self.http.post(url).body(body).send().await?; Ok(()) } @@ -90,7 +92,7 @@ mod tests { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); - let client = PubkyClient::test(&testnet).as_async(); + let client = PubkyClient::test(&testnet); let keypair = Keypair::random(); diff --git a/pubky/src/client/pkarr.rs b/pubky/src/client/pkarr.rs index a568c4c..f22ae43 100644 --- a/pubky/src/client/pkarr.rs +++ b/pubky/src/client/pkarr.rs @@ -10,26 +10,34 @@ use super::{Error, PubkyClient, Result, Url}; impl PubkyClient { /// Publish the SVCB record for `_pubky.`. - pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key())?; + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let existing = self.pkarr.resolve(&keypair.public_key()).await?; let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - self.pkarr.publish(&signed_packet)?; + self.pkarr.publish(&signed_packet).await?; Ok(()) } /// Resolve the homeserver for a pubky. - pub(crate) fn resolve_pubky_homeserver(&self, pubky: &PublicKey) -> Result<(PublicKey, Url)> { + 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) fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { + pub(crate) async fn resolve_endpoint(&self, target: &str) -> Result<(PublicKey, Url)> { // TODO: cache the result of this function? let mut target = target.to_string(); @@ -40,7 +48,7 @@ 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)?; + let response = self.pkarr.resolve(&public_key).await?; let done = parse_pubky_svcb( response, @@ -106,23 +114,24 @@ mod tests { pkarr_client.publish(&signed_packet).await.unwrap(); - tokio::task::spawn_blocking(move || { + { let client = PubkyClient::test(&testnet); let pubky = Keypair::random(); client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())); + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) + .await + .unwrap(); let (public_key, url) = client .resolve_pubky_homeserver(&pubky.public_key()) + .await .unwrap(); assert_eq!(public_key, server.public_key()); assert_eq!(url.host_str(), Some("localhost")); assert_eq!(url.port(), Some(server.port())); - }) - .await - .expect("task failed") + } } } diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index 68b8209..e2d396f 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -73,7 +73,7 @@ mod tests { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); - let client = PubkyClient::test(&testnet).as_async(); + let client = PubkyClient::test(&testnet); let keypair = Keypair::random(); diff --git a/pubky/src/client_async.rs b/pubky/src/client_async.rs deleted file mode 100644 index 6fa063e..0000000 --- a/pubky/src/client_async.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::thread; - -use bytes::Bytes; - -use pkarr::{Keypair, PublicKey}; -use pubky_common::session::Session; - -use crate::{client::PubkyClient, error::Result}; - -pub struct PubkyClientAsync(PubkyClient); - -impl PubkyClient { - pub fn as_async(&self) -> PubkyClientAsync { - PubkyClientAsync(self.clone()) - } -} - -impl PubkyClientAsync { - /// Async version of [PubkyClient::signup] - pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - let homeserver = homeserver.to_string(); - - thread::spawn(move || sender.send(client.signup(&keypair, &homeserver))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::session] - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.session(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signout] - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - - thread::spawn(move || sender.send(client.signout(&pubky))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::signin] - pub async fn signin(&self, keypair: &Keypair) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let keypair = keypair.clone(); - - thread::spawn(move || sender.send(client.signin(&keypair))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::put] - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - let path = path.to_string(); - let content = content.to_vec(); - - thread::spawn(move || sender.send(client.put(&pubky, &path, &content))); - - receiver.recv_async().await? - } - - /// Async version of [PubkyClient::get] - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { - let (sender, receiver) = flume::bounded::>(1); - - let client = self.0.clone(); - let pubky = pubky.clone(); - let path = path.to_string(); - - thread::spawn(move || sender.send(client.get(&pubky, &path))); - - receiver.recv_async().await? - } -} diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 2b5320e..398cb7b 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -30,17 +30,9 @@ pub enum Error { Flume(#[from] flume::RecvError), #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Ureq(#[from] Box), + Reqwest(#[from] reqwest::Error), #[error(transparent)] #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), } - -#[cfg(not(target_arch = "wasm32"))] -impl From for Error { - fn from(error: ureq::Error) -> Self { - Error::Ureq(Box::new(error)) - } -} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 91a117d..36ff2c0 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -16,7 +16,6 @@ macro_rules! if_wasm { if_not_wasm! { mod client; - mod client_async; use client::PubkyClient; } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 2414ba4..a8d438f 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -6,6 +6,7 @@ pub mod pkarr; #[wasm_bindgen] pub struct PubkyClient { + pub(crate) http: reqwest::Client, pub(crate) pkarr: pkarr::PkarrRelayClient, } @@ -14,6 +15,7 @@ impl PubkyClient { #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { + http: reqwest::Client::new(), pkarr: pkarr::PkarrRelayClient::default(), } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 6f03834..08b802d 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -13,50 +13,24 @@ 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<(), JsError> { - // let (audience, mut url) = self.resolve_endpoint(homeserver)?; + pub async fn signup(&self, keypair: &Keypair, homeserver: &str) -> Result { + let (audience, mut url) = self.resolve_endpoint(homeserver)?; - // url.set_path(&format!("/{}", keypair.public_key())); + url.set_path(&format!("/{}", keypair.public_key())); - // let body = AuthnSignature::generate(keypair, &audience).as_bytes(); + self.http + .put(&url) + .send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?; - // fetch_base(url.to_string(), "PUT", body).await?; - - self.publish_pubky_homeserver(keypair, homeserver).await?; + self.publish_pubky_homeserver(keypair, homeserver).await; Ok(()) } } -async fn fetch_base( - url: &String, - method: &str, - body: Option>, -) -> Result { - let mut opts = web_sys::RequestInit::new(); - opts.method(method); - opts.mode(RequestMode::Cors); - - if let Some(body) = body { - let body_bytes: &[u8] = &body; - let body_array: js_sys::Uint8Array = body_bytes.into(); - let js_value: &JsValue = body_array.as_ref(); - opts.body(Some(js_value)); - } - - let js_request = web_sys::Request::new_with_str_and_init(url, &opts)?; - - let window = web_sys::window().unwrap(); - let response = JsFuture::from(window.fetch_with_request(&js_request)).await?; - - let response: web_sys::Response = response.dyn_into()?; - - Ok(response) -} - #[cfg(test)] mod tests { - use wasm_bindgen_test::*; + use wasm_bindgen_test::wasm_bindgen_test; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); From c40ba8390be1e4bec2f474a4caf5007c4daba3cb Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 27 Jul 2024 19:31:26 +0300 Subject: [PATCH 11/26] feat(pubky): enable get and put methods --- pubky/src/client.rs | 2 +- pubky/src/client/public.rs | 32 ++++++++------------------------ 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/pubky/src/client.rs b/pubky/src/client.rs index 2839110..d114a72 100644 --- a/pubky/src/client.rs +++ b/pubky/src/client.rs @@ -1,6 +1,6 @@ mod auth; mod pkarr; -// mod public; +mod public; use std::{collections::HashMap, fmt::format, time::Duration}; diff --git a/pubky/src/client/public.rs b/pubky/src/client/public.rs index e2d396f..2c1b39f 100644 --- a/pubky/src/client/public.rs +++ b/pubky/src/client/public.rs @@ -5,41 +5,31 @@ use pkarr::PublicKey; use super::{PubkyClient, Result}; impl PubkyClient { - pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { let path = normalize_path(path); - let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - self.request(super::HttpMethod::Put, &url) - .send_bytes(content)?; + self.http.put(url).body(content.to_owned()).send().await?; Ok(()) } - pub fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { let path = normalize_path(path); - let (_, mut url) = self.resolve_pubky_homeserver(pubky)?; + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - let response = self.request(super::HttpMethod::Get, &url).call()?; - - let len = response - .header("Content-Length") - .and_then(|s| s.parse::().ok()) - // TODO: return an error in case content-length header is missing - .unwrap_or(0); + let response = self.http.get(url).send().await?; // TODO: bail on too large files. + let bytes = response.bytes().await?; - let mut bytes = vec![0; len as usize]; - - response.into_reader().read_exact(&mut bytes); - - Ok(bytes.into()) + Ok(bytes) } } @@ -86,12 +76,6 @@ mod tests { .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) .await; - if let Err(Error::Ureq(ureqerror)) = response { - if let Some(r) = ureqerror.into_response() { - dbg!(r.into_string()); - } - } - let response = client .get(&keypair.public_key(), "/pub/foo.txt") .await From dac22840658d217534a86e01b40957c3d5b5e92f Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 12:22:20 +0300 Subject: [PATCH 12/26] feat(homeserver): add in memory pkarr relay api for testing --- Cargo.lock | 121 +++++++++++++++++++++++++++ pubky-homeserver/Cargo.toml | 2 + pubky-homeserver/src/main.rs | 27 +++++- pubky-homeserver/src/routes.rs | 9 +- pubky-homeserver/src/routes/pkarr.rs | 69 +++++++++++++++ pubky-homeserver/src/server.rs | 2 + 6 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 pubky-homeserver/src/routes/pkarr.rs diff --git a/Cargo.lock b/Cargo.lock index dbb725b..61051c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -271,12 +320,58 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "cobs" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -819,6 +914,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "heed" version = "0.20.3" @@ -1019,6 +1120,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -1525,10 +1632,12 @@ dependencies = [ "axum-extra", "base32", "bytes", + "clap", "dirs-next", "flume", "futures-util", "heed", + "once_cell", "pkarr", "postcard", "pubky-common", @@ -2043,6 +2152,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2446,6 +2561,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index da0c5c7..68e30f0 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -9,10 +9,12 @@ axum = "0.7.5" axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" bytes = "1.6.1" +clap = { version = "4.5.11", features = ["derive"] } 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/main.rs b/pubky-homeserver/src/main.rs index a54fba4..0d22194 100644 --- a/pubky-homeserver/src/main.rs +++ b/pubky-homeserver/src/main.rs @@ -1,13 +1,36 @@ use anyhow::Result; +use pkarr::mainline::Testnet; use pubky_homeserver::Homeserver; +use clap::Parser; + +#[derive(Parser, Debug)] +struct Cli { + /// [tracing_subscriber::EnvFilter] + #[clap(short, long)] + tracing_env_filter: Option, + #[clap(long)] + testnet: bool, +} + #[tokio::main] async fn main() -> Result<()> { + let args = Cli::parse(); + tracing_subscriber::fmt() - .with_env_filter("pubky_homeserver=debug,tower_http=debug") + .with_env_filter( + args.tracing_env_filter + .unwrap_or("pubky_homeserver=debug,tower_http=debug".to_string()), + ) .init(); - let server = Homeserver::start(Default::default()).await?; + let server = if args.testnet { + let testnet = Testnet::new(3); + + Homeserver::start_test(&testnet).await? + } else { + Homeserver::start(Default::default()).await? + }; server.run_until_done().await?; diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 3b872c1..e1a4fef 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -8,11 +8,14 @@ use tower_http::trace::TraceLayer; use crate::server::AppState; +use self::pkarr::pkarr_router; + mod auth; +mod pkarr; mod public; mod root; -pub fn create_app(state: AppState) -> Router { +fn base(state: AppState) -> Router { Router::new() .route("/", get(root::handler)) .route("/:pubky", put(auth::signup)) @@ -28,3 +31,7 @@ pub fn create_app(state: AppState) -> Router { .layer(DefaultBodyLimit::max(16 * 1024)) .with_state(state) } + +pub fn create_app(state: AppState) -> Router { + base(state).merge(pkarr_router()) +} diff --git a/pubky-homeserver/src/routes/pkarr.rs b/pubky-homeserver/src/routes/pkarr.rs new file mode 100644 index 0000000..9c9253b --- /dev/null +++ b/pubky-homeserver/src/routes/pkarr.rs @@ -0,0 +1,69 @@ +use std::{collections::HashMap, sync::RwLock}; + +use axum::{ + body::{Body, Bytes}, + http::StatusCode, + response::IntoResponse, + routing::{get, put}, + Router, +}; +use futures_util::stream::StreamExt; +use once_cell::sync::OnceCell; + +use pkarr::{PublicKey, SignedPacket}; + +use crate::{ + error::{Error, Result}, + extractors::Pubky, +}; + +// 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 { + Router::new() + .route("/pkarr/:pubky", put(pkarr_put)) + .route("/pkarr/:pubky", get(pkarr_get)) +} + +pub async fn pkarr_put(pubky: Pubky, body: Body) -> Result { + let mut bytes = Vec::with_capacity(1104); + + let mut stream = body.into_data_stream(); + + while let Some(chunk) = stream.next().await { + bytes.extend_from_slice(&chunk?) + } + + let public_key = pubky.public_key().to_owned(); + + 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); + + 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()) { + return Ok(signed_packet.to_relay_payload()); + } + + Err(Error::with_status(StatusCode::NOT_FOUND)) +} diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index 0a2f3ae..7d37baf 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -82,6 +82,8 @@ impl Homeserver { /// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage. pub async fn start_test(testnet: &Testnet) -> Result { + info!("Running testnet.."); + Homeserver::start(Config::test(testnet)).await } From d35c586a1269040ef6ad2898707d070dfd497be7 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 18:38:21 +0300 Subject: [PATCH 13/26] feat(pubky): start testing js package against local homeserver --- Cargo.lock | 52 +------------ pubky-common/Cargo.toml | 3 + pubky-common/src/timestamp.rs | 13 +++- pubky-homeserver/Cargo.toml | 1 - pubky-homeserver/src/config.rs | 17 +++-- pubky-homeserver/src/main.rs | 11 ++- pubky-homeserver/src/routes.rs | 19 ++++- pubky-homeserver/src/routes/pkarr.rs | 38 ++++----- pubky-homeserver/src/server.rs | 30 +++++--- pubky/Cargo.toml | 16 ++-- pubky/pkg/test/auth.js | 13 ++-- pubky/src/bin/bundle_pubky_npm.rs | 2 +- pubky/src/error.rs | 14 ++++ pubky/src/lib.rs | 45 +++++------ pubky/src/{client.rs => native.rs} | 26 +++---- pubky/src/{client => native}/auth.rs | 18 +++-- pubky/src/{client => native}/pkarr.rs | 30 ++++++-- pubky/src/{client => native}/public.rs | 7 +- pubky/src/shared/pkarr.rs | 3 +- pubky/src/wasm.rs | 8 +- pubky/src/wasm/auth.rs | 23 ++++-- pubky/src/wasm/keys.rs | 13 ++++ pubky/src/wasm/pkarr.rs | 102 +++++++++++++++++++++++-- 23 files changed, 309 insertions(+), 195 deletions(-) rename pubky/src/{client.rs => native.rs} (76%) rename pubky/src/{client => native}/auth.rs (88%) rename pubky/src/{client => native}/pkarr.rs (85%) rename pubky/src/{client => native}/public.rs (92%) 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(()) } From 3cfd876808cca2319523bf02a8ecb320144e7981 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 18:45:24 +0300 Subject: [PATCH 14/26] refactor(pubky): remove unused flume --- pubky-homeserver/src/server.rs | 1 - pubky/Cargo.toml | 6 +----- pubky/src/error.rs | 4 ---- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/pubky-homeserver/src/server.rs b/pubky-homeserver/src/server.rs index f943203..12d497c 100644 --- a/pubky-homeserver/src/server.rs +++ b/pubky-homeserver/src/server.rs @@ -3,7 +3,6 @@ use std::{ }; use anyhow::{Error, Result}; -use lru::LruCache; use pubky_common::auth::AuthnVerifier; use tokio::{net::TcpListener, signal, sync::Mutex, task::JoinSet}; use tracing::{debug, info, warn}; diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 34e1d5c..6f89a23 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -18,12 +18,11 @@ reqwest = { version = "0.12.5", features = ["cookies"] } bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } +flume = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } -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 } @@ -48,9 +47,6 @@ tokio = "1.37.0" wasm-bindgen-test = "0.3.42" [features] -async = ["flume/async"] - -default = ["async"] [package.metadata.docs.rs] all-features = true diff --git a/pubky/src/error.rs b/pubky/src/error.rs index 18be027..e885f32 100644 --- a/pubky/src/error.rs +++ b/pubky/src/error.rs @@ -25,10 +25,6 @@ pub enum Error { #[error(transparent)] Url(#[from] url::ParseError), - #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] - Flume(#[from] flume::RecvError), - #[error(transparent)] Reqwest(#[from] reqwest::Error), From e0b58451b54873c2b6681ecc014674f809bb0339 Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 19:40:02 +0300 Subject: [PATCH 15/26] refactor(pubky): move pkarr logic to shared --- pubky/src/lib.rs | 2 +- pubky/src/native/auth.rs | 2 +- pubky/src/native/pkarr.rs | 75 ++-------------- pubky/src/shared/pkarr.rs | 177 ++++++++++++++++++++++---------------- pubky/src/wasm/pkarr.rs | 79 ++--------------- 5 files changed, 116 insertions(+), 219 deletions(-) diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 1c35ff8..48bcb81 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -20,5 +20,5 @@ pub use error::Error; pub struct PubkyClient { http: reqwest::Client, #[cfg(not(target_arch = "wasm32"))] - pkarr: PkarrClientAsync, + pub(crate) pkarr: PkarrClientAsync, } diff --git a/pubky/src/native/auth.rs b/pubky/src/native/auth.rs index 76ef81e..23622ad 100644 --- a/pubky/src/native/auth.rs +++ b/pubky/src/native/auth.rs @@ -33,7 +33,7 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [ureq::Error] if + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if /// the response has any other `>=400` status code. pub async fn session(&self, pubky: &PublicKey) -> Result { let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; diff --git a/pubky/src/native/pkarr.rs b/pubky/src/native/pkarr.rs index 5cced4b..3f11711 100644 --- a/pubky/src/native/pkarr.rs +++ b/pubky/src/native/pkarr.rs @@ -5,75 +5,21 @@ use pkarr::{ Keypair, PublicKey, SignedPacket, }; -use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; - use crate::{ error::{Error, Result}, PubkyClient, }; impl PubkyClient { - /// Publish the SVCB record for `_pubky.`. - pub(crate) async fn publish_pubky_homeserver( + pub(crate) async fn pkarr_resolve( &self, - keypair: &Keypair, - host: &str, - ) -> Result<()> { - let existing = self.pkarr.resolve(&keypair.public_key()).await?; - - let signed_packet = prepare_packet_for_signup(keypair, host, existing)?; - - self.pkarr.publish(&signed_packet).await?; - - Ok(()) + public_key: &PublicKey, + ) -> Result> { + Ok(self.pkarr.resolve(public_key).await?) } - /// 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) + pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + Ok(self.pkarr.publish(signed_packet).await?) } } @@ -88,15 +34,6 @@ 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/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index a5d35fc..d06b25c 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -5,101 +5,130 @@ use pkarr::{ Keypair, PublicKey, SignedPacket, }; -use crate::error::{Error, Result}; +use crate::{ + error::{Error, Result}, + PubkyClient, +}; const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3; -pub fn prepare_packet_for_signup( - keypair: &Keypair, - host: &str, - existing: Option, -) -> Result { - let mut packet = Packet::new_reply(0); +impl PubkyClient { + /// Publish the SVCB record for `_pubky.`. + pub(crate) async fn publish_pubky_homeserver( + &self, + keypair: &Keypair, + host: &str, + ) -> Result<()> { + let existing = self.pkarr_resolve(&keypair.public_key()).await?; - if let Some(existing) = existing { - for answer in existing.packet().answers.iter().cloned() { - if !answer.name.to_string().starts_with("_pubky") { - packet.answers.push(answer.into_owned()) + let mut packet = Packet::new_reply(0); + + if let Some(existing) = existing { + for answer in existing.packet().answers.iter().cloned() { + if !answer.name.to_string().starts_with("_pubky") { + packet.answers.push(answer.into_owned()) + } } } + + let svcb = SVCB::new(0, host.try_into()?); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "_pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(keypair, &packet)?; + + self.pkarr_publish(&signed_packet).await?; + + Ok(()) } - let svcb = SVCB::new(0, host.try_into()?); + /// Resolve the homeserver for a pubky. + pub(crate) async fn resolve_pubky_homeserver( + &self, + pubky: &PublicKey, + ) -> Result<(PublicKey, Url)> { + let target = format!("_pubky.{}", pubky); - packet.answers.push(pkarr::dns::ResourceRecord::new( - "_pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); + self.resolve_endpoint(&target) + .await + .map_err(|_| Error::Generic("Could not resolve homeserver".to_string())) + } - Ok(SignedPacket::from_packet(keypair, &packet)?) -} + /// 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? -pub fn parse_pubky_svcb( - signed_packet: Option, - public_key: &PublicKey, - target: &mut String, - homeserver_public_key: &mut Option, - host: &mut String, - step: &mut u8, -) -> bool { - *step += 1; + let mut target = target.to_string(); + let mut homeserver_public_key = None; + let mut host = target.clone(); - let mut prior = None; + let mut step = 0; - if let Some(signed_packet) = signed_packet { - for answer in signed_packet.resource_records(target) { - if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { - if svcb.priority == 0 { - prior = Some(svcb) - } else if let Some(sofar) = prior { - if svcb.priority >= sofar.priority { - prior = Some(svcb) + // PublicKey is very good at extracting the Pkarr TLD from a string. + while let Ok(public_key) = PublicKey::try_from(target.clone()) { + step += 1; + + let response = self + .pkarr_resolve(&public_key) + .await + .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + + let mut prior = None; + + if let Some(signed_packet) = response { + for answer in signed_packet.resource_records(&target) { + if let pkarr::dns::rdata::RData::SVCB(svcb) = &answer.rdata { + if svcb.priority == 0 { + prior = Some(svcb) + } else if let Some(sofar) = prior { + if svcb.priority >= sofar.priority { + prior = Some(svcb) + } + // TODO return random if priority is the same + } else { + prior = Some(svcb) + } } - // TODO return random if priority is the same - } else { - prior = Some(svcb) + } + + if let Some(svcb) = prior { + homeserver_public_key = Some(public_key.clone()); + target = svcb.target.to_string(); + + if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { + if port.len() < 2 { + // TODO: debug! Error encoding port! + } + let port = u16::from_be_bytes([port[0], port[1]]); + + host = format!("{target}:{port}"); + } else { + host.clone_from(&target); + }; + + if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION { + continue; + }; } } } - if let Some(svcb) = prior { - *homeserver_public_key = Some(public_key.clone()); - *target = svcb.target.to_string(); - - if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) { - if port.len() < 2 { - // TODO: debug! Error encoding port! - } - let port = u16::from_be_bytes([port[0], port[1]]); - - *host = format!("{target}:{port}"); + if let Some(homeserver) = homeserver_public_key { + let url = if host.starts_with("localhost") { + format!("http://{host}") } else { - host.clone_from(target); + format!("https://{host}") }; - return *step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION; + return Ok((homeserver, Url::parse(&url)?)); } + + Err(Error::ResolveEndpoint(original_target.into())) } - - true -} - -pub fn format_url( - original_target: &str, - homeserver_public_key: Option, - host: String, -) -> Result<(PublicKey, Url)> { - if let Some(homeserver) = homeserver_public_key { - let url = if host.starts_with("localhost") { - format!("http://{host}") - } else { - format!("https://{host}") - }; - - return Ok((homeserver, Url::parse(&url)?)); - } - - Err(Error::ResolveEndpoint(original_target.into())) } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 4af3e7d..91b85c2 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -8,86 +8,17 @@ pub use pkarr::{ }; use crate::error::{Error, Result}; -use crate::shared::pkarr::{format_url, parse_pubky_svcb, prepare_packet_for_signup}; use crate::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.`. - pub(crate) async fn publish_pubky_homeserver( - &self, - keypair: &Keypair, - host: &str, - ) -> Result<()> { - // 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, host, existing)?; - - // 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> { + pub(crate) async fn pkarr_resolve( + &self, + public_key: &PublicKey, + ) -> Result> { let res = self .http .get(format!("{TEST_RELAY}/{}", public_key)) @@ -106,7 +37,7 @@ impl PubkyClient { Ok(Some(existing)) } - async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> { + pub(crate) 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()) From 3cc81a5d0e387510c8cacb8513cd479b3ebf867d Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 20:10:03 +0300 Subject: [PATCH 16/26] test(pubky): add headless testing instead of examples --- Cargo.lock | 42 ------ examples/web/auth/.gitignore | 5 - examples/web/auth/index.html | 38 ----- examples/web/auth/package.json | 23 --- examples/web/auth/package.json0 | 30 ---- .../auth/src/components/RecoveryFileUpload.js | 78 ---------- examples/web/auth/src/components/layout.jsx | 21 --- examples/web/auth/src/index.jsx | 21 --- examples/web/auth/src/pages/Home.jsx | 43 ------ examples/web/auth/src/store.js | 90 ------------ examples/web/auth/src/style.css | 137 ------------------ examples/web/auth/vite.config.js | 12 -- examples/web/no-bundler/.gitignore | 3 - examples/web/no-bundler/README.md | 3 - examples/web/no-bundler/index.html | 46 ------ examples/web/no-bundler/package.json | 18 --- pubky/Cargo.toml | 3 - pubky/pkg/README.md | 46 ++++++ pubky/pkg/package.json | 10 +- pubky/pkg/test/auth.js | 26 +--- pubky/pkg/test/keys.js | 2 +- pubky/src/wasm/auth.rs | 36 +++-- 22 files changed, 85 insertions(+), 648 deletions(-) delete mode 100644 examples/web/auth/.gitignore delete mode 100644 examples/web/auth/index.html delete mode 100644 examples/web/auth/package.json delete mode 100644 examples/web/auth/package.json0 delete mode 100644 examples/web/auth/src/components/RecoveryFileUpload.js delete mode 100644 examples/web/auth/src/components/layout.jsx delete mode 100644 examples/web/auth/src/index.jsx delete mode 100644 examples/web/auth/src/pages/Home.jsx delete mode 100644 examples/web/auth/src/store.js delete mode 100644 examples/web/auth/src/style.css delete mode 100644 examples/web/auth/vite.config.js delete mode 100644 examples/web/no-bundler/.gitignore delete mode 100644 examples/web/no-bundler/README.md delete mode 100644 examples/web/no-bundler/index.html delete mode 100644 examples/web/no-bundler/package.json diff --git a/Cargo.lock b/Cargo.lock index 8d6ec73..ed8e5db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,16 +372,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1583,7 +1573,6 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", "web-sys", ] @@ -1890,12 +1879,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -2625,31 +2608,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "wasm-bindgen-test" -version = "0.3.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "scoped-tls", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "web-sys" version = "0.3.69" diff --git a/examples/web/auth/.gitignore b/examples/web/auth/.gitignore deleted file mode 100644 index fb59162..0000000 --- a/examples/web/auth/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.netlify -.parcel* -dist -node_modules -package-lock.json diff --git a/examples/web/auth/index.html b/examples/web/auth/index.html deleted file mode 100644 index ef33b63..0000000 --- a/examples/web/auth/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - Pubky demo - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/examples/web/auth/package.json b/examples/web/auth/package.json deleted file mode 100644 index 61e1abc..0000000 --- a/examples/web/auth/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "private": "true", - "main": "src/index.js", - "type": "module", - "scripts": { - "build": "vite build", - "start": "vite serve", - "preview": "vite preview", - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky": "file:../../../pubky/pkg", - - "@solidjs/router": "^0.10.9", - "solid-js": "^1.7.0", - "vite-plugin-solid": "^2.10.2" - }, - "devDependencies": { - "typescript": "^4.9.5", - "vite": "^4.1.4", - "vite-plugin-html": "^3.2.0" - } -} diff --git a/examples/web/auth/package.json0 b/examples/web/auth/package.json0 deleted file mode 100644 index 9bd267d..0000000 --- a/examples/web/auth/package.json0 +++ /dev/null @@ -1,30 +0,0 @@ -{ - "private": "true", - "type": "module", - "source": "src/index.html", - "browserslist": "> 0.5%, last 2 versions, not dead", - "scripts": { - "start": "parcel -p 7251", - "serve": "npm run start", - "build": "parcel build", - "lint": "standard --fix" - }, - "standard": { - "ignore": [ - "src/pages", - "src/components", - "dist" - ] - }, - "dependencies": { - "@solidjs/router": "^0.10.9", - "parcel": "^2.0.1", - "@synonymdev/pubky": "file:../../../pubky/pkg", - "solid-js": "^1.7.0" - }, - "devDependencies": { - "@babel/core": "^7.24.9", - "babel-preset-solid": "^1.2.5", - "solid-refresh": "^0.2.2" - } -} diff --git a/examples/web/auth/src/components/RecoveryFileUpload.js b/examples/web/auth/src/components/RecoveryFileUpload.js deleted file mode 100644 index 06b3ad0..0000000 --- a/examples/web/auth/src/components/RecoveryFileUpload.js +++ /dev/null @@ -1,78 +0,0 @@ -import { useNavigate } from '@solidjs/router' -import { createSignal } from 'solid-js' -import { crypto } from '@pubky/common' - -import { decryptRecoveryFile } from '../sdk/recovery.js' -import store from '../store.js' - -/** - * @param {"login" | "signup"} type - */ -const RecoveryFileUpload = ({ type }) => { - const [valid, setValid] = createSignal(false) - - const navigate = useNavigate() - - const onSubmit = async (e) => { - e.preventDefault() - - const form = e.target - - const file = form.file.files[0] - const passphrase = form['import-passphrase'].value - - const reader = new FileReader() - - reader.onload = async function(event) { - const recoveryFile = event.target.result - - const seedResult = await decryptRecoveryFile(recoveryFile, passphrase) - if (seedResult.isErr()) return alert(seedResult.error.message) - - const action = type === 'signup' - ? store.pubkyClient.signup.bind(store.pubkyClient) - : store.pubkyClient.login.bind(store.pubkyClient); - - const result = await action(seedResult.value) - crypto.zeroize(seedResult.value) - - if (result.isErr()) return alert(result.error.message) - - store.setCurrentUser({ id: result.value }) - - navigate("/", { replace: true }) - } - - // Read the file as text - reader.readAsText(file) - } - - const onUpdate = (e) => { - const form = e.target.parentElement.parentElement - - const file = form.file.files[0] - const passphrase = form['import-passphrase'].value - - if (passphrase.length > 0 && file) { - setValid(true) - } else { - setValid(false) - } - } - - return ( -
- - - -
- ) -} - -export default RecoveryFileUpload diff --git a/examples/web/auth/src/components/layout.jsx b/examples/web/auth/src/components/layout.jsx deleted file mode 100644 index 1d0c03b..0000000 --- a/examples/web/auth/src/components/layout.jsx +++ /dev/null @@ -1,21 +0,0 @@ -const Layout = ({ children }) => { - return ( - <> -
-
-

Pubky

-
-
- -
- {children} -
- -
-

This is a proof of concept for demonstration purposes only.

-
- - ) -} - -export default Layout diff --git a/examples/web/auth/src/index.jsx b/examples/web/auth/src/index.jsx deleted file mode 100644 index 1c78d71..0000000 --- a/examples/web/auth/src/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import { render } from 'solid-js/web' -import { Router, Route } from '@solidjs/router' - -import { PubkyClient } from "@synonymdev/pubky"; - -let client = new PubkyClient() -console.log(client); - -import Home from './pages/Home.jsx' -// import Login from './pages/Login.js' -// import Signup from './pages/Signup.js' - -render(() => ( - - - -), document.getElementById('app')) - -// -// -// diff --git a/examples/web/auth/src/pages/Home.jsx b/examples/web/auth/src/pages/Home.jsx deleted file mode 100644 index 307d1a1..0000000 --- a/examples/web/auth/src/pages/Home.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useNavigate } from '@solidjs/router' - -import store from '../store.js' -import Layout from '../components/Layout' - -const Home = () => { - const navigate = useNavigate() - - let currentUser = store.getCurrentUser() - - if (!currentUser) { - navigate('/login', { replace: true }) - } - - const logout = async () => { - // await store.pubkyClient.ready() - // - // const logoutResult = await store.pubkyClient.logout(currentUser.id) - // if (logoutResult.isErr()) { - // alert(logoutResult.error.message) - // return - // } - // - // store.removeCurrentUser() - // - // if (window.location.pathname === '/home') { - // navigate('/', { replace: true }) - // } else { - // navigate('/home', { replace: true }) - // } - } - - return ( - - Home.. -

Welcome {store.getCurrentUser()?.id}

-
- -
- ) -} - -export default Home diff --git a/examples/web/auth/src/store.js b/examples/web/auth/src/store.js deleted file mode 100644 index 910d6a5..0000000 --- a/examples/web/auth/src/store.js +++ /dev/null @@ -1,90 +0,0 @@ -import { createMutable } from 'solid-js/store' -// import z32 from 'z32' -// import { Result } from '@pubky/common' -// import { Level } from 'level' -// import Client from '@pubky/client' -// -// import { recoveryFile } from './sdk/recovery.js' -// -// // In real application it should be the server's Pkarr Id -// const DEFAULT_HOME_SERVER = 'http://localhost:7259' -// const DEFAULT_RELAY = 'https://relay.pkarr.org' - -class Store { - constructor() { - // this.db = new Level('app-db', { keyEncoding: 'utf8', valueEncoding: 'json' }) - // this.currentUser = null - // - // this.pubkyClient = new Client(DEFAULT_HOME_SERVER, { relay: DEFAULT_RELAY }) - // - // this.DEFAULT_HOME_SERVER = DEFAULT_HOME_SERVER - } - - // getUsers() { - // try { - // return JSON.parse(global.localStorage.getItem('users')) || [] - // } catch { - // return [] - // } - // } - - getCurrentUser() { - try { - return JSON.parse(global.localStorage.getItem('currentUser')) - } catch { - return null - } - } - - // setCurrentUser(user) { - // const users = this.getUsers() - // - // if (!users?.map(user => user.id).includes(user.id)) { - // global.localStorage.setItem('users', JSON.stringify([ - // ...users, - // user - // ])) - // } - // - // global.localStorage.setItem('currentUser', JSON.stringify(user)) - // } - // - // removeCurrentUser() { - // global.localStorage.removeItem('currentUser') - // } - // - // /** - // * @param {string} name - // * @param {string} passphrase - // * - // * @returns {Promise>} - // */ - // async createAccount(name, passphrase) { - // await this.pubkyClient.ready() - // - // const seed = Client.crypto.generateSeed() - // - // const keypair = Client.crypto.generateKeyPair(seed) - // Client.crypto.zeroize(keypair.secretKey) - // - // const userId = z32.encode(keypair.publicKey) - // - // const recoveryFileAndFilename = await recoveryFile(name, seed, passphrase) - // - // const signedUp = await this.pubkyClient.signup(seed) - // if (signedUp.isErr()) return signedUp - // - // Client.crypto.zeroize(seed) - // - // return Result.Ok({ - // userId, - // ...recoveryFileAndFilename - // }) - // } -} - -export default createMutable(new Store()) diff --git a/examples/web/auth/src/style.css b/examples/web/auth/src/style.css deleted file mode 100644 index 26c3d1d..0000000 --- a/examples/web/auth/src/style.css +++ /dev/null @@ -1,137 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, -body, -#app { - width: 100%; - height: 100%; - min-width: 320px; -} - -body, -#app { - display: flex; - flex-direction: column; - justify-content: space-between; - max-width: 600px; - margin: 0 auto; - padding: 2rem 1rem 0; - font-family: 'IBM Plex Sans', Helvetica, sans-serif; -} - -.button { - background: none; - border: none; - cursor: pointer; -} - -.button:disabled { - pointer-events: none; - opacity: 0.4; -} - -main { - height: 100%; - margin: 1rem 0; -} - -footer { - text-align: center; - color: #666; - padding: 1rem 0; - border-top: 1px solid #ddd; - margin-top: 1rem; -} - -footer p, -footer a { - font-size: 0.8rem !important; - padding-bottom: 0.3rem; -} - -.row { - display: flex; - justify-content: space-between; - align-items: baseline; -} - -a { - color: #000; - font-size: 1rem; - font-weight: 700; -} - -h1 { - font-size: 2rem; - font-weight: 700; -} - -.small { - font-size: 0.8rem; -} - -.button.primary { - background: black; - color: white; - - min-width: 90px; - width: 100%; - - padding: 0.4rem 0.8rem; - border: 2px solid #000; -} - -.button.primary:hover { - background: #222; -} - -.divider { - background: #333; - height: 1px; - margin-top: 1rem; - margin-bottom: 1rem; -} - -form { - display: flex; - flex-direction: column; -} - -label { - font-size: 0.9rem; - font-weight: 700; - margin-bottom: 1rem; -} - -form input { - font-size: 1rem; - width: 100%; - height: 2rem; - border: none; - box-shadow: none; - border-radius: 0; - border-bottom: 1px solid #ddd; -} - -form input:focus { - outline: none; - border-bottom: 2px solid #000; -} - -label.checkbox { - display: flex; - font-weight: normal; - line-height: 1rem; -} - -label.checkbox input { - width: 1rem; - margin: 0 0.5rem 0 0; -} -label.checkbox:focus-within { - outline: 1px solid; -} diff --git a/examples/web/auth/vite.config.js b/examples/web/auth/vite.config.js deleted file mode 100644 index 6dba9ca..0000000 --- a/examples/web/auth/vite.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "vite"; -// import wasmPack from "vite-plugin-wasm-pack"; -import solidPlugin from 'vite-plugin-solid'; -// import path from "node:path"; - -export default defineConfig({ - // pass your local crate path to the plugin - plugins: [ - // wasmPack(path.resolve("../../../pubky")), - solidPlugin() - ], -}); diff --git a/examples/web/no-bundler/.gitignore b/examples/web/no-bundler/.gitignore deleted file mode 100644 index 91a3983..0000000 --- a/examples/web/no-bundler/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules -package-lock.json diff --git a/examples/web/no-bundler/README.md b/examples/web/no-bundler/README.md deleted file mode 100644 index ddd6280..0000000 --- a/examples/web/no-bundler/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# No bundler - -An example of using Pubky wasm immidiatly in an `index.html` with no bundlers. diff --git a/examples/web/no-bundler/index.html b/examples/web/no-bundler/index.html deleted file mode 100644 index 0770e09..0000000 --- a/examples/web/no-bundler/index.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - Pubky demo - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/examples/web/no-bundler/package.json b/examples/web/no-bundler/package.json deleted file mode 100644 index 68d5fbc..0000000 --- a/examples/web/no-bundler/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "scripts": { - "start": "vite serve", - "preinstall": "cargo run --bin bundle_pubky_npm" - }, - "dependencies": { - "@synonymdev/pubky": "file:../../../pubky/pkg", - - "@solidjs/router": "^0.10.9", - "solid-js": "^1.7.0" - }, - "devDependencies": { - "typescript": "^4.9.5", - "vite": "^4.1.4", - "vite-plugin-html": "^3.2.0", - "vite-plugin-solid": "^2.10.2" - } -} diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 6f89a23..09d2a44 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -43,9 +43,6 @@ web-sys = { version = "0.3.69", features = [ pubky_homeserver = { path = "../pubky-homeserver" } tokio = "1.37.0" -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.42" - [features] [package.metadata.docs.rs] diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 32080e5..d965c2f 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -1,3 +1,49 @@ # Pubky JavaScript implementation of [Pubky](https://github.com/pubky/pubky). + +## Install + +```bash +npm install @synonymdev/pubky +``` + +## Getting started + +```js +import PubkyClient from "@synonymdev/pubky"; + +// Initialize PubkyClient with Pkarr relay(s). +let client = new PubkyClient(); + +// Generate a keypair +let keypair = Keypair.random(); + +// Create a new account +let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + +await client.signup(keypair, homeserver) +``` + +## Test and Development + +For test and development, you can run a local homeserver in a test network. + +If you don't have Cargo Installed, start by installing it: + +```bash +curl https://sh.rustup.rs -sSf | sh +``` + +Clone the Pubky repository: + +```bash +git clone https://github.com/pubky/pubky +cd pubky/ +``` + +Run the testnet server + +```bash +cargo run --bin pubky_homeserver -- --testnet +``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index b3e43e9..6532b95 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -10,7 +10,8 @@ }, "scripts": { "lint": "standard --fix", - "test": "brittle test/*.js -cov", + "test": "tape test/*.js -cov", + "test-browser": "browserify test/*.js -p esmify | npx tape-run", "preinstall": "cargo run --bin bundle_pubky_npm", "prepublishOnly": "npm run lint && npm run test" }, @@ -33,7 +34,10 @@ "identity" ], "devDependencies": { - "brittle": "^3.6.1", - "standard": "^17.1.0" + "browser-resolve": "^2.0.0", + "esmify": "^2.1.1", + "standard": "^17.1.0", + "tape": "^5.8.1", + "tape-run": "^11.0.0" } } diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 23cfe8f..fd17015 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -1,4 +1,4 @@ -import test from 'brittle' +import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' @@ -7,30 +7,14 @@ test('seed auth', async (t) => { let client = new PubkyClient(); let keypair = Keypair.random(); - let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); + let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver); - // const client = new Client( - // homeserver.homeserver.pkarr.serverPkarr.publicKey(), - // { - // relay: homeserver.testnet.relay - // } - // ) - // await client.ready() - // - // const seed = Client.crypto.generateSeed() - // const keypair = Client.crypto.generateKeyPair(seed) - // const expectedUserId = keypair.public_key().to_string() - // - // const userIdResult = await client.signup(seed) - // t.ok(userIdResult.isOk(), userIdResult.error) - // - // const userId = userIdResult.value - // t.is(userId, expectedUserId) - // + t.ok(true); + // const session = await client.session() - // t.ok(session?.users[userId]) + // t.ok(session) // // { // await client.logout(userId) diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index d01467a..76e3202 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -1,4 +1,4 @@ -import test from 'brittle' +import test from 'tape' import { Keypair } from '../index.js' diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs index 6c9d4ca..f5d60c6 100644 --- a/pubky/src/wasm/auth.rs +++ b/pubky/src/wasm/auth.rs @@ -1,10 +1,15 @@ -use pubky_common::auth::AuthnSignature; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::RequestMode; +use reqwest::StatusCode; + use pkarr::PkarrRelayClient; +use pubky_common::{auth::AuthnSignature, session::Session}; + +use crate::Error; + use super::{ keys::{Keypair, PublicKey}, PubkyClient, @@ -35,18 +40,29 @@ impl PubkyClient { Ok(()) } -} -#[cfg(test)] -mod tests { - use wasm_bindgen_test::wasm_bindgen_test; + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if + /// the response has any other `>=400` status code. + #[wasm_bindgen] + pub async fn session(&self, pubky: &PublicKey) -> Result { + let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + url.set_path(&format!("/{}/session", pubky)); - use super::*; + let res = self.http.get(url).send().await?; - #[wasm_bindgen_test] - async fn basic() { - // let client = PubkyClient::new(); + if res.status() == StatusCode::NOT_FOUND { + return Err(Error::NotSignedIn); + } + + if !res.status().is_success() { + res.error_for_status_ref()?; + }; + + let bytes = res.bytes().await?; + + Ok(Session::deserialize(&bytes)?) } } From d050866cceefb3fa3b63c7b6231009695c31804b Mon Sep 17 00:00:00 2001 From: nazeh Date: Sun, 28 Jul 2024 23:24:32 +0300 Subject: [PATCH 17/26] feat(pubky): auth working everywhere except nodejs --- Cargo.lock | 21 +++++- pubky-homeserver/Cargo.toml | 2 +- pubky-homeserver/src/routes.rs | 6 +- pubky-homeserver/src/routes/auth.rs | 29 ++++++-- pubky/pkg/test/auth.js | 40 ++++++----- pubky/src/error.rs | 6 +- pubky/src/native.rs | 61 ++++++++++++++--- pubky/src/native/pkarr.rs | 92 -------------------------- pubky/src/native/public.rs | 7 +- pubky/src/{native => shared}/auth.rs | 59 ++++++++++------- pubky/src/{shared.rs => shared/mod.rs} | 1 + pubky/src/shared/pkarr.rs | 70 +++++++++++++++++++- pubky/src/wasm.rs | 58 ++++++++++++++-- pubky/src/wasm/auth.rs | 68 ------------------- pubky/src/wasm/keys.rs | 2 +- pubky/src/wasm/pkarr.rs | 11 ++- pubky/src/wasm/session.rs | 6 ++ 17 files changed, 291 insertions(+), 248 deletions(-) delete mode 100644 pubky/src/native/pkarr.rs rename pubky/src/{native => shared}/auth.rs (63%) rename pubky/src/{shared.rs => shared/mod.rs} (51%) delete mode 100644 pubky/src/wasm/auth.rs create mode 100644 pubky/src/wasm/session.rs diff --git a/Cargo.lock b/Cargo.lock index ed8e5db..dbeea1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -205,6 +206,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -348,7 +361,7 @@ version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -884,6 +897,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" diff --git a/pubky-homeserver/Cargo.toml b/pubky-homeserver/Cargo.toml index 698a3e6..eaba493 100644 --- a/pubky-homeserver/Cargo.toml +++ b/pubky-homeserver/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.82" -axum = "0.7.5" +axum = { version = "0.7.5", features = ["macros"] } axum-extra = { version = "0.9.3", features = ["typed-header", "async-read-body"] } base32 = "0.5.1" bytes = "1.6.1" diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index 3f53d9b..0d3b7e3 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -41,10 +41,6 @@ pub fn create_app(state: AppState) -> 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(CorsLayer::very_permissive()) .layer(TraceLayer::new_for_http()) } diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index c38aa38..60761fb 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -1,6 +1,7 @@ use axum::{ + debug_handler, extract::{Request, State}, - http::{HeaderMap, StatusCode}, + http::{uri::Scheme, HeaderMap, StatusCode, Uri}, response::IntoResponse, Router, }; @@ -8,7 +9,7 @@ use axum_extra::{headers::UserAgent, TypedHeader}; use bytes::Bytes; use heed::BytesEncode; use postcard::to_allocvec; -use tower_cookies::{Cookie, Cookies}; +use tower_cookies::{cookie::SameSite, Cookie, Cookies}; use pubky_common::{ crypto::{random_bytes, random_hash}, @@ -26,16 +27,26 @@ use crate::{ server::AppState, }; +#[debug_handler] pub async fn signup( State(state): State, TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { // TODO: Verify invitation link. // TODO: add errors in case of already axisting user. - signin(State(state), TypedHeader(user_agent), cookies, pubky, body).await + signin( + State(state), + TypedHeader(user_agent), + cookies, + pubky, + uri, + body, + ) + .await } pub async fn session( @@ -57,6 +68,7 @@ pub async fn session( let session = session.to_owned(); rtxn.commit()?; + // TODO: add content-type return Ok(session); }; @@ -95,6 +107,7 @@ pub async fn signin( TypedHeader(user_agent): TypedHeader, cookies: Cookies, pubky: Pubky, + uri: Uri, body: Bytes, ) -> Result { let public_key = pubky.public_key(); @@ -135,7 +148,15 @@ pub async fn signin( sessions.put(&mut wtxn, &session_secret, &session.serialize())?; - cookies.add(Cookie::new(public_key.to_string(), session_secret)); + let mut cookie = Cookie::new(public_key.to_string(), session_secret); + cookie.set_path("/"); + if *uri.scheme().unwrap_or(&Scheme::HTTP) == Scheme::HTTPS { + cookie.set_secure(true); + cookie.set_same_site(SameSite::None); + } + cookie.set_http_only(true); + + cookies.add(cookie); wtxn.commit()?; diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index fd17015..a660d99 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -3,30 +3,28 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('seed auth', async (t) => { + const client = new PubkyClient() - let client = new PubkyClient(); + const keypair = Keypair.random() + const publicKey = keypair.public_key() - let keypair = Keypair.random(); + const homeserver = PublicKey.try_from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + await client.signup(keypair, homeserver) - let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); - await client.signup(keypair, homeserver); + const session = await client.session(publicKey) + t.ok(session) - t.ok(true); + { + await client.signout(publicKey) - // const session = await client.session() - // t.ok(session) - // - // { - // await client.logout(userId) - // - // const session = await client.session() - // t.absent(session?.users?.[userId]) - // } - // - // { - // await client.login(seed) - // - // const session = await client.session() - // t.ok(session?.users[userId]) - // } + const session = await client.session(publicKey) + t.notOk(session) + } + + { + await client.signin(keypair) + + const session = await client.session(publicKey) + t.ok(session) + } }) diff --git a/pubky/src/error.rs b/pubky/src/error.rs index e885f32..501168d 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("Not signed in")] - NotSignedIn, - // === Transparent === #[error(transparent)] Dns(#[from] SimpleDnsError), @@ -29,7 +26,6 @@ pub enum Error { Reqwest(#[from] reqwest::Error), #[error(transparent)] - #[cfg(not(target_arch = "wasm32"))] Session(#[from] pubky_common::session::Error), #[error("Could not resolve endpoint for {0}")] @@ -37,7 +33,7 @@ pub enum Error { } #[cfg(target_arch = "wasm32")] -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use wasm_bindgen::JsValue; #[cfg(target_arch = "wasm32")] impl From for JsValue { diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 90a1930..fa2ae3f 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,22 +1,31 @@ -pub mod auth; -pub mod pkarr; pub mod public; use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, - PkarrClient, PkarrClientAsync, Settings, + PkarrClient, PublicKey, Settings, SignedPacket, }; +use pkarr::Keypair; +use pubky_common::session::Session; +use reqwest::{Method, RequestBuilder}; +use url::Url; -use crate::PubkyClient; +use crate::{error::Result, PubkyClient}; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::builder() + .cookie_store(true) .user_agent(DEFAULT_USER_AGENT) .build() .unwrap(), @@ -25,7 +34,6 @@ impl PubkyClient { } } - #[cfg(not(target_arch = "wasm32"))] pub fn test(testnet: &Testnet) -> Self { Self { http: reqwest::Client::builder() @@ -45,10 +53,45 @@ impl PubkyClient { .as_async(), } } -} -impl Default for PubkyClient { - fn default() -> Self { - Self::new() + /// 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 an [Error::NotSignedIn] if so, 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 + } + + 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?) + } + + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + self.http.request(method, url) } } diff --git a/pubky/src/native/pkarr.rs b/pubky/src/native/pkarr.rs deleted file mode 100644 index 3f11711..0000000 --- a/pubky/src/native/pkarr.rs +++ /dev/null @@ -1,92 +0,0 @@ -use url::Url; - -use pkarr::{ - dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, -}; - -use crate::{ - error::{Error, Result}, - PubkyClient, -}; - -impl PubkyClient { - 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?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use pkarr::{ - dns::{rdata::SVCB, Packet}, - mainline::{dht::DhtSettings, Testnet}, - Keypair, PkarrClient, Settings, SignedPacket, - }; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn resolve_homeserver() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - // Publish an intermediate controller of the homeserver - let pkarr_client = PkarrClient::new(Settings { - dht: DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - ..Default::default() - }, - ..Default::default() - }) - .unwrap() - .as_async(); - - let intermediate = Keypair::random(); - - let mut packet = Packet::new_reply(0); - - let server_tld = server.public_key().to_string(); - - let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); - - packet.answers.push(pkarr::dns::ResourceRecord::new( - "pubky".try_into().unwrap(), - pkarr::dns::CLASS::IN, - 60 * 60, - pkarr::dns::rdata::RData::SVCB(svcb), - )); - - let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); - - pkarr_client.publish(&signed_packet).await.unwrap(); - - { - let client = PubkyClient::test(&testnet); - - let pubky = Keypair::random(); - - client - .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) - .await - .unwrap(); - - let (public_key, url) = client - .resolve_pubky_homeserver(&pubky.public_key()) - .await - .unwrap(); - - assert_eq!(public_key, server.public_key()); - assert_eq!(url.host_str(), Some("localhost")); - assert_eq!(url.port(), Some(server.port())); - } - } -} diff --git a/pubky/src/native/public.rs b/pubky/src/native/public.rs index ce2613e..9b2a271 100644 --- a/pubky/src/native/public.rs +++ b/pubky/src/native/public.rs @@ -50,12 +50,10 @@ fn normalize_path(path: &str) -> String { #[cfg(test)] mod tests { - use std::ops::Deref; use crate::*; use pkarr::{mainline::Testnet, Keypair}; - use pubky_common::session::Session; use pubky_homeserver::Homeserver; #[tokio::test] @@ -69,9 +67,10 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - let response = client + client .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await; + .await + .unwrap(); let response = client .get(&keypair.public_key(), "/pub/foo.txt") diff --git a/pubky/src/native/auth.rs b/pubky/src/shared/auth.rs similarity index 63% rename from pubky/src/native/auth.rs rename to pubky/src/shared/auth.rs index 23622ad..e78f84a 100644 --- a/pubky/src/native/auth.rs +++ b/pubky/src/shared/auth.rs @@ -1,4 +1,4 @@ -use reqwest::StatusCode; +use reqwest::{Method, StatusCode}; use pkarr::{Keypair, PublicKey}; use pubky_common::{auth::AuthnSignature, session::Session}; @@ -13,7 +13,11 @@ impl PubkyClient { /// /// 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<()> { + pub(crate) async fn inner_signup( + &self, + keypair: &Keypair, + homeserver: &PublicKey, + ) -> Result<()> { let homeserver = homeserver.to_string(); let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; @@ -24,7 +28,7 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.http.put(url).body(body).send().await?; + self.request(Method::PUT, url).body(body).send().await?; self.publish_pubky_homeserver(keypair, &homeserver).await?; @@ -33,17 +37,17 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; + /// Returns None if not signed in, or [reqwest::Error] + /// if the response has any other `>=404` status code. + pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result> { + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - let res = self.http.get(url).send().await?; + let res = self.request(Method::GET, url).send().await?; if res.status() == StatusCode::NOT_FOUND { - return Err(Error::NotSignedIn); + return Ok(None); } if !res.status().is_success() { @@ -52,22 +56,22 @@ impl PubkyClient { let bytes = res.bytes().await?; - Ok(Session::deserialize(&bytes)?) + Ok(Some(Session::deserialize(&bytes)?)) } /// Signout from a homeserver. - pub async fn signout(&self, pubky: &PublicKey) -> Result<()> { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; + pub async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> { + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{}/session", pubky)); - self.http.delete(url).send().await?; + self.request(Method::DELETE, url).send().await?; Ok(()) } /// Signin to a homeserver. - pub async fn signin(&self, keypair: &Keypair) -> Result<()> { + pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> { let pubky = keypair.public_key(); let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?; @@ -78,7 +82,7 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.http.post(url).body(body).send().await?; + self.request(Method::POST, url).body(body).send().await?; Ok(()) } @@ -86,11 +90,15 @@ impl PubkyClient { #[cfg(test)] mod tests { + + use std::time::Duration; + use crate::*; use pkarr::{mainline::Testnet, Keypair}; use pubky_common::session::Session; use pubky_homeserver::Homeserver; + use tokio::time::sleep; #[tokio::test] async fn basic_authn() { @@ -103,27 +111,30 @@ mod tests { client.signup(&keypair, &server.public_key()).await.unwrap(); - let session = client.session(&keypair.public_key()).await.unwrap(); + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); assert_eq!(session, Session { ..session.clone() }); client.signout(&keypair.public_key()).await.unwrap(); { - let session = client.session(&keypair.public_key()).await; + let session = client.session(&keypair.public_key()).await.unwrap(); - assert!(session.is_err()); - - match session { - Err(Error::NotSignedIn) => {} - _ => panic!("expected NotSignedInt error"), - } + assert!(session.is_none()); } client.signin(&keypair).await.unwrap(); { - let session = client.session(&keypair.public_key()).await.unwrap(); + let session = client + .session(&keypair.public_key()) + .await + .unwrap() + .unwrap(); assert_eq!(session, Session { ..session.clone() }); } diff --git a/pubky/src/shared.rs b/pubky/src/shared/mod.rs similarity index 51% rename from pubky/src/shared.rs rename to pubky/src/shared/mod.rs index ad39b70..c61bbfe 100644 --- a/pubky/src/shared.rs +++ b/pubky/src/shared/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod pkarr; diff --git a/pubky/src/shared/pkarr.rs b/pubky/src/shared/pkarr.rs index d06b25c..879e901 100644 --- a/pubky/src/shared/pkarr.rs +++ b/pubky/src/shared/pkarr.rs @@ -77,7 +77,7 @@ impl PubkyClient { let response = self .pkarr_resolve(&public_key) .await - .map_err(|e| Error::ResolveEndpoint(original_target.into()))?; + .map_err(|_| Error::ResolveEndpoint(original_target.into()))?; let mut prior = None; @@ -132,3 +132,71 @@ impl PubkyClient { Err(Error::ResolveEndpoint(original_target.into())) } } + +#[cfg(test)] +mod tests { + use super::*; + + use pkarr::{ + dns::{rdata::SVCB, Packet}, + mainline::{dht::DhtSettings, Testnet}, + Keypair, PkarrClient, Settings, SignedPacket, + }; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn resolve_homeserver() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + // Publish an intermediate controller of the homeserver + let pkarr_client = PkarrClient::new(Settings { + dht: DhtSettings { + bootstrap: Some(testnet.bootstrap.clone()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap() + .as_async(); + + let intermediate = Keypair::random(); + + let mut packet = Packet::new_reply(0); + + let server_tld = server.public_key().to_string(); + + let mut svcb = SVCB::new(0, server_tld.as_str().try_into().unwrap()); + + packet.answers.push(pkarr::dns::ResourceRecord::new( + "pubky".try_into().unwrap(), + pkarr::dns::CLASS::IN, + 60 * 60, + pkarr::dns::rdata::RData::SVCB(svcb), + )); + + let signed_packet = SignedPacket::from_packet(&intermediate, &packet).unwrap(); + + pkarr_client.publish(&signed_packet).await.unwrap(); + + { + let client = PubkyClient::test(&testnet); + + let pubky = Keypair::random(); + + client + .publish_pubky_homeserver(&pubky, &format!("pubky.{}", &intermediate.public_key())) + .await + .unwrap(); + + let (public_key, url) = client + .resolve_pubky_homeserver(&pubky.public_key()) + .await + .unwrap(); + + assert_eq!(public_key, server.public_key()); + assert_eq!(url.host_str(), Some("localhost")); + assert_eq!(url.port(), Some(server.port())); + } + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 554a4e8..01fcf73 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,18 +1,66 @@ use wasm_bindgen::prelude::*; -pub mod auth; -pub mod keys; -pub mod pkarr; +use reqwest::{Method, RequestBuilder}; +use url::Url; use crate::PubkyClient; +mod keys; +mod pkarr; +mod session; + +use keys::{Keypair, PublicKey}; +use session::Session; + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { - http: reqwest::Client::new(), - // pkarr: pkarr::PkarrRelayClient::default(), + http: reqwest::Client::builder().build().unwrap(), } } + + /// 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<(), JsValue> { + self.inner_signup(keypair.as_inner(), homeserver.as_inner()) + .await + .map_err(|e| e.into()) + } + + /// Check the current sesison for a given Pubky in its homeserver. + /// + /// Returns an [Error::NotSignedIn] if so, or [reqwest::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(|s| Session(s).into())) + .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. + #[wasm_bindgen] + pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> { + self.inner_signin(keypair.as_inner()) + .await + .map_err(|e| e.into()) + } + + pub(crate) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder { + self.http.request(method, url).fetch_credentials_include() + } } diff --git a/pubky/src/wasm/auth.rs b/pubky/src/wasm/auth.rs deleted file mode 100644 index f5d60c6..0000000 --- a/pubky/src/wasm/auth.rs +++ /dev/null @@ -1,68 +0,0 @@ -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::RequestMode; - -use reqwest::StatusCode; - -use pkarr::PkarrRelayClient; - -use pubky_common::{auth::AuthnSignature, session::Session}; - -use crate::Error; - -use super::{ - keys::{Keypair, PublicKey}, - PubkyClient, -}; - -#[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<(), 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())); - - let body = AuthnSignature::generate(keypair, &audience) - .as_bytes() - .to_owned(); - - self.http.put(url).body(body).send().await?; - - self.publish_pubky_homeserver(keypair, &homeserver).await?; - - Ok(()) - } - - /// Check the current sesison for a given Pubky in its homeserver. - /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. - #[wasm_bindgen] - pub async fn session(&self, pubky: &PublicKey) -> Result { - let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{}/session", pubky)); - - let res = self.http.get(url).send().await?; - - if res.status() == StatusCode::NOT_FOUND { - return Err(Error::NotSignedIn); - } - - if !res.status().is_success() { - res.error_for_status_ref()?; - }; - - let bytes = res.bytes().await?; - - Ok(Session::deserialize(&bytes)?) - } -} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index f54f7cb..cbd6ec7 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -9,7 +9,7 @@ pub struct Keypair(pkarr::Keypair); impl Keypair { #[wasm_bindgen] /// Generate a random [Keypair] - pub fn random(secret_key: js_sys::Uint8Array) -> Self { + pub fn random() -> Self { Self(pkarr::Keypair::random()) } diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 91b85c2..8b43299 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -1,17 +1,14 @@ use reqwest::StatusCode; -use url::Url; -use wasm_bindgen::prelude::*; -pub use pkarr::{ - dns::{rdata::SVCB, Packet}, - Keypair, PublicKey, SignedPacket, -}; +pub use pkarr::{PublicKey, SignedPacket}; -use crate::error::{Error, Result}; +use crate::error::Result; use crate::PubkyClient; const TEST_RELAY: &str = "http://localhost:15411/pkarr"; +// TODO: Add an in memory cache of packets + impl PubkyClient { //TODO: Allow multiple relays in parallel //TODO: migrate to pkarr::PkarrRelayClient diff --git a/pubky/src/wasm/session.rs b/pubky/src/wasm/session.rs new file mode 100644 index 0000000..ec2e8ca --- /dev/null +++ b/pubky/src/wasm/session.rs @@ -0,0 +1,6 @@ +use pubky_common::session; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Session(pub(crate) session::Session); From 9bef331da018d3e3fd8fecd0a5b8051f42684da3 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:22:59 +0300 Subject: [PATCH 18/26] chore(pubky): slight size optimization for wasm bundle size --- Cargo.toml | 4 ++++ pubky/Cargo.toml | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 85e44a1..9e2e527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,7 @@ members = [ "pubky","pubky-*"] # See: https://github.com/rust-lang/rust/issues/90148#issuecomment-949194352 resolver = "2" + +[profile.release] +lto = true +opt-level = 'z' diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 09d2a44..d71c0bb 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -47,3 +47,6 @@ tokio = "1.37.0" [package.metadata.docs.rs] all-features = true + +# [package.metadata.wasm-pack.profile.release] +# wasm-opt = ['-g', '-O'] From d64f6101028fa401a65876bd5de854a6e9bf3de5 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:27:53 +0300 Subject: [PATCH 19/26] feat(pubky): cookie jar for nodejs --- pubky/pkg/package.json | 9 ++++----- pubky/src/lib.rs | 9 +++++++++ pubky/src/native.rs | 5 ++++- pubky/src/shared/auth.rs | 14 +++++++++++--- pubky/src/wasm.rs | 42 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 67 insertions(+), 12 deletions(-) diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index 6532b95..e633cc3 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,18 +2,18 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.1.0", + "version": "0.0.1", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/pubky/pubky" }, "scripts": { - "lint": "standard --fix", "test": "tape test/*.js -cov", "test-browser": "browserify test/*.js -p esmify | npx tape-run", - "preinstall": "cargo run --bin bundle_pubky_npm", - "prepublishOnly": "npm run lint && npm run test" + "build": "cargo run --bin bundle_pubky_npm", + "preinstall": "npm run build", + "prepublishOnly": "npm run build && npm run test && npm run test-browser" }, "files": [ "nodejs/*", @@ -36,7 +36,6 @@ "devDependencies": { "browser-resolve": "^2.0.0", "esmify": "^2.1.1", - "standard": "^17.1.0", "tape": "^5.8.1", "tape-run": "^11.0.0" } diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 48bcb81..72f2df5 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -8,6 +8,12 @@ mod native; #[cfg(target_arch = "wasm32")] mod wasm; +#[cfg(target_arch = "wasm32")] +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + use wasm_bindgen::prelude::*; #[cfg(not(target_arch = "wasm32"))] @@ -21,4 +27,7 @@ pub struct PubkyClient { http: reqwest::Client, #[cfg(not(target_arch = "wasm32"))] pub(crate) pkarr: PkarrClientAsync, + /// A cookie jar for nodejs fetch. + #[cfg(target_arch = "wasm32")] + pub(crate) session_cookies: Arc>>, } diff --git a/pubky/src/native.rs b/pubky/src/native.rs index fa2ae3f..e6627cc 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -8,7 +8,7 @@ use ::pkarr::{ }; use pkarr::Keypair; use pubky_common::session::Session; -use reqwest::{Method, RequestBuilder}; +use reqwest::{Method, RequestBuilder, Response}; use url::Url; use crate::{error::Result, PubkyClient}; @@ -94,4 +94,7 @@ impl PubkyClient { pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { self.http.request(method, url) } + + pub(crate) fn store_session(&self, response: Response) {} + pub(crate) fn remove_session(&self, pubky: &PublicKey) {} } diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index e78f84a..276ab1d 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -20,15 +20,19 @@ impl PubkyClient { ) -> Result<()> { let homeserver = homeserver.to_string(); + let public_key = &keypair.public_key(); + let (audience, mut url) = self.resolve_endpoint(&homeserver).await?; - url.set_path(&format!("/{}", keypair.public_key())); + url.set_path(&format!("/{}", public_key)); let body = AuthnSignature::generate(keypair, &audience) .as_bytes() .to_owned(); - self.request(Method::PUT, url).body(body).send().await?; + let response = self.request(Method::PUT, url).body(body).send().await?; + + self.store_session(response); self.publish_pubky_homeserver(keypair, &homeserver).await?; @@ -67,6 +71,8 @@ impl PubkyClient { self.request(Method::DELETE, url).send().await?; + self.remove_session(pubky); + Ok(()) } @@ -82,7 +88,9 @@ impl PubkyClient { .as_bytes() .to_owned(); - self.request(Method::POST, url).body(body).send().await?; + let response = self.request(Method::POST, url).body(body).send().await?; + + self.store_session(response); Ok(()) } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 01fcf73..e572670 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -1,6 +1,11 @@ +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; + use wasm_bindgen::prelude::*; -use reqwest::{Method, RequestBuilder}; +use reqwest::{Method, RequestBuilder, Response}; use url::Url; use crate::PubkyClient; @@ -18,6 +23,7 @@ impl PubkyClient { pub fn new() -> Self { Self { http: reqwest::Client::builder().build().unwrap(), + session_cookies: Arc::new(RwLock::new(HashSet::new())), } } @@ -60,7 +66,37 @@ impl PubkyClient { .map_err(|e| e.into()) } - pub(crate) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder { - self.http.request(method, url).fetch_credentials_include() + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { + let request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + return request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); } } From ce2a00f020654fc0520ff48793954c97eca12f24 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 09:35:56 +0300 Subject: [PATCH 20/26] chore(pubky): add script to run testnet homeserver --- pubky/pkg/README.md | 9 ++++++--- pubky/pkg/package.json | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index d965c2f..961c93f 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -23,6 +23,9 @@ let keypair = Keypair.random(); let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver) + +// Verify that you are signed in. +const session = await client.session(publicKey) ``` ## Test and Development @@ -39,11 +42,11 @@ Clone the Pubky repository: ```bash git clone https://github.com/pubky/pubky -cd pubky/ +cd pubky/pkg ``` -Run the testnet server +Run the local testnet server ```bash -cargo run --bin pubky_homeserver -- --testnet +npm run testnet ``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index e633cc3..ef92f52 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -9,11 +9,13 @@ "url": "https://github.com/pubky/pubky" }, "scripts": { - "test": "tape test/*.js -cov", + "testnet": "cargo run -p pubky_homeserver -- --testnet", + "test": "npm run test-nodejs && npm run test-browser", + "test-nodejs": "tape test/*.js -cov", "test-browser": "browserify test/*.js -p esmify | npx tape-run", "build": "cargo run --bin bundle_pubky_npm", "preinstall": "npm run build", - "prepublishOnly": "npm run build && npm run test && npm run test-browser" + "prepublishOnly": "npm run build && npm run test" }, "files": [ "nodejs/*", From bd5b44e54454a3e7812e0cadd9bdbeaf694f33a0 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 13:13:56 +0300 Subject: [PATCH 21/26] feat(pubky): add put/get methods for js --- pubky/Cargo.toml | 13 +----- pubky/pkg/test/auth.js | 2 +- pubky/pkg/test/public.js | 46 ++++++++++++++++++++ pubky/src/native.rs | 27 +++++++++++- pubky/src/shared/mod.rs | 1 + pubky/src/shared/public.rs | 86 ++++++++++++++++++++++++++++++++++++++ pubky/src/wasm.rs | 44 +++++++------------ pubky/src/wasm/http.rs | 42 +++++++++++++++++++ pubky/src/wasm/keys.rs | 3 +- 9 files changed, 220 insertions(+), 44 deletions(-) create mode 100644 pubky/pkg/test/public.js create mode 100644 pubky/src/shared/public.rs create mode 100644 pubky/src/wasm/http.rs diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index d71c0bb..392402e 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -14,30 +14,21 @@ crate-type = ["cdylib", "rlib"] thiserror = "1.0.62" wasm-bindgen = "0.2.92" url = "2.5.2" -reqwest = { version = "0.12.5", features = ["cookies"] } bytes = "1.6.1" pubky-common = { version = "0.1.0", path = "../pubky-common" } -flume = "0.11.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] pkarr = { version="2.1.0", features = ["async"] } +reqwest = { version = "0.12.5", features = ["cookies"], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] pkarr = { version = "2.1.0", default-features = false } +reqwest = { version = "0.12.5", default-features = false } -futures = "0.3.29" js-sys = "0.3.69" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" -web-sys = { version = "0.3.69", features = [ - "console", - "Request", - "RequestInit", - "RequestMode", - "Response", - "Window", -] } [dev-dependencies] pubky_homeserver = { path = "../pubky-homeserver" } diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index a660d99..60f9b4c 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -8,7 +8,7 @@ test('seed auth', async (t) => { const keypair = Keypair.random() const publicKey = keypair.public_key() - const homeserver = PublicKey.try_from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo') await client.signup(keypair, homeserver) const session = await client.session(publicKey) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js new file mode 100644 index 0000000..b5ae7a9 --- /dev/null +++ b/pubky/pkg/test/public.js @@ -0,0 +1,46 @@ +import test from 'tape' + +import { PubkyClient, Keypair, PublicKey } from '../index.js' + +test('public: put/get', async (t) => { + const client = new PubkyClient(); + + const keypair = Keypair.random(); + + const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo'); + await client.signup(keypair, homeserver); + + const publicKey = keypair.public_key(); + + const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + + // PUT public data, by authorized client + await client.put(publicKey, "/pub/example.com/arbitrary", body); + + + // GET public data without signup or signin + { + const client = new PubkyClient(); + + let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + + t.ok(Buffer.from(response).equals(body)) + } + + // // DELETE public data, by authorized client + // await client.delete(publicKey, "/pub/example.com/arbitrary"); + // + // + // // GET public data without signup or signin + // { + // const client = new PubkyClient(); + // + // let response = await client.get(publicKey, "/pub/example.com/arbitrary"); + // + // t.notOk(response) + // } +}) + +test.skip("not found") + +test.skip("unauthorized") diff --git a/pubky/src/native.rs b/pubky/src/native.rs index e6627cc..b350892 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -1,11 +1,10 @@ -pub mod public; - use std::time::Duration; use ::pkarr::{ mainline::dht::{DhtSettings, Testnet}, PkarrClient, PublicKey, Settings, SignedPacket, }; +use bytes::Bytes; use pkarr::Keypair; use pubky_common::session::Session; use reqwest::{Method, RequestBuilder, Response}; @@ -21,6 +20,8 @@ impl Default for PubkyClient { } } +// === Public API === + impl PubkyClient { pub fn new() -> Self { Self { @@ -54,6 +55,8 @@ impl PubkyClient { } } + // === Auth === + /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key @@ -80,6 +83,24 @@ impl PubkyClient { self.inner_signin(keypair).await } + // === Public data === + + /// Upload a small payload to a given path. + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + self.inner_put(pubky, path, content).await + } + + /// Download a small payload from a given path relative to a pubky author. + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + self.inner_get(pubky, path).await + } +} + +// === Internals === + +impl PubkyClient { + // === Pkarr === + pub(crate) async fn pkarr_resolve( &self, public_key: &PublicKey, @@ -91,6 +112,8 @@ impl PubkyClient { Ok(self.pkarr.publish(signed_packet).await?) } + // === HTTP === + pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { self.http.request(method, url) } diff --git a/pubky/src/shared/mod.rs b/pubky/src/shared/mod.rs index c61bbfe..ec9bd27 100644 --- a/pubky/src/shared/mod.rs +++ b/pubky/src/shared/mod.rs @@ -1,2 +1,3 @@ pub mod auth; pub mod pkarr; +pub mod public; diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs new file mode 100644 index 0000000..ed2da36 --- /dev/null +++ b/pubky/src/shared/public.rs @@ -0,0 +1,86 @@ +use bytes::Bytes; + +use pkarr::PublicKey; +use reqwest::Method; + +use crate::{error::Result, PubkyClient}; + +impl PubkyClient { + pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + self.request(Method::PUT, url) + .body(content.to_owned()) + .send() + .await?; + + Ok(()) + } + + pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path); + + let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; + + url.set_path(&format!("/{pubky}/{path}")); + + let response = self.request(Method::GET, url).send().await?; + + // TODO: bail on too large files. + let bytes = response.bytes().await?; + + Ok(bytes) + } +} + +fn normalize_path(path: &str) -> String { + let mut path = path.to_string(); + + if path.starts_with('/') { + path = path[1..].to_string() + } + + // TODO: should we return error instead? + if path.ends_with('/') { + path = path[..path.len()].to_string() + } + + path +} + +#[cfg(test)] +mod tests { + + use crate::*; + + use pkarr::{mainline::Testnet, Keypair}; + use pubky_homeserver::Homeserver; + + #[tokio::test] + async fn put_get() { + let testnet = Testnet::new(3); + let server = Homeserver::start_test(&testnet).await.unwrap(); + + let client = PubkyClient::test(&testnet); + + let keypair = Keypair::random(); + + client.signup(&keypair, &server.public_key()).await.unwrap(); + + client + .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) + .await + .unwrap(); + + let response = client + .get(&keypair.public_key(), "/pub/foo.txt") + .await + .unwrap(); + + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) + } +} diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index e572670..cdff018 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -10,6 +10,7 @@ use url::Url; use crate::PubkyClient; +mod http; mod keys; mod pkarr; mod session; @@ -66,37 +67,22 @@ impl PubkyClient { .map_err(|e| e.into()) } - pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder { - let request = self.http.request(method, url).fetch_credentials_include(); + // === Public data === - for cookie in self.session_cookies.read().unwrap().iter() { - return request.header("Cookie", cookie); - } - - request + #[wasm_bindgen] + /// Upload a small payload to a given path. + pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<(), JsValue> { + self.inner_put(pubky.as_inner(), path, content) + .await + .map_err(|e| e.into()) } - // Support cookies for nodejs - - pub(crate) fn store_session(&self, response: Response) { - if let Some(cookie) = response - .headers() - .get("set-cookie") - .and_then(|h| h.to_str().ok()) - .and_then(|s| s.split(';').next()) - { - self.session_cookies - .write() - .unwrap() - .insert(cookie.to_string()); - } - } - pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { - let key = pubky.to_string(); - - self.session_cookies - .write() - .unwrap() - .retain(|cookie| !cookie.starts_with(&key)); + #[wasm_bindgen] + /// Download a small payload from a given path relative to a pubky author. + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + self.inner_get(pubky.as_inner(), path) + .await + .map(|b| (*b).into()) + .map_err(|e| e.into()) } } diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs new file mode 100644 index 0000000..52a0c9d --- /dev/null +++ b/pubky/src/wasm/http.rs @@ -0,0 +1,42 @@ +use crate::PubkyClient; + +use reqwest::{Method, RequestBuilder, Response}; +use url::Url; + +use ::pkarr::PublicKey; + +impl PubkyClient { + pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { + let request = self.http.request(method, url).fetch_credentials_include(); + + for cookie in self.session_cookies.read().unwrap().iter() { + return request.header("Cookie", cookie); + } + + request + } + + // Support cookies for nodejs + + pub(crate) fn store_session(&self, response: Response) { + if let Some(cookie) = response + .headers() + .get("set-cookie") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.split(';').next()) + { + self.session_cookies + .write() + .unwrap() + .insert(cookie.to_string()); + } + } + pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) { + let key = pubky.to_string(); + + self.session_cookies + .write() + .unwrap() + .retain(|cookie| !cookie.starts_with(&key)); + } +} diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index cbd6ec7..b5043bb 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -51,7 +51,8 @@ impl PublicKey { self.0.to_string() } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "from")] + /// @throws pub fn try_from(value: JsValue) -> Result { let string = value.as_string().ok_or(Error::Generic( "Couldn't create a PublicKey from this type of value".to_string(), From 65f94045b9d58d55e6f5916621ea12a9191e1c04 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:10:58 +0300 Subject: [PATCH 22/26] fix(pubky): cookies jar in nodejs --- pubky/src/wasm/http.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubky/src/wasm/http.rs b/pubky/src/wasm/http.rs index 52a0c9d..d89d4ce 100644 --- a/pubky/src/wasm/http.rs +++ b/pubky/src/wasm/http.rs @@ -7,10 +7,10 @@ use ::pkarr::PublicKey; impl PubkyClient { pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder { - let request = self.http.request(method, url).fetch_credentials_include(); + let mut request = self.http.request(method, url).fetch_credentials_include(); for cookie in self.session_cookies.read().unwrap().iter() { - return request.header("Cookie", cookie); + request = request.header("Cookie", cookie); } request From bdd07f579ce4694a9b522fcd9f607ca0a458f335 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:11:19 +0300 Subject: [PATCH 23/26] fix(pubky): cookies jar in nodejs --- pubky/src/native/public.rs | 82 -------------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 pubky/src/native/public.rs diff --git a/pubky/src/native/public.rs b/pubky/src/native/public.rs deleted file mode 100644 index 9b2a271..0000000 --- a/pubky/src/native/public.rs +++ /dev/null @@ -1,82 +0,0 @@ -use bytes::Bytes; - -use pkarr::PublicKey; - -use crate::{error::Result, PubkyClient}; - -impl PubkyClient { - pub async fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); - - self.http.put(url).body(content.to_owned()).send().await?; - - Ok(()) - } - - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); - - let response = self.http.get(url).send().await?; - - // TODO: bail on too large files. - let bytes = response.bytes().await?; - - Ok(bytes) - } -} - -fn normalize_path(path: &str) -> String { - let mut path = path.to_string(); - - if path.starts_with('/') { - path = path[1..].to_string() - } - - // TODO: should we return error instead? - if path.ends_with('/') { - path = path[..path.len()].to_string() - } - - path -} - -#[cfg(test)] -mod tests { - - use crate::*; - - use pkarr::{mainline::Testnet, Keypair}; - use pubky_homeserver::Homeserver; - - #[tokio::test] - async fn put_get() { - let testnet = Testnet::new(3); - let server = Homeserver::start_test(&testnet).await.unwrap(); - - let client = PubkyClient::test(&testnet); - - let keypair = Keypair::random(); - - client.signup(&keypair, &server.public_key()).await.unwrap(); - - client - .put(&keypair.public_key(), "/pub/foo.txt", &[0, 1, 2, 3, 4]) - .await - .unwrap(); - - let response = client - .get(&keypair.public_key(), "/pub/foo.txt") - .await - .unwrap(); - - assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) - } -} From 5b9a49898bb1e5245381f77458a29108a6066990 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:18:45 +0300 Subject: [PATCH 24/26] chore(pubky): clippy issues --- Cargo.lock | 403 +------------------------------------ pubky/pkg/test/keys.js | 2 +- pubky/src/native.rs | 7 +- pubky/src/shared/public.rs | 64 ++++-- pubky/src/wasm.rs | 16 +- pubky/src/wasm/keys.rs | 5 +- 6 files changed, 74 insertions(+), 423 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbeea1b..85d39a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,12 +113,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.3.0" @@ -266,12 +260,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.6.0" @@ -425,22 +413,6 @@ dependencies = [ "url", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - [[package]] name = "cpufeatures" version = "0.2.12" @@ -628,37 +600,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "fastrand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -683,21 +624,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -825,25 +751,6 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hash32" version = "0.2.1" @@ -853,12 +760,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "headers" version = "0.4.0" @@ -915,7 +816,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc30da4a93ff8cb98e535d595d6de42731d4719d707bc1c86f579158751a24e" dependencies = [ - "bitflags 2.6.0", + "bitflags", "byteorder", "heed-traits", "heed-types", @@ -1008,7 +909,6 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", "http", "http-body", "httparse", @@ -1020,39 +920,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" -dependencies = [ - "futures-util", - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.6" @@ -1093,16 +960,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -1148,16 +1005,10 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags", "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - [[package]] name = "litrs" version = "0.4.1" @@ -1273,23 +1124,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1331,50 +1165,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "openssl" -version = "0.10.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "overload" version = "0.1.1" @@ -1530,12 +1320,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - [[package]] name = "postcard" version = "1.0.8" @@ -1580,8 +1364,6 @@ name = "pubky" version = "0.1.0" dependencies = [ "bytes", - "flume", - "futures", "js-sys", "pkarr", "pubky-common", @@ -1592,7 +1374,6 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "web-sys", ] [[package]] @@ -1691,7 +1472,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -1759,33 +1540,25 @@ dependencies = [ "bytes", "cookie", "cookie_store", - "encoding_rs", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", - "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", - "system-configuration", "tokio", - "tokio-native-tls", "tower-service", "url", "wasm-bindgen", @@ -1794,21 +1567,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1824,59 +1582,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.23.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" - -[[package]] -name = "rustls-webpki" -version = "0.102.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.17" @@ -1889,44 +1594,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "self_cell" version = "1.0.4" @@ -2072,7 +1745,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" dependencies = [ - "bitflags 2.6.0", + "bitflags", ] [[package]] @@ -2175,39 +1848,6 @@ dependencies = [ "crossbeam-queue", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -dependencies = [ - "cfg-if", - "fastrand", - "rustix", - "windows-sys 0.52.0", -] - [[package]] name = "thiserror" version = "1.0.62" @@ -2314,27 +1954,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.11" @@ -2387,7 +2006,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.6.0", + "bitflags", "bytes", "http", "http-body", @@ -2505,12 +2124,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.2" @@ -2534,12 +2147,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" diff --git a/pubky/pkg/test/keys.js b/pubky/pkg/test/keys.js index 76e3202..306e21e 100644 --- a/pubky/pkg/test/keys.js +++ b/pubky/pkg/test/keys.js @@ -9,5 +9,5 @@ test('generate keys from a seed', async (t) => { const publicKey = keypair.public_key() - t.is(publicKey.to_string(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') + t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o') }) diff --git a/pubky/src/native.rs b/pubky/src/native.rs index b350892..5805241 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -91,9 +91,14 @@ impl PubkyClient { } /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result> { self.inner_get(pubky, path).await } + + // /// Delete a file at a path relative to a pubky author. + // pub async fn delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { + // self.inner_delete(pubky, path).await + // } } // === Internals === diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index ed2da36..2b1edaa 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -1,17 +1,14 @@ use bytes::Bytes; use pkarr::PublicKey; -use reqwest::Method; +use reqwest::{Method, Response, StatusCode}; +use url::Url; use crate::{error::Result, PubkyClient}; impl PubkyClient { pub async fn inner_put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> { - let path = normalize_path(path); - - let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; - - url.set_path(&format!("/{pubky}/{path}")); + let url = self.url(pubky, path).await?; self.request(Method::PUT, url) .body(content.to_owned()) @@ -21,23 +18,41 @@ impl PubkyClient { Ok(()) } - pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result { - let path = normalize_path(path); + pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result> { + let url = self.url(pubky, path).await?; + + let res = self.request(Method::GET, url).send().await?; + + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + + // TODO: bail on too large files. + let bytes = res.bytes().await?; + + Ok(Some(bytes)) + } + + pub async fn inner_delete(&self, pubky: &PublicKey, path: &str) -> Result<()> { + let url = self.url(pubky, path).await?; + + self.request(Method::DELETE, url).send().await?; + + Ok(()) + } + + async fn url(&self, pubky: &PublicKey, path: &str) -> Result { + let path = normalize_path(path)?; let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?; url.set_path(&format!("/{pubky}/{path}")); - let response = self.request(Method::GET, url).send().await?; - - // TODO: bail on too large files. - let bytes = response.bytes().await?; - - Ok(bytes) + Ok(url) } } -fn normalize_path(path: &str) -> String { +fn normalize_path(path: &str) -> Result { let mut path = path.to_string(); if path.starts_with('/') { @@ -49,7 +64,7 @@ fn normalize_path(path: &str) -> String { path = path[..path.len()].to_string() } - path + Ok(path) } #[cfg(test)] @@ -61,7 +76,7 @@ mod tests { use pubky_homeserver::Homeserver; #[tokio::test] - async fn put_get() { + async fn put_get_delete() { let testnet = Testnet::new(3); let server = Homeserver::start_test(&testnet).await.unwrap(); @@ -79,8 +94,21 @@ mod tests { let response = client .get(&keypair.public_key(), "/pub/foo.txt") .await + .unwrap() .unwrap(); - assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])) + assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); + + // client + // .delete(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // let response = client + // .get(&keypair.public_key(), "/pub/foo.txt") + // .await + // .unwrap(); + // + // assert_eq!(response, None); } } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index cdff018..7cadbbd 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -18,6 +18,12 @@ mod session; use keys::{Keypair, PublicKey}; use session::Session; +impl Default for PubkyClient { + fn default() -> Self { + Self::new() + } +} + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] @@ -47,7 +53,7 @@ impl PubkyClient { pub async fn session(&self, pubky: &PublicKey) -> Result, JsValue> { self.inner_session(pubky.as_inner()) .await - .map(|s| s.map(|s| Session(s).into())) + .map(|s| s.map(Session)) .map_err(|e| e.into()) } @@ -79,10 +85,14 @@ impl PubkyClient { #[wasm_bindgen] /// Download a small payload from a given path relative to a pubky author. - pub async fn get(&self, pubky: &PublicKey, path: &str) -> Result { + pub async fn get( + &self, + pubky: &PublicKey, + path: &str, + ) -> Result, JsValue> { self.inner_get(pubky.as_inner(), path) .await - .map(|b| (*b).into()) + .map(|b| b.map(|b| (&*b).into())) .map_err(|e| e.into()) } } diff --git a/pubky/src/wasm/keys.rs b/pubky/src/wasm/keys.rs index b5043bb..d1ef078 100644 --- a/pubky/src/wasm/keys.rs +++ b/pubky/src/wasm/keys.rs @@ -47,7 +47,8 @@ impl PublicKey { } #[wasm_bindgen] - pub fn to_string(&self) -> String { + /// Returns the z-base32 encoding of this public key + pub fn z32(&self) -> String { self.0.to_string() } @@ -59,7 +60,7 @@ impl PublicKey { ))?; Ok(PublicKey( - pkarr::PublicKey::try_from(string).map_err(|e| Error::Pkarr(e))?, + pkarr::PublicKey::try_from(string).map_err(Error::Pkarr)?, )) } } From 75d8acde65daf6c0d97d4bb0b2f42d7b53567176 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:48:51 +0300 Subject: [PATCH 25/26] feat(pubky): override default pkarr relays --- pubky/pkg/README.md | 28 ++++++++++++++++++++++++++-- pubky/pkg/package.json | 2 +- pubky/pkg/test/auth.js | 10 +++++----- pubky/pkg/test/public.js | 4 ++-- pubky/src/lib.rs | 2 ++ pubky/src/wasm.rs | 26 ++++++++++++++++++++++++++ pubky/src/wasm/pkarr.rs | 12 +++++++----- 7 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pubky/pkg/README.md b/pubky/pkg/README.md index 961c93f..784834a 100644 --- a/pubky/pkg/README.md +++ b/pubky/pkg/README.md @@ -11,7 +11,7 @@ npm install @synonymdev/pubky ## Getting started ```js -import PubkyClient from "@synonymdev/pubky"; +import { PubkyClient, Keypair, PublicKey } from '../index.js' // Initialize PubkyClient with Pkarr relay(s). let client = new PubkyClient(); @@ -20,12 +20,26 @@ let client = new PubkyClient(); let keypair = Keypair.random(); // Create a new account -let homeserver = PublicKey.try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); +let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); await client.signup(keypair, homeserver) // Verify that you are signed in. const session = await client.session(publicKey) + +const publicKey = keypair.public_key(); + +const body = Buffer.from(JSON.stringify({ foo: 'bar' })) + +// PUT public data, by authorized client +await client.put(publicKey, "/pub/example.com/arbitrary", body); + +// GET public data without signup or signin +{ + const client = new PubkyClient(); + + let response = await client.get(publicKey, "/pub/example.com/arbitrary"); +} ``` ## Test and Development @@ -50,3 +64,13 @@ Run the local testnet server ```bash npm run testnet ``` + +Pass the logged addresses as inputs to `PubkyClient` + +```js +import { PubkyClient, PublicKey } from '../index.js' + +const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]); + +let homeserver = PublicKey.from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"); +``` diff --git a/pubky/pkg/package.json b/pubky/pkg/package.json index ef92f52..0adc9b9 100644 --- a/pubky/pkg/package.json +++ b/pubky/pkg/package.json @@ -2,7 +2,7 @@ "name": "@synonymdev/pubky", "type": "module", "description": "Pubky client", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "repository": { "type": "git", diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index 60f9b4c..cd54e06 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -2,8 +2,8 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' -test('seed auth', async (t) => { - const client = new PubkyClient() +test('auth', async (t) => { + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) const keypair = Keypair.random() const publicKey = keypair.public_key() @@ -12,19 +12,19 @@ test('seed auth', async (t) => { await client.signup(keypair, homeserver) const session = await client.session(publicKey) - t.ok(session) + t.ok(session, "signup") { await client.signout(publicKey) const session = await client.session(publicKey) - t.notOk(session) + t.notOk(session, "singout") } { await client.signin(keypair) const session = await client.session(publicKey) - t.ok(session) + t.ok(session, "signin") } }) diff --git a/pubky/pkg/test/public.js b/pubky/pkg/test/public.js index b5ae7a9..ddf02a4 100644 --- a/pubky/pkg/test/public.js +++ b/pubky/pkg/test/public.js @@ -3,7 +3,7 @@ import test from 'tape' import { PubkyClient, Keypair, PublicKey } from '../index.js' test('public: put/get', async (t) => { - const client = new PubkyClient(); + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) const keypair = Keypair.random(); @@ -20,7 +20,7 @@ test('public: put/get', async (t) => { // GET public data without signup or signin { - const client = new PubkyClient(); + const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"]) let response = await client.get(publicKey, "/pub/example.com/arbitrary"); diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 72f2df5..ab6732b 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -30,4 +30,6 @@ pub struct PubkyClient { /// 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/wasm.rs b/pubky/src/wasm.rs index 7cadbbd..0607b75 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -24,6 +24,8 @@ impl Default for PubkyClient { } } +static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; + #[wasm_bindgen] impl PubkyClient { #[wasm_bindgen(constructor)] @@ -31,9 +33,33 @@ impl PubkyClient { 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(), } } + /// Set the 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 { + let relays: Vec = relays + .into_iter() + .filter_map(|name| name.as_string()) + .collect(); + + self.pkarr_relays = relays; + self + } + + #[wasm_bindgen(js_name = "getPkarrRelays")] + pub fn get_pkarr_relays(&self) -> Vec { + self.pkarr_relays + .clone() + .into_iter() + .map(JsValue::from) + .collect() + } + /// Signup to a homeserver and update Pkarr accordingly. /// /// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key diff --git a/pubky/src/wasm/pkarr.rs b/pubky/src/wasm/pkarr.rs index 8b43299..49726f6 100644 --- a/pubky/src/wasm/pkarr.rs +++ b/pubky/src/wasm/pkarr.rs @@ -5,20 +5,20 @@ pub use pkarr::{PublicKey, SignedPacket}; use crate::error::Result; use crate::PubkyClient; -const TEST_RELAY: &str = "http://localhost:15411/pkarr"; - // TODO: Add an in memory cache of packets impl PubkyClient { - //TODO: Allow multiple relays in parallel //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!("{TEST_RELAY}/{}", public_key)) + .get(format!("{relay}/{}", public_key)) .send() .await?; @@ -35,8 +35,10 @@ impl PubkyClient { } 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!("{TEST_RELAY}/{}", signed_packet.public_key())) + .put(format!("{relay}/{}", signed_packet.public_key())) .body(signed_packet.to_relay_payload()) .send() .await?; From 59839c521d33996608a63291c5056dd34cd3e305 Mon Sep 17 00:00:00 2001 From: nazeh Date: Mon, 29 Jul 2024 14:55:23 +0300 Subject: [PATCH 26/26] docs: fix some comments --- pubky-homeserver/src/config.rs | 2 -- pubky/src/native.rs | 4 ++-- pubky/src/wasm.rs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pubky-homeserver/src/config.rs b/pubky-homeserver/src/config.rs index 6949b09..2136e0c 100644 --- a/pubky-homeserver/src/config.rs +++ b/pubky-homeserver/src/config.rs @@ -11,8 +11,6 @@ const DEFAULT_HOMESERVER_PORT: u16 = 6287; const DEFAULT_STORAGE_DIR: &str = "pubky"; /// Server configuration -/// -/// The config is usually loaded from a file with [`Self::load`]. #[derive( // Serialize, Deserialize, Clone, diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 5805241..b63c844 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -67,8 +67,8 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. + /// 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 } diff --git a/pubky/src/wasm.rs b/pubky/src/wasm.rs index 0607b75..b04de3b 100644 --- a/pubky/src/wasm.rs +++ b/pubky/src/wasm.rs @@ -73,8 +73,8 @@ impl PubkyClient { /// Check the current sesison for a given Pubky in its homeserver. /// - /// Returns an [Error::NotSignedIn] if so, or [reqwest::Error] if - /// the response has any other `>=400` status code. + /// 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())