steam-backlog-enforcer/steam_backlog_enforcer/store_blocker.py
Krzysztof kuhy Rudnicki 57d262b711
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Pylint gate 10 over the whole package
Raise the pylint hook to --fail-under=10.0 and make it lint the whole
package on every run (pass_filenames: false): over the staged subset a
9.99 tree passed at commit time.

Ports de2aea3 (9b9342ba-1) onto main, minus its web/ and requirements.txt
bumps and the iptables dedupe f1822f1 already landed; _hltb_confidence and
_hltb_search keep main's 429 handling. _search_batch splits out
_acquire_auth / _search_headers, and _stats.py is back to 250 lines.

Drops the min-similarity-lines = 8 floor (0 duplicate-code hits at the
default) and sets max-attributes = 15 for the flat persisted records, as
kuhy decided. Tests stay outside pylint, as at every site already at 10.

Todo: fda71ed1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 16:57:35 +01:00

184 lines
6.2 KiB
Python

"""Block Steam Store access via /etc/hosts (hosts install script) and iptables.
/etc/hosts is protected by guard-lib (~/guard-lib, file-guard instance
"hosts"): chattr +i, a read-only self-bind-mount, and a systemd path-unit
watcher. This module checks if the Steam Store domains are already blocked
in /etc/hosts. If not, it runs the hosts install.sh (which must already
contain the Steam Store entries in its heredoc), going through guard-lib's
unlock/relock around any edit. As a belt-and-suspenders fallback, it also
blocks via iptables.
"""
from __future__ import annotations
import logging
import subprocess
from steam_backlog_enforcer._hosts_protection import (
_reblock_hosts,
rewrite_hosts_lines,
)
from steam_backlog_enforcer._store_iptables import (
_block_store_iptables,
_is_iptables_blocked,
_unblock_store_iptables,
flush_dns_cache,
)
from steam_backlog_enforcer._store_tools import (
BASH,
HOSTS_INSTALL_SCRIPT,
HOSTS_REDIRECT_IP,
SUDO,
)
from steam_backlog_enforcer.config import (
BLOCKED_DOMAINS,
HOSTS_FILE,
)
logger = logging.getLogger(__name__)
# The install script rewrites /etc/hosts and re-applies its guards.
_HOSTS_INSTALL_TIMEOUT_SECONDS = 120
def is_store_blocked() -> bool:
"""Check if Steam Store domains are blocked in /etc/hosts or iptables."""
return hosts_blocks_store() or _is_iptables_blocked()
def hosts_blocks_store() -> bool:
"""Check /etc/hosts alone: a file read, no subprocess, safe to poll."""
try:
content = HOSTS_FILE.read_text(encoding="utf-8")
# Check for at least the primary store domain.
if "store.steampowered.com" in content:
# Verify it's actually blocked (not commented out).
for line in content.splitlines():
stripped = line.strip()
if (
not stripped.startswith("#")
and "store.steampowered.com" in stripped
and stripped.startswith(HOSTS_REDIRECT_IP)
):
return True
except OSError:
pass
return False
def block_store() -> bool:
"""Block Steam Store: uncomment hosts entries, or run install script.
Returns True if at least one blocking method succeeded.
"""
if is_store_blocked():
logger.info("Steam Store already blocked in /etc/hosts.")
return True
# Try quick re-block (uncomment lines) first.
if _reblock_hosts() and is_store_blocked():
_block_store_iptables()
flush_dns_cache()
return True
# Fall back to the full hosts install script.
hosts_ok = _block_via_hosts_install()
ipt_ok = _block_store_iptables()
if hosts_ok or ipt_ok:
flush_dns_cache()
return True
logger.error("All store-blocking methods failed.")
return False
def _block_via_hosts_install() -> bool:
"""Run the hosts install.sh to apply /etc/hosts with Steam Store entries.
The install script handles: immutable flag removal, bind mount remounting,
writing the file, re-applying protections, and DoH disabling.
"""
if is_store_blocked():
logger.info("Steam Store already blocked in /etc/hosts.")
return True
if not HOSTS_INSTALL_SCRIPT.exists():
logger.error("hosts install script not found at %s", HOSTS_INSTALL_SCRIPT)
return False
try:
logger.info("Running hosts install script to block Steam Store...")
result = subprocess.run(
[SUDO, BASH, str(HOSTS_INSTALL_SCRIPT), "--no-flush-dns"],
capture_output=True,
text=True,
timeout=_HOSTS_INSTALL_TIMEOUT_SECONDS,
check=False,
)
except OSError, subprocess.SubprocessError:
logger.exception("Failed to run hosts install script")
return False
if result.returncode == 0:
logger.info("hosts install script succeeded.")
return True
logger.error(
"hosts install script failed (rc=%d): %s",
result.returncode,
result.stderr[-500:] if result.stderr else result.stdout[-500:],
)
return False
def unblock_store() -> bool:
"""Remove Steam Store blocks from both iptables and /etc/hosts."""
ipt_ok = _unblock_store_iptables()
hosts_ok = _unblock_hosts()
flush_dns_cache()
if not ipt_ok:
logger.warning("Failed to remove iptables rules.")
if not hosts_ok:
logger.warning("Failed to remove /etc/hosts entries.")
return ipt_ok or hosts_ok
# ──────────────────────────────────────────────────────────────
# /etc/hosts protection helpers
#
# /etc/hosts is managed by guard-lib (see ~/guard-lib) as file-guard
# instance "hosts": chattr +i, a read-only self-bind-mount, and a
# systemd path-unit watcher. "pacman-unlock" stops the watcher and
# collapses the bind mount (same primitive guard-lib's own pacman hooks
# use). Relocking uses "sync", NOT "pacman-relock" - pacman-relock calls
# fg_enforce, which treats any diff from the (stale) canonical as drift
# and reverts it; that's correct for pacman's own hook flow (undo
# unwanted tampering) but wrong here, where *we* are the one legitimately
# changing content - it would silently undo our own edit. "sync" instead
# adopts the just-written content as the new canonical.
# ──────────────────────────────────────────────────────────────
def _unblock_hosts() -> bool:
"""Comment out Steam Store entries in /etc/hosts."""
if not is_store_blocked():
logger.info("Steam Store not blocked in /etc/hosts, nothing to do.")
return True
return rewrite_hosts_lines(
_comment_out_blocked, "Commented out Steam Store entries in /etc/hosts."
)
def _comment_out_blocked(line: str) -> str | None:
"""The line commented out, if it is a live redirect of a blocked domain."""
stripped = line.strip()
if (
not stripped.startswith("#")
and stripped.startswith(HOSTS_REDIRECT_IP)
and any(d in stripped for d in BLOCKED_DOMAINS)
):
return f"# {line}" if line.endswith("\n") else f"# {line}\n"
return None