"""Serving Sublarr under a reverse-proxy path prefix. The settings page has offered a "Base URL" field for a long time. It stored the value and nothing ever read it: no route, no template, no link builder. Setting it did nothing at all. Wiring it follows the convention of the *arr applications this sits next to — the application answers under the prefix itself, so the proxy can forward the path unchanged (``proxy_pass http://host:5765;`` with no trailing slash). The un-prefixed paths keep working too, deliberately: a wrong value in that field would otherwise lock the user out of the UI that fixes it. Two things have to line up for the browser: * requests to ``//api/v1/...`` must reach the same routes as ``/api/v1/...``; * the served ``index.html`` must carry ```` so the relative asset URLs in the bundle resolve there and not against whatever deep client-side route the user reloaded on. """ from __future__ import annotations import pytest from db.config import save_config_entry def _set_base_url(client, value: str) -> None: """Store the value and reload settings — exactly what PUT /config does. The prefix is read off the settings singleton because the middleware runs before Flask has an application context, so a test that only writes the config entry would not see the prefix take effect and would prove nothing. """ from config import reload_settings from db.config import get_all_config_entries with client.application.app_context(): save_config_entry("base_url", value) reload_settings(get_all_config_entries()) @pytest.fixture def prefixed(client): """Set a base URL the way the settings page does.""" _set_base_url(client, "/sublarr") return client def test_api_answers_under_the_prefix(prefixed): resp = prefixed.get("/sublarr/api/v1/health") assert resp.status_code == 200, resp.get_data(as_text=True)[:200] assert "status" in resp.get_json(), resp.get_json() def test_api_still_answers_without_the_prefix(prefixed): """A wrong Base URL must not lock the user out of fixing it.""" resp = prefixed.get("/api/v1/health") assert resp.status_code == 200 assert "status" in resp.get_json(), resp.get_json() def test_unset_base_url_changes_nothing(client): _set_base_url(client, "") resp = client.get("/api/v1/health") assert resp.status_code == 200 assert "status" in resp.get_json(), resp.get_json() def test_a_prefix_that_only_looks_like_one_is_not_stripped(prefixed): """``/sublarrX`` shares a prefix string but is a different location. It must not reach the API. What it gets instead is the SPA catch-all, the same as any other unknown path — so the check is "did not reach health", not a status code. """ resp = prefixed.get("/sublarrX/api/v1/health") assert "status" not in (resp.get_json() or {}), resp.get_json() @pytest.mark.parametrize("stored", ["/sublarr", "sublarr", "/sublarr/", " /sublarr// "]) def test_the_shapes_a_user_types_all_work(client, stored): _set_base_url(client, stored) body = client.get("/sublarr/api/v1/health").get_json() assert "status" in (body or {}), body def test_index_html_carries_the_base_element(prefixed, tmp_path, monkeypatch): from flask import current_app static = tmp_path / "static" static.mkdir() (static / "index.html").write_text( '' '' "
", encoding="utf-8", ) with prefixed.application.app_context(): current_app.static_folder = str(static) body = prefixed.get("/sublarr/").get_data(as_text=True) assert ' alone: an inline script is # blocked by the strict script-src CSP, so it never ran (1.14.0-1.15.0). assert "", '/x";alert(1);//', "/x", "/x\\", "javascript:alert(1)", "//evil.example.com", ], ) def test_a_hostile_value_cannot_escape_either_sink(self, hostile): from base_path import inject_base_into_index, normalize_base_path shell = '' out = inject_base_into_index(shell, normalize_base_path(hostile)) assert "" not in out, out assert "onload=" not in out, out # exactly one base element survives, and no script at all assert out.count("" not in out, out @pytest.mark.parametrize( "bad", ['/x" onload="y', "/x