Commit Graph
536 Commits
Author SHA1 Message Date
Safihre 18987d6733 Separate script log link from history stage text
Move the rendering of the script log "More" link from the backend's `stage_log` content to the frontend. The backend now provides a `has_script_log` flag, allowing the frontend to dynamically display the link and fetch the script log on demand.

Includes a database migration to clean up old embedded links in existing `stage_log` entries. This improves separation of concerns, simplifies `stage_log` data, and allows for more flexible frontend rendering.
2026-09-15 14:37:52 +02:00
mnightingale 112d449446 Continue with the other RSS feed URIs when one fails 2026-09-11 14:35:33 +02:00
Safihre b18cdc38f8 Robustly strip HTML tags to prevent XSS
Previous HTML stripping could allow malformed or unclosed tags to pass through, potentially leading to Cross-Site Scripting (XSS) vulnerabilities if completed by the browser.

This change updates the regex to aggressively remove all HTML tags, even incomplete ones. The `knockout-extensions.js` code also now universally escapes HTML, removing a potential bypass. New tests confirm this behavior.
2026-09-10 12:37:01 +02:00
mnightingale f372698127 Write the queue admin once per batch when removing or adding many jobs 2026-09-10 11:47:34 +02:00
mnightingale 2db268e9a6 Insert queued jobs with bisect and queue each priority group in arrival order 2026-09-10 11:47:34 +02:00
mnightingale b69bd10725 Add tests covering NzbQueue add, remove, reorder, priority and sorting 2026-09-10 11:47:34 +02:00
Michael Nightingale d73cdc7397 Stop treating routable special-purpose ranges as a local network 2026-09-08 22:44:12 +02:00
Michael Nightingale c01371a6af Refuse requests whose client a forwarded header hides 2026-09-08 22:44:12 +02:00
Michael Nightingale d8188b5428 Split subnet normalization out of ip_in_subnet 2026-09-08 22:44:12 +02:00
Michael Nightingale 7d9056736f Assert what the address tests were only evaluating 2026-09-08 22:44:12 +02:00
Safihre 675d7714ca Skip symlink tests if creation is not possible
On Windows, creating symbolic links requires either an elevated shell or Developer Mode. This introduces a runtime check to verify if symlinks can be created. Tests that rely on symlink creation are now skipped if this check fails, preventing unnecessary test failures in environments without the required permissions.
2026-09-08 14:56:29 +02:00
Safihre 948500799a Skip filesystem link behavior tests on Windows
These tests rely on a specific behavior of '..' in paths involving symbolic links, which differs on Windows where '..' is collapsed before the filesystem resolves links.
2026-09-08 12:16:47 +02:00
mnightingale d8815ef983 Treat a dangling link as a taken name and refuse it as a write target 2026-09-08 11:19:38 +02:00
mnightingale ff537363c6 Sanitize each part before checking it for traversal or the admin folder 2026-09-08 11:19:37 +02:00
mnightingale b58a25df07 Pin the tarfile behaviour that contains members against links in the destination 2026-09-08 11:19:36 +02:00
mnightingale 1ce8faafeb Contain renames using the resolved paths so links cannot redirect them 2026-09-08 11:19:35 +02:00
mnightingale 7dbecb0a2d Skip links and other non-regular members when extracting tar files 2026-09-08 11:19:34 +02:00
mnightingale e4162b093a Store the API connection info per config file so instances do not collide 2026-08-30 20:59:01 +02:00
mnightingale be5d394ebd Tolerate the reload that deleting a server triggers 2026-08-30 17:01:15 +02:00
mnightingale 55285c5cf9 Skip the save button's artificial one second delay with Playwright's clock 2026-08-30 17:01:15 +02:00
mnightingale c3f6b2d582 Keep pytest-playwright out of the pytest session tavern nests inside a test 2026-08-30 17:01:15 +02:00
mnightingale 5088e14fa6 Drive the functional tests with Playwright instead of Selenium 2026-08-30 17:01:15 +02:00
mnightingale 139b4a52b6 Run test coroutines on a thread of their own when a loop is already running 2026-08-30 17:01:15 +02:00
mnightingale fd9acd2ea8 Serve the urlgrabber tests from a local server instead of httpbin 2026-08-30 17:01:15 +02:00
mnightingale f7513c05b9 Stop the nested tavern session collecting the xdist worker cache dirs (#3599) 2026-08-30 10:11:14 +02:00
Safihre b263c6cfe6 Replace anonymous session cookie with stable CSRF identity (#3596)
Previously, UI access for login-bypassed clients relied on issuing a stateless "anonymous session" cookie to enable CSRF protection. This commit removes that cookie mechanism.

Instead, when the login is bypassed (e.g., for local clients without credentials), the CSRF token is now bound to a stable, internal "anonymous" identity. This maintains robust CSRF protection for browser interactions without requiring an explicit session cookie, simplifying overall session management.
2026-08-28 11:19:54 +02:00
mnightingale b192a7eca4 Persistent sessions, CSRF tokens, and no apikey in the web interface (#3559)
* Add a session store that keeps web-UI logins in the admin folder

* Authenticate the web interface with session cookies and CSRF tokens

* Stop embedding the apikey in the web interface

* Cover the session, CSRF and apikey changes
2026-08-27 13:05:12 +02:00
mnightingale f498147189 Restrict mode-based API key logic to actual API routes
The `mode` parameter in API calls enables specific behaviors and key bypasses (e.g., 'version', 'auth', NZB-key). Previously, this logic was applied universally by `check_apikey`. This change ensures such `mode`-based logic is only honored when accessing the dedicated `/api` endpoint, preventing potential API key bypasses on other web UI routes.
2026-08-25 13:24:22 +02:00
mnightingale e36d989ee5 Sanitize PP-values 2026-08-25 13:24:13 +02:00
Safihre e0acbd75e2 Harden paths that could overwrite __ADMIN__ files
Prevent directory creation in `renamer` to overwrite `__ADMIN__`
Check the whole new path for `__ADMIN__` in `renamer`
Drop `__ADMIN__` from names that are allowed to have sub-directories
Resolve the path before checking it against `__ADMIN__`
2026-08-25 13:22:12 +02:00
Safihre 93546e5b7a Add SafeUnpickler to guard against pickle-attacks (#3585)
* Add SafeUnpickler to guard against pickle-attacks

* Harden pickle unpickler with explicit allowlist

The previous `SafeUnpickler` allowed any class from `sabnzbd.*` by wildcard, which could still enable deserialization attacks if a "gadget class" (e.g., with a malicious `__del__` method) was present within our own package.

This commit renames the class to `RestrictedUnpickler` and changes its logic to only allow classes explicitly defined in `_SAFE_GLOBALS`. This significantly enhances security by preventing the unpickling of any unlisted classes, including those from within `sabnzbd`. Adds `os.stat_result` and `sabnzbd.nzb.*` classes to the allowlist for compatibility.
2026-08-24 22:40:04 +02:00
mnightingale 5973e5b6ed Stop tests that create NZBs from racing (#3590) 2026-08-24 09:00:03 +02:00
mnightingale 3813a810ed Write articles through sabctools.FileWriter, and optionally straight from the decoder (#3562)
* Use sabctools.FileWriter for article writes

* Keep the FileWriter open on the NzbFile, bounded by an LRU

* Stream decoded articles straight to their file

* Fail the article, not the connection, when a streaming write fails

* Pause on a disk error from a streamed write

A full disk cannot be fixed by fetching the article again, so mirror the assembler
and pause instead of spending its retries and failing the job as incomplete.

* Treat an exhausted quota as a full disk

* Use sabctools 9.7.0
2026-08-21 11:08:24 +02:00
mnightingale 537ac2e7a4 Tighten IPv6 and Host header handling (#3564)
* Refuse unbracketed IPv6 addresses in the Host header

* Bracket the IPv6 client address logged with warnings

* Decide the Secure cookie attribute from the request scope
2026-08-19 11:30:13 +02:00
mnightingale cb2c44cfc2 Unpack rar sets that par2 moved into a subfolder (#3553)
* Reproduce bug

* Let sanitize_filename keep par2 sub-directories

Par2 sets can store their files inside a folder, always using a forward slash as
separator no matter which platform created the set. We replaced that slash with an
underscore, so the name could never match what par2 called the file.

With allow_subdirs the separator is kept and every part is sanitized on its own.
The result stays local to the folder it is used in: empty parts, "." and ".." are
dropped, so neither a leading slash nor any amount of traversal can point outside.

* Write files into the sub-directory par2 names them in

The name from the par2 pack is what we already rename files to, but the separator
was stripped so the file landed flat with an underscore. Post-processing then had
to move it into place afterwards, which is how the volumes ended up somewhere the
unpacker did not look.

Assemble straight into the sub-directory instead. get_unique_filepath() creates the
folder and returns the name relative to the download folder, so nzf.filename now
holds that relative name. analyze_rar_filename() takes the basename, so a setname
still matches what rar_unpack() derives from a path with setname_from_path().

* Match existing files in sub-directories on retry

check_existing_files() listed the job folder without descending into it, so on a
retry the volumes par2 put in a folder of their own were never matched and got
downloaded again.

List the sub-directories too, skipping the admin folder, and match on the name
relative to the job folder. Rename bookkeeping is normalized to the platform
separator, par2 always reports a forward slash.

* Find rar sets in the job's sub-directories

* Wait for the direct unpacker to finish

* Update nzf.setname and nzf.vol after deobfuscating names

* Fix double extraction

* Ruff
2026-08-19 11:13:39 +02:00
mnightingale adde7b961c Fix rss "Clear Downloaded" did not appear to do anything (#3569) 2026-08-18 12:30:38 +02:00
mnightingale 9639d651f4 Don't let uvicorn logging pollute the log and notifications (#3579) 2026-08-18 12:26:26 +02:00
mnightingale f481496e5d Fix rss clearing at startup and downloaded items still in the feed (#3576)
* Only purge unconfigured rss feeds at startup

* Only perform remove obsolete actions if the feed was read

* Update seen_at so downloaded items are not removed while still in the feed

* Increase hardcoded retention to 7 days
2026-08-18 11:46:51 +02:00
mnightingale d45a1d8bf0 Fix pytest-xdist failure due to selenium driver in class global (#3573)
* Fix pytest-xdist failure due to selenium driver in class global

* ruff
2026-08-16 22:41:03 +02:00
mnightingale 59218a687d Fix race in TestAddingNZBsClean and share the NZB-adding helpers (#3571)
Jobs are listed in the history output as soon as they enter the
post-processing queue, and a stopped job has its status set to Failed at
the very start of process_job. The duplicate alternative is only released
near the end of it, so waiting for the job to show up in the history as
failed does not guarantee the alternative was already released. The gap
is tiny when running serially, but wide enough to fail the test under the
load of pytest-xdist. Wait for the label to be dropped instead.

Also move the helpers shared by both adding-NZB test classes into a base
class in testhelper, so the clean tests no longer have to import the
other test module to copy its methods.
2026-08-16 15:46:05 +02:00
mnightingale 35d5355f49 Replace CherryPy with Uvicorn and Starlette (#3550)
* Migrate web interface from CherryPy to Uvicorn/Starlette

Squashed rebase of feature/uvicorn (34 commits) onto develop, reconciled
with ~3 months of intervening develop changes.

Replaces the CherryPy webserver and request handling with Uvicorn/Starlette
across the API, web interface, RSS, config pages and related modules.

Reconciliation with develop during the rebase:
- api.py: kept develop's security/behaviour fixes (orphan path-traversal
  guard, expanded log redaction incl. host_whitelist and
  remote_label_replacement, get_dconfig single-return, get_retryable_jobs,
  connections default, translated NNTP test errors) on top of the Starlette
  request/response rewrite.
- interface.py: ported the RSS route handlers to develop's DB-backed
  RSSRepository API (process_feed, rss_repository / find_job_by_url /
  clear_feed / clear_downloaded / flag_downloaded).
- misc.py: kept develop's hachoir-based get_media_duration.
- requirements.txt: dropped the CherryPy stack, adopted develop's newer pins.

Also applied ruff --fix (PEP 604 unions, builtin generics) to align with
develop's lint config.

Verified: ruff check, black --check, and the affected test suites
(9413 passed, 1 skipped) all pass.

* Update starlette/uvicorn versions

* Fix race issues in global rss state

* Fix test race in server shutdown

The uvicorn migration turned /shutdown (and the shutdown API) into fire-and-forget: it spawned shutdown_program() in a background thread and replied immediately, whereas develop ran it synchronously and only replied once halt() had persisted all state. Because the module-scoped test teardown doesn't wait for the process to exit, the next module's clean_cache_dir wiped the shared cache dir (and reused the fixed port) while the previous instance was still saving state and holding the port — producing the three intermittent failures (deleted sabnzbd.log → "File log disabled or not found"; un-persisted [sorters] → KeyError; stale instance → missing wizard .quoteBlock).

* Fix robots and description, add favicon

* Remove remains of http basic auth

* Setup Starlette once configuration is available, fix static file relative cwd and url_base config

* abort_and_show_error when webserver fails to start

* Guard stopping webserver that never started

* Delegate XFF handling to ProxyHeadersMiddleware

* Merged params at request.state.params instead of modifying private apis

* Both shutdown routes share implementation and do not block event loop

* Run sync handlers via run_in_threadpool and facilitate eventual migration to async

* Pool database connections

* Online backup of database due to WAL changes

* Fix exception on None request.client (test clients or unix sockets)

* Fix flakey tests due to process not fully shutting down

* Restore X-Frame-Options behaviour via middleware

* Fix set_config_default with multiple keywords

* Remove broken logging call

* Restore api logging functionality

* Cache-Control: no-store

* Login only via POST

* Remove 401 (basic-auth) and add 404 handling via redirect

* Fix crash when shutdown not an int

* Use BaseRedirectResponse helper

* Remove trailing slashes from wizard routes

* URL helper, absolute URLs everywhere, fixes issues with nested navigation

* Fix scheduler adding multiple daysofweek

* Restore CherryPy api behaviour merging body with query params (body wins)

* Clearer documentation of get_request_params and request_params

* First stage supporting gradual api async

* Fix rss ajax consuming flash

* Restore access log functionality

* Hostname check in middleware

* Request logging in middleware

* Param parsing in middleware

* Security checks in middleware

* secured_expose is now purely route registration

* Lookup api handler once per request

* Fix flakey alert dialogs

* Trigger restart via BackgroundTask

* Restore CherryPy first param wins and get/post consistency

* Remove dead code

* Secure cookies based on protocol the client used

* Fix various issues with port_is_free

1. port_is_free answered the wrong question. It connect-probed ("is something answering?") rather than bind-probed ("can I bind?"). A port could report free and then kill startup at uvicorn's bind().
2. The bind-all remap crossed address families. :: was mapped to 127.0.0.1, probing IPv4 for an IPv6 bind — a regression against portend, which maps :: → ::1.
3. The call sites passed the wrong host. browserhost is a client-reachable address; the thing that has to be bindable is web_host.
4. Errors were swallowed. A bare except OSError hid gaierror, so an unresolvable host reported "free".
5. find_free_port had a port-0 trap. Under a bind-probe, currentport=0 always succeeds and returned 0 — the old failure sentinel. Now guarded, and None instead of 0.
6. Ports 80/443 were misdiagnosed. EACCES was folded into "occupied", producing ten futile probes and a panic claiming another program held the port. PermissionError now propagates to a dedicated panic explaining the actual remedies.
7. The tests were largely tautological. Three tests covering one branch, an IPv6 test with no IPv6 in it, a timeout test that never engaged the timeout, TOCTOU-prone fixed-range probes, no SO_REUSEADDR on the helper listener, and nothing asserting the property that matters — that "free" implies bindable.
8. A portability bug I introduced, then fixed. I'd baked Linux SO_REUSEADDR overlap semantics into four assertions; macOS differs. Now platform-aware, with the IPv6 regression re-covered by checking the socket family directly.

* Claim the bind address for uvicorn on startup, resolves "49" in err handling from cherrypy

* Rename function BaseRedirectResponse to base_redirect_response

* Restore error response on change web directory

* Add missing typings

* Fix return type of retry job for future types

* A better fix for xdist compatibility - test overwrote db_path

* Secure session cookies (rss flash)

* Inline or remove some functions

* Retry job futuretype behaviour

* Sneak a worksteal fix in

* Test and fix retry_job futuretype behaviour
2026-08-11 13:27:51 +01:00
mnightingale ec6ba3baae Speed up tests with pytest-xdist (#3546)
* Speed up tests with pytest-xdist

* Ignore request failures when the server disappeared

* Fix tests which modify module globals

* Fix lang change leaking

* Fix transaction compilation order breaking tests

* Always load a clean config and fix related tests

* No module globals

* Fix other test classes assigning module level by making calls

* Replace from sabnzbd.cfg import so monkeypatch works

* More global poisoning

* Fix dependant tests which worksteal breaks

* More global vars

* Drop loadscope until more failures are resolved

* Due to how pytest-xdist is implemented, the -s/--capture=no option does not work

* monkeypatch db_path

* Also patch startup_done

* loadscope
2026-08-10 17:44:18 +01:00
Safihre ba36e62695 Refactor Config object (#3552) 2026-08-10 09:24:53 +02:00
mnightingale 3d7f5d4289 Relax disk space requirements when not unpacking and on the same device (#3535)
* Test existing behaviour

* Relax disk space requirements when not unpacking and the same device
2026-08-03 11:44:11 +02:00
mnightingale dc5620a52c cgroup-aware memory limits for the article cache (#3528)
* Apply memory limits from cgroups

* Apply cgroup limits to article cache

* Fix this bug

* _physical_memory return None when undetermined

* Mark platform on test group

* Remove MemoryInfo dataclass

* Cap memory with reservation

* Keep the slightly broken 32 vs 64 bit code

* 512 *MEBI because the numbers look nicer
2026-07-31 09:42:47 +03:00
mnightingale 47a507f5a6 Fix flaky test_api_queue_sort test and to_unit at boundaries (#3522)
* Fix flaky sort test by comparing at the same resolution

* Fix consistency of values printed at reduced precision

* Replace the float-string roundtrip with rounding and adjust to next unit if required

* Comment styling
2026-07-28 17:59:47 +03:00
Parsa YazdaniandQuixThe2nd 404060373b feat: add 'Sort by Remaining Size' queue sort option (#3512)
* feat: add 'Sort by Remaining Size' queue sort option

Add a new 'remaining_bytes' sort field that sorts the queue by absolute
remaining bytes (bytes - bytes_tried), complementing the existing
'remaining' sort which uses percentage downloaded and the 'size' sort
which uses total bytes.

Changes:
- nzbqueue.py: add remaining_bytes sort field, fix update_sort_order()
  to parse field+direction from config instead of hardcoding 'remaining'
- skintext.py: add Glitter-sortRemainingBytesAsc/Desc translation strings,
  update auto_sort explanation text
- Glitter UI: add dropdown entries and JS handlers for both directions
- Config: add auto_sort options for remaining_bytes asc/desc
- Tests: parametrize remaining_bytes asc/desc against sizeleft slot field

* style: wrap long string for Black formatting

* fix: limit remaining-size sorting to ascending

---------

Co-authored-by: QuixThe2nd <QuixThe2nd@users.noreply.github.com>
2026-07-26 16:28:08 +03:00
mnightingale db57321cb8 Fix postproc scripts on Windows (#3514)
* Verify external_processing failure on Windows

* Implement fix

* Clip complete_dir and newfiles in external_processing
2026-07-25 11:50:20 +03:00
mnightingale b825636d3f Fix crc32 may be being combined out-of-order (#3519) 2026-07-25 11:47:23 +03:00
mnightingale ba2099d006 Add RSS Age rule (#3498)
* RSS age rule

* Support years (y) and months (mo) and do not approximate durations in seconds

* Better handling of entries with no age

* Formatting

* Allow >=, =>, <=, and =< aliases

* Implement feedback

* Fix existing issue with From SxxEyy in default row

* Test unitless ages
2026-07-16 21:24:41 +02:00