diff --git a/pubky-homeserver/src/database/tables/sessions.rs b/pubky-homeserver/src/database/tables/sessions.rs index 4ecd228..55a8e3c 100644 --- a/pubky-homeserver/src/database/tables/sessions.rs +++ b/pubky-homeserver/src/database/tables/sessions.rs @@ -31,7 +31,8 @@ impl DB { cookies: Cookies, public_key: &PublicKey, ) -> anyhow::Result>> { - if let Some(cookie) = cookies.get(&public_key.to_string()) { + // TODO: support coookie for key in the path + if let Some(cookie) = cookies.get("session_id") { let rtxn = self.env.read_txn()?; let sessions: SessionsTable = self diff --git a/pubky-homeserver/src/extractors.rs b/pubky-homeserver/src/extractors.rs index 28a8790..29ff058 100644 --- a/pubky-homeserver/src/extractors.rs +++ b/pubky-homeserver/src/extractors.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, ops::Deref}; use axum::{ async_trait, @@ -55,11 +55,26 @@ where } } +#[derive(Debug)] pub struct EntryPath(pub(crate) String); impl EntryPath { pub fn as_str(&self) -> &str { - self.0.as_str() + self.as_ref() + } +} + +impl std::fmt::Display for EntryPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +impl Deref for EntryPath { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -80,7 +95,11 @@ where .get("path") .ok_or_else(|| (StatusCode::NOT_FOUND, "entry path missing").into_response())?; - Ok(EntryPath(path.to_string())) + if parts.uri.to_string().starts_with("/pub/") { + Ok(EntryPath(format!("pub/{}", path))) + } else { + Ok(EntryPath(path.to_string())) + } } } diff --git a/pubky-homeserver/src/routes.rs b/pubky-homeserver/src/routes.rs index be87c26..352dc58 100644 --- a/pubky-homeserver/src/routes.rs +++ b/pubky-homeserver/src/routes.rs @@ -21,15 +21,26 @@ fn base(state: AppState) -> Router { .route("/", get(root::handler)) .route("/signup", post(auth::signup)) .route("/session", post(auth::signin)) - // Pub - .route("/pub/*path", get(public::get_subdomain)) + // Routes for Pubky in the Hostname. // + // The default and wortks with native Pubky client. + // - Data routes + .route("/pub/*path", get(public::read::get)) + .route("/pub/*path", head(public::read::head)) + .route("/pub/*path", put(public::write::put)) + .route("/pub/*path", delete(public::write::delete)) + // Pubky in the path. + // + // Important to support web browsers until they support Pkarr domains natively. + // - Session routes .route("/:pubky/session", get(auth::session)) .route("/:pubky/session", delete(auth::signout)) - .route("/:pubky/*path", put(public::put)) - .route("/:pubky/*path", get(public::get)) - .route("/:pubky/*path", head(public::head)) - .route("/:pubky/*path", delete(public::delete)) + // - Data routes + .route("/:pubky/*path", get(public::read::get)) + .route("/:pubky/*path", head(public::read::head)) + .route("/:pubky/*path", put(public::write::put)) + .route("/:pubky/*path", delete(public::write::delete)) + // Events .route("/events/", get(feed::feed)) .layer(CookieManagerLayer::new()) // TODO: revisit if we enable streaming big payloads diff --git a/pubky-homeserver/src/routes/auth.rs b/pubky-homeserver/src/routes/auth.rs index eaf3a57..59f080e 100644 --- a/pubky-homeserver/src/routes/auth.rs +++ b/pubky-homeserver/src/routes/auth.rs @@ -119,7 +119,11 @@ pub async fn signin( .sessions .put(&mut wtxn, &session_secret, &session)?; - let mut cookie = Cookie::new(public_key.to_string(), session_secret); + let mut cookie = if true { + Cookie::new("session_id", session_secret) + } else { + Cookie::new(public_key.to_string(), session_secret) + }; cookie.set_path("/"); diff --git a/pubky-homeserver/src/routes/public/mod.rs b/pubky-homeserver/src/routes/public/mod.rs new file mode 100644 index 0000000..c85d31f --- /dev/null +++ b/pubky-homeserver/src/routes/public/mod.rs @@ -0,0 +1,52 @@ +use axum::http::StatusCode; +use pkarr::PublicKey; +use tower_cookies::Cookies; + +use crate::{ + error::{Error, Result}, + server::AppState, +}; + +pub mod read; +pub mod write; + +/// Authorize write (PUT or DELETE) for Public paths. +fn authorize( + state: &mut AppState, + cookies: Cookies, + public_key: &PublicKey, + path: &str, +) -> Result<()> { + // TODO: can we move this logic to the extractor or a layer + // to perform this validation? + let session = state + .db + .get_session(cookies, public_key)? + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + if session.pubky() == public_key + && session.capabilities().iter().any(|cap| { + path.starts_with(&cap.scope[1..]) + && cap + .actions + .contains(&pubky_common::capabilities::Action::Write) + }) + { + return Ok(()); + } + + Err(Error::with_status(StatusCode::FORBIDDEN)) +} + +fn verify(path: &str) -> Result<()> { + if !path.starts_with("pub/") { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); + } + + // TODO: should we forbid paths ending with `/`? + + Ok(()) +} diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public/read.rs similarity index 70% rename from pubky-homeserver/src/routes/public.rs rename to pubky-homeserver/src/routes/public/read.rs index 8f4b7a2..8f08d91 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public/read.rs @@ -4,11 +4,8 @@ use axum::{ http::{header, HeaderMap, HeaderValue, Response, StatusCode}, response::IntoResponse, }; -use futures_util::stream::StreamExt; use httpdate::HttpDate; -use pkarr::PublicKey; -use std::{io::Write, str::FromStr}; -use tower_cookies::Cookies; +use std::str::FromStr; use crate::{ database::tables::entries::Entry, @@ -17,65 +14,12 @@ use crate::{ server::AppState, }; -pub async fn put( - State(mut state): State, - pubky: Pubky, - path: EntryPath, - cookies: Cookies, - body: Body, -) -> Result { - let public_key = pubky.public_key().clone(); - let path = path.as_str().to_string(); - - verify(&path)?; - authorize(&mut state, cookies, &public_key, &path)?; - - let mut entry_writer = state.db.write_entry(&public_key, &path)?; - - let mut stream = body.into_data_stream(); - while let Some(next) = stream.next().await { - let chunk = next?; - entry_writer.write_all(&chunk)?; - } - - let _entry = entry_writer.commit()?; - - // TODO: return relevant headers, like Etag? - - Ok(()) -} - -pub async fn get( - State(state): State, - headers: HeaderMap, - pubky: Pubky, - path: EntryPath, - params: ListQueryParams, -) -> Result { - Ok(get_common(state, headers, pubky, path.as_str(), params).await?) -} - -pub async fn get_subdomain( - State(state): State, - headers: HeaderMap, - pubky: Pubky, - path: EntryPath, - params: ListQueryParams, -) -> Result { - Ok(get_common( - state, - headers, - pubky, - &format!("pub/{}", path.as_str()), - params, - ) - .await?) -} +use super::verify; pub async fn head( State(state): State, - headers: HeaderMap, pubky: Pubky, + headers: HeaderMap, path: EntryPath, ) -> Result { verify(path.as_str())?; @@ -91,18 +35,18 @@ pub async fn head( ) } -async fn get_common( - state: AppState, +pub async fn get( + State(state): State, headers: HeaderMap, pubky: Pubky, - path: &str, + path: EntryPath, params: ListQueryParams, ) -> Result { - verify(path)?; - let public_key = pubky.public_key().clone(); - let path = path.to_string(); + verify(&path)?; - if path.ends_with('/') { + let public_key = pubky.public_key().clone(); + + if path.as_str().ends_with('/') { let txn = state.db.env.read_txn()?; let path = format!("{public_key}/{path}"); @@ -210,70 +154,6 @@ pub fn get_entry( } } -pub async fn delete( - State(mut state): State, - pubky: Pubky, - path: EntryPath, - cookies: Cookies, -) -> Result { - let public_key = pubky.public_key().clone(); - let path = path.as_str(); - - authorize(&mut state, cookies, &public_key, path)?; - verify(path)?; - - // TODO: should we wrap this with `tokio::task::spawn_blocking` in case it takes too long? - let deleted = state.db.delete_entry(&public_key, path)?; - - if !deleted { - // TODO: if the path ends with `/` return a `CONFLICT` error? - return Err(Error::with_status(StatusCode::NOT_FOUND)); - }; - - Ok(()) -} - -/// Authorize write (PUT or DELETE) for Public paths. -fn authorize( - state: &mut AppState, - cookies: Cookies, - public_key: &PublicKey, - path: &str, -) -> Result<()> { - // TODO: can we move this logic to the extractor or a layer - // to perform this validation? - let session = state - .db - .get_session(cookies, public_key)? - .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; - - if session.pubky() == public_key - && session.capabilities().iter().any(|cap| { - path.starts_with(&cap.scope[1..]) - && cap - .actions - .contains(&pubky_common::capabilities::Action::Write) - }) - { - return Ok(()); - } - - Err(Error::with_status(StatusCode::FORBIDDEN)) -} - -fn verify(path: &str) -> Result<()> { - if !path.starts_with("pub/") { - return Err(Error::new( - StatusCode::FORBIDDEN, - "Writing to directories other than '/pub/' is forbidden".into(), - )); - } - - // TODO: should we forbid paths ending with `/`? - - Ok(()) -} - impl From<&Entry> for HeaderMap { fn from(entry: &Entry) -> Self { let mut headers = HeaderMap::new(); diff --git a/pubky-homeserver/src/routes/public/write.rs b/pubky-homeserver/src/routes/public/write.rs new file mode 100644 index 0000000..a6300e4 --- /dev/null +++ b/pubky-homeserver/src/routes/public/write.rs @@ -0,0 +1,63 @@ +use std::io::Write; + +use futures_util::stream::StreamExt; + +use axum::{body::Body, extract::State, http::StatusCode, response::IntoResponse}; +use tower_cookies::Cookies; + +use crate::{ + error::{Error, Result}, + extractors::{EntryPath, Pubky}, + server::AppState, +}; + +use super::{authorize, verify}; + +pub async fn delete( + State(mut state): State, + pubky: Pubky, + path: EntryPath, + cookies: Cookies, +) -> Result { + let public_key = pubky.public_key().clone(); + + verify(&path)?; + authorize(&mut state, cookies, &public_key, &path)?; + + // TODO: should we wrap this with `tokio::task::spawn_blocking` in case it takes too long? + let deleted = state.db.delete_entry(&public_key, &path)?; + + if !deleted { + // TODO: if the path ends with `/` return a `CONFLICT` error? + return Err(Error::with_status(StatusCode::NOT_FOUND)); + }; + + Ok(()) +} + +pub async fn put( + State(mut state): State, + pubky: Pubky, + path: EntryPath, + cookies: Cookies, + body: Body, +) -> Result { + let public_key = pubky.public_key().clone(); + + verify(&path)?; + authorize(&mut state, cookies, &public_key, &path)?; + + let mut entry_writer = state.db.write_entry(&public_key, &path)?; + + let mut stream = body.into_data_stream(); + while let Some(next) = stream.next().await { + let chunk = next?; + entry_writer.write_all(&chunk)?; + } + + let _entry = entry_writer.commit()?; + + // TODO: return relevant headers, like Etag? + + Ok(()) +} diff --git a/pubky/src/lib.rs b/pubky/src/lib.rs index 69583c5..aa8ef76 100644 --- a/pubky/src/lib.rs +++ b/pubky/src/lib.rs @@ -10,6 +10,8 @@ mod native; #[cfg(target_arch = "wasm32")] mod wasm; +use std::{fmt::Debug, sync::Arc}; + use wasm_bindgen::prelude::*; pub use error::Error; @@ -18,9 +20,16 @@ pub use error::Error; pub use crate::shared::list_builder::ListBuilder; /// A client for Pubky homeserver API, as well as generic HTTP requests to Pubky urls. -#[derive(Debug, Clone)] +#[derive(Clone)] #[wasm_bindgen] pub struct Client { + cookie_store: Arc, http: reqwest::Client, pub(crate) pkarr: pkarr::Client, } + +impl Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Pubky Client").finish() + } +} diff --git a/pubky/src/native.rs b/pubky/src/native.rs index 9a3e49f..d304a06 100644 --- a/pubky/src/native.rs +++ b/pubky/src/native.rs @@ -5,6 +5,7 @@ use pkarr::mainline::Testnet; use crate::Client; mod api; +mod http; mod internals; static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -46,13 +47,19 @@ impl Settings { pub fn build(self) -> Result { let pkarr = pkarr::Client::new(self.pkarr_settings)?; + let cookie_store = Arc::new(reqwest::cookie::Jar::default()); + + let http = reqwest::Client::builder() + .cookie_provider(cookie_store.clone()) + // TODO: use persistent cookie jar + .dns_resolver(Arc::new(pkarr.clone())) + .user_agent(DEFAULT_USER_AGENT) + .build() + .expect("config expected to not error"); + Ok(Client { - http: reqwest::Client::builder() - .cookie_store(true) - .dns_resolver(Arc::new(pkarr.clone())) - .user_agent(DEFAULT_USER_AGENT) - .build() - .unwrap(), + cookie_store, + http, pkarr, }) } diff --git a/pubky/src/native/api/mod.rs b/pubky/src/native/api/mod.rs index a502667..f43316f 100644 --- a/pubky/src/native/api/mod.rs +++ b/pubky/src/native/api/mod.rs @@ -1,4 +1,3 @@ -pub mod http; pub mod recovery_file; // TODO: put the Homeserver API behind a feature flag diff --git a/pubky/src/native/api/public.rs b/pubky/src/native/api/public.rs index 12a67c4..e2a9753 100644 --- a/pubky/src/native/api/public.rs +++ b/pubky/src/native/api/public.rs @@ -3,16 +3,6 @@ use url::Url; use crate::{error::Result, shared::list_builder::ListBuilder, Client}; impl Client { - /// Upload a small payload to a given path. - pub async fn put>(&self, url: T, content: &[u8]) -> Result<()> { - self.inner_put(url, content).await - } - - /// Delete a file at a path relative to a pubky author. - pub async fn delete>(&self, url: T) -> Result<()> { - self.inner_delete(url).await - } - /// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send]. /// /// `url` sets the path you want to lest within. diff --git a/pubky/src/native/api/http.rs b/pubky/src/native/http.rs similarity index 95% rename from pubky/src/native/api/http.rs rename to pubky/src/native/http.rs index 41b9b04..787f4e2 100644 --- a/pubky/src/native/api/http.rs +++ b/pubky/src/native/http.rs @@ -1,3 +1,5 @@ +//! HTTP methods that support `https://` with Pkarr domains, and `pubky://` URLs + use reqwest::{IntoUrl, Method, RequestBuilder}; use crate::Client; @@ -67,9 +69,9 @@ impl Client { /// # Errors /// /// This method fails whenever the supplied `Url` cannot be parsed. - // pub fn put(&self, url: U) -> RequestBuilder { - // self.request(Method::PUT, url) - // } + pub fn put(&self, url: U) -> RequestBuilder { + self.request(Method::PUT, url) + } /// Convenience method to make a `PATCH` request to a URL. /// @@ -99,9 +101,9 @@ impl Client { /// # Errors /// /// This method fails whenever the supplied `Url` cannot be parsed. - // pub fn delete(&self, url: U) -> RequestBuilder { - // self.request(Method::DELETE, url) - // } + pub fn delete(&self, url: U) -> RequestBuilder { + self.request(Method::DELETE, url) + } /// Convenience method to make a `HEAD` request to a URL. /// diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 670c1f0..df8219e 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -38,14 +38,21 @@ impl Client { let body = AuthToken::sign(keypair, vec![Capability::root()]).serialize(); let response = self - .inner_request(Method::POST, url.clone()) - .await + .http + .request(Method::POST, url.clone()) .body(body) .send() - .await?; + .await? + .error_for_status()?; self.publish_pubky_homeserver(keypair, &homeserver).await?; + self.cookie_store.set_cookies( + &mut response.headers().values(), + &url::Url::parse(&format!("http://_pubky.{}", keypair.public_key())) + .expect("url from keypair doesn't fail"), + ); + let bytes = response.bytes().await?; Ok(Session::deserialize(&bytes)?) @@ -144,22 +151,19 @@ impl Client { .await .body(encrypted_token) .send() - .await?; + .await? + .error_for_status(); Ok(()) } pub(crate) async fn signin_with_authtoken(&self, token: &AuthToken) -> Result { - let mut url = Url::parse(&format!("https://{}/session", token.pubky()))?; - - self.resolve_url(&mut url).await?; - let response = self - .inner_request(Method::POST, url) - .await + .request(Method::POST, format!("pubky://{}/session", token.pubky())) .body(token.serialize()) .send() - .await?; + .await? + .error_for_status()?; let bytes = response.bytes().await?; @@ -262,7 +266,7 @@ mod tests { } #[tokio::test] - async fn authz() { + async fn authz() -> anyhow::Result<()> { let testnet = Testnet::new(10).unwrap(); let server = Homeserver::start_test(&testnet).await.unwrap(); @@ -299,30 +303,32 @@ mod tests { // Test access control enforcement client - .put(format!("pubky://{pubky}/pub/pubky.app/foo").as_str(), &[]) - .await - .unwrap(); + .put(format!("pubky://{pubky}/pub/pubky.app/foo")) + .body(vec![]) + .send() + .await? + .error_for_status()?; assert_eq!( client - .put(format!("pubky://{pubky}/pub/pubky.app").as_str(), &[]) - .await - .map_err(|e| match e { - crate::Error::Reqwest(e) => e.status(), - _ => None, - }), - Err(Some(StatusCode::FORBIDDEN)) + .put(format!("pubky://{pubky}/pub/pubky.app")) + .body(vec![]) + .send() + .await? + .status(), + StatusCode::FORBIDDEN ); assert_eq!( client - .put(format!("pubky://{pubky}/pub/foo.bar/file").as_str(), &[]) - .await - .map_err(|e| match e { - crate::Error::Reqwest(e) => e.status(), - _ => None, - }), - Err(Some(StatusCode::FORBIDDEN)) + .put(format!("pubky://{pubky}/pub/foo.bar/file")) + .body(vec![]) + .send() + .await? + .status(), + StatusCode::FORBIDDEN ); + + Ok(()) } } diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 351609f..b0fb120 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -1,7 +1,4 @@ -use bytes::Bytes; - use pkarr::PublicKey; -use reqwest::{Method, StatusCode}; use url::Url; use crate::{ @@ -12,31 +9,6 @@ use crate::{ use super::{list_builder::ListBuilder, pkarr::Endpoint}; impl Client { - pub(crate) async fn inner_put>(&self, url: T, content: &[u8]) -> Result<()> { - let url = self.pubky_to_http(url).await?; - - let response = self - .inner_request(Method::PUT, url) - .await - .body(content.to_owned()) - .send() - .await?; - - response.error_for_status()?; - - Ok(()) - } - - pub(crate) async fn inner_delete>(&self, url: T) -> Result<()> { - let url = self.pubky_to_http(url).await?; - - let response = self.inner_request(Method::DELETE, url).await.send().await?; - - response.error_for_status_ref()?; - - Ok(()) - } - pub(crate) fn inner_list>(&self, url: T) -> Result { Ok(ListBuilder::new( self, @@ -78,8 +50,6 @@ impl Client { #[cfg(test)] mod tests { - use core::panic; - use crate::*; use bytes::Bytes; @@ -101,13 +71,18 @@ mod tests { let url = format!("pubky://{}/pub/foo.txt", keypair.public_key()); let url = url.as_str(); - client.put(url, &[0, 1, 2, 3, 4]).await?; + client + .put(url) + .body(vec![0, 1, 2, 3, 4]) + .send() + .await? + .error_for_status()?; let response = client.get(url).send().await?.bytes().await?; assert_eq!(response, bytes::Bytes::from(vec![0, 1, 2, 3, 4])); - client.delete(url).await.unwrap(); + client.delete(url).send().await?.error_for_status()?; let response = client.get(url).send().await?; @@ -139,19 +114,18 @@ mod tests { // TODO: remove extra client after switching to subdomains. other_client.signup(&other, &server.public_key()).await?; - let response = other_client.put(url, &[0, 1, 2, 3, 4]).await; - - match response { - Err(Error::Reqwest(error)) => { - assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) - } - _ => { - panic!("expected error StatusCode::UNAUTHORIZED") - } - } + assert_eq!( + other_client + .put(url) + .body(vec![0, 1, 2, 3, 4]) + .send() + .await? + .status(), + StatusCode::UNAUTHORIZED + ); } - client.put(url, &[0, 1, 2, 3, 4]).await?; + client.put(url).body(vec![0, 1, 2, 3, 4]).send().await?; { let other = Keypair::random(); @@ -159,16 +133,10 @@ mod tests { // TODO: remove extra client after switching to subdomains. other_client.signup(&other, &server.public_key()).await?; - let response = other_client.delete(url).await; - - match response { - Err(Error::Reqwest(error)) => { - assert!(error.status() == Some(StatusCode::UNAUTHORIZED)) - } - _ => { - panic!("expected error StatusCode::UNAUTHORIZED") - } - } + assert_eq!( + other_client.delete(url).send().await?.status(), + StatusCode::UNAUTHORIZED + ); } let response = client.get(url).send().await?.bytes().await?; @@ -203,7 +171,7 @@ mod tests { ]; for url in urls { - client.put(url.as_str(), &[0]).await?; + client.put(url).body(vec![0]).send().await?; } let url = format!("pubky://{pubky}/pub/example.com/extra"); @@ -378,20 +346,14 @@ mod tests { ]; for url in urls { - client.put(url.as_str(), &[0]).await.unwrap(); + client.put(url).body(vec![0]).send().await?; } let url = format!("pubky://{pubky}/pub/"); let url = url.as_str(); { - let list = client - .list(url) - .unwrap() - .shallow(true) - .send() - .await - .unwrap(); + let list = client.list(url)?.shallow(true).send().await?; assert_eq!( list, @@ -409,14 +371,7 @@ mod tests { } { - let list = client - .list(url) - .unwrap() - .shallow(true) - .limit(2) - .send() - .await - .unwrap(); + let list = client.list(url)?.shallow(true).limit(2).send().await?; assert_eq!( list, @@ -430,14 +385,12 @@ mod tests { { let list = client - .list(url) - .unwrap() + .list(url)? .shallow(true) .limit(2) .cursor("example.com/a.txt") .send() - .await - .unwrap(); + .await?; assert_eq!( list, @@ -451,14 +404,12 @@ mod tests { { let list = client - .list(url) - .unwrap() + .list(url)? .shallow(true) .limit(3) .cursor("example.com/") .send() - .await - .unwrap(); + .await?; assert_eq!( list, @@ -472,14 +423,7 @@ mod tests { } { - let list = client - .list(url) - .unwrap() - .reverse(true) - .shallow(true) - .send() - .await - .unwrap(); + let list = client.list(url)?.reverse(true).shallow(true).send().await?; assert_eq!( list, @@ -498,14 +442,12 @@ mod tests { { let list = client - .list(url) - .unwrap() + .list(url)? .reverse(true) .shallow(true) .limit(2) .send() - .await - .unwrap(); + .await?; assert_eq!( list, @@ -519,15 +461,13 @@ mod tests { { let list = client - .list(url) - .unwrap() + .list(url)? .shallow(true) .reverse(true) .limit(2) .cursor("file2") .send() - .await - .unwrap(); + .await?; assert_eq!( list, @@ -541,15 +481,13 @@ mod tests { { let list = client - .list(url) - .unwrap() + .list(url)? .shallow(true) .reverse(true) .limit(2) .cursor("example.con/") .send() - .await - .unwrap(); + .await?; assert_eq!( list, @@ -566,14 +504,14 @@ mod tests { #[tokio::test] async fn list_events() -> anyhow::Result<()> { - let testnet = Testnet::new(10).unwrap(); - let server = Homeserver::start_test(&testnet).await.unwrap(); + let testnet = Testnet::new(10)?; + let server = Homeserver::start_test(&testnet).await?; let client = Client::test(&testnet); let keypair = Keypair::random(); - client.signup(&keypair, &server.public_key()).await.unwrap(); + client.signup(&keypair, &server.public_key()).await?; let pubky = keypair.public_key(); @@ -591,8 +529,8 @@ mod tests { ]; for url in urls { - client.put(url.as_str(), &[0]).await.unwrap(); - client.delete(url.as_str()).await.unwrap(); + client.put(&url).body(vec![0]).send().await?; + client.delete(url).send().await?; } let feed_url = format!("http://localhost:{}/events/", server.port()); @@ -606,10 +544,9 @@ mod tests { let response = client .request(Method::GET, format!("{feed_url}?limit=10")) .send() - .await - .unwrap(); + .await?; - let text = response.text().await.unwrap(); + let text = response.text().await?; let lines = text.split('\n').collect::>(); cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); @@ -636,10 +573,9 @@ mod tests { let response = client .request(Method::GET, format!("{feed_url}?limit=10&cursor={cursor}")) .send() - .await - .unwrap(); + .await?; - let text = response.text().await.unwrap(); + let text = response.text().await?; let lines = text.split('\n').collect::>(); assert_eq!( @@ -665,21 +601,21 @@ mod tests { #[tokio::test] async fn read_after_event() -> anyhow::Result<()> { - let testnet = Testnet::new(10).unwrap(); - let server = Homeserver::start_test(&testnet).await.unwrap(); + let testnet = Testnet::new(10)?; + let server = Homeserver::start_test(&testnet).await?; let client = Client::test(&testnet); let keypair = Keypair::random(); - client.signup(&keypair, &server.public_key()).await.unwrap(); + client.signup(&keypair, &server.public_key()).await?; let pubky = keypair.public_key(); let url = format!("pubky://{pubky}/pub/a.com/a.txt"); let url = url.as_str(); - client.put(url, &[0]).await.unwrap(); + client.put(url).body(vec![0]).send().await?; let feed_url = format!("http://localhost:{}/events/", server.port()); let feed_url = feed_url.as_str(); @@ -690,10 +626,9 @@ mod tests { let response = client .request(Method::GET, format!("{feed_url}?limit=10")) .send() - .await - .unwrap(); + .await?; - let text = response.text().await.unwrap(); + let text = response.text().await?; let lines = text.split('\n').collect::>(); let cursor = lines.last().unwrap().split(" ").last().unwrap().to_string(); @@ -716,8 +651,8 @@ mod tests { #[tokio::test] async fn dont_delete_shared_blobs() -> anyhow::Result<()> { - let testnet = Testnet::new(10).unwrap(); - let homeserver = Homeserver::start_test(&testnet).await.unwrap(); + let testnet = Testnet::new(10)?; + let homeserver = Homeserver::start_test(&testnet).await?; let client = Client::test(&testnet); let homeserver_pubky = homeserver.public_key(); @@ -725,8 +660,8 @@ mod tests { 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(); + client.signup(&user_1, &homeserver_pubky).await?; + client.signup(&user_2, &homeserver_pubky).await?; let user_1_id = user_1.public_key(); let user_2_id = user_2.public_key(); @@ -735,13 +670,13 @@ mod tests { let url_2 = format!("pubky://{user_2_id}/pub/pubky.app/file/file_1"); let file = vec![1]; - client.put(url_1.as_str(), &file).await.unwrap(); - client.put(url_2.as_str(), &file).await.unwrap(); + client.put(&url_1).body(file.clone()).send().await?; + client.put(&url_2).body(file.clone()).send().await?; // Delete file 1 - client.delete(url_1.as_str()).await.unwrap(); + client.delete(url_1).send().await?.error_for_status()?; - let blob = client.get(url_2.as_str()).send().await?.bytes().await?; + let blob = client.get(url_2).send().await?.bytes().await?; assert_eq!(blob, file); @@ -750,10 +685,9 @@ mod tests { let response = client .request(Method::GET, format!("{feed_url}")) .send() - .await - .unwrap(); + .await?; - let text = response.text().await.unwrap(); + let text = response.text().await?; let lines = text.split('\n').collect::>(); assert_eq!( @@ -773,27 +707,27 @@ mod tests { async fn stream() -> anyhow::Result<()> { // TODO: test better streaming API - let testnet = Testnet::new(10).unwrap(); - let server = Homeserver::start_test(&testnet).await.unwrap(); + let testnet = Testnet::new(10)?; + let server = Homeserver::start_test(&testnet).await?; let client = Client::test(&testnet); let keypair = Keypair::random(); - client.signup(&keypair, &server.public_key()).await.unwrap(); + client.signup(&keypair, &server.public_key()).await?; 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, &bytes).await.unwrap(); + client.put(url).body(bytes.clone()).send().await?; let response = client.get(url).send().await?.bytes().await?; assert_eq!(response, bytes); - client.delete(url).await.unwrap(); + client.delete(url).send().await?; let response = client.get(url).send().await?; diff --git a/pubky/src/wasm/internals.rs b/pubky/src/wasm/internals.rs index 6180315..0d387a5 100644 --- a/pubky/src/wasm/internals.rs +++ b/pubky/src/wasm/internals.rs @@ -1,7 +1,7 @@ use reqwest::{Method, RequestBuilder}; use url::Url; -use pkarr::{EndpointResolver, PublicKey}; +use pkarr::PublicKey; use crate::{error::Result, Client};