Skip to content

Commit 272e766

Browse files
committed
add FILE_LIST_SCAN_ENABLED to opt out of the file name check
1 parent b28e904 commit 272e766

6 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ Configure either SABnzbd or NZBGet for NZB downloads. If both are configured, SA
306306
| `IMPORT_HARDLINK_FALLBACK` | `error` | What a hardlink import does when source and library are on different filesystems: `error`, `copy`, `symlink`, `move` |
307307
| `REMOVE_TORRENT_AFTER_IMPORT` | `false` | Remove the torrent from the client once imported (never deletes the data under a source-preserving mode) |
308308
| `EXTRACT_ARCHIVES` | `false` | Auto-extract downloaded archives |
309+
| `FILE_LIST_SCAN_ENABLED` | `true` | Block a download whose file names carry a dangerous extension. Set `false` if your sources legitimately ship `.bat`/`.cmd` next to the payload |
309310
| `MAX_RETRIES` | `2` | Download retry attempts |
310311
| `RETRY_BACKOFF_SECONDS` | `60` | Seconds between retries |
311312

‎internal/config/config.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ type Config struct {
4242
ClamAVSocket string
4343
DockerSocket string
4444

45+
// FileListScanEnabled gates the pre-download file name check.
46+
FileListScanEnabled bool
47+
4548
// Import behavior. ImportMode decides whether a finished download is
4649
// moved into the library or preserved in place (hardlink/symlink/copy)
4750
// so a torrent can keep seeding. ImportHardlinkFallback applies when a
@@ -174,6 +177,8 @@ func Load() *Config {
174177
ClamAVSocket: envStr("CLAMAV_SOCKET", "/run/clamav/clamd.sock"),
175178
DockerSocket: envStr("DOCKER_SOCKET", "/var/run/docker.sock"),
176179

180+
FileListScanEnabled: envBool("FILE_LIST_SCAN_ENABLED", true),
181+
177182
ImportMode: envImportMode("IMPORT_MODE"),
178183
ImportHardlinkFallback: envHardlinkFallback("IMPORT_HARDLINK_FALLBACK"),
179184

‎internal/config/config_test.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,15 @@ func TestEnvInt(t *testing.T) {
264264
t.Errorf("got %d, want fallback 7", got)
265265
}
266266
}
267+
268+
func TestFileListScanEnabled(t *testing.T) {
269+
if !Load().FileListScanEnabled {
270+
t.Error("FileListScanEnabled should default to true")
271+
}
272+
273+
os.Setenv("FILE_LIST_SCAN_ENABLED", "false")
274+
defer os.Unsetenv("FILE_LIST_SCAN_ENABLED")
275+
if Load().FileListScanEnabled {
276+
t.Error("FILE_LIST_SCAN_ENABLED=false should turn the scan off")
277+
}
278+
}

‎internal/download/helpers_test.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ func newTestConfig(t *testing.T) *config.Config {
4040
ClamAVSocket: filepath.Join(base, "no-such-clamav.sock"),
4141
DockerSocket: filepath.Join(base, "no-such-docker.sock"),
4242
MaxRetries: 2,
43+
// Mirrors the production default. A zero value here would silently
44+
// disable the scan for every test in this package.
45+
FileListScanEnabled: true,
4346
}
4447
}
4548

‎internal/download/manager.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func (m *Manager) watchGameTorrent(jobID, title, platf, platSlug string, isPC bo
223223
}
224224

225225
// Layer 1: scan file list once metadata is available
226-
if !fileScanDone && t.Progress > 0 {
226+
if m.cfg.FileListScanEnabled && !fileScanDone && t.Progress > 0 {
227227
m.jobs.Update(jobID, "detail", "Scanning file list...")
228228
isSafe, issues := safety.ScanTorrentFileList(m.qb, t.Hash)
229229
fileScanDone = true

‎internal/download/manager_test.go‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,3 +963,41 @@ func TestMaybeExtractArchives(t *testing.T) {
963963
}
964964
})
965965
}
966+
967+
// Some sources legitimately ship a .bat next to the payload, and the operator
968+
// has no other way past a filename match.
969+
func TestDownloadTorrentFileListScanDisabled(t *testing.T) {
970+
cfg := newTestConfig(t)
971+
jobs := newTestJobs(t)
972+
cfg.QBURL = "configured"
973+
cfg.FileListScanEnabled = false
974+
975+
qm := newQbitMock(t)
976+
qm.setFiles([]qbit.TorrentFile{{Name: "Game/keygen.bat"}, {Name: "Game/setup.scr"}})
977+
qm.setTorrents([]qbit.Torrent{{
978+
Name: "Repack Game",
979+
Hash: "hash-optout",
980+
Progress: 0.5, // metadata available, still downloading
981+
}})
982+
983+
m := New(cfg, jobs, qm.client())
984+
jobID, err := m.DownloadTorrent("magnet:x", "Repack Game", "PC", "", true)
985+
if err != nil {
986+
t.Fatalf("unexpected error: %v", err)
987+
}
988+
989+
// With the scan on, the watcher acts on its first pass, so a short wait is
990+
// enough to catch it having done so.
991+
deadline := time.Now().Add(3 * time.Second)
992+
for time.Now().Before(deadline) {
993+
if len(qm.deleteCalls()) != 0 {
994+
t.Fatalf("torrent was acted on despite the scan being disabled: %+v", qm.deleteCalls())
995+
}
996+
if job, ok := jobs.Get(jobID); ok {
997+
if status, _ := job["status"].(string); status == "error" {
998+
t.Fatalf("job errored despite the scan being disabled: %v", job["error"])
999+
}
1000+
}
1001+
time.Sleep(100 * time.Millisecond)
1002+
}
1003+
}

0 commit comments

Comments
 (0)