Refactor backend services and utilities
- Update service layer implementations - Improve configuration handling utilities - Update documentation tasks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,26 +1,3 @@
|
|||||||
### T-07 · Break cross-service import: `jail_config_service` imports `jail_service`
|
|
||||||
|
|
||||||
**Where found:** `backend/app/services/jail_config_service.py` — `import app.services.jail_service as jail_service`
|
|
||||||
|
|
||||||
**Why this is needed:** Services at the same layer should not depend on each other. Shared logic should move to a lower-level utility. This creates a dependency cycle risk and makes both services harder to test independently.
|
|
||||||
|
|
||||||
**Goal:** Shared socket operations extracted to `app/utils/` or `app/utils/jail_socket.py`. No service imports a sibling service.
|
|
||||||
|
|
||||||
**What to do:**
|
|
||||||
1. Identify which functions from `jail_service` are called by `jail_config_service`.
|
|
||||||
2. Extract shared low-level socket helpers to `app/utils/jail_socket.py` (or extend `fail2ban_client.py`).
|
|
||||||
3. Update both services to import from the utility layer.
|
|
||||||
|
|
||||||
**Possible traps and issues:**
|
|
||||||
- Some `jail_service` functions that are called may themselves import geo, config, or other services — trace the full dependency graph before extracting.
|
|
||||||
- APScheduler tasks reference `jail_service` — ensure they still work after any reorganisation.
|
|
||||||
|
|
||||||
**Docs changes needed:** `Docs/Architekture.md` — add rule: services must not import sibling services.
|
|
||||||
|
|
||||||
**Doc references:** `Docs/Architekture.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### T-08 · `_SOCKET_TIMEOUT` defined 6× — `constants.py` constant unused
|
### T-08 · `_SOCKET_TIMEOUT` defined 6× — `constants.py` constant unused
|
||||||
|
|
||||||
**Where found:** `backend/app/utils/constants.py:16` (defines `FAIL2BAN_SOCKET_TIMEOUT_SECONDS = 5.0` but is never imported); `ban_service.py` (5.0), `jail_service.py` (10.0), `config_service.py` (10.0), `server_service.py` (10.0), `log_service.py` (10.0), `jail_config_service.py` (10.0), `config_file_utils.py` (10.0)
|
**Where found:** `backend/app/utils/constants.py:16` (defines `FAIL2BAN_SOCKET_TIMEOUT_SECONDS = 5.0` but is never imported); `ban_service.py` (5.0), `jail_service.py` (10.0), `config_service.py` (10.0), `server_service.py` (10.0), `log_service.py` (10.0), `jail_config_service.py` (10.0), `config_file_utils.py` (10.0)
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ async def _get_active_jail_names(socket_path: str) -> set[str]:
|
|||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
# Allowlist pattern for action names used in path construction.
|
# Allowlist pattern for action names used in path construction.
|
||||||
_SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
_SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ from app.models.ban import (
|
|||||||
from app.repositories import fail2ban_db_repo
|
from app.repositories import fail2ban_db_repo
|
||||||
from app.repositories import history_archive_repo as default_history_archive_repo
|
from app.repositories import history_archive_repo as default_history_archive_repo
|
||||||
from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service
|
from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service
|
||||||
from app.utils.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
|
from app.utils.constants import (
|
||||||
|
DEFAULT_PAGE_SIZE,
|
||||||
|
FAIL2BAN_SOCKET_TIMEOUT,
|
||||||
|
MAX_PAGE_SIZE,
|
||||||
|
)
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
)
|
)
|
||||||
@@ -73,10 +77,6 @@ async def get_fail2ban_db_path(socket_path: str) -> str:
|
|||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 5.0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def ban_ip(socket_path: str, jail: str, ip: str) -> None:
|
async def ban_ip(socket_path: str, jail: str, ip: str) -> None:
|
||||||
"""Ban an IP address in the specified jail."""
|
"""Ban an IP address in the specified jail."""
|
||||||
@@ -85,7 +85,7 @@ async def ban_ip(socket_path: str, jail: str, ip: str) -> None:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["set", jail, "banip", ip]))
|
ok(await client.send(["set", jail, "banip", ip]))
|
||||||
@@ -102,7 +102,7 @@ async def unban_ip(socket_path: str, ip: str, jail: str | None = None) -> None:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
if jail is None:
|
if jail is None:
|
||||||
ok(await client.send(["unban", ip]))
|
ok(await client.send(["unban", ip]))
|
||||||
@@ -254,7 +254,7 @@ async def get_active_bans(
|
|||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
global_status = to_dict(ok(await client.send(["status"])))
|
global_status = to_dict(ok(await client.send(["status"])))
|
||||||
jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip()
|
jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip()
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from app.services.settings_service import (
|
|||||||
from app.services.settings_service import (
|
from app.services.settings_service import (
|
||||||
set_map_color_thresholds as util_set_map_color_thresholds,
|
set_map_color_thresholds as util_set_map_color_thresholds,
|
||||||
)
|
)
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT
|
||||||
from app.utils.fail2ban_client import Fail2BanClient
|
from app.utils.fail2ban_client import Fail2BanClient
|
||||||
from app.utils.fail2ban_response import (
|
from app.utils.fail2ban_response import (
|
||||||
ensure_list,
|
ensure_list,
|
||||||
@@ -61,8 +62,6 @@ from app.utils.fail2ban_response import (
|
|||||||
|
|
||||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Custom exceptions
|
# Custom exceptions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -134,7 +133,7 @@ async def get_jail_config(socket_path: str, name: str) -> JailConfigResponse:
|
|||||||
JailNotFoundError: If *name* is not a known jail.
|
JailNotFoundError: If *name* is not a known jail.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
# Verify existence.
|
# Verify existence.
|
||||||
try:
|
try:
|
||||||
@@ -207,7 +206,7 @@ async def list_jail_configs(socket_path: str) -> JailConfigListResponse:
|
|||||||
Raises:
|
Raises:
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
global_status = to_dict(ok(await client.send(["status"])))
|
global_status = to_dict(ok(await client.send(["status"])))
|
||||||
jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip()
|
jail_list_raw: str = str(global_status.get("Jail list", "") or "").strip()
|
||||||
@@ -272,7 +271,7 @@ async def update_jail_config(
|
|||||||
if err:
|
if err:
|
||||||
raise ConfigValidationError(f"Invalid regex in 'prefregex': {err!r} (pattern: {update.prefregex!r})")
|
raise ConfigValidationError(f"Invalid regex in 'prefregex': {err!r} (pattern: {update.prefregex!r})")
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
# Verify existence.
|
# Verify existence.
|
||||||
try:
|
try:
|
||||||
@@ -391,7 +390,7 @@ async def get_global_config(socket_path: str) -> GlobalConfigResponse:
|
|||||||
Raises:
|
Raises:
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
(
|
(
|
||||||
log_level_raw,
|
log_level_raw,
|
||||||
@@ -424,7 +423,7 @@ async def update_global_config(socket_path: str, update: GlobalConfigUpdate) ->
|
|||||||
ConfigOperationError: If a ``set`` command is rejected.
|
ConfigOperationError: If a ``set`` command is rejected.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
async def _set_global(key: str, value: Fail2BanToken) -> None:
|
async def _set_global(key: str, value: Fail2BanToken) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -476,7 +475,7 @@ async def add_log_path(
|
|||||||
ConfigOperationError: If the command is rejected by fail2ban.
|
ConfigOperationError: If the command is rejected by fail2ban.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["status", jail, "short"]))
|
ok(await client.send(["status", jail, "short"]))
|
||||||
@@ -513,7 +512,7 @@ async def delete_log_path(
|
|||||||
ConfigOperationError: If the command is rejected by fail2ban.
|
ConfigOperationError: If the command is rejected by fail2ban.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["status", jail, "short"]))
|
ok(await client.send(["status", jail, "short"]))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT_FAST
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
Fail2BanConnectionError,
|
Fail2BanConnectionError,
|
||||||
@@ -64,8 +65,7 @@ class Fail2BanMetadataService:
|
|||||||
|
|
||||||
async def _resolve_db_path(self, socket_path: str) -> str:
|
async def _resolve_db_path(self, socket_path: str) -> str:
|
||||||
"""Query fail2ban for the configured database path."""
|
"""Query fail2ban for the configured database path."""
|
||||||
socket_timeout: float = 5.0
|
async with Fail2BanClient(socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT_FAST) as client:
|
||||||
async with Fail2BanClient(socket_path, timeout=socket_timeout) as client:
|
|
||||||
response = await client.send(["get", "dbfile"])
|
response = await client.send(["get", "dbfile"])
|
||||||
|
|
||||||
if not isinstance(response, tuple) or len(response) != 2:
|
if not isinstance(response, tuple) or len(response) != 2:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import structlog
|
|||||||
from app import __version__
|
from app import __version__
|
||||||
from app.models.config import ServiceStatusResponse
|
from app.models.config import ServiceStatusResponse
|
||||||
from app.models.server import ServerStatus
|
from app.models.server import ServerStatus
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT_FAST
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
Fail2BanCommand,
|
Fail2BanCommand,
|
||||||
@@ -35,8 +36,6 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
|||||||
# Internal helpers
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 5.0
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ async def get_service_status(
|
|||||||
if server_status.online:
|
if server_status.online:
|
||||||
client = Fail2BanClient(
|
client = Fail2BanClient(
|
||||||
socket_path=socket_path,
|
socket_path=socket_path,
|
||||||
timeout=_SOCKET_TIMEOUT,
|
timeout=FAIL2BAN_SOCKET_TIMEOUT_FAST,
|
||||||
)
|
)
|
||||||
log_level_raw, log_target_raw = await asyncio.gather(
|
log_level_raw, log_target_raw = await asyncio.gather(
|
||||||
_safe_get_typed(client, ["get", "loglevel"], "INFO"),
|
_safe_get_typed(client, ["get", "loglevel"], "INFO"),
|
||||||
@@ -129,7 +128,7 @@ async def get_service_status(
|
|||||||
|
|
||||||
async def probe(
|
async def probe(
|
||||||
socket_path: str,
|
socket_path: str,
|
||||||
timeout: float = _SOCKET_TIMEOUT,
|
timeout: float = FAIL2BAN_SOCKET_TIMEOUT_FAST,
|
||||||
) -> ServerStatus:
|
) -> ServerStatus:
|
||||||
"""Probe the fail2ban daemon and return a
|
"""Probe the fail2ban daemon and return a
|
||||||
:class:`~app.models.server.ServerStatus`.
|
:class:`~app.models.server.ServerStatus`.
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ async def _get_active_jail_names(socket_path: str) -> set[str]:
|
|||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
# Sections that are not jail definitions.
|
# Sections that are not jail definitions.
|
||||||
_META_SECTIONS: frozenset[str] = frozenset({"INCLUDES", "DEFAULT"})
|
_META_SECTIONS: frozenset[str] = frozenset({"INCLUDES", "DEFAULT"})
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from app.models.jail import (
|
|||||||
)
|
)
|
||||||
from app.services import geo_service
|
from app.services import geo_service
|
||||||
from app.utils.config_file_utils import start_daemon, wait_for_fail2ban
|
from app.utils.config_file_utils import start_daemon, wait_for_fail2ban
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
Fail2BanCommand,
|
Fail2BanCommand,
|
||||||
@@ -73,8 +74,6 @@ class IpLookupResult(TypedDict):
|
|||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
# Capability detection for optional fail2ban transmitter commands (backend, idle).
|
# Capability detection for optional fail2ban transmitter commands (backend, idle).
|
||||||
# These commands are not supported in all fail2ban versions. Caching the result
|
# These commands are not supported in all fail2ban versions. Caching the result
|
||||||
# avoids sending unsupported commands every polling cycle and spamming the
|
# avoids sending unsupported commands every polling cycle and spamming the
|
||||||
@@ -223,7 +222,7 @@ async def list_jails(socket_path: str) -> JailListResponse:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
# 1. Fetch global status to get jail names.
|
# 1. Fetch global status to get jail names.
|
||||||
global_status = to_dict(ok(await client.send(["status"])))
|
global_status = to_dict(ok(await client.send(["status"])))
|
||||||
@@ -376,7 +375,7 @@ async def get_jail(socket_path: str, name: str) -> JailDetailResponse:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
# Verify the jail exists by sending a status command first.
|
# Verify the jail exists by sending a status command first.
|
||||||
try:
|
try:
|
||||||
@@ -493,7 +492,7 @@ async def start_jail(socket_path: str, name: str) -> None:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["start", name]))
|
ok(await client.send(["start", name]))
|
||||||
log.info("jail_started", jail=name)
|
log.info("jail_started", jail=name)
|
||||||
@@ -518,7 +517,7 @@ async def stop_jail(socket_path: str, name: str) -> None:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["stop", name]))
|
ok(await client.send(["stop", name]))
|
||||||
log.info("jail_stopped", jail=name)
|
log.info("jail_stopped", jail=name)
|
||||||
@@ -548,7 +547,7 @@ async def set_idle(socket_path: str, name: str, *, on: bool) -> None:
|
|||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
state = "on" if on else "off"
|
state = "on" if on else "off"
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["set", name, "idle", state]))
|
ok(await client.send(["set", name, "idle", state]))
|
||||||
log.info("jail_idle_toggled", jail=name, idle=on)
|
log.info("jail_idle_toggled", jail=name, idle=on)
|
||||||
@@ -578,7 +577,7 @@ async def reload_jail(socket_path: str, name: str) -> None:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["reload", name, [], [["start", name]]]))
|
ok(await client.send(["reload", name, [], [["start", name]]]))
|
||||||
log.info("jail_reloaded", jail=name)
|
log.info("jail_reloaded", jail=name)
|
||||||
@@ -608,7 +607,7 @@ async def restart(socket_path: str) -> None:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["stop"]))
|
ok(await client.send(["stop"]))
|
||||||
log.info("fail2ban_stopped_for_restart")
|
log.info("fail2ban_stopped_for_restart")
|
||||||
@@ -619,7 +618,7 @@ async def restart(socket_path: str) -> None:
|
|||||||
async def restart_daemon(
|
async def restart_daemon(
|
||||||
socket_path: str,
|
socket_path: str,
|
||||||
start_cmd_parts: list[str],
|
start_cmd_parts: list[str],
|
||||||
max_wait_seconds: float = _SOCKET_TIMEOUT,
|
max_wait_seconds: float = FAIL2BAN_SOCKET_TIMEOUT,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Restart the fail2ban daemon and verify it comes back online.
|
"""Restart the fail2ban daemon and verify it comes back online.
|
||||||
|
|
||||||
@@ -781,7 +780,7 @@ async def get_jail_banned_ips(
|
|||||||
# Clamp page_size to the allowed maximum.
|
# Clamp page_size to the allowed maximum.
|
||||||
page_size = min(page_size, _MAX_PAGE_SIZE)
|
page_size = min(page_size, _MAX_PAGE_SIZE)
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
# Verify the jail exists.
|
# Verify the jail exists.
|
||||||
try:
|
try:
|
||||||
@@ -896,7 +895,7 @@ async def get_ignore_list(socket_path: str, name: str) -> list[str]:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
raw = ok(await client.send(["get", name, "ignoreip"]))
|
raw = ok(await client.send(["get", name, "ignoreip"]))
|
||||||
return ensure_list(raw)
|
return ensure_list(raw)
|
||||||
@@ -926,7 +925,7 @@ async def add_ignore_ip(socket_path: str, name: str, ip: str) -> None:
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Invalid IP address or network: {ip!r}") from exc
|
raise ValueError(f"Invalid IP address or network: {ip!r}") from exc
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["set", name, "addignoreip", ip]))
|
ok(await client.send(["set", name, "addignoreip", ip]))
|
||||||
log.info("ignore_ip_added", jail=name, ip=ip)
|
log.info("ignore_ip_added", jail=name, ip=ip)
|
||||||
@@ -950,7 +949,7 @@ async def del_ignore_ip(socket_path: str, name: str, ip: str) -> None:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["set", name, "delignoreip", ip]))
|
ok(await client.send(["set", name, "delignoreip", ip]))
|
||||||
log.info("ignore_ip_removed", jail=name, ip=ip)
|
log.info("ignore_ip_removed", jail=name, ip=ip)
|
||||||
@@ -975,7 +974,7 @@ async def get_ignore_self(socket_path: str, name: str) -> bool:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
raw = ok(await client.send(["get", name, "ignoreself"]))
|
raw = ok(await client.send(["get", name, "ignoreself"]))
|
||||||
return bool(raw)
|
return bool(raw)
|
||||||
@@ -1000,7 +999,7 @@ async def set_ignore_self(socket_path: str, name: str, *, on: bool) -> None:
|
|||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
value = "true" if on else "false"
|
value = "true" if on else "false"
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
ok(await client.send(["set", name, "ignoreself", value]))
|
ok(await client.send(["set", name, "ignoreself", value]))
|
||||||
log.info("ignore_self_toggled", jail=name, on=on)
|
log.info("ignore_self_toggled", jail=name, on=on)
|
||||||
@@ -1047,7 +1046,7 @@ async def lookup_ip(
|
|||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
raise ValueError(f"Invalid IP address: {ip!r}") from exc
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
with contextlib.suppress(ValueError, Fail2BanConnectionError):
|
with contextlib.suppress(ValueError, Fail2BanConnectionError):
|
||||||
# Use fail2ban's "banned <ip>" command which checks all jails.
|
# Use fail2ban's "banned <ip>" command which checks all jails.
|
||||||
@@ -1120,7 +1119,7 @@ async def unban_all_ips(socket_path: str) -> int:
|
|||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
~app.utils.fail2ban_client.Fail2BanConnectionError: If the socket
|
||||||
cannot be reached.
|
cannot be reached.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
count: int = int(str(ok(await client.send(["unban", "--all"])) or 0))
|
count: int = int(str(ok(await client.send(["unban", "--all"])) or 0))
|
||||||
log.info("all_ips_unbanned", count=count)
|
log.info("all_ips_unbanned", count=count)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.models.config import (
|
|||||||
RegexTestResponse,
|
RegexTestResponse,
|
||||||
)
|
)
|
||||||
from app.utils.async_utils import run_blocking
|
from app.utils.async_utils import run_blocking
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
Fail2BanConnectionError,
|
Fail2BanConnectionError,
|
||||||
@@ -30,8 +31,6 @@ from app.utils.fail2ban_response import ok
|
|||||||
|
|
||||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
_NON_FILE_LOG_TARGETS: frozenset[str] = frozenset(
|
_NON_FILE_LOG_TARGETS: frozenset[str] = frozenset(
|
||||||
{"STDOUT", "STDERR", "SYSLOG", "SYSTEMD-JOURNAL"}
|
{"STDOUT", "STDERR", "SYSLOG", "SYSTEMD-JOURNAL"}
|
||||||
)
|
)
|
||||||
@@ -85,7 +84,7 @@ async def read_fail2ban_log(
|
|||||||
validates that the target is a readable file, then returns the last
|
validates that the target is a readable file, then returns the last
|
||||||
*lines* entries optionally filtered by *filter_text*.
|
*lines* entries optionally filtered by *filter_text*.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
log_level_raw, log_target_raw = await asyncio.gather(
|
log_level_raw, log_target_raw = await asyncio.gather(
|
||||||
_safe_get_typed(client, ["get", "loglevel"], "INFO"),
|
_safe_get_typed(client, ["get", "loglevel"], "INFO"),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import structlog
|
|||||||
|
|
||||||
from app.exceptions import ServerOperationError
|
from app.exceptions import ServerOperationError
|
||||||
from app.models.server import ServerSettings, ServerSettingsResponse, ServerSettingsUpdate
|
from app.models.server import ServerSettings, ServerSettingsResponse, ServerSettingsUpdate
|
||||||
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT
|
||||||
from app.utils.fail2ban_client import Fail2BanClient, Fail2BanCommand, Fail2BanResponse
|
from app.utils.fail2ban_client import Fail2BanClient, Fail2BanCommand, Fail2BanResponse
|
||||||
from app.utils.fail2ban_response import ok
|
from app.utils.fail2ban_response import ok
|
||||||
|
|
||||||
@@ -28,8 +29,6 @@ type Fail2BanSettingValue = str | int | bool
|
|||||||
|
|
||||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
|
|
||||||
def _to_int(value: object | None, default: int) -> int:
|
def _to_int(value: object | None, default: int) -> int:
|
||||||
"""Convert a raw value to an int, falling back to a default.
|
"""Convert a raw value to an int, falling back to a default.
|
||||||
@@ -105,7 +104,7 @@ async def get_settings(socket_path: str) -> ServerSettingsResponse:
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
(
|
(
|
||||||
log_level_raw,
|
log_level_raw,
|
||||||
@@ -160,7 +159,7 @@ async def update_settings(socket_path: str, update: ServerSettingsUpdate) -> Non
|
|||||||
ServerOperationError: If any ``set`` command is rejected.
|
ServerOperationError: If any ``set`` command is rejected.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
async def _set(key: str, value: Fail2BanSettingValue) -> None:
|
async def _set(key: str, value: Fail2BanSettingValue) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -197,7 +196,7 @@ async def flush_logs(socket_path: str) -> str:
|
|||||||
ServerOperationError: If the command is rejected.
|
ServerOperationError: If the command is rejected.
|
||||||
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
~app.utils.fail2ban_client.Fail2BanConnectionError: Socket unreachable.
|
||||||
"""
|
"""
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
try:
|
try:
|
||||||
response = await client.send(["flushlogs"])
|
response = await client.send(["flushlogs"])
|
||||||
result = ok(cast("Fail2BanResponse", response))
|
result = ok(cast("Fail2BanResponse", response))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from app.models.config import (
|
|||||||
JailValidationIssue,
|
JailValidationIssue,
|
||||||
JailValidationResult,
|
JailValidationResult,
|
||||||
)
|
)
|
||||||
from app.utils.constants import FAIL2BAN_TRUTHY_VALUES
|
from app.utils.constants import FAIL2BAN_SOCKET_TIMEOUT, FAIL2BAN_TRUTHY_VALUES
|
||||||
from app.utils.fail2ban_client import (
|
from app.utils.fail2ban_client import (
|
||||||
Fail2BanClient,
|
Fail2BanClient,
|
||||||
Fail2BanConnectionError,
|
Fail2BanConnectionError,
|
||||||
@@ -32,8 +32,6 @@ from app.utils.fail2ban_response import ok, to_dict
|
|||||||
|
|
||||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||||
|
|
||||||
_SOCKET_TIMEOUT: float = 10.0
|
|
||||||
|
|
||||||
# Allowlist pattern for jail names used in path construction.
|
# Allowlist pattern for jail names used in path construction.
|
||||||
_SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
_SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||||
|
|
||||||
@@ -253,7 +251,7 @@ def _parse_jails_sync(
|
|||||||
async def _get_active_jail_names(socket_path: str) -> set[str]:
|
async def _get_active_jail_names(socket_path: str) -> set[str]:
|
||||||
"""Fetch the set of currently running jail names from fail2ban."""
|
"""Fetch the set of currently running jail names from fail2ban."""
|
||||||
try:
|
try:
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
|
|
||||||
status_raw = ok(await client.send(["status"]))
|
status_raw = ok(await client.send(["status"]))
|
||||||
status_dict = to_dict(status_raw)
|
status_dict = to_dict(status_raw)
|
||||||
@@ -272,7 +270,7 @@ async def _get_active_jail_names(socket_path: str) -> set[str]:
|
|||||||
async def _probe_fail2ban_running(socket_path: str) -> bool:
|
async def _probe_fail2ban_running(socket_path: str) -> bool:
|
||||||
"""Return ``True`` when fail2ban responds successfully to a status request."""
|
"""Return ``True`` when fail2ban responds successfully to a status request."""
|
||||||
try:
|
try:
|
||||||
client = Fail2BanClient(socket_path=socket_path, timeout=_SOCKET_TIMEOUT)
|
client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT)
|
||||||
response = await client.send(["status"])
|
response = await client.send(["status"])
|
||||||
code, _ = cast("Fail2BanResponse", response)
|
code, _ = cast("Fail2BanResponse", response)
|
||||||
return code == 0
|
return code == 0
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ from typing import Final
|
|||||||
DEFAULT_FAIL2BAN_SOCKET: Final[str] = "/var/run/fail2ban/fail2ban.sock"
|
DEFAULT_FAIL2BAN_SOCKET: Final[str] = "/var/run/fail2ban/fail2ban.sock"
|
||||||
"""Default path to the fail2ban Unix domain socket."""
|
"""Default path to the fail2ban Unix domain socket."""
|
||||||
|
|
||||||
FAIL2BAN_SOCKET_TIMEOUT_SECONDS: Final[float] = 5.0
|
FAIL2BAN_SOCKET_TIMEOUT_FAST: Final[float] = 5.0
|
||||||
"""Maximum seconds to wait for a response from the fail2ban socket."""
|
"""Maximum seconds for fast operations (health checks, metadata probes)."""
|
||||||
|
|
||||||
|
FAIL2BAN_SOCKET_TIMEOUT: Final[float] = 10.0
|
||||||
|
"""Maximum seconds for command operations (config, jail management)."""
|
||||||
|
|
||||||
FAIL2BAN_TRUTHY_VALUES: Final[frozenset[str]] = frozenset({"true", "yes", "1"})
|
FAIL2BAN_TRUTHY_VALUES: Final[frozenset[str]] = frozenset({"true", "yes", "1"})
|
||||||
"""String values treated as boolean true by fail2ban configuration parsers."""
|
"""String values treated as boolean true by fail2ban configuration parsers."""
|
||||||
|
|||||||
Reference in New Issue
Block a user