Merge pull request #22 from pubky/feat/ffi

Feat/ffi
This commit is contained in:
Nuh
2024-07-29 15:04:16 +03:00
committed by GitHub
40 changed files with 1597 additions and 604 deletions

357
Cargo.lock generated
View File

@@ -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"
@@ -78,6 +127,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
"http",
@@ -150,6 +200,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"
@@ -259,12 +321,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 0.5.0",
"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 = "const-oid"
version = "0.9.6"
@@ -295,9 +403,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa"
dependencies = [
"cookie",
"idna",
"indexmap",
"idna 0.5.0",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
@@ -329,15 +437,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"
@@ -501,28 +600,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fiat-crypto"
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"
@@ -677,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"
@@ -721,6 +798,18 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "heed"
version = "0.20.3"
@@ -828,6 +917,7 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
@@ -837,12 +927,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]]
@@ -856,14 +961,16 @@ dependencies = [
]
[[package]]
name = "indexmap"
version = "2.2.6"
name = "ipnet"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
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"
@@ -1246,19 +1353,27 @@ 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"
dependencies = [
"bytes",
"flume",
"js-sys",
"pkarr",
"pubky-common",
"pubky_homeserver",
"reqwest",
"thiserror",
"tokio",
"ureq",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
]
[[package]]
@@ -1268,6 +1383,7 @@ dependencies = [
"base32",
"blake3",
"ed25519-dalek",
"js-sys",
"once_cell",
"pkarr",
"postcard",
@@ -1285,6 +1401,7 @@ dependencies = [
"axum-extra",
"base32",
"bytes",
"clap",
"dirs-next",
"flume",
"futures-util",
@@ -1300,6 +1417,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"
@@ -1404,18 +1531,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "ring"
version = "0.17.8"
name = "reqwest"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
@@ -1433,38 +1582,6 @@ dependencies = [
"semver",
]
[[package]]
name = "rustls"
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",
"zeroize",
]
[[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"
@@ -1687,6 +1804,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"
@@ -1968,6 +2091,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"
@@ -1995,30 +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 = "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"
@@ -2026,10 +2131,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"idna 0.5.0",
"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"
@@ -2042,6 +2153,15 @@ 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"
@@ -2124,15 +2244,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"
@@ -2294,6 +2405,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"

View File

@@ -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'

View File

@@ -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"

View File

@@ -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())
}
}
}

View File

@@ -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();

View File

@@ -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}")]

View File

@@ -5,10 +5,11 @@ 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"
clap = { version = "4.5.11", features = ["derive"] }
dirs-next = "2.0.0"
flume = "0.11.0"
futures-util = "0.3.30"

View File

@@ -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;
@@ -11,21 +11,20 @@ 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,
)]
pub struct Config {
port: Option<u16>,
bootstrap: Option<Vec<String>>,
domain: String,
pub port: Option<u16>,
pub bootstrap: Option<Vec<String>>,
pub domain: String,
/// Path to the storage directory
///
/// Defaults to a directory in the OS data directory
storage: Option<PathBuf>,
keypair: Keypair,
pub storage: Option<PathBuf>,
pub keypair: Keypair,
pub request_timeout: Option<Duration>,
}
impl Config {
@@ -50,6 +49,7 @@ impl Config {
Self {
bootstrap,
storage,
request_timeout: Some(Duration::from_millis(10)),
..Default::default()
}
}
@@ -93,6 +93,7 @@ impl Default for Config {
domain: "localhost".to_string(),
storage: None,
keypair: Keypair::random(),
request_timeout: None,
}
}
}

View File

@@ -1,13 +1,41 @@
use anyhow::Result;
use pubky_homeserver::Homeserver;
use pkarr::{mainline::Testnet, Keypair};
use pubky_homeserver::{config::Config, Homeserver};
use clap::Parser;
#[derive(Parser, Debug)]
struct Cli {
/// [tracing_subscriber::EnvFilter]
#[clap(short, long)]
tracing_env_filter: Option<String>,
#[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(Config {
port: Some(15411),
keypair: Keypair::from_secret_key(&[0_u8; 32]),
..Config::test(&testnet)
})
.await?
} else {
Homeserver::start(Default::default()).await?
};
server.run_until_done().await?;

View File

