mirror of
https://github.com/aljazceru/pubky-core.git
synced 2025-12-31 04:44:37 +01:00
feat(js): export AuthRequest in js as a wrapper around native
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2194,9 +2194,11 @@ dependencies = [
|
||||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"console_log",
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"flume",
|
||||
"futures-lite",
|
||||
"futures-util",
|
||||
"http-relay",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use pubky::Client;
|
||||
use pubky::{Client, PublicKey};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use pubky_common::crypto::PublicKey;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Cli {
|
||||
@@ -29,7 +27,7 @@ async fn main() -> Result<()> {
|
||||
println!("Enter your recovery_file's passphrase to signup:");
|
||||
let passphrase = rpassword::read_password()?;
|
||||
|
||||
let keypair = pubky_common::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?;
|
||||
let keypair = pubky::recovery_file::decrypt_recovery_file(&recovery_file, &passphrase)?;
|
||||
|
||||
println!("Successfully decrypted the recovery file, signing up to the homeserver:");
|
||||
|
||||
|
||||
8
pubky/build.rs
Normal file
8
pubky/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use cfg_aliases::cfg_aliases;
|
||||
|
||||
fn main() {
|
||||
// Convenience aliases
|
||||
cfg_aliases! {
|
||||
wasm_browser: { all(target_family = "wasm", target_os = "unknown") },
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
allow-unwrap-in-tests = true
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'tape'
|
||||
|
||||
import { Client, Keypair, PublicKey } from '../index.cjs'
|
||||
import { Client, Keypair, PublicKey, setLogLevel } from '../index.cjs'
|
||||
|
||||
const HOMESERVER_PUBLICKEY = PublicKey.from('8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo')
|
||||
const TESTNET_HTTP_RELAY = "http://localhost:15412/link";
|
||||
@@ -65,9 +65,12 @@ test("Auth: 3rd party signin", async (t) => {
|
||||
// Third party app side
|
||||
let capabilities = "/pub/pubky.app/:rw,/pub/foo.bar/file:r";
|
||||
let client = Client.testnet();
|
||||
let [pubkyauth_url, pubkyauthResponse] = client
|
||||
let authRequest = client
|
||||
.authRequest(TESTNET_HTTP_RELAY, capabilities);
|
||||
|
||||
let pubkyauthUrl = authRequest.url();
|
||||
let pubkyauthResponse = authRequest.response();
|
||||
|
||||
if (globalThis.document) {
|
||||
// Skip `sendAuthToken` in browser
|
||||
// TODO: figure out why does it fail in browser unit tests
|
||||
@@ -81,7 +84,7 @@ test("Auth: 3rd party signin", async (t) => {
|
||||
|
||||
await client.signup(keypair, HOMESERVER_PUBLICKEY);
|
||||
|
||||
await client.sendAuthToken(keypair, pubkyauth_url)
|
||||
await client.sendAuthToken(keypair, pubkyauthUrl)
|
||||
}
|
||||
|
||||
let authedPubky = await pubkyauthResponse;
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
//!
|
||||
|
||||
mod shared;
|
||||
// TODO: deny missing docs.
|
||||
// #![deny(missing_docs)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
// TODO: deny unwrap
|
||||
#![cfg_attr(any(), deny(clippy::unwrap_used))]
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
macro_rules! cross_debug {
|
||||
($($arg:tt)*) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
log::debug!($($arg)*);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
tracing::debug!($($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod native;
|
||||
#[cfg(wasm_browser)]
|
||||
mod wasm;
|
||||
|
||||
use std::fmt::Debug;
|
||||
#[cfg(not(wasm_browser))]
|
||||
pub use crate::native::Client;
|
||||
pub use crate::native::{api::auth::AuthRequest, api::public::ListBuilder, ClientBuilder};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
#[cfg(wasm_browser)]
|
||||
pub use native::Client as NativeClient;
|
||||
#[cfg(wasm_browser)]
|
||||
pub use wasm::Client;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use crate::shared::list_builder::ListBuilder;
|
||||
|
||||
/// A client for Pubky homeserver API, as well as generic HTTP requests to Pubky urls.
|
||||
#[derive(Clone)]
|
||||
#[wasm_bindgen]
|
||||
pub struct Client {
|
||||
http: reqwest::Client,
|
||||
pkarr: pkarr::Client,
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
cookie_store: std::sync::Arc<native::CookieJar>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
icann_http: reqwest::Client,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
testnet: bool,
|
||||
}
|
||||
|
||||
impl Debug for Client {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Pubky Client").finish()
|
||||
}
|
||||
}
|
||||
// Re-exports
|
||||
pub use pkarr::{Keypair, PublicKey};
|
||||
pub use pubky_common::recovery_file;
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
pub mod internal {
|
||||
#[cfg(not(wasm_browser))]
|
||||
pub mod cookies;
|
||||
pub mod pkarr;
|
||||
}
|
||||
pub mod api {
|
||||
pub mod auth;
|
||||
#[cfg(not(wasm_browser))]
|
||||
pub mod http;
|
||||
pub mod public;
|
||||
}
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
use mainline::Testnet;
|
||||
|
||||
use crate::Client;
|
||||
static DEFAULT_USER_AGENT: &str = concat!("pubky.org", "@", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
mod api;
|
||||
mod cookies;
|
||||
mod http;
|
||||
#[macro_export]
|
||||
macro_rules! handle_http_error {
|
||||
($res:expr) => {
|
||||
if let Err(status) = $res.error_for_status_ref() {
|
||||
return match $res.text().await {
|
||||
Ok(text) => Err(anyhow::anyhow!("{status}. Error message: {text}")),
|
||||
_ => Err(anyhow::anyhow!("{status}")),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use cookies::CookieJar;
|
||||
|
||||
static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ClientBuilder {
|
||||
pkarr: pkarr::ClientBuilder,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
#[cfg(not(wasm_browser))]
|
||||
/// Sets the following:
|
||||
/// - Pkarr client's DHT bootstrap nodes = `testnet` bootstrap nodes.
|
||||
/// - Pkarr client's resolvers = `testnet` bootstrap nodes.
|
||||
/// - Pkarr client's DHT request timeout = 500 milliseconds. (unless in CI, then it is left as default 2000)
|
||||
pub fn testnet(mut self, testnet: &Testnet) -> Self {
|
||||
pub fn testnet(&mut self, testnet: &Testnet) -> &mut Self {
|
||||
let bootstrap = testnet.bootstrap.clone();
|
||||
|
||||
self.pkarr.bootstrap(&bootstrap);
|
||||
@@ -34,15 +55,27 @@ impl ClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a [mainline::DhtBuilder] if `None`, and allows mutating it with a callback function.
|
||||
pub fn pkarr<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: FnOnce(&mut pkarr::ClientBuilder) -> &mut pkarr::ClientBuilder,
|
||||
{
|
||||
f(&mut self.pkarr);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Build [Client]
|
||||
pub fn build(self) -> Result<Client, BuildError> {
|
||||
pub fn build(&self) -> Result<Client, BuildError> {
|
||||
let pkarr = self.pkarr.build()?;
|
||||
|
||||
let cookie_store = Arc::new(CookieJar::default());
|
||||
#[cfg(not(wasm_browser))]
|
||||
let cookie_store = Arc::new(internal::cookies::CookieJar::default());
|
||||
|
||||
// TODO: allow custom user agent, but force a Pubky user agent information
|
||||
let user_agent = DEFAULT_USER_AGENT;
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
let http = reqwest::ClientBuilder::from(pkarr.clone())
|
||||
// TODO: use persistent cookie jar
|
||||
.cookie_provider(cookie_store.clone())
|
||||
@@ -50,27 +83,61 @@ impl ClientBuilder {
|
||||
.build()
|
||||
.expect("config expected to not error");
|
||||
|
||||
let icann_http = reqwest::ClientBuilder::new()
|
||||
.cookie_provider(cookie_store.clone())
|
||||
#[cfg(wasm_browser)]
|
||||
let http = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
.build()
|
||||
.expect("config expected to not error");
|
||||
|
||||
Ok(Client {
|
||||
cookie_store,
|
||||
http,
|
||||
icann_http,
|
||||
pkarr,
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
icann_http: reqwest::Client::builder()
|
||||
// TODO: use persistent cookie jar
|
||||
.cookie_provider(cookie_store.clone())
|
||||
.user_agent(user_agent)
|
||||
.build()
|
||||
.expect("config expected to not error"),
|
||||
#[cfg(not(wasm_browser))]
|
||||
cookie_store,
|
||||
|
||||
#[cfg(wasm_browser)]
|
||||
testnet: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BuildError {
|
||||
#[error(transparent)]
|
||||
/// Error building Pkarr client.
|
||||
PkarrBuildError(#[from] pkarr::errors::BuildError),
|
||||
}
|
||||
|
||||
/// A client for Pubky homeserver API, as well as generic HTTP requests to Pubky urls.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
pub(crate) http: reqwest::Client,
|
||||
pub(crate) pkarr: pkarr::Client,
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
pub(crate) cookie_store: std::sync::Arc<internal::cookies::CookieJar>,
|
||||
#[cfg(not(wasm_browser))]
|
||||
pub(crate) icann_http: reqwest::Client,
|
||||
|
||||
#[cfg(wasm_browser)]
|
||||
pub(crate) testnet: bool,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Returns a builder to edit settings before creating [Client].
|
||||
pub fn builder() -> ClientBuilder {
|
||||
ClientBuilder::default()
|
||||
}
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
/// Create a client connected to the local network
|
||||
/// with the bootstrapping node: `localhost:6881`
|
||||
pub fn testnet() -> Result<Self, BuildError> {
|
||||
@@ -83,15 +150,9 @@ impl Client {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(wasm_browser))]
|
||||
/// Alias to `pubky::Client::builder().testnet(testnet).build().unwrap()`
|
||||
pub(crate) fn test(testnet: &Testnet) -> Client {
|
||||
Client::builder().testnet(testnet).build().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BuildError {
|
||||
#[error(transparent)]
|
||||
/// Error building Pkarr client.
|
||||
PkarrBuildError(#[from] pkarr::errors::BuildError),
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::session::Session;
|
||||
use reqwest::IntoUrl;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine};
|
||||
use reqwest::{IntoUrl, Method, StatusCode};
|
||||
use url::Url;
|
||||
|
||||
use pkarr::PublicKey;
|
||||
|
||||
use pubky_common::capabilities::Capabilities;
|
||||
use pkarr::{Keypair, PublicKey};
|
||||
use pubky_common::{
|
||||
auth::AuthToken,
|
||||
capabilities::{Capabilities, Capability},
|
||||
crypto::{decrypt, encrypt, hash, random_bytes},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::Client;
|
||||
use crate::handle_http_error;
|
||||
|
||||
use super::super::Client;
|
||||
|
||||
impl Client {
|
||||
/// Signup to a homeserver and update Pkarr accordingly.
|
||||
@@ -17,25 +24,179 @@ impl Client {
|
||||
/// 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
|
||||
let response = self
|
||||
.cross_request(Method::POST, format!("https://{}/signup", homeserver))
|
||||
.await
|
||||
.body(AuthToken::sign(keypair, vec![Capability::root()]).serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
self.publish_homeserver(keypair, &homeserver.to_string())
|
||||
.await?;
|
||||
|
||||
// Store the cookie to the correct URL.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.cookie_store
|
||||
.store_session_after_signup(&response, &keypair.public_key());
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Session::deserialize(&bytes)?)
|
||||
}
|
||||
|
||||
/// Check the current sessison for a given Pubky in its homeserver.
|
||||
/// Check the current session for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns [Session] or `None` (if received `404 NOT_FOUND`),
|
||||
/// or [reqwest::Error] if the response has any other `>=400` status code.
|
||||
/// Returns None if not signed in, or [reqwest::Error]
|
||||
/// if the response has any other `>=404` status code.
|
||||
pub async fn session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
|
||||
self.inner_session(pubky).await
|
||||
let response = self
|
||||
.cross_request(Method::GET, format!("pubky://{}/session", pubky))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Some(Session::deserialize(&bytes)?))
|
||||
}
|
||||
|
||||
/// Signout from a homeserver.
|
||||
pub async fn signout(&self, pubky: &PublicKey) -> Result<()> {
|
||||
self.inner_signout(pubky).await
|
||||
let response = self
|
||||
.cross_request(Method::DELETE, format!("pubky://{}/session", pubky))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.cookie_store.delete_session_after_signout(pubky);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Signin to a homeserver.
|
||||
pub async fn signin(&self, keypair: &Keypair) -> Result<Session> {
|
||||
self.inner_signin(keypair).await
|
||||
let token = AuthToken::sign(keypair, vec![Capability::root()]);
|
||||
|
||||
self.signin_with_authtoken(&token).await
|
||||
}
|
||||
|
||||
pub async fn send_auth_token<T: IntoUrl>(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: &T,
|
||||
) -> Result<()> {
|
||||
let pubkyauth_url = Url::parse(
|
||||
pubkyauth_url
|
||||
.as_str()
|
||||
.replace("pubkyauth_url", "http")
|
||||
.as_str(),
|
||||
)?;
|
||||
|
||||
let query_params: HashMap<String, String> =
|
||||
pubkyauth_url.query_pairs().into_owned().collect();
|
||||
|
||||
let relay = query_params
|
||||
.get("relay")
|
||||
.map(|r| url::Url::parse(r).expect("Relay query param to be valid URL"))
|
||||
.expect("Missing relay query param");
|
||||
|
||||
let client_secret = query_params
|
||||
.get("secret")
|
||||
.map(|s| {
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
let bytes = engine.decode(s).expect("invalid client_secret");
|
||||
let arr: [u8; 32] = bytes.try_into().expect("invalid client_secret");
|
||||
|
||||
arr
|
||||
})
|
||||
.expect("Missing client secret");
|
||||
|
||||
let capabilities = query_params
|
||||
.get("caps")
|
||||
.map(|caps_string| {
|
||||
caps_string
|
||||
.split(',')
|
||||
.filter_map(|cap| Capability::try_from(cap).ok())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let token = AuthToken::sign(keypair, capabilities);
|
||||
|
||||
let encrypted_token = encrypt(&token.serialize(), &client_secret)?;
|
||||
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
|
||||
let mut callback_url = relay.clone();
|
||||
let mut path_segments = callback_url.path_segments_mut().unwrap();
|
||||
path_segments.pop_if_empty();
|
||||
let channel_id = engine.encode(hash(&client_secret).as_bytes());
|
||||
path_segments.push(&channel_id);
|
||||
drop(path_segments);
|
||||
|
||||
let response = self
|
||||
.cross_request(Method::POST, callback_url)
|
||||
.await
|
||||
.body(encrypted_token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn signin_with_authtoken(&self, token: &AuthToken) -> Result<Session> {
|
||||
let response = self
|
||||
.cross_request(Method::POST, format!("pubky://{}/session", token.pubky()))
|
||||
.await
|
||||
.body(token.serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Session::deserialize(&bytes)?)
|
||||
}
|
||||
|
||||
pub(crate) fn create_auth_request(
|
||||
&self,
|
||||
relay: &mut Url,
|
||||
capabilities: &Capabilities,
|
||||
) -> Result<(Url, [u8; 32])> {
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
|
||||
let client_secret: [u8; 32] = random_bytes::<32>();
|
||||
|
||||
let pubkyauth_url = Url::parse(&format!(
|
||||
"pubkyauth:///?caps={capabilities}&secret={}&relay={relay}",
|
||||
engine.encode(client_secret)
|
||||
))?;
|
||||
|
||||
let mut segments = relay
|
||||
.path_segments_mut()
|
||||
.map_err(|_| anyhow::anyhow!("Invalid relay"))?;
|
||||
|
||||
// remove trailing slash if any.
|
||||
segments.pop_if_empty();
|
||||
let channel_id = &engine.encode(hash(&client_secret).as_bytes());
|
||||
segments.push(channel_id);
|
||||
drop(segments);
|
||||
|
||||
Ok((pubkyauth_url, client_secret))
|
||||
}
|
||||
|
||||
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
|
||||
@@ -55,24 +216,59 @@ impl Client {
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let future = async move {
|
||||
let result = this
|
||||
.subscribe_to_auth_response(relay, &client_secret, tx.clone())
|
||||
.await;
|
||||
tx.send(result)
|
||||
});
|
||||
let _ = tx.send(result);
|
||||
};
|
||||
|
||||
#[cfg(not(wasm_browser))]
|
||||
tokio::spawn(future);
|
||||
#[cfg(wasm_browser)]
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
|
||||
Ok(AuthRequest { 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: IntoUrl>(
|
||||
pub(crate) async fn subscribe_to_auth_response(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: &T,
|
||||
) -> Result<()> {
|
||||
self.inner_send_auth_token(keypair, pubkyauth_url).await
|
||||
relay: Url,
|
||||
client_secret: &[u8; 32],
|
||||
tx: flume::Sender<Result<PublicKey>>,
|
||||
) -> anyhow::Result<PublicKey> {
|
||||
let response = loop {
|
||||
match self
|
||||
.cross_request(Method::GET, relay.clone())
|
||||
.await
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
break Ok(response);
|
||||
}
|
||||
Err(error) => {
|
||||
if error.is_timeout() && !tx.is_disconnected() {
|
||||
cross_debug!("Connection to HttpRelay timedout, reconnecting...");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break Err(error);
|
||||
}
|
||||
}
|
||||
}?;
|
||||
cross_debug!("LOOPING xxx {:?}", &response);
|
||||
|
||||
let encrypted_token = response.bytes().await?;
|
||||
let token_bytes = decrypt(&encrypted_token, client_secret)
|
||||
.map_err(|e| anyhow::anyhow!("Got invalid token: {e}"))?;
|
||||
let token = AuthToken::verify(&token_bytes)?;
|
||||
|
||||
if !token.capabilities().is_empty() {
|
||||
self.signin_with_authtoken(&token).await?;
|
||||
}
|
||||
|
||||
Ok(token.pubky().clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,3 +290,167 @@ impl AuthRequest {
|
||||
.expect("sender dropped unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
use http_relay::HttpRelay;
|
||||
use mainline::Testnet;
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::capabilities::{Capabilities, Capability};
|
||||
use pubky_homeserver::Homeserver;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_authn() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
client.signout(&keypair.public_key()).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client.session(&keypair.public_key()).await.unwrap();
|
||||
|
||||
assert!(session.is_none());
|
||||
}
|
||||
|
||||
client.signin(&keypair).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let http_relay = HttpRelay::builder().build().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities =
|
||||
"/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let pubky_auth_request = client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
client
|
||||
.send_auth_token(&keypair, pubky_auth_request.url())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let public_key = pubky_auth_request.response().await.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_users() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let first_keypair = Keypair::random();
|
||||
let second_keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&first_keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.signup(&second_keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&first_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &first_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
let session = client
|
||||
.session(&second_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &second_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
use pkarr::PublicKey;
|
||||
use reqwest::{IntoUrl, Method, RequestBuilder};
|
||||
|
||||
use crate::Client;
|
||||
use super::super::Client;
|
||||
|
||||
impl Client {
|
||||
#[cfg(not(wasm_browser))]
|
||||
/// Start building a `Request` with the `Method` and `Url`.
|
||||
///
|
||||
/// Returns a `RequestBuilder`, which will allow setting headers and
|
||||
@@ -29,6 +30,8 @@ impl Client {
|
||||
|
||||
return self.http.request(method, url);
|
||||
} else if url.starts_with("https://") && PublicKey::try_from(url).is_err() {
|
||||
// TODO: remove icann_http when we can control reqwest connection
|
||||
// and or create a tls config per connection.
|
||||
return self.icann_http.request(method, url);
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ impl Client {
|
||||
|
||||
// === Private Methods ===
|
||||
|
||||
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
|
||||
pub(crate) async fn cross_request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
|
||||
self.request(method, url)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
pub mod auth;
|
||||
pub mod public;
|
||||
@@ -1,14 +1,897 @@
|
||||
use reqwest::IntoUrl;
|
||||
use reqwest::{IntoUrl, Method};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{shared::list_builder::ListBuilder, Client};
|
||||
use crate::handle_http_error;
|
||||
|
||||
use super::super::Client;
|
||||
|
||||
impl Client {
|
||||
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
|
||||
///
|
||||
/// `url` sets the path you want to lest within.
|
||||
pub fn list<T: IntoUrl>(&self, url: T) -> Result<ListBuilder> {
|
||||
self.inner_list(url)
|
||||
Ok(ListBuilder::new(self, url))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper struct to edit Pubky homeserver's list API options before sending them.
|
||||
#[derive(Debug)]
|
||||
pub struct ListBuilder<'a> {
|
||||
url: String,
|
||||
reverse: bool,
|
||||
limit: Option<u16>,
|
||||
cursor: Option<&'a str>,
|
||||
client: &'a Client,
|
||||
shallow: bool,
|
||||
}
|
||||
|
||||
impl<'a> ListBuilder<'a> {
|
||||
/// Create a new List request builder
|
||||
pub(crate) fn new<T: IntoUrl>(client: &'a Client, url: T) -> Self {
|
||||
Self {
|
||||
client,
|
||||
url: url.as_str().to_string(),
|
||||
limit: None,
|
||||
cursor: None,
|
||||
reverse: false,
|
||||
shallow: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `reverse` option.
|
||||
pub fn reverse(mut self, reverse: bool) -> Self {
|
||||
self.reverse = reverse;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `limit` value.
|
||||
pub fn limit(mut self, limit: u16) -> Self {
|
||||
self.limit = limit.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `cursor` value.
|
||||
///
|
||||
/// Either a full `pubky://` Url (from previous list response),
|
||||
/// or a path (to a file or directory) relative to the `url`
|
||||
pub fn cursor(mut self, cursor: &'a str) -> Self {
|
||||
self.cursor = cursor.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shallow(mut self, shallow: bool) -> Self {
|
||||
self.shallow = shallow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Send the list request.
|
||||
///
|
||||
/// Returns a list of Pubky URLs of the files in the path of the `url`
|
||||
/// respecting [ListBuilder::reverse], [ListBuilder::limit] and [ListBuilder::cursor]
|
||||
/// options.
|
||||
pub async fn send(self) -> Result<Vec<String>> {
|
||||
let mut url = url::Url::parse(&self.url)?;
|
||||
|
||||
if !url.path().ends_with('/') {
|
||||
let path = url.path().to_string();
|
||||
let mut parts = path.split('/').collect::<Vec<&str>>();
|
||||
parts.pop();
|
||||
|
||||
let path = format!("{}/", parts.join("/"));
|
||||
|
||||
url.set_path(&path)
|
||||
}
|
||||
|
||||
let mut query = url.query_pairs_mut();
|
||||
|
||||
if self.reverse {
|
||||
query.append_key_only("reverse");
|
||||
}
|
||||
|
||||
if self.shallow {
|
||||
query.append_key_only("shallow");
|
||||
}
|
||||
|
||||
if let Some(limit) = self.limit {
|
||||
query.append_pair("limit", &limit.to_string());
|
||||
}
|
||||
|
||||
if let Some(cursor) = self.cursor {
|
||||
query.append_pair("cursor", cursor);
|
||||
}
|
||||
|
||||
drop(query);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.cross_request(Method::GET, url)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
// TODO: bail on too large files.
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&bytes)
|
||||
.lines()
|
||||
.map(String::from)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
use bytes::Bytes;
|
||||
use mainline::Testnet;
|
||||
use pkarr::Keypair;
|
||||
use pubky_homeserver::Homeserver;
|
||||
use reqwest::{Method, StatusCode};
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_get_delete() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
|
||||
client
|
||||
.delete(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unauthorized_put_delete() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let public_key = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{public_key}/pub/foo.txt");
|
||||
let url = url.as_str();
|
||||
|
||||
let other_client = Client::test(&testnet);
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client.delete(url).send().await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/z.wrong/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/example.com/extra");
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().limit(2).send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
],
|
||||
"normal list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("cc-nested/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor(&format!("pubky://{pubky}/pub/example.com/a.txt"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a full url cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a leading / cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
],
|
||||
"reverse list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
],
|
||||
"reverse list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("d.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"reverse list with limit and cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shallow() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/");
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
],
|
||||
"normal list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"normal list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.cursor("example.com/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
],
|
||||
"normal list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(3)
|
||||
.cursor("example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
],
|
||||
"normal list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
],
|
||||
"reverse list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
],
|
||||
"reverse list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("file2")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
],
|
||||
"reverse list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("example.con/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"reverse list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_events() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
client.delete(url).send().await.unwrap();
|
||||
}
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let cursor;
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.con"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con"),
|
||||
format!("PUT pubky://{pubky}/pub/file"),
|
||||
format!("DEL pubky://{pubky}/pub/file"),
|
||||
format!("PUT pubky://{pubky}/pub/file2"),
|
||||
format!("DEL pubky://{pubky}/pub/file2"),
|
||||
format!("PUT pubky://{pubky}/pub/z.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/z.com/a.txt"),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_after_event() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/a.com/a.txt");
|
||||
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.bytes().await.unwrap();
|
||||
|
||||
assert_eq!(body.as_ref(), &[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dont_delete_shared_blobs() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let homeserver_pubky = homeserver.public_key();
|
||||
|
||||
let user_1 = Keypair::random();
|
||||
let user_2 = Keypair::random();
|
||||
|
||||
client.signup(&user_1, &homeserver_pubky).await.unwrap();
|
||||
client.signup(&user_2, &homeserver_pubky).await.unwrap();
|
||||
|
||||
let user_1_id = user_1.public_key();
|
||||
let user_2_id = user_2.public_key();
|
||||
|
||||
let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1");
|
||||
let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1");
|
||||
|
||||
let file = vec![1];
|
||||
client.put(&url_1).body(file.clone()).send().await.unwrap();
|
||||
client.put(&url_2).body(file.clone()).send().await.unwrap();
|
||||
|
||||
// Delete file 1
|
||||
client
|
||||
.delete(url_1)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let blob = client
|
||||
.get(url_2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blob, file);
|
||||
|
||||
let feed_url = format!("https://{}/events/", homeserver.public_key());
|
||||
|
||||
let response = client
|
||||
.request(Method::GET, feed_url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",),
|
||||
format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream() {
|
||||
// TODO: test better streaming API
|
||||
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
let bytes = Bytes::from(vec![0; 1024 * 1024]);
|
||||
|
||||
client.put(url).body(bytes.clone()).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes);
|
||||
|
||||
client.delete(url).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
use pubky_common::{
|
||||
crypto::Keypair,
|
||||
recovery_file::{create_recovery_file, decrypt_recovery_file},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
/// 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)?)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::RwLock};
|
||||
use pkarr::PublicKey;
|
||||
use reqwest::{cookie::CookieStore, header::HeaderValue, Response};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct CookieJar {
|
||||
pubky_sessions: RwLock<HashMap<String, String>>,
|
||||
normal_jar: RwLock<cookie_store::CookieStore>,
|
||||
36
pubky/src/native/internal/pkarr.rs
Normal file
36
pubky/src/native/internal/pkarr.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use pkarr::{dns::rdata::SVCB, Keypair, SignedPacket};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::super::Client;
|
||||
|
||||
impl Client {
|
||||
/// Publish the HTTPS record for `_pubky.<public_key>`.
|
||||
pub(crate) async fn publish_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> {
|
||||
// TODO: Before making public, consider the effect on other records and other mirrors
|
||||
|
||||
let existing = self.pkarr.resolve_most_recent(&keypair.public_key()).await;
|
||||
|
||||
let mut signed_packet_builder = SignedPacket::builder();
|
||||
|
||||
if let Some(ref existing) = existing {
|
||||
for answer in existing.resource_records("_pubky") {
|
||||
if !answer.name.to_string().starts_with("_pubky") {
|
||||
signed_packet_builder = signed_packet_builder.record(answer.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let svcb = SVCB::new(0, host.try_into()?);
|
||||
|
||||
let signed_packet = SignedPacket::builder()
|
||||
.https("_pubky".try_into().unwrap(), svcb, 60 * 60)
|
||||
.sign(keypair)?;
|
||||
|
||||
self.pkarr
|
||||
.publish(&signed_packet, existing.map(|s| s.timestamp()))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine};
|
||||
use reqwest::{IntoUrl, Method, StatusCode};
|
||||
use url::Url;
|
||||
|
||||
use pkarr::{Keypair, PublicKey};
|
||||
use pubky_common::{
|
||||
auth::AuthToken,
|
||||
capabilities::{Capabilities, Capability},
|
||||
crypto::{decrypt, encrypt, hash, random_bytes},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{handle_http_error, Client};
|
||||
|
||||
impl Client {
|
||||
/// Signup to a homeserver and update Pkarr accordingly.
|
||||
///
|
||||
/// The homeserver is a Pkarr domain name, where the TLD is a Pkarr public key
|
||||
/// for example "pubky.o4dksfbqk85ogzdb5osziw6befigbuxmuxkuxq8434q89uj56uyy"
|
||||
pub(crate) async fn inner_signup(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
homeserver: &PublicKey,
|
||||
) -> Result<Session> {
|
||||
let response = self
|
||||
.inner_request(Method::POST, format!("https://{}/signup", homeserver))
|
||||
.await
|
||||
.body(AuthToken::sign(keypair, vec![Capability::root()]).serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
self.publish_homeserver(keypair, &homeserver.to_string())
|
||||
.await?;
|
||||
|
||||
// Store the cookie to the correct URL.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.cookie_store
|
||||
.store_session_after_signup(&response, &keypair.public_key());
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Session::deserialize(&bytes)?)
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
///
|
||||
/// Returns None if not signed in, or [reqwest::Error]
|
||||
/// if the response has any other `>=404` status code.
|
||||
pub(crate) async fn inner_session(&self, pubky: &PublicKey) -> Result<Option<Session>> {
|
||||
let response = self
|
||||
.inner_request(Method::GET, format!("pubky://{}/session", pubky))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Some(Session::deserialize(&bytes)?))
|
||||
}
|
||||
|
||||
/// Signout from a homeserver.
|
||||
pub(crate) async fn inner_signout(&self, pubky: &PublicKey) -> Result<()> {
|
||||
let response = self
|
||||
.inner_request(Method::DELETE, format!("pubky://{}/session", pubky))
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.cookie_store.delete_session_after_signout(pubky);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Signin to a homeserver.
|
||||
pub(crate) async fn inner_signin(&self, keypair: &Keypair) -> Result<Session> {
|
||||
let token = AuthToken::sign(keypair, vec![Capability::root()]);
|
||||
|
||||
self.signin_with_authtoken(&token).await
|
||||
}
|
||||
|
||||
pub(crate) async fn inner_send_auth_token<T: IntoUrl>(
|
||||
&self,
|
||||
keypair: &Keypair,
|
||||
pubkyauth_url: T,
|
||||
) -> Result<()> {
|
||||
let pubkyauth_url = Url::parse(
|
||||
pubkyauth_url
|
||||
.as_str()
|
||||
.replace("pubkyauth_url", "http")
|
||||
.as_str(),
|
||||
)?;
|
||||
|
||||
let query_params: HashMap<String, String> =
|
||||
pubkyauth_url.query_pairs().into_owned().collect();
|
||||
|
||||
let relay = query_params
|
||||
.get("relay")
|
||||
.map(|r| url::Url::parse(r).expect("Relay query param to be valid URL"))
|
||||
.expect("Missing relay query param");
|
||||
|
||||
let client_secret = query_params
|
||||
.get("secret")
|
||||
.map(|s| {
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
let bytes = engine.decode(s).expect("invalid client_secret");
|
||||
let arr: [u8; 32] = bytes.try_into().expect("invalid client_secret");
|
||||
|
||||
arr
|
||||
})
|
||||
.expect("Missing client secret");
|
||||
|
||||
let capabilities = query_params
|
||||
.get("caps")
|
||||
.map(|caps_string| {
|
||||
caps_string
|
||||
.split(',')
|
||||
.filter_map(|cap| Capability::try_from(cap).ok())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let token = AuthToken::sign(keypair, capabilities);
|
||||
|
||||
let encrypted_token = encrypt(&token.serialize(), &client_secret)?;
|
||||
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
|
||||
let mut callback_url = relay.clone();
|
||||
let mut path_segments = callback_url.path_segments_mut().unwrap();
|
||||
path_segments.pop_if_empty();
|
||||
let channel_id = engine.encode(hash(&client_secret).as_bytes());
|
||||
path_segments.push(&channel_id);
|
||||
drop(path_segments);
|
||||
|
||||
let response = self
|
||||
.inner_request(Method::POST, callback_url)
|
||||
.await
|
||||
.body(encrypted_token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn signin_with_authtoken(&self, token: &AuthToken) -> Result<Session> {
|
||||
let response = self
|
||||
.inner_request(Method::POST, format!("pubky://{}/session", token.pubky()))
|
||||
.await
|
||||
.body(token.serialize())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(Session::deserialize(&bytes)?)
|
||||
}
|
||||
|
||||
pub(crate) fn create_auth_request(
|
||||
&self,
|
||||
relay: &mut Url,
|
||||
capabilities: &Capabilities,
|
||||
) -> Result<(Url, [u8; 32])> {
|
||||
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
|
||||
|
||||
let client_secret: [u8; 32] = random_bytes::<32>();
|
||||
|
||||
let pubkyauth_url = Url::parse(&format!(
|
||||
"pubkyauth:///?caps={capabilities}&secret={}&relay={relay}",
|
||||
engine.encode(client_secret)
|
||||
))?;
|
||||
|
||||
let mut segments = relay
|
||||
.path_segments_mut()
|
||||
.map_err(|_| anyhow::anyhow!("Invalid relay"))?;
|
||||
|
||||
// remove trailing slash if any.
|
||||
segments.pop_if_empty();
|
||||
let channel_id = &engine.encode(hash(&client_secret).as_bytes());
|
||||
segments.push(channel_id);
|
||||
drop(segments);
|
||||
|
||||
Ok((pubkyauth_url, client_secret))
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribe_to_auth_response(
|
||||
&self,
|
||||
relay: Url,
|
||||
client_secret: &[u8; 32],
|
||||
) -> Result<PublicKey> {
|
||||
// TODO: use a clearnet client.
|
||||
let response = reqwest::get(relay).await?;
|
||||
let encrypted_token = response.bytes().await?;
|
||||
let token_bytes = decrypt(&encrypted_token, client_secret)
|
||||
.map_err(|e| anyhow::anyhow!("Got invalid token: {e}"))?;
|
||||
let token = AuthToken::verify(&token_bytes)?;
|
||||
|
||||
if !token.capabilities().is_empty() {
|
||||
self.signin_with_authtoken(&token).await?;
|
||||
}
|
||||
|
||||
Ok(token.pubky().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
use http_relay::HttpRelay;
|
||||
use mainline::Testnet;
|
||||
use pkarr::Keypair;
|
||||
use pubky_common::capabilities::{Capabilities, Capability};
|
||||
use pubky_homeserver::Homeserver;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_authn() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
client.signout(&keypair.public_key()).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client.session(&keypair.public_key()).await.unwrap();
|
||||
|
||||
assert!(session.is_none());
|
||||
}
|
||||
|
||||
client.signin(&keypair).await.unwrap();
|
||||
|
||||
{
|
||||
let session = client
|
||||
.session(&keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authz() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let http_relay = HttpRelay::builder().build().await.unwrap();
|
||||
let http_relay_url = http_relay.local_link_url();
|
||||
|
||||
let keypair = Keypair::random();
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
// Third party app side
|
||||
let capabilities: Capabilities =
|
||||
"/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let (pubkyauth_url, pubkyauth_response) =
|
||||
client.auth_request(http_relay_url, &capabilities).unwrap();
|
||||
|
||||
// Authenticator side
|
||||
{
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
client
|
||||
.send_auth_token(&keypair, pubkyauth_url)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let public_key = pubkyauth_response
|
||||
.await
|
||||
.expect("sender to not be dropped")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&public_key, &pubky);
|
||||
|
||||
let session = client.session(&pubky).await.unwrap().unwrap();
|
||||
assert_eq!(session.capabilities(), &capabilities.0);
|
||||
|
||||
// Test access control enforcement
|
||||
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app/foo"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/pubky.app"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client
|
||||
.put(format!("pubky://{pubky}/pub/foo.bar/file"))
|
||||
.body(vec![])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_users() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let first_keypair = Keypair::random();
|
||||
let second_keypair = Keypair::random();
|
||||
|
||||
client
|
||||
.signup(&first_keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.signup(&second_keypair, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = client
|
||||
.session(&first_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &first_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
|
||||
let session = client
|
||||
.session(&second_keypair.public_key())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.pubky(), &second_keypair.public_key());
|
||||
assert!(session.capabilities().contains(&Capability::root()));
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use reqwest::{IntoUrl, Method};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{handle_http_error, Client};
|
||||
|
||||
/// Helper struct to edit Pubky homeserver's list API options before sending them.
|
||||
#[derive(Debug)]
|
||||
pub struct ListBuilder<'a> {
|
||||
url: String,
|
||||
reverse: bool,
|
||||
limit: Option<u16>,
|
||||
cursor: Option<&'a str>,
|
||||
client: &'a Client,
|
||||
shallow: bool,
|
||||
}
|
||||
|
||||
impl<'a> ListBuilder<'a> {
|
||||
/// Create a new List request builder
|
||||
pub(crate) fn new<T: IntoUrl>(client: &'a Client, url: T) -> Self {
|
||||
Self {
|
||||
client,
|
||||
url: url.as_str().to_string(),
|
||||
limit: None,
|
||||
cursor: None,
|
||||
reverse: false,
|
||||
shallow: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `reverse` option.
|
||||
pub fn reverse(mut self, reverse: bool) -> Self {
|
||||
self.reverse = reverse;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `limit` value.
|
||||
pub fn limit(mut self, limit: u16) -> Self {
|
||||
self.limit = limit.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `cursor` value.
|
||||
///
|
||||
/// Either a full `pubky://` Url (from previous list response),
|
||||
/// or a path (to a file or directory) relative to the `url`
|
||||
pub fn cursor(mut self, cursor: &'a str) -> Self {
|
||||
self.cursor = cursor.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shallow(mut self, shallow: bool) -> Self {
|
||||
self.shallow = shallow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Send the list request.
|
||||
///
|
||||
/// Returns a list of Pubky URLs of the files in the path of the `url`
|
||||
/// respecting [ListBuilder::reverse], [ListBuilder::limit] and [ListBuilder::cursor]
|
||||
/// options.
|
||||
pub async fn send(self) -> Result<Vec<String>> {
|
||||
let mut url = url::Url::parse(&self.url)?;
|
||||
|
||||
if !url.path().ends_with('/') {
|
||||
let path = url.path().to_string();
|
||||
let mut parts = path.split('/').collect::<Vec<&str>>();
|
||||
parts.pop();
|
||||
|
||||
let path = format!("{}/", parts.join("/"));
|
||||
|
||||
url.set_path(&path)
|
||||
}
|
||||
|
||||
let mut query = url.query_pairs_mut();
|
||||
|
||||
if self.reverse {
|
||||
query.append_key_only("reverse");
|
||||
}
|
||||
|
||||
if self.shallow {
|
||||
query.append_key_only("shallow");
|
||||
}
|
||||
|
||||
if let Some(limit) = self.limit {
|
||||
query.append_pair("limit", &limit.to_string());
|
||||
}
|
||||
|
||||
if let Some(cursor) = self.cursor {
|
||||
query.append_pair("cursor", cursor);
|
||||
}
|
||||
|
||||
drop(query);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.inner_request(Method::GET, url)
|
||||
.await
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
handle_http_error!(response);
|
||||
|
||||
// TODO: bail on too large files.
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&bytes)
|
||||
.lines()
|
||||
.map(String::from)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pub mod auth;
|
||||
pub mod list_builder;
|
||||
pub mod pkarr;
|
||||
pub mod public;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! handle_http_error {
|
||||
($res:expr) => {
|
||||
if let Err(status) = $res.error_for_status_ref() {
|
||||
return match $res.text().await {
|
||||
Ok(text) => Err(anyhow::anyhow!("{status}. Error message: {text}")),
|
||||
_ => Err(anyhow::anyhow!("{status}")),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use pkarr::{dns::rdata::SVCB, Keypair, SignedPacket};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::Client;
|
||||
|
||||
impl Client {
|
||||
/// Publish the HTTPS record for `_pubky.<public_key>`.
|
||||
pub(crate) async fn publish_homeserver(&self, keypair: &Keypair, host: &str) -> Result<()> {
|
||||
// TODO: Before making public, consider the effect on other records and other mirrors
|
||||
|
||||
let existing = self.pkarr.resolve_most_recent(&keypair.public_key()).await;
|
||||
|
||||
let mut signed_packet_builder = SignedPacket::builder();
|
||||
|
||||
if let Some(ref existing) = existing {
|
||||
for answer in existing.resource_records("_pubky") {
|
||||
if !answer.name.to_string().starts_with("_pubky") {
|
||||
signed_packet_builder = signed_packet_builder.record(answer.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let svcb = SVCB::new(0, host.try_into()?);
|
||||
|
||||
let signed_packet = SignedPacket::builder()
|
||||
.https("_pubky".try_into().unwrap(), svcb, 60 * 60)
|
||||
.sign(keypair)?;
|
||||
|
||||
self.pkarr
|
||||
.publish(&signed_packet, existing.map(|s| s.timestamp()))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub(crate) resolve_icann_domain() {
|
||||
//
|
||||
// let original_url = url.as_str();
|
||||
// let mut url = Url::parse(original_url).expect("Invalid url in inner_request");
|
||||
//
|
||||
// if url.scheme() == "pubky" {
|
||||
// // TODO: use https for anything other than testnet
|
||||
// url.set_scheme("http")
|
||||
// .expect("couldn't replace pubky:// with http://");
|
||||
// url.set_host(Some(&format!("_pubky.{}", url.host_str().unwrap_or(""))))
|
||||
// .expect("couldn't map pubk://<pubky> to https://_pubky.<pubky>");
|
||||
// }
|
||||
//
|
||||
// let qname = url.host_str().unwrap_or("").to_string();
|
||||
//
|
||||
// if PublicKey::try_from(original_url).is_ok() {
|
||||
// let mut stream = self.pkarr.resolve_https_endpoints(&qname);
|
||||
//
|
||||
// let mut so_far: Option<Endpoint> = None;
|
||||
//
|
||||
// while let Some(endpoint) = stream.next().await {
|
||||
// if let Some(ref e) = so_far {
|
||||
// if e.domain() == "." && endpoint.domain() != "." {
|
||||
// so_far = Some(endpoint);
|
||||
// }
|
||||
// } else {
|
||||
// so_far = Some(endpoint)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let Some(e) = so_far {
|
||||
// url.set_host(Some(e.domain()))
|
||||
// .expect("coultdn't use the resolved endpoint's domain");
|
||||
// url.set_port(Some(e.port()))
|
||||
// .expect("coultdn't use the resolved endpoint's port");
|
||||
//
|
||||
// return self.http.request(method, url).fetch_credentials_include();
|
||||
// } else {
|
||||
// // TODO: didn't find any domain, what to do?
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// self.http.request(method, url).fetch_credentials_include()
|
||||
// }
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::Client;
|
||||
|
||||
use super::list_builder::ListBuilder;
|
||||
|
||||
impl Client {
|
||||
pub(crate) fn inner_list<T: IntoUrl>(&self, url: T) -> Result<ListBuilder> {
|
||||
Ok(ListBuilder::new(self, url))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
use bytes::Bytes;
|
||||
use mainline::Testnet;
|
||||
use pkarr::Keypair;
|
||||
use pubky_homeserver::Homeserver;
|
||||
use reqwest::{Method, StatusCode};
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_get_delete() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
|
||||
client
|
||||
.delete(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unauthorized_put_delete() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let public_key = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{public_key}/pub/foo.txt");
|
||||
let url = url.as_str();
|
||||
|
||||
let other_client = Client::test(&testnet);
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.put(url)
|
||||
.body(vec![0, 1, 2, 3, 4])
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let other = Keypair::random();
|
||||
|
||||
// TODO: remove extra client after switching to subdomains.
|
||||
other_client
|
||||
.signup(&other, &server.public_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
other_client.delete(url).send().await.unwrap().status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.wrong/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/z.wrong/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/example.com/extra");
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client.list(&url).unwrap().limit(2).send().await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
],
|
||||
"normal list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("cc-nested/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
],
|
||||
"normal list with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor(&format!("pubky://{pubky}/pub/example.com/a.txt"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a full url cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.limit(2)
|
||||
.cursor("/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"normal list with limit and a leading / cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
],
|
||||
"reverse list with no limit or cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
],
|
||||
"reverse list with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("d.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/cc-nested/z.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
],
|
||||
"reverse list with limit and cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shallow() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(url).body(vec![0]).send().await.unwrap();
|
||||
}
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/");
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
],
|
||||
"normal list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"normal list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.cursor("example.com/a.txt")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
],
|
||||
"normal list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.limit(3)
|
||||
.cursor("example.com/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
],
|
||||
"normal list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
format!("pubky://{pubky}/pub/a.com/"),
|
||||
],
|
||||
"reverse list shallow"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.reverse(true)
|
||||
.shallow(true)
|
||||
.limit(2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/z.com/"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
],
|
||||
"reverse list shallow with limit but no cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("file2")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/example.con/"),
|
||||
],
|
||||
"reverse list shallow with limit and a file cursor"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let list = client
|
||||
.list(&url)
|
||||
.unwrap()
|
||||
.shallow(true)
|
||||
.reverse(true)
|
||||
.limit(2)
|
||||
.cursor("example.con/")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
list,
|
||||
vec![
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/example.com/"),
|
||||
],
|
||||
"reverse list shallow with limit and a directory cursor"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_events() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let urls = vec![
|
||||
format!("pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("pubky://{pubky}/pub/example.con"),
|
||||
format!("pubky://{pubky}/pub/file"),
|
||||
format!("pubky://{pubky}/pub/file2"),
|
||||
format!("pubky://{pubky}/pub/z.com/a.txt"),
|
||||
];
|
||||
|
||||
for url in urls {
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
client.delete(url).send().await.unwrap();
|
||||
}
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let cursor;
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/a.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/b.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/c.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.com/d.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con/d.txt"),
|
||||
format!("PUT pubky://{pubky}/pub/example.con"),
|
||||
format!("DEL pubky://{pubky}/pub/example.con"),
|
||||
format!("PUT pubky://{pubky}/pub/file"),
|
||||
format!("DEL pubky://{pubky}/pub/file"),
|
||||
format!("PUT pubky://{pubky}/pub/file2"),
|
||||
format!("DEL pubky://{pubky}/pub/file2"),
|
||||
format!("PUT pubky://{pubky}/pub/z.com/a.txt"),
|
||||
format!("DEL pubky://{pubky}/pub/z.com/a.txt"),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_after_event() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let pubky = keypair.public_key();
|
||||
|
||||
let url = format!("pubky://{pubky}/pub/a.com/a.txt");
|
||||
|
||||
client.put(&url).body(vec![0]).send().await.unwrap();
|
||||
|
||||
let feed_url = format!("https://{}/events/", server.public_key());
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
{
|
||||
let response = client
|
||||
.request(Method::GET, format!("{feed_url}?limit=10"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{pubky}/pub/a.com/a.txt"),
|
||||
format!("cursor: {cursor}",)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body = response.bytes().await.unwrap();
|
||||
|
||||
assert_eq!(body.as_ref(), &[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dont_delete_shared_blobs() {
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let homeserver = Homeserver::start_test(&testnet).await.unwrap();
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let homeserver_pubky = homeserver.public_key();
|
||||
|
||||
let user_1 = Keypair::random();
|
||||
let user_2 = Keypair::random();
|
||||
|
||||
client.signup(&user_1, &homeserver_pubky).await.unwrap();
|
||||
client.signup(&user_2, &homeserver_pubky).await.unwrap();
|
||||
|
||||
let user_1_id = user_1.public_key();
|
||||
let user_2_id = user_2.public_key();
|
||||
|
||||
let url_1 = format!("pubky://{user_1_id}/pub/pubky.app/file/file_1");
|
||||
let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1");
|
||||
|
||||
let file = vec![1];
|
||||
client.put(&url_1).body(file.clone()).send().await.unwrap();
|
||||
client.put(&url_2).body(file.clone()).send().await.unwrap();
|
||||
|
||||
// Delete file 1
|
||||
client
|
||||
.delete(url_1)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let blob = client
|
||||
.get(url_2)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.bytes()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blob, file);
|
||||
|
||||
let feed_url = format!("https://{}/events/", homeserver.public_key());
|
||||
|
||||
let response = client
|
||||
.request(Method::GET, feed_url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.error_for_status()
|
||||
.unwrap();
|
||||
|
||||
let text = response.text().await.unwrap();
|
||||
let lines = text.split('\n').collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
format!("PUT pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
format!("PUT pubky://{user_2_id}/pub/pubky.app/file/file_1",),
|
||||
format!("DEL pubky://{user_1_id}/pub/pubky.app/file/file_1",),
|
||||
lines.last().unwrap().to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream() {
|
||||
// TODO: test better streaming API
|
||||
|
||||
let testnet = Testnet::new(10).unwrap();
|
||||
let server = Homeserver::start_test(&testnet).await.unwrap();
|
||||
|
||||
let client = Client::test(&testnet);
|
||||
|
||||
let keypair = Keypair::random();
|
||||
|
||||
client.signup(&keypair, &server.public_key()).await.unwrap();
|
||||
|
||||
let url = format!("pubky://{}/pub/foo.txt", keypair.public_key());
|
||||
let url = url.as_str();
|
||||
|
||||
let bytes = Bytes::from(vec![0; 1024 * 1024]);
|
||||
|
||||
client.put(url).body(bytes.clone()).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap().bytes().await.unwrap();
|
||||
|
||||
assert_eq!(response, bytes);
|
||||
|
||||
client.delete(url).send().await.unwrap();
|
||||
|
||||
let response = client.get(url).send().await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::Client;
|
||||
pub mod api {
|
||||
pub mod auth;
|
||||
pub mod http;
|
||||
pub mod public;
|
||||
pub mod recovery_file;
|
||||
}
|
||||
|
||||
mod api;
|
||||
mod http;
|
||||
mod wrappers;
|
||||
pub mod wrappers {
|
||||
pub mod keys;
|
||||
pub mod session;
|
||||
}
|
||||
|
||||
static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/"];
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Client(crate::NativeClient);
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
@@ -12,18 +23,16 @@ impl Default for Client {
|
||||
}
|
||||
}
|
||||
|
||||
static TESTNET_RELAYS: [&str; 1] = ["http://localhost:15411/"];
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Client {
|
||||
#[wasm_bindgen(constructor)]
|
||||
/// Create Client with default Settings including default relays
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
http: reqwest::Client::builder().build().unwrap(),
|
||||
pkarr: pkarr::Client::builder().build().unwrap(),
|
||||
testnet: false,
|
||||
}
|
||||
Self(
|
||||
crate::NativeClient::builder()
|
||||
.build()
|
||||
.expect("building a default NativeClient should be infallible"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a client with with configurations appropriate for local testing:
|
||||
@@ -32,15 +41,19 @@ impl Client {
|
||||
/// and read the homeserver HTTP port from the [reserved service parameter key](pubky_common::constants::reserved_param_keys::HTTP_PORT)
|
||||
#[wasm_bindgen]
|
||||
pub fn testnet() -> Self {
|
||||
Self {
|
||||
http: reqwest::Client::builder().build().unwrap(),
|
||||
pkarr: pkarr::Client::builder()
|
||||
let mut builder = crate::NativeClient::builder();
|
||||
|
||||
builder.pkarr(|builder| {
|
||||
builder
|
||||
.relays(&TESTNET_RELAYS)
|
||||
.expect("testnet relays are valid urls")
|
||||
.build()
|
||||
.unwrap(),
|
||||
testnet: true,
|
||||
}
|
||||
});
|
||||
|
||||
let mut client = builder.build().expect("testnet build should be infallibl");
|
||||
|
||||
client.testnet = true;
|
||||
|
||||
Self(client)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ use url::Url;
|
||||
|
||||
use pubky_common::capabilities::Capabilities;
|
||||
|
||||
use crate::Client;
|
||||
use crate::wasm::wrappers::{
|
||||
keys::{Keypair, PublicKey},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
use crate::wasm::wrappers::keys::{Keypair, PublicKey};
|
||||
use crate::wasm::wrappers::session::Session;
|
||||
use super::super::Client;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -24,19 +26,21 @@ impl Client {
|
||||
homeserver: &PublicKey,
|
||||
) -> Result<Session, JsValue> {
|
||||
Ok(Session(
|
||||
self.inner_signup(keypair.as_inner(), homeserver.as_inner())
|
||||
self.0
|
||||
.signup(keypair.as_inner(), homeserver.as_inner())
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Check the current sesison for a given Pubky in its homeserver.
|
||||
/// Check the current session 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.
|
||||
/// Returns [Session] or `None` (if received `404 NOT_FOUND`),
|
||||
/// or throws the received 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())
|
||||
self.0
|
||||
.session(pubky.as_inner())
|
||||
.await
|
||||
.map(|s| s.map(Session))
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
@@ -45,7 +49,8 @@ impl Client {
|
||||
/// Signout from a homeserver.
|
||||
#[wasm_bindgen]
|
||||
pub async fn signout(&self, pubky: &PublicKey) -> Result<(), JsValue> {
|
||||
self.inner_signout(pubky.as_inner())
|
||||
self.0
|
||||
.signout(pubky.as_inner())
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
@@ -53,7 +58,8 @@ impl Client {
|
||||
/// 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())
|
||||
self.0
|
||||
.signin(keypair.as_inner())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
@@ -63,35 +69,18 @@ impl Client {
|
||||
/// 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>]
|
||||
/// Returns a [AuthRequest]
|
||||
#[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(|_| "Invalid relay Url")?;
|
||||
|
||||
let (pubkyauth_url, client_secret) = self
|
||||
.create_auth_request(
|
||||
&mut relay,
|
||||
pub fn auth_request(&self, relay: &str, capabilities: &str) -> Result<AuthRequest, JsValue> {
|
||||
let auth_request = self
|
||||
.0
|
||||
.auth_request(
|
||||
relay,
|
||||
&Capabilities::try_from(capabilities).map_err(|_| "Invalid capaiblities")?,
|
||||
)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
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(|e| JsValue::from_str(&e.to_string()))
|
||||
};
|
||||
|
||||
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)
|
||||
Ok(AuthRequest(auth_request))
|
||||
}
|
||||
|
||||
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
|
||||
@@ -104,10 +93,40 @@ impl Client {
|
||||
) -> Result<(), JsValue> {
|
||||
let pubkyauth_url: Url = pubkyauth_url.try_into().map_err(|_| "Invalid relay Url")?;
|
||||
|
||||
self.inner_send_auth_token(keypair.as_inner(), pubkyauth_url)
|
||||
self.0
|
||||
.send_auth_token(keypair.as_inner(), &pubkyauth_url)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct AuthRequest(crate::AuthRequest);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl AuthRequest {
|
||||
/// Returns the Pubky Auth url, which you should show to the user
|
||||
/// to request an authentication or authorization token.
|
||||
///
|
||||
/// Wait for this token using `this.response()`.
|
||||
#[wasm_bindgen]
|
||||
pub fn url(&self) -> String {
|
||||
self.0.url().as_str().to_string()
|
||||
}
|
||||
|
||||
/// Wait for the user to send an authentication or authorization proof.
|
||||
///
|
||||
/// If successful, you should expect an instance of [PublicKey]
|
||||
///
|
||||
/// Otherwise it will throw an error.
|
||||
#[wasm_bindgen]
|
||||
pub async fn response(&self) -> Result<PublicKey, JsValue> {
|
||||
self.0
|
||||
.response()
|
||||
.await
|
||||
.map(PublicKey::from)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use futures_lite::StreamExt;
|
||||
use pkarr::extra::endpoints::Endpoint;
|
||||
use pkarr::PublicKey;
|
||||
|
||||
use crate::Client;
|
||||
use super::super::Client;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Client {
|
||||
@@ -27,7 +27,7 @@ impl Client {
|
||||
|
||||
let request_init = request_init.unwrap_or_default();
|
||||
|
||||
if let Some(pubky_host) = self.prepare_request(&mut url).await {
|
||||
if let Some(pubky_host) = self.0.prepare_request(&mut url).await {
|
||||
let headers = request_init.get_headers();
|
||||
|
||||
let headers = if headers.is_null() || headers.is_undefined() {
|
||||
@@ -74,9 +74,9 @@ fn js_fetch(req: &web_sys::Request) -> Promise {
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// A wrapper around [reqwest::Client::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn inner_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
|
||||
impl crate::NativeClient {
|
||||
/// A wrapper around [NativeClient::request], with the same signature between native and wasm.
|
||||
pub(crate) async fn cross_request<T: IntoUrl>(&self, method: Method, url: T) -> RequestBuilder {
|
||||
let original_url = url.as_str();
|
||||
let mut url = Url::parse(original_url).expect("Invalid url in inner_request");
|
||||
|
||||
@@ -95,7 +95,7 @@ impl Client {
|
||||
/// - Transforms pubky:// url to http(s):// urls
|
||||
/// - Resolves a clearnet host to call with fetch
|
||||
/// - Returns the `pubky-host` value if available
|
||||
pub(super) async fn prepare_request(&self, url: &mut Url) -> Option<String> {
|
||||
pub(crate) async fn prepare_request(&self, url: &mut Url) -> Option<String> {
|
||||
let host = url.host_str().unwrap_or("").to_string();
|
||||
|
||||
if url.scheme() == "pubky" {
|
||||
@@ -116,10 +116,10 @@ impl Client {
|
||||
pubky_host
|
||||
}
|
||||
|
||||
pub async fn transform_url(&self, url: &mut Url) {
|
||||
pub(crate) async fn transform_url(&self, url: &mut Url) {
|
||||
let clone = url.clone();
|
||||
let qname = clone.host_str().unwrap_or("");
|
||||
log::debug!("Prepare request {}", url.as_str());
|
||||
cross_debug!("Prepare request {}", url.as_str());
|
||||
|
||||
let mut stream = self.pkarr.resolve_https_endpoints(qname);
|
||||
|
||||
@@ -163,7 +163,7 @@ impl Client {
|
||||
} else {
|
||||
// TODO: didn't find any domain, what to do?
|
||||
// return an error.
|
||||
log::debug!("Could not resolve host: {}", qname);
|
||||
cross_debug!("Could not resolve host: {}", qname);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
pub mod auth;
|
||||
pub mod public;
|
||||
@@ -3,7 +3,7 @@
|
||||
use js_sys::Array;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::Client;
|
||||
use super::super::Client;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Client {
|
||||
@@ -28,7 +28,8 @@ impl Client {
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
return self
|
||||
.inner_list(url)
|
||||
.0
|
||||
.list(url)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
@@ -48,7 +49,8 @@ impl Client {
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
|
||||
self.inner_list(url)
|
||||
self.0
|
||||
.list(url)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?
|
||||
.reverse(reverse.unwrap_or(false))
|
||||
.limit(limit.unwrap_or(u16::MAX))
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//! Wasm wrappers around structs that we need to be turned into Classes
|
||||
//! in JavaScript.
|
||||
|
||||
pub mod keys;
|
||||
pub mod session;
|
||||
Reference in New Issue
Block a user