mirror of
https://github.com/aljazceru/pubky-core.git
synced 2026-01-22 15:34:21 +01:00
357
Cargo.lock
generated
357
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}")]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
61
pubky-homeserver/src/routes/pkarr.rs
Normal file
61
pubky-homeserver/src/routes/pkarr.rs
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
5
pubky/pkg/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
nodejs/*
|
||||
browser.js
|
||||
coverage
|
||||
node_modules
|
||||
package-lock.json
|
||||
21
pubky/pkg/LICENSE
Normal file
21
pubky/pkg/LICENSE
Normal 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
76
pubky/pkg/README.md
Normal 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
1
pubky/pkg/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nodejs/pubky.js'
|
||||
44
pubky/pkg/package.json
Normal file
44
pubky/pkg/package.json
Normal 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
30
pubky/pkg/test/auth.js
Normal 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
13
pubky/pkg/test/keys.js
Normal 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
46
pubky/pkg/test/public.js
Normal 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")
|
||||
65
pubky/src/bin/bundle_pubky_npm.rs
Normal file
65
pubky/src/bin/bundle_pubky_npm.rs
Normal 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
59
pubky/src/bin/patch.mjs
Normal 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);
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]))
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
128
pubky/src/native.rs
Normal 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
150
pubky/src/shared/auth.rs
Normal 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
3
pubky/src/shared/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod pkarr;
|
||||
pub mod public;
|
||||
@@ -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
114
pubky/src/shared/public.rs
Normal 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
124
pubky/src/wasm.rs
Normal 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
42
pubky/src/wasm/http.rs
Normal 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
72
pubky/src/wasm/keys.rs
Normal 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
48
pubky/src/wasm/pkarr.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
6
pubky/src/wasm/session.rs
Normal file
6
pubky/src/wasm/session.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use pubky_common::session;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Session(pub(crate) session::Session);
|
||||
Reference in New Issue
Block a user