@@ -1,18 +1,27 @@
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;
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))
@@ -21,10 +30,17 @@ pub fn create_app(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?).
.layer(DefaultBodyLimit::max(16 * 1024))
.with_state(state)
}
pub fn create_app(state: AppState) -> Router {
base(state.clone())
// TODO: Only enable this for test environments?
.nest("/pkarr", pkarr_router(state))
.layer(CorsLayer::very_permissive())
.layer(TraceLayer::new_for_http())
}

View File

@@ -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<AppState>,
TypedHeader(user_agent): TypedHeader<UserAgent>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
// 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<UserAgent>,
cookies: Cookies,
pubky: Pubky,
uri: Uri,
body: Bytes,
) -> Result<impl IntoResponse> {
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()?;

View File

@@ -0,0 +1,61 @@
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 pkarr::{PublicKey, SignedPacket};
use tracing::debug;
use crate::{
error::{Error, Result},
extractors::Pubky,
server::AppState,
};
/// 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(state: AppState) -> Router {
Router::new()
.route("/:pubky", put(pkarr_put))
.route("/:pubky", get(pkarr_get))
.with_state(state)
}
pub async fn pkarr_put(
State(mut state): State<AppState>,
pubky: Pubky,
body: Body,
) -> Result<impl IntoResponse> {
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))?;
state.pkarr_client.publish(&signed_packet).await?;
Ok(())
}
pub async fn pkarr_get(State(state): State<AppState>, pubky: Pubky) -> Result<impl IntoResponse> {
if let Some(signed_packet) = state.pkarr_client.resolve(pubky.public_key()).await? {
return Ok(signed_packet.to_relay_payload());
}
Err(Error::with_status(StatusCode::NOT_FOUND))
}

View File

