Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion payjoin-mailroom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions payjoin-mailroom/src/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -147,7 +147,7 @@ impl<D: Db> Service<D> {
}
}

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"]) =>
Expand All @@ -161,8 +161,6 @@ impl<D: Db> Service<D> {
}
.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)
}

Expand Down
14 changes: 11 additions & 3 deletions payjoin-mailroom/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -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;

Expand Down Expand Up @@ -378,14 +380,20 @@ 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)
.layer(
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);

Expand Down Expand Up @@ -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`)

@Mshehu5 Mshehu5 Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK
nit: Could we add a comment a like this next to the CorsLayer setup since OPTIONS is handled by CorsLayer not is_relay_re quest could be more would help

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little confuse on what you mean due to wordings. However cors isnt just for options request, it handles both origin in request and access-control* in response

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,
Expand Down
24 changes: 2 additions & 22 deletions payjoin-mailroom/src/ohttp_relay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}

Expand Down Expand Up @@ -255,21 +250,6 @@ fn parse_gateway_uri_from_path(path: &str, default: &GatewayUri) -> Result<Gatew
}
}

fn handle_preflight() -> Response<BoxBody<Bytes, hyper::Error>> {
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<BoxBody<Bytes, hyper::Error>> { Response::new(empty()) }

#[instrument]
Expand Down
Loading