diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 80991de..86384ff 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,42 +1,3 @@ -### Issue #29: LOW-MEDIUM - Missing .editorconfig - -**Where found**: -- No `.editorconfig` file - -**Why this is needed**: -Different developers use different editors with different default formatting, causing inconsistent code. - -**Goal**: -Enforce consistent formatting across all editors. - -**What to do**: -1. Create `.editorconfig`: - ```ini - root = true - - [*] - charset = utf-8 - end_of_line = lf - insert_final_newline = true - - [*.py] - indent_style = space - indent_size = 4 - - [*.{js,ts,tsx,jsx}] - indent_style = space - indent_size = 2 - ``` -2. Add editorconfig plugin to IDE guides - -**Docs changes needed**: -- Add to development setup instructions - -**Doc references**: -- DATABASE_API_DEPLOYMENT_ISSUES.md - Issue "12.3 Missing .editorconfig" - ---- - ### Issue #30: LOW-MEDIUM - IPv4-Mapped IPv6 Address Duplicates **Where found**: diff --git a/backend/app/services/ban_service.py b/backend/app/services/ban_service.py index 61a09e3..6fd32d7 100644 --- a/backend/app/services/ban_service.py +++ b/backend/app/services/ban_service.py @@ -57,6 +57,7 @@ from app.utils.fail2ban_response import ( ok, to_dict, ) +from app.utils.ip_utils import normalise_ip from app.utils.time_utils import since_unix if TYPE_CHECKING: @@ -96,10 +97,11 @@ async def ban_ip(socket_path: str, jail: str, ip: str) -> None: except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc + normalized = normalise_ip(ip) client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) try: - ok(await client.send(["set", jail, "banip", ip])) + ok(await client.send(["set", jail, "banip", normalized])) except ValueError as exc: if is_not_found_error(exc): raise JailNotFoundError(jail) from exc @@ -113,14 +115,15 @@ async def unban_ip(socket_path: str, ip: str, jail: str | None = None) -> None: except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc + normalized = normalise_ip(ip) client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) if jail is None: - ok(await client.send(["unban", ip])) + ok(await client.send(["unban", normalized])) return try: - ok(await client.send(["set", jail, "unbanip", ip])) + ok(await client.send(["set", jail, "unbanip", normalized])) except ValueError as exc: if is_not_found_error(exc): raise JailNotFoundError(jail) from exc diff --git a/backend/app/services/blocklist_parser.py b/backend/app/services/blocklist_parser.py index 94db970..fe306f6 100644 --- a/backend/app/services/blocklist_parser.py +++ b/backend/app/services/blocklist_parser.py @@ -8,7 +8,7 @@ from __future__ import annotations import structlog -from app.utils.ip_utils import is_valid_ip, is_valid_network +from app.utils.ip_utils import is_valid_ip, is_valid_network, normalise_ip log: structlog.stdlib.BoundLogger = structlog.get_logger() @@ -65,7 +65,7 @@ class BlocklistParser: # Accept only individual IP addresses, skip CIDRs and malformed if is_valid_ip(stripped): - valid_ips.append(stripped) + valid_ips.append(normalise_ip(stripped)) else: skipped += 1 @@ -101,7 +101,8 @@ class BlocklistParser: if is_valid_ip(stripped) or is_valid_network(stripped): valid += 1 if len(entries) < sample_lines: - entries.append(stripped) + # Normalise individual IPs; keep networks as-is + entries.append(normalise_ip(stripped) if is_valid_ip(stripped) else stripped) else: skipped += 1 diff --git a/backend/app/services/jail_service.py b/backend/app/services/jail_service.py index 5e0873a..414f154 100644 --- a/backend/app/services/jail_service.py +++ b/backend/app/services/jail_service.py @@ -48,6 +48,7 @@ from app.utils.fail2ban_response import ( ok, to_dict, ) +from app.utils.ip_utils import normalise_ip from app.utils.jail_socket import reload_all from app.utils.runtime_state import JailServiceState # noqa: TC001 @@ -639,6 +640,7 @@ def _parse_ban_entry(entry: str, jail: str) -> DomainActiveBan | None: # Validate IP ipaddress.ip_address(ip) + ip = normalise_ip(ip) if len(parts) < 2: # Entry has no time info — return with unknown times. @@ -1009,6 +1011,7 @@ async def lookup_ip( except ValueError as exc: raise ValueError(f"Invalid IP address: {ip!r}") from exc + ip = normalise_ip(ip) client = Fail2BanClient(socket_path=socket_path, timeout=FAIL2BAN_SOCKET_TIMEOUT) with contextlib.suppress(ValueError, Fail2BanConnectionError): diff --git a/backend/app/utils/client_ip.py b/backend/app/utils/client_ip.py index b2c917f..6575178 100644 --- a/backend/app/utils/client_ip.py +++ b/backend/app/utils/client_ip.py @@ -9,6 +9,8 @@ from __future__ import annotations import ipaddress from typing import TYPE_CHECKING +from app.utils.ip_utils import normalise_ip + if TYPE_CHECKING: from fastapi import Request @@ -50,7 +52,7 @@ def get_client_ip( # If no trusted proxies are configured, use immediate IP directly if not trusted_proxies: - return immediate_ip + return normalise_ip(immediate_ip) # Check if the immediate connection is from a trusted proxy if not _is_trusted_proxy(immediate_ip, trusted_proxies): @@ -64,15 +66,15 @@ def get_client_ip( # Take the first IP in the list client_ip = forwarded_for.split(",")[0].strip() if client_ip: - return client_ip + return normalise_ip(client_ip) # Fall back to X-Real-IP real_ip = request.headers.get("X-Real-IP", "").strip() if real_ip: - return real_ip + return normalise_ip(real_ip) # No forwarded headers found, use immediate connection - return immediate_ip + return normalise_ip(immediate_ip) def _is_trusted_proxy(ip: str, trusted_proxies: list[str]) -> bool: diff --git a/backend/app/utils/rate_limiter.py b/backend/app/utils/rate_limiter.py index 24d4032..fefe878 100644 --- a/backend/app/utils/rate_limiter.py +++ b/backend/app/utils/rate_limiter.py @@ -56,6 +56,7 @@ from app.utils.constants import ( LOGIN_PENALTY_MAX_SECONDS, LOGIN_PENALTY_MULTIPLIER, ) +from app.utils.ip_utils import normalise_ip if TYPE_CHECKING: from collections.abc import Mapping @@ -104,6 +105,7 @@ class RateLimiter: ``True`` if the request is allowed (past penalty period), ``False`` if currently blocked by exponential backoff. """ + ip_address = normalise_ip(ip_address) now = time() if ip_address not in self._failures: @@ -192,6 +194,7 @@ class RateLimiter: Args: ip_address: The client IP address whose login attempt failed. """ + ip_address = normalise_ip(ip_address) now = time() if ip_address not in self._failures: @@ -294,6 +297,7 @@ class GlobalRateLimiter: A tuple of (is_allowed, retry_after_seconds). If is_allowed is True, retry_after_seconds is 0. If False, it's the estimated time to wait. """ + ip_address = normalise_ip(ip_address) now = time() if ip_address not in self._requests: @@ -347,6 +351,7 @@ class GlobalRateLimiter: """ now = time() + ip_address = normalise_ip(ip_address) requests = self._get_bucket_deque(bucket, ip_address, max_requests, window_seconds) cutoff = now - window_seconds