fix(pubky): fix authz test with in process http relay

This commit is contained in:
nazeh
2024-12-14 16:37:58 +03:00
parent 8a799a27e3
commit 453aa40d88
4 changed files with 140 additions and 35 deletions

9
Cargo.lock generated
View File

@@ -1575,7 +1575,7 @@ checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
[[package]]
name = "mainline"
version = "4.1.0"
source = "git+https://github.com/pubky/mainline?branch=v5#2948896ac074cd5bf9728966dcd076de35e7e763"
source = "git+https://github.com/pubky/mainline?branch=v5#c5e02270223bfa49a791a627a54348e0494762e5"
dependencies = [
"bytes",
"crc",
@@ -1921,7 +1921,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkarr"
version = "3.0.0"
source = "git+https://github.com/Pubky/pkarr?branch=v3#c3510a82c5a3a496349d8dc030edd96f750f1471"
source = "git+https://github.com/Pubky/pkarr?branch=v3#6821166474cd567a1e11f39e335b42c0029d4063"
dependencies = [
"base32",
"byteorder",
@@ -1958,7 +1958,7 @@ dependencies = [
[[package]]
name = "pkarr-relay"
version = "0.1.0"
source = "git+https://github.com/Pubky/pkarr?branch=v3#c3510a82c5a3a496349d8dc030edd96f750f1471"
source = "git+https://github.com/Pubky/pkarr?branch=v3#6821166474cd567a1e11f39e335b42c0029d4063"
dependencies = [
"anyhow",
"axum",
@@ -2063,11 +2063,14 @@ name = "pubky"
version = "0.3.0"
dependencies = [
"anyhow",
"axum",
"axum-server",
"base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
"futures-lite",
"futures-util",
"js-sys",
"pkarr",
"pubky-common",

View File

@@ -41,6 +41,9 @@ web-sys = "0.3.76"
[dev-dependencies]
anyhow = "1.0.94"
axum = "0.7.9"
axum-server = "0.7.1"
futures-util = "0.3.31"
pubky-homeserver = { path = "../pubky-homeserver" }
tokio = "1.42.0"

View File

@@ -1,5 +1,6 @@
use pkarr::Keypair;
use pubky_common::session::Session;
use reqwest::IntoUrl;
use tokio::sync::oneshot;
use url::Url;
@@ -40,30 +41,21 @@ impl Client {
/// Return `pubkyauth://` url and wait for the incoming [AuthToken]
/// verifying that AuthToken, and if capabilities were requested, signing in to
/// the Pubky's homeserver and returning the [Session] information.
pub fn auth_request(
pub fn auth_request<T: IntoUrl>(
&self,
relay: impl TryInto<Url>,
relay: T,
capabilities: &Capabilities,
) -> Result<(Url, tokio::sync::oneshot::Receiver<PublicKey>)> {
let mut relay: Url = relay
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid relay Url"))?;
) -> Result<(Url, tokio::sync::oneshot::Receiver<Result<PublicKey>>)> {
let mut relay: Url = relay.into_url()?;
let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?;
let (tx, rx) = oneshot::channel::<PublicKey>();
let (tx, rx) = oneshot::channel::<Result<PublicKey>>();
let this = self.clone();
tokio::spawn(async move {
let to_send = this
.subscribe_to_auth_response(relay, &client_secret)
.await?;
tx.send(to_send)
.map_err(|_| anyhow::anyhow!("Failed to send the session after signing in with token, since the receiver is dropped"))?;
Ok::<(), anyhow::Error>(())
tx.send(this.subscribe_to_auth_response(relay, &client_secret).await)
});
Ok((pubkyauth_url, rx))
@@ -71,17 +63,11 @@ impl Client {
/// Sign an [pubky_common::auth::AuthToken], encrypt it and send it to the
/// source of the pubkyauth request url.
pub async fn send_auth_token<T: TryInto<Url>>(
pub async fn send_auth_token<T: IntoUrl>(
&self,
keypair: &Keypair,
pubkyauth_url: T,
) -> Result<()> {
let url: Url = pubkyauth_url
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid Url"))?;
self.inner_send_auth_token(keypair, url).await?;
Ok(())
self.inner_send_auth_token(keypair, pubkyauth_url).await
}
}

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use base64::{alphabet::URL_SAFE, engine::general_purpose::NO_PAD, Engine};
use reqwest::{Method, StatusCode};
use reqwest::{IntoUrl, Method, StatusCode};
use url::Url;
use pkarr::{Keypair, PublicKey};
@@ -90,11 +90,18 @@ impl Client {
self.signin_with_authtoken(&token).await
}
pub(crate) async fn inner_send_auth_token(
pub(crate) async fn inner_send_auth_token<T: IntoUrl>(
&self,
keypair: &Keypair,
pubkyauth_url: Url,
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();
@@ -130,14 +137,14 @@ impl Client {
let engine = base64::engine::GeneralPurpose::new(&URL_SAFE, NO_PAD);
let mut callback = relay.clone();
let mut path_segments = callback.path_segments_mut().unwrap();
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);
self.inner_request(Method::POST, callback)
self.inner_request(Method::POST, callback_url)
.await
.body(encrypted_token)
.send()
@@ -196,7 +203,8 @@ impl Client {
// 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)?;
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() {
@@ -210,6 +218,8 @@ impl Client {
#[cfg(test)]
mod tests {
use std::net::SocketAddr;
use crate::*;
use pkarr::{mainline::Testnet, Keypair};
@@ -258,11 +268,107 @@ mod tests {
}
}
async fn http_relay_server() -> Option<SocketAddr> {
use axum::{
body::{Body, Bytes},
extract::{Path, State},
response::IntoResponse,
routing::get,
Router,
};
use axum_server::Handle;
use futures_util::stream::StreamExt;
use std::{
collections::HashMap,
net::SocketAddr,
sync::{Arc, Mutex},
};
use tokio::sync::Notify;
// Shared state to store GET requests and their notifications
type SharedState = Arc<Mutex<HashMap<String, (Vec<u8>, Arc<Notify>)>>>;
let shared_state: SharedState = Arc::new(Mutex::new(HashMap::new()));
// Handler for the GET endpoint
async fn subscribe(
Path(id): Path<String>,
State(state): State<SharedState>,
) -> impl IntoResponse {
// Create a notification for this ID
let notify = Arc::new(Notify::new());
{
let mut map = state.lock().unwrap();
// Store the notification and return it when POST arrives
map.entry(id.clone())
.or_insert_with(|| (vec![], notify.clone()));
}
notify.notified().await;
// Respond with the data stored for this ID
let map = state.lock().unwrap();
if let Some((data, _)) = map.get(&id) {
Bytes::from(data.clone()).into_response()
} else {
(axum::http::StatusCode::NOT_FOUND, "Not Found").into_response()
}
}
// Handler for the POST endpoint
async fn publish(
Path(id): Path<String>,
State(state): State<SharedState>,
body: Body,
) -> impl IntoResponse {
// Aggregate the body into bytes
let mut stream = body.into_data_stream();
let mut bytes = vec![];
while let Some(next) = stream.next().await {
let chunk = next.map_err(|e| e.to_string()).unwrap();
bytes.extend_from_slice(&chunk);
}
// Notify any waiting GET request for this ID
let mut map = state.lock().unwrap();
if let Some((storage, notify)) = map.get_mut(&id) {
*storage = bytes;
notify.notify_one();
Ok(())
} else {
Err((
axum::http::StatusCode::NOT_FOUND,
"No waiting GET request for this ID",
))
}
}
let app = Router::new()
.route("/:id", get(subscribe).post(publish))
.with_state(shared_state);
let handle = Handle::new();
let cloned = handle.clone();
tokio::spawn(async {
axum_server::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
.handle(cloned)
.serve(app.into_make_service())
.await
.unwrap();
});
handle.listening().await
}
#[tokio::test]
async fn authz() {
let testnet = Testnet::new(10).unwrap();
let server = Homeserver::start_test(&testnet).await.unwrap();
let http_relay_url = http_relay_server().await.unwrap();
let keypair = Keypair::random();
let pubky = keypair.public_key();
@@ -270,8 +376,12 @@ mod tests {
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("https://demo.httprelay.io/link", &capabilities)
.auth_request(
&format!("http://{}", http_relay_url.to_string()),
&capabilities,
)
.unwrap();
// Authenticator side
@@ -286,7 +396,10 @@ mod tests {
.unwrap();
}
let public_key = pubkyauth_response.await.unwrap();
let public_key = pubkyauth_response
.await
.expect("sender to not be dropped")
.unwrap();
assert_eq!(&public_key, &pubky);