Merge pull request #36 from pubky/feat/pkarr-reqwest

Feat/pkarr reqwest
This commit is contained in:
Nuh
2024-10-01 08:36:10 +03:00
committed by GitHub
37 changed files with 712 additions and 603 deletions

9
Cargo.lock generated
View File

@@ -1066,9 +1066,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"
@@ -1689,6 +1689,7 @@ dependencies = [
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
@@ -1899,9 +1900,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.7"
version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",

View File

@@ -1,4 +1,4 @@
//! Strictly monotonic unix timestamp in microseconds
//! Absolutely monotonic unix timestamp in microseconds
use serde::{Deserialize, Serialize};
use std::fmt::Display;
@@ -31,7 +31,7 @@ impl TimestampFactory {
}
pub fn now(&mut self) -> Timestamp {
// Ensure strict monotonicity.
// Ensure absolute monotonicity.
self.last_time = (system_time() & TIME_MASK).max(self.last_time + CLOCK_MASK + 1);
// Add clock_id to the end of the timestamp
@@ -48,13 +48,13 @@ impl Default for TimestampFactory {
static DEFAULT_FACTORY: Lazy<Mutex<TimestampFactory>> =
Lazy::new(|| Mutex::new(TimestampFactory::default()));
/// STrictly monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64.
/// Absolutely monotonic timestamp since [SystemTime::UNIX_EPOCH] in microseconds as u64.
///
/// The purpose of this timestamp is to unique per "user", not globally,
/// it achieves this by:
/// 1. Override the last byte with a random `clock_id`, reducing the probability
/// of two matching timestamps across multiple machines/threads.
/// 2. Gurantee that the remaining 3 bytes are ever increasing (strictly monotonic) within
/// 2. Gurantee that the remaining 3 bytes are ever increasing (absolutely monotonic) within
/// the same thread regardless of the wall clock value
///
/// This timestamp is also serialized as BE bytes to remain sortable.
@@ -215,7 +215,7 @@ mod tests {
use super::*;
#[test]
fn strictly_monotonic() {
fn absolutely_monotonic() {
const COUNT: usize = 100;
let mut set = HashSet::with_capacity(COUNT);

View File

@@ -168,8 +168,8 @@ impl Config {
self.bootstrap.to_owned()
}
pub fn domain(&self) -> &Option<String> {
&self.domain
pub fn domain(&self) -> Option<&String> {
self.domain.as_ref()
}
pub fn keypair(&self) -> &Keypair {

View File

@@ -1,10 +1,14 @@
# Use testnet network (local DHT) for testing.
testnet = false
# testnet = false
# Secret key (in hex) to generate the Homeserver's Keypair
secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
# Domain to be published in Pkarr records for this server to be accessible by.
domain = "localhost"
# secret_key = "0000000000000000000000000000000000000000000000000000000000000000"
# ICANN domain pointing to this server to allow browsers to connect to it.
# domain = "example.com"
# Port for the Homeserver to listen on.
port = 6287
# Storage directory Defaults to <System's Data Directory>
# storage = ""

View File

@@ -1,44 +1,63 @@
//! Pkarr related task
use std::net::Ipv4Addr;
use pkarr::{
dns::{rdata::SVCB, Packet},
Keypair, SignedPacket,
dns::{
rdata::{RData, A, SVCB},
Packet,
},
SignedPacket,
};
use crate::config::Config;
pub async fn publish_server_packet(
pkarr_client: &pkarr::Client,
keypair: &Keypair,
domain: &str,
config: &Config,
port: u16,
) -> anyhow::Result<()> {
// TODO: Try to resolve first before publishing.
let mut packet = Packet::new_reply(0);
let mut svcb = SVCB::new(0, domain.try_into()?);
let default = ".".to_string();
let target = config.domain().unwrap_or(&default);
let mut svcb = SVCB::new(0, target.as_str().try_into()?);
// Publishing port only for localhost domain,
// assuming any other domain will point to a reverse proxy
// at the conventional ports.
if domain == "localhost" {
svcb.priority = 1;
svcb.set_port(port);
// TODO: Add more parameteres like the signer key!
// svcb.set_param(key, value)
};
// TODO: announce A/AAAA records as well for Noise connections?
// Or maybe Iroh's magic socket
svcb.priority = 1;
svcb.set_port(port);
packet.answers.push(pkarr::dns::ResourceRecord::new(
"@".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
pkarr::dns::rdata::RData::SVCB(svcb),
RData::HTTPS(svcb.clone().into()),
));
let signed_packet = SignedPacket::from_packet(keypair, &packet)?;
if config.domain().is_none() {
// TODO: remove after remvoing Pubky shared/public
// and add local host IP address instead.
svcb.target = "localhost".try_into().unwrap();
packet.answers.push(pkarr::dns::ResourceRecord::new(
"@".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
RData::HTTPS(svcb.clone().into()),
));
packet.answers.push(pkarr::dns::ResourceRecord::new(
"@".try_into().unwrap(),
pkarr::dns::CLASS::IN,
60 * 60,
RData::A(A::from(Ipv4Addr::from([127, 0, 0, 1]))),
));
}
// TODO: announce A/AAAA records as well for TLS connections?
let signed_packet = SignedPacket::from_packet(config.keypair(), &packet)?;
pkarr_client.publish(&signed_packet).await?;

View File

@@ -51,7 +51,7 @@ impl Homeserver {
let state = AppState {
verifier: AuthVerifier::default(),
db,
pkarr_client,
pkarr_client: pkarr_client.clone(),
config: config.clone(),
port,
};
@@ -70,20 +70,10 @@ impl Homeserver {
info!("Homeserver listening on http://localhost:{port}");
publish_server_packet(
&state.pkarr_client,
config.keypair(),
&state
.config
.domain()
.clone()
.unwrap_or("localhost".to_string()),
port,
)
.await?;
publish_server_packet(&pkarr_client, &config, port).await?;
info!(
"Homeserver listening on pubky://{}",
"Homeserver listening on http://{}",
config.keypair().public_key()
);

View File

@@ -18,19 +18,23 @@ bytes = "^1.7.1"
base64 = "0.22.1"
pubky-common = { version = "0.1.0", path = "../pubky-common" }
pkarr = { workspace = true }
pkarr = { workspace = true, features = ["endpoints"] }
# Native dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
reqwest = { version = "0.12.5", features = ["cookies", "rustls-tls"], default-features = false }
tokio = { version = "1.37.0", features = ["full"] }
# Wasm dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
reqwest = { version = "0.12.5", default-features = false }
js-sys = "0.3.69"
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
js-sys = "0.3.69"
web-sys = "0.3.70"
[dev-dependencies]
pubky_homeserver = { path = "../pubky-homeserver" }
tokio = "1.37.0"

View File

@@ -1,4 +1,3 @@
index.cjs
browser.js
coverage
node_modules

6
pubky/pkg/index.cjs Normal file
View File

@@ -0,0 +1,6 @@
const makeFetchCookie = require("fetch-cookie").default;
let originalFetch = globalThis.fetch;
globalThis.fetch = makeFetchCookie(originalFetch);
module.exports = require('./nodejs/pubky')

View File

@@ -37,5 +37,8 @@
"esmify": "^2.1.1",
"tape": "^5.8.1",
"tape-run": "^11.0.0"
},
"dependencies": {
"fetch-cookie": "^3.0.1"
}
}

26
pubky/pkg/test/http.js Normal file
View File

@@ -0,0 +1,26 @@
import test from 'tape'
import { PubkyClient, Keypair, PublicKey } from '../index.cjs'
const TLD = '8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo';
// TODO: test HTTPs too somehow.
test("basic fetch", async (t) => {
let client = PubkyClient.testnet();
// 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);
})

View File

@@ -55,12 +55,3 @@ const bytes = __toBinary(${JSON.stringify(await readFile(path.join(__dirname, `.
);
await writeFile(path.join(__dirname, `../../pkg/browser.js`), patched + "\nglobalThis['pubky'] = imports");
// Move outside of nodejs
await Promise.all([".js", ".d.ts", "_bg.wasm"].map(suffix =>
rename(
path.join(__dirname, `../../pkg/nodejs/${name}${suffix}`),
path.join(__dirname, `../../pkg/${suffix === '.js' ? "index.cjs" : (name + suffix)}`),
))
)

View File

@@ -12,9 +12,6 @@ pub enum Error {
#[error("Generic error: {0}")]
Generic(String),
#[error("Could not resolve endpoint for {0}")]
ResolveEndpoint(String),
#[error("Could not convert the passed type into a Url")]
InvalidUrl,
@@ -42,6 +39,9 @@ pub enum Error {
#[error(transparent)]
AuthToken(#[from] pubky_common::auth::Error),
#[error("Could not resolve Endpoint for domain: {0}")]
ResolveEndpoint(String),
}
#[cfg(target_arch = "wasm32")]

View File

@@ -6,11 +6,6 @@ mod native;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "wasm32")]
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
use wasm_bindgen::prelude::*;
@@ -23,11 +18,5 @@ pub use crate::shared::list_builder::ListBuilder;
#[wasm_bindgen]
pub struct PubkyClient {
http: reqwest::Client,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) pkarr: pkarr::Client,
/// A cookie jar for nodejs fetch.
#[cfg(target_arch = "wasm32")]
pub(crate) session_cookies: Arc<RwLock<HashSet<String>>>,
#[cfg(target_arch = "wasm32")]
pub(crate) pkarr_relays: Vec<String>,
}

View File

@@ -1,25 +1,14 @@
use std::net::ToSocketAddrs;
use std::time::Duration;
use std::{net::ToSocketAddrs, sync::Arc};
use bytes::Bytes;
use pubky_common::{
capabilities::Capabilities,
recovery_file::{create_recovery_file, decrypt_recovery_file},
session::Session,
};
use reqwest::{RequestBuilder, Response};
use tokio::sync::oneshot;
use url::Url;
use ::pkarr::mainline::dht::Testnet;
use pkarr::Keypair;
use crate::PubkyClient;
use ::pkarr::{mainline::dht::Testnet, PublicKey, SignedPacket};
mod api;
mod internals;
use crate::{
error::{Error, Result},
shared::list_builder::ListBuilder,
PubkyClient,
};
use internals::PkarrResolver;
static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
@@ -63,13 +52,19 @@ impl PubkyClientBuilder {
/// Build [PubkyClient]
pub fn build(self) -> PubkyClient {
// TODO: convert to Result<PubkyClient>
let pkarr = pkarr::Client::new(self.pkarr_settings).unwrap();
let dns_resolver: PkarrResolver = (&pkarr).into();
PubkyClient {
http: reqwest::Client::builder()
.cookie_store(true)
.dns_resolver(Arc::new(dns_resolver))
.user_agent(DEFAULT_USER_AGENT)
.build()
.unwrap(),
pkarr: pkarr::Client::new(self.pkarr_settings).unwrap(),
pkarr,
}
}
}
@@ -80,8 +75,6 @@ impl Default for PubkyClient {
}
}
// === Public API ===
impl PubkyClient {
/// Returns a builder to edit settings before creating [PubkyClient].
pub fn builder() -> PubkyClientBuilder {
@@ -111,148 +104,4 @@ impl PubkyClient {
builder.build()
}
// === Getters ===
/// Returns a reference to the internal [pkarr] Client.
pub fn pkarr(&self) -> &pkarr::Client {
&self.pkarr
}
// === Auth ===
/// Signup to a homeserver and update Pkarr accordingly.
///
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<Session> {
self.inner_signup(keypair, homeserver).await
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
/// or [reqwest::Error] if the response has any other `>=400` status code.
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
self.inner_session(pubky).await
}
/// Signout from a homeserver.
pub async fn signout(&self, pubky: &PublicKey) -> Result<()> {
self.inner_signout(pubky).await
}
/// Signin to a homeserver.
pub async fn signin(&self, keypair: &Keypair) -> Result<Session> {
self.inner_signin(keypair).await
}
// === Public data ===
/// Upload a small payload to a given path.
pub async fn put<T: TryInto<Url>>(&self, url: T, content: &[u8]) -> Result<()> {
self.inner_put(url, content).await
}
/// Download a small payload from a given path relative to a pubky author.
pub async fn get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
self.inner_get(url).await
}
/// Delete a file at a path relative to a pubky author.
pub async fn delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
self.inner_delete(url).await
}
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
///
/// `url` sets the path you want to lest within.
pub fn list<T: TryInto<Url>>(&self, url: T) -> Result<ListBuilder> {
self.inner_list(url)
}
// === Helpers ===
/// Create a recovery file of the `keypair`, containing the secret key encrypted
/// using the `passphrase`.
pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result<Vec<u8>> {
Ok(create_recovery_file(keypair, passphrase)?)
}
/// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`.
pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result<Keypair> {
Ok(decrypt_recovery_file(recovery_file, passphrase)?)
}
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
/// verifying that AuthToken, and if capabilities were requested, signing in to
/// the Pubky's homeserver and returning the [Session] information.
pub fn auth_request(
&self,
relay: impl TryInto<Url>,
capabilities: &Capabilities,
) -> Result<(Url, tokio::sync::oneshot::Receiver<PublicKey>)> {
let mut relay: Url = relay
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?;
let (tx, rx) = oneshot::channel::<PublicKey>();
let this = self.clone();
tokio::spawn(async move {
let to_send = this
.subscribe_to_auth_response(relay, &client_secret)
.await?;
tx.send(to_send)
.map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?;
Ok::<(), Error>(())
});
Ok((pubkyauth_url, rx))
}
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
/// source of the pubkyauth request url.
pub async fn send_auth_token<T: TryInto<Url>>(
&self,
keypair: &Keypair,
pubkyauth_url: T,
) -> Result<()> {
let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?;
self.inner_send_auth_token(keypair, url).await?;
Ok(())
}
}
// === Internals ===
impl PubkyClient {
// === Pkarr ===
pub(crate) async fn pkarr_resolve(
&self,
public_key: &PublicKey,
) -> Result<Option<SignedPacket>> {
Ok(self.pkarr.resolve(public_key).await?)
}
pub(crate) async fn pkarr_publish(&self, signed_packet: &SignedPacket) -> Result<()> {
Ok(self.pkarr.publish(signed_packet).await?)
}
// === HTTP ===
pub(crate) fn request(&self, method: reqwest::Method, url: Url) -> RequestBuilder {
self.http.request(method, url)
}
pub(crate) fn store_session(&self, _: &Response) {}
pub(crate) fn remove_session(&self, _: &PublicKey) {}
}

View File

@@ -0,0 +1,85 @@
use pkarr::Keypair;
use pubky_common::session::Session;
use tokio::sync::oneshot;
use url::Url;
use pkarr::PublicKey;
use pubky_common::capabilities::Capabilities;
use crate::error::{Error, Result};
use crate::PubkyClient;
impl PubkyClient {
/// Signup to a homeserver and update Pkarr accordingly.
///
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
pub async fn signup(&self, keypair: &Keypair, homeserver: &PublicKey) -> Result<Session> {
self.inner_signup(keypair, homeserver).await
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
/// or [reqwest::Error] if the response has any other `>=400` status code.
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
self.inner_session(pubky).await
}
/// Signout from a homeserver.
pub async fn signout(&self, pubky: &PublicKey) -> Result<()> {
self.inner_signout(pubky).await
}
/// Signin to a homeserver.
pub async fn signin(&self, keypair: &Keypair) -> Result<Session> {
self.inner_signin(keypair).await
}
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
/// verifying that AuthToken, and if capabilities were requested, signing in to
/// the Pubky's homeserver and returning the [Session] information.
pub fn auth_request(
&self,
relay: impl TryInto<Url>,
capabilities: &Capabilities,
) -> Result<(Url, tokio::sync::oneshot::Receiver<PublicKey>)> {
let mut relay: Url = relay
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?;
let (tx, rx) = oneshot::channel::<PublicKey>();
let this = self.clone();
tokio::spawn(async move {
let to_send = this
.subscribe_to_auth_response(relay, &client_secret)
.await?;
tx.send(to_send)
.map_err(|_| Error::Generic("Failed to send the session after signing in with token, since the receiver is dropped".into()))?;
Ok::<(), Error>(())
});
Ok((pubkyauth_url, rx))
}
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
/// source of the pubkyauth request url.
pub async fn send_auth_token<T: TryInto<Url>>(
&self,
keypair: &Keypair,
pubkyauth_url: T,
) -> Result<()> {
let url: Url = pubkyauth_url.try_into().map_err(|_| Error::InvalidUrl)?;
self.inner_send_auth_token(keypair, url).await?;
Ok(())
}
}

View File

@@ -0,0 +1,65 @@
use reqwest::{IntoUrl, Method, RequestBuilder};
use crate::PubkyClient;
impl PubkyClient {
/// Start building a `Request` with the `Method` and `Url`.
///
/// Returns a `RequestBuilder`, which will allow setting headers and
/// the request body before sending.
///
/// Differs from [reqwest::Client::request], in that it can make requests
/// to URLs with a [pkarr::PublicKey] as Top Level Domain, by resolving
/// corresponding endpoints, and verifying TLS certificates accordingly.
///
/// # Errors
///
/// This method fails whenever the supplied `Url` cannot be parsed.
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
self.http.request(method, url)
}
}
#[cfg(test)]
mod tests {
use pkarr::mainline::Testnet;
use pubky_homeserver::Homeserver;
use crate::*;
#[tokio::test]
async fn http_get_pubky() {
let testnet = Testnet::new(10).unwrap();
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
let client = PubkyClient::builder().testnet(&testnet).build();
let url = format!("http://{}/", homeserver.public_key());
let response = client
.request(Default::default(), url)
.send()
.await
.unwrap();
assert_eq!(response.status(), 200)
}
#[tokio::test]
async fn http_get_icann() {
let testnet = Testnet::new(10).unwrap();
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);
}
}

View File

@@ -0,0 +1,6 @@
pub mod http;
pub mod recovery_file;
// TODO: put the Homeserver API behind a feature flag
pub mod auth;
pub mod public;

View File

@@ -0,0 +1,28 @@
use bytes::Bytes;
use url::Url;
use crate::{error::Result, shared::list_builder::ListBuilder, PubkyClient};
impl PubkyClient {
/// Upload a small payload to a given path.
pub async fn put<T: TryInto<Url>>(&self, url: T, content: &[u8]) -> Result<()> {
self.inner_put(url, content).await
}
/// Download a small payload from a given path relative to a pubky author.
pub async fn get<T: TryInto<Url>>(&self, url: T) -> Result<Option<Bytes>> {
self.inner_get(url).await
}
/// Delete a file at a path relative to a pubky author.
pub async fn delete<T: TryInto<Url>>(&self, url: T) -> Result<()> {
self.inner_delete(url).await
}
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
///
/// `url` sets the path you want to lest within.
pub fn list<T: TryInto<Url>>(&self, url: T) -> Result<ListBuilder> {
self.inner_list(url)
}
}

View File

@@ -0,0 +1,19 @@
use pubky_common::{
crypto::Keypair,
recovery_file::{create_recovery_file, decrypt_recovery_file},
};
use crate::{error::Result, PubkyClient};
impl PubkyClient {
/// Create a recovery file of the `keypair`, containing the secret key encrypted
/// using the `passphrase`.
pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Result<Vec<u8>> {
Ok(create_recovery_file(keypair, passphrase)?)
}
/// Recover a keypair from a recovery file by decrypting the secret key using `passphrase`.
pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result<Keypair> {
Ok(decrypt_recovery_file(recovery_file, passphrase)?)
}
}

View File

@@ -0,0 +1,45 @@
use reqwest::RequestBuilder;
use url::Url;
use crate::PubkyClient;
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 ===
/// 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)
}
}

View File

@@ -38,13 +38,12 @@ impl PubkyClient {
let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize();
let response = self
.request(Method::POST, url.clone())
.inner_request(Method::POST, url.clone())
.await
.body(body)
.send()
.await?;
self.store_session(&response);
self.publish_pubky_homeserver(keypair, &homeserver).await?;
let bytes = response.bytes().await?;
@@ -61,7 +60,7 @@ impl PubkyClient {
url.set_path(&format!("/{}/session", pubky));
let res = self.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);
@@ -82,9 +81,7 @@ impl PubkyClient {
url.set_path(&format!("/{}/session", pubky));
self.request(Method::DELETE, url).send().await?;
self.remove_session(pubky);
self.inner_request(Method::DELETE, url).await.send().await?;
Ok(())
}
@@ -143,7 +140,8 @@ impl PubkyClient {
path_segments.push(&channel_id);
drop(path_segments);
self.request(Method::POST, callback)
self.inner_request(Method::POST, callback)
.await
.body(encrypted_token)
.send()
.await?;
@@ -170,13 +168,12 @@ impl PubkyClient {
self.resolve_url(&mut url).await?;
let response = self
.request(Method::POST, url)
.inner_request(Method::POST, url)
.await
.body(token.serialize())
.send()
.await?;
self.store_session(&response);
let bytes = response.bytes().await?;
Ok(Session::deserialize(&bytes)?)
@@ -309,6 +306,9 @@ mod tests {
assert_eq!(&public_key, &pubky);
let session = client.session(&pubky).await.unwrap().unwrap();
assert_eq!(session.capabilities(), &capabilities.0);
// Test access control enforcement
client

View File

@@ -90,7 +90,12 @@ impl<'a> ListBuilder<'a> {
drop(query);
let response = self.client.request(Method::GET, url).send().await?;
let response = self
.client
.inner_request(Method::GET, url)
.await
.send()
.await?;
response.error_for_status_ref()?;

View File

@@ -19,7 +19,7 @@ impl PubkyClient {
keypair: &Keypair,
host: &str,
) -> Result<()> {
let existing = self.pkarr_resolve(&keypair.public_key()).await?;
let existing = self.pkarr.resolve(&keypair.public_key()).await?;
let mut packet = Packet::new_reply(0);
@@ -42,7 +42,7 @@ impl PubkyClient {
let signed_packet = SignedPacket::from_packet(keypair, &packet)?;
self.pkarr_publish(&signed_packet).await?;
self.pkarr.publish(&signed_packet).await?;
Ok(())
}
@@ -81,7 +81,8 @@ impl PubkyClient {
step += 1;
if let Some(signed_packet) = self
.pkarr_resolve(&public_key)
.pkarr
.resolve(&public_key)
.await
.map_err(|_| Error::ResolveEndpoint(original_target.into()))?
{
@@ -185,8 +186,8 @@ mod tests {
rdata::{HTTPS, SVCB},
Packet,
},
mainline::{dht::DhtSettings, Testnet},
Keypair, Settings, SignedPacket,
mainline::Testnet,
Keypair, SignedPacket,
};
use pubky_homeserver::Homeserver;
@@ -194,14 +195,7 @@ mod tests {
async fn resolve_endpoint_https() {
let testnet = Testnet::new(10).unwrap();
let pkarr_client = pkarr::Client::new(Settings {
dht: DhtSettings {
bootstrap: Some(testnet.bootstrap.clone()),
..Default::default()
},
..Default::default()
})
.unwrap();
let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap();
let domain = "example.com";
let mut target;
@@ -284,14 +278,7 @@ mod tests {
let server = Homeserver::start_test(&testnet).await.unwrap();
// Publish an intermediate controller of the homeserver
let pkarr_client = pkarr::Client::new(Settings {
dht: DhtSettings {
bootstrap: Some(testnet.bootstrap.clone()),
..Default::default()
},
..Default::default()
})
.unwrap();
let pkarr_client = pkarr::Client::builder().testnet(&testnet).build().unwrap();
let intermediate = Keypair::random();

View File

@@ -16,7 +16,8 @@ impl PubkyClient {
let url = self.pubky_to_http(url).await?;
let response = self
.request(Method::PUT, url)
.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.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.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
.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
.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
.request(
Method::GET,
format!("{feed_url}?limit=10").as_str().try_into().unwrap(),
)
.request(Method::GET, format!("{feed_url}?limit=10"))
.send()
.await
.unwrap();
@@ -762,8 +751,9 @@ mod tests {
);
}
let get = client.get(url.as_str()).await.unwrap();
dbg!(get);
let get = client.get(url.as_str()).await.unwrap().unwrap();
assert_eq!(get.as_ref(), &[0]);
}
#[tokio::test]
@@ -800,10 +790,7 @@ mod tests {
let feed_url = format!("http://localhost:{}/events/", homeserver.port());
let response = client
.request(
Method::GET,
format!("{feed_url}").as_str().try_into().unwrap(),
)
.request(Method::GET, format!("{feed_url}"))
.send()
.await
.unwrap();

View File

@@ -1,26 +1,10 @@
use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
use js_sys::{Array, Uint8Array};
use wasm_bindgen::prelude::*;
use url::Url;
use pubky_common::capabilities::Capabilities;
use crate::error::Error;
use crate::PubkyClient;
mod http;
mod keys;
mod pkarr;
mod recovery_file;
mod session;
use keys::{Keypair, PublicKey};
use session::Session;
mod api;
mod internals;
mod wrappers;
impl Default for PubkyClient {
fn default() -> Self {
@@ -28,7 +12,6 @@ impl Default for PubkyClient {
}
}
static DEFAULT_RELAYS: [&str; 1] = ["https://relay.pkarr.org"];
static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/pkarr"];
#[wasm_bindgen]
@@ -37,8 +20,7 @@ impl PubkyClient {
pub fn new() -> Self {
Self {
http: reqwest::Client::builder().build().unwrap(),
session_cookies: Arc::new(RwLock::new(HashSet::new())),
pkarr_relays: DEFAULT_RELAYS.into_iter().map(|s| s.to_string()).collect(),
pkarr: pkarr::Client::builder().build().unwrap(),
}
}
@@ -48,203 +30,10 @@ impl PubkyClient {
pub fn testnet() -> Self {
Self {
http: reqwest::Client::builder().build().unwrap(),
session_cookies: Arc::new(RwLock::new(HashSet::new())),
pkarr_relays: TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect(),
pkarr: pkarr::Client::builder()
.relays(TESTNET_RELAYS.into_iter().map(|s| s.to_string()).collect())
.build()
.unwrap(),
}
}
/// Set Pkarr relays used for publishing and resolving Pkarr packets.
///
/// By default, [PubkyClient] will use `["https://relay.pkarr.org"]`
#[wasm_bindgen(js_name = "setPkarrRelays")]
pub fn set_pkarr_relays(mut self, relays: Vec<String>) -> Self {
self.pkarr_relays = relays;
self
}
// Read the set of pkarr relays used by this client.
#[wasm_bindgen(js_name = "getPkarrRelays")]
pub fn get_pkarr_relays(&self) -> Vec<String> {
self.pkarr_relays.clone()
}
/// Signup to a homeserver and update Pkarr accordingly.
///
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
#[wasm_bindgen]
pub async fn signup(
&self,
keypair: &Keypair,
homeserver: &PublicKey,
) -> Result<Session, JsValue> {
Ok(Session(
self.inner_signup(keypair.as_inner(), homeserver.as_inner())
.await
.map_err(JsValue::from)?,
))
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
/// or throws the recieved error if the response has any other `>=400` status code.
#[wasm_bindgen]
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>, JsValue> {
self.inner_session(pubky.as_inner())
.await
.map(|s| s.map(Session))
.map_err(|e| e.into())
}
/// Signout from a homeserver.
#[wasm_bindgen]
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
self.inner_signout(pubky.as_inner())
.await
.map_err(|e| e.into())
}
/// Signin to a homeserver using the root Keypair.
#[wasm_bindgen]
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
self.inner_signin(keypair.as_inner())
.await
.map(|_| ())
.map_err(|e| e.into())
}
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
/// verifying that AuthToken, and if capabilities were requested, signing in to
/// the Pubky's homeserver and returning the [Session] information.
///
/// Returns a tuple of [pubkyAuthUrl, Promise<Session>]
#[wasm_bindgen(js_name = "authRequest")]
pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result<js_sys::Array, JsValue> {
let mut relay: Url = relay
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
let (pubkyauth_url, client_secret) = self.create_auth_request(
&mut relay,
&Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?,
)?;
let this = self.clone();
let future = async move {
this.subscribe_to_auth_response(relay, &client_secret)
.await
.map(|pubky| JsValue::from(PublicKey(pubky)))
.map_err(|err| JsValue::from_str(&format!("{:?}", err)))
};
let promise = wasm_bindgen_futures::future_to_promise(future);
// Return the URL and the promise
let js_tuple = js_sys::Array::new();
js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref()));
js_tuple.push(&promise);
Ok(js_tuple)
}
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
/// source of the pubkyauth request url.
#[wasm_bindgen(js_name = "sendAuthToken")]
pub async fn send_auth_token(
&self,
keypair: &Keypair,
pubkyauth_url: &str,
) -> Result<(), JsValue> {
let pubkyauth_url: Url = pubkyauth_url
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url)
.await?;
Ok(())
}
// === Public data ===
#[wasm_bindgen]
/// Upload a small payload to a given path.
pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> {
self.inner_put(url, content).await.map_err(|e| e.into())
}
/// Download a small payload from a given path relative to a pubky author.
#[wasm_bindgen]
pub async fn get(&self, url: &str) -> Result<Option<Uint8Array>, JsValue> {
self.inner_get(url)
.await
.map(|b| b.map(|b| (&*b).into()))
.map_err(|e| e.into())
}
/// Delete a file at a path relative to a pubky author.
#[wasm_bindgen]
pub async fn delete(&self, url: &str) -> Result<(), JsValue> {
self.inner_delete(url).await.map_err(|e| e.into())
}
/// Returns a list of Pubky urls (as strings).
///
/// - `url`: The Pubky url (string) to the directory you want to list its content.
/// - `cursor`: Either a full `pubky://` Url (from previous list response),
/// or a path (to a file or directory) relative to the `url`
/// - `reverse`: List in reverse order
/// - `limit` Limit the number of urls in the response
/// - `shallow`: List directories and files, instead of flat list of files.
#[wasm_bindgen]
pub async fn list(
&self,
url: &str,
cursor: Option<String>,
reverse: Option<bool>,
limit: Option<u16>,
shallow: Option<bool>,
) -> Result<Array, JsValue> {
// TODO: try later to return Vec<String> from async function.
if let Some(cursor) = cursor {
return self
.inner_list(url)?
.reverse(reverse.unwrap_or(false))
.limit(limit.unwrap_or(u16::MAX))
.cursor(&cursor)
.shallow(shallow.unwrap_or(false))
.send()
.await
.map(|urls| {
let js_array = Array::new();
for url in urls {
js_array.push(&JsValue::from_str(&url));
}
js_array
})
.map_err(|e| e.into());
}
self.inner_list(url)?
.reverse(reverse.unwrap_or(false))
.limit(limit.unwrap_or(u16::MAX))
.shallow(shallow.unwrap_or(false))
.send()
.await
.map(|urls| {
let js_array = Array::new();
for url in urls {
js_array.push(&JsValue::from_str(&url));
}
js_array
})
.map_err(|e| e.into())
}
}

113
pubky/src/wasm/api/auth.rs Normal file
View File

@@ -0,0 +1,113 @@
use url::Url;
use pubky_common::capabilities::Capabilities;
use crate::Error;
use crate::PubkyClient;
use crate::wasm::wrappers::keys::{Keypair, PublicKey};
use crate::wasm::wrappers::session::Session;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
impl PubkyClient {
/// Signup to a homeserver and update Pkarr accordingly.
///
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
#[wasm_bindgen]
pub async fn signup(
&self,
keypair: &Keypair,
homeserver: &PublicKey,
) -> Result<Session, JsValue> {
Ok(Session(
self.inner_signup(keypair.as_inner(), homeserver.as_inner())
.await
.map_err(JsValue::from)?,
))
}
/// Check the current sesison for a given Pubky in its homeserver.
///
/// Returns [Session] or `None` (if recieved `404 NOT_FOUND`),
/// or throws the recieved error if the response has any other `>=400` status code.
#[wasm_bindgen]
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>, JsValue> {
self.inner_session(pubky.as_inner())
.await
.map(|s| s.map(Session))
.map_err(|e| e.into())
}
/// Signout from a homeserver.
#[wasm_bindgen]
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
self.inner_signout(pubky.as_inner())
.await
.map_err(|e| e.into())
}
/// Signin to a homeserver using the root Keypair.
#[wasm_bindgen]
pub async fn signin(&self, keypair: &Keypair) -> Result<(), JsValue> {
self.inner_signin(keypair.as_inner())
.await
.map(|_| ())
.map_err(|e| e.into())
}
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
/// verifying that AuthToken, and if capabilities were requested, signing in to
/// the Pubky's homeserver and returning the [Session] information.
///
/// Returns a tuple of [pubkyAuthUrl, Promise<Session>]
#[wasm_bindgen(js_name = "authRequest")]
pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result<js_sys::Array, JsValue> {
let mut relay: Url = relay
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
let (pubkyauth_url, client_secret) = self.create_auth_request(
&mut relay,
&Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?,
)?;
let this = self.clone();
let future = async move {
this.subscribe_to_auth_response(relay, &client_secret)
.await
.map(|pubky| JsValue::from(PublicKey(pubky)))
.map_err(|err| JsValue::from_str(&format!("{:?}", err)))
};
let promise = wasm_bindgen_futures::future_to_promise(future);
// Return the URL and the promise
let js_tuple = js_sys::Array::new();
js_tuple.push(&JsValue::from_str(pubkyauth_url.as_ref()));
js_tuple.push(&promise);
Ok(js_tuple)
}
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
/// source of the pubkyauth request url.
#[wasm_bindgen(js_name = "sendAuthToken")]
pub async fn send_auth_token(
&self,
keypair: &Keypair,
pubkyauth_url: &str,
) -> Result<(), JsValue> {
let pubkyauth_url: Url = pubkyauth_url
.try_into()
.map_err(|_| Error::Generic("Invalid relay Url".into()))?;
self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,56 @@
//! Fetch method handling HTTP and Pubky urls with Pkarr TLD.
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use reqwest::Url;
use crate::PubkyClient;
use super::super::internals::resolve;
#[wasm_bindgen]
impl PubkyClient {
#[wasm_bindgen]
pub async fn fetch(
&self,
url: &str,
init: &web_sys::RequestInit,
) -> Result<js_sys::Promise, JsValue> {
let mut url: Url = url.try_into().map_err(|err| {
JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `url`; {:?}", err))
})?;
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| {
JsValue::from_str(&format!("PubkyClient::fetch(): Invalid `init`; {:?}", err))
})?;
Ok(js_fetch(&js_req))
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = fetch)]
fn fetch_with_request(input: &web_sys::Request) -> Promise;
}
fn js_fetch(req: &web_sys::Request) -> Promise {
use wasm_bindgen::{JsCast, JsValue};
let global = js_sys::global();
if let Ok(true) = js_sys::Reflect::has(&global, &JsValue::from_str("ServiceWorkerGlobalScope"))
{
global
.unchecked_into::<web_sys::ServiceWorkerGlobalScope>()
.fetch_with_request(req)
} else {
// browser
fetch_with_request(req)
}
}

View File

@@ -0,0 +1,6 @@
pub mod http;
pub mod recovery_file;
// TODO: put the Homeserver API behind a feature flag
pub mod auth;
pub mod public;

View File

@@ -0,0 +1,87 @@
use wasm_bindgen::prelude::*;
use js_sys::{Array, Uint8Array};
use crate::PubkyClient;
#[wasm_bindgen]
impl PubkyClient {
#[wasm_bindgen]
/// Upload a small payload to a given path.
pub async fn put(&self, url: &str, content: &[u8]) -> Result<(), JsValue> {
self.inner_put(url, content).await.map_err(|e| e.into())
}
/// Download a small payload from a given path relative to a pubky author.
#[wasm_bindgen]
pub async fn get(&self, url: &str) -> Result<Option<Uint8Array>, JsValue> {
self.inner_get(url)
.await
.map(|b| b.map(|b| (&*b).into()))
.map_err(|e| e.into())
}
/// Delete a file at a path relative to a pubky author.
#[wasm_bindgen]
pub async fn delete(&self, url: &str) -> Result<(), JsValue> {
self.inner_delete(url).await.map_err(|e| e.into())
}
/// Returns a list of Pubky urls (as strings).
///
/// - `url`: The Pubky url (string) to the directory you want to list its content.
/// - `cursor`: Either a full `pubky://` Url (from previous list response),
/// or a path (to a file or directory) relative to the `url`
/// - `reverse`: List in reverse order
/// - `limit` Limit the number of urls in the response
/// - `shallow`: List directories and files, instead of flat list of files.
#[wasm_bindgen]
pub async fn list(
&self,
url: &str,
cursor: Option<String>,
reverse: Option<bool>,
limit: Option<u16>,
shallow: Option<bool>,
) -> Result<Array, JsValue> {
// TODO: try later to return Vec<String> from async function.
if let Some(cursor) = cursor {
return self
.inner_list(url)?
.reverse(reverse.unwrap_or(false))
.limit(limit.unwrap_or(u16::MAX))
.cursor(&cursor)
.shallow(shallow.unwrap_or(false))
.send()
.await
.map(|urls| {
let js_array = Array::new();
for url in urls {
js_array.push(&JsValue::from_str(&url));
}
js_array
})
.map_err(|e| e.into());
}
self.inner_list(url)?
.reverse(reverse.unwrap_or(false))
.limit(limit.unwrap_or(u16::MAX))
.shallow(shallow.unwrap_or(false))
.send()
.await
.map(|urls| {
let js_array = Array::new();
for url in urls {
js_array.push(&JsValue::from_str(&url));
}
js_array
})
.map_err(|e| e.into())
}
}

View File

@@ -3,7 +3,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
use crate::error::Error;
use super::keys::Keypair;
use crate::wasm::wrappers::keys::Keypair;
/// Create a recovery file of the `keypair`, containing the secret key encrypted
/// using the `passphrase`.

View File

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

View File

@@ -0,0 +1,33 @@
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 {
/// 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()
}
}

View File

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

View File

@@ -0,0 +1,5 @@
//! Wasm wrappers around structs that we need to be turned into Classes
//! in JavaScript.
pub mod keys;
pub mod session;