mirror of
https://github.com/aljazceru/pubky-core.git
synced 2025-12-31 21:04:34 +01:00
feat(pubky): use resolve_endpoint from pkarr
This commit is contained in:
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -117,9 +117,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.82"
|
||||
version = "0.1.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
|
||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -190,7 +190,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.1",
|
||||
"tokio",
|
||||
"tower 0.5.1",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -236,7 +236,7 @@ dependencies = [
|
||||
"serde",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.1",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -1133,8 +1133,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.8"
|
||||
source = "git+https://github.com/hyperium/hyper-util.git#2639193e9134a235db42cca16c8cff7f21f61661"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -1145,7 +1146,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1224,9 +1224,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.158"
|
||||
version = "0.2.159"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
@@ -1556,26 +1556,6 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.14"
|
||||
@@ -1591,7 +1571,6 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
[[package]]
|
||||
name = "pkarr"
|
||||
version = "3.0.0"
|
||||
source = "git+https://github.com/Pubky/pkarr?branch=v3#445c0db448b9e70f2a63a5b91e6d84f1ef466aeb"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"bytes",
|
||||
@@ -1604,12 +1583,14 @@ dependencies = [
|
||||
"js-sys",
|
||||
"lru",
|
||||
"mainline",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"simple-dns",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -1625,9 +1606,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
@@ -1689,6 +1670,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"pkarr",
|
||||
"pubky-common",
|
||||
@@ -1846,9 +1828,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.4"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853"
|
||||
checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -2431,18 +2413,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2591,9 +2573,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.21"
|
||||
version = "0.22.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -2602,21 +2584,6 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.1"
|
||||
@@ -3128,9 +3095,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -16,6 +16,3 @@ serde = { version = "^1.0.209", features = ["derive"] }
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[patch.crates-io]
|
||||
hyper-util = { git = "https://github.com/hyperium/hyper-util.git" }
|
||||
|
||||
@@ -6,10 +6,20 @@ const TLD = '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo';
|
||||
|
||||
// TODO: test HTTPs too somehow.
|
||||
|
||||
test.skip("basic fetch", async (t) => {
|
||||
test("basic fetch", async (t) => {
|
||||
let client = PubkyClient.testnet();
|
||||
|
||||
let response = await client.fetch(`http://${TLD}/`, new Uint8Array([]));
|
||||
// Normal TLD
|
||||
{
|
||||
|
||||
let response = await client.fetch(`http://relay.pkarr.org/`);
|
||||
|
||||
t.equal(response.status, 200);
|
||||
}
|
||||
|
||||
|
||||
// Pubky
|
||||
let response = await client.fetch(`http://${TLD}/`);
|
||||
|
||||
t.equal(response.status, 200);
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::PubkyClient;
|
||||
mod api;
|
||||
mod internals;
|
||||
|
||||
use internals::resolver::PkarrResolver;
|
||||
use internals::PkarrResolver;
|
||||
|
||||
static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
@@ -55,7 +55,7 @@ impl PubkyClientBuilder {
|
||||
// TODO: convert to Result<PubkyClient>
|
||||
|
||||
let pkarr = pkarr::Client::new(self.pkarr_settings).unwrap();
|
||||
let dns_resolver: PkarrResolver = pkarr.clone().into();
|
||||
let dns_resolver: PkarrResolver = (&pkarr).into();
|
||||
|
||||
PubkyClient {
|
||||
http: reqwest::Client::builder()
|
||||
|
||||
@@ -28,7 +28,7 @@ mod tests {
|
||||
use crate::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get() {
|
||||
async fn http_get_pubky() {
|
||||
let testnet = Testnet::new(10);
|
||||
|
||||
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
|
||||
@@ -45,4 +45,21 @@ mod tests {
|
||||
|
||||
assert_eq!(response.status(), 200)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_get_icann() {
|
||||
let testnet = Testnet::new(10);
|
||||
|
||||
let client = PubkyClient::builder().testnet(&testnet).build();
|
||||
|
||||
let url = format!("http://example.com/");
|
||||
|
||||
let response = client
|
||||
.request(Default::default(), url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Public API modules
|
||||
|
||||
pub mod http;
|
||||
pub mod recovery_file;
|
||||
|
||||
@@ -3,13 +3,43 @@ use url::Url;
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
mod endpoints;
|
||||
pub mod resolver;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use pkarr::{Client, EndpointResolver, PublicKey};
|
||||
use reqwest::dns::{Addrs, Resolve};
|
||||
|
||||
pub struct PkarrResolver(Client);
|
||||
|
||||
impl Resolve for PkarrResolver {
|
||||
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
|
||||
let client = self.0.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let name = name.as_str();
|
||||
|
||||
if PublicKey::try_from(name).is_ok() {
|
||||
let endpoint = client.resolve_endpoint(name).await?;
|
||||
|
||||
let addrs: Addrs = Box::new(endpoint.to_socket_addrs().into_iter());
|
||||
return Ok(addrs);
|
||||
};
|
||||
|
||||
Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&pkarr::Client> for PkarrResolver {
|
||||
fn from(pkarr: &pkarr::Client) -> Self {
|
||||
PkarrResolver(pkarr.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl PubkyClient {
|
||||
// === HTTP ===
|
||||
|
||||
pub(crate) fn inner_request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
|
||||
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn inner_request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
|
||||
self.http.request(method, url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
|
||||
use pkarr::dns::rdata::{RData, SVCB};
|
||||
use pkarr::dns::ResourceRecord;
|
||||
use pkarr::SignedPacket;
|
||||
use pubky_common::timestamp::Timestamp;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Endpoint {
|
||||
pub(crate) target: String,
|
||||
// public_key: PublicKey,
|
||||
pub(crate) port: u16,
|
||||
pub(crate) addrs: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
/// 1. Find the SVCB or HTTPS records with the lowest priority
|
||||
/// 2. Choose a random one of the list of the above
|
||||
/// 3. If the target is `.`, check A and AAAA records (https://www.rfc-editor.org/rfc/rfc9460#name-special-handling-of-in-targ)
|
||||
pub(crate) fn find(
|
||||
signed_packet: &SignedPacket,
|
||||
target: &str,
|
||||
is_svcb: bool,
|
||||
) -> Option<Endpoint> {
|
||||
let mut lowest_priority = u16::MAX;
|
||||
let mut lowest_priority_index = 0;
|
||||
let mut records = vec![];
|
||||
|
||||
for record in signed_packet.resource_records(target) {
|
||||
if let Some(svcb) = get_svcb(record, is_svcb) {
|
||||
match svcb.priority.cmp(&lowest_priority) {
|
||||
std::cmp::Ordering::Equal => records.push(svcb),
|
||||
std::cmp::Ordering::Less => {
|
||||
lowest_priority_index = records.len();
|
||||
lowest_priority = svcb.priority;
|
||||
records.push(svcb)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Good enough random selection
|
||||
let now = Timestamp::now();
|
||||
let slice = &records[lowest_priority_index..];
|
||||
let index = if slice.is_empty() {
|
||||
0
|
||||
} else {
|
||||
(now.into_inner() as usize) % slice.len()
|
||||
};
|
||||
|
||||
slice.get(index).map(|s| {
|
||||
let target = s.target.to_string();
|
||||
|
||||
let mut addrs: Vec<IpAddr> = vec![];
|
||||
|
||||
if &target == "." {
|
||||
for record in signed_packet.resource_records("@") {
|
||||
match &record.rdata {
|
||||
RData::A(ip) => addrs.push(IpAddr::V4(ip.address.into())),
|
||||
RData::AAAA(ip) => addrs.push(IpAddr::V6(ip.address.into())),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Endpoint {
|
||||
target,
|
||||
// public_key: signed_packet.public_key(),
|
||||
port: u16::from_be_bytes(
|
||||
s.get_param(SVCB::PORT)
|
||||
.unwrap_or_default()
|
||||
.try_into()
|
||||
.unwrap_or([0, 0]),
|
||||
),
|
||||
addrs,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_socket_addrs(&self) -> std::io::Result<std::vec::IntoIter<SocketAddr>> {
|
||||
if self.target == "." {
|
||||
let port = self.port;
|
||||
return Ok(self
|
||||
.addrs
|
||||
.iter()
|
||||
.map(|addr| SocketAddr::from((*addr, port)))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter());
|
||||
}
|
||||
|
||||
format!("{}:{}", self.target, self.port).to_socket_addrs()
|
||||
}
|
||||
}
|
||||
|
||||
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 std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
use pkarr::{dns, Keypair};
|
||||
|
||||
#[tokio::test]
|
||||
async fn endpoint_target() {
|
||||
let mut packet = dns::Packet::new_reply(0);
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("foo").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::HTTPS(SVCB::new(0, "https.example.com".try_into().unwrap()).into()),
|
||||
));
|
||||
// Make sure HTTPS only follows HTTPs
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("foo").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::SVCB(SVCB::new(0, "protocol.example.com".try_into().unwrap())),
|
||||
));
|
||||
// Make sure SVCB only follows SVCB
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("foo").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::HTTPS(SVCB::new(0, "https.example.com".try_into().unwrap()).into()),
|
||||
));
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("_foo").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::SVCB(SVCB::new(0, "protocol.example.com".try_into().unwrap())),
|
||||
));
|
||||
let keypair = Keypair::random();
|
||||
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
|
||||
|
||||
let tld = keypair.public_key();
|
||||
|
||||
// Follow foo.tld HTTPS records
|
||||
let endpoint = Endpoint::find(&signed_packet, &format!("foo.{tld}"), false).unwrap();
|
||||
assert_eq!(endpoint.target, "https.example.com");
|
||||
|
||||
// Follow _foo.tld SVCB records
|
||||
let endpoint = Endpoint::find(&signed_packet, &format!("_foo.{tld}"), true).unwrap();
|
||||
assert_eq!(endpoint.target, "protocol.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_to_socket_addrs() {
|
||||
let mut packet = dns::Packet::new_reply(0);
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("@").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::A(Ipv4Addr::from_str("209.151.148.15").unwrap().into()),
|
||||
));
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("@").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::AAAA(Ipv6Addr::from_str("2a05:d014:275:6201::64").unwrap().into()),
|
||||
));
|
||||
|
||||
let mut svcb = SVCB::new(1, ".".try_into().unwrap());
|
||||
svcb.set_port(6881);
|
||||
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("@").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::HTTPS(svcb.into()),
|
||||
));
|
||||
let keypair = Keypair::random();
|
||||
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
|
||||
|
||||
// Follow foo.tld HTTPS records
|
||||
let endpoint = Endpoint::find(
|
||||
&signed_packet,
|
||||
&signed_packet.public_key().to_string(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(endpoint.target, ".");
|
||||
|
||||
let addrs = endpoint.to_socket_addrs().unwrap();
|
||||
assert_eq!(
|
||||
addrs.map(|s| s.to_string()).collect::<Vec<_>>(),
|
||||
vec!["209.151.148.15:6881", "[2a05:d014:275:6201::64]:6881"]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use pkarr::PublicKey;
|
||||
use reqwest::dns::{Addrs, Resolve};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::endpoints::Endpoint;
|
||||
|
||||
const DEFAULT_MAX_CHAIN_LENGTH: u8 = 3;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PkarrResolver {
|
||||
pkarr: pkarr::Client,
|
||||
max_chain_length: u8,
|
||||
}
|
||||
|
||||
impl PkarrResolver {
|
||||
pub fn new(pkarr: pkarr::Client, max_chain_length: u8) -> Self {
|
||||
PkarrResolver {
|
||||
pkarr,
|
||||
max_chain_length,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a `qname` to an alternative [Endpoint] as defined in [RFC9460](https://www.rfc-editor.org/rfc/rfc9460#name-terminology).
|
||||
///
|
||||
/// A `qname` is can be either a regular domain name for HTTPS endpoints,
|
||||
/// or it could use Attrleaf naming pattern for cusotm protcol. For example:
|
||||
/// `_foo.example.com` for `foo://example.com`.
|
||||
async fn resolve_endpoint(&self, qname: &str) -> Result<Endpoint> {
|
||||
let target = qname;
|
||||
// TODO: cache the result of this function?
|
||||
|
||||
let is_svcb = target.starts_with('_');
|
||||
|
||||
let mut step = 0;
|
||||
let mut svcb: Option<Endpoint> = None;
|
||||
|
||||
loop {
|
||||
let current = svcb.clone().map_or(target.to_string(), |s| s.target);
|
||||
if let Ok(tld) = PublicKey::try_from(current.clone()) {
|
||||
if let Ok(Some(signed_packet)) = self.pkarr.resolve(&tld).await {
|
||||
if step >= self.max_chain_length {
|
||||
break;
|
||||
};
|
||||
step += 1;
|
||||
|
||||
// Choose most prior SVCB record
|
||||
svcb = Endpoint::find(&signed_packet, ¤t, is_svcb);
|
||||
|
||||
// TODO: support wildcard?
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(svcb) = svcb {
|
||||
if PublicKey::try_from(svcb.target.as_str()).is_err() {
|
||||
return Ok(svcb);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::ResolveEndpoint(target.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve for PkarrResolver {
|
||||
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
|
||||
let client = self.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let name = name.as_str();
|
||||
|
||||
if PublicKey::try_from(name).is_ok() {
|
||||
let endpoint = client.resolve_endpoint(name).await?;
|
||||
|
||||
// let addrs = format!("{}:{}", x.target, x.port).to_socket_addrs()?;
|
||||
|
||||
let addrs: Addrs = Box::new(endpoint.to_socket_addrs()?);
|
||||
|
||||
return Ok(addrs);
|
||||
};
|
||||
|
||||
Ok(Box::new(format!("{name}:0").to_socket_addrs().unwrap()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&pkarr::Client> for PkarrResolver {
|
||||
fn from(pkarr: &pkarr::Client) -> Self {
|
||||
pkarr.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pkarr::Client> for PkarrResolver {
|
||||
fn from(pkarr: pkarr::Client) -> Self {
|
||||
Self::new(pkarr, DEFAULT_MAX_CHAIN_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pkarr::dns::rdata::{A, SVCB};
|
||||
use pkarr::dns::{self, rdata::RData};
|
||||
use pkarr::SignedPacket;
|
||||
use pkarr::{mainline::Testnet, Keypair};
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
fn generate_subtree(
|
||||
client: pkarr::Client,
|
||||
depth: u8,
|
||||
branching: u8,
|
||||
domain: Option<String>,
|
||||
) -> Pin<Box<dyn Future<Output = PublicKey>>> {
|
||||
Box::pin(async move {
|
||||
let keypair = Keypair::random();
|
||||
|
||||
let mut packet = dns::Packet::new_reply(0);
|
||||
|
||||
for _ in 0..branching {
|
||||
let mut svcb = SVCB::new(0, ".".try_into().unwrap());
|
||||
|
||||
if depth == 0 {
|
||||
svcb.priority = 1;
|
||||
svcb.set_port((branching) as u16 * 1000);
|
||||
|
||||
if let Some(target) = &domain {
|
||||
let target: &'static str = Box::leak(target.clone().into_boxed_str());
|
||||
svcb.target = target.try_into().unwrap()
|
||||
}
|
||||
} else {
|
||||
let target =
|
||||
generate_subtree(client.clone(), depth - 1, branching, domain.clone())
|
||||
.await
|
||||
.to_string();
|
||||
let target: &'static str = Box::leak(target.into_boxed_str());
|
||||
svcb.target = target.try_into().unwrap();
|
||||
};
|
||||
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("@").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::HTTPS(svcb.into()),
|
||||
));
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
packet.answers.push(dns::ResourceRecord::new(
|
||||
dns::Name::new("@").unwrap(),
|
||||
dns::CLASS::IN,
|
||||
3600,
|
||||
RData::A(A { address: 10 }),
|
||||
));
|
||||
}
|
||||
|
||||
let signed_packet = SignedPacket::from_packet(&keypair, &packet).unwrap();
|
||||
client.publish(&signed_packet).await.unwrap();
|
||||
|
||||
keypair.public_key()
|
||||
})
|
||||
}
|
||||
|
||||
fn generate(
|
||||
client: pkarr::Client,
|
||||
depth: u8,
|
||||
branching: u8,
|
||||
domain: Option<String>,
|
||||
) -> Pin<Box<dyn Future<Output = PublicKey>>> {
|
||||
generate_subtree(client, depth - 1, branching, domain)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_endpoints() {
|
||||
let testnet = Testnet::new(3);
|
||||
let pkarr = pkarr::Client::builder().testnet(&testnet).build().unwrap();
|
||||
|
||||
let resolver: PkarrResolver = (&pkarr).into();
|
||||
let tld = generate(pkarr, 3, 3, Some("example.com".to_string())).await;
|
||||
|
||||
let endpoint = resolver.resolve_endpoint(&tld.to_string()).await.unwrap();
|
||||
assert_eq!(endpoint.target, "example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn max_chain_exceeded() {
|
||||
let testnet = Testnet::new(3);
|
||||
let pkarr = pkarr::Client::builder().testnet(&testnet).build().unwrap();
|
||||
|
||||
let resolver: PkarrResolver = (&pkarr).into();
|
||||
|
||||
let tld = generate(pkarr, 4, 3, Some("example.com".to_string())).await;
|
||||
|
||||
let endpoint = resolver.resolve_endpoint(&tld.to_string()).await;
|
||||
|
||||
assert_eq!(
|
||||
match endpoint {
|
||||
Err(error) => error.to_string(),
|
||||
_ => "".to_string(),
|
||||
},
|
||||
crate::Error::ResolveEndpoint(tld.to_string()).to_string()
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_addresses() {
|
||||
let testnet = Testnet::new(3);
|
||||
let pkarr = pkarr::Client::builder().testnet(&testnet).build().unwrap();
|
||||
|
||||
let resolver: PkarrResolver = (&pkarr).into();
|
||||
let tld = generate(pkarr, 3, 3, None).await;
|
||||
|
||||
let endpoint = resolver.resolve_endpoint(&tld.to_string()).await.unwrap();
|
||||
assert_eq!(endpoint.target, ".");
|
||||
assert_eq!(endpoint.port, 3000);
|
||||
assert_eq!(
|
||||
endpoint
|
||||
.to_socket_addrs()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
vec!["0.0.0.10:3000"]
|
||||
);
|
||||
dbg!(&endpoint);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ impl PubkyClient {
|
||||
|
||||
let response = self
|
||||
.inner_request(Method::POST, url.clone())
|
||||
.await
|
||||
.body(body)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -59,7 +60,7 @@ impl PubkyClient {
|
||||
|
||||
url.set_path(&format!("/{}/session", pubky));
|
||||
|
||||
let res = self.inner_request(Method::GET, url).send().await?;
|
||||
let res = self.inner_request(Method::GET, url).await.send().await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
@@ -80,7 +81,7 @@ impl PubkyClient {
|
||||
|
||||
url.set_path(&format!("/{}/session", pubky));
|
||||
|
||||
self.inner_request(Method::DELETE, url).send().await?;
|
||||
self.inner_request(Method::DELETE, url).await.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -140,6 +141,7 @@ impl PubkyClient {
|
||||
drop(path_segments);
|
||||
|
||||
self.inner_request(Method::POST, callback)
|
||||
.await
|
||||
.body(encrypted_token)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -167,6 +169,7 @@ impl PubkyClient {
|
||||
|
||||
let response = self
|
||||
.inner_request(Method::POST, url)
|
||||
.await
|
||||
.body(token.serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -90,7 +90,12 @@ impl<'a> ListBuilder<'a> {
|
||||
|
||||
drop(query);
|
||||
|
||||
let response = self.client.inner_request(Method::GET, url).send().await?;
|
||||
let response = self
|
||||
.client
|
||||
.inner_request(Method::GET, url)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
response.error_for_status_ref()?;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ impl PubkyClient {
|
||||
|
||||
let response = self
|
||||
.inner_request(Method::PUT, url)
|
||||
.await
|
||||
.body(content.to_owned())
|
||||
.send()
|
||||
.await?;
|
||||
@@ -29,7 +30,7 @@ impl PubkyClient {
|
||||
pub(crate) async fn inner_get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
|
||||
let url = self.pubky_to_http(url).await?;
|
||||
|
||||
let response = self.inner_request(Method::GET, url).send().await?;
|
||||
let response = self.inner_request(Method::GET, url).await.send().await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
@@ -46,7 +47,7 @@ impl PubkyClient {
|
||||
pub(crate) async fn inner_delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
|
||||
let url = self.pubky_to_http(url).await?;
|
||||
|
||||
let response = self.inner_request(Method::DELETE, url).send().await?;
|
||||
let response = self.inner_request(Method::DELETE, url).await.send().await?;
|
||||
|
||||
response.error_for_status_ref()?;
|
||||
|
||||
@@ -650,10 +651,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.inner_request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -683,13 +681,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.inner_request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10&cursor={cursor}")
|
||||
.as_str()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -740,10 +732,7 @@ mod tests {
|
||||
|
||||
{
|
||||
let response = client
|
||||
.inner_request(
|
||||
Method::GET,
|
||||
format!("{feed_url}?limit=10").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -801,10 +790,7 @@ mod tests {
|
||||
let feed_url = format!("http://localhost:{}/events/", homeserver.port());
|
||||
|
||||
let response = client
|
||||
.inner_request(
|
||||
Method::GET,
|
||||
format!("{feed_url}").as_str().try_into().unwrap(),
|
||||
)
|
||||
.request(Method::GET, format!("{feed_url}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -7,6 +7,8 @@ use reqwest::Url;
|
||||
|
||||
use crate::PubkyClient;
|
||||
|
||||
use super::super::internals::resolve;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl PubkyClient {
|
||||
#[wasm_bindgen]
|
||||
@@ -19,7 +21,9 @@ impl PubkyClient {
|
||||
JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `url`; {:?}", err))
|
||||
})?;
|
||||
|
||||
self.resolve_url(&mut url).await.map_err(JsValue::from)?;
|
||||
resolve(&self.pkarr, &mut url)
|
||||
.await
|
||||
.map_err(|err| JsValue::from_str(&format!("PubkyClient::fetch(): {:?}", err)))?;
|
||||
|
||||
let js_req =
|
||||
web_sys::Request::new_with_str_and_init(url.as_str(), init).map_err(|err| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Public API modules
|
||||
|
||||
pub mod auth;
|
||||
pub mod http;
|
||||
pub mod public;
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
pub mod auth;
|
||||
pub mod public;
|
||||
@@ -1,10 +1,33 @@
|
||||
use crate::PubkyClient;
|
||||
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use url::Url;
|
||||
|
||||
use pkarr::{EndpointResolver, PublicKey};
|
||||
|
||||
use crate::{error::Result, PubkyClient};
|
||||
|
||||
// TODO: remove expect
|
||||
pub async fn resolve(pkarr: &pkarr::Client, url: &mut Url) -> Result<()> {
|
||||
let qname = url.host_str().expect("URL TO HAVE A HOST!").to_string();
|
||||
|
||||
// If http and has a Pubky TLD, switch to socket addresses.
|
||||
if url.scheme() == "http" && PublicKey::try_from(qname.as_str()).is_ok() {
|
||||
let endpoint = pkarr.resolve_endpoint(&qname).await?;
|
||||
|
||||
if let Some(socket_address) = endpoint.to_socket_addrs().into_iter().next() {
|
||||
url.set_host(Some(&socket_address.to_string()))?;
|
||||
let _ = url.set_port(Some(socket_address.port()));
|
||||
} else if let Some(port) = endpoint.port() {
|
||||
url.set_host(Some(endpoint.target()))?;
|
||||
let _ = url.set_port(Some(port));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl PubkyClient {
|
||||
pub(crate) fn inner_request(&self, method: Method, url: Url) -> RequestBuilder {
|
||||
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn inner_request(&self, method: Method, url: Url) -> RequestBuilder {
|
||||
self.http.request(method, url).fetch_credentials_include()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user