refactoring-backend #3
@@ -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**:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user