mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-14 17:28:25 +00:00
Draft until #1571 merges: this is stacked on it, so the first two commits here are that PR's and only the last one is new. I will rebase once #1571 is in. Shelfmark strips credentials and links out of text in five places, written in different PRs with different patterns: the log formatter, `_safe_url` and `_redact_urls_in_text` in `torrent_utils.py`, `redact_status_message` in `viewer_redaction.py`, the `sed` rules in `genDebug.sh`, and the password handling in the settings registry. Between them there were three regexes that find a URL in text, two separate userinfo patterns and no shared list of what counts as a secret. #1571 made the gap visible, because every pattern added to the logger had to be remembered somewhere else too. This moves the text and URL part into `shelfmark/core/redaction.py`, which imports nothing from the app so the logger can depend on it: - `redact_secrets` (mask in place, for logs) moved from `core/logger.py`, patterns unchanged. The logger imports it. - `safe_url` and `redact_urls_in_text` (keep scheme, host and path, drop the query) moved from `torrent_utils.py` and are public now, so `alldebrid.py` no longer imports a private name from another module. - `strip_links` (remove links entirely) is what `redact_status_message` now calls. It and `redact_urls_in_text` share one link pattern, the one the viewer redaction already used. Two small behaviour changes come from sharing that pattern. A closing bracket or quote right after a URL is no longer swallowed into it when scrubbing an exception message. And the scheme-less path that requests prints after `url: ` now keeps only what is before its query in `redact_urls_in_text`, where before it passed through as it was (the log formatter masked the key, but the path and query stayed in the message). Also here: the two `sed` rules in `genDebug.sh` that mask userinfo now stop at `?` and `#` like the logger pattern does, with a test that fails on the old script. Not in this PR: the debug bundle zips the live log directory as it is, so log lines written before #1571 are not redacted in it. Fixing that means copying the logs to a staging folder and running them through this module, which I would do as a separate change. Tests: a new file covers `safe_url`, `redact_urls_in_text` and `strip_links` directly, and two checks that the viewer and the logger really use these. Full suite passes.
75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
"""The shared helpers that take credentials and links out of text."""
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core import logger as logger_module
|
|
from shelfmark.core import redaction
|
|
from shelfmark.core.viewer_redaction import redact_status_message
|
|
|
|
|
|
class TestSafeUrl:
|
|
def test_keeps_scheme_host_and_path_and_drops_the_query_and_fragment(self):
|
|
url = "https://prowlarr.example:9696/1/download?apikey=s3cr3t&link=abc#frag"
|
|
|
|
assert redaction.safe_url(url) == "https://prowlarr.example:9696/1/download"
|
|
|
|
def test_is_cut_to_the_limit(self):
|
|
assert len(redaction.safe_url("https://t.example/" + "a" * 500, limit=40)) == 40
|
|
|
|
@pytest.mark.parametrize("value", ["magnet:?xt=urn:btih:abc", "not a url", ""])
|
|
def test_a_value_that_is_not_a_url_is_not_echoed(self, value):
|
|
assert redaction.safe_url(value) == "<unparsed url: str>"
|
|
|
|
|
|
class TestRedactUrlsInText:
|
|
def test_every_url_loses_its_query(self):
|
|
text = "GET https://a.example/x?apikey=k1 then https://b.example/y?token=k2 failed"
|
|
|
|
assert redaction.redact_urls_in_text(text) == (
|
|
"GET https://a.example/x then https://b.example/y failed"
|
|
)
|
|
|
|
def test_a_closing_bracket_or_quote_is_not_part_of_the_link(self):
|
|
text = "failed (https://a.example/x?apikey=k1) and 'https://b.example/y?key=k2'"
|
|
|
|
assert redaction.redact_urls_in_text(text) == (
|
|
"failed (https://a.example/x) and 'https://b.example/y'"
|
|
)
|
|
|
|
def test_the_path_a_failed_request_prints_keeps_everything_before_its_query(self):
|
|
text = (
|
|
"Max retries exceeded with url: /api/v1/indexer/1/download?apikey=s3cr3t&link=abc (x)"
|
|
)
|
|
|
|
assert redaction.redact_urls_in_text(text) == (
|
|
"Max retries exceeded with url: /api/v1/indexer/1/download (x)"
|
|
)
|
|
|
|
def test_text_without_a_link_is_untouched(self):
|
|
assert redaction.redact_urls_in_text("Connection refused") == "Connection refused"
|
|
|
|
|
|
class TestStripLinks:
|
|
def test_replaces_a_full_url(self):
|
|
assert redaction.strip_links("401 for url: https://i.example/dl?apikey=k") == (
|
|
"401 for url: (link removed)"
|
|
)
|
|
|
|
def test_replaces_the_scheme_less_path_after_url(self):
|
|
assert redaction.strip_links("Max retries exceeded with url: /a/b?apikey=k (Caused)") == (
|
|
"Max retries exceeded with url: (link removed) (Caused)"
|
|
)
|
|
|
|
def test_the_scheme_is_case_insensitive(self):
|
|
assert redaction.strip_links("HTTPS://I.example/dl?k=1 failed") == "(link removed) failed"
|
|
|
|
|
|
def test_the_viewer_message_redaction_is_the_shared_one():
|
|
message = "Unauthorized for url: https://i.example/dl?apikey=k"
|
|
|
|
assert redact_status_message(message) == redaction.strip_links(message)
|
|
|
|
|
|
def test_the_log_formatter_masks_with_the_shared_function():
|
|
assert logger_module.redact_secrets is redaction.redact_secrets
|