mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
stream-cap rates responses instead of buffering then checking size
resp.bytes() already pulls the whole body into memory before the 5MB check ran, so a malicious server could still OOM the process during download. read_body_capped aborts mid-stream once the cap is hit. also fix goal-seek probe fallback: it could update lo/hi to a genuine bracket without ever setting found=true, throwing "no solution" on cases that do have one.
This commit is contained in:
parent
509926bcef
commit
b285f546c2
4 changed files with 60 additions and 35 deletions
18
src-tauri/Cargo.lock
generated
18
src-tauri/Cargo.lock
generated
|
|
@ -3038,6 +3038,7 @@ dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
|
@ -3057,12 +3058,14 @@ dependencies = [
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams 0.4.2",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
@ -3102,7 +3105,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams 0.5.0",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4924,6 +4927,19 @@ dependencies = [
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-streams"
|
name = "wasm-streams"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ tauri-plugin-updater = "2"
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,24 @@ async fn read_rates_cache(app: &AppHandle) -> Option<RatesCache> {
|
||||||
serde_json::from_str(&raw).ok()
|
serde_json::from_str(&raw).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RATES_RESPONSE_LIMIT: usize = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Reads the body as a stream and aborts as soon as it exceeds `limit`,
|
||||||
|
/// instead of buffering an unbounded body in memory before checking its size.
|
||||||
|
async fn read_body_capped(resp: reqwest::Response, limit: usize) -> Result<Vec<u8>, String> {
|
||||||
|
use futures::StreamExt;
|
||||||
|
let mut stream = resp.bytes_stream();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk.map_err(|e| e.to_string())?;
|
||||||
|
buf.extend_from_slice(&chunk);
|
||||||
|
if buf.len() > limit {
|
||||||
|
return Err("response too large".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
||||||
let client = HTTP_CLIENT.get_or_init(|| {
|
let client = HTTP_CLIENT.get_or_init(|| {
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
|
|
@ -334,10 +352,7 @@ async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
|
let bytes = read_body_capped(resp, RATES_RESPONSE_LIMIT).await?;
|
||||||
if bytes.len() > 5 * 1024 * 1024 {
|
|
||||||
return Err("rates api response too large".into());
|
|
||||||
}
|
|
||||||
let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?;
|
let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if body["result"] != "success" {
|
if body["result"] != "success" {
|
||||||
|
|
@ -352,8 +367,7 @@ async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
||||||
ids.join(",")
|
ids.join(",")
|
||||||
);
|
);
|
||||||
if let Ok(resp) = client.get(&url).send().await {
|
if let Ok(resp) = client.get(&url).send().await {
|
||||||
if let Ok(bytes) = resp.bytes().await {
|
if let Ok(bytes) = read_body_capped(resp, RATES_RESPONSE_LIMIT).await {
|
||||||
if bytes.len() <= 5 * 1024 * 1024 {
|
|
||||||
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||||||
for (code, id) in CRYPTO {
|
for (code, id) in CRYPTO {
|
||||||
if let Some(price) = json[id]["usd"].as_f64() {
|
if let Some(price) = json[id]["usd"].as_f64() {
|
||||||
|
|
@ -365,7 +379,6 @@ async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RatesPayload {
|
Ok(RatesPayload {
|
||||||
date: body["time_last_update_utc"]
|
date: body["time_last_update_utc"]
|
||||||
|
|
@ -558,10 +571,7 @@ async fn fetch_historical_rates(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
|
let bytes = read_body_capped(resp, RATES_RESPONSE_LIMIT).await?;
|
||||||
if bytes.len() > 5 * 1024 * 1024 {
|
|
||||||
return Err("rates api response too large".into());
|
|
||||||
}
|
|
||||||
let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?;
|
let body: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?;
|
||||||
let rates = parse_rate_map(body["rates"].as_object().ok_or("no rates in response")?);
|
let rates = parse_rate_map(body["rates"].as_object().ok_or("no rates in response")?);
|
||||||
if let Ok(serialized) = serde_json::to_string(&rates) {
|
if let Ok(serialized) = serde_json::to_string(&rates) {
|
||||||
|
|
@ -614,8 +624,7 @@ async fn fetch_historical_rates_batch(
|
||||||
futures.push(async move {
|
futures.push(async move {
|
||||||
let mut rate_map = None;
|
let mut rate_map = None;
|
||||||
if let Ok(resp) = client_clone.get(&url).send().await {
|
if let Ok(resp) = client_clone.get(&url).send().await {
|
||||||
if let Ok(bytes) = resp.bytes().await {
|
if let Ok(bytes) = read_body_capped(resp, RATES_RESPONSE_LIMIT).await {
|
||||||
if bytes.len() <= 5 * 1024 * 1024 {
|
|
||||||
if let Ok(body) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
if let Ok(body) = serde_json::from_slice::<serde_json::Value>(&bytes) {
|
||||||
if let Some(rates_obj) = body["rates"].as_object() {
|
if let Some(rates_obj) = body["rates"].as_object() {
|
||||||
let daily_rates = parse_rate_map(rates_obj);
|
let daily_rates = parse_rate_map(rates_obj);
|
||||||
|
|
@ -628,7 +637,6 @@ async fn fetch_historical_rates_batch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
rate_map
|
rate_map
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -424,12 +424,13 @@ function evalGoalSeek(lhsNode: Node, rhsNode: Node, ctx: EvalCtx): Value {
|
||||||
for (const p of probes) {
|
for (const p of probes) {
|
||||||
const pDec = new Decimal(p);
|
const pDec = new Decimal(p);
|
||||||
const fp = safeEval(pDec);
|
const fp = safeEval(pDec);
|
||||||
if (fp !== null) {
|
if (fp === null) continue;
|
||||||
if (flo === null) { lo = pDec; flo = fp; }
|
if (flo === null) { lo = pDec; flo = fp; }
|
||||||
else if (flo.mul(fp).lte(0)) { hi = pDec; fhi = fp; found = true; break; }
|
|
||||||
else if (fhi === null) { hi = pDec; fhi = fp; }
|
else if (fhi === null) { hi = pDec; fhi = fp; }
|
||||||
else if (fhi.mul(fp).lte(0)) { lo = pDec; flo = fp; found = true; break; }
|
else if (flo.mul(fp).lte(0)) { hi = pDec; fhi = fp; }
|
||||||
}
|
else if (fhi.mul(fp).lte(0)) { lo = pDec; flo = fp; }
|
||||||
|
else continue;
|
||||||
|
if (flo !== null && fhi !== null && flo.mul(fhi).lte(0)) { found = true; break; }
|
||||||
}
|
}
|
||||||
if (!found) throw new EvalError("no solution");
|
if (!found) throw new EvalError("no solution");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue