From 24d3d064bb0745cc680fe883fe70bf59e8e9aba7 Mon Sep 17 00:00:00 2001 From: jcfp Date: Sun, 14 Feb 2021 17:05:26 +0100 Subject: [PATCH] add unwanted extensions whitelist mode (#1798) * add unwanted extensions whitelist mode * only call get_ext once * remove unneeded .lower() --- interfaces/Config/templates/config_switches.tmpl | 4 +++ sabnzbd/assembler.py | 12 +++++-- sabnzbd/cfg.py | 1 + sabnzbd/filesystem.py | 17 +++++++++ sabnzbd/interface.py | 1 + sabnzbd/nzbstuff.py | 6 ++-- sabnzbd/skintext.py | 8 +++-- tests/test_filesystem.py | 46 ++++++++++++++++++++++++ 8 files changed, 87 insertions(+), 8 deletions(-) diff --git a/interfaces/Config/templates/config_switches.tmpl b/interfaces/Config/templates/config_switches.tmpl index 6df2c3b..ff09155 100644 --- a/interfaces/Config/templates/config_switches.tmpl +++ b/interfaces/Config/templates/config_switches.tmpl @@ -120,6 +120,10 @@
+ $T('explain-unwanted_extensions')
diff --git a/sabnzbd/assembler.py b/sabnzbd/assembler.py index 6e68e19..eb198dd 100644 --- a/sabnzbd/assembler.py +++ b/sabnzbd/assembler.py @@ -30,7 +30,15 @@ from typing import Tuple, Optional, List import sabnzbd from sabnzbd.misc import get_all_passwords, match_str -from sabnzbd.filesystem import set_permissions, clip_path, has_win_device, diskspace, get_filename, get_ext +from sabnzbd.filesystem import ( + set_permissions, + clip_path, + has_win_device, + diskspace, + get_filename, + get_ext, + has_unwanted_extension, +) from sabnzbd.constants import Status, GIGI, MAX_ASSEMBLER_QUEUE import sabnzbd.cfg as cfg from sabnzbd.nzbstuff import NzbObject, NzbFile @@ -376,7 +384,7 @@ def check_encrypted_and_unwanted_files(nzo: NzbObject, filepath: str) -> Tuple[b if cfg.unwanted_extensions() and cfg.action_on_unwanted_extensions(): for somefile in zf.namelist(): logging.debug("File contains: %s", somefile) - if get_ext(somefile).replace(".", "").lower() in cfg.unwanted_extensions(): + if has_unwanted_extension(somefile): logging.debug("Unwanted file %s", somefile) unwanted = somefile zf.close() diff --git a/sabnzbd/cfg.py b/sabnzbd/cfg.py index ea33ff2..d24d678 100644 --- a/sabnzbd/cfg.py +++ b/sabnzbd/cfg.py @@ -196,6 +196,7 @@ sanitize_safe = OptionBool("misc", "sanitize_safe", False) cleanup_list = OptionList("misc", "cleanup_list") unwanted_extensions = OptionList("misc", "unwanted_extensions") action_on_unwanted_extensions = OptionNumber("misc", "action_on_unwanted_extensions", 0) +unwanted_extensions_mode = OptionNumber("misc", "unwanted_extensions_mode", 0) new_nzb_on_failure = OptionBool("misc", "new_nzb_on_failure", False) history_retention = OptionStr("misc", "history_retention", "0") enable_meta = OptionBool("misc", "enable_meta", True) diff --git a/sabnzbd/filesystem.py b/sabnzbd/filesystem.py index 8da0da7..38a03f6 100644 --- a/sabnzbd/filesystem.py +++ b/sabnzbd/filesystem.py @@ -58,6 +58,23 @@ def get_ext(filename: str) -> str: return "" +def has_unwanted_extension(filename: str) -> bool: + """ Determine if a filename has an unwanted extension, given the configured mode """ + extension = get_ext(filename).replace(".", "") + if extension and sabnzbd.cfg.unwanted_extensions(): + return ( + # Blacklisted + sabnzbd.cfg.unwanted_extensions_mode() == 0 + and extension in sabnzbd.cfg.unwanted_extensions() + ) or ( + # Not whitelisted + sabnzbd.cfg.unwanted_extensions_mode() == 1 + and extension not in sabnzbd.cfg.unwanted_extensions() + ) + else: + return bool(sabnzbd.cfg.unwanted_extensions_mode()) + + def get_filename(path: str) -> str: """ Return path without the file extension """ try: diff --git a/sabnzbd/interface.py b/sabnzbd/interface.py index f60ca70..c50969d 100644 --- a/sabnzbd/interface.py +++ b/sabnzbd/interface.py @@ -1231,6 +1231,7 @@ SWITCH_LIST = ( "new_nzb_on_failure", "unwanted_extensions", "action_on_unwanted_extensions", + "unwanted_extensions_mode", "sanitize_safe", "rating_enable", "rating_api_key", diff --git a/sabnzbd/nzbstuff.py b/sabnzbd/nzbstuff.py index 00240f5..bbd8658 100644 --- a/sabnzbd/nzbstuff.py +++ b/sabnzbd/nzbstuff.py @@ -78,6 +78,7 @@ from sabnzbd.filesystem import ( make_script_path, globber, is_valid_script, + has_unwanted_extension, ) from sabnzbd.decorators import synchronized import sabnzbd.config as config @@ -890,10 +891,7 @@ class NzbObject(TryList): # Check if there is any unwanted extension in plain sight in the NZB itself for nzf in self.files: - if ( - cfg.action_on_unwanted_extensions() >= 1 - and get_ext(nzf.filename).replace(".", "") in cfg.unwanted_extensions() - ): + if cfg.action_on_unwanted_extensions() and has_unwanted_extension(nzf.filename): # ... we found an unwanted extension logging.warning(T("Unwanted Extension in file %s (%s)"), nzf.filename, self.final_name) # Pause, or Abort: diff --git a/sabnzbd/skintext.py b/sabnzbd/skintext.py index 2ae2150..6192fc2 100644 --- a/sabnzbd/skintext.py +++ b/sabnzbd/skintext.py @@ -495,9 +495,13 @@ SKIN_TEXT = { "nodupes-tag": TT("Tag job"), #: Four way switch for duplicates "abort": TT("Abort"), #: Three way switch for encrypted posts "opt-action_on_unwanted_extensions": TT("Action when unwanted extension detected"), - "explain-action_on_unwanted_extensions": TT("Action when an unwanted extension is detected in RAR files"), + "explain-action_on_unwanted_extensions": TT("Action when an unwanted extension is detected"), "opt-unwanted_extensions": TT("Unwanted extensions"), - "explain-unwanted_extensions": TT("List all unwanted extensions. For example: exe or exe, com"), + "unwanted_extensions_blacklist": TT("Blacklist"), + "unwanted_extensions_whitelist": TT("Whitelist"), + "explain-unwanted_extensions": TT( + "Select a mode and list all (un)wanted extensions. For example: exe or exe, com" + ), "opt-sfv_check": TT("Enable SFV-based checks"), "explain-sfv_check": TT("Do an extra verification based on SFV files."), "opt-script_can_fail": TT("User script can flag job as failed"), diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 3a9b3c2..fd5c0d8 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -983,3 +983,49 @@ class TestSetPermissions(ffs.TestCase, PermissionCheckerHelper): def test_dir1755_umask4755_setting(self): # Sticky bit on directory, umask with setuid self._runner("1755", "4755") + + +class TestUnwantedExtensions: + # Only test lowercase extensions without a leading dot: the unwanted_extensions + # setting is sanitized accordingly in interface.saveSwitches() before saving. + test_extensions = "iso, cmd, bat, sh" + # Test parameters as (filename, result) tuples, with result given for blacklist mode + test_params = [ + ("ubuntu.iso", True), + ("par2.cmd", True), + ("freedos.BAT", True), + ("Debian.installer.SH", True), + ("FREEBSD.ISO", True), + ("par2.CmD", True), + ("freedos.baT", True), + ("Debian.Installer.sh", True), + ("ubuntu.torrent", False), + ("par2.cmd.notcmd", False), + ("freedos.tab", False), + (".SH.hs", False), + ("No_Extension", False), + (480, False), + (None, False), + ("", False), + ([], False), + ] + + @set_config({"unwanted_extensions_mode": 0, "unwanted_extensions": test_extensions}) + def test_has_unwanted_extension_blacklist_mode(self): + for filename, result in self.test_params: + assert filesystem.has_unwanted_extension(filename) is result + + @set_config({"unwanted_extensions_mode": 1, "unwanted_extensions": test_extensions}) + def test_has_unwanted_extension_whitelist_mode(self): + for filename, result in self.test_params: + assert filesystem.has_unwanted_extension(filename) is not result + + @set_config({"unwanted_extensions_mode": 0, "unwanted_extensions": ""}) + def test_has_unwanted_extension_empty_blacklist(self): + for filename, result in self.test_params: + assert filesystem.has_unwanted_extension(filename) is False + + @set_config({"unwanted_extensions_mode": 1, "unwanted_extensions": ""}) + def test_has_unwanted_extension_empty_whitelist(self): + for filename, result in self.test_params: + assert filesystem.has_unwanted_extension(filename) is True