diff --git a/payjoin-mailroom/Cargo.toml b/payjoin-mailroom/Cargo.toml index 8f3f92569..5538f9a31 100644 --- a/payjoin-mailroom/Cargo.toml +++ b/payjoin-mailroom/Cargo.toml @@ -78,7 +78,7 @@ tokio-rustls-acme = { version = "0.9.0", features = ["axum"], optional = true } tokio-stream = { version = "0.1.17", features = ["net"] } tokio-tungstenite = { version = "0.27.0", optional = true } tower = "0.5.2" -tower-http = { version = "0.6.11", features = ["trace"] } +tower-http = { version = "0.6.6", features = ["cors", "trace"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } unicode-segmentation = "=1.12.0" diff --git a/payjoin-mailroom/src/directory.rs b/payjoin-mailroom/src/directory.rs index 038cc4070..6f3301e77 100644 --- a/payjoin-mailroom/src/directory.rs +++ b/payjoin-mailroom/src/directory.rs @@ -5,7 +5,7 @@ use std::task::{Context, Poll}; use anyhow::Result; use axum::body::{Body, Bytes}; -use axum::http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}; +use axum::http::header::{HeaderValue, CONTENT_TYPE}; use axum::http::{Method, Request, Response, StatusCode, Uri}; use http_body_util::BodyExt; use payjoin::directory::{ShortId, ShortIdError, ENCAPSULATED_MESSAGE_BYTES}; @@ -147,7 +147,7 @@ impl Service { } } - let mut response = match (parts.method, path_segments.as_slice()) { + let response = match (parts.method, path_segments.as_slice()) { (Method::POST, ["", ".well-known", "ohttp-gateway"]) => self.handle_ohttp_gateway(body).await, (Method::GET, ["", ".well-known", "ohttp-gateway"]) => @@ -161,8 +161,6 @@ impl Service { } .unwrap_or_else(|e| e.to_response()); - // Allow CORS for third-party access - response.headers_mut().insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); Ok(response) } diff --git a/payjoin-mailroom/src/lib.rs b/payjoin-mailroom/src/lib.rs index fb31e0a96..7b3745dba 100644 --- a/payjoin-mailroom/src/lib.rs +++ b/payjoin-mailroom/src/lib.rs @@ -1,6 +1,7 @@ #[cfg(feature = "access-control")] use axum::extract::connect_info::Connected; use axum::extract::State; +use axum::http::header::{CONTENT_LENGTH, CONTENT_TYPE}; use axum::http::Method; use axum::response::{IntoResponse, Response}; #[cfg(feature = "access-control")] @@ -11,6 +12,7 @@ use opentelemetry_sdk::metrics::SdkMeterProvider; use rand::Rng; use tokio_listener::{Listener, SystemOptions, UserOptions}; use tower::{Service, ServiceBuilder}; +use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tracing::info; @@ -378,6 +380,11 @@ fn build_app(services: Services) -> Router { #[cfg(feature = "access-control")] let geoip = services.geoip.clone(); + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods([Method::CONNECT, Method::GET, Method::OPTIONS, Method::POST]) + .allow_headers([CONTENT_TYPE, CONTENT_LENGTH]); + #[allow(unused_mut)] let mut router = Router::new() .fallback(route_request) @@ -385,7 +392,8 @@ fn build_app(services: Services) -> Router { ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(axum::middleware::from_fn_with_state(metrics.clone(), track_metrics)) - .layer(axum::middleware::from_fn_with_state(metrics, track_connections)), + .layer(axum::middleware::from_fn_with_state(metrics, track_connections)) + .layer(cors), ) .with_state(services); @@ -421,17 +429,17 @@ async fn route_request( /// Determines if a request should be routed to the OHTTP relay service. /// /// Routing rules: -/// - `(OPTIONS, _)` => CORS preflight handling /// - `(CONNECT, _)` => OHTTP bootstrap tunneling /// - `(POST, "/")` => relay to default gateway (needed for backwards-compatibility only) /// - `(POST, /http(s)://...)` => RFC 9540 opt-in gateway specified in path /// - `(GET, /http(s)://...)` => OHTTP bootstrap via WebSocket with opt-in gateway +/// - `(OPTIONS, _)` => CORS preflight handling (handled by `tower_http::cors::CorsLayer`) fn is_relay_request(req: &axum::extract::Request) -> bool { let method = req.method(); let path = req.uri().path(); match (method, path) { - (&Method::OPTIONS, _) | (&Method::CONNECT, _) | (&Method::POST, "/") => true, + (&Method::CONNECT, _) | (&Method::POST, "/") => true, (&Method::POST, p) | (&Method::GET, p) if p.starts_with("/http://") || p.starts_with("/https://") => true, diff --git a/payjoin-mailroom/src/ohttp_relay/mod.rs b/payjoin-mailroom/src/ohttp_relay/mod.rs index 30b996172..f997cd986 100644 --- a/payjoin-mailroom/src/ohttp_relay/mod.rs +++ b/payjoin-mailroom/src/ohttp_relay/mod.rs @@ -10,10 +10,7 @@ use http::uri::Authority; use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Empty, Full}; use hyper::body::{Bytes, Incoming}; -use hyper::header::{ - HeaderValue, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, - ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_LENGTH, CONTENT_TYPE, -}; +use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; use hyper::{Method, Request, Response}; use hyper_rustls::builderstates::WantsSchemes; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; @@ -182,8 +179,7 @@ where let path = req.uri().path(); let authority = req.uri().authority().cloned(); - let mut res = match (&method, path) { - (&Method::OPTIONS, _) => Ok(handle_preflight()), + let res = match (&method, path) { (&Method::GET, "/health") => Ok(health_check().await), (&Method::POST, _) => match parse_gateway_uri(&method, path, authority, config).await { Ok(gateway_uri) => handle_ohttp_relay(req, config, gateway_uri).await, @@ -206,7 +202,6 @@ where _ => Err(Error::NotFound), } .unwrap_or_else(|e| e.to_response()); - res.headers_mut().insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); Ok(res) } @@ -255,21 +250,6 @@ fn parse_gateway_uri_from_path(path: &str, default: &GatewayUri) -> Result Response> { - let mut res = Response::new(empty()); - *res.status_mut() = hyper::StatusCode::NO_CONTENT; - res.headers_mut().insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); - res.headers_mut().insert( - ACCESS_CONTROL_ALLOW_METHODS, - HeaderValue::from_static("CONNECT, GET, OPTIONS, POST"), - ); - res.headers_mut().insert( - ACCESS_CONTROL_ALLOW_HEADERS, - HeaderValue::from_static("Content-Type, Content-Length"), - ); - res -} - async fn health_check() -> Response> { Response::new(empty()) } #[instrument]