Backend (tasks 1.1, 1.5–1.8): - pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x, structlog, bcrypt; ruff + mypy strict configured - Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation) - SQLite schema: settings, sessions, blocklist_sources, import_log; WAL mode + foreign keys; idempotent init_db() - FastAPI app factory with lifespan (DB, aiohttp session, scheduler), CORS, unhandled-exception handler, GET /api/health - Fail2BanClient: async Unix-socket wrapper using run_in_executor, custom error types, async context manager - Utility modules: ip_utils, time_utils, constants - 47 tests; ruff 0 errors; mypy --strict 0 errors Frontend (tasks 1.2–1.4): - Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier - Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants - Typed fetch API client (ApiError, get/post/put/del) + endpoints constants - tsc --noEmit 0 errors
102 lines
2.5 KiB
Python
102 lines
2.5 KiB
Python
"""IP address and CIDR range validation and normalisation utilities.
|
|
|
|
All IP handling in BanGUI goes through these helpers to enforce consistency
|
|
and prevent malformed addresses from reaching fail2ban.
|
|
"""
|
|
|
|
import ipaddress
|
|
|
|
|
|
def is_valid_ip(address: str) -> bool:
|
|
"""Return ``True`` if *address* is a valid IPv4 or IPv6 address.
|
|
|
|
Args:
|
|
address: The string to validate.
|
|
|
|
Returns:
|
|
``True`` if the string represents a valid IP address, ``False`` otherwise.
|
|
"""
|
|
try:
|
|
ipaddress.ip_address(address)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def is_valid_network(cidr: str) -> bool:
|
|
"""Return ``True`` if *cidr* is a valid IPv4 or IPv6 network in CIDR notation.
|
|
|
|
Args:
|
|
cidr: The string to validate, e.g. ``"192.168.0.0/24"``.
|
|
|
|
Returns:
|
|
``True`` if the string is a valid CIDR network, ``False`` otherwise.
|
|
"""
|
|
try:
|
|
ipaddress.ip_network(cidr, strict=False)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def is_valid_ip_or_network(value: str) -> bool:
|
|
"""Return ``True`` if *value* is a valid IP address or CIDR network.
|
|
|
|
Args:
|
|
value: The string to validate.
|
|
|
|
Returns:
|
|
``True`` if the string is a valid IP address or CIDR range.
|
|
"""
|
|
return is_valid_ip(value) or is_valid_network(value)
|
|
|
|
|
|
def normalise_ip(address: str) -> str:
|
|
"""Return a normalised string representation of an IP address.
|
|
|
|
IPv6 addresses are compressed to their canonical short form.
|
|
IPv4 addresses are returned unchanged.
|
|
|
|
Args:
|
|
address: A valid IP address string.
|
|
|
|
Returns:
|
|
Normalised IP address string.
|
|
|
|
Raises:
|
|
ValueError: If *address* is not a valid IP address.
|
|
"""
|
|
return str(ipaddress.ip_address(address))
|
|
|
|
|
|
def normalise_network(cidr: str) -> str:
|
|
"""Return a normalised string representation of a CIDR network.
|
|
|
|
Host bits are masked to produce the network address.
|
|
|
|
Args:
|
|
cidr: A valid CIDR network string, e.g. ``"192.168.1.5/24"``.
|
|
|
|
Returns:
|
|
Normalised network string, e.g. ``"192.168.1.0/24"``.
|
|
|
|
Raises:
|
|
ValueError: If *cidr* is not a valid network.
|
|
"""
|
|
return str(ipaddress.ip_network(cidr, strict=False))
|
|
|
|
|
|
def ip_version(address: str) -> int:
|
|
"""Return 4 or 6 depending on the IP version of *address*.
|
|
|
|
Args:
|
|
address: A valid IP address string.
|
|
|
|
Returns:
|
|
``4`` for IPv4, ``6`` for IPv6.
|
|
|
|
Raises:
|
|
ValueError: If *address* is not a valid IP address.
|
|
"""
|
|
return ipaddress.ip_address(address).version
|