mirror of
https://github.com/aljazceru/pubky-core.git
synced 2025-12-31 21:04:34 +01:00
feat(pubky): update HEAD, GET, PUT, and DELETE for pub/ endpoints to use hostnames
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
|
||||
52
pubky-homeserver/src/routes/public/mod.rs
Normal file
52
pubky-homeserver/src/routes/public/mod.rs
Normal 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(())
|
||||
}
|
||||
@@ -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();
|
||||
63
pubky-homeserver/src/routes/public/write.rs
Normal file
63
pubky-homeserver/src/routes/public/write.rs
Normal 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(())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod http;
|
||||
pub mod recovery_file;
|
||||
|
||||
// TODO: put the Homeserver API behind a feature flag
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use reqwest::{Method, RequestBuilder};
|
||||
use url::Url;
|
||||
|
||||
use pkarr::{EndpointResolver, PublicKey};
|
||||
use pkarr::PublicKey;
|
||||
|
||||
use crate::{error::Result, Client};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user