fix(torrent): harden allocation and credential isolation

- Extend allocation-phase eligibility to preallocated Torrent admission while excluding none, verify-only, and media work.

- Strip Torrent metadata credentials at intake, persistence, renderer, native, and Aria2 header boundaries.

- Add restart, batch-admission, persistence, and native regression coverage.
This commit is contained in:
NimBold
2026-08-19 08:49:02 +03:30
parent 2bce25868c
commit 566632b7ad
7 changed files with 378 additions and 90 deletions
+28
View File
@@ -9,6 +9,7 @@ import {
categoryForDownload,
categoryForFileName,
isAllocationPhaseVisible,
isAllocationPhaseEligible,
isValidTorrentExcludeTrackerList,
isValidTorrentTrackerList,
normalizeTorrentEncryptionPolicy,
@@ -88,6 +89,24 @@ describe('download persistence progress snapshots', () => {
expect(persisted.headers).toBeUndefined();
});
it('does not create a credential gate for Torrent metadata context', () => {
const persisted = redactDownloadForPersistence({
...item('paused'),
isTorrent: true,
username: 'browser-user',
password: 'secret',
cookies: 'session=metadata-only',
headers: 'User-Agent: browser',
credentialsRequired: true,
});
expect(persisted.credentialsRequired).toBeUndefined();
expect(persisted.username).toBeUndefined();
expect(persisted.password).toBeUndefined();
expect(persisted.cookies).toBeUndefined();
expect(persisted.headers).toBeUndefined();
});
it.each(['queued', 'staged', 'retrying', 'processing'] as const)(
'keeps byte counters for %s snapshots',
(status) => {
@@ -116,6 +135,15 @@ describe('allocation phase visibility', () => {
expect(isAllocationPhaseVisible(true, 'completed')).toBe(false);
expect(isAllocationPhaseVisible(false, 'downloading')).toBe(false);
});
it('uses Torrent allocation settings and excludes media and verify-only work', () => {
expect(isAllocationPhaseEligible({ isTorrent: true, torrentFileAllocation: undefined })).toBe(true);
expect(isAllocationPhaseEligible({ isTorrent: true, torrentFileAllocation: 'prealloc' })).toBe(true);
expect(isAllocationPhaseEligible({ isTorrent: true, torrentFileAllocation: 'none' })).toBe(false);
expect(isAllocationPhaseEligible({ isTorrent: true, torrentVerifyOnly: true })).toBe(false);
expect(isAllocationPhaseEligible({ isTorrent: true, isMedia: true })).toBe(false);
expect(isAllocationPhaseEligible({ isTorrent: false, isMedia: false })).toBe(true);
});
});
describe('Torrent tracker input validation', () => {
+21 -1
View File
@@ -55,6 +55,20 @@ export const isAllocationPhaseVisible = (
status: DownloadStatus,
): boolean => allocationPending && status !== 'completed' && status !== 'paused';
/**
* Allocation is a transient admission phase. Normal downloads retain the
* existing preallocation behavior; Torrent rows use Aria2's Torrent-specific
* allocation setting and never show the hint for verification-only work.
*/
export const isAllocationPhaseEligible = (
download: Pick<DownloadItem, 'isMedia' | 'isTorrent' | 'torrentFileAllocation' | 'torrentVerifyOnly'>,
): boolean => {
if (download.isMedia === true) return false;
if (download.isTorrent !== true) return true;
return download.torrentVerifyOnly !== true
&& normalizeTorrentFileAllocation(download.torrentFileAllocation) !== 'none';
};
export const DOWNLOAD_CONNECTIONS_MIN = 1;
export const DOWNLOAD_CONNECTIONS_MAX = 16;
@@ -590,7 +604,13 @@ const VOLATILE_PROGRESS_STATUSES = new Set([
*/
export const redactDownloadForPersistence = (item: DownloadItem): DownloadItem => {
const copy: DownloadItem = { ...item };
if (item.credentialsRequired === true
if (item.isTorrent === true) {
// Torrent request credentials belong only to metadata acquisition. A
// legacy row may still carry the marker or username in memory, but neither
// may turn a cached-metadata Torrent into a credential-gated restart.
delete copy.credentialsRequired;
delete copy.username;
} else if (item.credentialsRequired === true
|| DOWNLOAD_SECRET_FIELDS.some(field => Boolean(item[field]))) {
copy.credentialsRequired = true;
}