fix(service): redact proxy credentials in logs

Proxy URIs may contain embedded userinfo (username/password). Add a small sanitizer helper and use it for proxy mapping and proxy selection logs to avoid leaking credentials.
This commit is contained in:
Andy
2026-02-07 20:36:25 -07:00
parent 984a8b9efa
commit b9fb928292

View File

@@ -5,7 +5,7 @@ from collections.abc import Generator
from http.cookiejar import CookieJar from http.cookiejar import CookieJar
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Union
from urllib.parse import urlparse from urllib.parse import urlparse, urlunparse
import click import click
import m3u8 import m3u8
@@ -27,6 +27,45 @@ from unshackle.core.tracks import Chapters, Tracks
from unshackle.core.utilities import get_cached_ip_info, get_ip_info from unshackle.core.utilities import get_cached_ip_info, get_ip_info
def sanitize_proxy_for_log(uri: Optional[str]) -> Optional[str]:
"""
Sanitize a proxy URI for logs by redacting any embedded userinfo (username/password).
Examples:
- http://user:pass@host:8080 -> http://REDACTED@host:8080
- socks5h://user@host:1080 -> socks5h://REDACTED@host:1080
"""
if uri is None:
return None
if not isinstance(uri, str):
return str(uri)
if not uri:
return uri
try:
parsed = urlparse(uri)
# Handle schemeless proxies like "user:pass@host:port"
if not parsed.scheme and not parsed.netloc and "@" in uri and "://" not in uri:
# Parse as netloc using a dummy scheme, then strip scheme back out.
dummy = urlparse(f"http://{uri}")
netloc = dummy.netloc
if "@" in netloc:
netloc = f"REDACTED@{netloc.split('@', 1)[1]}"
# urlparse("http://...") sets path to "" for typical netloc-only strings; keep it just in case.
return f"{netloc}{dummy.path}"
netloc = parsed.netloc
if "@" in netloc:
netloc = f"REDACTED@{netloc.split('@', 1)[1]}"
return urlunparse(parsed._replace(netloc=netloc))
except Exception:
if "@" in uri:
return f"REDACTED@{uri.split('@', 1)[1]}"
return uri
class Service(metaclass=ABCMeta): class Service(metaclass=ABCMeta):
"""The Service Base Class.""" """The Service Base Class."""
@@ -75,7 +114,9 @@ class Service(metaclass=ABCMeta):
# Check if there's a mapping for this query # Check if there's a mapping for this query
mapped_value = proxy_map.get(full_proxy_key) mapped_value = proxy_map.get(full_proxy_key)
if mapped_value: if mapped_value:
self.log.info(f"Found service-specific proxy mapping: {full_proxy_key} -> {mapped_value}") self.log.info(
f"Found service-specific proxy mapping: {full_proxy_key} -> {sanitize_proxy_for_log(mapped_value)}"
)
# Query the proxy provider with the mapped value # Query the proxy provider with the mapped value
if proxy_provider_name: if proxy_provider_name:
# Specific provider requested # Specific provider requested
@@ -87,9 +128,13 @@ class Service(metaclass=ABCMeta):
mapped_proxy_uri = proxy_provider.get_proxy(mapped_value) mapped_proxy_uri = proxy_provider.get_proxy(mapped_value)
if mapped_proxy_uri: if mapped_proxy_uri:
proxy = mapped_proxy_uri proxy = mapped_proxy_uri
self.log.info(f"Using mapped proxy from {proxy_provider.__class__.__name__}: {proxy}") self.log.info(
f"Using mapped proxy from {proxy_provider.__class__.__name__}: {sanitize_proxy_for_log(proxy)}"
)
else: else:
self.log.warning(f"Failed to get proxy for mapped value '{mapped_value}', using default") self.log.warning(
f"Failed to get proxy for mapped value '{sanitize_proxy_for_log(mapped_value)}', using default"
)
else: else:
self.log.warning(f"Proxy provider '{proxy_provider_name}' not found, using default proxy") self.log.warning(f"Proxy provider '{proxy_provider_name}' not found, using default proxy")
else: else:
@@ -98,10 +143,14 @@ class Service(metaclass=ABCMeta):
mapped_proxy_uri = proxy_provider.get_proxy(mapped_value) mapped_proxy_uri = proxy_provider.get_proxy(mapped_value)
if mapped_proxy_uri: if mapped_proxy_uri:
proxy = mapped_proxy_uri proxy = mapped_proxy_uri
self.log.info(f"Using mapped proxy from {proxy_provider.__class__.__name__}: {proxy}") self.log.info(
f"Using mapped proxy from {proxy_provider.__class__.__name__}: {sanitize_proxy_for_log(proxy)}"
)
break break
else: else:
self.log.warning(f"No provider could resolve mapped value '{mapped_value}', using default") self.log.warning(
f"No provider could resolve mapped value '{sanitize_proxy_for_log(mapped_value)}', using default"
)
if not proxy: if not proxy:
# don't override the explicit proxy set by the user, even if they may be geoblocked # don't override the explicit proxy set by the user, even if they may be geoblocked