Skip to content

Commit 501c79c

Browse files
committed
V1 body limit
This pr addresses #941, it implements a body size limit layer to reject v1 request whose body is greater than 7168 byts. The handler for direct v1 sender request has no size check and calls body.collect() irrespective of payload size. The limiter prevents this behaviour. the direct v1 call was also extracted into its own router.
1 parent 27cc8a1 commit 501c79c

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

payjoin-mailroom/src/directory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CHACHA20_POLY1305_NONCE_LEN: usize = 32; // chacha20poly1305 n_k
1818
const POLY1305_TAG_SIZE: usize = 16;
1919
pub const BHTTP_REQ_BYTES: usize =
2020
ENCAPSULATED_MESSAGE_BYTES - (CHACHA20_POLY1305_NONCE_LEN + POLY1305_TAG_SIZE);
21-
const V1_MAX_BUFFER_SIZE: usize = 65536;
21+
pub(crate) const V1_MAX_BUFFER_SIZE: usize = 7168;
2222

2323
const V1_REJECT_RES_JSON: &str =
2424
r#"{{"errorCode": "original-psbt-rejected ", "message": "Body is not a string"}}"#;

payjoin-mailroom/src/lib.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use tokio_listener::{Listener, SystemOptions, UserOptions};
1313
use tower::{Service, ServiceBuilder};
1414
use tracing::info;
1515

16+
use crate::directory::V1_MAX_BUFFER_SIZE;
1617
use crate::ohttp_relay::SentinelTag;
1718

1819
#[cfg(feature = "access-control")]
@@ -354,8 +355,12 @@ fn build_app(services: Services) -> Router {
354355
#[cfg(feature = "access-control")]
355356
let geoip = services.geoip.clone();
356357

358+
let v1_method_router = axum::routing::post(v1_post_handler).fallback(route_request);
359+
let v1_router = Router::new().route("/{id}", v1_method_router);
360+
357361
#[allow(unused_mut)]
358362
let mut router = Router::new()
363+
.merge(v1_router)
359364
.fallback(route_request)
360365
.layer(
361366
ServiceBuilder::new()
@@ -374,6 +379,19 @@ fn build_app(services: Services) -> Router {
374379
router
375380
}
376381

382+
async fn v1_post_handler(
383+
State(mut services): State<Services>,
384+
req: axum::extract::Request,
385+
) -> Response {
386+
let (parts, body) = req.into_parts();
387+
let limited = http_body_util::Limited::new(body, V1_MAX_BUFFER_SIZE);
388+
let req = axum::http::Request::from_parts(parts, axum::body::Body::new(limited));
389+
match services.directory.call(req).await {
390+
Ok(res) => res.into_response(),
391+
Err(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
392+
}
393+
}
394+
377395
async fn route_request(
378396
State(mut services): State<Services>,
379397
req: axum::extract::Request,
@@ -565,4 +583,45 @@ mod tests {
565583
assert!(metric_names.contains(&TOTAL_CONNECTIONS), "missing total_connections");
566584
assert!(metric_names.contains(&ACTIVE_CONNECTIONS), "missing active_connections");
567585
}
586+
587+
#[tokio::test]
588+
async fn v1_post_rejects_oversized_body() {
589+
use axum::body::Body;
590+
use axum::http::Request;
591+
use payjoin::directory::ShortId;
592+
use tower::ServiceExt;
593+
594+
let tempdir = tempdir().unwrap();
595+
let config = Config::new(
596+
"[::]:0".parse().expect("valid listener address"),
597+
tempdir.path().to_path_buf(),
598+
Duration::from_secs(2),
599+
Some(crate::config::V1Config::default()),
600+
);
601+
602+
let sentinel_tag = generate_sentinel_tag();
603+
let services = Services {
604+
directory: init_directory(&config, sentinel_tag).await.unwrap(),
605+
relay: crate::ohttp_relay::Service::new(sentinel_tag).await,
606+
metrics: MetricsService::new(None),
607+
#[cfg(feature = "access-control")]
608+
geoip: None,
609+
};
610+
611+
let app = build_app(services);
612+
613+
let id = ShortId([0u8; 8]).to_string();
614+
let oversized = vec![b'a'; V1_MAX_BUFFER_SIZE + 1];
615+
let request = Request::builder()
616+
.method("POST")
617+
.uri(format!("/{id}"))
618+
.body(Body::from(oversized))
619+
.unwrap();
620+
let response = ServiceExt::<Request<Body>>::oneshot(app, request).await.unwrap();
621+
assert_eq!(
622+
response.status(),
623+
axum::http::StatusCode::BAD_REQUEST,
624+
"oversized v1 POST body should be rejected"
625+
);
626+
}
568627
}

0 commit comments

Comments
 (0)