fix(release): harden speed limits and extension replay checks

This commit is contained in:
NimBold
2026-07-15 09:02:40 +03:30
parent 6ff0047d6c
commit b81e8391e1
3 changed files with 54 additions and 9 deletions
+9 -1
View File
@@ -203,7 +203,15 @@ async fn ping_handler(
None => return Err(StatusCode::FORBIDDEN),
};
if verify_signature(signature, timestamp_str, &body, &state.pairing_token).is_err() {
let timestamp = match verify_signature(signature, timestamp_str, &body, &state.pairing_token) {
Ok(timestamp) => timestamp,
Err(_) => return Err(StatusCode::FORBIDDEN),
};
// Discovery probes are authenticated requests too. Claim the verified
// signature before signing a proof so a captured /ping signature cannot
// be replayed with arbitrary client nonces during its validity window.
if !claim_request(signature, timestamp, &state.replay_cache) {
return Err(StatusCode::FORBIDDEN);
}
+13 -1
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest';
import { displayValueFromPresetBase, presetBaseFromDisplayValue } from './SpeedLimiterView';
import {
convertSpeedValue,
displayValueFromPresetBase,
presetBaseFromDisplayValue,
} from './SpeedLimiterView';
describe('SpeedLimiterView speed conversions', () => {
it('converts KB/s and MB/s using binary units consistently', () => {
@@ -8,4 +12,12 @@ describe('SpeedLimiterView speed conversions', () => {
expect(presetBaseFromDisplayValue(5, 'MB/s')).toBe(5);
expect(displayValueFromPresetBase(5, 'MB/s')).toBe(5);
});
it('round-trips non-MiB-aligned presets through the integer KiB backend value', () => {
const presetBase = presetBaseFromDisplayValue(1500, 'KB/s');
expect(displayValueFromPresetBase(presetBase, 'KB/s')).toBe(1500);
expect(convertSpeedValue(1500, 'KB/s', 'MB/s')).toBe(1500 / 1024);
expect(convertSpeedValue(convertSpeedValue(1500, 'KB/s', 'MB/s'), 'MB/s', 'KB/s')).toBe(1500);
});
});
+32 -7
View File
@@ -8,12 +8,28 @@ type SpeedUnit = 'KB/s' | 'MB/s';
const MAX_LIMIT_KIB = 10_485_760;
const MAX_LIMIT_MB = 10240;
const KIB_PER_MIB = 1024;
export function speedValueToKiB(value: number, unit: SpeedUnit): number {
const numericValue = Number.isFinite(value) ? value : 1;
const valueKiB = unit === 'MB/s' ? numericValue * KIB_PER_MIB : numericValue;
return Math.max(1, Math.min(MAX_LIMIT_KIB, Math.round(valueKiB)));
}
export function speedValueFromKiB(valueKiB: number, unit: SpeedUnit): number {
const normalizedKiB = Math.max(1, Math.min(MAX_LIMIT_KIB, Math.round(valueKiB)));
return unit === 'MB/s' ? normalizedKiB / KIB_PER_MIB : normalizedKiB;
}
export function convertSpeedValue(value: number, fromUnit: SpeedUnit, toUnit: SpeedUnit): number {
return speedValueFromKiB(speedValueToKiB(value, fromUnit), toUnit);
}
export function parseLimit(limit: string, fallback: number): { value: number; unit: SpeedUnit } {
const match = limit.trim().match(/^(\d+(?:\.\d+)?)\s*([km]?)b?(?:\/s)?$/i);
const valueKiB = match
? Math.max(1, Math.round(Number(match[1]) * (match[2].toLowerCase() === 'm' ? 1024 : 1)))
: fallback;
? speedValueToKiB(Number(match[1]) * (match[2].toLowerCase() === 'm' ? KIB_PER_MIB : 1), 'KB/s')
: speedValueToKiB(fallback, 'KB/s');
return valueKiB >= 1024 && valueKiB % 1024 === 0
? { value: valueKiB / 1024, unit: 'MB/s' }
@@ -24,17 +40,17 @@ function sanitizePresetValues(values: number[]): number[] {
const cleaned = values
.map(value => Number(value))
.filter(value => Number.isFinite(value) && value > 0)
.map(value => Math.min(MAX_LIMIT_MB, Math.round(value * 100) / 100));
.map(value => speedValueFromKiB(speedValueToKiB(value, 'MB/s'), 'MB/s'));
return Array.from(new Set(cleaned)).sort((a, b) => a - b);
}
export function presetBaseFromDisplayValue(value: number, unit: SpeedUnit): number {
return unit === 'MB/s' ? value : value / 1024;
return speedValueFromKiB(speedValueToKiB(value, unit), 'MB/s');
}
export function displayValueFromPresetBase(value: number, unit: SpeedUnit): number {
return unit === 'MB/s' ? value : Math.round(value * 1024);
return speedValueFromKiB(speedValueToKiB(value, 'MB/s'), unit);
}
function formatPresetValue(value: number): string {
@@ -75,7 +91,7 @@ export default function SpeedLimiterView() {
if (savingRef.current) return;
savingRef.current = true;
const numericValue = Math.max(1, Math.min(Number(value) || 1, unit === 'MB/s' ? 10240 : MAX_LIMIT_KIB));
const valueKiB = Math.min(MAX_LIMIT_KIB, Math.round(unit === 'MB/s' ? numericValue * 1024 : numericValue));
const valueKiB = speedValueToKiB(numericValue, unit);
setIsSaving(true);
try {
await setGlobalSpeedLimit(enabled ? `${valueKiB}K` : '');
@@ -127,6 +143,13 @@ export default function SpeedLimiterView() {
});
};
const changeUnit = (nextUnit: SpeedUnit) => {
if (nextUnit === unit) return;
setValue(convertSpeedValue(value, unit, nextUnit));
setCustomPresetValue(convertSpeedValue(customPresetValue, unit, nextUnit));
setUnit(nextUnit);
};
return (
<div className="flex-1 flex h-full flex-col overflow-hidden bg-main-bg">
<WindowDragRegion />
@@ -169,6 +192,7 @@ export default function SpeedLimiterView() {
<input
type="number"
min="1"
step="any"
value={value}
disabled={!enabled || isSaving}
onChange={event => setValue(Math.max(1, Number(event.target.value) || 1))}
@@ -180,7 +204,7 @@ export default function SpeedLimiterView() {
key={option}
type="button"
disabled={!enabled || isSaving}
onClick={() => setUnit(option)}
onClick={() => changeUnit(option)}
className={`rounded px-3 py-1.5 text-[12px] font-medium ${
unit === option ? 'bg-accent text-white' : 'text-text-secondary hover:bg-item-hover'
}`}
@@ -228,6 +252,7 @@ export default function SpeedLimiterView() {
<input
type="number"
min="1"
step="any"
value={customPresetValue}
disabled={!enabled || isSaving}
onChange={event => setCustomPresetValue(Math.max(1, Number(event.target.value) || 1))}