diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bdf4daa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to pkarr client and server will be documented in this file. + +## [Unreleased] + +### Added + +- Add `SignedPacket::builder()` and convenient methods to create `A`, `AAAA`, `CNAME`, `TXT`, `SVCB`, and `HTTPS` records. +- Add `SignedPacket::all_resource_records()` to access all resource records without accessing the dns packet. +- Use `pubky_timestamp::Timestamp` +- Impl `PartialEq, Eq` for `SignedPacket`. +- Impl `From` for `CacheKey`. +- Add `SignedPacket::serialize` and `SignedPacket::deserialize`. +- Derive `serde::Serialize` and `serde::Deserialize` for `SignedPacket`. +- Add `pkarr::LmdbCache` for persistent cache using lmdb. +- Add `pkarr.pubky.org` as an extra default Relay and Resolver. +- Add feature `endpoints` to resolve `HTTPS` and `SVCB` endpoints over Pkarr +- Add feature `reqwest-resolve` to create a custom `reqwest::dns::Resolve` implementation from `Client` and `relay::client::Client` +- Add feature `tls` to create `rustls::ClientConfig` from `Client` and `relay::client::Client` and create `rustls::ServerCongif` from `KeyPair`. +- Add feature `reqwest-builder` to create a `reqwest::ClientBuilder` from `Client` and `relay::client::Client` using custom dns resolver and preconfigured rustls client config. + +### Changed + +- replace `z32` with *(base32)*. +- `SignedPacket::last_seen` is a `Timestamp` instead of u64. +- make `rand` non-optional, and remove the feature flag. +- replace `ureq` with `reqwest` to work with HTTP/2 relays, and Wasm. +- update `mainline` to v3.0.0 +- `Client::shutdown` and `Client::shutdown_sync` are now idempotent and return `()`. +- `Client::resolve`, `Client::resolve_sync` and `relay::Client::resolve` return expired cached `SignedPacket` _before_ making query to the network (Relays/Resolvers/Dht). +- Export `Settings` as client builder. +- Update `simple-dns` so you can't use `Name::new("@")`, instead you should use `Name::new(".")`, `SignedPacket::resource_records("@")` still works. + +### Removed + +- Remvoed `relay_client_web`, replaced with *(pkarr::relay::Client)*. +- Removed `SignedPacket::from_packet`. +- Removed `SignedPacket::packet` getter. diff --git a/Cargo.lock b/Cargo.lock index d5546d2..d33260e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -43,43 +43,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arc-swap" @@ -99,15 +99,24 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -116,15 +125,42 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "aws-lc-rs" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7c2840b66236045acd2607d5866e274380afd87ef99d6226e961e2cb47df45" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] [[package]] name = "axum" -version = "0.7.5" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -146,9 +182,9 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -156,9 +192,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -169,7 +205,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -195,25 +231,31 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tower", + "tower 0.4.13", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.22.1" @@ -226,6 +268,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -258,30 +323,58 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.1.11" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.15" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -289,9 +382,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -301,9 +394,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -313,15 +406,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" @@ -333,16 +441,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -351,9 +449,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -373,6 +471,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -432,7 +536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -479,6 +583,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "document-features" version = "0.2.10" @@ -497,6 +612,12 @@ dependencies = [ "phf", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -528,12 +649,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -542,9 +697,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -574,14 +729,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" dependencies = [ "nonempty", - "thiserror", + "thiserror 1.0.69", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -594,9 +755,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -604,15 +765,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -621,15 +782,28 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -638,15 +812,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -656,9 +830,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -672,6 +846,22 @@ dependencies = [ "slab", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + [[package]] name = "generic-array" version = "0.14.7" @@ -697,9 +887,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "governor" @@ -723,9 +919,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -740,12 +936,41 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -754,9 +979,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "heed" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "620033c8c8edfd2f53e6f99a30565eb56a33b42c468e3ad80e21d85fb93bafb0" +checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ "bitflags", "byteorder", @@ -792,6 +1017,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "1.1.0" @@ -828,9 +1062,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -840,9 +1074,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -856,61 +1090,238 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -920,11 +1331,27 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" -version = "0.2.155" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] [[package]] name = "libredox" @@ -936,6 +1363,18 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" @@ -944,9 +1383,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lmdb-master-sys" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de7e761853c15ca72821d9f928d7bb123ef4c05377c4e7ab69fa1c742f91d24" +checksum = "472c3760e2a8d0f61f322fb36788021bb36d573c502b50fa3e2bcaac3ec326c9" dependencies = [ "cc", "doxygen-rs", @@ -971,18 +1410,19 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" [[package]] name = "mainline" -version = "2.0.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +checksum = "0e18c8b0210572062a02c4de8c448865f4ca89824c4ac7da64a0c2669ea2c405" dependencies = [ "bytes", "crc", + "document-features", "ed25519-dalek", "flume", "lru", @@ -991,7 +1431,7 @@ dependencies = [ "serde_bencode", "serde_bytes", "sha1_smol", - "thiserror", + "thiserror 2.0.3", "tracing", ] @@ -1023,22 +1463,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "minicov" -version = "0.3.5" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169" -dependencies = [ - "cc", - "walkdir", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -1053,11 +1489,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "mockito" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b34bd91b9e5c5b06338d392463e1318d683cf82ec3d3af4014609be6e2108d" +checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" dependencies = [ "assert-json-diff", "bytes", @@ -1092,6 +1534,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nonempty" version = "0.7.0" @@ -1116,18 +1568,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "overload" @@ -1145,6 +1597,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1168,6 +1626,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1218,18 +1682,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", @@ -1238,9 +1702,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1250,8 +1714,13 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "2.2.0" +version = "3.0.0" dependencies = [ + "anyhow", + "axum", + "axum-server", + "base32", + "byteorder", "bytes", "clap", "document-features", @@ -1259,22 +1728,33 @@ dependencies = [ "ed25519-dalek", "flume", "futures", + "futures-lite", + "genawaiter", + "getrandom", + "heed", "js-sys", + "libc", "lru", "mainline", "mockito", + "once_cell", + "postcard", + "pubky-timestamp", "rand", + "reqwest", + "rustls", + "rustls-webpki", "self_cell", + "serde", + "sha1_smol", "simple-dns", - "thiserror", + "thiserror 2.0.3", + "tokio", + "tokio-rustls", "tracing", "tracing-subscriber", - "ureq", - "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", - "web-sys", - "z32", + "webpki-roots", ] [[package]] @@ -1284,17 +1764,17 @@ dependencies = [ "anyhow", "axum", "axum-server", - "byteorder", "bytes", "clap", "dirs-next", "governor", - "heed", "http", + "httpdate", "pkarr", + "pubky-timestamp", "rustls", "serde", - "thiserror", + "thiserror 2.0.3", "tokio", "toml", "tower-http", @@ -1315,9 +1795,22 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] [[package]] name = "ppv-lite86" @@ -1328,15 +1821,39 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] +[[package]] +name = "pubky-timestamp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084b6e5bfcc186781b71257d636b660f20e94bb588c3ba52393fd9faf7a7bfda" +dependencies = [ + "document-features", + "getrandom", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + [[package]] name = "quanta" version = "0.12.3" @@ -1352,11 +1869,63 @@ dependencies = [ "winapi", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls", + "socket2", + "thiserror 2.0.3", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.3", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1393,43 +1962,43 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.1.0" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ "bitflags", ] [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -1443,13 +2012,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -1460,9 +2029,51 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] [[package]] name = "ring" @@ -1485,21 +2096,47 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -1511,26 +2148,29 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1538,9 +2178,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -1548,21 +2188,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1583,9 +2208,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.207" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -1611,9 +2236,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.207" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -1622,9 +2247,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1644,9 +2269,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -1721,9 +2346,9 @@ checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" [[package]] name = "simple-dns" -version = "0.6.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +checksum = "b8f1740a36513fc97c5309eb1b8e8f108b0e95899c66c23fd7259625d4fdb686" dependencies = [ "bitflags", ] @@ -1751,9 +2376,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1787,6 +2412,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" @@ -1801,9 +2432,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.74" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -1818,9 +2449,12 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synchronoise" @@ -1831,20 +2465,51 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" -version = "1.0.63" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", @@ -1861,6 +2526,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1878,9 +2553,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -1918,9 +2593,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -1952,9 +2627,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -1973,6 +2648,21 @@ dependencies = [ "futures-util", "pin-project", "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", "tokio", "tower-layer", "tower-service", @@ -1981,15 +2671,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "bitflags", "bytes", "http", "http-body", - "http-body-util", "pin-project-lite", "tower-layer", "tower-service", @@ -2010,25 +2699,25 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tower_governor" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" +checksum = "aea939ea6cfa7c4880f3e7422616624f97a567c16df67b53b11f0d03917a8e46" dependencies = [ "axum", "forwarded-header-value", "governor", "http", "pin-project", - "thiserror", - "tower", + "thiserror 1.0.69", + "tower 0.5.1", "tracing", ] [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -2038,9 +2727,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -2049,9 +2738,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -2070,9 +2759,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -2087,31 +2776,22 @@ dependencies = [ ] [[package]] -name = "typenum" -version = "1.17.0" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "unicode-bidi" -version = "0.3.15" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" @@ -2119,32 +2799,29 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" -dependencies = [ - "base64", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots", -] - [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2164,13 +2841,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "walkdir" -version = "2.5.0" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "same-file", - "winapi-util", + "try-lock", ] [[package]] @@ -2181,9 +2857,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", "once_cell", @@ -2192,9 +2868,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", "log", @@ -2207,21 +2883,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2229,9 +2906,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -2242,53 +2919,49 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] -name = "wasm-bindgen-test" -version = "0.3.43" +name = "web-sys" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9" +checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" dependencies = [ - "console_error_panic_hook", "js-sys", - "minicov", - "scoped-tls", "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", ] [[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.43" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "web-sys" -version = "0.3.70" +name = "webpki-roots" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ - "js-sys", - "wasm-bindgen", + "rustls-pki-types", ] [[package]] -name = "webpki-roots" -version = "0.26.3" +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ - "rustls-pki-types", + "either", + "home", + "once_cell", + "rustix", ] [[package]] @@ -2308,19 +2981,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "winapi-util" -version = "0.1.9" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-sys 0.59.0", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] [[package]] name = "windows-sys" @@ -2472,18 +3166,48 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] -name = "z32" -version = "1.1.1" +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb37266251c28b03d08162174a91c3a092e3bd4f476f8205ee1c507b78b7bdc" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zerocopy" @@ -2506,8 +3230,51 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/README.md b/README.md index 0b338fb..73bd88d 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ The simplest possible streamlined integration between the Domain Name System and Where we are going, this [https://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy](https://app.pkarr.org/?pk=o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy) resolves everywhere! ## TLDR -- To publish resource records for your key, sign a small encoded DNS packet (<= 1000 bytes) and publish it on the [Mainline DHT](https://en.wikipedia.org/wiki/Mainline_DHT) (through a relay if necessary). -- To resolve some key's resources, applications query the DHT directly, or through a relay, and verify the signature themselves. -- The DHT drops records after a few hours, so users, their friends, or service providers should periodically republish their records to the DHT. -- Clients and Pkarr servers cache records extensively using the `TTL` values in them to minimize DHT traffic as much as possible for improved scalability and reliability. -- Existing applications unaware of Pkarr can still resolve Pkarr TLDs, if the DNS server they query recognize Pkarr TLDs and use Mainline as a parallel root server to ICANN. +- To publish resource records for your key, sign a small encoded DNS packet (<= 1000 bytes) and publish it on the DHT (through a relay if necessary). +- To resolve some key's resources, applications query the DHT directly, or through a [relay](./design/relays.md), and verify the signature themselves. +- Clients and Pkarr servers cache records extensively and minimize DHT traffic as much as possible for improved scalability. +- The DHT drops records after a few hours, so users, their friends, or service providers should periodically republish their records to the DHT. Also Pkarr servers could republish records recently requested, to keep popular records alive too. +- Optional: Existing applications unaware of Pkarr can still function if the user added a Pkarr-aware DNS servers to their operating system DNS servers. ## DEMO Try the [web app demo](https://app.pkarr.org). -Or if you prefer Rust [Examples](./pkarr/examples) +Or if you prefer Rust [Examples](./pkarr/examples/README.md) ## TOC - [Architecture](#Architecture) diff --git a/design/relays.md b/design/relays.md index 143f5a2..d4d5235 100644 --- a/design/relays.md +++ b/design/relays.md @@ -38,6 +38,7 @@ On receiving a PUT request, the relay server should: ``` GET /:z-base32-encoded-key HTTP/2 +If-Modified-Since: Fri, 18 Oct 2024 13:24:21 GMT ``` #### Response @@ -48,11 +49,13 @@ Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, PUT, OPTIONS Content-Type: application/pkarr.org/relays#payload Cache-Control: public, max-age=300 +Last-Modified: Fri, 18 Oct 2024 13:24:21 GMT ``` `Cache-Control` header would help browsers reduce their reliance on the relay, the `max-age` should be set to be the minimum `ttl` in the resource records in the packet or some minimum ttl chosen by the relay. +`If-Modified-Since` can be sent by the client to avoid downloading packets they already have, when the relay responds with `304 Not Modified`. Body is described at [Payload](#Payload) encoding section. diff --git a/pkarr/Cargo.toml b/pkarr/Cargo.toml index 53c020f..083db85 100644 --- a/pkarr/Cargo.toml +++ b/pkarr/Cargo.toml @@ -1,72 +1,113 @@ [package] name = "pkarr" -version = "2.2.0" +version = "3.0.0" authors = ["Nuh "] edition = "2021" description = "Public-Key Addressable Resource Records (Pkarr); publish and resolve DNS records over Mainline DHT" license = "MIT" repository = "https://git.pkarr.org" keywords = ["mainline", "dht", "dns", "decentralized", "identity"] +categories = ["network-programming"] [dependencies] -bytes = "1.7.1" -document-features = "0.2.8" -ed25519-dalek = "2.0.0" -self_cell = "1.0.2" -simple-dns = "0.6.1" -thiserror = "1.0.49" -tracing = "0.1.40" -z32 = "1.1.1" -rand = { version = "0.8.5", optional = true } -lru = { version = "0.12.3", default-features = false } -flume = { version = "0.11.0", features = ["select", "eventual-fairness"], default-features = false } +base32 = "0.5.1" +bytes = "1.9.0" +ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +self_cell = "1.0.4" +simple-dns = "0.9.1" +thiserror = "2.0.3" +tracing = "0.1.41" +dyn-clone = "1.0.17" +document-features = "0.2.10" +rand = "0.8.5" +once_cell = {version = "1.20.2", default-features = false } +lru = { version = "0.12.5", default-features = false } +flume = { version = "0.11.1", features = ["select", "eventual-fairness", "async"], default-features = false , optional = true } + +# feat: relay dependencies +tokio = { version = "1.41.1", optional = true, default-features = false } +# inherited from feat:dht as well. +sha1_smol = { version = "1.0.1", optional = true } + +# feat: serde dependencies +serde = { version = "1.0.215", features = ["derive"], optional = true } + +# feat: endpoints dependencies +futures-lite = { version = "2.5.0", default-features = false, features= ["std"], optional = true } +genawaiter = { version = "0.99.1", default-features = false, features = ["futures03"], optional = true } + +# feat: reqwest-builder +webpki-roots = { version = "0.26.7", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -# Dht client dependencies: -mainline = { version = "2.0.1", optional = true } -dyn-clone = { version = "1.0.17", optional = true } +pubky-timestamp = { version = "0.2.0", default-features = false } + +# feat: dht dependencies +mainline = { version = "4.1.0", optional = true } + +# feat: relay dependencies +reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls"], optional = true } -# Relay client dependencies -ureq = { version = "2.10", default-features = false, features = ["tls"], optional = true } +# feat: tls +rustls = { version = "0.23", default-features = false, features = ["ring"], optional = true } +webpki = { package = "rustls-webpki", version = "0.102", optional = true } + +# feat: lmdb-cache defendencies +heed = { version = "0.20.5", default-features = false, optional = true } +byteorder = { version = "1.5.0", default-features = false, optional = true } +libc = { version = "0.2.167", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -futures = "0.3.29" -js-sys = "0.3.69" -wasm-bindgen = "0.2.92" -wasm-bindgen-futures = "0.4.42" -web-sys = { version = "0.3.69", features = [ - "console", - "Request", - "RequestInit", - "RequestMode", - "Response", - "Window", -] } +pubky-timestamp = { version = "0.2.0", default-features = false, features = ["httpdate"] } +js-sys = "0.3.74" +futures = "0.3.31" +getrandom = { version = "0.2", features = ["js"] } +reqwest = { version = "0.12.9", default-features = false } +sha1_smol = "1.0.1" +wasm-bindgen-futures = "0.4.47" [dev-dependencies] -futures = "0.3.29" +anyhow = "1.0.93" +axum = "0.7.9" +axum-server = { version = "0.7.1", features = ["tls-rustls-no-provider"] } +postcard = { version = "1.1.1", features = ["alloc"] } +tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] } +tokio-rustls = "0.26.0" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -clap = { version = "4.4.8", features = ["derive"] } -mockito = "1.4.0" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = "0.3.42" +clap = { version = "4.5.21", features = ["derive"] } +mockito = "1.6.1" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } [features] -## Use [PkarrClient] -dht = ["dep:mainline", "dep:dyn-clone"] -## Use [Keypair::random] -rand = ["dep:rand", "ed25519-dalek/rand_core"] -## Use async versions of [PkarrClient] and/or [PkarrRelayClient] -async = ["flume/async"] -## Use [PkarrRelayClient] -relay = ["dep:ureq"] +# Clients +## Use [client::dht::Client] +dht = ["dep:mainline", "dep:flume"] +## Use [client::relay::Client] +relay = ["dep:reqwest", "dep:tokio", "dep:sha1_smol", "dep:flume"] + +## Derive serde Serialize/Deserialize for PublicKey +serde = ["dep:serde", "pubky-timestamp/serde", "pubky-timestamp/httpdate"] + +# Extra +## Use [crate::extra::lmdb-cache::LmdbCache] +lmdb-cache = ["dep:heed", "dep:byteorder", "dep:libc"] +## Use [extra::endpoints::EndpointsResolver] trait implementation for [Client] and [client::relay::Client] +endpoints = ["dep:futures-lite", "dep:genawaiter"] +## Use [reqwest::dns::Resolve] trait implementation for [Client] and [client::relay::Client] +reqwest-resolve = ["dep:reqwest", "endpoints"] +## Use [rustls::ClientConfig] from [Client] and [client::relay::Client] for e2ee transport to Pkarr endpoints +tls = ["rustls", "ed25519-dalek/pkcs8", "dep:webpki"] +## Create a [reqwest::ClientBuilder] from [Client] or [client::relay::Client] +reqwest-builder = ["tls", "reqwest-resolve"] + ## Use all features -full = ["dht", "async", "relay", "rand"] +full = ["dht", "relay", "serde", "endpoints", "lmdb-cache", "reqwest-resolve", "tls", "reqwest-builder"] -default = ["dht", "rand"] +default = ["full"] [package.metadata.docs.rs] all-features = true + +[lints.clippy] +unwrap_used = "deny" diff --git a/pkarr/README.md b/pkarr/README.md index 37ae8b3..e02292f 100644 --- a/pkarr/README.md +++ b/pkarr/README.md @@ -9,3 +9,10 @@ Publish and resolve DNS packets over Mainline DHT. ## Get started Check the [Examples](https://github.com/Nuhvi/pkarr/tree/main/pkarr/examples). + +## WebAssembly support + +This version of Pkarr assumes that you are running Wasm in a JavaScript environment, +and using the Relays clients, so you can't use it in Wasi for example, nor can you +use some Wasi bindings to use the DHT directly. If you really need Wasi support, please +open an issue on `https://git.pkarr.org`. diff --git a/pkarr/clippy.toml b/pkarr/clippy.toml new file mode 100644 index 0000000..154626e --- /dev/null +++ b/pkarr/clippy.toml @@ -0,0 +1 @@ +allow-unwrap-in-tests = true diff --git a/pkarr/examples/README.md b/pkarr/examples/README.md index 77f2a57..d56f4a2 100644 --- a/pkarr/examples/README.md +++ b/pkarr/examples/README.md @@ -6,8 +6,36 @@ cargo run --example publish ``` +or to use a Relay client: + +```sh +cargo run --features relay --example publish +``` + ## Resolve ```sh cargo run --example resolve ``` + +or to use a Relay client: + +```sh +cargo run --features relay --example resolve +``` + +## HTTP + +Run an HTTP server listening on a Pkarr key + +```sh +cargo run --features endpoints --example http-serve +``` + +An HTTPs url will be printend with the Pkarr key as the TLD, paste in another terminal window: + +```sh +cargo run --features reqwest-resolve --example http-get +``` + +And you should see a `Hello, World!` response. diff --git a/pkarr/examples/http-get.rs b/pkarr/examples/http-get.rs new file mode 100644 index 0000000..b806d86 --- /dev/null +++ b/pkarr/examples/http-get.rs @@ -0,0 +1,42 @@ +//! Make an HTTP request over to a Pkarr address using Reqwest + +use reqwest::Method; +use tracing::Level; +use tracing_subscriber; + +use clap::Parser; + +use pkarr::{Client, PublicKey}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Url to GET from + url: String, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); + + let cli = Cli::parse(); + let url = cli.url; + + let reqwest = if PublicKey::try_from(url.as_str()).is_err() { + // If it is not a Pkarr domain, use normal Reqwest + reqwest::Client::new() + } else { + let client = Client::builder().build()?; + + reqwest::ClientBuilder::from(client).build()? + }; + + println!("GET {url}.."); + let response = reqwest.request(Method::GET, &url).send().await?; + + let body = response.text().await?; + + println!("{body}"); + + Ok(()) +} diff --git a/pkarr/examples/http-serve.rs b/pkarr/examples/http-serve.rs new file mode 100644 index 0000000..facb8b1 --- /dev/null +++ b/pkarr/examples/http-serve.rs @@ -0,0 +1,76 @@ +//! Run an HTTP server listening on a Pkarr domain +//! +//! This server will _not_ be accessible from other networks +//! unless the provided IP is public and the port number is forwarded. + +use tracing::Level; +use tracing_subscriber; + +use axum::{routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; + +use std::net::{SocketAddr, ToSocketAddrs}; +use std::sync::Arc; + +use clap::Parser; + +use pkarr::{dns::rdata::SVCB, Client, Keypair, SignedPacket}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// IP address to listen on + ip: String, + /// Port number to listen no + port: u16, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); + + let cli = Cli::parse(); + + let addr = format!("{}:{}", cli.ip, cli.port) + .to_socket_addrs()? + .next() + .ok_or(anyhow::anyhow!( + "Could not convert IP and port to socket addresses" + ))?; + + let keypair = Keypair::random(); + + let client = Client::builder().build()?; + + // Run a server on Pkarr + println!("Server listening on {addr}"); + + // You should republish this every time the socket address change + // and once an hour otherwise. + publish_server_pkarr(&client, &keypair, &addr).await; + + println!("Server running on https://{}", keypair.public_key()); + + let server = axum_server::bind_rustls( + addr, + RustlsConfig::from_config(Arc::new(keypair.to_rpk_rustls_server_config())), + ); + + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + server.serve(app.into_make_service()).await?; + + Ok(()) +} + +async fn publish_server_pkarr(client: &Client, keypair: &Keypair, socket_addr: &SocketAddr) { + let mut svcb = SVCB::new(0, ".".try_into().expect("infallible")); + svcb.set_port(socket_addr.port()); + + let signed_packet = SignedPacket::builder() + .https(".".try_into().unwrap(), svcb, 60 * 60) + .address(".".try_into().unwrap(), socket_addr.ip(), 60 * 60) + .sign(&keypair) + .unwrap(); + + client.publish(&signed_packet).await.unwrap(); +} diff --git a/pkarr/examples/publish.rs b/pkarr/examples/publish.rs index 99bc2c7..edc4b9c 100644 --- a/pkarr/examples/publish.rs +++ b/pkarr/examples/publish.rs @@ -11,32 +11,33 @@ use tracing_subscriber; use std::time::Instant; -use pkarr::{dns, Keypair, PkarrClient, Result, SignedPacket}; +use pkarr::{Keypair, SignedPacket}; -fn main() -> Result<()> { +#[cfg(feature = "relay")] +use pkarr::client::relay::Client; +#[cfg(not(feature = "relay"))] +use pkarr::Client; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_max_level(Level::DEBUG) + .with_env_filter("pkarr") .init(); - let client = PkarrClient::builder().build().unwrap(); + let client = Client::builder().build()?; let keypair = Keypair::random(); - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into()?), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet)?; + let signed_packet = SignedPacket::builder() + .txt("_foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair)?; let instant = Instant::now(); println!("\nPublishing {} ...", keypair.public_key()); - match client.publish(&signed_packet) { + match client.publish(&signed_packet).await { Ok(()) => { println!( "\nSuccessfully published {} in {:?}", diff --git a/pkarr/examples/resolve.rs b/pkarr/examples/resolve.rs index 1a1f2bc..9a04972 100644 --- a/pkarr/examples/resolve.rs +++ b/pkarr/examples/resolve.rs @@ -11,10 +11,15 @@ use std::{ time::{Duration, Instant}, }; -use pkarr::{PkarrClient, PublicKey}; - use clap::Parser; +use pkarr::PublicKey; + +#[cfg(feature = "relay")] +use pkarr::client::relay::Client; +#[cfg(not(feature = "relay"))] +use pkarr::Client; + #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { @@ -22,7 +27,8 @@ struct Cli { public_key: String, } -fn main() { +#[tokio::main] +async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_max_level(Level::DEBUG) .with_env_filter("pkarr") @@ -36,23 +42,25 @@ fn main() { .try_into() .expect("Invalid zbase32 encoded key"); - let client = PkarrClient::builder().build().unwrap(); + let client = Client::builder().build()?; println!("Resolving Pkarr: {} ...", cli.public_key); println!("\n=== COLD LOOKUP ==="); - resolve(&client, &public_key); + resolve(&client, &public_key).await; // loop { sleep(Duration::from_secs(1)); println!("=== SUBSEQUENT LOOKUP ==="); - resolve(&client, &public_key) + resolve(&client, &public_key).await; // } + + Ok(()) } -fn resolve(client: &PkarrClient, public_key: &PublicKey) { +async fn resolve(client: &Client, public_key: &PublicKey) { let start = Instant::now(); - match client.resolve(public_key) { + match client.resolve(public_key).await { Ok(Some(signed_packet)) => { println!( "\nResolved in {:?} milliseconds {}", diff --git a/pkarr/src/base/cache.rs b/pkarr/src/base/cache.rs new file mode 100644 index 0000000..0b7b4f2 --- /dev/null +++ b/pkarr/src/base/cache.rs @@ -0,0 +1,113 @@ +//! Trait and inmemory implementation of [Cache] + +use dyn_clone::DynClone; +use lru::LruCache; +use std::fmt::Debug; +use std::num::NonZeroUsize; +use std::sync::{Arc, Mutex}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +use mainline::MutableItem; + +use crate::SignedPacket; + +/// The sha1 hash of the [crate::PublicKey] used as the key in [Cache]. +pub type CacheKey = [u8; 20]; + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +impl From<&crate::PublicKey> for CacheKey { + fn from(public_key: &crate::PublicKey) -> CacheKey { + MutableItem::target_from_key(public_key.as_bytes(), &None).into() + } +} + +#[cfg(any(target_arch = "wasm32", all(not(feature = "dht"), feature = "relay")))] +impl From<&crate::PublicKey> for CacheKey { + fn from(public_key: &crate::PublicKey) -> CacheKey { + let mut encoded = vec![]; + + encoded.extend(public_key.as_bytes()); + + let mut hasher = sha1_smol::Sha1::new(); + hasher.update(&encoded); + hasher.digest().bytes() + } +} + +#[cfg(any(target_arch = "wasm32", feature = "dht", feature = "relay"))] +impl From for CacheKey { + fn from(value: crate::PublicKey) -> Self { + (&value).into() + } +} + +pub trait Cache: Debug + Send + Sync + DynClone { + fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Puts [SignedPacket] into cache. + fn put(&self, key: &CacheKey, signed_packet: &SignedPacket); + /// Reads [SignedPacket] from cache, while moving it to the head of the LRU list. + fn get(&self, key: &CacheKey) -> Option; + /// Reads [SignedPacket] from cache, without changing the LRU list. + /// + /// Used for internal reads that are not initiated by the user directly, + /// like comparing an received signed packet with existing one. + /// + /// Useful to implement differently from [Cache::get], if you are implementing + /// persistent cache where writes are slower than reads. + /// + /// Otherwise it will just use [Cache::get]. + fn get_read_only(&self, key: &CacheKey) -> Option { + self.get(key) + } +} + +dyn_clone::clone_trait_object!(Cache); + +/// A thread safe wrapper around [lru::LruCache] +#[derive(Debug, Clone)] +pub struct InMemoryCache { + inner: Arc>>, +} + +impl InMemoryCache { + /// Creats a new `LRU` cache that holds at most `cap` items. + pub fn new(capacity: NonZeroUsize) -> Self { + Self { + inner: Arc::new(Mutex::new(LruCache::new(capacity))), + } + } +} + +impl Cache for InMemoryCache { + fn len(&self) -> usize { + self.inner.lock().expect("mutex lock").len() + } + + /// Puts [SignedPacket], if a version of the packet already exists, + /// and it has the same [SignedPacket::as_bytes], then only [SignedPacket::last_seen] will be + /// updated, otherwise the input will be cloned. + fn put(&self, key: &CacheKey, signed_packet: &SignedPacket) { + let mut lock = self.inner.lock().expect("mutex lock"); + + match lock.get_mut(key) { + Some(existing) => { + if existing.as_bytes() == signed_packet.as_bytes() { + // just refresh the last_seen + existing.set_last_seen(signed_packet.last_seen()) + } else { + lock.put(*key, signed_packet.clone()); + } + } + None => { + lock.put(*key, signed_packet.clone()); + } + } + } + + fn get(&self, key: &CacheKey) -> Option { + self.inner.lock().expect("mutex lock").get(key).cloned() + } +} diff --git a/pkarr/src/keys.rs b/pkarr/src/base/keys.rs similarity index 58% rename from pkarr/src/keys.rs rename to pkarr/src/base/keys.rs index 40a9627..6e95b3a 100644 --- a/pkarr/src/keys.rs +++ b/pkarr/src/base/keys.rs @@ -1,20 +1,29 @@ //! Utility structs for Ed25519 keys. -use crate::{Error, Result}; -use ed25519_dalek::{SecretKey, Signature, Signer, SigningKey, Verifier, VerifyingKey}; -#[cfg(feature = "rand")] +#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] +use ed25519_dalek::pkcs8::{Document, EncodePrivateKey, EncodePublicKey}; +use ed25519_dalek::{ + SecretKey, Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey, +}; use rand::rngs::OsRng; +#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] +use rustls::{ + crypto::ring::sign::any_eddsa_type, pki_types::CertificateDer, + server::AlwaysResolvesServerRawPublicKeys, sign::CertifiedKey, ServerConfig, +}; use std::{ fmt::{self, Debug, Display, Formatter}, hash::Hash, }; -#[derive(Clone)] +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, PartialEq, Eq)] /// Ed25519 keypair to sign dns [Packet](crate::SignedPacket)s. pub struct Keypair(SigningKey); impl Keypair { - #[cfg(feature = "rand")] pub fn random() -> Keypair { let mut csprng = OsRng; let signing_key: SigningKey = SigningKey::generate(&mut csprng); @@ -30,11 +39,8 @@ impl Keypair { self.0.sign(message) } - pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<()> { - self.0 - .verify(message, signature) - .map_err(|_| Error::InvalidEd25519Signature)?; - Ok(()) + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SignatureError> { + self.0.verify(message, signature) } pub fn secret_key(&self) -> SecretKey { @@ -52,16 +58,67 @@ impl Keypair { pub fn to_uri_string(&self) -> String { self.public_key().to_uri_string() } + + #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] + /// Return a RawPublicKey certified key according to [RFC 7250](https://tools.ietf.org/html/rfc7250) + /// useful to use with [rustls::ConfigBuilder::with_cert_resolver] and [rustls::server::AlwaysResolvesServerRawPublicKeys] + pub fn to_rpk_certified_key(&self) -> CertifiedKey { + let client_private_key = any_eddsa_type( + &self + .0 + .to_pkcs8_der() + .expect("Keypair::to_rpk_certificate: convert secret key to pkcs8 der") + .as_bytes() + .into(), + ) + .expect("Keypair::to_rpk_certificate: convert KeyPair to rustls SigningKey"); + + let client_public_key = client_private_key + .public_key() + .expect("Keypair::to_rpk_certificate: load SPKI"); + let client_public_key_as_cert = CertificateDer::from(client_public_key.to_vec()); + + CertifiedKey::new(vec![client_public_key_as_cert], client_private_key) + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] + /// Create a [rustls::ServerConfig] using this keypair as a RawPublicKey certificate according to [RFC 7250](https://tools.ietf.org/html/rfc7250) + pub fn to_rpk_rustls_server_config(&self) -> ServerConfig { + let cert_resolver = + AlwaysResolvesServerRawPublicKeys::new(self.to_rpk_certified_key().into()); + + ServerConfig::builder_with_provider(rustls::crypto::ring::default_provider().into()) + .with_safe_default_protocol_versions() + .expect("version supported by ring") + .with_no_client_auth() + .with_cert_resolver(std::sync::Arc::new(cert_resolver)) + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] +impl From for ServerConfig { + /// calls [Keypair::to_rpk_rustls_server_config] + fn from(keypair: Keypair) -> Self { + keypair.to_rpk_rustls_server_config() + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] +impl From<&Keypair> for ServerConfig { + /// calls [Keypair::to_rpk_rustls_server_config] + fn from(keypair: &Keypair) -> Self { + keypair.to_rpk_rustls_server_config() + } } /// Ed25519 public key to verify a signature over dns [Packet](crate::SignedPacket)s. /// -/// It can formatted to and parsed from a [zbase32](z32) string. +/// It can formatted to and parsed from a z-base32 string. #[derive(Clone, Eq, PartialEq, Hash)] -pub struct PublicKey(VerifyingKey); +pub struct PublicKey(pub(crate) VerifyingKey); impl PublicKey { - /// Format the public key as [zbase32](z32) string. + /// Format the public key as z-base32 string. pub fn to_z32(&self) -> String { self.to_string() } @@ -72,11 +129,8 @@ impl PublicKey { } /// Verify a signature over a message. - pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<()> { - self.0 - .verify(message, signature) - .map_err(|_| Error::InvalidEd25519Signature)?; - Ok(()) + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SignatureError> { + self.0.verify(message, signature) } /// Return a reference to the underlying [VerifyingKey] @@ -93,34 +147,53 @@ impl PublicKey { pub fn as_bytes(&self) -> &[u8; 32] { self.0.as_bytes() } + + #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] + pub fn to_public_key_der(&self) -> Document { + self.0.to_public_key_der().expect("to_public_key_der") + } +} + +impl AsRef for Keypair { + fn as_ref(&self) -> &Keypair { + self + } +} + +impl AsRef for PublicKey { + fn as_ref(&self) -> &PublicKey { + self + } } impl TryFrom<&[u8]> for PublicKey { - type Error = Error; + type Error = PublicKeyError; fn try_from(bytes: &[u8]) -> Result { let bytes_32: &[u8; 32] = bytes .try_into() - .map_err(|_| Error::InvalidPublicKeyLength(bytes.len()))?; + .map_err(|_| PublicKeyError::InvalidPublicKeyLength(bytes.len()))?; Ok(Self( - VerifyingKey::from_bytes(bytes_32).map_err(|_| Error::InvalidEd25519PublicKey)?, + VerifyingKey::from_bytes(bytes_32) + .map_err(|_| PublicKeyError::InvalidEd25519PublicKey)?, )) } } impl TryFrom<&[u8; 32]> for PublicKey { - type Error = Error; + type Error = PublicKeyError; fn try_from(public: &[u8; 32]) -> Result { Ok(Self( - VerifyingKey::from_bytes(public).map_err(|_| Error::InvalidEd25519PublicKey)?, + VerifyingKey::from_bytes(public) + .map_err(|_| PublicKeyError::InvalidEd25519PublicKey)?, )) } } impl TryFrom<&str> for PublicKey { - type Error = Error; + type Error = PublicKeyError; /// Convert the TLD in a `&str` to a [PublicKey]. /// @@ -135,7 +208,8 @@ impl TryFrom<&str> for PublicKey { /// - `https://foo.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy.#hash` /// - `https://foo@bar.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy.?q=v` /// - `https://foo@bar.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy.:8888?q=v` - fn try_from(s: &str) -> Result { + /// - `https://yg4gxe7z1r7mr6orids9fh95y7gxhdsxjqi6nngsxxtakqaxr5no.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy` + fn try_from(s: &str) -> Result { let mut s = s; if s.len() > 52 { @@ -182,26 +256,34 @@ impl TryFrom<&str> for PublicKey { } } - let bytes = z32::decode(s.as_bytes())?; + let bytes = if let Some(v) = base32::decode(base32::Alphabet::Z, s) { + Ok(v) + } else { + Err(PublicKeyError::InvalidPublicKeyEncoding) + }?; let verifying_key = VerifyingKey::try_from(bytes.as_slice()) - .map_err(|_| Error::InvalidPublicKeyLength(bytes.len()))?; + .map_err(|_| PublicKeyError::InvalidPublicKeyLength(bytes.len()))?; Ok(PublicKey(verifying_key)) } } impl TryFrom for PublicKey { - type Error = Error; + type Error = PublicKeyError; - fn try_from(s: String) -> Result { + fn try_from(s: String) -> Result { s.as_str().try_into() } } impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", z32::encode(self.0.as_bytes())) + write!( + f, + "{}", + base32::encode(base32::Alphabet::Z, self.0.as_bytes()) + ) } } @@ -223,6 +305,46 @@ impl Debug for PublicKey { } } +#[cfg(feature = "serde")] +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.to_bytes(); + bytes.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: [u8; 32] = Deserialize::deserialize(deserializer)?; + + (&bytes).try_into().map_err(serde::de::Error::custom) + } +} + +#[derive(thiserror::Error, Debug)] +/// Errors while trying to create a [PublicKey] +pub enum PublicKeyError { + #[error("Invalid PublicKey length, expected 32 bytes but got: {0}")] + InvalidPublicKeyLength(usize), + + #[error("Invalid Ed25519 publickey; Cannot decompress Edwards point")] + InvalidEd25519PublicKey, + + #[error("Invalid PublicKey encoding")] + InvalidPublicKeyEncoding, + + #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")] + // DNS packet endocded and compressed is larger than 1000 bytes + PacketTooLarge(usize), +} + #[cfg(test)] mod tests { use super::*; @@ -391,4 +513,76 @@ mod tests { let public_key: PublicKey = str.try_into().unwrap(); assert_eq!(public_key.verifying_key().as_bytes(), &expected); } + + #[cfg(feature = "serde")] + #[test] + fn serde() { + let str = "yg4gxe7z1r7mr6orids9fh95y7gxhdsxjqi6nngsxxtakqaxr5no"; + let expected = [ + 1, 180, 103, 163, 183, 145, 58, 178, 122, 4, 168, 237, 242, 243, 251, 7, 76, 254, 14, + 207, 75, 171, 225, 8, 214, 123, 227, 133, 59, 15, 38, 197, + ]; + + let public_key: PublicKey = str.try_into().unwrap(); + + let bytes = postcard::to_allocvec(&public_key).unwrap(); + + assert_eq!(bytes, expected) + } + + #[test] + fn from_uri_multiple_pkarr() { + // Should only catch the TLD. + + let str = + "https://o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy.yg4gxe7z1r7mr6orids9fh95y7gxhdsxjqi6nngsxxtakqaxr5no"; + let expected = [ + 1, 180, 103, 163, 183, 145, 58, 178, 122, 4, 168, 237, 242, 243, 251, 7, 76, 254, 14, + 207, 75, 171, 225, 8, 214, 123, 227, 133, 59, 15, 38, 197, + ]; + + let public_key: PublicKey = str.try_into().unwrap(); + assert_eq!(public_key.verifying_key().as_bytes(), &expected); + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] + #[test] + fn pkcs8() { + let str = "yg4gxe7z1r7mr6orids9fh95y7gxhdsxjqi6nngsxxtakqaxr5no"; + let public_key: PublicKey = str.try_into().unwrap(); + + let der = public_key.to_public_key_der(); + + assert_eq!( + der.as_bytes(), + [ + // Algorithm and other stuff. + 48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0, // + // Key + 1, 180, 103, 163, 183, 145, 58, 178, 122, 4, 168, 237, 242, 243, 251, 7, 76, 254, + 14, 207, 75, 171, 225, 8, 214, 123, 227, 133, 59, 15, 38, 197, + ] + ) + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] + #[test] + fn certificate() { + use rustls::SignatureAlgorithm; + + let keypair = Keypair::from_secret_key(&[0; 32]); + + let certified_key = keypair.to_rpk_certified_key(); + + assert_eq!(certified_key.key.algorithm(), SignatureAlgorithm::ED25519); + + assert_eq!( + certified_key.end_entity_cert().unwrap().as_ref(), + [ + 48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0, 59, 106, 39, 188, 206, 182, 164, 45, + 98, 163, 168, 208, 42, 111, 13, 115, 101, 50, 21, 119, 29, 226, 67, 166, 58, 192, + 72, 161, 139, 89, 218, 41, + ] + ) + } } diff --git a/pkarr/src/base/mod.rs b/pkarr/src/base/mod.rs new file mode 100644 index 0000000..ad1894c --- /dev/null +++ b/pkarr/src/base/mod.rs @@ -0,0 +1,5 @@ +//! Basic typest, traits and utilities. + +pub mod cache; +pub mod keys; +pub mod signed_packet; diff --git a/pkarr/src/base/signed_packet.rs b/pkarr/src/base/signed_packet.rs new file mode 100644 index 0000000..1581289 --- /dev/null +++ b/pkarr/src/base/signed_packet.rs @@ -0,0 +1,1071 @@ +//! Signed DNS packet + +use crate::{Keypair, PublicKey}; +use bytes::{Bytes, BytesMut}; +use ed25519_dalek::{Signature, SignatureError}; +use self_cell::self_cell; +use simple_dns::{ + rdata::{RData, A, AAAA, HTTPS, SVCB, TXT}, + Name, Packet, ResourceRecord, SimpleDnsError, CLASS, +}; +use std::{ + char, + fmt::{self, Display, Formatter}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use pubky_timestamp::Timestamp; + +#[derive(Debug, Default)] +pub struct SignedPacketBuilder(Vec>); + +impl SignedPacketBuilder { + /// Insert a [ResourceRecord] + pub fn record(mut self, record: ResourceRecord<'_>) -> Self { + self.0.push(record.into_owned()); + self + } + + /// Insert any type of [RData] + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn rdata(self, name: Name<'_>, rdata: RData, ttl: u32) -> Self { + self.record(ResourceRecord::new(name.to_owned(), CLASS::IN, ttl, rdata)) + } + + /// Insert an `A` record. + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn a(self, name: Name<'_>, address: Ipv4Addr, ttl: u32) -> Self { + self.rdata( + name, + RData::A(A { + address: address.into(), + }), + ttl, + ) + } + + /// Insert an `AAAA` record. + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn aaaa(self, name: Name<'_>, address: Ipv6Addr, ttl: u32) -> Self { + self.rdata( + name, + RData::AAAA(AAAA { + address: address.into(), + }), + ttl, + ) + } + + /// Insert an `A` or `AAAA` record. + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn address(self, name: Name<'_>, address: IpAddr, ttl: u32) -> Self { + match address { + IpAddr::V4(addr) => self.a(name, addr, ttl), + IpAddr::V6(addr) => self.aaaa(name, addr, ttl), + } + } + + /// Insert a `CNAME` record. + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn cname(self, name: Name<'_>, cname: Name<'_>, ttl: u32) -> Self { + self.rdata(name, RData::CNAME(cname.into()), ttl) + } + + /// Insert a `TXT` record. + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn txt(self, name: Name<'_>, text: TXT<'_>, ttl: u32) -> Self { + self.rdata(name, RData::TXT(text), ttl) + } + + /// Insert an `HTTPS` record + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn https(self, name: Name<'_>, svcb: SVCB, ttl: u32) -> Self { + self.rdata(name, RData::HTTPS(HTTPS(svcb)), ttl) + } + + /// Insert an `SVCB record + /// + /// You can set the name to `.` to point ot the Apex + /// (the public key, of the keypair used in [Self::sign]) + pub fn svcb(self, name: Name<'_>, svcb: SVCB, ttl: u32) -> Self { + self.rdata(name, RData::SVCB(svcb), ttl) + } + + /// Alias to [Self::sign] + pub fn build(self, keypair: &Keypair) -> Result { + self.sign(keypair) + } + + /// Create a [Packet] from the [ResourceRecord]s inserted so far and sign + /// it with the given [Keypair]. + /// + /// Read more about how names will be normalized in [SignedPacket::from_answers]. + pub fn sign(self, keypair: &Keypair) -> Result { + SignedPacket::from_answers(keypair, &self.0) + } +} + +const DOT: char = '.'; + +self_cell!( + struct Inner { + owner: Bytes, + + #[covariant] + dependent: Packet, + } + + impl{Debug, PartialEq, Eq} +); + +impl Inner { + fn try_from_parts( + public_key: &PublicKey, + signature: &Signature, + timestamp: u64, + encoded_packet: &Bytes, + ) -> Result { + // Create the inner bytes from timestamp> + let mut bytes = BytesMut::with_capacity(encoded_packet.len() + 104); + + bytes.extend_from_slice(public_key.as_bytes()); + bytes.extend_from_slice(&signature.to_bytes()); + bytes.extend_from_slice(×tamp.to_be_bytes()); + bytes.extend_from_slice(encoded_packet); + + Self::try_new(bytes.into(), |bytes| Packet::parse(&bytes[104..])) + } + + fn try_from_bytes(bytes: Bytes) -> Result { + Inner::try_new(bytes.to_owned(), |bytes| Packet::parse(&bytes[104..])) + } +} + +#[derive(Debug, PartialEq, Eq)] +/// Signed DNS packet +pub struct SignedPacket { + inner: Inner, + last_seen: Timestamp, +} + +impl SignedPacket { + pub const MAX_BYTES: u64 = 1104; + + /// Create a [SignedPacket] using a builder. + /// + /// ``` + /// use pkarr::{SignedPacket, Keypair, dns::rdata::SVCB}; + /// + /// let keypair = Keypair::random(); + /// + /// let signed_packet = SignedPacket::builder() + /// // A record + /// .address( + /// "_derp_region.iroh.".try_into().unwrap(), + /// "1.1.1.1".parse().unwrap(), + /// 30, + /// ) + /// // AAAA record + /// .address( + /// "_derp_region.iroh.".try_into().unwrap(), + /// "3002:0bd6:0000:0000:0000:ee00:0033:6778".parse().unwrap(), + /// 30, + /// ) + /// // CNAME record + /// .cname( + /// "subdomain.".try_into().unwrap(), + /// "example.com".try_into().unwrap(), + /// 30 + /// ) + /// // TXT record + /// .txt( + /// "_proto".try_into().unwrap(), + /// "foo=bar".try_into().unwrap(), + /// 30 + /// ) + /// // HTTPS record + /// .https( + /// // You can make a record at the Apex (at the same TLD as your public key) + /// ".".try_into().unwrap(), + /// SVCB::new(0, "https.example.com".try_into().unwrap()), + /// 3600, + /// ) + /// // SVCB record + /// .https( + /// ".".try_into().unwrap(), + /// SVCB::new(0, "https.example.com".try_into().unwrap()), + /// 3600, + /// ) + /// .sign(&keypair) + /// .unwrap(); + /// ``` + pub fn builder() -> SignedPacketBuilder { + SignedPacketBuilder::default() + } + + /// Creates a [SignedPacket] from a [PublicKey] and the [relays](https://github.com/Nuhvi/pkarr/blob/main/design/relays.md) payload. + pub fn from_relay_payload( + public_key: &PublicKey, + payload: &Bytes, + ) -> Result { + let mut bytes = BytesMut::with_capacity(payload.len() + 32); + + bytes.extend_from_slice(public_key.as_bytes()); + bytes.extend_from_slice(payload); + + SignedPacket::from_bytes(&bytes.into()) + } + + /// Creates a new [SignedPacket] from a [Keypair] and [ResourceRecord]s as the `answers` + /// section of a DNS [Packet]. + /// + /// It will also normalize the names of the [ResourceRecord]s to be relative to the origin, + /// which would be the z-base32 encoded [PublicKey] of the [Keypair] used to sign the Packet. + /// + /// If any name is empty or just a `.`, it will be normalized to the public key of the keypair. + fn from_answers( + keypair: &Keypair, + answers: &[ResourceRecord<'_>], + ) -> Result { + let mut packet = Packet::new_reply(0); + + let origin = keypair.public_key().to_z32(); + + // Normalize names to the origin TLD + let normalized_names: Vec = answers + .iter() + .map(|answer| normalize_name(&origin, answer.name.to_string())) + .collect(); + + answers.iter().enumerate().for_each(|(index, answer)| { + packet.answers.push(ResourceRecord::new( + Name::new_unchecked(&normalized_names[index]).to_owned(), + answer.class, + answer.ttl, + answer.rdata.clone(), + )) + }); + + // Encode the packet as `v` and verify its length + let encoded_packet: Bytes = packet.build_bytes_vec_compressed()?.into(); + + if encoded_packet.len() > 1000 { + return Err(SignedPacketError::PacketTooLarge(encoded_packet.len())); + } + + let timestamp = Timestamp::now().into(); + + let signature = keypair.sign(&signable(timestamp, &encoded_packet)); + + Ok(SignedPacket { + inner: Inner::try_from_parts( + &keypair.public_key(), + &signature, + timestamp, + &encoded_packet, + )?, + last_seen: Timestamp::now(), + }) + } + + // === Getters === + + /// Returns the serialized signed packet: + /// `<32 bytes public_key><64 bytes signature><8 bytes big-endian timestamp in microseconds>` + pub fn as_bytes(&self) -> &Bytes { + self.inner.borrow_owner() + } + + /// Returns a serialized representation of this [SignedPacket] including + /// the [SignedPacket::last_seen] timestamp followed by the returned value from [SignedPacket::as_bytes]. + pub fn serialize(&self) -> Bytes { + let mut bytes = Vec::with_capacity(SignedPacket::MAX_BYTES as usize); + bytes.extend_from_slice(&self.last_seen.to_bytes()); + bytes.extend_from_slice(self.as_bytes()); + + bytes.into() + } + + /// Deserialize [SignedPacket] from a serialized version for persistent storage using + /// [SignedPacket::serialize]. + /// + /// If deserializing the [SignedPacket::last_seen] failed, or is far in the future, + /// it will be unwrapped to default, i.e the UNIX_EPOCH. + /// + /// That is useful for backwards compatibility if you + /// ever stored the [SignedPacket::last_seen] as Little Endian in previous versions. + pub fn deserialize(bytes: &[u8]) -> Result { + let mut last_seen = Timestamp::try_from(&bytes[0..8]).unwrap_or_default(); + + if last_seen > (Timestamp::now() + 60_000_000) { + last_seen = Timestamp::from(0) + } + + Ok(SignedPacket { + inner: Inner::try_from_bytes(bytes[8..].to_owned().into())?, + last_seen, + }) + } + + /// Returns a slice of the serialized [SignedPacket] omitting the leading public_key, + /// to be sent as a request/response body to or from [relays](https://github.com/Nuhvi/pkarr/blob/main/design/relays.md). + pub fn to_relay_payload(&self) -> Bytes { + self.inner.borrow_owner().slice(32..) + } + + /// Returns the [PublicKey] of the signer of this [SignedPacket] + pub fn public_key(&self) -> PublicKey { + PublicKey::try_from(&self.inner.borrow_owner()[0..32]).expect("SignedPacket::public_key()") + } + + /// Returns the [Signature] of the the bencoded sequence number concatenated with the + /// encoded and compressed packet, as defined in [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html) + pub fn signature(&self) -> Signature { + Signature::try_from(&self.inner.borrow_owner()[32..96]).expect("SignedPacket::signature()") + } + + /// Returns the timestamp in microseconds since the [UNIX_EPOCH](std::time::UNIX_EPOCH). + /// + /// This timestamp is authored by the controller of the keypair, + /// and it is trusted as a way to order which packets where authored after which, + /// but it shouldn't be used for caching for example, instead, use [Self::last_seen] + /// which is set when you create a new packet. + pub fn timestamp(&self) -> Timestamp { + let bytes = self.inner.borrow_owner(); + let slice: [u8; 8] = bytes[96..104] + .try_into() + .expect("SignedPacket::timestamp()"); + + u64::from_be_bytes(slice).into() + } + + /// Returns the DNS [Packet] compressed and encoded. + pub fn encoded_packet(&self) -> Bytes { + self.inner.borrow_owner().slice(104..) + } + + /// Return the DNS [Packet]. + pub(crate) fn packet(&self) -> &Packet { + self.inner.borrow_dependent() + } + + /// Unix last_seen time in microseconds + pub fn last_seen(&self) -> &Timestamp { + &self.last_seen + } + + // === Setters === + + /// Set the [Self::last_seen] property + pub fn set_last_seen(&mut self, last_seen: &Timestamp) { + self.last_seen = last_seen.into(); + } + + // === Public Methods === + + /// Set the [Self::last_seen] to the current system time + pub fn refresh(&mut self) { + self.last_seen = Timestamp::now(); + } + + /// Return whether this [SignedPacket] is more recent than the given one. + /// If the timestamps are erqual, the one with the largest value is considered more recent. + /// Usefel for determining which packet contains the latest information from the Dht. + /// Assumes that both packets have the same [PublicKey], you shouldn't compare packets from + /// different keys. + pub fn more_recent_than(&self, other: &SignedPacket) -> bool { + // In the rare ocasion of timestamp collission, + // we use the one with the largest value + if self.timestamp() == other.timestamp() { + self.encoded_packet() > other.encoded_packet() + } else { + self.timestamp() > other.timestamp() + } + } + + /// Returns true if both packets have the same timestamp and packet, + /// and only differ in [Self::last_seen] + pub fn is_same_as(&self, other: &SignedPacket) -> bool { + self.as_bytes() == other.as_bytes() + } + + /// Return and iterator over the [ResourceRecord]s in the Answers section of the DNS [Packet] + /// that matches the given name. The name will be normalized to the origin TLD of this packet. + /// + /// You can use `@` to filter the resource records at the Apex (the public key). + pub fn resource_records(&self, name: &str) -> impl Iterator { + let origin = self.public_key().to_z32(); + let normalized_name = normalize_name(&origin, name.to_string()); + self.all_resource_records() + .filter(move |rr| rr.name.to_string() == normalized_name) + } + + /// Similar to [resource_records](SignedPacket::resource_records), but filters out + /// expired records, according the the [Self::last_seen] value and each record's `ttl`. + pub fn fresh_resource_records(&self, name: &str) -> impl Iterator { + let origin = self.public_key().to_z32(); + let normalized_name = normalize_name(&origin, name.to_string()); + + self.all_resource_records() + .filter(move |rr| rr.name.to_string() == normalized_name && rr.ttl > self.elapsed()) + } + + /// Returns all resource records in this packet + pub fn all_resource_records(&self) -> impl Iterator { + self.packet().answers.iter() + } + + /// calculates the remaining seconds by comparing the [Self::ttl] (clamped by `min` and `max`) + /// to the [Self::last_seen]. + /// + /// # Panics + /// + /// Panics if `min` < `max` + pub fn expires_in(&self, min: u32, max: u32) -> u32 { + match self.ttl(min, max).overflowing_sub(self.elapsed()) { + (_, true) => 0, + (ttl, false) => ttl, + } + } + + /// Returns the smallest `ttl` in the resource records, + /// calmped with `min` and `max`. + /// + /// # Panics + /// + /// Panics if `min` < `max` + pub fn ttl(&self, min: u32, max: u32) -> u32 { + self.packet() + .answers + .iter() + .map(|rr| rr.ttl) + .min() + .map_or(min, |v| v.clamp(min, max)) + } + + /// Returns whether or not this packet is considered expired according to + /// a given `min` and `max` TTLs + pub fn is_expired(&self, min: u32, max: u32) -> bool { + self.expires_in(min, max) == 0 + } + + // === Private Methods === + + /// Time since the [Self::last_seen] in seconds + fn elapsed(&self) -> u32 { + ((Timestamp::now().as_u64() - self.last_seen.as_u64()) / 1_000_000) as u32 + } + + /// Creates a [Self] from the serialized representation: + /// `<32 bytes public_key><64 bytes signature><8 bytes big-endian timestamp in microseconds>` + /// + /// Performs the following validations: + /// - Bytes minimum length + /// - Validates the PublicKey + /// - Verifies the Signature + /// - Validates the DNS packet encoding + /// + /// You can skip all these validations by using [Self::from_bytes_unchecked] instead. + /// + /// You can use [Self::from_relay_payload] instead if you are receiving a response from an HTTP relay. + fn from_bytes(bytes: &Bytes) -> Result { + if bytes.len() < 104 { + return Err(SignedPacketError::InvalidSignedPacketBytesLength( + bytes.len(), + )); + } + if (bytes.len() as u64) > SignedPacket::MAX_BYTES { + return Err(SignedPacketError::PacketTooLarge(bytes.len())); + } + let public_key = PublicKey::try_from(&bytes[..32])?; + let signature = Signature::from_bytes( + bytes[32..96] + .try_into() + .expect("SignedPacket::from_bytes(); Signature from 64 bytes"), + ); + let timestamp = u64::from_be_bytes( + bytes[96..104] + .try_into() + .expect("SignedPacket::from_bytes(); Timestamp from 8 bytes"), + ); + + let encoded_packet = &bytes.slice(104..); + + public_key.verify(&signable(timestamp, encoded_packet), &signature)?; + + Ok(SignedPacket { + inner: Inner::try_from_bytes(bytes.to_owned())?, + last_seen: Timestamp::now(), + }) + } + + /// Useful for cloning a [SignedPacket], or cerating one from a previously checked bytes, + /// like ones stored on disk or in a database. + fn from_bytes_unchecked(bytes: &Bytes, last_seen: impl Into) -> SignedPacket { + SignedPacket { + inner: Inner::try_from_bytes(bytes.to_owned()) + .expect("called SignedPacket::from_bytes_unchecked on invalid bytes"), + last_seen: last_seen.into(), + } + } +} + +fn signable(timestamp: u64, v: &Bytes) -> Bytes { + let mut signable = format!("3:seqi{}e1:v{}:", timestamp, v.len()).into_bytes(); + signable.extend(v); + signable.into() +} + +fn normalize_name(origin: &str, name: String) -> String { + let name = if name.ends_with(DOT) { + name[..name.len() - 1].to_string() + } else { + name + }; + + let parts: Vec<&str> = name.split('.').collect(); + let last = *parts.last().unwrap_or(&""); + + if last == origin { + // Already normalized. + return name.to_string(); + } + + if last == "@" || last.is_empty() { + // Shorthand of origin + return origin.to_string(); + } + + format!("{}.{}", name, origin) +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +use mainline::MutableItem; + +use super::keys::PublicKeyError; + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +impl From<&SignedPacket> for MutableItem { + fn from(s: &SignedPacket) -> Self { + let seq = s.timestamp().as_u64() as i64; + let packet = s.inner.borrow_owner().slice(104..); + + Self::new_signed_unchecked( + s.public_key().to_bytes(), + s.signature().to_bytes(), + packet, + seq, + None, + ) + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +impl TryFrom<&MutableItem> for SignedPacket { + type Error = SignedPacketError; + + fn try_from(i: &MutableItem) -> Result { + let public_key = PublicKey::try_from(i.key())?; + let seq = *i.seq() as u64; + let signature: Signature = i.signature().into(); + + Ok(Self { + inner: Inner::try_from_parts(&public_key, &signature, seq, i.value())?, + last_seen: Timestamp::now(), + }) + } +} + +impl AsRef<[u8]> for SignedPacket { + /// Returns the SignedPacket as a bytes slice with the format: + /// `<6 bytes timestamp in microseconds>` + fn as_ref(&self) -> &[u8] { + self.inner.borrow_owner() + } +} + +impl Clone for SignedPacket { + fn clone(&self) -> Self { + Self::from_bytes_unchecked(self.as_bytes(), self.last_seen) + } +} + +impl Display for SignedPacket { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "SignedPacket ({}):\n last_seen: {} seconds ago\n timestamp: {},\n signature: {}\n records:\n", + &self.public_key(), + &self.elapsed(), + &self.timestamp(), + &self.signature(), + )?; + + for answer in &self.packet().answers { + writeln!( + f, + " {} IN {} {}\n", + &answer.name, + &answer.ttl, + match &answer.rdata { + RData::A(A { address }) => format!("A {}", Ipv4Addr::from(*address)), + RData::AAAA(AAAA { address }) => format!("AAAA {}", Ipv6Addr::from(*address)), + #[allow(clippy::to_string_in_format_args)] + RData::CNAME(name) => format!("CNAME {}", name.to_string()), + RData::TXT(txt) => { + format!( + "TXT \"{}\"", + txt.clone() + .try_into() + .unwrap_or("__INVALID_TXT_VALUE_".to_string()) + ) + } + _ => format!("{:?}", answer.rdata), + } + )?; + } + + writeln!(f)?; + + Ok(()) + } +} + +// === Serialization === + +#[cfg(feature = "serde")] +impl Serialize for SignedPacket { + /// Serialize a [SignedPacket] for persistent storage. + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.serialize().serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for SignedPacket { + /// Deserialize a [SignedPacket] from persistent storage. + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: Vec = Deserialize::deserialize(deserializer)?; + + SignedPacket::deserialize(&bytes).map_err(serde::de::Error::custom) + } +} + +#[derive(thiserror::Error, Debug)] +/// Errors trying to parse or create a [SignedPacket] +pub enum SignedPacketError { + #[error(transparent)] + SignatureError(#[from] SignatureError), + + #[error(transparent)] + PublicKeyError(#[from] PublicKeyError), + + #[error(transparent)] + /// Transparent [simple_dns::SimpleDnsError] + DnsError(#[from] simple_dns::SimpleDnsError), + + #[error("Invalid SignedPacket bytes length, expected at least 104 bytes but got: {0}")] + /// Serialized signed packets are `<32 bytes publickey><64 bytes signature><8 bytes + /// timestamp>`. + InvalidSignedPacketBytesLength(usize), + + #[error("Invalid relay payload size, expected at least 72 bytes but got: {0}")] + /// Relay api http-body should be `<64 bytes signature><8 bytes timestamp> + /// `. + InvalidRelayPayloadSize(usize), + + #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")] + // DNS packet endocded and compressed is larger than 1000 bytes + PacketTooLarge(usize), +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use simple_dns::rdata::CNAME; + + use super::*; + + use crate::{DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL}; + + #[test] + fn normalize_names() { + let origin = "ed4mn3aoazuf1ahpy9rz1nyswhukbj5483ryefwkue7fbp3egkzo"; + + assert_eq!(normalize_name(origin, ".".to_string()), origin); + assert_eq!(normalize_name(origin, "@".to_string()), origin); + assert_eq!(normalize_name(origin, "@.".to_string()), origin); + assert_eq!(normalize_name(origin, origin.to_string()), origin); + assert_eq!( + normalize_name(origin, "_derp_region.irorh".to_string()), + format!("_derp_region.irorh.{}", origin) + ); + assert_eq!( + normalize_name(origin, format!("_derp_region.irorh.{}", origin)), + format!("_derp_region.irorh.{}", origin) + ); + assert_eq!( + normalize_name(origin, format!("_derp_region.irorh.{}.", origin)), + format!("_derp_region.irorh.{}", origin) + ); + } + + #[test] + fn sign_verify() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .address( + "_derp_region.iroh.".try_into().unwrap(), + "1.1.1.1".parse().unwrap(), + 30, + ) + .sign(&keypair) + .unwrap(); + + assert!(SignedPacket::from_relay_payload( + &signed_packet.public_key(), + &signed_packet.to_relay_payload() + ) + .is_ok()); + } + + #[test] + fn from_too_large_bytes() { + let keypair = Keypair::random(); + + let bytes = Bytes::from(vec![0; 1073]); + let error = SignedPacket::from_relay_payload(&keypair.public_key(), &bytes); + + assert!(error.is_err()); + } + + #[test] + fn from_too_large_packet() { + let keypair = Keypair::random(); + + let mut builder = SignedPacket::builder(); + + for _ in 0..100 { + builder = builder.address( + "_derp_region.iroh.".try_into().unwrap(), + "1.1.1.1".parse().unwrap(), + 30, + ); + } + + let error = builder.sign(&keypair); + + assert!(error.is_err()); + } + + #[test] + fn resource_records_iterator() { + let keypair = Keypair::random(); + + let target = ResourceRecord::new( + Name::new("_derp_region.iroh.").unwrap(), + simple_dns::CLASS::IN, + 30, + RData::A(A { + address: Ipv4Addr::new(1, 1, 1, 1).into(), + }), + ); + + let signed_packet = SignedPacket::builder() + .record(target.clone()) + .address( + "something-else".try_into().unwrap(), + "1.1.1.1".parse().unwrap(), + 30, + ) + .sign(&keypair) + .unwrap(); + + let iter = signed_packet.resource_records("_derp_region.iroh"); + assert_eq!(iter.count(), 1); + + for record in signed_packet.resource_records("_derp_region.iroh") { + assert_eq!(record.rdata, target.rdata); + } + } + + #[cfg(feature = "dht")] + #[test] + fn to_mutable() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .address( + "_derp_region.iroh.".try_into().unwrap(), + "1.1.1.1".parse().unwrap(), + 30, + ) + .sign(&keypair) + .unwrap(); + + let item: MutableItem = (&signed_packet).into(); + let seq = signed_packet.timestamp().as_u64() as i64; + + let expected = MutableItem::new( + keypair.secret_key().into(), + signed_packet + .packet() + .build_bytes_vec_compressed() + .unwrap() + .into(), + seq, + None, + ); + + assert_eq!(item, expected); + } + + #[test] + fn compressed_names() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .cname(".".try_into().unwrap(), "foobar".try_into().unwrap(), 30) + .cname(".".try_into().unwrap(), "foobar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + assert_eq!( + signed_packet + .resource_records("@") + .map(|r| r.rdata.clone()) + .collect::>(), + vec![ + RData::CNAME(CNAME("foobar".try_into().unwrap())), + RData::CNAME(CNAME("foobar".try_into().unwrap())) + ] + ) + } + + #[test] + fn to_bytes_from_bytes() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("_foo".try_into().unwrap(), "hello".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + let bytes = signed_packet.as_bytes(); + let from_bytes = SignedPacket::from_bytes(bytes).unwrap(); + assert_eq!(signed_packet.as_bytes(), from_bytes.as_bytes()); + let from_bytes2 = SignedPacket::from_bytes_unchecked(bytes, &signed_packet.last_seen); + assert_eq!(signed_packet.as_bytes(), from_bytes2.as_bytes()); + + let public_key = keypair.public_key(); + let payload = signed_packet.to_relay_payload(); + let from_relay_payload = SignedPacket::from_relay_payload(&public_key, &payload).unwrap(); + assert_eq!(signed_packet.as_bytes(), from_relay_payload.as_bytes()); + } + + #[test] + fn clone() { + let keypair = Keypair::random(); + + let signed = SignedPacket::builder() + .txt("_foo".try_into().unwrap(), "hello".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + let cloned = signed.clone(); + + assert_eq!(cloned.as_bytes(), signed.as_bytes()); + } + + #[test] + fn expires_in_minimum_ttl() { + let keypair = Keypair::random(); + + let mut signed_packet = SignedPacket::builder() + .txt("_foo".try_into().unwrap(), "hello".try_into().unwrap(), 10) + .sign(&keypair) + .unwrap(); + + signed_packet.last_seen -= 20 * 1_000_000_u64; + + assert!( + signed_packet.expires_in(30, u32::MAX) > 0, + "input minimum_ttl is 30 so ttl = 30" + ); + + assert!( + signed_packet.expires_in(0, u32::MAX) == 0, + "input minimum_ttl is 0 so ttl = 10 (smallest in resource records)" + ); + } + + #[test] + fn expires_in_maximum_ttl() { + let keypair = Keypair::random(); + + let mut signed_packet = SignedPacket::builder() + .txt( + "_foo".try_into().unwrap(), + "hello".try_into().unwrap(), + 3 * DEFAULT_MAXIMUM_TTL, + ) + .sign(&keypair) + .unwrap(); + + signed_packet.last_seen -= 2 * (DEFAULT_MAXIMUM_TTL as u64) * 1_000_000; + + assert!( + signed_packet.expires_in(0, DEFAULT_MAXIMUM_TTL) == 0, + "input maximum_ttl is the dfeault 86400 so maximum ttl = 86400" + ); + + assert!( + signed_packet.expires_in(0, 7 * DEFAULT_MAXIMUM_TTL) > 0, + "input maximum_ttl is 7 * 86400 so ttl = 3 * 86400 (smallest in resource records)" + ); + } + + #[test] + fn fresh_resource_records() { + let keypair = Keypair::random(); + + let mut signed_packet = SignedPacket::builder() + .txt("_foo".try_into().unwrap(), "hello".try_into().unwrap(), 30) + .txt("_foo".try_into().unwrap(), "world".try_into().unwrap(), 60) + .sign(&keypair) + .unwrap(); + + signed_packet.last_seen -= 30 * 1_000_000; + + assert_eq!(signed_packet.fresh_resource_records("_foo").count(), 1); + } + + #[test] + fn ttl_empty() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder().sign(&keypair).unwrap(); + + assert_eq!( + signed_packet.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), + 300 + ); + } + + #[test] + fn ttl_with_records_less_than_minimum() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt( + "_foo".try_into().unwrap(), + "hello".try_into().unwrap(), + DEFAULT_MINIMUM_TTL / 2, + ) + .txt( + "_foo".try_into().unwrap(), + "world".try_into().unwrap(), + DEFAULT_MINIMUM_TTL / 4, + ) + .sign(&keypair) + .unwrap(); + + assert_eq!( + signed_packet.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), + DEFAULT_MINIMUM_TTL + ); + + assert_eq!( + signed_packet.ttl(0, DEFAULT_MAXIMUM_TTL), + DEFAULT_MINIMUM_TTL / 4 + ); + } + + #[test] + fn ttl_with_records_more_than_maximum() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt( + "_foo".try_into().unwrap(), + "hello".try_into().unwrap(), + DEFAULT_MAXIMUM_TTL * 2, + ) + .txt( + "_foo".try_into().unwrap(), + "world".try_into().unwrap(), + DEFAULT_MAXIMUM_TTL * 4, + ) + .sign(&keypair) + .unwrap(); + + assert_eq!( + signed_packet.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), + DEFAULT_MAXIMUM_TTL + ); + + assert_eq!( + signed_packet.ttl(0, DEFAULT_MAXIMUM_TTL * 8), + DEFAULT_MAXIMUM_TTL * 2 + ); + } + + #[cfg(feature = "serde")] + #[test] + fn serde() { + use postcard::{from_bytes, to_allocvec}; + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .address( + "_derp_region.iroh.".try_into().unwrap(), + "1.1.1.1".parse().unwrap(), + 30, + ) + .sign(&keypair) + .unwrap(); + + let serialized = to_allocvec(&signed_packet).unwrap(); + let deserialized: SignedPacket = from_bytes(&serialized).unwrap(); + + assert_eq!(deserialized, signed_packet); + + // Backwards compat + { + let mut bytes = vec![]; + + bytes.extend_from_slice(&[210, 1]); + bytes.extend_from_slice(&signed_packet.last_seen().as_u64().to_le_bytes()); + bytes.extend_from_slice(signed_packet.as_bytes()); + + let deserialized: SignedPacket = from_bytes(&bytes).unwrap(); + + assert_eq!(deserialized.as_bytes(), signed_packet.as_bytes()); + assert_eq!(deserialized.last_seen(), &Timestamp::from(0)); + } + } +} diff --git a/pkarr/src/cache.rs b/pkarr/src/cache.rs deleted file mode 100644 index 5179e2a..0000000 --- a/pkarr/src/cache.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Trait and inmemory implementation of [PkarrCache] - -use dyn_clone::DynClone; -use lru::LruCache; -use mainline::Id; -use std::fmt::Debug; -use std::num::NonZeroUsize; -use std::sync::{Arc, Mutex}; - -use crate::SignedPacket; - -/// The sha1 hash of the [crate::PublicKey] used as the key in [PkarrCache]. -pub type PkarrCacheKey = Id; - -pub trait PkarrCache: Debug + Send + Sync + DynClone { - fn len(&self) -> usize; - fn is_empty(&self) -> bool { - self.len() == 0 - } - /// Puts [SignedPacket] into cache. - fn put(&self, key: &PkarrCacheKey, signed_packet: &SignedPacket); - /// Reads [SignedPacket] from cache, while moving it to the head of the LRU list. - fn get(&self, key: &PkarrCacheKey) -> Option; - /// Reads [SignedPacket] from cache, without changing the LRU list. - /// - /// Used for internal reads that are not initiated by the user directly, - /// like comparing an received signed packet with existing one. - /// - /// Useful to implement differently from [PkarrCache::get], if you are implementing - /// persistent cache where writes are slower than reads. - /// - /// Otherwise it will just use [PkarrCache::get]. - fn get_read_only(&self, key: &PkarrCacheKey) -> Option { - self.get(key) - } -} - -dyn_clone::clone_trait_object!(PkarrCache); - -/// A thread safe wrapper around `LruCache` -#[derive(Debug, Clone)] -pub struct InMemoryPkarrCache { - inner: Arc>>, -} - -impl InMemoryPkarrCache { - /// Creats a new `LRU` cache that holds at most `cap` items. - pub fn new(capacity: NonZeroUsize) -> Self { - Self { - inner: Arc::new(Mutex::new(LruCache::new(capacity))), - } - } -} - -impl PkarrCache for InMemoryPkarrCache { - fn len(&self) -> usize { - self.inner.lock().unwrap().len() - } - - /// Puts [SignedPacket], if a version of the packet already exists, - /// and it has the same [SignedPacket::as_bytes], then only [SignedPacket::last_seen] will be - /// updated, otherwise the input will be cloned. - fn put(&self, target: &Id, signed_packet: &SignedPacket) { - let mut lock = self.inner.lock().unwrap(); - - match lock.get_mut(target) { - Some(existing) => { - if existing.as_bytes() == signed_packet.as_bytes() { - // just refresh the last_seen - existing.set_last_seen(signed_packet.last_seen()) - } else { - lock.put(*target, signed_packet.clone()); - } - } - None => { - lock.put(*target, signed_packet.clone()); - } - } - } - - fn get(&self, key: &PkarrCacheKey) -> Option { - self.inner.lock().unwrap().get(key).cloned() - } -} diff --git a/pkarr/src/client.rs b/pkarr/src/client.rs deleted file mode 100644 index c61d5e3..0000000 --- a/pkarr/src/client.rs +++ /dev/null @@ -1,583 +0,0 @@ -//! Pkarr client for publishing and resolving [SignedPacket]s over [mainline]. - -use flume::{Receiver, Sender}; -use mainline::{ - dht::DhtSettings, - rpc::{ - messages, QueryResponse, QueryResponseSpecific, ReceivedFrom, ReceivedMessage, Response, - Rpc, - }, - Id, MutableItem, -}; -use std::{ - collections::HashMap, - net::{SocketAddr, ToSocketAddrs}, - num::NonZeroUsize, - thread, -}; -use tracing::{debug, trace}; - -use crate::{ - cache::{InMemoryPkarrCache, PkarrCache}, - DEFAULT_CACHE_SIZE, DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL, DEFAULT_RESOLVERS, -}; -use crate::{Error, PublicKey, Result, SignedPacket}; - -#[derive(Debug)] -/// [PkarrClient]'s settings -pub struct Settings { - pub dht: DhtSettings, - /// A set of [resolver](https://pkarr.org/resolvers)s - /// to be queried alongside the Dht routing table, to - /// lower the latency on cold starts, and help if the - /// Dht is missing values not't republished often enough. - /// - /// Defaults to [DEFAULT_RESOLVERS] - pub resolvers: Option>, - /// Defaults to [DEFAULT_CACHE_SIZE] - pub cache_size: NonZeroUsize, - /// Used in the `min` parameter in [SignedPacket::expires_in]. - /// - /// Defaults to [DEFAULT_MINIMUM_TTL] - pub minimum_ttl: u32, - /// Used in the `max` parameter in [SignedPacket::expires_in]. - /// - /// Defaults to [DEFAULT_MAXIMUM_TTL] - pub maximum_ttl: u32, - /// Custom [PkarrCache] implementation, defaults to [InMemoryPkarrCache] - pub cache: Option>, -} - -impl Default for Settings { - fn default() -> Self { - Self { - dht: DhtSettings::default(), - cache_size: NonZeroUsize::new(DEFAULT_CACHE_SIZE).unwrap(), - resolvers: Some( - DEFAULT_RESOLVERS - .iter() - .flat_map(|resolver| resolver.to_socket_addrs()) - .flatten() - .collect::>(), - ), - minimum_ttl: DEFAULT_MINIMUM_TTL, - maximum_ttl: DEFAULT_MAXIMUM_TTL, - cache: None, - } - } -} - -#[derive(Debug, Default)] -/// Builder for [PkarrClient] -pub struct PkarrClientBuilder { - settings: Settings, -} - -impl PkarrClientBuilder { - /// Set custom set of [resolvers](Settings::resolvers). - pub fn resolvers(mut self, resolvers: Option>) -> Self { - self.settings.resolvers = resolvers.map(|resolvers| { - resolvers - .iter() - .flat_map(|resolver| resolver.to_socket_addrs()) - .flatten() - .collect::>() - }); - self - } - - /// Set the [Settings::cache_size]. - /// - /// Controls the capacity of [PkarrCache]. - pub fn cache_size(mut self, cache_size: NonZeroUsize) -> Self { - self.settings.cache_size = cache_size; - self - } - - /// Set the [Settings::minimum_ttl] value. - /// - /// Limits how soon a [SignedPacket] is considered expired. - pub fn minimum_ttl(mut self, ttl: u32) -> Self { - self.settings.minimum_ttl = ttl; - self.settings.maximum_ttl = self.settings.maximum_ttl.clamp(ttl, u32::MAX); - self - } - - /// Set the [Settings::maximum_ttl] value. - /// - /// Limits how long it takes before a [SignedPacket] is considered expired. - pub fn maximum_ttl(mut self, ttl: u32) -> Self { - self.settings.maximum_ttl = ttl; - self.settings.minimum_ttl = self.settings.minimum_ttl.clamp(0, ttl); - self - } - - /// Set a custom implementation of [PkarrCache]. - pub fn cache(mut self, cache: Box) -> Self { - self.settings.cache = Some(cache); - self - } - - /// Set [DhtSettings] - pub fn dht_settings(mut self, settings: DhtSettings) -> Self { - self.settings.dht = settings; - self - } - - pub fn build(self) -> Result { - PkarrClient::new(self.settings) - } -} - -#[derive(Clone, Debug)] -/// Pkarr client for publishing and resolving [SignedPacket]s over [mainline]. -pub struct PkarrClient { - pub(crate) address: Option, - pub(crate) sender: Sender, - pub(crate) cache: Box, - pub(crate) minimum_ttl: u32, - pub(crate) maximum_ttl: u32, -} - -impl PkarrClient { - pub fn new(settings: Settings) -> Result { - let (sender, receiver) = flume::bounded(32); - - let rpc = Rpc::new(&settings.dht)?; - - let local_addr = rpc.local_addr(); - - let cache = settings - .cache - .clone() - .unwrap_or(Box::new(InMemoryPkarrCache::new(settings.cache_size))); - let cache_clone = cache.clone(); - - let client = PkarrClient { - address: Some(local_addr), - sender, - cache, - minimum_ttl: settings.minimum_ttl, - maximum_ttl: settings.maximum_ttl, - }; - - thread::Builder::new() - .name("PkarrClient loop".to_string()) - .spawn(move || run(rpc, cache_clone, settings, receiver))?; - - Ok(client) - } - - /// Returns a builder to edit settings before creating PkarrClient. - pub fn builder() -> PkarrClientBuilder { - PkarrClientBuilder::default() - } - - // === Getters === - - /// Returns the local address of the udp socket this node is listening on. - /// - /// Returns `None` if the node is shutdown - pub fn local_addr(&self) -> Option { - self.address - } - - /// Returns a reference to the internal cache. - pub fn cache(&self) -> &dyn PkarrCache { - self.cache.as_ref() - } - - // === Public Methods === - - /// Publishes a [SignedPacket] to the Dht. - /// - /// # Errors - /// - Returns a [Error::DhtIsShutdown] if [PkarrClient::shutdown] was called, or - /// the loop in the actor thread is stopped for any reason (like thread panic). - /// - Returns a [Error::PublishInflight] if the client is currently publishing the same public_key. - /// - Returns a [Error::NotMostRecent] if the provided signed packet is older than most recent. - /// - Returns a [Error::MainlineError] if the Dht received an unexpected error otherwise. - pub fn publish(&self, signed_packet: &SignedPacket) -> Result<()> { - match self.publish_inner(signed_packet)?.recv() { - Ok(Ok(_)) => Ok(()), - Ok(Err(error)) => match error { - mainline::Error::PutQueryIsInflight(_) => Err(Error::PublishInflight), - _ => Err(Error::MainlineError(error)), - }, - // Since we pass this sender to `Rpc::put`, the only reason the sender, - // would be dropped, is if `Rpc` is dropped, which should only happeng on shutdown. - Err(_) => Err(Error::DhtIsShutdown), - } - } - - /// Returns a [SignedPacket] from cache if it is not expired, otherwise, - /// it will query the Dht, and return the first valid response, which may - /// or may not be expired itself. - /// - /// If the Dht was called, in the background, it continues receiving responses - /// and updating the cache with any more recent valid packets it receives. - /// - /// # Errors - /// - Returns a [Error::DhtIsShutdown] if [PkarrClient::shutdown] was called, or - /// the loop in the actor thread is stopped for any reason (like thread panic). - pub fn resolve(&self, public_key: &PublicKey) -> Result> { - Ok(self.resolve_inner(public_key)?.recv().ok()) - } - - /// Shutdown the actor thread loop. - pub fn shutdown(&mut self) -> Result<()> { - let (sender, receiver) = flume::bounded(1); - - self.sender - .send(ActorMessage::Shutdown(sender)) - .map_err(|_| Error::DhtIsShutdown)?; - - receiver.recv()?; - - self.address = None; - - Ok(()) - } - - // === Private Methods === - - pub(crate) fn publish_inner( - &self, - signed_packet: &SignedPacket, - ) -> Result>> { - let mutable_item: MutableItem = (signed_packet).into(); - - if let Some(current) = self.cache.get(mutable_item.target()) { - if current.timestamp() > signed_packet.timestamp() { - return Err(Error::NotMostRecent); - } - }; - - self.cache.put(mutable_item.target(), signed_packet); - - let (sender, receiver) = flume::bounded::>(1); - - self.sender - .send(ActorMessage::Publish(mutable_item, sender)) - .map_err(|_| Error::DhtIsShutdown)?; - - Ok(receiver) - } - - pub(crate) fn resolve_inner(&self, public_key: &PublicKey) -> Result> { - let target = MutableItem::target_from_key(public_key.as_bytes(), &None); - - let (sender, receiver) = flume::bounded::(1); - - let cached_packet = self.cache.get(&target); - - if let Some(ref cached) = cached_packet { - let expires_in = cached.expires_in(self.minimum_ttl, self.maximum_ttl); - - if expires_in > 0 { - debug!(expires_in, "Have fresh signed_packet in cache."); - - sender - .send(cached.clone()) - .map_err(|_| Error::DhtIsShutdown)?; - - return Ok(receiver); - } - - debug!(expires_in, "Have expired signed_packet in cache."); - } else { - debug!("Cache mess"); - } - - self.sender - .send(ActorMessage::Resolve( - target, - sender, - // Sending the `timestamp` of the known cache, help save some bandwith, - // since remote nodes will not send the encoded packet if they don't know - // any more recent versions. - cached_packet.as_ref().map(|cached| cached.timestamp()), - )) - .map_err(|_| Error::DhtIsShutdown)?; - - Ok(receiver) - } -} - -fn run( - mut rpc: Rpc, - cache: Box, - settings: Settings, - receiver: Receiver, -) { - debug!(?settings, "Starting PkarrClient main loop.."); - - let mut server = settings.dht.server; - let mut senders: HashMap>> = HashMap::new(); - - loop { - // === Receive actor messages === - if let Ok(actor_message) = receiver.try_recv() { - match actor_message { - ActorMessage::Shutdown(sender) => { - drop(receiver); - let _ = sender.send(()); - break; - } - ActorMessage::Publish(mutable_item, sender) => { - let target = mutable_item.target(); - - let request = messages::PutRequestSpecific::PutMutable( - messages::PutMutableRequestArguments { - target: *target, - v: mutable_item.value().to_vec(), - k: mutable_item.key().to_vec(), - seq: *mutable_item.seq(), - sig: mutable_item.signature().to_vec(), - salt: None, - cas: None, - }, - ); - - rpc.put(*target, request, Some(sender)) - } - ActorMessage::Resolve(target, sender, most_recent_known_timestamp) => { - if let Some(set) = senders.get_mut(&target) { - set.push(sender); - } else { - senders.insert(target, vec![sender]); - }; - - let request = messages::RequestTypeSpecific::GetValue( - messages::GetValueRequestArguments { - target, - seq: most_recent_known_timestamp.map(|t| t as i64), - // seq: None, - salt: None, - }, - ); - - rpc.get(target, request, None, settings.resolvers.clone()) - } - } - } - - // === Dht Tick === - - let report = rpc.tick(); - - // === Drop senders to done queries === - for id in &report.done_get_queries { - senders.remove(id); - } - - // === Receive and handle incoming messages === - if let Some(ReceivedFrom { from, message }) = &report.received_from { - // match &report.received_from { - // Some(ReceivedFrom { from, message }) => match message { - match message { - // === Responses === - ReceivedMessage::QueryResponse(response) => { - match response { - // === Got Mutable Value === - QueryResponse { - target, - response: QueryResponseSpecific::Value(Response::Mutable(mutable_item)), - } => { - if let Ok(signed_packet) = &SignedPacket::try_from(mutable_item) { - let new_packet = - if let Some(ref cached) = cache.get_read_only(target) { - if signed_packet.more_recent_than(cached) { - debug!( - ?target, - "Received more recent packet than in cache" - ); - Some(signed_packet) - } else { - None - } - } else { - debug!(?target, "Received new packet after cache miss"); - Some(signed_packet) - }; - - if let Some(packet) = new_packet { - cache.put(target, packet); - - if let Some(set) = senders.get(target) { - for sender in set { - let _ = sender.send(packet.clone()); - } - } - } - } - } - // === Got NoMoreRecentValue === - QueryResponse { - target, - response: QueryResponseSpecific::Value(Response::NoMoreRecentValue(seq)), - } => { - if let Some(mut cached) = cache.get_read_only(target) { - if (*seq as u64) == cached.timestamp() { - trace!("Remote node has the a packet with same timestamp, refreshing cached packet."); - - cached.refresh(); - cache.put(target, &cached); - - // Send the found sequence as a timestamp to the caller to decide what to do - // with it. - if let Some(set) = senders.get(target) { - for sender in set { - let _ = sender.send(cached.clone()); - } - } - } - }; - } - // Ignoring errors, as they are logged in `mainline` crate already. - _ => {} - }; - } - // === Requests === - ReceivedMessage::Request((transaction_id, request)) => { - if let Some(server) = server.as_mut() { - server.handle_request(&mut rpc, *from, *transaction_id, request); - } - } - }; - } - } - - debug!("PkarrClient main terminated"); -} - -pub enum ActorMessage { - Publish(MutableItem, Sender>), - Resolve(Id, Sender, Option), - Shutdown(Sender<()>), -} - -#[cfg(test)] -mod tests { - use mainline::Testnet; - - use super::*; - use crate::{dns, Keypair, SignedPacket}; - - #[test] - fn shutdown() { - let testnet = Testnet::new(3); - - let mut a = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - assert_ne!(a.local_addr(), None); - - a.shutdown().unwrap(); - - assert_eq!(a.local_addr(), None); - } - - #[test] - fn publish_resolve() { - let testnet = Testnet::new(10); - - let a = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let _ = a.publish(&signed_packet); - - let b = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - let resolved = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - - let from_cache = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); - assert_eq!(from_cache.last_seen(), resolved.last_seen()); - } - - #[test] - fn thread_safe() { - let testnet = Testnet::new(10); - - let a = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let _ = a.publish(&signed_packet); - - let b = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - thread::spawn(move || { - let resolved = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - - let from_cache = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); - assert_eq!(from_cache.last_seen(), resolved.last_seen()); - }) - .join() - .unwrap(); - } -} diff --git a/pkarr/src/client/dht.rs b/pkarr/src/client/dht.rs new file mode 100644 index 0000000..6c59561 --- /dev/null +++ b/pkarr/src/client/dht.rs @@ -0,0 +1,755 @@ +//! Pkarr client for publishing and resolving [SignedPacket]s over [mainline]. + +use flume::{Receiver, Sender}; +use mainline::{ + errors::PutError, + rpc::{ + messages, QueryResponse, QueryResponseSpecific, ReceivedFrom, ReceivedMessage, Response, + Rpc, + }, + Id, MutableItem, Testnet, +}; +use std::{ + collections::HashMap, + net::{SocketAddr, ToSocketAddrs}, + num::NonZeroUsize, + thread, +}; +use tracing::{debug, trace}; + +use crate::{ + Cache, InMemoryCache, DEFAULT_CACHE_SIZE, DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL, + DEFAULT_RESOLVERS, +}; +use crate::{PublicKey, SignedPacket}; + +#[derive(Debug)] +/// [Client]'s settings +pub struct Settings { + pub(crate) dht_settings: mainline::Settings, + /// A set of [resolver](https://pkarr.org/resolvers)s + /// to be queried alongside the Dht routing table, to + /// lower the latency on cold starts, and help if the + /// Dht is missing values not't republished often enough. + /// + /// Defaults to [DEFAULT_RESOLVERS] + pub(crate) resolvers: Option>, + /// Defaults to [DEFAULT_CACHE_SIZE] + pub(crate) cache_size: NonZeroUsize, + /// Used in the `min` parameter in [SignedPacket::expires_in]. + /// + /// Defaults to [DEFAULT_MINIMUM_TTL] + pub(crate) minimum_ttl: u32, + /// Used in the `max` parameter in [SignedPacket::expires_in]. + /// + /// Defaults to [DEFAULT_MAXIMUM_TTL] + pub(crate) maximum_ttl: u32, + /// Custom [Cache] implementation, defaults to [InMemoryCache] + pub(crate) cache: Option>, +} + +impl Default for Settings { + fn default() -> Self { + Self { + dht_settings: mainline::Dht::builder(), + cache_size: NonZeroUsize::new(DEFAULT_CACHE_SIZE) + .expect("NonZeroUsize from DEFAULT_CACHE_SIZE"), + resolvers: Some( + DEFAULT_RESOLVERS + .iter() + .flat_map(|resolver| resolver.to_socket_addrs()) + .flatten() + .collect::>(), + ), + minimum_ttl: DEFAULT_MINIMUM_TTL, + maximum_ttl: DEFAULT_MAXIMUM_TTL, + cache: None, + } + } +} + +impl Settings { + /// Set custom set of [resolvers](Settings::resolvers). + pub fn resolvers(mut self, resolvers: Option>) -> Self { + self.resolvers = resolvers.map(|resolvers| { + resolvers + .iter() + .flat_map(|resolver| resolver.to_socket_addrs()) + .flatten() + .collect::>() + }); + self + } + + /// Set the [Settings::cache_size]. + /// + /// Controls the capacity of [Cache]. + pub fn cache_size(mut self, cache_size: NonZeroUsize) -> Self { + self.cache_size = cache_size; + self + } + + /// Set the [Settings::minimum_ttl] value. + /// + /// Limits how soon a [SignedPacket] is considered expired. + pub fn minimum_ttl(mut self, ttl: u32) -> Self { + self.minimum_ttl = ttl; + self.maximum_ttl = self.maximum_ttl.clamp(ttl, u32::MAX); + self + } + + /// Set the [Settings::maximum_ttl] value. + /// + /// Limits how long it takes before a [SignedPacket] is considered expired. + pub fn maximum_ttl(mut self, ttl: u32) -> Self { + self.maximum_ttl = ttl; + self.minimum_ttl = self.minimum_ttl.clamp(0, ttl); + self + } + + /// Set a custom implementation of [Cache]. + pub fn cache(mut self, cache: Box) -> Self { + self.cache = Some(cache); + self + } + + /// Set [Settings::dht_settings] + pub fn dht_settings(mut self, settings: mainline::Settings) -> Self { + self.dht_settings = settings; + self + } + + /// Convienent methot to set the [mainline::Settings::bootstrap] from [mainline::Testnet::bootstrap] + pub fn testnet(mut self, testnet: &Testnet) -> Self { + self.dht_settings = self.dht_settings.bootstrap(&testnet.bootstrap); + + self + } + + pub fn build(self) -> Result { + Client::new(self) + } +} + +#[derive(Clone, Debug)] +/// Pkarr client for publishing and resolving [SignedPacket]s over [mainline]. +pub struct Client { + sender: Sender, + cache: Box, + minimum_ttl: u32, + maximum_ttl: u32, +} + +impl Client { + pub fn new(settings: Settings) -> Result { + let (sender, receiver) = flume::bounded(32); + + let cache = settings + .cache + .clone() + .unwrap_or(Box::new(InMemoryCache::new(settings.cache_size))); + let cache_clone = cache.clone(); + + let client = Client { + sender, + cache, + minimum_ttl: settings.minimum_ttl, + maximum_ttl: settings.maximum_ttl, + }; + + debug!(?settings, "Starting Client main loop.."); + + thread::Builder::new() + .name("Pkarr Dht actor thread".to_string()) + .spawn(move || run(cache_clone, settings, receiver))?; + + let (tx, rx) = flume::bounded(1); + + client + .sender + .send(ActorMessage::Check(tx)) + .expect("actor thread unexpectedly shutdown"); + + rx.recv().expect("infallible")?; + + Ok(client) + } + + /// Returns a builder to edit settings before creating Client. + pub fn builder() -> Settings { + Settings::default() + } + + // === Getters === + + /// Returns [Info] about the running session from the actor thread. + pub fn info(&self) -> Result { + let (tx, rx) = flume::bounded(1); + + self.sender + .send(ActorMessage::Info(tx)) + .map_err(|_| ClientWasShutdown)?; + + rx.recv().map_err(|_| ClientWasShutdown) + } + + /// Returns a reference to the internal cache. + pub fn cache(&self) -> &dyn Cache { + self.cache.as_ref() + } + + // === Public Methods === + + /// Publishes a [SignedPacket] to the Dht. + pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<(), PublishError> { + self.publish_inner(signed_packet)? + .recv_async() + .await + .expect("Query was dropped before sending a response, please open an issue.") + .map_err(|error| match error { + PutError::PutQueryIsInflight(_) => PublishError::PublishInflight, + _ => PublishError::MainlinePutError(error), + })?; + + Ok(()) + } + + /// Returns a [SignedPacket] from cache if it is not expired, otherwise, + /// it will query the Dht, and return the first valid response, which may + /// or may not be expired itself. + /// + /// If the Dht was called, in the background, it continues receiving responses + /// and updating the cache with any more recent valid packets it receives. + /// + /// # Errors + /// - Returns a [ClientWasShutdown] if [Client::shutdown] was called, or + /// the loop in the actor thread is stopped for any reason (like thread panic). + pub async fn resolve( + &self, + public_key: &PublicKey, + ) -> Result, ClientWasShutdown> { + Ok(self.resolve_inner(public_key)?.recv_async().await.ok()) + } + + /// Shutdown the actor thread loop. + pub async fn shutdown(&mut self) { + let (sender, receiver) = flume::bounded(1); + + let _ = self.sender.send(ActorMessage::Shutdown(sender)); + let _ = receiver.recv_async().await; + } + + // === Sync === + + /// Publishes a [SignedPacket] to the Dht. + pub fn publish_sync(&self, signed_packet: &SignedPacket) -> Result<(), PublishError> { + self.publish_inner(signed_packet)? + .recv() + .expect("Query was dropped before sending a response, please open an issue.") + .map_err(|error| match error { + PutError::PutQueryIsInflight(_) => PublishError::PublishInflight, + _ => PublishError::MainlinePutError(error), + })?; + + Ok(()) + } + + /// Returns a [SignedPacket] from cache if it is not expired, otherwise, + /// it will query the Dht, and return the first valid response, which may + /// or may not be expired itself. + /// + /// If the Dht was called, in the background, it continues receiving responses + /// and updating the cache with any more recent valid packets it receives. + /// + /// # Errors + /// - Returns a [ClientWasShutdown] if [Client::shutdown] was called, or + /// the loop in the actor thread is stopped for any reason (like thread panic). + pub fn resolve_sync( + &self, + public_key: &PublicKey, + ) -> Result, ClientWasShutdown> { + Ok(self.resolve_inner(public_key)?.recv().ok()) + } + + /// Shutdown the actor thread loop. + pub fn shutdown_sync(&self) { + let (sender, receiver) = flume::bounded(1); + + let _ = self.sender.send(ActorMessage::Shutdown(sender)); + let _ = receiver.recv(); + } + + // === Private Methods === + + pub(crate) fn publish_inner( + &self, + signed_packet: &SignedPacket, + ) -> Result>, PublishError> { + let mutable_item: MutableItem = (signed_packet).into(); + + if let Some(current) = self.cache.get(mutable_item.target().as_bytes()) { + if current.timestamp() > signed_packet.timestamp() { + return Err(PublishError::NotMostRecent); + } + }; + + self.cache + .put(mutable_item.target().as_bytes(), signed_packet); + + let (sender, receiver) = flume::bounded::>(1); + + self.sender + .send(ActorMessage::Publish(mutable_item, sender)) + .map_err(|_| PublishError::ClientWasShutdown)?; + + Ok(receiver) + } + + pub(crate) fn resolve_inner( + &self, + public_key: &PublicKey, + ) -> Result, ClientWasShutdown> { + let target = MutableItem::target_from_key(public_key.as_bytes(), &None); + + let cached_packet = self.cache.get(target.as_bytes()); + + let (tx, rx) = flume::bounded::(1); + + let as_ref = cached_packet.as_ref(); + + // Should query? + if as_ref + .as_ref() + .map(|c| c.is_expired(self.minimum_ttl, self.maximum_ttl)) + .unwrap_or(true) + { + debug!( + ?public_key, + "querying the DHT to hydrate our cache for later." + ); + + self.sender + .send(ActorMessage::Resolve( + target, + tx.clone(), + // Sending the `timestamp` of the known cache, help save some bandwith, + // since remote nodes will not send the encoded packet if they don't know + // any more recent versions. + // most_recent_known_timestamp, + as_ref.map(|cached| cached.timestamp().as_u64()), + )) + .map_err(|_| ClientWasShutdown)?; + } + + if let Some(cached_packet) = cached_packet { + debug!( + public_key = ?cached_packet.public_key(), + "responding with cached packet even if expired" + ); + + // If the receiver was dropped.. no harm. + let _ = tx.send(cached_packet); + } + + Ok(rx) + } +} + +#[derive(Debug)] +pub struct ClientWasShutdown; + +impl std::error::Error for ClientWasShutdown {} + +impl std::fmt::Display for ClientWasShutdown { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Pkarr Client was shutdown") + } +} + +#[derive(thiserror::Error, Debug)] +/// Errors occuring during publishing a [SignedPacket] +pub enum PublishError { + #[error("Found a more recent SignedPacket in the client's cache")] + /// Found a more recent SignedPacket in the client's cache + NotMostRecent, + + #[error("Pkarr Client was shutdown")] + ClientWasShutdown, + + #[error("Publish query is already inflight for the same public_key")] + /// [crate::Client::publish] is already inflight to the same public_key + PublishInflight, + + #[error(transparent)] + MainlinePutError(#[from] PutError), +} + +fn run(cache: Box, settings: Settings, receiver: Receiver) { + match settings.dht_settings.build_rpc() { + Ok(mut rpc) => { + let mut server = settings.dht_settings.into_server(); + actor_thread(&mut rpc, &mut server, cache, receiver, settings.resolvers) + } + Err(err) => { + if let Ok(ActorMessage::Check(sender)) = receiver.try_recv() { + let _ = sender.send(Err(err)); + } + } + } +} + +fn actor_thread( + rpc: &mut Rpc, + server: &mut Option>, + cache: Box, + receiver: Receiver, + resolvers: Option>, +) { + let mut senders: HashMap>> = HashMap::new(); + + loop { + // === Receive actor messages === + if let Ok(actor_message) = receiver.try_recv() { + match actor_message { + ActorMessage::Shutdown(sender) => { + drop(receiver); + let _ = sender.send(()); + break; + } + ActorMessage::Publish(mutable_item, sender) => { + let target = mutable_item.target(); + + let request = messages::PutRequestSpecific::PutMutable( + messages::PutMutableRequestArguments { + target: *target, + v: mutable_item.value().to_vec(), + k: mutable_item.key().to_vec(), + seq: *mutable_item.seq(), + sig: mutable_item.signature().to_vec(), + salt: None, + cas: None, + }, + ); + + rpc.put(*target, request, Some(sender)) + } + ActorMessage::Resolve(target, sender, most_recent_known_timestamp) => { + if let Some(set) = senders.get_mut(&target) { + set.push(sender); + } else { + senders.insert(target, vec![sender]); + }; + + let request = messages::RequestTypeSpecific::GetValue( + messages::GetValueRequestArguments { + target, + seq: most_recent_known_timestamp.map(|t| t as i64), + // seq: None, + salt: None, + }, + ); + + rpc.get(target, request, None, resolvers.clone()) + } + ActorMessage::Info(sender) => { + let local_addr = rpc.local_addr(); + + let _ = sender.send(Info { local_addr }); + } + ActorMessage::Check(sender) => { + let _ = sender.send(Ok(())); + } + } + } + + // === Dht Tick === + + let report = rpc.tick(); + + // === Drop senders to done queries === + for id in &report.done_get_queries { + if let Some(senders) = senders.remove(id) { + if let Some(cached) = cache.get(id.as_bytes()) { + debug!(public_key = ?cached.public_key(), "Returning expired cache as a fallback"); + // Send cached packets if available + for sender in senders { + let _ = sender.send(cached.clone()); + } + } + }; + } + + // === Receive and handle incoming messages === + if let Some(ReceivedFrom { from, message }) = &report.received_from { + match message { + // === Responses === + ReceivedMessage::QueryResponse(response) => { + match response { + // === Got Mutable Value === + QueryResponse { + target, + response: QueryResponseSpecific::Value(Response::Mutable(mutable_item)), + } => { + if let Ok(signed_packet) = &SignedPacket::try_from(mutable_item) { + let new_packet = if let Some(ref cached) = + cache.get_read_only(target.as_bytes()) + { + if signed_packet.more_recent_than(cached) { + debug!( + ?target, + "Received more recent packet than in cache" + ); + Some(signed_packet) + } else { + None + } + } else { + debug!(?target, "Received new packet after cache miss"); + Some(signed_packet) + }; + + if let Some(packet) = new_packet { + cache.put(target.as_bytes(), packet); + + if let Some(set) = senders.get(target) { + for sender in set { + let _ = sender.send(packet.clone()); + } + } + } + } + } + // === Got NoMoreRecentValue === + QueryResponse { + target, + response: QueryResponseSpecific::Value(Response::NoMoreRecentValue(seq)), + } => { + if let Some(mut cached) = cache.get_read_only(target.as_bytes()) { + if (*seq as u64) == cached.timestamp().as_u64() { + trace!("Remote node has the a packet with same timestamp, refreshing cached packet."); + + cached.refresh(); + cache.put(target.as_bytes(), &cached); + + // Send the found sequence as a timestamp to the caller to decide what to do + // with it. + if let Some(set) = senders.get(target) { + for sender in set { + let _ = sender.send(cached.clone()); + } + } + } + }; + } + // Ignoring errors, as they are logged in `mainline` crate already. + _ => {} + }; + } + // === Requests === + ReceivedMessage::Request((transaction_id, request)) => { + if let Some(server) = server.as_mut() { + server.handle_request(rpc, *from, *transaction_id, request); + } + } + }; + } + } + + debug!("Client main loop terminated"); +} + +enum ActorMessage { + Publish(MutableItem, Sender>), + Resolve(Id, Sender, Option), + Shutdown(Sender<()>), + Info(Sender), + Check(Sender>), +} + +pub struct Info { + local_addr: Result, +} + +impl Info { + /// Local UDP Ipv4 socket address that this node is listening on. + pub fn local_addr(&self) -> Result<&SocketAddr, std::io::Error> { + self.local_addr + .as_ref() + .map_err(|e| std::io::Error::new(e.kind(), e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use mainline::Testnet; + + use super::*; + use crate::{Keypair, SignedPacket}; + + #[test] + fn shutdown_sync() { + let testnet = Testnet::new(3).unwrap(); + + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let local_addr = client.info().unwrap().local_addr().unwrap().to_string(); + + println!("{}", local_addr); + + assert!(client.info().unwrap().local_addr().is_ok()); + + client.shutdown_sync(); + + assert!(client.info().is_err()); + } + + #[test] + fn publish_resolve_sync() { + let testnet = Testnet::new(10).unwrap(); + + let a = Client::builder().testnet(&testnet).build().unwrap(); + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + a.publish_sync(&signed_packet).unwrap(); + + let b = Client::builder().testnet(&testnet).build().unwrap(); + + let resolved = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); + + let from_cache = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); + assert_eq!(from_cache.last_seen(), resolved.last_seen()); + } + + #[test] + fn thread_safe_sync() { + let testnet = Testnet::new(10).unwrap(); + + let a = Client::builder().testnet(&testnet).build().unwrap(); + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + a.publish_sync(&signed_packet).unwrap(); + + let b = Client::builder().testnet(&testnet).build().unwrap(); + + thread::spawn(move || { + let resolved = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); + + let from_cache = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); + assert_eq!(from_cache.last_seen(), resolved.last_seen()); + }) + .join() + .unwrap(); + } + + #[tokio::test] + async fn shutdown() { + let testnet = Testnet::new(3).unwrap(); + + let mut a = Client::builder().testnet(&testnet).build().unwrap(); + + assert!(a.info().unwrap().local_addr().is_ok()); + + a.shutdown().await; + + assert!(a.info().is_err()); + } + + #[tokio::test] + async fn publish_resolve() { + let testnet = Testnet::new(10).unwrap(); + + let a = Client::builder().testnet(&testnet).build().unwrap(); + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + a.publish(&signed_packet).await.unwrap(); + + let b = Client::builder().testnet(&testnet).build().unwrap(); + + let resolved = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); + + let from_cache = b.resolve_sync(&keypair.public_key()).unwrap().unwrap(); + assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); + assert_eq!(from_cache.last_seen(), resolved.last_seen()); + } + + #[tokio::test] + async fn thread_safe() { + let testnet = Testnet::new(10).unwrap(); + + let a = Client::builder().testnet(&testnet).build().unwrap(); + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + a.publish(&signed_packet).await.unwrap(); + + let b = Client::builder().testnet(&testnet).build().unwrap(); + + tokio::spawn(async move { + let resolved = b.resolve(&keypair.public_key()).await.unwrap().unwrap(); + assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); + + let from_cache = b.resolve(&keypair.public_key()).await.unwrap().unwrap(); + assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); + assert_eq!(from_cache.last_seen(), resolved.last_seen()); + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn return_expired_packet_fallback() { + let testnet = Testnet::new(10).unwrap(); + + let client = Client::builder() + .testnet(&testnet) + .dht_settings( + mainline::Settings::default().request_timeout(Duration::from_millis(10).into()), + ) + // Everything is expired + .maximum_ttl(0) + .build() + .unwrap(); + + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder().sign(&keypair).unwrap(); + + client + .cache() + .put(&keypair.public_key().into(), &signed_packet); + + let resolved = client.resolve(&keypair.public_key()).await.unwrap(); + + assert_eq!(resolved, Some(signed_packet)); + } +} diff --git a/pkarr/src/client/mod.rs b/pkarr/src/client/mod.rs new file mode 100644 index 0000000..6eed8ff --- /dev/null +++ b/pkarr/src/client/mod.rs @@ -0,0 +1,14 @@ +//! Client implementation. + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +pub(crate) mod dht; +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +pub use dht::{Client, Settings}; + +#[cfg(target_arch = "wasm32")] +pub(crate) mod relay; +#[cfg(target_arch = "wasm32")] +pub use relay::{Client, Settings}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] +pub mod relay; diff --git a/pkarr/src/client/relay.rs b/pkarr/src/client/relay.rs new file mode 100644 index 0000000..9c3ac6e --- /dev/null +++ b/pkarr/src/client/relay.rs @@ -0,0 +1,551 @@ +//! Pkarr client for publishing and resolving [SignedPacket]s over [relays](https://pkarr.org/relays). + +use std::fmt::{self, Debug, Display, Formatter}; +use std::num::NonZeroUsize; + +use reqwest::header::HeaderValue; +use reqwest::{header, Response, StatusCode}; +use tracing::debug; + +#[cfg(target_arch = "wasm32")] +use futures::future::select_ok; + +#[cfg(not(target_arch = "wasm32"))] +use tokio::task::JoinSet; + +use crate::{ + Cache, InMemoryCache, PublicKey, SignedPacket, DEFAULT_CACHE_SIZE, DEFAULT_MAXIMUM_TTL, + DEFAULT_MINIMUM_TTL, DEFAULT_RELAYS, +}; + +#[derive(Debug, Clone)] +/// [Client]'s settings +pub struct Settings { + pub(crate) relays: Vec, + /// Defaults to [DEFAULT_CACHE_SIZE] + pub(crate) cache_size: NonZeroUsize, + /// Used in the `min` parameter in [SignedPacket::expires_in]. + /// + /// Defaults to [DEFAULT_MINIMUM_TTL] + pub(crate) minimum_ttl: u32, + /// Used in the `max` parametere in [SignedPacket::expires_in]. + /// + /// Defaults to [DEFAULT_MAXIMUM_TTL] + pub(crate) maximum_ttl: u32, + /// Custom [reqwest::Client] + pub(crate) http_client: reqwest::Client, + /// Custom [Cache] implementation, defaults to [InMemoryCache] + pub(crate) cache: Option>, +} + +impl Default for Settings { + fn default() -> Self { + Self { + relays: DEFAULT_RELAYS.map(|s| s.into()).to_vec(), + cache_size: NonZeroUsize::new(DEFAULT_CACHE_SIZE) + .expect("NonZeroUsize from DEFAULT_CACHE_SIZE"), + minimum_ttl: DEFAULT_MINIMUM_TTL, + maximum_ttl: DEFAULT_MAXIMUM_TTL, + http_client: reqwest::Client::new(), + cache: None, + } + } +} + +impl Settings { + /// Set the relays to publish and resolve [SignedPacket]s to and from. + pub fn relays(mut self, relays: Vec) -> Self { + self.relays = relays; + self + } + + /// Set the [Settings::cache_size]. + /// + /// Controls the capacity of [Cache]. + pub fn cache_size(mut self, cache_size: NonZeroUsize) -> Self { + self.cache_size = cache_size; + self + } + + /// Set the [Settings::minimum_ttl] value. + /// + /// Limits how soon a [SignedPacket] is considered expired. + pub fn minimum_ttl(mut self, ttl: u32) -> Self { + self.minimum_ttl = ttl; + self.maximum_ttl = self.maximum_ttl.clamp(ttl, u32::MAX); + self + } + + /// Set the [Settings::maximum_ttl] value. + /// + /// Limits how long it takes before a [SignedPacket] is considered expired. + pub fn maximum_ttl(mut self, ttl: u32) -> Self { + self.maximum_ttl = ttl; + self.minimum_ttl = self.minimum_ttl.clamp(0, ttl); + self + } + + /// Set a custom implementation of [Cache]. + pub fn cache(mut self, cache: Box) -> Self { + self.cache = Some(cache); + self + } + + pub fn build(self) -> Result { + Client::new(self) + } +} + +#[derive(Debug, Clone)] +/// Pkarr client for publishing and resolving [SignedPacket]s over [relays](https://pkarr.org/relays). +pub struct Client { + http_client: reqwest::Client, + relays: Vec, + cache: Box, + minimum_ttl: u32, + maximum_ttl: u32, +} + +impl Default for Client { + fn default() -> Self { + Self::new(Settings::default()).expect("Pkarr Relay client default") + } +} + +impl Client { + pub fn new(settings: Settings) -> Result { + if settings.relays.is_empty() { + return Err(EmptyListOfRelays); + } + + let cache = settings + .cache + .clone() + .unwrap_or(Box::new(InMemoryCache::new(settings.cache_size))); + + Ok(Self { + http_client: settings.http_client, + relays: settings.relays, + cache, + minimum_ttl: settings.minimum_ttl, + maximum_ttl: settings.maximum_ttl, + }) + } + + /// Returns a builder to edit settings before creating Client. + pub fn builder() -> Settings { + Settings::default() + } + + /// Returns a reference to the internal cache. + pub fn cache(&self) -> &dyn Cache { + self.cache.as_ref() + } + + /// Publishes a [SignedPacket] to this client's relays. + /// + /// Return the first successful completion, or the last failure. + pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<(), PublishToRelayError> { + let public_key = signed_packet.public_key(); + + if let Some(current) = self.cache.get(&public_key.as_ref().into()) { + if current.timestamp() > signed_packet.timestamp() { + return Err(PublishToRelayError::NotMostRecent); + } + }; + + self.cache.put(&public_key.as_ref().into(), signed_packet); + + Ok(self.race_publish(signed_packet).await?) + } + + /// Resolve a [SignedPacket] from this client's relays. + /// + /// Return the first successful response, or the failure from the last responding relay. + /// + /// # Errors + /// - Returns [reqwest::Error] if all relays responded with a status >= 400 + /// (except 404 in which case you should receive Ok(None)) or something wrong + /// with the transport, transparent from [reqwest::Error]. + /// + /// In that case, return the last error we got from the last responding relay. + pub async fn resolve( + &self, + public_key: &PublicKey, + ) -> Result, reqwest::Error> { + let cached_packet = self.cache.get(&(public_key.into())); + + let (tx, rx) = flume::bounded::, reqwest::Error>>(1); + + let as_ref = cached_packet.as_ref(); + + // Should query? + if as_ref + .as_ref() + .map(|c| c.is_expired(self.minimum_ttl, self.maximum_ttl)) + .unwrap_or(true) + { + debug!( + ?public_key, + "querying relays to hydrate our cache for later." + ); + + let pubky = public_key.clone(); + let tx = tx.clone(); + let this = self.clone(); + + #[cfg(not(target_arch = "wasm32"))] + tokio::task::spawn(async move { + // If the receiver was dropped.. no harm. + let _ = tx.send(this.race_resolve(&pubky, None).await); + }); + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move { + // If the receiver was dropped.. no harm. + let _ = tx.send(this.race_resolve(&pubky, None).await); + }); + } + + if let Some(cached_packet) = cached_packet { + debug!( + public_key = ?cached_packet.public_key(), + "responding with cached packet even if expired" + ); + + // If the receiver was dropped.. no harm. + let _ = tx.send(Ok(Some(cached_packet))); + } + + rx.recv_async() + .await + .expect("at least one sender should send before being dropped") + } + + // === Native Race implementation === + + #[cfg(not(target_arch = "wasm32"))] + async fn race_publish(&self, signed_packet: &SignedPacket) -> Result<(), reqwest::Error> { + let mut futures = JoinSet::new(); + + for relay in self.relays.clone() { + let signed_packet = signed_packet.clone(); + let this = self.clone(); + + futures.spawn(async move { this.publish_to_relay(&relay, signed_packet).await }); + } + + let mut last_error = None; + + while let Some(result) = futures.join_next().await { + match result { + Ok(Ok(_)) => return Ok(()), + Ok(Err(error)) => last_error = Some(error), + Err(joinerror) => { + debug!(?joinerror); + } + } + } + + Err(last_error.expect("failed to receive any error responses!")) + } + + #[cfg(not(target_arch = "wasm32"))] + async fn race_resolve( + &self, + public_key: &PublicKey, + cached_packet: Option, + ) -> Result, reqwest::Error> { + let mut futures = JoinSet::new(); + + for relay in self.relays.clone() { + let public_key = public_key.clone(); + let cached = cached_packet.clone(); + let this = self.clone(); + + futures.spawn(async move { this.resolve_from_relay(&relay, public_key, cached).await }); + } + + let mut result: Result, reqwest::Error> = Ok(None); + + while let Some(task_result) = futures.join_next().await { + match task_result { + Ok(Ok(Some(signed_packet))) => { + self.cache + .put(&signed_packet.public_key().as_ref().into(), &signed_packet); + + return Ok(Some(signed_packet)); + } + Ok(Err(error)) => result = Err(error), + Ok(_) => {} + Err(joinerror) => { + debug!(?joinerror); + } + } + } + + result + } + + // === Wasm === + + #[cfg(target_arch = "wasm32")] + async fn race_publish(&self, signed_packet: &SignedPacket) -> Result<(), reqwest::Error> { + let futures = self.relays.iter().map(|relay| { + let signed_packet = signed_packet.clone(); + let this = self.clone(); + + Box::pin(async move { this.publish_to_relay(relay, signed_packet).await }) + }); + + match select_ok(futures).await { + Ok((_, _)) => Ok(()), + Err(e) => Err(e), + } + } + + #[cfg(target_arch = "wasm32")] + async fn race_resolve( + &self, + public_key: &PublicKey, + cached_packet: Option, + ) -> Result, reqwest::Error> { + let futures = self.relays.iter().map(|relay| { + let public_key = public_key.clone(); + let cached = cached_packet.clone(); + let this = self.clone(); + + Box::pin(async move { this.resolve_from_relay(relay, public_key, cached).await }) + }); + + let mut result: Result, reqwest::Error> = Ok(None); + + match select_ok(futures).await { + Ok((Some(signed_packet), _)) => { + self.cache + .put(&signed_packet.public_key().as_ref().into(), &signed_packet); + + return Ok(Some(signed_packet)); + } + Err(error) => result = Err(error), + Ok(_) => {} + } + + result + } + + // === Private Methods === + + async fn publish_to_relay( + &self, + relay: &str, + signed_packet: SignedPacket, + ) -> Result { + let url = format!("{relay}/{}", signed_packet.public_key()); + + self.http_client + .put(&url) + .body(signed_packet.to_relay_payload()) + .send() + .await + .map_err(|error| { + debug!(?url, ?error, "Error response"); + + error + }) + } + + async fn resolve_from_relay( + &self, + relay: &str, + public_key: PublicKey, + cached_packet: Option, + ) -> Result, reqwest::Error> { + let url = format!("{relay}/{public_key}"); + + let mut request = self.http_client.get(&url); + + if let Some(httpdate) = cached_packet + .as_ref() + .map(|c| c.timestamp().format_http_date()) + { + request = request.header( + header::IF_MODIFIED_SINCE, + HeaderValue::from_str(httpdate.as_str()) + .expect("httpdate to be valid header value"), + ); + } + + match request.send().await { + Ok(response) => { + if response.status() == StatusCode::NOT_FOUND { + debug!(?url, "SignedPacket not found"); + return Ok(None); + } + + let response = response.error_for_status()?; + + if response.content_length().unwrap_or_default() > SignedPacket::MAX_BYTES { + debug!(?url, "Response too large"); + + return Ok(None); + } + + let payload = response.bytes().await?; + + match SignedPacket::from_relay_payload(&public_key, &payload) { + Ok(signed_packet) => Ok(choose_most_recent(signed_packet, cached_packet)), + Err(error) => { + debug!(?url, ?error, "Invalid signed_packet"); + + Ok(None) + } + } + } + Err(error) => { + debug!(?url, ?error, "Resolve Error response"); + + Err(error) + } + } + } +} + +fn choose_most_recent( + signed_packet: SignedPacket, + cached_packet: Option, +) -> Option { + if let Some(ref cached) = cached_packet { + if signed_packet.more_recent_than(cached) { + debug!( + public_key = ?signed_packet.public_key(), + "Received more recent packet than in cache" + ); + Some(signed_packet) + } else { + None + } + } else { + debug!(public_key= ?signed_packet.public_key(), "Received new packet after cache miss"); + Some(signed_packet) + } +} + +#[derive(thiserror::Error, Debug)] +/// Errors during publishing a [SignedPacket] to a list of relays +pub enum PublishToRelayError { + #[error("SignedPacket's timestamp is not the most recent")] + /// Failed to publish because there is a more recent packet. + NotMostRecent, + + #[error(transparent)] + /// Transparent [reqwest::Error] + /// + /// All relays responded with non-2xx status code, + /// or something wrong with the transport, transparent from [reqwest::Error]. + /// + /// This was last error response. + RelayError(#[from] reqwest::Error), +} + +#[derive(Debug)] +pub struct EmptyListOfRelays; + +impl std::error::Error for EmptyListOfRelays {} + +impl Display for EmptyListOfRelays { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "Can not create a Pkarr relay Client with an empty list of relays" + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Keypair, SignedPacket}; + + #[tokio::test] + async fn publish_resolve() { + let keypair = Keypair::random(); + + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 30) + .sign(&keypair) + .unwrap(); + + let mut server = mockito::Server::new_async().await; + + let path = format!("/{}", signed_packet.public_key()); + + server + .mock("PUT", path.as_str()) + .with_header("content-type", "text/plain") + .with_status(200) + .create(); + server + .mock("GET", path.as_str()) + .with_body(signed_packet.to_relay_payload()) + .create(); + + let relays = vec![server.url()]; + let a = Client::builder().relays(relays.clone()).build().unwrap(); + let b = Client::builder().relays(relays).build().unwrap(); + + a.publish(&signed_packet).await.unwrap(); + + let resolved = b.resolve(&keypair.public_key()).await.unwrap().unwrap(); + + assert_eq!(a.cache().len(), 1); + assert_eq!(b.cache().len(), 1); + + assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); + } + + #[tokio::test] + async fn not_found() { + let keypair = Keypair::random(); + + let mut server = mockito::Server::new_async().await; + + let path = format!("/{}", keypair.public_key()); + + server.mock("GET", path.as_str()).with_status(404).create(); + + let relays = vec![server.url()]; + let client = Client::builder().relays(relays).build().unwrap(); + + let resolved = client.resolve(&keypair.public_key()).await.unwrap(); + + assert!(resolved.is_none()); + } + + #[tokio::test] + async fn return_expired_packet_fallback() { + let keypair = Keypair::random(); + + let mut server = mockito::Server::new_async().await; + + let path = format!("/{}", keypair.public_key()); + + server.mock("GET", path.as_str()).with_status(404).create(); + + let relays = vec![server.url()]; + let client = Client::builder() + .relays(relays) + .maximum_ttl(0) + .build() + .unwrap(); + + let signed_packet = SignedPacket::builder().sign(&keypair).unwrap(); + + client + .cache() + .put(&keypair.public_key().into(), &signed_packet); + + let resolved = client.resolve(&keypair.public_key()).await.unwrap(); + + assert_eq!(resolved, Some(signed_packet)); + } +} diff --git a/pkarr/src/client_async.rs b/pkarr/src/client_async.rs deleted file mode 100644 index 0c49b1a..0000000 --- a/pkarr/src/client_async.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Async version of [PkarrClient] - -use std::net::SocketAddr; - -use super::{ - cache::PkarrCache, - client::{ActorMessage, PkarrClient}, -}; -use crate::{Error, PublicKey, Result, SignedPacket}; - -#[derive(Clone, Debug)] -/// Async version of [PkarrClient] -pub struct PkarrClientAsync(PkarrClient); - -impl PkarrClient { - /// Returns [PkarrClientAsync] - pub fn as_async(self) -> PkarrClientAsync { - PkarrClientAsync(self) - } -} - -impl PkarrClientAsync { - // === Getters === - - /// Returns the local address of the udp socket this node is listening on. - /// - /// Returns `None` if the node is shutdown - pub fn local_addr(&self) -> Option { - self.0.address - } - - /// Returns a reference to the internal cache. - pub fn cache(&self) -> &dyn PkarrCache { - self.0.cache.as_ref() - } - - // === Public Methods === - - /// Publishes a [SignedPacket] to the Dht. - /// - /// # Errors - /// - Returns a [Error::DhtIsShutdown] if [PkarrClient::shutdown] was called, or - /// the loop in the actor thread is stopped for any reason (like thread panic). - /// - Returns a [Error::PublishInflight] if the client is currently publishing the same public_key. - /// - Returns a [Error::NotMostRecent] if the provided signed packet is older than most recent. - /// - Returns a [Error::MainlineError] if the Dht received an unexpected error otherwise. - pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<()> { - match self.0.publish_inner(signed_packet)?.recv_async().await { - Ok(Ok(_)) => Ok(()), - Ok(Err(error)) => match error { - mainline::Error::PutQueryIsInflight(_) => Err(Error::PublishInflight), - _ => Err(Error::MainlineError(error)), - }, - // Since we pass this sender to `Rpc::put`, the only reason the sender, - // would be dropped, is if `Rpc` is dropped, which should only happeng on shutdown. - Err(_) => Err(Error::DhtIsShutdown), - } - } - - /// Returns the first valid [SignedPacket] available from cache, or the Dht. - /// - /// If the Dht was called, in the background, it continues receiving responses - /// and updating the cache. - /// - /// # Errors - /// - Returns a [Error::DhtIsShutdown] if [PkarrClient::shutdown] was called, or - /// the loop in the actor thread is stopped for any reason (like thread panic). - pub async fn resolve(&self, public_key: &PublicKey) -> Result> { - Ok(self.0.resolve_inner(public_key)?.recv_async().await.ok()) - } - - /// Shutdown the actor thread loop. - pub async fn shutdown(&mut self) -> Result<()> { - let (sender, receiver) = flume::bounded(1); - - self.0 - .sender - .send(ActorMessage::Shutdown(sender)) - .map_err(|_| Error::DhtIsShutdown)?; - - receiver.recv_async().await?; - - self.0.address = None; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use mainline::{dht::DhtSettings, Testnet}; - - use super::*; - use crate::{dns, Keypair, SignedPacket}; - - #[test] - fn shutdown() { - async fn test() { - let testnet = Testnet::new(3); - - let mut a = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - assert_ne!(a.local_addr(), None); - - a.shutdown().unwrap(); - - assert_eq!(a.local_addr(), None); - } - - futures::executor::block_on(test()); - } - - #[test] - fn publish_resolve() { - async fn test() { - let testnet = Testnet::new(10); - - let a = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap.clone()), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let _ = a.publish(&signed_packet); - - let b = PkarrClient::builder() - .dht_settings(DhtSettings { - bootstrap: Some(testnet.bootstrap), - request_timeout: None, - server: None, - port: None, - }) - .build() - .unwrap(); - - let resolved = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - - let from_cache = b.resolve(&keypair.public_key()).unwrap().unwrap(); - assert_eq!(from_cache.as_bytes(), signed_packet.as_bytes()); - assert_eq!(from_cache.last_seen(), resolved.last_seen()); - } - - futures::executor::block_on(test()); - } -} diff --git a/pkarr/src/error.rs b/pkarr/src/error.rs deleted file mode 100644 index 140db70..0000000 --- a/pkarr/src/error.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Main Crate Error - -// Alias Result to be the crate Result. -pub type Result = core::result::Result; - -#[derive(thiserror::Error, Debug)] -/// Pkarr crate error enum. -pub enum Error { - #[error(transparent)] - /// Transparent [std::io::Error] - IO(#[from] std::io::Error), - - #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] - #[error(transparent)] - /// Transparent [mainline::Error] - MainlineError(#[from] mainline::Error), - - // === Keys errors === - #[error("Invalid PublicKey length, expected 32 bytes but got: {0}")] - InvalidPublicKeyLength(usize), - - #[error("Invalid Ed25519 publickey; Cannot decompress Edwards point")] - InvalidEd25519PublicKey, - - #[error("Invalid Ed25519 signature")] - InvalidEd25519Signature, - - #[error(transparent)] - InvalidPublicKeyEncoding(#[from] z32::Z32Error), - - // === Packets errors === - #[error(transparent)] - /// Transparent [simple_dns::SimpleDnsError] - DnsError(#[from] simple_dns::SimpleDnsError), - - #[error("Invalid SignedPacket bytes length, expected at least 104 bytes but got: {0}")] - /// Serialized signed packets are `<32 bytes publickey><64 bytes signature><8 bytes - /// timestamp>`. - InvalidSignedPacketBytesLength(usize), - - #[error("Invalid relay payload size, expected at least 72 bytes but got: {0}")] - /// Relay api http-body should be `<64 bytes signature><8 bytes timestamp> - /// `. - InvalidRelayPayloadSize(usize), - - #[error("DNS Packet is too large, expected max 1000 bytes but got: {0}")] - // DNS packet endocded and compressed is larger than 1000 bytes - PacketTooLarge(usize), - - // === Flume errors === - #[error(transparent)] - /// Transparent [flume::RecvError] - Receive(#[from] flume::RecvError), - - #[error("Dht is shutdown")] - /// The dht was shutdown. - DhtIsShutdown, - - #[error("Publish query is already inflight for the same public_key")] - /// [crate::PkarrClient::publish] is already inflight to the same public_key - PublishInflight, - - #[error("SignedPacket's timestamp is not the most recent")] - /// Failed to publish because there is a more recent packet. - NotMostRecent, - - // === Relay errors === - #[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] - #[error(transparent)] - /// Transparent [ureq::Error] - RelayError(#[from] Box), - - #[cfg(any(feature = "relay", target_arch = "wasm32"))] - #[error("Empty list of relays")] - /// Empty list of relays - EmptyListOfRelays, - - // === Wasm === - #[cfg(target_arch = "wasm32")] - #[error("Relay response with status: {0}, and message: {1}")] - /// A response was successfully received but had status code >= 400. - WasmRelayError(u16, String), - - #[cfg(target_arch = "wasm32")] - #[error("JS error")] - JsError(wasm_bindgen::JsValue), -} diff --git a/pkarr/src/extra/endpoints/endpoint.rs b/pkarr/src/extra/endpoints/endpoint.rs new file mode 100644 index 0000000..3e72cf5 --- /dev/null +++ b/pkarr/src/extra/endpoints/endpoint.rs @@ -0,0 +1,256 @@ +use crate::{ + dns::{ + rdata::{RData, SVCB}, + ResourceRecord, + }, + PublicKey, SignedPacket, +}; +use std::{ + collections::HashSet, + net::{IpAddr, SocketAddr, ToSocketAddrs}, +}; + +use rand::{seq::SliceRandom, thread_rng}; + +#[derive(Debug, Clone)] +/// An alternative Endpoint for a `qname`, from either [RData::SVCB] or [RData::HTTPS] dns records +pub struct Endpoint { + target: String, + public_key: PublicKey, + port: u16, + /// SocketAddrs from the [SignedPacket] + addrs: Vec, +} + +impl Endpoint { + /// Returns a stack of endpoints from a SignedPacket + /// + /// 1. Find the SVCB or HTTPS records + /// 2. Sort them by priority (reverse) + /// 3. Shuffle records within each priority + /// 3. If the target is `.`, keep track of A and AAAA records see [rfc9460](https://www.rfc-editor.org/rfc/rfc9460#name-special-handling-of-in-targ) + pub(crate) fn parse( + signed_packet: &SignedPacket, + target: &str, + // TODO: change is_svcb to a better name + is_svcb: bool, + ) -> Vec { + let mut records = signed_packet + .resource_records(target) + .filter_map(|record| get_svcb(record, is_svcb)) + .collect::>(); + + // TODO: support wildcard? + + // Shuffle the vector first + let mut rng = thread_rng(); + records.shuffle(&mut rng); + // Sort by priority + records.sort_by(|a, b| b.priority.cmp(&a.priority)); + + let mut addrs = HashSet::new(); + for record in signed_packet.resource_records("@") { + match &record.rdata { + RData::A(ip) => { + addrs.insert(IpAddr::V4(ip.address.into())); + } + RData::AAAA(ip) => { + addrs.insert(IpAddr::V6(ip.address.into())); + } + _ => {} + } + } + let addrs = addrs.into_iter().collect::>(); + + records + .into_iter() + .map(|s| { + let target = s.target.to_string(); + + let target = if target == "." || target.is_empty() { + ".".to_string() + } else { + target + }; + + let port = s + .get_param(SVCB::PORT) + .map(|bytes| { + let mut arr = [0_u8; 2]; + arr[0] = bytes[0]; + arr[1] = bytes[1]; + + u16::from_be_bytes(arr) + }) + .unwrap_or_default(); + + let addrs = if &target == "." { + addrs.clone() + } else { + Vec::with_capacity(0) + }; + + Endpoint { + target, + port, + public_key: signed_packet.public_key(), + addrs, + } + }) + .collect::>() + } + + /// Returns the [SVCB] record's `target` value. + /// + /// Useful in web browsers where we can't use [Self::to_socket_addrs] + pub fn domain(&self) -> &str { + &self.target + } + + pub fn port(&self) -> u16 { + self.port + } + + /// Return the [PublicKey] of the [SignedPacket] this endpoint was found at. + /// + /// This is useful as the [PublicKey] of the endpoint (server), and could be + /// used for TLS. + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// Return an iterator of [SocketAddr], either by resolving the [Endpoint::domain] using normal DNS, + /// or, if the target is ".", return the [RData::A] or [RData::AAAA] records + /// from the endpoint's [SignedPacket], if available. + pub fn to_socket_addrs(&self) -> Vec { + if self.target == "." { + let port = self.port; + + return self + .addrs + .iter() + .map(|addr| SocketAddr::from((*addr, port))) + .collect::>(); + } + + if cfg!(target_arch = "wasm32") { + vec![] + } else { + format!("{}:{}", self.target, self.port) + .to_socket_addrs() + .map_or(vec![], |v| v.collect::>()) + } + } +} + +fn get_svcb<'a>(record: &'a ResourceRecord, is_svcb: bool) -> Option<&'a SVCB<'a>> { + match &record.rdata { + RData::SVCB(svcb) => { + if is_svcb { + Some(svcb) + } else { + None + } + } + + RData::HTTPS(curr) => { + if is_svcb { + None + } else { + Some(&curr.0) + } + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::Keypair; + + #[tokio::test] + async fn endpoint_domain() { + let keypair = Keypair::random(); + let signed_packet = SignedPacket::builder() + .https( + "foo".try_into().unwrap(), + SVCB::new(0, "https.example.com".try_into().unwrap()), + 3600, + ) + .svcb( + "foo".try_into().unwrap(), + SVCB::new(0, "protocol.example.com".try_into().unwrap()), + 3600, + ) + // Make sure SVCB only follows SVCB + .https( + "foo".try_into().unwrap(), + SVCB::new(0, "https.example.com".try_into().unwrap()), + 3600, + ) + .svcb( + "_foo".try_into().unwrap(), + SVCB::new(0, "protocol.example.com".try_into().unwrap()), + 3600, + ) + .sign(&keypair) + .unwrap(); + + let tld = keypair.public_key(); + + // Follow foo.tld HTTPS records + let endpoint = Endpoint::parse(&signed_packet, &format!("foo.{tld}"), false) + .pop() + .unwrap(); + assert_eq!(endpoint.domain(), "https.example.com"); + + // Follow _foo.tld SVCB records + let endpoint = Endpoint::parse(&signed_packet, &format!("_foo.{tld}"), true) + .pop() + .unwrap(); + assert_eq!(endpoint.domain(), "protocol.example.com"); + } + + #[test] + fn endpoint_to_socket_addrs() { + let mut svcb = SVCB::new(1, ".".try_into().unwrap()); + svcb.set_port(6881); + + let keypair = Keypair::random(); + let signed_packet = SignedPacket::builder() + .address( + ".".try_into().unwrap(), + "209.151.148.15".parse().unwrap(), + 3600, + ) + .address( + ".".try_into().unwrap(), + "2a05:d014:275:6201::64".parse().unwrap(), + 3600, + ) + .https(".".try_into().unwrap(), svcb, 3600) + .sign(&keypair) + .unwrap(); + + // Follow foo.tld HTTPS records + let endpoint = Endpoint::parse( + &signed_packet, + &signed_packet.public_key().to_string(), + false, + ) + .pop() + .unwrap(); + + assert_eq!(endpoint.domain(), "."); + + let mut addrs = endpoint.to_socket_addrs(); + addrs.sort(); + + assert_eq!( + addrs.into_iter().map(|s| s.to_string()).collect::>(), + vec!["209.151.148.15:6881", "[2a05:d014:275:6201::64]:6881"] + ) + } +} diff --git a/pkarr/src/extra/endpoints/mod.rs b/pkarr/src/extra/endpoints/mod.rs new file mode 100644 index 0000000..c63a785 --- /dev/null +++ b/pkarr/src/extra/endpoints/mod.rs @@ -0,0 +1,201 @@ +//! implementation of EndpointResolver trait for different clients + +mod endpoint; +mod resolver; + +pub use endpoint::Endpoint; +pub use resolver::EndpointsResolver; +use resolver::ResolveError; + +use crate::{PublicKey, SignedPacket}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +impl EndpointsResolver for crate::client::dht::Client { + async fn resolve(&self, public_key: &PublicKey) -> Result, ResolveError> { + self.resolve(public_key).await.map_err(|error| match error { + crate::client::dht::ClientWasShutdown => ResolveError::ClientWasShutdown, + }) + } +} + +#[cfg(any(target_arch = "wasm32", feature = "relay"))] +impl EndpointsResolver for crate::client::relay::Client { + async fn resolve(&self, public_key: &PublicKey) -> Result, ResolveError> { + self.resolve(public_key) + .await + .map_err(ResolveError::Reqwest) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::dns::rdata::SVCB; + use crate::{mainline::Testnet, Client, Keypair}; + use crate::{PublicKey, SignedPacket}; + + use std::future::Future; + use std::net::IpAddr; + use std::pin::Pin; + use std::str::FromStr; + + // TODO: test SVCB too. + + fn generate_subtree( + client: Client, + depth: u8, + branching: u8, + domain: Option, + ips: Vec, + port: Option, + ) -> Pin>> { + Box::pin(async move { + let keypair = Keypair::random(); + + let mut builder = SignedPacket::builder(); + + for _ in 0..branching { + let mut svcb = SVCB::new(0, ".".try_into().unwrap()); + + if depth == 0 { + svcb.priority = 1; + + if let Some(port) = port { + svcb.set_port(port); + } + + if let Some(target) = &domain { + let target: &'static str = Box::leak(target.clone().into_boxed_str()); + svcb.target = target.try_into().unwrap() + } + + for ip in ips.clone() { + builder = builder.address(".".try_into().unwrap(), ip, 3600); + } + } else { + let target = generate_subtree( + client.clone(), + depth - 1, + branching, + domain.clone(), + ips.clone(), + port, + ) + .await + .to_string(); + let target: &'static str = Box::leak(target.into_boxed_str()); + svcb.target = target.try_into().unwrap(); + }; + + builder = builder.https(".".try_into().unwrap(), svcb, 3600); + } + + let signed_packet = builder.sign(&keypair).unwrap(); + + client.publish(&signed_packet).await.unwrap(); + + keypair.public_key() + }) + } + + /// depth of (3): A -> B -> C + /// branch of (2): A -> B0, A -> B1 + /// domain, ips, and port are all at the end (C, or B1) + fn generate( + client: &Client, + depth: u8, + branching: u8, + domain: Option, + ips: Vec, + port: Option, + ) -> Pin>> { + generate_subtree(client.clone(), depth - 1, branching, domain, ips, port) + } + + #[tokio::test] + async fn direct_endpoint_resolution() { + let testnet = Testnet::new(3).unwrap(); + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let tld = generate(&client, 1, 1, Some("example.com".to_string()), vec![], None).await; + + let endpoint = client + .resolve_https_endpoint(&tld.to_string()) + .await + .unwrap(); + + assert_eq!(endpoint.domain(), "example.com"); + assert_eq!(endpoint.public_key(), &tld); + } + + #[tokio::test] + async fn resolve_endpoints() { + let testnet = Testnet::new(3).unwrap(); + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let tld = generate(&client, 3, 3, Some("example.com".to_string()), vec![], None).await; + + let endpoint = client + .resolve_https_endpoint(&tld.to_string()) + .await + .unwrap(); + + assert_eq!(endpoint.domain(), "example.com"); + } + + #[tokio::test] + async fn empty() { + let testnet = Testnet::new(3).unwrap(); + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let pubky = Keypair::random().public_key(); + + let endpoint = client.resolve_https_endpoint(&pubky.to_string()).await; + + assert!(endpoint.is_err()); + } + + #[tokio::test] + async fn max_chain_exceeded() { + let testnet = Testnet::new(3).unwrap(); + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let tld = generate(&client, 4, 3, Some("example.com".to_string()), vec![], None).await; + + let endpoint = client.resolve_https_endpoint(&tld.to_string()).await; + + assert!(endpoint.is_err()); + } + + #[tokio::test] + async fn resolve_addresses() { + let testnet = Testnet::new(3).unwrap(); + let client = Client::builder().testnet(&testnet).build().unwrap(); + + let tld = generate( + &client, + 3, + 3, + None, + vec![IpAddr::from_str("0.0.0.10").unwrap()], + Some(3000), + ) + .await; + + let endpoint = client + .resolve_https_endpoint(&tld.to_string()) + .await + .unwrap(); + + assert_eq!(endpoint.domain(), "."); + assert_eq!( + endpoint + .to_socket_addrs() + .into_iter() + .map(|s| s.to_string()) + .collect::>(), + vec!["0.0.0.10:3000"] + ); + } +} diff --git a/pkarr/src/extra/endpoints/resolver.rs b/pkarr/src/extra/endpoints/resolver.rs new file mode 100644 index 0000000..d0e2d76 --- /dev/null +++ b/pkarr/src/extra/endpoints/resolver.rs @@ -0,0 +1,139 @@ +//! EndpointResolver trait + +use futures_lite::{pin, Stream, StreamExt}; +use genawaiter::sync::Gen; + +use crate::{PublicKey, SignedPacket}; + +use super::Endpoint; + +const DEFAULT_MAX_CHAIN_LENGTH: u8 = 3; + +pub trait EndpointsResolver { + /// Returns an async stream of [HTTPS][crate::dns::rdata::RData::HTTPS] [Endpoint]s + fn resolve_https_endpoints(&self, qname: &str) -> impl Stream { + self.resolve_endpoints(qname, false) + } + + /// Returns an async stream of [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint]s + fn resolve_svcb_endpoints(&self, qname: &str) -> impl Stream { + self.resolve_endpoints(qname, true) + } + + /// Helper method that returns the first [HTTPS][crate::dns::rdata::RData::HTTPS] [Endpoint] in the Async stream from [EndpointsResolver::resolve_https_endpoints] + fn resolve_https_endpoint( + &self, + qname: &str, + ) -> impl std::future::Future> { + async move { + let stream = self.resolve_https_endpoints(qname); + + pin!(stream); + + match stream.next().await { + Some(endpoint) => Ok(endpoint), + None => { + tracing::trace!(?qname, "failed to resolve endpoint"); + Err(FailedToResolveEndpoint) + } + } + } + } + + /// Helper method that returns the first [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint] in the Async stream from [EndpointsResolver::resolve_svcb_endpoints] + fn resolve_svcb_endpoint( + &self, + qname: &str, + ) -> impl std::future::Future> { + async move { + let stream = self.resolve_https_endpoints(qname); + + pin!(stream); + + match stream.next().await { + Some(endpoint) => Ok(endpoint), + None => Err(FailedToResolveEndpoint), + } + } + } + + /// A wrapper around the specific Pkarr client's resolve method. + fn resolve( + &self, + public_key: &PublicKey, + ) -> impl std::future::Future, ResolveError>>; + + /// Returns an async stream of either [HTTPS][crate::dns::rdata::RData::HTTPS] or [SVCB][crate::dns::rdata::RData::SVCB] [Endpoint]s + fn resolve_endpoints(&self, qname: &str, is_svcb: bool) -> impl Stream { + Gen::new(|co| async move { + // TODO: cache the result of this function? + // TODO: test load balancing + // TODO: test failover + // TODO: custom max_chain_length + + let mut depth = 0; + let mut stack: Vec = Vec::new(); + + // Initialize the stack with endpoints from the starting domain. + if let Ok(tld) = PublicKey::try_from(qname) { + if let Ok(Some(signed_packet)) = self.resolve(&tld).await { + depth += 1; + stack.extend(Endpoint::parse(&signed_packet, qname, is_svcb)); + } + } + + while let Some(next) = stack.pop() { + let current = next.domain(); + + // Attempt to resolve the domain as a public key. + match PublicKey::try_from(current) { + Ok(tld) => match self.resolve(&tld).await { + Ok(Some(signed_packet)) if depth < DEFAULT_MAX_CHAIN_LENGTH => { + depth += 1; + let endpoints = Endpoint::parse(&signed_packet, current, is_svcb); + + tracing::trace!(?qname, ?depth, ?endpoints, "resolved endpoints"); + + stack.extend(endpoints); + } + _ => break, // Stop on resolution failure or chain length exceeded. + }, + // Yield if the domain is not pointing to another Pkarr TLD domain. + Err(_) => co.yield_(next).await, + } + } + }) + } +} + +#[derive(thiserror::Error, Debug)] +/// Resolve Error from a client +pub enum ResolveError { + ClientWasShutdown, + #[cfg(any(target_arch = "wasm32", feature = "relay"))] + Reqwest(reqwest::Error), +} + +impl std::fmt::Display for ResolveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Resolve endpoint error from the client::resolve {:?}", + self + ) + } +} + +#[derive(Debug)] +pub struct FailedToResolveEndpoint; + +impl std::error::Error for FailedToResolveEndpoint {} + +impl std::fmt::Display for FailedToResolveEndpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Could not resolve clear net endpoint for the Pkarr domain" + ) + } +} diff --git a/pkarr/src/extra/lmdb_cache.rs b/pkarr/src/extra/lmdb_cache.rs new file mode 100644 index 0000000..3d1cdc0 --- /dev/null +++ b/pkarr/src/extra/lmdb_cache.rs @@ -0,0 +1,430 @@ +//! Persistent [crate::base::cache::Cache] implementation using LMDB's bindings [heed] + +use std::{ + borrow::Cow, + fs, + path::Path, + sync::{Arc, RwLock}, + time::Duration, +}; + +use byteorder::LittleEndian; +use heed::{ + types::U64, BoxedError, BytesDecode, BytesEncode, Database, Env, EnvOpenOptions, RwTxn, +}; +use libc::{sysconf, _SC_PAGESIZE}; + +use tracing::debug; + +use pubky_timestamp::Timestamp; + +use crate::{ + base::cache::{Cache, CacheKey}, + SignedPacket, +}; + +const MAX_MAP_SIZE: usize = 10995116277760; // 10 TB +const MIN_MAP_SIZE: usize = 10 * 1024 * 1024; // 10 mb + +const SIGNED_PACKET_TABLE: &str = "pkarrcache:signed_packet"; +const KEY_TO_TIME_TABLE: &str = "pkarrcache:key_to_time"; +const TIME_TO_KEY_TABLE: &str = "pkarrcache:time_to_key"; + +type SignedPacketsTable = Database; +type KeyToTimeTable = Database>; +type TimeToKeyTable = Database, CacheKeyCodec>; + +pub struct CacheKeyCodec; + +impl<'a> BytesEncode<'a> for CacheKeyCodec { + type EItem = CacheKey; + + fn bytes_encode(key: &Self::EItem) -> Result, BoxedError> { + Ok(Cow::Owned(key.to_vec())) + } +} + +impl<'a> BytesDecode<'a> for CacheKeyCodec { + type DItem = CacheKey; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + let key: [u8; 20] = bytes.try_into()?; + Ok(key) + } +} + +pub struct SignedPacketCodec; + +impl<'a> BytesEncode<'a> for SignedPacketCodec { + type EItem = SignedPacket; + + fn bytes_encode(signed_packet: &Self::EItem) -> Result, BoxedError> { + Ok(Cow::Owned(signed_packet.serialize().to_vec())) + } +} + +impl<'a> BytesDecode<'a> for SignedPacketCodec { + type DItem = SignedPacket; + + fn bytes_decode(bytes: &'a [u8]) -> Result { + Ok(SignedPacket::deserialize(bytes)?) + } +} + +#[derive(Debug, Clone)] +/// Persistent [crate::base::cache::Cache] implementation using LMDB's bindings [heed] +pub struct LmdbCache { + capacity: usize, + env: Env, + signed_packets_table: SignedPacketsTable, + key_to_time_table: KeyToTimeTable, + time_to_key_table: TimeToKeyTable, + batch: Arc>>, +} + +impl LmdbCache { + /// Creates a new [LmdbCache] at the `env_path` and set the [heed::EnvOpenOptions::map_size] + /// to a multiple of the `capacity` by [SignedPacket::MAX_BYTES], aligned to system's page size, + /// a maximum of 10 TB, and a minimum of 10 MB. + pub fn new(env_path: &Path, capacity: usize) -> Result { + let page_size = unsafe { sysconf(_SC_PAGESIZE) as usize }; + + // Page aligned but more than enough bytes for `capacity` many SignedPacket + let map_size = capacity + .checked_mul(SignedPacket::MAX_BYTES as usize) + .and_then(|x| x.checked_add(page_size)) + .and_then(|x| x.checked_div(page_size)) + .and_then(|x| x.checked_mul(page_size)) + .unwrap_or(MAX_MAP_SIZE) + .max(MIN_MAP_SIZE); + + fs::create_dir_all(env_path)?; + + let env = unsafe { + EnvOpenOptions::new() + .map_size(map_size) + .max_dbs(3) + .open(env_path)? + }; + + let mut wtxn = env.write_txn()?; + + let signed_packets_table: SignedPacketsTable = + env.create_database(&mut wtxn, Some(SIGNED_PACKET_TABLE))?; + let key_to_time_table: KeyToTimeTable = + env.create_database(&mut wtxn, Some(KEY_TO_TIME_TABLE))?; + let time_to_key_table: TimeToKeyTable = + env.create_database(&mut wtxn, Some(TIME_TO_KEY_TABLE))?; + + wtxn.commit()?; + + let instance = Self { + capacity, + env, + signed_packets_table, + key_to_time_table, + time_to_key_table, + batch: Arc::new(RwLock::new(vec![])), + }; + + let clone = instance.clone(); + std::thread::spawn(move || loop { + debug!(size = ?clone.len(), "Cache stats"); + std::thread::sleep(Duration::from_secs(60)); + }); + + Ok(instance) + } + + pub fn internal_len(&self) -> Result { + let rtxn = self.env.read_txn()?; + let len = self.signed_packets_table.len(&rtxn)? as usize; + rtxn.commit()?; + + Ok(len) + } + + pub fn internal_put( + &self, + key: &CacheKey, + signed_packet: &SignedPacket, + ) -> Result<(), heed::Error> { + if self.capacity == 0 { + return Ok(()); + } + + let mut wtxn = self.env.write_txn()?; + + let packets = self.signed_packets_table; + let key_to_time = self.key_to_time_table; + let time_to_key = self.time_to_key_table; + + let batch = self.batch.read().expect("LmdbCache::batch.read()"); + update_lru(&mut wtxn, packets, key_to_time, time_to_key, &batch)?; + + let len = packets.len(&wtxn)? as usize; + + if len >= self.capacity { + debug!(?len, ?self.capacity, "Reached cache capacity, deleting extra item."); + + let mut iter = time_to_key.iter(&wtxn)?; + + if let Some((time, key)) = iter.next().transpose()? { + drop(iter); + + time_to_key.delete(&mut wtxn, &time)?; + key_to_time.delete(&mut wtxn, &key)?; + packets.delete(&mut wtxn, &key)?; + }; + } + + if let Some(old_time) = key_to_time.get(&wtxn, key)? { + time_to_key.delete(&mut wtxn, &old_time)?; + } + + let new_time = Timestamp::now(); + + time_to_key.put(&mut wtxn, &new_time.as_u64(), key)?; + key_to_time.put(&mut wtxn, key, &new_time.as_u64())?; + + packets.put(&mut wtxn, key, signed_packet)?; + + wtxn.commit()?; + + Ok(()) + } + + pub fn internal_get(&self, key: &CacheKey) -> Result, heed::Error> { + self.batch + .write() + .expect("LmdbCache::batch.write()") + .push(*key); + + self.internal_get_read_only(key) + } + + pub fn internal_get_read_only( + &self, + key: &CacheKey, + ) -> Result, heed::Error> { + let rtxn = self.env.read_txn()?; + + if let Some(signed_packet) = self.signed_packets_table.get(&rtxn, key)? { + return Ok(Some(signed_packet)); + } + + rtxn.commit()?; + + Ok(None) + } +} + +fn update_lru( + wtxn: &mut RwTxn, + packets: SignedPacketsTable, + key_to_time: KeyToTimeTable, + time_to_key: TimeToKeyTable, + to_update: &[CacheKey], +) -> Result<(), heed::Error> { + for key in to_update { + if packets.get(wtxn, key)?.is_some() { + if let Some(time) = key_to_time.get(wtxn, key)? { + time_to_key.delete(wtxn, &time)?; + }; + + let new_time = Timestamp::now(); + + time_to_key.put(wtxn, &new_time.as_u64(), key)?; + key_to_time.put(wtxn, key, &new_time.as_u64())?; + } + } + + Ok(()) +} + +impl Cache for LmdbCache { + fn len(&self) -> usize { + match self.internal_len() { + Ok(result) => result, + Err(error) => { + debug!(?error, "Error in LmdbCache::len"); + 0 + } + } + } + + fn put(&self, key: &CacheKey, signed_packet: &SignedPacket) { + if let Err(error) = self.internal_put(key, signed_packet) { + debug!(?error, "Error in LmdbCache::put"); + }; + } + + fn get(&self, key: &CacheKey) -> Option { + match self.internal_get(key) { + Ok(result) => result, + Err(error) => { + debug!(?error, "Error in LmdbCache::get"); + + None + } + } + } + + fn get_read_only(&self, key: &CacheKey) -> Option { + match self.internal_get_read_only(key) { + Ok(result) => result, + Err(error) => { + debug!(?error, "Error in LmdbCache::get"); + + None + } + } + } +} + +#[derive(thiserror::Error, Debug)] +/// Pkarr crate error enum. +pub enum Error { + #[error(transparent)] + /// Transparent [heed::Error] + Lmdb(#[from] heed::Error), + + #[error(transparent)] + /// Transparent [std::io::Error] + IO(#[from] std::io::Error), +} + +#[cfg(test)] +mod tests { + use crate::Keypair; + + use super::*; + + #[test] + fn max_map_size() { + let env_path = std::env::temp_dir().join(Timestamp::now().to_string()); + + LmdbCache::new(&env_path, usize::MAX).unwrap(); + } + + #[test] + fn lru_capacity() { + let env_path = std::env::temp_dir().join(Timestamp::now().to_string()); + + let cache = LmdbCache::new(&env_path, 2).unwrap(); + + let mut keys = vec![]; + + for i in 0..2 { + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), i) + .sign(&Keypair::random()) + .unwrap(); + + let key = CacheKey::from(signed_packet.public_key()); + cache.put(&key, &signed_packet); + + keys.push((key, signed_packet)); + } + + assert_eq!( + cache.get_read_only(&keys.first().unwrap().0).unwrap(), + keys.first().unwrap().1, + "first key saved" + ); + assert_eq!( + cache.get_read_only(&keys.last().unwrap().0).unwrap(), + keys.last().unwrap().1, + "second key saved" + ); + + assert_eq!(cache.len(), 2); + + // Put another one, effectively deleting the oldest. + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 3) + .sign(&Keypair::random()) + .unwrap(); + let key = CacheKey::from(signed_packet.public_key()); + cache.put(&key, &signed_packet); + + assert_eq!(cache.len(), 2); + + assert!( + cache.get_read_only(&keys.first().unwrap().0).is_none(), + "oldest key dropped" + ); + assert_eq!( + cache.get_read_only(&keys.last().unwrap().0).unwrap(), + keys.last().unwrap().1, + "more recent key survived" + ); + assert_eq!( + cache.get_read_only(&key).unwrap(), + signed_packet, + "most recent key survived" + ) + } + + #[test] + fn lru_capacity_refresh_oldest() { + let env_path = std::env::temp_dir().join(Timestamp::now().to_string()); + + let cache = LmdbCache::new(&env_path, 2).unwrap(); + + let mut keys = vec![]; + + for i in 0..2 { + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), i) + .sign(&Keypair::random()) + .unwrap(); + + let key = CacheKey::from(signed_packet.public_key()); + cache.put(&key, &signed_packet); + + keys.push((key, signed_packet)); + } + + assert_eq!( + cache.get_read_only(&keys.first().unwrap().0).unwrap(), + keys.first().unwrap().1, + "first key saved" + ); + assert_eq!( + cache.get_read_only(&keys.last().unwrap().0).unwrap(), + keys.last().unwrap().1, + "second key saved" + ); + + // refresh the oldest + cache.get(&keys.first().unwrap().0).unwrap(); + + assert_eq!(cache.len(), 2); + + // Put another one, effectively deleting the oldest. + let signed_packet = SignedPacket::builder() + .txt("foo".try_into().unwrap(), "bar".try_into().unwrap(), 3) + .sign(&Keypair::random()) + .unwrap(); + let key = CacheKey::from(signed_packet.public_key()); + cache.put(&key, &signed_packet); + + assert_eq!(cache.len(), 2); + + assert!( + cache.get_read_only(&keys.last().unwrap().0).is_none(), + "oldest key dropped" + ); + assert_eq!( + cache.get_read_only(&keys.first().unwrap().0).unwrap(), + keys.first().unwrap().1, + "refreshed key survived" + ); + assert_eq!( + cache.get_read_only(&key).unwrap(), + signed_packet, + "most recent key survived" + ) + } +} diff --git a/pkarr/src/extra/mod.rs b/pkarr/src/extra/mod.rs new file mode 100644 index 0000000..3ec4dff --- /dev/null +++ b/pkarr/src/extra/mod.rs @@ -0,0 +1,105 @@ +#[cfg(feature = "endpoints")] +pub mod endpoints; + +#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-resolve"))] +pub mod reqwest; + +#[cfg(all(not(target_arch = "wasm32"), feature = "tls"))] +pub mod tls; + +#[cfg(all(not(target_arch = "wasm32"), feature = "lmdb-cache"))] +pub mod lmdb_cache; + +#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-builder"))] +impl From for ::reqwest::ClientBuilder { + /// Create a [reqwest::ClientBuilder][::reqwest::ClientBuilder] from this Pkarr client, + /// using it as a [dns_resolver][::reqwest::ClientBuilder::dns_resolver], + /// and a [preconfigured_tls][::reqwest::ClientBuilder::use_preconfigured_tls] client + /// config that uses [rustls::crypto::ring::default_provider()] and follows the + /// [tls for pkarr domains](https://pkarr.org/tls) spec. + fn from(client: crate::Client) -> Self { + ::reqwest::ClientBuilder::new() + .dns_resolver(std::sync::Arc::new(client.clone())) + .use_preconfigured_tls(rustls::ClientConfig::from(client)) + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "reqwest-builder"))] +impl From for ::reqwest::ClientBuilder { + /// Create a [reqwest::ClientBuilder][::reqwest::ClientBuilder] from this Pkarr client, + /// using it as a [dns_resolver][::reqwest::ClientBuilder::dns_resolver], + /// and a [preconfigured_tls][::reqwest::ClientBuilder::use_preconfigured_tls] client + /// config that uses [rustls::crypto::ring::default_provider()] and follows the + /// [tls for pkarr domains](https://pkarr.org/tls) spec. + fn from(client: crate::client::relay::Client) -> Self { + ::reqwest::ClientBuilder::new() + .dns_resolver(std::sync::Arc::new(client.clone())) + .use_preconfigured_tls(rustls::ClientConfig::from(client)) + } +} + +#[cfg(test)] +mod tests { + use mainline::Testnet; + use std::net::SocketAddr; + use std::net::TcpListener; + use std::sync::Arc; + + use axum::{routing::get, Router}; + use axum_server::tls_rustls::RustlsConfig; + + use crate::{dns::rdata::SVCB, Client, Keypair, SignedPacket}; + + async fn publish_server_pkarr(client: &Client, keypair: &Keypair, socket_addr: &SocketAddr) { + let mut svcb = SVCB::new(0, ".".try_into().unwrap()); + svcb.set_port(socket_addr.port()); + + let signed_packet = SignedPacket::builder() + .https(".".try_into().unwrap(), svcb, 60 * 60) + .address(".".try_into().unwrap(), socket_addr.ip(), 60 * 60) + .sign(&keypair) + .unwrap(); + + client.publish(&signed_packet).await.unwrap(); + } + + #[tokio::test] + async fn reqwest_pkarr_domain() { + let testnet = Testnet::new(3).unwrap(); + + let keypair = Keypair::random(); + + { + // Run a server on Pkarr + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); // Bind to any available port + let address = listener.local_addr().unwrap(); + + let client = Client::builder().testnet(&testnet).build().unwrap(); + publish_server_pkarr(&client, &keypair, &address).await; + + println!("Server running on https://{}", keypair.public_key()); + + let server = axum_server::from_tcp_rustls( + listener, + RustlsConfig::from_config(Arc::new((&keypair).into())), + ); + + tokio::spawn(server.serve(app.into_make_service())); + } + + // Client setup + let pkarr_client = Client::builder().testnet(&testnet).build().unwrap(); + let reqwest = reqwest::ClientBuilder::from(pkarr_client).build().unwrap(); + + // Make a request + let response = reqwest + .get(format!("https://{}", keypair.public_key())) + .send() + .await + .unwrap(); + + assert_eq!(response.status(), reqwest::StatusCode::OK); + assert_eq!(response.text().await.unwrap(), "Hello, world!"); + } +} diff --git a/pkarr/src/extra/reqwest.rs b/pkarr/src/extra/reqwest.rs new file mode 100644 index 0000000..9fb3583 --- /dev/null +++ b/pkarr/src/extra/reqwest.rs @@ -0,0 +1,45 @@ +use reqwest::dns::{Addrs, Resolve}; + +use crate::{Client, PublicKey}; + +use super::endpoints::EndpointsResolver; + +use std::net::ToSocketAddrs; + +impl Resolve for Client { + fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { + let client = self.clone(); + Box::pin(resolve(client, name)) + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] +impl Resolve for crate::client::relay::Client { + fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving { + let client = self.clone(); + Box::pin(resolve(client, name)) + } +} + +async fn resolve( + client: impl EndpointsResolver, + name: reqwest::dns::Name, +) -> Result> { + let name = name.as_str(); + + if PublicKey::try_from(name).is_ok() { + let endpoint = client.resolve_https_endpoint(name).await?; + + let addrs = endpoint.to_socket_addrs().into_iter(); + + tracing::trace!(?name, ?endpoint, ?addrs, "Resolved an endpoint"); + + return Ok(Box::new(addrs.into_iter())); + }; + + Ok(Box::new( + format!("{name}:0") + .to_socket_addrs() + .expect("formatting a name and port to socket address"), + )) +} diff --git a/pkarr/src/extra/tls.rs b/pkarr/src/extra/tls.rs new file mode 100644 index 0000000..3300134 --- /dev/null +++ b/pkarr/src/extra/tls.rs @@ -0,0 +1,167 @@ +use std::{fmt::Debug, sync::Arc}; + +use futures_lite::{pin, stream::block_on}; +use rustls::{ + client::danger::{DangerousClientConfigBuilder, ServerCertVerified, ServerCertVerifier}, + crypto::{verify_tls13_signature_with_raw_key, WebPkiSupportedAlgorithms}, + pki_types::SubjectPublicKeyInfoDer, + CertificateError, SignatureScheme, +}; +use tracing::{instrument, Level}; + +use crate::Client; + +use crate::extra::endpoints::EndpointsResolver; + +#[derive(Debug)] +pub struct CertVerifier(T); + +static SUPPORTED_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { + all: &[webpki::ring::ED25519], + mapping: &[(SignatureScheme::ED25519, &[webpki::ring::ED25519])], +}; + +impl ServerCertVerifier for CertVerifier { + #[instrument(ret(level = Level::TRACE), err(level = Level::TRACE))] + /// Verify Pkarr public keys + fn verify_server_cert( + &self, + endpoint_certificate: &rustls::pki_types::CertificateDer<'_>, + intermediates: &[rustls::pki_types::CertificateDer<'_>], + host_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + if !intermediates.is_empty() { + return Err(rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer, + )); + } + + let end_entity_as_spki = SubjectPublicKeyInfoDer::from(endpoint_certificate.as_ref()); + let expected_spki = end_entity_as_spki.as_ref(); + + let qname = host_name.to_str(); + + // Resolve HTTPS endpoints and hope that the cached SignedPackets didn't chance + // since the last time we resolved endpoints to establish the connection in the + // first place. + let stream = self.0.resolve_https_endpoints(&qname); + pin!(stream); + for endpoint in block_on(stream) { + if endpoint.public_key().to_public_key_der().as_bytes() == expected_spki { + return Ok(ServerCertVerified::assertion()); + } + } + + // Repeat for SVCB endpoints + let stream = self.0.resolve_svcb_endpoints(&qname); + pin!(stream); + for endpoint in block_on(stream) { + if endpoint.public_key().to_public_key_der().as_bytes() == expected_spki { + return Ok(ServerCertVerified::assertion()); + } + } + + Err(rustls::Error::InvalidCertificate( + CertificateError::UnknownIssuer, + )) + } + + #[instrument(ret(level = Level::DEBUG), err(level = Level::DEBUG))] + /// Verify a message signature using a raw public key and the first TLS 1.3 compatible + /// supported scheme. + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + verify_tls13_signature_with_raw_key( + message, + &SubjectPublicKeyInfoDer::from(cert.as_ref()), + dss, + &SUPPORTED_ALGORITHMS, + ) + } + + #[instrument(ret(level = Level::DEBUG), err(level = Level::DEBUG))] + /// Verify a message signature using a raw public key and the first TLS 1.3 compatible + /// supported scheme. + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + verify_tls13_signature_with_raw_key( + message, + &SubjectPublicKeyInfoDer::from(cert.as_ref()), + dss, + &SUPPORTED_ALGORITHMS, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![SignatureScheme::ED25519] + } + + fn requires_raw_public_keys(&self) -> bool { + true + } +} + +impl CertVerifier { + pub(crate) fn new(pkarr_client: T) -> Self { + CertVerifier(pkarr_client) + } +} + +impl From for CertVerifier { + fn from(pkarr_client: Client) -> Self { + CertVerifier::new(pkarr_client) + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] +impl From for CertVerifier { + fn from(pkarr_client: crate::client::relay::Client) -> Self { + CertVerifier::new(pkarr_client) + } +} + +impl From for rustls::ClientConfig { + /// Creates a [rustls::ClientConfig] that uses [rustls::crypto::ring::default_provider()] + /// and no client auth and follows the [tls for pkarr domains](https://pkarr.org/tls) spec. + /// + /// If you want more control, create a [CertVerifier] from this [Client] to use as a [custom certificate verifier][DangerousClientConfigBuilder::with_custom_certificate_verifier]. + fn from(client: Client) -> Self { + let verifier: CertVerifier = client.into(); + + create_client_config_with_ring() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth() + } +} + +#[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] +impl From for rustls::ClientConfig { + /// Creates a [rustls::ClientConfig] that uses [rustls::crypto::ring::default_provider()] + /// and no client auth and follows the [tls for pkarr domains](https://pkarr.org/tls) spec. + /// + /// If you want more control, create a [CertVerifier] from this [Client] to use as a [custom certificate verifier][DangerousClientConfigBuilder::with_custom_certificate_verifier]. + fn from(client: crate::client::relay::Client) -> Self { + let verifier: CertVerifier = client.into(); + + create_client_config_with_ring() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_no_client_auth() + } +} + +fn create_client_config_with_ring() -> DangerousClientConfigBuilder { + rustls::ClientConfig::builder_with_provider(rustls::crypto::ring::default_provider().into()) + .with_safe_default_protocol_versions() + .expect("version supported by ring") + .dangerous() +} diff --git a/pkarr/src/lib.rs b/pkarr/src/lib.rs index 7fffe55..87f69dd 100644 --- a/pkarr/src/lib.rs +++ b/pkarr/src/lib.rs @@ -3,22 +3,15 @@ #![doc = document_features::document_features!()] //! -macro_rules! if_dht { - ($($item:item)*) => {$( - #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] - $item - )*} -} - // Modules -mod error; -mod keys; -mod signed_packet; +mod base; +pub mod client; +pub mod extra; -// Common exports -pub use crate::error::{Error, Result}; -pub use crate::keys::{Keypair, PublicKey}; -pub use crate::signed_packet::{system_time, SignedPacket}; +// Exports +pub use base::cache::{Cache, CacheKey, InMemoryCache}; +pub use base::keys::{Keypair, PublicKey}; +pub use base::signed_packet::SignedPacket; /// Default minimum TTL: 5 minutes pub const DEFAULT_MINIMUM_TTL: u32 = 300; @@ -26,57 +19,32 @@ pub const DEFAULT_MINIMUM_TTL: u32 = 300; pub const DEFAULT_MAXIMUM_TTL: u32 = 24 * 60 * 60; /// Default cache size: 1000 pub const DEFAULT_CACHE_SIZE: usize = 1000; +/// Default [relay](https://pkarr.org/relays)s +pub const DEFAULT_RELAYS: [&str; 2] = ["https://relay.pkarr.org", "https://pkarr.pubky.org"]; +/// Default [resolver](https://pkarr.org/resolvers)s +pub const DEFAULT_RESOLVERS: [&str; 2] = ["resolver.pkarr.org:6881", "pkarr.pubky.org:6881"]; -pub const DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"]; - -pub const DEFAULT_RESOLVERS: [&str; 1] = ["resolver.pkarr.org:6881"]; +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +pub use client::dht::Info; +#[cfg(any(target_arch = "wasm32", feature = "dht"))] +pub use client::{Client, Settings}; // Rexports pub use bytes; pub use simple_dns as dns; -#[cfg(not(target_arch = "wasm32"))] -macro_rules! if_async { - ($($item:item)*) => {$( - #[cfg(all(not(target_arch = "wasm32"), feature = "async"))] - $item - )*} -} - -macro_rules! if_relay { - ($($item:item)*) => {$( - #[cfg(all(not(target_arch = "wasm32"), feature = "relay"))] - $item - )*} -} +#[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] +pub use mainline; -if_dht! { - mod cache; - mod client; +pub mod errors { + //! Exported errors - if_async! { - mod client_async; - pub use client_async::PkarrClientAsync; - } + #[cfg(all(not(target_arch = "wasm32"), feature = "dht"))] + pub use super::client::dht::{ClientWasShutdown, PublishError}; - pub use client::{PkarrClientBuilder, PkarrClient, Settings}; - pub use cache::{PkarrCache, PkarrCacheKey, InMemoryPkarrCache}; + #[cfg(any(target_arch = "wasm32", feature = "relay"))] + pub use super::client::relay::{EmptyListOfRelays, PublishToRelayError}; - // Rexports - pub use mainline; + pub use super::base::keys::PublicKeyError; + pub use super::base::signed_packet::SignedPacketError; } - -if_relay! { - mod relay_client; - pub use relay_client::{PkarrRelayClient, RelaySettings}; - - if_async! { - mod relay_client_async; - pub use relay_client_async::PkarrRelayClientAsync; - } -} - -#[cfg(target_arch = "wasm32")] -mod relay_client_web; -#[cfg(target_arch = "wasm32")] -pub use relay_client_web::PkarrRelayClient; diff --git a/pkarr/src/relay_client.rs b/pkarr/src/relay_client.rs deleted file mode 100644 index e49193a..0000000 --- a/pkarr/src/relay_client.rs +++ /dev/null @@ -1,332 +0,0 @@ -//! Pkarr client for publishing and resolving [SignedPacket]s over [relays](https://pkarr.org/relays). - -use std::{ - io::Read, - num::NonZeroUsize, - sync::{Arc, Mutex}, - thread, -}; - -use flume::Receiver; -use lru::LruCache; -use tracing::debug; -use ureq::Agent; - -use crate::{ - Error, PublicKey, Result, SignedPacket, DEFAULT_CACHE_SIZE, DEFAULT_MAXIMUM_TTL, - DEFAULT_MINIMUM_TTL, DEFAULT_RELAYS, -}; - -#[derive(Debug, Clone)] -/// [PkarrRelayClient]'s settings -pub struct RelaySettings { - pub relays: Vec, - /// Defaults to [DEFAULT_CACHE_SIZE] - pub cache_size: NonZeroUsize, - /// Used in the `min` parameter in [SignedPacket::expires_in]. - /// - /// Defaults to [DEFAULT_MINIMUM_TTL] - pub minimum_ttl: u32, - /// Used in the `max` parametere in [SignedPacket::expires_in]. - /// - /// Defaults to [DEFAULT_MAXIMUM_TTL] - pub maximum_ttl: u32, - /// Custom [ureq::Agent] - pub http_client: Agent, -} - -impl Default for RelaySettings { - fn default() -> Self { - Self { - relays: DEFAULT_RELAYS.map(|s| s.into()).to_vec(), - cache_size: NonZeroUsize::new(DEFAULT_CACHE_SIZE).unwrap(), - minimum_ttl: DEFAULT_MINIMUM_TTL, - maximum_ttl: DEFAULT_MAXIMUM_TTL, - http_client: ureq::Agent::new(), - } - } -} - -#[derive(Debug, Clone)] -/// Pkarr client for publishing and resolving [SignedPacket]s over [relays](https://pkarr.org/relays). -pub struct PkarrRelayClient { - http_client: Agent, - relays: Vec, - cache: Arc>>, - minimum_ttl: u32, - maximum_ttl: u32, -} - -impl Default for PkarrRelayClient { - fn default() -> Self { - Self::new(RelaySettings::default()).unwrap() - } -} - -impl PkarrRelayClient { - pub fn new(settings: RelaySettings) -> Result { - if settings.relays.is_empty() { - return Err(Error::EmptyListOfRelays); - } - - Ok(Self { - http_client: settings.http_client, - relays: settings.relays, - cache: Arc::new(Mutex::new(LruCache::new(settings.cache_size))), - minimum_ttl: settings.minimum_ttl, - maximum_ttl: settings.maximum_ttl, - }) - } - - /// Returns a reference to the internal cache. - pub fn cache(&self) -> &Mutex> { - self.cache.as_ref() - } - - /// Publishes a [SignedPacket] to this client's relays. - /// - /// Return the first successful completion, or the last failure. - /// - /// # Errors - /// - Returns a [Error::NotMostRecent] if the provided signed packet is older than most recent. - /// - Returns a [Error::RelayError] from the last responding relay, if all relays - /// responded with non-2xx status codes. - pub fn publish(&self, signed_packet: &SignedPacket) -> Result<()> { - let mut last_error = Error::EmptyListOfRelays; - - while let Ok(response) = self.publish_inner(signed_packet)?.recv() { - match response { - Ok(_) => return Ok(()), - Err(error) => { - last_error = error; - } - } - } - - Err(last_error) - } - - /// Resolve a [SignedPacket] from this client's relays. - /// - /// Return the first successful response, or the failure from the last responding relay. - /// - /// # Errors - /// - /// - Returns [Error::RelayError] if the relay responded with a status >= 400 - /// (except 404 in which case you should receive Ok(None)) or something wrong - /// with the transport, transparent from [ureq::Error]. - /// - Returns [Error::IO] if something went wrong while reading the payload. - pub fn resolve(&self, public_key: &PublicKey) -> Result> { - if let Some(signed_packet) = self.resolve_inner(public_key).recv()?? { - self.cache - .lock() - .unwrap() - .put(public_key.clone(), signed_packet.clone()); - - return Ok(Some(signed_packet)); - }; - - Ok(None) - } - - // === Private Methods === - - pub(crate) fn publish_inner( - &self, - signed_packet: &SignedPacket, - ) -> Result>> { - let public_key = signed_packet.public_key(); - let mut cache = self.cache.lock().unwrap(); - - if let Some(current) = cache.get(&public_key) { - if current.timestamp() > signed_packet.timestamp() { - return Err(Error::NotMostRecent); - } - }; - - cache.put(public_key.to_owned(), signed_packet.clone()); - drop(cache); - - let (sender, receiver) = flume::bounded::>(1); - - for relay in self.relays.clone() { - let url = format!("{relay}/{public_key}"); - let http_client = self.http_client.clone(); - let sender = sender.clone(); - let signed_packet = signed_packet.clone(); - - thread::spawn(move || { - match http_client - .put(&url) - .send_bytes(&signed_packet.to_relay_payload()) - { - Ok(_) => { - let _ = sender.send(Ok(())); - } - Err(error) => { - debug!(?url, ?error, "Error response"); - let _ = sender.send(Err(Error::RelayError(Box::new(error)))); - } - } - }); - } - - Ok(receiver) - } - - pub(crate) fn resolve_inner( - &self, - public_key: &PublicKey, - ) -> Receiver>> { - let mut cache = self.cache.lock().unwrap(); - - let cached_packet = cache.get(public_key); - - let (sender, receiver) = flume::bounded::>>(1); - - if let Some(cached) = cached_packet { - let expires_in = cached.expires_in(self.minimum_ttl, self.maximum_ttl); - - if expires_in > 0 { - debug!(expires_in, "Have fresh signed_packet in cache."); - let _ = sender.send(Ok(Some(cached.clone()))); - - return receiver; - } - - debug!(expires_in, "Have expired signed_packet in cache."); - } else { - debug!("Cache mess"); - }; - - for relay in self.relays.clone() { - let url = format!("{relay}/{public_key}"); - let http_client = self.http_client.clone(); - let public_key = public_key.clone(); - let cached_packet = cached_packet.cloned(); - let sender = sender.clone(); - - thread::spawn(move || match http_client.get(&url).call() { - Ok(response) => { - let mut reader = response.into_reader(); - let mut payload = vec![]; - - if let Err(err) = reader.read_to_end(&mut payload) { - let _ = sender.send(Err(err.into())); - } else { - match SignedPacket::from_relay_payload(&public_key, &payload.into()) { - Ok(signed_packet) => { - let new_packet = if let Some(ref cached) = cached_packet { - if signed_packet.more_recent_than(cached) { - debug!( - ?public_key, - "Received more recent packet than in cache" - ); - Some(signed_packet) - } else { - None - } - } else { - debug!(?public_key, "Received new packet after cache miss"); - Some(signed_packet) - }; - - let _ = sender.send(Ok(new_packet)); - } - Err(error) => { - debug!(?url, ?error, "Invalid signed_packet"); - let _ = sender.send(Err(error)); - } - }; - } - } - Err(ureq::Error::Status(404, _)) => { - debug!(?url, "SignedPacket not found"); - let _ = sender.send(Ok(None)); - } - Err(error) => { - debug!(?url, ?error, "Error response"); - let _ = sender.send(Err(Error::RelayError(Box::new(error)))); - } - }); - } - - receiver - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{dns, Keypair, SignedPacket}; - - #[test] - fn publish_resolve() { - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let mut server = mockito::Server::new(); - - let path = format!("/{}", signed_packet.public_key()); - - server - .mock("PUT", path.as_str()) - .with_header("content-type", "text/plain") - .with_status(200) - .create(); - server - .mock("GET", path.as_str()) - .with_body(signed_packet.to_relay_payload()) - .create(); - - let relays: Vec = vec![server.url()]; - let settings = RelaySettings { - relays, - ..RelaySettings::default() - }; - - let a = PkarrRelayClient::new(settings.clone()).unwrap(); - let b = PkarrRelayClient::new(settings).unwrap(); - - a.publish(&signed_packet).unwrap(); - - let resolved = b.resolve(&keypair.public_key()).unwrap().unwrap(); - - assert_eq!(a.cache().lock().unwrap().len(), 1); - assert_eq!(b.cache().lock().unwrap().len(), 1); - - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - } - - #[test] - fn not_found() { - let keypair = Keypair::random(); - - let mut server = mockito::Server::new(); - - let path = format!("/{}", keypair.public_key()); - - server.mock("GET", path.as_str()).with_status(404).create(); - - let relays: Vec = vec![server.url()]; - let settings = RelaySettings { - relays, - ..RelaySettings::default() - }; - - let client = PkarrRelayClient::new(settings.clone()).unwrap(); - - let resolved = client.resolve(&keypair.public_key()).unwrap(); - - assert!(resolved.is_none()); - } -} diff --git a/pkarr/src/relay_client_async.rs b/pkarr/src/relay_client_async.rs deleted file mode 100644 index c9fd38a..0000000 --- a/pkarr/src/relay_client_async.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Pkarr client for publishing and resolving [SignedPacket]s over [relays](https://pkarr.org/relays). - -use std::sync::Mutex; - -use lru::LruCache; - -use crate::{Error, PkarrRelayClient, PublicKey, Result, SignedPacket}; - -pub struct PkarrRelayClientAsync(PkarrRelayClient); - -impl PkarrRelayClient { - pub fn as_async(self) -> PkarrRelayClientAsync { - PkarrRelayClientAsync(self) - } -} - -impl PkarrRelayClientAsync { - /// Returns a reference to the internal cache. - pub fn cache(&self) -> &Mutex> { - self.0.cache() - } - - /// Publishes a [SignedPacket] to this client's relays. - /// - /// Return the first successful completion, or the last failure. - /// - /// # Errors - /// - Returns a [Error::NotMostRecent] if the provided signed packet is older than most recent. - /// - Returns a [Error::RelayError] from the last responding relay, if all relays - /// responded with non-2xx status codes. - pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<()> { - let mut last_error = Error::EmptyListOfRelays; - - while let Ok(response) = self.0.publish_inner(signed_packet)?.recv_async().await { - match response { - Ok(_) => return Ok(()), - Err(error) => { - last_error = error; - } - } - } - - Err(last_error) - } - - /// Resolve a [SignedPacket] from this client's relays. - /// - /// Return the first successful response, or the failure from the last responding relay. - /// - /// # Errors - /// - /// - Returns [Error::RelayError] if the relay responded with a status >= 400 - /// (except 404 in which case you should receive Ok(None)) or something wrong - /// with the transport, transparent from [ureq::Error]. - /// - Returns [Error::IO] if something went wrong while reading the payload. - pub async fn resolve(&self, public_key: &PublicKey) -> Result> { - if let Some(signed_packet) = self.0.resolve_inner(public_key).recv_async().await?? { - self.cache() - .lock() - .unwrap() - .put(public_key.clone(), signed_packet.clone()); - - return Ok(Some(signed_packet)); - }; - - Ok(None) - } -} - -#[cfg(test)] -mod tests { - use crate::{dns, Keypair, PkarrRelayClient, RelaySettings, SignedPacket}; - - #[test] - fn publish_resolve() { - async fn test() { - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let mut server = mockito::Server::new(); - - let path = format!("/{}", signed_packet.public_key()); - - server - .mock("PUT", path.as_str()) - .with_header("content-type", "text/plain") - .with_status(200) - .create(); - server - .mock("GET", path.as_str()) - .with_body(signed_packet.to_relay_payload()) - .create(); - - let relays: Vec = vec![server.url()]; - let settings = RelaySettings { - relays, - ..RelaySettings::default() - }; - - let a = PkarrRelayClient::new(settings.clone()).unwrap().as_async(); - let b = PkarrRelayClient::new(settings).unwrap().as_async(); - - a.publish(&signed_packet).await.unwrap(); - - let resolved = b.resolve(&keypair.public_key()).await.unwrap().unwrap(); - - assert_eq!(a.cache().lock().unwrap().len(), 1); - assert_eq!(b.cache().lock().unwrap().len(), 1); - - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - } - - futures::executor::block_on(test()); - } - - #[test] - fn not_found() { - async fn test() { - let keypair = Keypair::random(); - - let mut server = mockito::Server::new(); - - let path = format!("/{}", keypair.public_key()); - - server.mock("GET", path.as_str()).with_status(404).create(); - - let relays: Vec = vec![server.url()]; - let settings = RelaySettings { - relays, - ..RelaySettings::default() - }; - - let client = PkarrRelayClient::new(settings.clone()).unwrap().as_async(); - - let resolved = client.resolve(&keypair.public_key()).await.unwrap(); - - assert!(resolved.is_none()); - } - - futures::executor::block_on(test()); - } -} diff --git a/pkarr/src/relay_client_web.rs b/pkarr/src/relay_client_web.rs deleted file mode 100644 index ba1aab3..0000000 --- a/pkarr/src/relay_client_web.rs +++ /dev/null @@ -1,231 +0,0 @@ -use futures::future::select_ok; -use std::str; - -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; -use web_sys::RequestMode; - -use crate::{Error, PublicKey, Result, SignedPacket, DEFAULT_RELAYS}; - -use tracing::debug; - -#[derive(Debug, Clone)] -pub struct PkarrRelayClient { - relays: Vec, -} - -impl Default for PkarrRelayClient { - fn default() -> Self { - Self::new(DEFAULT_RELAYS.map(|s| s.into()).to_vec()).unwrap() - } -} - -impl PkarrRelayClient { - pub fn new(relays: Vec) -> Result { - if relays.is_empty() { - return Err(Error::EmptyListOfRelays); - } - - Ok(Self { relays }) - } - - /// Publishes a [SignedPacket] to this client's relays. - /// - /// Return the first successful completion, or the last failure. - /// - /// # Errors - /// - Returns [Error::WasmRelayError] For Error responses - /// - Returns [Error::JsError] If an error happened on JS side. - pub async fn publish(&self, signed_packet: &SignedPacket) -> Result<()> { - let futures = self.relays.iter().map(|relay| { - Box::pin(async move { - let url = format!("{relay}/{}", signed_packet.public_key()); - publish_inner(&url, signed_packet.to_relay_payload().to_vec()).await - }) - }); - - match select_ok(futures).await { - Ok((response, _)) => Ok(response), - Err(e) => Err(e), - } - } - - /// Resolve a [SignedPacket] from this client's relays. - /// - /// Return the first successful response, or the failure from the last responding relay. - /// - /// # Errors - /// - /// - Returns [Error::WasmRelayError] For Error responses - /// (except 404, these get converted to Ok(None). - /// - Returns [Error::JsError] If an error happened on JS side. - pub async fn resolve(&self, public_key: &PublicKey) -> Result> { - let futures = self.relays.iter().map(|relay| { - Box::pin(async move { - let url = format!("{relay}/{public_key}"); - - match resolve_inner(&url).await { - Ok(bytes) => { - match SignedPacket::from_relay_payload(public_key, &bytes.into()) { - Ok(signed_packet) => { - return Ok(Some(signed_packet)); - } - Err(error) => { - debug!(?url, ?error, "Invalid signed_packet"); - return Err(error); - } - } - } - Err(error) => { - debug!(?url, ?error, "Error response"); - return Err(error); - } - } - }) - }); - - match select_ok(futures).await { - Ok((response, _)) => Ok(response), - Err(e) => { - if let Error::WasmRelayError(404, _) = e { - return Ok(None); - } - - Err(e) - } - } - } -} - -async fn publish_inner(url: &String, bytes: Vec) -> Result<()> { - let response = fetch_base(url, "PUT", Some(bytes)).await?; - let bytes = response_body(&response).await?; - - if !response.ok() { - return Err(Error::WasmRelayError( - response.status(), - str::from_utf8(&bytes).ok().unwrap_or("").to_string(), - )); - } - - Ok(()) -} - -async fn resolve_inner(url: &String) -> Result> { - let response = fetch_base(url, "GET", None).await?; - let bytes = response_body(&response).await?; - - if !response.ok() { - return Err(Error::WasmRelayError( - response.status(), - str::from_utf8(&bytes).ok().unwrap_or("").to_string(), - )); - } - - Ok(bytes) -} - -async fn response_body(response: &web_sys::Response) -> Result> { - let array_buffer = JsFuture::from( - response - .array_buffer() - .map_err(|error| Error::JsError(error))?, - ) - .await - .map_err(|error| Error::JsError(error))?; - - let uint8_array = js_sys::Uint8Array::new(&array_buffer); - - Ok(uint8_array.to_vec()) -} - -async fn fetch_base( - url: &String, - method: &str, - body: Option>, -) -> Result { - let mut opts = web_sys::RequestInit::new(); - opts.method(method); - opts.mode(RequestMode::Cors); - - if let Some(body) = body { - let body_bytes: &[u8] = &body; - let body_array: js_sys::Uint8Array = body_bytes.into(); - let js_value: &JsValue = body_array.as_ref(); - opts.body(Some(js_value)); - } - - let js_request = web_sys::Request::new_with_str_and_init(url, &opts) - .map_err(|error| Error::JsError(error))?; - - let window = web_sys::window().unwrap(); - let response = JsFuture::from(window.fetch_with_request(&js_request)) - .await - .map_err(|error| Error::JsError(error))?; - - let response: web_sys::Response = response.dyn_into().map_err(|error| Error::JsError(error))?; - - Ok(response) -} - -#[cfg(test)] -mod tests { - use wasm_bindgen_test::wasm_bindgen_test; - - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - use crate::{dns, Keypair, PkarrRelayClient, SignedPacket}; - - #[macro_export] - macro_rules! log { - ($($arg:expr),*) => { - web_sys::console::debug_1(&format!($($arg),*).into()); - }; - } - - const TEST_RELAY: &str = "http://localhost:6881"; - - #[wasm_bindgen_test] - async fn basic() { - let keypair = Keypair::random(); - - let mut packet = dns::Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("foo").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::TXT("bar".try_into().unwrap()), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let client = - PkarrRelayClient::new(vec!["http://fail.non".to_string(), TEST_RELAY.to_string()]) - .unwrap(); - - client.publish(&signed_packet).await.unwrap(); - - let resolved = client - .resolve(&keypair.public_key()) - .await - .unwrap() - .unwrap(); - - log!("{:?}", resolved); - - assert_eq!(resolved.as_bytes(), signed_packet.as_bytes()); - } - - #[wasm_bindgen_test] - async fn not_found() { - let keypair = Keypair::random(); - - let client = PkarrRelayClient::new(vec![TEST_RELAY.to_string()]).unwrap(); - - let resolved = client.resolve(&keypair.public_key()).await.unwrap(); - - log!("{:?}", resolved); - - assert!(resolved.is_none()); - } -} diff --git a/pkarr/src/signed_packet.rs b/pkarr/src/signed_packet.rs deleted file mode 100644 index a95a726..0000000 --- a/pkarr/src/signed_packet.rs +++ /dev/null @@ -1,841 +0,0 @@ -//! Signed DNS packet - -use crate::{Error, Keypair, PublicKey, Result}; -use bytes::{Bytes, BytesMut}; -use ed25519_dalek::Signature; -use self_cell::self_cell; -use simple_dns::{ - rdata::{RData, A, AAAA}, - Name, Packet, ResourceRecord, -}; -use std::{ - char, - fmt::{self, Display, Formatter}, - net::{Ipv4Addr, Ipv6Addr}, -}; - -#[cfg(not(target_arch = "wasm32"))] -use std::time::SystemTime; - -const DOT: char = '.'; - -self_cell!( - struct Inner { - owner: Bytes, - - #[covariant] - dependent: Packet, - } - - impl{Debug} -); - -impl Inner { - fn try_from_parts( - public_key: &PublicKey, - signature: &Signature, - timestamp: u64, - encoded_packet: &Bytes, - ) -> Result { - // Create the inner bytes from timestamp> - let mut bytes = BytesMut::with_capacity(encoded_packet.len() + 104); - - bytes.extend_from_slice(public_key.as_bytes()); - bytes.extend_from_slice(&signature.to_bytes()); - bytes.extend_from_slice(×tamp.to_be_bytes()); - bytes.extend_from_slice(encoded_packet); - - Ok(Self::try_new(bytes.into(), |bytes| { - Packet::parse(&bytes[104..]) - })?) - } - - fn try_from_bytes(bytes: &Bytes) -> Result { - Ok(Inner::try_new(bytes.to_owned(), |bytes| { - Packet::parse(&bytes[104..]) - })?) - } -} - -#[derive(Debug)] -/// Signed DNS packet -pub struct SignedPacket { - inner: Inner, - last_seen: u64, -} - -impl SignedPacket { - /// Creates a [Self] from the serialized representation: - /// `<32 bytes public_key><64 bytes signature><8 bytes big-endian timestamp in microseconds>` - /// - /// Performs the following validations: - /// - Bytes minimum length - /// - Validates the PublicKey - /// - Verifies the Signature - /// - Validates the DNS packet encoding - /// - /// You can skip all these validations by using [Self::from_bytes_unchecked] instead. - /// - /// You can use [Self::from_relay_payload] instead if you are receiving a response from an HTTP relay. - /// - /// # Errors - /// - Returns [crate::Error::InvalidSignedPacketBytesLength] if `bytes.len()` is smaller than 104 bytes - /// - Returns [crate::Error::PacketTooLarge] if `bytes.len()` is bigger than 1104 bytes - /// - Returns [crate::Error::InvalidEd25519PublicKey] if the first 32 bytes are invalid `ed25519` public key - /// - Returns [crate::Error::InvalidEd25519Signature] if the following 64 bytes are invalid `ed25519` signature - /// - Returns [crate::Error::DnsError] if it failed to parse the DNS Packet after the first 104 bytes - pub fn from_bytes(bytes: &Bytes) -> Result { - if bytes.len() < 104 { - return Err(Error::InvalidSignedPacketBytesLength(bytes.len())); - } - if bytes.len() > 1104 { - return Err(Error::PacketTooLarge(bytes.len())); - } - let public_key = PublicKey::try_from(&bytes[..32])?; - let signature = Signature::from_bytes(bytes[32..96].try_into().unwrap()); - let timestamp = u64::from_be_bytes(bytes[96..104].try_into().unwrap()); - - let encoded_packet = &bytes.slice(104..); - - public_key.verify(&signable(timestamp, encoded_packet), &signature)?; - - Ok(SignedPacket { - inner: Inner::try_from_bytes(bytes)?, - last_seen: system_time(), - }) - } - - /// Useful for cloning a [SignedPacket], or cerating one from a previously checked bytes, - /// like ones stored on disk or in a database. - pub fn from_bytes_unchecked(bytes: &Bytes, last_seen: u64) -> SignedPacket { - SignedPacket { - inner: Inner::try_from_bytes(bytes).unwrap(), - last_seen, - } - } - - /// Creates a [SignedPacket] from a [PublicKey] and the [relays](https://github.com/Nuhvi/pkarr/blob/main/design/relays.md) payload. - /// - /// # Errors - /// - Returns [crate::Error::InvalidSignedPacketBytesLength] if `payload` is too small - /// - Returns [crate::Error::PacketTooLarge] if the payload is too large. - /// - Returns [crate::Error::InvalidEd25519Signature] if the signature in the payload is invalid - /// - Returns [crate::Error::DnsError] if it failed to parse the DNS Packet - pub fn from_relay_payload(public_key: &PublicKey, payload: &Bytes) -> Result { - let mut bytes = BytesMut::with_capacity(payload.len() + 32); - - bytes.extend_from_slice(public_key.as_bytes()); - bytes.extend_from_slice(payload); - - SignedPacket::from_bytes(&bytes.into()) - } - - /// Creates a new [SignedPacket] from a [Keypair] and a DNS [Packet]. - /// - /// It will also normalize the names of the [ResourceRecord]s to be relative to the origin, - /// which would be the [zbase32](z32) encoded [PublicKey] of the [Keypair] used to sign the Packet. - /// - /// # Errors - /// - Returns [crate::Error::DnsError] if the packet is invalid or it failed to compress or encode it. - pub fn from_packet(keypair: &Keypair, packet: &Packet) -> Result { - // Normalize names to the origin TLD - let mut inner = Packet::new_reply(0); - - let origin = keypair.public_key().to_z32(); - - let normalized_names: Vec = packet - .answers - .iter() - .map(|answer| normalize_name(&origin, answer.name.to_string())) - .collect(); - - packet - .answers - .iter() - .enumerate() - .for_each(|(index, answer)| { - let new_new_name = Name::new_unchecked(&normalized_names[index]); - - inner.answers.push(ResourceRecord::new( - new_new_name.clone(), - answer.class, - answer.ttl, - answer.rdata.clone(), - )) - }); - - // Encode the packet as `v` and verify its length - let encoded_packet: Bytes = inner.build_bytes_vec_compressed()?.into(); - - if encoded_packet.len() > 1000 { - return Err(Error::PacketTooLarge(encoded_packet.len())); - } - - let timestamp = system_time(); - - let signature = keypair.sign(&signable(timestamp, &encoded_packet)); - - Ok(SignedPacket { - inner: Inner::try_from_parts( - &keypair.public_key(), - &signature, - timestamp, - &encoded_packet, - )?, - last_seen: system_time(), - }) - } - - // === Getters === - - /// Returns the serialized signed packet: - /// `<32 bytes public_key><64 bytes signature><8 bytes big-endian timestamp in microseconds>` - pub fn as_bytes(&self) -> &Bytes { - self.inner.borrow_owner() - } - - /// Returns a slice of the serialized [SignedPacket] omitting the leading public_key, - /// to be sent as a request/response body to or from [relays](https://github.com/Nuhvi/pkarr/blob/main/design/relays.md). - pub fn to_relay_payload(&self) -> Bytes { - self.inner.borrow_owner().slice(32..) - } - - /// Returns the [PublicKey] of the signer of this [SignedPacket] - pub fn public_key(&self) -> PublicKey { - PublicKey::try_from(&self.inner.borrow_owner()[0..32]).unwrap() - } - - /// Returns the [Signature] of the the bencoded sequence number concatenated with the - /// encoded and compressed packet, as defined in [BEP_0044](https://www.bittorrent.org/beps/bep_0044.html) - pub fn signature(&self) -> Signature { - Signature::try_from(&self.inner.borrow_owner()[32..96]).unwrap() - } - - /// Returns the timestamp in microseconds since the [UNIX_EPOCH](std::time::UNIX_EPOCH). - /// - /// This timestamp is authored by the controller of the keypair, - /// and it is trusted as a way to order which packets where authored after which, - /// but it shouldn't be used for caching for example, instead, use [Self::last_seen] - /// which is set when you create a new packet. - pub fn timestamp(&self) -> u64 { - let bytes = self.inner.borrow_owner(); - let slice: [u8; 8] = bytes[96..104].try_into().unwrap(); - - u64::from_be_bytes(slice) - } - - /// Returns the DNS [Packet] compressed and encoded. - pub fn encoded_packet(&self) -> Bytes { - self.inner.borrow_owner().slice(104..) - } - - /// Return the DNS [Packet]. - pub fn packet(&self) -> &Packet { - self.inner.borrow_dependent() - } - - /// Unix last_seen time in microseconds - pub fn last_seen(&self) -> &u64 { - &self.last_seen - } - - // === Setters === - - /// Set the [Self::last_seen] property - pub fn set_last_seen(&mut self, last_seen: &u64) { - self.last_seen = *last_seen; - } - - // === Public Methods === - - /// Set the [Self::last_seen] to the current system time - pub fn refresh(&mut self) { - self.last_seen = system_time(); - } - - /// Return whether this [SignedPacket] is more recent than the given one. - /// If the timestamps are erqual, the one with the largest value is considered more recent. - /// Usefel for determining which packet contains the latest information from the Dht. - /// Assumes that both packets have the same [PublicKey], you shouldn't compare packets from - /// different keys. - pub fn more_recent_than(&self, other: &SignedPacket) -> bool { - // In the rare ocasion of timestamp collission, - // we use the one with the largest value - if self.timestamp() == other.timestamp() { - self.encoded_packet() > other.encoded_packet() - } else { - self.timestamp() > other.timestamp() - } - } - - /// Returns true if both packets have the same timestamp and packet, - /// and only differ in [Self::last_seen] - pub fn is_same_as(&self, other: &SignedPacket) -> bool { - self.as_bytes() == other.as_bytes() - } - - /// Return and iterator over the [ResourceRecord]s in the Answers section of the DNS [Packet] - /// that matches the given name. The name will be normalized to the origin TLD of this packet. - pub fn resource_records(&self, name: &str) -> impl Iterator { - let origin = self.public_key().to_z32(); - let normalized_name = normalize_name(&origin, name.to_string()); - self.packet() - .answers - .iter() - .filter(move |rr| rr.name == Name::new(&normalized_name).unwrap()) - } - - /// Similar to [resource_records](SignedPacket::resource_records), but filters out - /// expired records, according the the [Self::last_seen] value and each record's `ttl`. - pub fn fresh_resource_records(&self, name: &str) -> impl Iterator { - let origin = self.public_key().to_z32(); - let normalized_name = normalize_name(&origin, name.to_string()); - - self.packet().answers.iter().filter(move |rr| { - rr.name == Name::new(&normalized_name).unwrap() && rr.ttl > self.elapsed() - }) - } - - /// calculates the remaining seconds by comparing the [Self::ttl] (clamped by `min` and `max`) - /// to the [Self::last_seen]. - /// - /// # Panics - /// - /// Panics if `min` < `max` - pub fn expires_in(&self, min: u32, max: u32) -> u32 { - match self.ttl(min, max).overflowing_sub(self.elapsed()) { - (_, true) => 0, - (ttl, false) => ttl, - } - } - - /// Returns the smallest `ttl` in the [Self::packet] resource records, - /// calmped with `min` and `max`. - /// - /// # Panics - /// - /// Panics if `min` < `max` - pub fn ttl(&self, min: u32, max: u32) -> u32 { - self.packet() - .answers - .iter() - .map(|rr| rr.ttl) - .min() - .map_or(min, |v| v.clamp(min, max)) - } - - // === Private Methods === - - /// Time since the [Self::last_seen] in seconds - fn elapsed(&self) -> u32 { - ((system_time() - self.last_seen) / 1_000_000) as u32 - } -} - -fn signable(timestamp: u64, v: &Bytes) -> Bytes { - let mut signable = format!("3:seqi{}e1:v{}:", timestamp, v.len()).into_bytes(); - signable.extend(v); - signable.into() -} - -#[cfg(not(target_arch = "wasm32"))] -/// Return the number of microseconds since [SystemTime::UNIX_EPOCH] -pub fn system_time() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("time drift") - .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 -} - -if_dht! { - use mainline::MutableItem; - - impl From<&SignedPacket> for MutableItem { - fn from(s: &SignedPacket) -> Self { - let seq: i64 = s.timestamp() as i64; - let packet = s.inner.borrow_owner().slice(104..); - - Self::new_signed_unchecked( - s.public_key().to_bytes(), - s.signature().to_bytes(), - packet, - seq, - None, - ) - } - } - - impl TryFrom<&MutableItem> for SignedPacket { - type Error = Error; - - fn try_from(i: &MutableItem) -> Result { - let public_key = PublicKey::try_from(i.key()).unwrap(); - let seq = *i.seq() as u64; - let signature: Signature = i.signature().into(); - - Ok(Self { - inner: Inner::try_from_parts(&public_key, &signature, seq, i.value())?, - last_seen: system_time(), - }) - } - } -} - -impl AsRef<[u8]> for SignedPacket { - /// Returns the SignedPacket as a bytes slice with the format: - /// `<6 bytes timestamp in microseconds>` - fn as_ref(&self) -> &[u8] { - self.inner.borrow_owner() - } -} - -impl Clone for SignedPacket { - fn clone(&self) -> Self { - Self::from_bytes_unchecked(self.as_bytes(), self.last_seen) - } -} - -impl Display for SignedPacket { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "SignedPacket ({}):\n last_seen: {} seconds ago\n timestamp: {},\n signature: {}\n records:\n", - &self.public_key(), - &self.elapsed(), - &self.timestamp(), - &self.signature(), - )?; - - for answer in &self.packet().answers { - writeln!( - f, - " {} IN {} {}\n", - &answer.name, - &answer.ttl, - match &answer.rdata { - RData::A(A { address }) => format!("A {}", Ipv4Addr::from(*address)), - RData::AAAA(AAAA { address }) => format!("AAAA {}", Ipv6Addr::from(*address)), - #[allow(clippy::to_string_in_format_args)] - RData::CNAME(name) => format!("CNAME {}", name.to_string()), - RData::TXT(txt) => { - format!( - "TXT \"{}\"", - txt.clone() - .try_into() - .unwrap_or("__INVALID_TXT_VALUE_".to_string()) - ) - } - _ => format!("{:?}", answer.rdata), - } - )?; - } - - writeln!(f)?; - - Ok(()) - } -} - -fn normalize_name(origin: &str, name: String) -> String { - let name = if name.ends_with(DOT) { - name[..name.len() - 1].to_string() - } else { - name - }; - - let parts: Vec<&str> = name.split('.').collect(); - let last = *parts.last().unwrap_or(&""); - - if last == origin { - // Already normalized. - return name.to_string(); - } - - if last == "@" || last.is_empty() { - // Shorthand of origin - return origin.to_string(); - } - - format!("{}.{}", name, origin) -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use super::*; - use crate::dns; - - use crate::{DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL}; - - #[test] - fn normalize_names() { - let origin = "ed4mn3aoazuf1ahpy9rz1nyswhukbj5483ryefwkue7fbp3egkzo"; - - assert_eq!(normalize_name(origin, ".".to_string()), origin); - assert_eq!(normalize_name(origin, "@".to_string()), origin); - assert_eq!(normalize_name(origin, "@.".to_string()), origin); - assert_eq!(normalize_name(origin, origin.to_string()), origin); - assert_eq!( - normalize_name(origin, "_derp_region.irorh".to_string()), - format!("_derp_region.irorh.{}", origin) - ); - assert_eq!( - normalize_name(origin, format!("_derp_region.irorh.{}", origin)), - format!("_derp_region.irorh.{}", origin) - ); - assert_eq!( - normalize_name(origin, format!("_derp_region.irorh.{}.", origin)), - format!("_derp_region.irorh.{}", origin) - ); - } - - #[test] - fn sign_verify() { - let keypair = Keypair::random(); - - let mut packet = Packet::new_reply(0); - packet.answers.push(ResourceRecord::new( - Name::new("_derp_region.iroh.").unwrap(), - simple_dns::CLASS::IN, - 30, - RData::A(A { - address: Ipv4Addr::new(1, 1, 1, 1).into(), - }), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - assert!(SignedPacket::from_relay_payload( - &signed_packet.public_key(), - &signed_packet.to_relay_payload() - ) - .is_ok()); - } - - #[test] - fn from_too_large_bytes() { - let keypair = Keypair::random(); - - let bytes = Bytes::from(vec![0; 1073]); - let error = SignedPacket::from_relay_payload(&keypair.public_key(), &bytes); - - assert!(error.is_err()); - } - - #[test] - fn from_too_large_packet() { - let keypair = Keypair::random(); - - let mut packet = Packet::new_reply(0); - for _ in 0..100 { - packet.answers.push(ResourceRecord::new( - Name::new("_derp_region.iroh.").unwrap(), - simple_dns::CLASS::IN, - 30, - RData::A(A { - address: Ipv4Addr::new(1, 1, 1, 1).into(), - }), - )); - } - - let error = SignedPacket::from_packet(&keypair, &packet); - - assert!(error.is_err()); - } - - #[test] - fn resource_records_iterator() { - let keypair = Keypair::random(); - - let target = ResourceRecord::new( - Name::new("_derp_region.iroh.").unwrap(), - simple_dns::CLASS::IN, - 30, - RData::A(A { - address: Ipv4Addr::new(1, 1, 1, 1).into(), - }), - ); - - let mut packet = Packet::new_reply(0); - packet.answers.push(target.clone()); - packet.answers.push(ResourceRecord::new( - Name::new("something else").unwrap(), - simple_dns::CLASS::IN, - 30, - RData::A(A { - address: Ipv4Addr::new(1, 1, 1, 1).into(), - }), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - let iter = signed_packet.resource_records("_derp_region.iroh"); - assert_eq!(iter.count(), 1); - - for record in signed_packet.resource_records("_derp_region.iroh") { - assert_eq!(record.rdata, target.rdata); - } - } - - #[test] - fn to_mutable() { - let keypair = Keypair::random(); - - let mut packet = Packet::new_reply(0); - packet.answers.push(ResourceRecord::new( - Name::new("_derp_region.iroh.").unwrap(), - simple_dns::CLASS::IN, - 30, - RData::A(A { - address: Ipv4Addr::new(1, 1, 1, 1).into(), - }), - )); - - let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap(); - let item: MutableItem = (&signed_packet).into(); - let seq: i64 = signed_packet.timestamp() as i64; - - let expected = MutableItem::new( - keypair.secret_key().into(), - signed_packet - .packet() - .build_bytes_vec_compressed() - .unwrap() - .into(), - seq, - None, - ); - - assert_eq!(item, expected); - } - - #[test] - fn compressed_names() { - let keypair = Keypair::random(); - - let name = "foobar"; - let dup = name; - - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("@").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::CNAME(dns::Name::new(name).unwrap().into()), - )); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("@").unwrap(), - dns::CLASS::IN, - 30, - dns::rdata::RData::CNAME(dns::Name::new(dup).unwrap().into()), - )); - - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - assert_eq!( - signed - .resource_records("@") - .map(|r| r.rdata.clone()) - .collect::>(), - packet - .answers - .iter() - .map(|r| r.rdata.clone()) - .collect::>() - ) - } - - #[test] - fn to_bytes_from_bytes() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 30, - RData::TXT("hello".try_into().unwrap()), - )); - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - let bytes = signed.as_bytes(); - let from_bytes = SignedPacket::from_bytes(bytes).unwrap(); - assert_eq!(signed.as_bytes(), from_bytes.as_bytes()); - let from_bytes2 = SignedPacket::from_bytes_unchecked(bytes, signed.last_seen); - assert_eq!(signed.as_bytes(), from_bytes2.as_bytes()); - - let public_key = keypair.public_key(); - let payload = signed.to_relay_payload(); - let from_relay_payload = SignedPacket::from_relay_payload(&public_key, &payload).unwrap(); - assert_eq!(signed.as_bytes(), from_relay_payload.as_bytes()); - } - - #[test] - fn clone() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 30, - RData::TXT("hello".try_into().unwrap()), - )); - - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - let cloned = signed.clone(); - - assert_eq!(cloned.as_bytes(), signed.as_bytes()); - } - - #[test] - fn expires_in_minimum_ttl() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 10, - RData::TXT("hello".try_into().unwrap()), - )); - - let mut signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - signed.last_seen = system_time() - (20 * 1_000_000); - - assert!( - signed.expires_in(30, u32::MAX) > 0, - "input minimum_ttl is 30 so ttl = 30" - ); - - assert!( - signed.expires_in(0, u32::MAX) == 0, - "input minimum_ttl is 0 so ttl = 10 (smallest in resource records)" - ); - } - - #[test] - fn expires_in_maximum_ttl() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 3 * DEFAULT_MAXIMUM_TTL, - RData::TXT("hello".try_into().unwrap()), - )); - - let mut signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - signed.last_seen = system_time() - (2 * (DEFAULT_MAXIMUM_TTL as u64) * 1_000_000); - - assert!( - signed.expires_in(0, DEFAULT_MAXIMUM_TTL) == 0, - "input maximum_ttl is the dfeault 86400 so maximum ttl = 86400" - ); - - assert!( - signed.expires_in(0, 7 * DEFAULT_MAXIMUM_TTL) > 0, - "input maximum_ttl is 7 * 86400 so ttl = 3 * 86400 (smallest in resource records)" - ); - } - - #[test] - fn fresh_resource_records() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 30, - RData::TXT("hello".try_into().unwrap()), - )); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - 60, - RData::TXT("world".try_into().unwrap()), - )); - - let mut signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - signed.last_seen = system_time() - (30 * 1_000_000); - - assert_eq!(signed.fresh_resource_records("_foo").count(), 1); - } - - #[test] - fn ttl_empty() { - let keypair = Keypair::random(); - let packet = Packet::new_reply(0); - - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - assert_eq!(signed.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), 300); - } - - #[test] - fn ttl_with_records_less_than_minimum() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - DEFAULT_MINIMUM_TTL / 2, - RData::TXT("hello".try_into().unwrap()), - )); - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - DEFAULT_MINIMUM_TTL / 4, - RData::TXT("world".try_into().unwrap()), - )); - - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - assert_eq!( - signed.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), - DEFAULT_MINIMUM_TTL - ); - - assert_eq!(signed.ttl(0, DEFAULT_MAXIMUM_TTL), DEFAULT_MINIMUM_TTL / 4); - } - - #[test] - fn ttl_with_records_more_than_maximum() { - let keypair = Keypair::random(); - let mut packet = Packet::new_reply(0); - - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - DEFAULT_MAXIMUM_TTL * 2, - RData::TXT("world".try_into().unwrap()), - )); - - packet.answers.push(dns::ResourceRecord::new( - dns::Name::new("_foo").unwrap(), - dns::CLASS::IN, - DEFAULT_MAXIMUM_TTL * 4, - RData::TXT("world".try_into().unwrap()), - )); - - let signed = SignedPacket::from_packet(&keypair, &packet).unwrap(); - - assert_eq!( - signed.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL), - DEFAULT_MAXIMUM_TTL - ); - - assert_eq!( - signed.ttl(0, DEFAULT_MAXIMUM_TTL * 8), - DEFAULT_MAXIMUM_TTL * 2 - ); - } -} diff --git a/server/Cargo.toml b/server/Cargo.toml index a7753f4..9da6dd4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,23 +7,23 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.82" -axum = "0.7.5" -tokio = { version = "1.37.0", features = ["full"] } -tower-http = { version = "0.5.2", features = ["cors", "trace"] } -tracing = "0.1.40" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -axum-server = { version = "0.7.0", features = ["tls-rustls-no-provider"] } +anyhow = "1.0.93" +axum = "0.7.9" +tokio = { version = "1.41.1", features = ["full"] } +tower-http = { version = "0.6.2", features = ["cors", "trace"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +axum-server = { version = "0.7.1", features = ["tls-rustls-no-provider"] } rustls = { version = "0.23", default-features = false, features = ["ring"] } http = "1.1.0" -thiserror = "1.0.49" -bytes = "1.7.1" -tower_governor = "0.4.2" +thiserror = "2.0.3" +bytes = "1.9.0" +tower_governor = "0.4.3" governor = "0.6.3" -heed = { version = "0.20.0", default-features = false } -byteorder = "1.5.0" -serde = { version = "1.0.199", features = ["derive"] } -toml = "0.8.12" -clap = { version = "4.5.1", features = ["derive"] } +serde = { version = "1.0.215", features = ["derive"] } +toml = "0.8.19" +clap = { version = "4.5.21", features = ["derive"] } dirs-next = "2.0.0" -pkarr = { version = "2.2.0", path = "../pkarr", features = ["async"] } +pkarr = { version = "3.0.0", path = "../pkarr", features = ["dht", "lmdb-cache"] } +httpdate = "1.0.3" +pubky-timestamp = { version = "0.2.0", features = ["httpdate"] } diff --git a/server/src/cache.rs b/server/src/cache.rs deleted file mode 100644 index 72609ae..0000000 --- a/server/src/cache.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::{borrow::Cow, path::Path, time::Duration}; - -use pkarr::{system_time, PkarrCache, PkarrCacheKey, SignedPacket}; - -use byteorder::LittleEndian; -use heed::{types::U64, BoxedError, BytesDecode, BytesEncode, Database, Env, EnvOpenOptions}; - -use anyhow::Result; -use tracing::debug; - -const PKARR_CACHE_TABLE_NAME_SIGNED_PACKET: &str = "pkarrcache:signed_packet"; -const PKARR_CACHE_TABLE_NAME_KEY_TO_TIME: &str = "pkarrcache:key_to_time"; -const PKARR_CACHE_TABLE_NAME_TIME_TO_KEY: &str = "pkarrcache:time_to_key"; - -type PkarrCacheSignedPacketsTable = Database; -type PkarrCacheKeyToTimeTable = Database>; -type PkarrCacheTimeToKeyTable = Database, PkarrCacheKeyCodec>; - -pub struct PkarrCacheKeyCodec; - -impl<'a> BytesEncode<'a> for PkarrCacheKeyCodec { - type EItem = PkarrCacheKey; - - fn bytes_encode(key: &Self::EItem) -> Result, BoxedError> { - Ok(Cow::Owned(key.bytes.to_vec())) - } -} - -impl<'a> BytesDecode<'a> for PkarrCacheKeyCodec { - type DItem = PkarrCacheKey; - - fn bytes_decode(bytes: &'a [u8]) -> Result { - Ok(PkarrCacheKey::from_bytes(bytes)?) - } -} - -pub struct SignedPacketCodec; - -impl<'a> BytesEncode<'a> for SignedPacketCodec { - type EItem = SignedPacket; - - fn bytes_encode(signed_packet: &Self::EItem) -> Result, BoxedError> { - let bytes = signed_packet.as_bytes(); - - let mut vec = Vec::with_capacity(bytes.len() + 8); - - vec.extend(>::bytes_encode(signed_packet.last_seen())?.as_ref()); - vec.extend(bytes); - - Ok(Cow::Owned(vec)) - } -} - -impl<'a> BytesDecode<'a> for SignedPacketCodec { - type DItem = SignedPacket; - - fn bytes_decode(bytes: &'a [u8]) -> Result { - let last_seen = >::bytes_decode(bytes)?; - - Ok(SignedPacket::from_bytes_unchecked( - &bytes[8..].to_vec().into(), - last_seen, - )) - } -} - -#[derive(Debug, Clone)] -pub struct HeedPkarrCache { - capacity: usize, - env: Env, -} - -impl HeedPkarrCache { - pub fn new(env_path: &Path, capacity: usize) -> Result { - // Page aligned but more than enough bytes for `capacity` many SignedPacket - let map_size = (((capacity * 1112) + 4095) / 4096) * 4096; - - let env = unsafe { - EnvOpenOptions::new() - .map_size(map_size) - .max_dbs(3) - .open(env_path)? - }; - - let mut wtxn = env.write_txn()?; - let _: PkarrCacheSignedPacketsTable = - env.create_database(&mut wtxn, Some(PKARR_CACHE_TABLE_NAME_SIGNED_PACKET))?; - let _: PkarrCacheKeyToTimeTable = - env.create_database(&mut wtxn, Some(PKARR_CACHE_TABLE_NAME_KEY_TO_TIME))?; - let _: PkarrCacheTimeToKeyTable = - env.create_database(&mut wtxn, Some(PKARR_CACHE_TABLE_NAME_TIME_TO_KEY))?; - - wtxn.commit()?; - - let instance = Self { capacity, env }; - - let clone = instance.clone(); - std::thread::spawn(move || loop { - debug!(size = ?clone.len(), "Cache stats"); - std::thread::sleep(Duration::from_secs(60)); - }); - - Ok(instance) - } - - pub fn internal_len(&self) -> Result { - let rtxn = self.env.read_txn()?; - - let db: PkarrCacheSignedPacketsTable = self - .env - .open_database(&rtxn, Some(PKARR_CACHE_TABLE_NAME_SIGNED_PACKET))? - .unwrap(); - - Ok(db.len(&rtxn)? as usize) - } - - pub fn internal_put(&self, key: &PkarrCacheKey, signed_packet: &SignedPacket) -> Result<()> { - if self.capacity == 0 { - return Ok(()); - } - - let mut wtxn = self.env.write_txn()?; - - let packets: PkarrCacheSignedPacketsTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_SIGNED_PACKET))? - .unwrap(); - - let key_to_time: PkarrCacheKeyToTimeTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_KEY_TO_TIME))? - .unwrap(); - - let time_to_key: PkarrCacheTimeToKeyTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_TIME_TO_KEY))? - .unwrap(); - - let len = packets.len(&wtxn)? as usize; - - if len >= self.capacity { - debug!(?len, ?self.capacity, "Reached cache capacity, deleting extra item."); - - let mut iter = time_to_key.rev_iter(&wtxn)?; - - if let Some((time, key)) = iter.next().transpose()? { - drop(iter); - - time_to_key.delete(&mut wtxn, &time)?; - key_to_time.delete(&mut wtxn, &key)?; - packets.delete(&mut wtxn, &key)?; - }; - } - - if let Some(old_time) = key_to_time.get(&wtxn, key)? { - time_to_key.delete(&mut wtxn, &old_time)?; - } - - let new_time = system_time(); - - time_to_key.put(&mut wtxn, &new_time, key)?; - key_to_time.put(&mut wtxn, key, &new_time)?; - - packets.put(&mut wtxn, key, signed_packet)?; - - wtxn.commit()?; - - Ok(()) - } - - pub fn internal_get(&self, key: &PkarrCacheKey) -> Result> { - let mut wtxn = self.env.write_txn()?; - - let packets: PkarrCacheSignedPacketsTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_SIGNED_PACKET))? - .unwrap(); - - let key_to_time: PkarrCacheKeyToTimeTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_KEY_TO_TIME))? - .unwrap(); - let time_to_key: PkarrCacheTimeToKeyTable = self - .env - .open_database(&wtxn, Some(PKARR_CACHE_TABLE_NAME_TIME_TO_KEY))? - .unwrap(); - - if let Some(signed_packet) = packets.get(&wtxn, key)? { - if let Some(time) = key_to_time.get(&wtxn, key)? { - time_to_key.delete(&mut wtxn, &time)?; - }; - - let new_time = system_time(); - - time_to_key.put(&mut wtxn, &new_time, key)?; - key_to_time.put(&mut wtxn, key, &new_time)?; - - wtxn.commit()?; - - return Ok(Some(signed_packet)); - } - - wtxn.commit()?; - - Ok(None) - } - - pub fn internal_get_read_only(&self, key: &PkarrCacheKey) -> Result> { - let rtxn = self.env.read_txn()?; - - let packets: PkarrCacheSignedPacketsTable = self - .env - .open_database(&rtxn, Some(PKARR_CACHE_TABLE_NAME_SIGNED_PACKET))? - .unwrap(); - - if let Some(signed_packet) = packets.get(&rtxn, key)? { - return Ok(Some(signed_packet)); - } - - rtxn.commit()?; - - Ok(None) - } -} - -impl PkarrCache for HeedPkarrCache { - fn len(&self) -> usize { - match self.internal_len() { - Ok(result) => result, - Err(error) => { - debug!(?error, "Error in HeedPkarrCache::len"); - 0 - } - } - } - - fn put(&self, key: &PkarrCacheKey, signed_packet: &SignedPacket) { - if let Err(error) = self.internal_put(key, signed_packet) { - debug!(?error, "Error in HeedPkarrCache::put"); - }; - } - - fn get(&self, key: &PkarrCacheKey) -> Option { - match self.internal_get(key) { - Ok(result) => result, - Err(error) => { - debug!(?error, "Error in HeedPkarrCache::get"); - - None - } - } - } - - fn get_read_only(&self, key: &PkarrCacheKey) -> Option { - match self.internal_get_read_only(key) { - Ok(result) => result, - Err(error) => { - debug!(?error, "Error in HeedPkarrCache::get"); - - None - } - } - } -} diff --git a/server/src/config.rs b/server/src/config.rs index c151368..410dbee 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -27,13 +27,13 @@ pub struct Config { /// /// Defaults to a directory in the OS data directory cache_path: Option, - /// See [pkarr::Settings::cache_size] + /// See [pkarr::client::Settings::cache_size] cache_size: Option, /// Resolvers /// /// Other servers to query in parallel with the Dht queries /// - /// See [pkarr::Settings::resolvers] + /// See [pkarr::client::Settings::resolvers] resolvers: Option>, /// See [pkarr::Settings::minimum_ttl] minimum_ttl: Option, diff --git a/server/src/dht_server.rs b/server/src/dht_server.rs index 37832ea..6547eb5 100644 --- a/server/src/dht_server.rs +++ b/server/src/dht_server.rs @@ -4,6 +4,7 @@ use std::{ }; use pkarr::{ + extra::lmdb_cache::LmdbCache, mainline::{ self, rpc::{ @@ -16,18 +17,18 @@ use pkarr::{ server::Server, MutableItem, }, - PkarrCache, + Cache, }; use tracing::debug; -use crate::{cache::HeedPkarrCache, rate_limiting::IpRateLimiter}; +use crate::rate_limiting::IpRateLimiter; /// DhtServer with Rate limiting pub struct DhtServer { - inner: mainline::server::DhtServer, + inner: mainline::server::DefaultServer, resolvers: Option>, - cache: Box, + cache: Box, minimum_ttl: u32, maximum_ttl: u32, rate_limiter: IpRateLimiter, @@ -41,7 +42,7 @@ impl Debug for DhtServer { impl DhtServer { pub fn new( - cache: Box, + cache: Box, resolvers: Option>, minimum_ttl: u32, maximum_ttl: u32, @@ -49,7 +50,7 @@ impl DhtServer { ) -> Self { Self { // Default DhtServer used to stay a good citizen servicing the Dht. - inner: mainline::server::DhtServer::default(), + inner: mainline::server::DefaultServer::default(), cache, resolvers: resolvers.map(|resolvers| { resolvers @@ -78,57 +79,18 @@ impl Server for DhtServer { .. } = request { - let should_query = if let Some(cached) = self.cache.get(target) { - debug!( - public_key = ?cached.public_key(), - ?target, - "cache hit responding with packet!" - ); + let cached_packet = self.cache.get(target.as_bytes()); - // Respond with what we have, even if expired. - let mutable_item = MutableItem::from(&cached); + let as_ref = cached_packet.as_ref(); - rpc.response( - from, - transaction_id, - ResponseSpecific::GetMutable(GetMutableResponseArguments { - responder_id: *rpc.id(), - // Token doesn't matter much, as we are most likely _not_ the - // closest nodes, so we shouldn't expect an PUT requests based on - // this response. - token: vec![0, 0, 0, 0], - nodes: None, - v: mutable_item.value().to_vec(), - k: mutable_item.key().to_vec(), - seq: *mutable_item.seq(), - sig: mutable_item.signature().to_vec(), - }), - ); - - // If expired, we try to hydrate the packet from the DHT. - let expires_in = cached.expires_in(self.minimum_ttl, self.maximum_ttl); - let expired = expires_in == 0; + // Should query? + if as_ref + .as_ref() + .map(|c| c.is_expired(self.minimum_ttl, self.maximum_ttl)) + .unwrap_or(true) + { + debug!(?target, "querying the DHT to hydrate our cache for later."); - if expired { - debug!( - public_key = ?cached.public_key(), - ?target, - ?expires_in, - "cache expired, querying the DHT to hydrate our cache for later." - ); - }; - - expired - } else { - debug!( - ?target, - "cache miss, querying the DHT to hydrate our cache for later." - ); - true - }; - - // Either cache miss or expired cached packet - if should_query { // Rate limit nodes that are making too many request forcing us to making too // many queries, either by querying the same non-existent key, or many unique keys. if self.rate_limiter.is_limited(&from.ip()) { @@ -146,6 +108,33 @@ impl Server for DhtServer { ); }; } + + // Respond with what we have, even if expired. + if let Some(cached_packet) = cached_packet { + debug!( + public_key = ?cached_packet.public_key(), + "responding with cached packet even if expired" + ); + + let mutable_item = MutableItem::from(&cached_packet); + + rpc.response( + from, + transaction_id, + ResponseSpecific::GetMutable(GetMutableResponseArguments { + responder_id: *rpc.id(), + // Token doesn't matter much, as we are most likely _not_ the + // closest nodes, so we shouldn't expect a PUT requests based on + // this response. + token: vec![0, 0, 0, 0], + nodes: None, + v: mutable_item.value().to_vec(), + k: mutable_item.key().to_vec(), + seq: *mutable_item.seq(), + sig: mutable_item.signature().to_vec(), + }), + ); + } }; // Do normal Dht request handling (peers, mutable, immutable, and routing). diff --git a/server/src/error.rs b/server/src/error.rs index cbe837d..92a36d2 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -68,3 +68,9 @@ impl From for Error { Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(value)) } } + +impl From for Error { + fn from(value: pkarr::errors::ClientWasShutdown) -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR, Some(value)) + } +} diff --git a/server/src/handlers.rs b/server/src/handlers.rs index 3449a1d..c7e5080 100644 --- a/server/src/handlers.rs +++ b/server/src/handlers.rs @@ -1,10 +1,12 @@ +use std::str::FromStr; + use axum::extract::Path; use axum::http::HeaderMap; use axum::{extract::State, response::IntoResponse}; use bytes::Bytes; use http::{header, StatusCode}; -use pkarr::mainline::MutableItem; +use httpdate::HttpDate; use tracing::error; use pkarr::{PublicKey, DEFAULT_MAXIMUM_TTL, DEFAULT_MINIMUM_TTL}; @@ -29,15 +31,19 @@ pub async fn put( .publish(&signed_packet) .await .map_err(|error| match error { - pkarr::Error::PublishInflight => Error::new(StatusCode::TOO_MANY_REQUESTS, Some(error)), - pkarr::Error::NotMostRecent => Error::new(StatusCode::CONFLICT, Some(error)), - pkarr::Error::DhtIsShutdown => { - error!("Dht is shutdown"); - Error::with_status(StatusCode::INTERNAL_SERVER_ERROR) + pkarr::errors::PublishError::PublishInflight => { + Error::new(StatusCode::TOO_MANY_REQUESTS, Some(error)) + } + pkarr::errors::PublishError::NotMostRecent => { + Error::new(StatusCode::CONFLICT, Some(error)) + } + pkarr::errors::PublishError::ClientWasShutdown => { + error!("Pkarr client was shutdown"); + Error::new(StatusCode::INTERNAL_SERVER_ERROR, Some(error)) } error => { error!(?error, "Unexpected error"); - Error::with_status(StatusCode::INTERNAL_SERVER_ERROR) + Error::new(StatusCode::INTERNAL_SERVER_ERROR, Some(error)) } })?; @@ -47,57 +53,56 @@ pub async fn put( pub async fn get( State(state): State, Path(public_key): Path, + request_headers: HeaderMap, ) -> Result { let public_key = PublicKey::try_from(public_key.as_str()) .map_err(|error| Error::new(StatusCode::BAD_REQUEST, Some(error)))?; - let signed_packet = { - if let Some(signed_packet) = - state - .client - .resolve(&public_key) - .await - .map_err(|error| match error { - pkarr::Error::DhtIsShutdown => { - error!("Dht is shutdown"); - Error::with_status(StatusCode::INTERNAL_SERVER_ERROR) - } - error => { - error!(?error, "Unexpected error"); - Error::with_status(StatusCode::INTERNAL_SERVER_ERROR) - } - })? - { - Some(signed_packet) - } else { - // Respond with what we have, even if expired. - // TODO: move this fallback to the client itself, closing #67 - state - .client - .cache() - .get_read_only(&MutableItem::target_from_key(public_key.as_bytes(), &None)) - } - }; - - if let Some(signed_packet) = signed_packet { + if let Some(signed_packet) = state.client.resolve(&public_key).await? { tracing::debug!(?public_key, "cache hit responding with packet!"); - let body = signed_packet.to_relay_payload(); - - let ttl = signed_packet.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL); - - let mut header_map = HeaderMap::new(); + let mut response_headers = HeaderMap::new(); - header_map.insert( + response_headers.insert( header::CONTENT_TYPE, "application/pkarr.org/relays#payload".try_into().unwrap(), ); - header_map.insert( + response_headers.insert( header::CACHE_CONTROL, - format!("public, max-age={}", ttl).try_into().unwrap(), + format!( + "public, max-age={}", + signed_packet.ttl(DEFAULT_MINIMUM_TTL, DEFAULT_MAXIMUM_TTL) + ) + .try_into() + .unwrap(), ); + response_headers.insert( + header::LAST_MODIFIED, + signed_packet + .timestamp() + .format_http_date() + .try_into() + .expect("expect last-modified to be a valid HeaderValue"), + ); + + let mut response = response_headers.into_response(); + + // Handle IF_MODIFIED_SINCE + if let Some(condition_http_date) = request_headers + .get(header::IF_MODIFIED_SINCE) + .and_then(|h| h.to_str().ok()) + .and_then(|s| HttpDate::from_str(s).ok()) + { + let entry_http_date: HttpDate = signed_packet.timestamp().into(); + + if condition_http_date >= entry_http_date { + *response.status_mut() = StatusCode::NOT_MODIFIED; + } + } else { + *response.body_mut() = signed_packet.to_relay_payload().into(); + }; - Ok((header_map, body)) + Ok(response) } else { Err(Error::with_status(StatusCode::NOT_FOUND)) } diff --git a/server/src/http_server.rs b/server/src/http_server.rs index 8f12730..1a569cb 100644 --- a/server/src/http_server.rs +++ b/server/src/http_server.rs @@ -7,7 +7,7 @@ use tower_http::cors::CorsLayer; use tower_http::trace::TraceLayer; use tracing::{info, warn}; -use pkarr::PkarrClientAsync; +use pkarr::Client; use crate::rate_limiting::IpRateLimiter; @@ -18,7 +18,7 @@ pub struct HttpServer { impl HttpServer { /// Spawn the server pub async fn spawn( - client: PkarrClientAsync, + client: Client, port: u16, rate_limiter: IpRateLimiter, ) -> Result { @@ -92,5 +92,5 @@ pub fn create_app(state: AppState, rate_limiter: IpRateLimiter) -> Router { #[derive(Debug, Clone)] pub struct AppState { - pub client: PkarrClientAsync, + pub client: Client, } diff --git a/server/src/main.rs b/server/src/main.rs index 4099932..4800470 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,4 +1,3 @@ -mod cache; mod config; mod dht_server; mod error; @@ -7,15 +6,13 @@ mod http_server; mod rate_limiting; use anyhow::Result; -use cache::HeedPkarrCache; use clap::Parser; use config::Config; -use std::fs; use std::path::PathBuf; use tracing::{debug, info}; use http_server::HttpServer; -use pkarr::{mainline::dht::DhtSettings, PkarrClient}; +use pkarr::{extra::lmdb_cache::LmdbCache, mainline, Client}; #[derive(Parser, Debug)] struct Cli { @@ -46,32 +43,30 @@ async fn main() -> Result<()> { debug!(?config, "Pkarr server config"); - let env_path = &config.cache_path()?; - fs::create_dir_all(env_path)?; - let cache = Box::new(HeedPkarrCache::new(env_path, config.cache_size()).unwrap()); + let cache = Box::new(LmdbCache::new(&config.cache_path()?, config.cache_size())?); let rate_limiter = rate_limiting::IpRateLimiter::new(config.rate_limiter()); - let client = PkarrClient::builder() - .dht_settings(DhtSettings { - port: Some(config.dht_port()), - server: Some(Box::new(dht_server::DhtServer::new( - cache.clone(), - config.resolvers(), - config.minimum_ttl(), - config.maximum_ttl(), - rate_limiter.clone(), - ))), - ..DhtSettings::default() - }) + let client = Client::builder() + .dht_settings( + mainline::Settings::default() + .port(config.dht_port()) + .custom_server(Box::new(dht_server::DhtServer::new( + cache.clone(), + config.resolvers(), + config.minimum_ttl(), + config.maximum_ttl(), + rate_limiter.clone(), + ))), + ) .resolvers(config.resolvers()) .minimum_ttl(config.minimum_ttl()) .maximum_ttl(config.maximum_ttl()) .cache(cache) - .build()? - .as_async(); + .build()?; - let udp_address = client.local_addr().unwrap(); + let info = client.info()?; + let udp_address = info.local_addr()?; info!("Running as a resolver on UDP socket {udp_address}");