Files
BanGUI/backend/app/services/blocklist_ban_executor.py
Lukas e08a16c7dd Refactor: Split blocklist import flow into focused components
Extracted the monolithic import_source() function (776 lines) into focused,
testable components with clear single responsibilities:

- BlocklistDownloader: HTTP download with exponential backoff retry logic
  * Handles transient failures (429, 5xx errors, timeouts)
  * Configurable retry attempts and backoff strategy
  * 93% test coverage

- BlocklistParser: Parse and validate IP addresses
  * Extract valid IPv4/IPv6 addresses from text
  * Skip CIDRs and malformed entries gracefully
  * Separate parsing from validation concerns
  * 100% test coverage

- BanExecutor: Ban execution with error handling
  * Ban IPs via fail2ban socket
  * Stop on JailNotFoundError (jail doesn't exist)
  * Continue on JailOperationError (individual ban failures)
  * 100% test coverage

- BlocklistImportWorkflow: Thin orchestrator
  * Coordinates the download → parse → ban → log flow
  * Pre-warms geo cache with newly banned IPs
  * 96% test coverage

- blocklist_service.py: Maintains public API
  * Source CRUD (create, read, update, delete)
  * URL validation and preview functionality
  * Scheduling configuration and import triggers
  * 92% test coverage

Benefits:
* Each component is independently testable with mock dependencies
* Error handling is explicit and localized
* Components can evolve independently
* Logging is contextual and clear
* Retry and transient error handling are isolated

Testing:
* All 36 existing blocklist_service tests pass
* All 13 blocklist import task tests pass
* Added 17 comprehensive component unit tests
* Combined 96%+ coverage on new modules
* Zero type errors in new code

Documentation:
* Updated Refactoring.md with detailed architecture notes
* Added component architecture diagram to Architekture.md
* Documented ownership and responsibilities of each component

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-27 18:34:11 +02:00

85 lines
2.5 KiB
Python

"""Blocklist ban executor component.
Executes bans via fail2ban for a list of IP addresses, handling errors and
logging failures.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import structlog
from app.exceptions import JailNotFoundError, JailOperationError
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
log: structlog.stdlib.BoundLogger = structlog.get_logger()
class BanExecutor:
"""Executes bans via fail2ban for blocklist-sourced IPs."""
def __init__(
self,
ban_ip: Callable[[str, str, str], Awaitable[None]],
) -> None:
"""Initialize the ban executor.
Args:
ban_ip: Async callable that bans an IP in a jail.
Signature: async def ban_ip(socket_path: str, jail: str, ip: str) -> None
"""
self.ban_ip = ban_ip
async def ban_ips(
self,
socket_path: str,
jail: str,
ips: list[str],
) -> tuple[int, int, str | None]:
"""Ban a list of IPs in the specified fail2ban jail.
On first JailNotFoundError, stops processing (the jail doesn't exist).
On JailOperationError, records the error but continues with next IPs.
Other exceptions are treated as fatal and raised.
Args:
socket_path: Path to fail2ban Unix socket.
jail: Name of the fail2ban jail.
ips: List of IP addresses to ban.
Returns:
Tuple of (successful bans count, failed bans count, first error or None).
Raises:
Exception: If an unexpected error occurs (not JailNotFoundError or
JailOperationError).
"""
successful = 0
failed = 0
first_error: str | None = None
for ip in ips:
try:
await self.ban_ip(socket_path, jail, ip)
successful += 1
except JailNotFoundError as exc:
# Jail doesn't exist — no point continuing
first_error = str(exc)
log.warning(
"blocklist_jail_not_found",
jail=jail,
error=str(exc),
)
break
except JailOperationError as exc:
# Individual ban failed, but continue
failed += 1
if first_error is None:
first_error = str(exc)
log.debug("blocklist_ban_failed", ip=ip, error=str(exc))
return successful, failed, first_error