mirror of
https://github.com/alexta69/metube.git
synced 2026-06-13 16:40:05 +00:00
Gate manual yt-dlp overrides behind flag
Agent-Logs-Url: https://github.com/alexta69/metube/sessions/31b4274d-cf48-4260-b73b-633cbcd2bb09 Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
565a715037
commit
6e9b2dd7b3
+10
-35
@@ -59,6 +59,7 @@ class Config:
|
||||
'YTDL_OPTIONS_FILE': '',
|
||||
'YTDL_OPTIONS_PRESETS': '{}',
|
||||
'YTDL_OPTIONS_PRESETS_FILE': '',
|
||||
'ALLOW_YTDL_OPTIONS_OVERRIDES': 'false',
|
||||
'ROBOTS_TXT': '',
|
||||
'HOST': '0.0.0.0',
|
||||
'PORT': '8081',
|
||||
@@ -72,7 +73,7 @@ class Config:
|
||||
'ENABLE_ACCESSLOG': 'false',
|
||||
}
|
||||
|
||||
_BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'HTTPS', 'ENABLE_ACCESSLOG')
|
||||
_BOOLEAN = ('DOWNLOAD_DIRS_INDEXABLE', 'CUSTOM_DIRS', 'CREATE_CUSTOM_DIRS', 'DELETE_FILE_ON_TRASHCAN', 'HTTPS', 'ENABLE_ACCESSLOG', 'ALLOW_YTDL_OPTIONS_OVERRIDES')
|
||||
|
||||
def __init__(self):
|
||||
for k, v in self._DEFAULTS.items():
|
||||
@@ -126,6 +127,7 @@ class Config:
|
||||
'PUBLIC_HOST_AUDIO_URL',
|
||||
'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT',
|
||||
'SUBSCRIPTION_DEFAULT_CHECK_INTERVAL',
|
||||
'ALLOW_YTDL_OPTIONS_OVERRIDES',
|
||||
)
|
||||
|
||||
def frontend_safe(self) -> dict:
|
||||
@@ -232,36 +234,7 @@ VALID_VIDEO_CODECS = {'auto', 'h264', 'h265', 'av1', 'vp9'}
|
||||
VALID_VIDEO_FORMATS = {'any', 'mp4', 'ios'}
|
||||
VALID_AUDIO_FORMATS = {'m4a', 'mp3', 'opus', 'wav', 'flac'}
|
||||
VALID_THUMBNAIL_FORMATS = {'jpg'}
|
||||
BLOCKED_YTDL_OVERRIDE_KEYS = frozenset({
|
||||
'exec',
|
||||
'exec_before_dl',
|
||||
'exec_cmd',
|
||||
'external_downloader',
|
||||
'external_downloader_args',
|
||||
'format',
|
||||
'ignore_no_formats_error',
|
||||
'no_color',
|
||||
'outtmpl',
|
||||
'paths',
|
||||
'postprocessor_hooks',
|
||||
'progress_hooks',
|
||||
'quiet',
|
||||
'socket_timeout',
|
||||
'verbose',
|
||||
})
|
||||
|
||||
|
||||
def _iter_nested_keys(value):
|
||||
if isinstance(value, dict):
|
||||
for key, nested in value.items():
|
||||
yield str(key)
|
||||
yield from _iter_nested_keys(nested)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
yield from _iter_nested_keys(item)
|
||||
|
||||
|
||||
def _parse_ytdl_options_overrides(value) -> dict:
|
||||
def _parse_ytdl_options_overrides(value, *, enabled: bool) -> dict:
|
||||
if value is None or value == '':
|
||||
return {}
|
||||
|
||||
@@ -274,9 +247,8 @@ def _parse_ytdl_options_overrides(value) -> dict:
|
||||
if not isinstance(value, dict):
|
||||
raise web.HTTPBadRequest(reason='ytdl_options_overrides must be a JSON object')
|
||||
|
||||
blocked_keys = sorted({key for key in _iter_nested_keys(value) if key in BLOCKED_YTDL_OVERRIDE_KEYS})
|
||||
if blocked_keys:
|
||||
raise web.HTTPBadRequest(reason=f'ytdl_options_overrides contains disallowed keys: {blocked_keys}')
|
||||
if value and not enabled:
|
||||
raise web.HTTPBadRequest(reason='ytdl_options_overrides are disabled')
|
||||
|
||||
return value
|
||||
|
||||
@@ -497,7 +469,10 @@ def parse_download_options(post: dict) -> dict:
|
||||
subtitle_language = str(subtitle_language).strip()
|
||||
subtitle_mode = str(subtitle_mode).strip()
|
||||
ytdl_options_preset = str(ytdl_options_preset).strip()
|
||||
ytdl_options_overrides = _parse_ytdl_options_overrides(ytdl_options_overrides)
|
||||
ytdl_options_overrides = _parse_ytdl_options_overrides(
|
||||
ytdl_options_overrides,
|
||||
enabled=config.ALLOW_YTDL_OPTIONS_OVERRIDES,
|
||||
)
|
||||
|
||||
if chapter_template and ('..' in chapter_template or chapter_template.startswith('/') or chapter_template.startswith('\\')):
|
||||
raise web.HTTPBadRequest(reason='chapter_template must not contain ".." or start with a path separator')
|
||||
|
||||
+13
-1
@@ -64,6 +64,7 @@ async def test_add_ok(mock_dqueue):
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_passes_preset_and_overrides(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.config, "YTDL_OPTIONS_PRESETS", {"Preset A": {"writesubtitles": True}})
|
||||
monkeypatch.setattr(main.config, "ALLOW_YTDL_OPTIONS_OVERRIDES", True)
|
||||
req = _json_request(
|
||||
_valid_video_add_body(
|
||||
ytdl_options_preset="Preset A",
|
||||
@@ -151,12 +152,23 @@ async def test_add_invalid_ytdl_options_override_json(mock_dqueue):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_blocked_ytdl_options_override_key(mock_dqueue):
|
||||
async def test_add_rejects_ytdl_options_overrides_when_disabled(mock_dqueue):
|
||||
req = _json_request(_valid_video_add_body(ytdl_options_overrides='{"exec": "rm -rf /"}'))
|
||||
with pytest.raises(web.HTTPBadRequest):
|
||||
await main.add(req)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_allows_any_ytdl_options_override_key_when_enabled(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.config, "ALLOW_YTDL_OPTIONS_OVERRIDES", True)
|
||||
req = _json_request(_valid_video_add_body(ytdl_options_overrides='{"exec": "echo hi"}'))
|
||||
resp = await main.add(req)
|
||||
assert resp.status == 200
|
||||
call = mock_dqueue.add.await_args
|
||||
assert call is not None
|
||||
assert call.args[14] == {"exec": "echo hi"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_unknown_ytdl_preset(mock_dqueue):
|
||||
req = _json_request(_valid_video_add_body(ytdl_options_preset="Missing"))
|
||||
|
||||
@@ -59,6 +59,12 @@ class ConfigTests(unittest.TestCase):
|
||||
safe = c.frontend_safe()
|
||||
self.assertNotIn("YTDL_OPTIONS", safe)
|
||||
self.assertNotIn("HOST", safe)
|
||||
self.assertEqual(safe["ALLOW_YTDL_OPTIONS_OVERRIDES"], False)
|
||||
|
||||
def test_allow_ytdl_options_overrides_boolean_loaded(self):
|
||||
with patch.dict(os.environ, _base_env(ALLOW_YTDL_OPTIONS_OVERRIDES="true"), clear=False):
|
||||
c = Config()
|
||||
self.assertTrue(c.ALLOW_YTDL_OPTIONS_OVERRIDES)
|
||||
|
||||
def test_runtime_override_roundtrip(self):
|
||||
with patch.dict(os.environ, _base_env(), clear=False):
|
||||
|
||||
@@ -99,25 +99,34 @@ class FrontendSafeTests(unittest.TestCase):
|
||||
self.assertIn(key, safe)
|
||||
self.assertNotIn("YTDL_OPTIONS", safe)
|
||||
self.assertNotIn("DOWNLOAD_DIR", safe)
|
||||
self.assertIn("ALLOW_YTDL_OPTIONS_OVERRIDES", safe)
|
||||
|
||||
|
||||
class ParseYtdlOverridesTests(unittest.TestCase):
|
||||
def test_empty_override_string_returns_empty_dict(self):
|
||||
self.assertEqual(main._parse_ytdl_options_overrides(""), {})
|
||||
self.assertEqual(main._parse_ytdl_options_overrides("", enabled=False), {})
|
||||
|
||||
def test_rejects_non_object_json(self):
|
||||
with self.assertRaises(main.web.HTTPBadRequest):
|
||||
main._parse_ytdl_options_overrides('["bad"]')
|
||||
main._parse_ytdl_options_overrides('["bad"]', enabled=True)
|
||||
|
||||
def test_rejects_blocked_keys(self):
|
||||
def test_rejects_non_empty_overrides_when_disabled(self):
|
||||
with self.assertRaises(main.web.HTTPBadRequest):
|
||||
main._parse_ytdl_options_overrides('{"exec": "rm -rf /"}')
|
||||
main._parse_ytdl_options_overrides('{"exec": "rm -rf /"}', enabled=False)
|
||||
|
||||
def test_allows_any_keys_when_enabled(self):
|
||||
self.assertEqual(
|
||||
main._parse_ytdl_options_overrides('{"exec": "rm -rf /"}', enabled=True),
|
||||
{"exec": "rm -rf /"},
|
||||
)
|
||||
|
||||
|
||||
class ParseDownloadOptionsTests(unittest.TestCase):
|
||||
def test_accepts_known_preset_and_overrides(self):
|
||||
previous = dict(main.config.YTDL_OPTIONS_PRESETS)
|
||||
previous_allow = main.config.ALLOW_YTDL_OPTIONS_OVERRIDES
|
||||
main.config.YTDL_OPTIONS_PRESETS = {"With subtitles": {"writesubtitles": True}}
|
||||
main.config.ALLOW_YTDL_OPTIONS_OVERRIDES = True
|
||||
try:
|
||||
parsed = main.parse_download_options({
|
||||
"url": "https://example.com/v",
|
||||
@@ -130,6 +139,7 @@ class ParseDownloadOptionsTests(unittest.TestCase):
|
||||
})
|
||||
finally:
|
||||
main.config.YTDL_OPTIONS_PRESETS = previous
|
||||
main.config.ALLOW_YTDL_OPTIONS_OVERRIDES = previous_allow
|
||||
self.assertEqual(parsed["ytdl_options_preset"], "With subtitles")
|
||||
self.assertEqual(parsed["ytdl_options_overrides"], {"writesubtitles": True})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user