mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
The fleet SNATs to one IP, so nothing on the OTA path may key on IP. - /api/update/check: EARLY-RETURN before any filesystem call when the breaker won't offer (rate-backoff / up-to-date / phantom / client-newer). A looping client that gets rate-backoff now does ZERO fs — the flood can't become a statSync flood. - lib/apk-cache.js: resolve APK path/size/mtime once at boot + refresh on an interval; the check/download endpoints read cached metadata (get() does no fs, proven by test). - lib/ota-download-guard.js + /download/apk: GLOBAL concurrency + rate caps + critical- band shed (503 Retry-After), NEVER per-IP. Replaces the per-IP-per-10min log throttle (which hid the flood under SNAT) with a per-window served/shed aggregate so a download flood is VISIBLE. Bounded single rolling-state object; in-flight released on finish/close. - Breaker unchanged; no IP limiting or device_id requirement added (legacy field clients send no device_id on OTA checks — must keep working). Tests: apk-cache get() = 0 statSync over 1000 reads; download guard sheds past global concurrency + per-window rate + critical band; admit() has no IP parameter. Suite 259/259. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
40 lines
1.4 KiB
JavaScript
40 lines
1.4 KiB
JavaScript
'use strict';
|
|
// #146 hardening (Item C) — cache the OTA APK resolution so no /api/update/check or
|
|
// /download/apk does a per-request synchronous filesystem call. The path/size/mtime are
|
|
// resolved once at boot and refreshed on an interval (like the frontend-hash refresh),
|
|
// so a poll/download flood can't turn into an existsSync/statSync flood on the loop.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const config = require('../config');
|
|
|
|
// A copy under DATA_DIR wins (container operators mount /data/ScreenTinker.apk),
|
|
// else the legacy in-repo root path — same order as the old resolveApkPath().
|
|
function candidates() {
|
|
return [path.join(config.dataDir, 'ScreenTinker.apk'), path.join(__dirname, '..', '..', 'ScreenTinker.apk')];
|
|
}
|
|
|
|
let cache = { path: null, exists: false, size: 0, mtime: 0 };
|
|
|
|
function refresh() {
|
|
for (const p of candidates()) {
|
|
try { const st = fs.statSync(p); cache = { path: p, exists: true, size: st.size, mtime: st.mtimeMs }; return cache; } catch (_) { /* next */ }
|
|
}
|
|
cache = { path: null, exists: false, size: 0, mtime: 0 };
|
|
return cache;
|
|
}
|
|
|
|
function get() { return cache; }
|
|
|
|
let timer = null;
|
|
function start() {
|
|
refresh(); // resolve once at boot
|
|
if (!timer) {
|
|
timer = setInterval(refresh, config.otaApkRefreshMs);
|
|
if (timer.unref) timer.unref();
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
module.exports = { start, refresh, get };
|