From 453aa40d8827bc52d8e321dfe601cf7f7548a29b Mon Sep 17 00:00:00 2001 From: nazeh Date: Sat, 14 Dec 2024 16:37:58 +0300 Subject: [PATCH] fix(pubky): fix authz test with in process http relay --- Cargo.lock | 9 ++- pubky/Cargo.toml | 3 + pubky/src/native/api/auth.rs | 32 +++------ pubky/src/shared/auth.rs | 131 ++++++++++++++++++++++++++++++++--- 4 files changed, 140 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 515f6f1..d0284a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/pubky/Cargo.toml b/pubky/Cargo.toml index 6dc5193..8d4cf2a 100644 --- a/pubky/Cargo.toml +++ b/pubky/Cargo.toml @@ -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" diff --git a/pubky/src/native/api/auth.rs b/pubky/src/native/api/auth.rs index 8a9965f..72357c4 100644 --- a/pubky/src/native/api/auth.rs +++ b/pubky/src/native/api/auth.rs @@ -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( &self, - relay: impl TryInto, + relay: T, capabilities: &Capabilities, - ) -> Result<(Url, tokio::sync::oneshot::Receiver)> { - let mut relay: Url = relay - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid relay Url"))?; + ) -> Result<(Url, tokio::sync::oneshot::Receiver>)> { + let mut relay: Url = relay.into_url()?; let (pubkyauth_url, client_secret) = self.create_auth_request(&mut relay, capabilities)?; - let (tx, rx) = oneshot::channel::(); + let (tx, rx) = oneshot::channel::>(); 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>( + pub async fn send_auth_token( &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 } } diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 2484729..06b250a 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -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( &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 = 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 { + 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, Arc)>>>; + let shared_state: SharedState = Arc::new(Mutex::new(HashMap::new())); + + // Handler for the GET endpoint + async fn subscribe( + Path(id): Path, + State(state): State, + ) -> 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, + State(state): State, + 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);