diff --git a/src-tauri/src/extension_server.rs b/src-tauri/src/extension_server.rs index f06c9c4..a48bbab 100644 --- a/src-tauri/src/extension_server.rs +++ b/src-tauri/src/extension_server.rs @@ -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); } diff --git a/src/components/SpeedLimiterView.test.ts b/src/components/SpeedLimiterView.test.ts index 176d3f5..e4a7cc8 100644 --- a/src/components/SpeedLimiterView.test.ts +++ b/src/components/SpeedLimiterView.test.ts @@ -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); + }); }); diff --git a/src/components/SpeedLimiterView.tsx b/src/components/SpeedLimiterView.tsx index fdf8307..9d81b23 100644 --- a/src/components/SpeedLimiterView.tsx +++ b/src/components/SpeedLimiterView.tsx @@ -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 (
@@ -169,6 +192,7 @@ export default function SpeedLimiterView() { 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() { setCustomPresetValue(Math.max(1, Number(event.target.value) || 1))}