Skip to content

Commit ffbb9af

Browse files
authored
Merge pull request #151 from freeinternet865/fix/range-parallel-total-cap
fix(relay): cap synthetic range stitching size
2 parents f6fcbff + c915477 commit ffbb9af

1 file changed

Lines changed: 40 additions & 6 deletions

File tree

src/domain_fronter.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ type PooledStream = TlsStream<TcpStream>;
5959
const POOL_TTL_SECS: u64 = 45;
6060
const POOL_MAX: usize = 80;
6161
const REQUEST_TIMEOUT_SECS: u64 = 25;
62+
const RANGE_PARALLEL_CHUNK_BYTES: u64 = 256 * 1024;
63+
// Keep synthetic range stitching bounded. Without this, a buggy or hostile
64+
// origin can advertise `Content-Range: bytes 0-1/<huge>` and make us build a
65+
// massive range plan or preallocate an enormous response buffer.
66+
const MAX_STITCHED_RANGE_BYTES: u64 = 64 * 1024 * 1024;
6267

6368
struct PoolEntry {
6469
stream: PooledStream,
@@ -648,8 +653,8 @@ impl DomainFronter {
648653
headers: &[(String, String)],
649654
body: &[u8],
650655
) -> Vec<u8> {
651-
const CHUNK: u64 = 256 * 1024;
652656
const MAX_PARALLEL: usize = 16;
657+
let chunk = RANGE_PARALLEL_CHUNK_BYTES;
653658

654659
if method != "GET" || !body.is_empty() {
655660
return self.relay(method, url, headers, body).await;
@@ -662,7 +667,7 @@ impl DomainFronter {
662667

663668
// Probe with the first chunk.
664669
let mut probe_headers: Vec<(String, String)> = headers.to_vec();
665-
probe_headers.push(("Range".into(), format!("bytes=0-{}", CHUNK - 1)));
670+
probe_headers.push(("Range".into(), format!("bytes=0-{}", chunk - 1)));
666671
let first = self.relay(method, url, &probe_headers, body).await;
667672

668673
let (status, resp_headers, resp_body) = match split_response(&first) {
@@ -676,7 +681,7 @@ impl DomainFronter {
676681
return first;
677682
}
678683

679-
let probe_range = match validate_probe_range(status, &resp_headers, resp_body, CHUNK - 1)
684+
let probe_range = match validate_probe_range(status, &resp_headers, resp_body, chunk - 1)
680685
{
681686
Some(r) => r,
682687
None => {
@@ -689,15 +694,27 @@ impl DomainFronter {
689694
};
690695
let total = probe_range.total;
691696

692-
if total <= CHUNK || (probe_range.end + 1) >= total {
697+
if total <= chunk || (probe_range.end + 1) >= total {
693698
return rewrite_206_to_200(&first);
694699
}
695700

701+
let total_usize = match checked_stitched_range_capacity(total) {
702+
Some(v) => v,
703+
None => {
704+
tracing::warn!(
705+
"range-parallel: Content-Range total {} for {} is too large; falling back to single GET",
706+
total,
707+
url,
708+
);
709+
return self.relay(method, url, headers, body).await;
710+
}
711+
};
712+
696713
// Plan remaining ranges after what the probe already returned.
697714
let mut ranges: Vec<(u64, u64)> = Vec::new();
698715
let mut start = probe_range.end + 1;
699716
while start < total {
700-
let end = (start + CHUNK - 1).min(total - 1);
717+
let end = (start + chunk - 1).min(total - 1);
701718
ranges.push((start, end));
702719
start = end + 1;
703720
}
@@ -734,7 +751,7 @@ impl DomainFronter {
734751
.await;
735752

736753
// Stitch: probe body first, then the chunks in order.
737-
let mut full = Vec::with_capacity(total as usize);
754+
let mut full = Vec::with_capacity(total_usize);
738755
full.extend_from_slice(resp_body);
739756
for (start, end, chunk) in fetches {
740757
match chunk {
@@ -1374,6 +1391,13 @@ fn validate_probe_range(
13741391
Some(range)
13751392
}
13761393

1394+
fn checked_stitched_range_capacity(total: u64) -> Option<usize> {
1395+
if total > MAX_STITCHED_RANGE_BYTES {
1396+
return None;
1397+
}
1398+
usize::try_from(total).ok()
1399+
}
1400+
13771401
fn extract_exact_range_body(
13781402
raw: &[u8],
13791403
start: u64,
@@ -2402,6 +2426,16 @@ mod tests {
24022426
assert!(validate_probe_range(206, &headers, b"hey", 4).is_none());
24032427
}
24042428

2429+
#[test]
2430+
fn stitched_range_capacity_rejects_absurd_total() {
2431+
assert_eq!(
2432+
checked_stitched_range_capacity(MAX_STITCHED_RANGE_BYTES),
2433+
Some(MAX_STITCHED_RANGE_BYTES as usize),
2434+
);
2435+
assert_eq!(checked_stitched_range_capacity(MAX_STITCHED_RANGE_BYTES + 1), None);
2436+
assert_eq!(checked_stitched_range_capacity(u64::MAX), None);
2437+
}
2438+
24052439
#[test]
24062440
fn extract_exact_range_body_rejects_mismatched_content_range() {
24072441
let raw = b"HTTP/1.1 206 Partial Content\r\n\

0 commit comments

Comments
 (0)