From f8abb85e9e6d92fb0e2f441f17361fcab6aa3eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 14 Mar 2025 17:50:57 +0100 Subject: [PATCH] refactor(oidc_cli): Use LocalServerBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- Cargo.lock | 90 ++--------------------- examples/oidc_cli/Cargo.toml | 4 +- examples/oidc_cli/src/main.rs | 134 +++++++--------------------------- 3 files changed, 32 insertions(+), 196 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdfe571e3..20d5411b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,47 +377,13 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "axum" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ - "axum-core 0.5.0", + "axum-core", "bytes", "form_urlencoded", "futures-util", @@ -427,7 +393,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -439,28 +405,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", + "tower", "tower-layer", "tower-service", "tracing", @@ -1601,7 +1546,6 @@ name = "example-oidc-cli" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.9", "dirs 5.0.1", "futures-util", "matrix-sdk", @@ -1610,7 +1554,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tower 0.4.13", "tracing-subscriber", "url", ] @@ -2871,12 +2814,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -2920,7 +2857,7 @@ dependencies = [ "async-channel", "async-stream", "async-trait", - "axum 0.8.1", + "axum", "backoff", "bytes", "bytesize", @@ -2968,7 +2905,7 @@ dependencies = [ "tokio-stream", "tokio-test", "tokio-util", - "tower 0.5.2", + "tower", "tracing", "tracing-subscriber", "uniffi", @@ -4380,7 +4317,7 @@ dependencies = [ "tokio-rustls", "tokio-socks", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -5454,21 +5391,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" diff --git a/examples/oidc_cli/Cargo.toml b/examples/oidc_cli/Cargo.toml index 80d45d4c3..8380da1ab 100644 --- a/examples/oidc_cli/Cargo.toml +++ b/examples/oidc_cli/Cargo.toml @@ -11,7 +11,6 @@ test = false [dependencies] anyhow = { workspace = true } -axum = "0.7.4" dirs = "5.0.1" futures-util = { workspace = true } matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" } @@ -19,13 +18,12 @@ rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tower = { version = "0.4.13", features = ["util"] } tracing-subscriber = { workspace = true } url = { workspace = true } [dependencies.matrix-sdk] path = "../../crates/matrix-sdk" -features = ["experimental-oidc"] +features = ["experimental-oidc", "local-server"] [lints] workspace = true diff --git a/examples/oidc_cli/src/main.rs b/examples/oidc_cli/src/main.rs index 930cf566a..e2443de41 100644 --- a/examples/oidc_cli/src/main.rs +++ b/examples/oidc_cli/src/main.rs @@ -13,20 +13,13 @@ // limitations under the License. use std::{ - convert::Infallible, - future::IntoFuture, io::{self, Write}, - ops::Range, + net::{Ipv4Addr, Ipv6Addr}, path::{Path, PathBuf}, - sync::{Arc, Mutex}, + sync::Arc, }; use anyhow::{anyhow, bail}; -use axum::{ - http::{Method, Request, StatusCode}, - response::IntoResponse, - routing::any_service, -}; use futures_util::StreamExt; use matrix_sdk::{ authentication::oidc::{ @@ -42,13 +35,13 @@ use matrix_sdk::{ events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, serde::Raw, }, + utils::local_server::{LocalServerBuilder, LocalServerRedirectHandle}, Client, ClientBuildError, Result, RoomState, }; use matrix_sdk_ui::sync_service::SyncService; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; -use tokio::{fs, io::AsyncBufReadExt as _, net::TcpListener, sync::oneshot}; -use tower::service_fn; +use tokio::{fs, io::AsyncBufReadExt as _}; use url::Url; /// A command-line tool to demonstrate the steps requiring an interaction with @@ -224,12 +217,12 @@ impl OidcCli { // Here we spawn a server to listen on the loopback interface. Another option // would be to register a custom URI scheme with the system and handle // the redirect when the custom URI scheme is opened. - let (redirect_uri, data_rx, signal_tx) = spawn_local_server().await?; + let (redirect_uri, server_handle) = LocalServerBuilder::new().spawn().await?; let OidcAuthorizationData { url, state } = oidc.login(redirect_uri, None)?.build().await?; - let authorization_code = match use_auth_url(&url, &state, data_rx, signal_tx).await { + let authorization_code = match use_auth_url(&url, &state, server_handle).await { Ok(code) => code, Err(err) => { oidc.abort_authorization(&state).await; @@ -707,10 +700,13 @@ async fn build_client(data_dir: &Path) -> anyhow::Result<(Client, ClientSession) /// possible, for example by using the most secure signing algorithms supported /// by the provider. fn client_metadata() -> Raw { - // Native clients should be able to register the loopback interface and then - // point to any port when needing a redirect URI. An alternative is to use a - // custom URI scheme registered with the OS. - let redirect_uri = Url::parse("http://127.0.0.1").expect("Couldn't parse redirect URI"); + // Native clients should be able to register the IPv4 and IPv6 loopback + // interfaces and then point to any port when needing a redirect URI. An + // alternative is to use a custom URI scheme registered with the OS. + let ipv4_localhost_uri = Url::parse(&format!("http://{}/", Ipv4Addr::LOCALHOST)) + .expect("Couldn't parse IPv4 redirect URI"); + let ipv6_localhost_uri = Url::parse(&format!("http://[{}]/", Ipv6Addr::LOCALHOST)) + .expect("Couldn't parse IPv6 redirect URI"); let client_uri = Localized::new( Url::parse("https://github.com/matrix-org/matrix-rust-sdk") .expect("Couldn't parse client URI"), @@ -730,7 +726,9 @@ fn client_metadata() -> Raw { // browser). ApplicationType::Native, // We are going to use the Authorization Code flow. - vec![OauthGrantType::AuthorizationCode { redirect_uris: vec![redirect_uri] }], + vec![OauthGrantType::AuthorizationCode { + redirect_uris: vec![ipv4_localhost_uri, ipv6_localhost_uri], + }], client_uri, ) }; @@ -744,21 +742,20 @@ fn client_metadata() -> Raw { async fn use_auth_url( url: &Url, state: &CsrfToken, - data_rx: oneshot::Receiver, - signal_tx: oneshot::Sender<()>, + server_handle: LocalServerRedirectHandle, ) -> anyhow::Result { println!("\nPlease authenticate yourself at: {url}\n"); println!("Then proceed to the authorization.\n"); - let response_query = data_rx.await?; - signal_tx.send(()).expect("Receiver is still alive"); + let response_query = server_handle.await; - let code = match AuthorizationResponse::parse_query(&response_query)? { - AuthorizationResponse::Success(code) => code, - AuthorizationResponse::Error(err) => { - return Err(anyhow!(err.error)); - } - }; + let code = + match AuthorizationResponse::parse_query(response_query.as_deref().unwrap_or_default())? { + AuthorizationResponse::Success(code) => code, + AuthorizationResponse::Error(err) => { + return Err(anyhow!(err.error)); + } + }; // Here we only manage one authorization at a time so, if the state string is // wrong, it is an error. Some clients might want to allow several @@ -771,87 +768,6 @@ async fn use_auth_url( Ok(code) } -/// Spawn a local server to listen on redirects at the end of the authorization -/// process. -/// -/// Returns the URL the server listens to, the receiver that will receive the -/// data returned by the provider and a sender to shutdown the server. -async fn spawn_local_server( -) -> anyhow::Result<(Url, oneshot::Receiver, oneshot::Sender<()>)> { - /// The range of ports the SSO server will try to bind to randomly. - /// - /// This is used to avoid binding to a port blocked by browsers. - /// See . - const SSO_SERVER_BIND_RANGE: Range = 20000..30000; - /// The number of times the SSO server will try to bind to a random port - const SSO_SERVER_BIND_TRIES: u8 = 10; - - // The channel used to shutdown the server when we are done with it. - let (signal_tx, signal_rx) = oneshot::channel::<()>(); - // The channel used to transmit the data received a the redirect URL. - let (data_tx, data_rx) = oneshot::channel::(); - let data_tx_mutex = Arc::new(Mutex::new(Some(data_tx))); - - // We bind to the IPv4 loopback interface. - let mut redirect_url = Url::parse("http://127.0.0.1:0/") - .expect("Couldn't parse good known loopback interface URL"); - - // Bind a TCP listener to a random port. - let listener = { - let host = redirect_url.host_str().expect("The redirect URL doesn't have a host"); - let mut n = 0u8; - - loop { - let port = thread_rng().gen_range(SSO_SERVER_BIND_RANGE); - match TcpListener::bind((host, port)).await { - Ok(l) => { - redirect_url - .set_port(Some(port)) - .expect("Could not set new port on redirect URL"); - break l; - } - Err(_) if n < SSO_SERVER_BIND_TRIES => { - n += 1; - } - Err(e) => { - return Err(e.into()); - } - } - } - }; - - // Set up the server. - let router = any_service(service_fn(move |request: Request<_>| { - let data_tx_mutex = data_tx_mutex.clone(); - async move { - // Reject methods others than HEAD or GET. - if request.method() != Method::HEAD && request.method() != Method::GET { - return Ok::<_, Infallible>(StatusCode::METHOD_NOT_ALLOWED.into_response()); - } - - // We only need to get the first response so we consume the transmitter the - // first time. - if let Some(data_tx) = data_tx_mutex.lock().unwrap().take() { - let query_string = request.uri().query().unwrap_or_default(); - - data_tx.send(query_string.to_owned()).expect("The receiver is still alive"); - } - - Ok("The authorization step is complete. You can close this page and go back to the oidc-cli.".into_response()) - } - })); - - let server = axum::serve(listener, router) - .with_graceful_shutdown(async { - signal_rx.await.ok(); - }) - .into_future(); - - tokio::spawn(server); - - Ok((redirect_url, data_rx, signal_tx)) -} - /// Handle room messages. async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) { // We only want to log text messages in joined rooms.