@@ -1,13 +1,15 @@
use std::{future::IntoFuture, net::SocketAddr};
use std::{
collections::HashMap, future::IntoFuture, net::SocketAddr, num::NonZeroUsize, sync::Arc,
};
use anyhow::{Error, Result};
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 +25,7 @@ pub struct Homeserver {
pub(crate) struct AppState {
pub verifier: AuthnVerifier,
pub db: DB,
pub pkarr_client: PkarrClientAsync,
}
impl Homeserver {
@@ -33,9 +36,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 +74,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}");
@@ -82,6 +87,8 @@ impl Homeserver {
/// Test version of [Homeserver::start], using mainline Testnet, and a temporary storage.
pub async fn start_test(testnet: &Testnet) -> Result<Self> {
info!("Running testnet..");
Homeserver::start(Config::test(testnet)).await
}

View File

@@ -2,22 +2,42 @@
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"]
[dependencies]
thiserror = "1.0.62"
wasm-bindgen = "0.2.92"
url = "2.5.2"
bytes = "1.6.1"
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(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 }
js-sys = "0.3.69"
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
[dev-dependencies]
pubky_homeserver = { path = "../pubky-homeserver" }
tokio = "1.37.0"
[features]
async = ["flume/async"]
default = ["async"]
[package.metadata.docs.rs]
all-features = true
# [package.metadata.wasm-pack.profile.release]
# wasm-opt = ['-g', '-O']

5
pubky/pkg/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
nodejs/*
browser.js
coverage
node_modules
package-lock.json

21
pubky/pkg/LICENSE Normal file
View File

@@ -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.

76
pubky/pkg/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Pubky
JavaScript implementation of [Pubky](https://github.com/pubky/pubky).
## Install
```bash
npm install @synonymdev/pubky
```
## Getting started
```js
import { PubkyClient, Keypair, PublicKey } from '../index.js'
// Initialize PubkyClient with Pkarr relay(s).
let client = new PubkyClient();
// Generate a keypair
let keypair = Keypair.random();
// Create a new account
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
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/pkg
```
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");
```

1
pubky/pkg/index.js Normal file
View File

@@ -0,0 +1 @@
export * from './nodejs/pubky.js'

44
pubky/pkg/package.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "@synonymdev/pubky",
"type": "module",
"description": "Pubky client",
"version": "0.0.2",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/pubky/pubky"
},
"scripts": {
"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"
},
"files": [
"nodejs/*",
"index.js",
"browser.js"
],
"main": "index.js",
"browser": "browser.js",
"types": "pubky.d.ts",
"sideEffects": [
"./snippets/*"
],
"keywords": [
"web",
"dht",
"dns",
"decentralized",
"identity"
],
"devDependencies": {
"browser-resolve": "^2.0.0",
"esmify": "^2.1.1",
"tape": "^5.8.1",
"tape-run": "^11.0.0"
}
}

30
pubky/pkg/test/auth.js Normal file
View File

@@ -0,0 +1,30 @@
import test from 'tape'
import { PubkyClient, Keypair, PublicKey } from '../index.js'
test('auth', async (t) => {
const client = new PubkyClient().setPkarrRelays(["http://localhost:15411/pkarr"])
const keypair = Keypair.random()
const publicKey = keypair.public_key()
const homeserver = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo')
await client.signup(keypair, homeserver)
const session = await client.session(publicKey)
t.ok(session, "signup")
{
await client.signout(publicKey)
const session = await client.session(publicKey)
t.notOk(session, "singout")
}
{
await client.signin(keypair)
const session = await client.session(publicKey)
t.ok(session, "signin")
}
})

13
pubky/pkg/test/keys.js Normal file
View File

@@ -0,0 +1,13 @@
import test from 'tape'
import { Keypair } from '../index.js'
test('generate keys from a seed', async (t) => {
const secretkey = Buffer.from('5aa93b299a343aa2691739771f2b5b85e740ca14c685793d67870f88fa89dc51', 'hex')
const keypair = Keypair.from_secret_key(secretkey)
const publicKey = keypair.public_key()
t.is(publicKey.z32(), 'gcumbhd7sqit6nn457jxmrwqx9pyymqwamnarekgo3xppqo6a19o')
})

46
pubky/pkg/test/public.js Normal file
View File

@@ -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().setPkarrRelays(["http://localhost:15411/pkarr"])
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().setPkarrRelays(["http://localhost:15411/pkarr"])
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")

View File

@@ -0,0 +1,65 @@
use std::env;
use std::io;
use std::process::{Command, ExitStatus};
// If the process hangs, try `cargo clean` to remove all locks.
fn main() {
println!("Building wasm for pubky...");
build_wasm("nodejs").unwrap();
patch().unwrap();
}
fn build_wasm(target: &str) -> io::Result<ExitStatus> {
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,
"--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<ExitStatus> {
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([format!("{manifest_dir}/src/bin/patch.mjs")])
.output()?;
println!(
"patch.mjs output: {}",
String::from_utf8_lossy(&output.stdout)
);
if !output.status.success() {
eprintln!(
"patch.mjs failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(output.status)
}

59
pubky/src/bin/patch.mjs Normal file
View File

@@ -0,0 +1,59 @@
// 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";
import { fileURLToPath } from 'node:url';
import path, { dirname } from 'node:path';
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(path.join(__dirname, `../../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(path.join(__dirname, `../../pkg/nodejs/${name}_bg.wasm`), "base64"))
});
`,
);
await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched);

View File

@@ -1,75 +0,0 @@
mod auth;
mod pkarr;
mod public;
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};
#[derive(Debug, Clone)]
pub struct PubkyClient {
agent: Agent,
pkarr: PkarrClient,
}
impl PubkyClient {
pub fn new() -> Self {
Self {
agent: Agent::new(),
pkarr: PkarrClient::new(Default::default()).unwrap(),
}
}
pub fn test(testnet: &Testnet) -> Self {
Self {
agent: Agent::new(),
pkarr: PkarrClient::new(Settings {
dht: DhtSettings {
request_timeout: Some(Duration::from_millis(10)),
bootstrap: Some(testnet.bootstrap.to_owned()),
..DhtSettings::default()
},
..Settings::default()
})
.unwrap(),
}
}
// === Public Methods ===
// === Private Methods ===
fn request(&self, method: HttpMethod, url: &Url) -> ureq::Request {
self.agent.request_url(method.into(), url)
}
}
impl Default for PubkyClient {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum HttpMethod {
Get,
Put,
Post,
Delete,
}
impl From<HttpMethod> for &str {
fn from(value: HttpMethod) -> Self {
match value {
HttpMethod::Get => "GET",
HttpMethod::Put => "PUT",
HttpMethod::Post => "POST",
HttpMethod::Delete => "DELETE",
}
}
}

View File

@@ -1,128 +0,0 @@
use crate::PubkyClient;
use pubky_common::{auth::AuthnSignature, session::Session};
use super::{Error, HttpMethod, Result};
use pkarr::{Keypair, PublicKey};
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)?;
url.set_path(&format!("/{}", keypair.public_key()));
self.request(HttpMethod::Put, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())?;
self.publish_pubky_homeserver(keypair, homeserver);
Ok(())
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// 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<Session> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;
url.set_path(&format!("/{}/session", pubky));
let mut bytes = vec![];
let result = self.request(HttpMethod::Get, &url).call().map_err(Box::new);
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(),
}
})?;
reader.into_reader().read_to_end(&mut bytes);
Ok(Session::deserialize(&bytes)?)
}
/// Signout from a homeserver.
pub fn signout(&self, pubky: &PublicKey) -> Result<()> {
let (homeserver, mut url) = self.resolve_pubky_homeserver(pubky)?;
url.set_path(&format!("/{}/session", pubky));
self.request(HttpMethod::Delete, &url)
.call()
.map_err(Box::new)?;
Ok(())
}
/// Signin to a homeserver.
pub fn signin(&self, keypair: &Keypair) -> Result<()> {
let pubky = keypair.public_key();
let (audience, mut url) = self.resolve_pubky_homeserver(&pubky)?;
url.set_path(&format!("/{}/session", &pubky));
self.request(HttpMethod::Post, &url)
.send_bytes(AuthnSignature::generate(keypair, &audience).as_bytes())
.map_err(Box::new)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::*;
use pkarr::{mainline::Testnet, Keypair};
use pubky_common::session::Session;
use pubky_homeserver::Homeserver;
#[tokio::test]
async fn basic_authn() {
let testnet = Testnet::new(3);
let server = Homeserver::start_test(&testnet).await.unwrap();
let client = PubkyClient::test(&testnet).as_async();
let keypair = Keypair::random();
client
.signup(&keypair, &server.public_key().to_string())
.await
.unwrap();
let session = client.session(&keypair.public_key()).await.unwrap();
assert_eq!(session, Session { ..session.clone() });
client.signout(&keypair.public_key()).await.unwrap();
{
let session = client.session(&keypair.public_key()).await;
assert!(session.is_err());
match session {
Err(Error::NotSignedIn) => {}
_ => assert!(false, "expected NotSignedInt error"),
}
}
client.signin(&keypair).await.unwrap();
{
let session = client.session(&keypair.public_key()).await.unwrap();
assert_eq!(session, Session { ..session.clone() });
}
}
}

View File

@@ -1,104 +0,0 @@
use bytes::Bytes;
use pkarr::PublicKey;
use crate::PubkyClient;
use super::Result;
impl PubkyClient {
pub fn put(&self, pubky: &PublicKey, path: &str, content: &[u8]) -> Result<()> {
let path = normalize_path(path);
let (_, mut url) = self.resolve_pubky_homeserver(pubky)?;
url.set_path(&format!("/{pubky}/{path}"));
self.request(super::HttpMethod::Put, &url)
.send_bytes(content)?;
Ok(())
}
pub fn get(&self, pubky: &PublicKey, path: &str) -> Result<Bytes> {
let path = normalize_path(path);
let (_, mut url) = self.resolve_pubky_homeserver(pubky)?;
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::<u64>().ok())
// TODO: return an error in case content-length header is missing
.unwrap_or(0);
// TODO: bail on too large files.
let mut bytes = vec![0; len as usize];
response.into_reader().read_exact(&mut bytes);
Ok(bytes.into())
}
}
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 std::ops::Deref;
use crate::*;
use pkarr::{mainline::Testnet, Keypair};
use pubky_common::session::Session;
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).as_async();
let keypair = Keypair::random();
client
.signup(&keypair, &server.public_key().to_string())
.await
.unwrap();
let response = client
.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
.unwrap();
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]))
}
}

View File

@@ -1,94 +0,0 @@
use std::thread;
use bytes::Bytes;
use pkarr::{Keypair, PublicKey};
use pubky_common::session::Session;
use crate::{error::Result, PubkyClient};
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::<Result<()>>(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<Session> {
let (sender, receiver) = flume::bounded::<Result<Session>>(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::<Result<()>>(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::<Result<()>>(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::<Result<()>>(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<Bytes> {
let (sender, receiver) = flume::bounded::<Result<Bytes>>(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?
}
}

View File

@@ -12,9 +12,6 @@ pub enum Error {
#[error("Generic error: {0}")]
Generic(String),
#[error("Not signed in")]
NotSignedIn,
// === Transparent ===
#[error(transparent)]
Dns(#[from] SimpleDnsError),
@@ -22,21 +19,26 @@ pub enum Error {
#[error(transparent)]
Pkarr(#[from] pkarr::Error),
#[error(transparent)]
Flume(#[from] flume::RecvError),
#[error(transparent)]
Ureq(#[from] Box<ureq::Error>),
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Session(#[from] pubky_common::session::Error),
#[error("Could not resolve endpoint for {0}")]
ResolveEndpoint(String),
}
impl From<ureq::Error> for Error {
fn from(error: ureq::Error) -> Self {
Error::Ureq(Box::new(error))
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;
#[cfg(target_arch = "wasm32")]
impl From<Error> for JsValue {
fn from(error: Error) -> JsValue {
let error_message = error.to_string();
js_sys::Error::new(&error_message).into()
}
}

View File

@@ -1,8 +1,35 @@
#![allow(unused)]
mod client;
mod client_async;
mod error;
mod shared;
#[cfg(not(target_arch = "wasm32"))]
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"))]
use ::pkarr::PkarrClientAsync;
pub use client::PubkyClient;
pub use error::Error;
#[derive(Debug, Clone)]
#[wasm_bindgen]
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<RwLock<HashSet<String>>>,
#[cfg(target_arch = "wasm32")]
pub(crate) pkarr_relays: Vec<String>,
}

128
pubky/src/native.rs Normal file
View File

@@ -0,0 +1,128 @@
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};
use url::Url;
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()
}
}
// === Public API ===
impl PubkyClient {
pub fn new() -> Self {
Self {
http: reqwest::Client::builder()
.cookie_store(true)
.user_agent(DEFAULT_USER_AGENT)
.build()
.unwrap(),
#[cfg(not(target_arch = "wasm32"))]
pkarr: PkarrClient::new(Default::default()).unwrap().as_async(),
}
}
pub fn test(testnet: &Testnet) -> Self {
Self {
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)),
bootstrap: Some(testnet.bootstrap.to_owned()),
..DhtSettings::default()
},
..Settings::default()
})
.unwrap()
.as_async(),
}
}
// === Auth ===
/// Signup to a homeserver and update Pkarr accordingly.
///
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<()> {
self.inner_signup(keypair, homeserver).await
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
/// or [reqwest::Error] if the response has any other `>=400` status code.
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
self.inner_session(pubky).await
}
/// Signout from a homeserver.
pub async fn signout(&self, pubky: &PublicKey) -> Result<()> {
self.inner_signout(pubky).await
}
/// Signin to a homeserver.
pub async fn signin(&self, keypair: &Keypair) -> Result<()> {
self.inner_signin(keypair).await
}
// === Public data ===
/// Upload a small payload to a given path.
pub async fn put(&self, 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<Option<Bytes>> {
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 ===
impl PubkyClient {
// === Pkarr ===
pub(crate) async fn pkarr_resolve(
&self,
public_key: &PublicKey,
) -> Result<Option<SignedPacket>> {
Ok(self.pkarr.resolve(public_key).await?)
}
pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> {
Ok(self.pkarr.publish(signed_packet).await?)
}
// === HTTP ===
pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
self.http.request(method, url)
}
pub(crate) fn store_session(&self, response: Response) {}
pub(crate) fn remove_session(&self, pubky: &PublicKey) {}
}

150
pubky/src/shared/auth.rs Normal file
View File

@@ -0,0 +1,150 @@
use reqwest::{Method, StatusCode};
use pkarr::{Keypair, PublicKey};
use pubky_common::{auth::AuthnSignature, session::Session};
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(crate) async fn inner_signup(
&self,
keypair: &Keypair,
homeserver: &PublicKey,
) -> 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!("/{}", public_key));
let body = AuthnSignature::generate(keypair, &audience)
.as_bytes()
.to_owned();
let response = self.request(Method::PUT, url).body(body).send().await?;
self.store_session(response);
self.publish_pubky_homeserver(keypair, &homeserver).await?;
Ok(())
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// 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<Option<Session>> {
let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?;
url.set_path(&format!("/{}/session", pubky));
let res = self.request(Method::GET, url).send().await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
if !res.status().is_success() {
res.error_for_status_ref()?;
};
let bytes = res.bytes().await?;
Ok(Some(Session::deserialize(&bytes)?))
}
/// Signout from a homeserver.
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.request(Method::DELETE, url).send().await?;
self.remove_session(pubky);
Ok(())
}
/// Signin to a homeserver.
pub async fn inner_signin(&self, keypair: &Keypair) -> Result<()> {
let pubky = keypair.public_key();
let (audience, mut url) = self.resolve_pubky_homeserver(&pubky).await?;
url.set_path(&format!("/{}/session", &pubky));
let body = AuthnSignature::generate(keypair, &audience)
.as_bytes()
.to_owned();
let response = self.request(Method::POST, url).body(body).send().await?;
self.store_session(response);
Ok(())
}
}
#[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() {
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();
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.unwrap();
assert!(session.is_none());
}
client.signin(&keypair).await.unwrap();
{
let session = client
.session(&keypair.public_key())
.await
.unwrap()
.unwrap();
assert_eq!(session, Session { ..session.clone() });
}
}
}

3
pubky/src/shared/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod auth;
pub mod pkarr;
pub mod public;

View File

@@ -1,19 +1,29 @@
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 super::{Error, PubkyClient, Result, Url};
use crate::{
error::{Error, Result},
PubkyClient,
};
const MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION: u8 = 3;
impl PubkyClient {
/// Publish the SVCB record for `_pubky.<public_key>`.
pub(crate) fn publish_pubky_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> {
pub(crate) async fn publish_pubky_homeserver(
&self,
keypair: &Keypair,
host: &str,
) -> Result<()> {
let existing = self.pkarr_resolve(&keypair.public_key()).await?;
let mut packet = Packet::new_reply(0);
if let Some(existing) = self.pkarr.resolve(&keypair.public_key())? {
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())
@@ -32,34 +42,46 @@ impl PubkyClient {
let signed_packet = SignedPacket::from_packet(keypair, &packet)?;
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)> {
let original_target = target;
// 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;
step += 1;
let response = self
.pkarr_resolve(&public_key)
.await
.map_err(|_| 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 {
@@ -76,7 +98,7 @@ impl PubkyClient {
}
if let Some(svcb) = prior {
homeserver_public_key = Some(public_key);
homeserver_public_key = Some(public_key.clone());
target = svcb.target.to_string();
if let Some(port) = svcb.get_param(pkarr::dns::rdata::SVCB::PORT) {
@@ -90,11 +112,11 @@ impl PubkyClient {
host.clone_from(&target);
};
continue;
if step >= MAX_RECURSIVE_PUBKY_HOMESERVER_RESOLUTION {
continue;
};
}
};
break;
}
}
if let Some(homeserver) = homeserver_public_key {
@@ -107,7 +129,7 @@ impl PubkyClient {
return Ok((homeserver, Url::parse(&url)?));
}
Err(Error::Generic("Could not resolve endpoint".to_string()))
Err(Error::ResolveEndpoint(original_target.into()))
}
}
@@ -157,23 +179,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")
}
}
}

114
pubky/src/shared/public.rs Normal file
View File

@@ -0,0 +1,114 @@
use bytes::Bytes;
use pkarr::PublicKey;
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 url = self.url(pubky, path).await?;
self.request(Method::PUT, url)
.body(content.to_owned())
.send()
.await?;
Ok(())
}
pub async fn inner_get(&self, pubky: &PublicKey, path: &str) -> Result<Option<Bytes>> {
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<Url> {
let path = normalize_path(path)?;
let (_, mut url) = self.resolve_pubky_homeserver(pubky).await?;
url.set_path(&format!("/{pubky}/{path}"));
Ok(url)
}
}
fn normalize_path(path: &str) -> Result<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()
}
Ok(path)
}
#[cfg(test)]
mod tests {
use crate::*;
use pkarr::{mainline::Testnet, Keypair};
use pubky_homeserver::Homeserver;
#[tokio::test]
async fn put_get_delete() {
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()
.unwrap();
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);
}
}

124
pubky/src/wasm.rs Normal file
View File

@@ -0,0 +1,124 @@
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
use wasm_bindgen::prelude::*;
use reqwest::{Method, RequestBuilder, Response};
use url::Url;
use crate::PubkyClient;
mod http;
mod keys;
mod pkarr;
mod session;
use keys::{Keypair, PublicKey};
use session::Session;
impl Default for PubkyClient {
fn default() -> Self {
Self::new()
}
}
static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"];
#[wasm_bindgen]
impl PubkyClient {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
http: reqwest::Client::builder().build().unwrap(),
session_cookies: Arc::new(RwLock::new(HashSet::new())),
pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(),
}
}
/// 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<JsValue>) -> Self {
let relays: Vec<String> = 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<JsValue> {
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
/// 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 [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<Option<Session>, JsValue> {
self.inner_session(pubky.as_inner())
.await
.map(|s| s.map(Session))
.map_err(|e| e.into())
}
/// Signout from a homeserver.
#[wasm_bindgen]
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
self.inner_signout(pubky.as_inner())
.await
.map_err(|e| e.into())
}
/// Signin to a homeserver.
#[wasm_bindgen]
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
self.inner_signin(keypair.as_inner())
.await
.map_err(|e| e.into())
}
// === Public data ===
#[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())
}
#[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<Option<js_sys::Uint8Array>, JsValue> {
self.inner_get(pubky.as_inner(), path)
.await
.map(|b| b.map(|b| (&*b).into()))
.map_err(|e| e.into())
}
}

42
pubky/src/wasm/http.rs Normal file
View File

@@ -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 mut request = self.http.request(method, url).fetch_credentials_include();
for cookie in self.session_cookies.read().unwrap().iter() {
request = request.header("Cookie", cookie);
}
request
}
// Support cookies for nodejs
pub(crate) fn store_session(&self, response: Response) {
if let Some(cookie) = response
.headers()
.get("set-cookie")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.split(';').next())
{
self.session_cookies
.write()
.unwrap()
.insert(cookie.to_string());
}
}
pub(crate) fn remove_session(&self, pubky: &pkarr::PublicKey) {
let key = pubky.to_string();
self.session_cookies
.write()
.unwrap()
.retain(|cookie| !cookie.starts_with(&key));
}
}

72
pubky/src/wasm/keys.rs Normal file
View File

@@ -0,0 +1,72 @@
use wasm_bindgen::prelude::*;
use crate::Error;
#[wasm_bindgen]
pub struct Keypair(pkarr::Keypair);
#[wasm_bindgen]
impl Keypair {
#[wasm_bindgen]
/// Generate a random [Keypair]
pub fn random() -> Self {
Self(pkarr::Keypair::random())
}
#[wasm_bindgen]
/// Generate a [Keypair] from a secret key.
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))
}
#[wasm_bindgen]
/// Returns the [PublicKey] of this keypair.
pub fn public_key(&self) -> PublicKey {
PublicKey(self.0.public_key())
}
}
impl Keypair {
pub fn as_inner(&self) -> &pkarr::Keypair {
&self.0
}
}
#[wasm_bindgen]
pub struct PublicKey(pkarr::PublicKey);
#[wasm_bindgen]
impl PublicKey {
#[wasm_bindgen]
/// 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]
/// Returns the z-base32 encoding of this public key
pub fn z32(&self) -> String {
self.0.to_string()
}
#[wasm_bindgen(js_name = "from")]
/// @throws
pub fn try_from(value: JsValue) -> Result<PublicKey, JsValue> {
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(Error::Pkarr)?,
))
}
}
impl PublicKey {
pub fn as_inner(&self) -> &pkarr::PublicKey {
&self.0
}
}

48
pubky/src/wasm/pkarr.rs Normal file
View File

@@ -0,0 +1,48 @@
use reqwest::StatusCode;
pub use pkarr::{PublicKey, SignedPacket};
use crate::error::Result;
use crate::PubkyClient;
// TODO: Add an in memory cache of packets
impl PubkyClient {
//TODO: migrate to pkarr::PkarrRelayClient
pub(crate) async fn pkarr_resolve(
&self,
public_key: &PublicKey,
) -> Result<Option<SignedPacket>> {
//TODO: Allow multiple relays in parallel
let relay = self.pkarr_relays.first().expect("initialized with relays");
let res = self
.http
.get(format!("{relay}/{}", public_key))
.send()
.await?;
if res.status() == StatusCode::NOT_FOUND {
return Ok(None);
};
// TODO: guard against too large responses.
let bytes = res.bytes().await?;
let existing = SignedPacket::from_relay_payload(public_key, &bytes)?;
Ok(Some(existing))
}
pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> {
let relay = self.pkarr_relays.first().expect("initialized with relays");
self.http
.put(format!("{relay}/{}", signed_packet.public_key()))
.body(signed_packet.to_relay_payload())
.send()
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,6 @@
use pubky_common::session;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Session(pub(crate) session::Session);