feat(pubky): update HEAD, GET, PUT, and DELETE for pub/ endpoints to use hostnames

This commit is contained in:
nazeh
2024-11-24 21:26:38 +03:00
parent 05cad66366
commit 6109df3d43
15 changed files with 304 additions and 327 deletions

View File

@@ -31,7 +31,8 @@ impl DB {
cookies: Cookies,
public_key: &PublicKey,
) -> anyhow::Result<Option<Vec<u8>>> {
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

View File

@@ -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()))
}
}
}

View File

@@ -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

View File

@@ -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("/");

View File

@@ -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(())
}

View File

@@ -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<AppState>,
pubky: Pubky,
path: EntryPath,
cookies: Cookies,
body: Body,
) -> Result<impl IntoResponse> {
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<AppState>,
headers: HeaderMap,
pubky: Pubky,
path: EntryPath,
params: ListQueryParams,
) -> Result<impl IntoResponse> {
Ok(get_common(state, headers, pubky, path.as_str(), params).await?)
}
pub async fn get_subdomain(
State(state): State<AppState>,
headers: HeaderMap,
pubky: Pubky,
path: EntryPath,
params: ListQueryParams,
) -> Result<impl IntoResponse> {
Ok(get_common(
state,
headers,
pubky,
&format!("pub/{}", path.as_str()),
params,
)
.await?)
}
use super::verify;
pub async fn head(
State(state): State<AppState>,
headers: HeaderMap,
pubky: Pubky,
headers: HeaderMap,
path: EntryPath,
) -> Result<impl IntoResponse> {
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<AppState>,
headers: HeaderMap,
pubky: Pubky,
path: &str,
path: EntryPath,
params: ListQueryParams,
) -> Result<impl IntoResponse> {
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<AppState>,
pubky: Pubky,
path: EntryPath,
cookies: Cookies,
) -> Result<impl IntoResponse> {
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();

View File

@@ -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<AppState>,
pubky: Pubky,
path: EntryPath,
cookies: Cookies,
) -> Result<impl IntoResponse> {
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<AppState>,
pubky: Pubky,
path: EntryPath,
cookies: Cookies,
body: Body,
) -> Result<impl IntoResponse> {
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(())
}

View File

@@ -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<dyn reqwest::cookie::CookieStore + 'static>,
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()
}
}

View File

@@ -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<Client, std::io::Error> {
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,
})
}

View File

@@ -1,4 +1,3 @@
pub mod http;
pub mod recovery_file;
// TODO: put the Homeserver API behind a feature flag

View File

@@ -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<T: TryInto<Url>>(&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<T: TryInto<Url>>(&self, url: T) -> Result<()> {
self.inner_delete(url).await
}
/// Returns a [ListBuilder] to help pass options before calling [ListBuilder::send].
///
/// `url` sets the path you want to lest within.

View File

@@ -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<U: IntoUrl>(&self, url: U) -> RequestBuilder {
// self.request(Method::PUT, url)
// }
pub fn put<U: IntoUrl>(&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<U: IntoUrl>(&self, url: U) -> RequestBuilder {
// self.request(Method::DELETE, url)
// }
pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
self.request(Method::DELETE, url)
}
/// Convenience method to make a `HEAD` request to a URL.
///

View File

@@ -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<Session> {
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(())
}
}

View File

@@ -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<T: TryInto<Url>>(&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<T: TryInto<Url>>(&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<T: TryInto<Url>>(&self, url: T) -> Result<ListBuilder> {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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?;

View File

@@ -1,7 +1,7 @@
use reqwest::{Method, RequestBuilder};
use url::Url;
use pkarr::{EndpointResolver, PublicKey};
use pkarr::PublicKey;
use crate::{error::Result, Client};