linuxrouter/docker/routlin-dash/app/sanitize.py
2026-05-21 01:34:42 -04:00

224 lines
6.5 KiB
Python

import re
import ipaddress
# Curated IANA timezone list for the dropdown. Validation accepts any entry from this set.
VALID_TIMEZONES = [
'UTC',
# Americas
'America/New_York',
'America/Detroit',
'America/Indiana/Indianapolis',
'America/Chicago',
'America/Denver',
'America/Phoenix',
'America/Los_Angeles',
'America/Anchorage',
'America/Adak',
'Pacific/Honolulu',
'America/Toronto',
'America/Vancouver',
'America/Winnipeg',
'America/Halifax',
'America/St_Johns',
'America/Mexico_City',
'America/Bogota',
'America/Lima',
'America/Santiago',
'America/Caracas',
'America/Sao_Paulo',
'America/Argentina/Buenos_Aires',
'America/Montevideo',
# Europe
'Europe/London',
'Europe/Dublin',
'Europe/Lisbon',
'Europe/Paris',
'Europe/Berlin',
'Europe/Amsterdam',
'Europe/Brussels',
'Europe/Madrid',
'Europe/Rome',
'Europe/Zurich',
'Europe/Vienna',
'Europe/Stockholm',
'Europe/Oslo',
'Europe/Copenhagen',
'Europe/Helsinki',
'Europe/Warsaw',
'Europe/Prague',
'Europe/Budapest',
'Europe/Bucharest',
'Europe/Athens',
'Europe/Istanbul',
'Europe/Moscow',
'Europe/Kyiv',
# Africa
'Africa/Casablanca',
'Africa/Lagos',
'Africa/Cairo',
'Africa/Nairobi',
'Africa/Johannesburg',
# Asia
'Asia/Dubai',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Karachi',
'Asia/Kolkata',
'Asia/Colombo',
'Asia/Dhaka',
'Asia/Yangon',
'Asia/Bangkok',
'Asia/Ho_Chi_Minh',
'Asia/Singapore',
'Asia/Kuala_Lumpur',
'Asia/Jakarta',
'Asia/Shanghai',
'Asia/Hong_Kong',
'Asia/Taipei',
'Asia/Manila',
'Asia/Seoul',
'Asia/Tokyo',
'Asia/Yakutsk',
'Asia/Vladivostok',
# Australia / Pacific
'Australia/Perth',
'Australia/Darwin',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Sydney',
'Australia/Melbourne',
'Australia/Hobart',
'Pacific/Auckland',
'Pacific/Fiji',
'Pacific/Guam',
'Pacific/Honolulu',
]
_TIMEZONE_SET = set(VALID_TIMEZONES)
def _strip(value, pattern, max_len):
return re.sub(pattern, '', str(value).strip())[:max_len]
def text(value, max_len=200):
"""General description: letters, digits, spaces, basic punctuation. No quotes/braces/brackets/slashes."""
return _strip(value, r'''["'{}\[\]\\/<>;`^~]''', max_len)
def description(value, max_len=200):
"""Human-readable description: letters, digits, hyphens, parentheses, commas, forward slashes, spaces.
Whitespace collapsed; no sequential commas or slashes."""
s = re.sub(r'[^A-Za-z0-9\-()/,\s]', '', str(value))
s = re.sub(r'\s+', ' ', s)
s = re.sub(r',{2,}', ',', s)
s = re.sub(r'/{2,}', '/', s)
s = re.sub(r'-{2,}', '-', s)
s = re.sub(r'\({2,}', '(', s)
s = re.sub(r'\){2,}', ')', s)
return s.strip()[:max_len]
def name(value, max_len=40):
"""Identifier: lowercase letters, digits, hyphens only. No sequential hyphens."""
s = re.sub(r'[\s_]+', '-', str(value).strip().lower())
s = re.sub(r'[^a-z0-9-]', '', s)[:max_len]
s = re.sub(r'-{2,}', '-', s)
return s.strip('-')
def domainname(value, max_len=253):
"""Hostname or domain: letters, digits, hyphens, dots. Lowercased."""
return _strip(value.lower(), r'[^a-z0-9\-.]', max_len)
def domainlist(lines):
"""Sanitize a list of domain name strings, returning only non-empty results."""
return [h for v in lines if (h := domainname(v))]
def ip(value, max_len=45):
"""IPv4 or IPv6 address. Returns '' if not a valid address."""
cleaned = _strip(value, r'[^0-9a-fA-F.:]', max_len)
try:
ipaddress.ip_address(cleaned)
return cleaned
except ValueError:
return ''
def ip_or_cidr(value, max_len=49):
"""IP address or CIDR subnet. Returns '' if not valid."""
cleaned = _strip(value, r'[^0-9a-fA-F.:/]', max_len)
try:
if '/' in cleaned:
ipaddress.ip_network(cleaned, strict=False)
else:
ipaddress.ip_address(cleaned)
return cleaned
except ValueError:
return ''
def mac(value):
"""MAC address in aa:bb:cc:dd:ee:ff format. Colons required; no other separators accepted.
Returns lowercase colon-separated MAC if valid, '' otherwise."""
s = str(value).strip().lower()
if re.fullmatch(r'([0-9a-f]{2}:){5}[0-9a-f]{2}', s):
return s
return ''
def url(value, max_len=500):
"""URL: printable ASCII except quotes, braces, brackets, backslash, spaces."""
return _strip(value, r'''["'{}\[\]\\ ]''', max_len)
def interface_name(value, max_len=32):
"""Network interface name: letters, digits, hyphens, underscores, dots."""
return _strip(value, r'[^A-Za-z0-9\-_.]', max_len)
def port(value):
"""Port number string, validated 1-65535. Returns '' if invalid."""
digits = re.sub(r'[^0-9]', '', str(value))
try:
n = int(digits)
if 1 <= n <= 65535:
return str(n)
except (ValueError, TypeError):
pass
return ''
def time_24h(value, max_len=5):
"""24-hour time HH:MM: digits and colon only."""
return _strip(value, r'[^0-9:]', max_len)
def email(value, max_len=254):
"""Email address: letters, digits, @, dot, hyphen, underscore, plus. Lowercased."""
return _strip(value.lower(), r'[^a-z0-9@.\-_+]', max_len)
def timezone(value):
"""Timezone string: must be in VALID_TIMEZONES list. Returns '' if not found."""
return value if value in _TIMEZONE_SET else ''
def filterlist(submitted, allowed):
"""Filter a list of submitted values to those present in the allowed set, after sanitizing each."""
allowed = set(allowed)
return [n for v in submitted if (n := name(v)) in allowed]
def filtervalue(value, allowed):
"""Return the sanitized value if it exists in the allowed set, otherwise ''."""
n = name(value)
return n if n in set(allowed) else ''
_DOTTED_TO_PREFIX = {
'255.0.0.0': 8, '255.255.0.0': 16, '255.255.255.0': 24,
'255.255.255.128': 25, '255.255.255.192': 26,
'255.255.255.224': 27, '255.255.255.240': 28,
'255.255.255.248': 29, '255.255.255.252': 30,
}
def subnet_mask(value):
"""Subnet prefix length 1-30 (integer). Also accepts legacy dotted notation.
Returns int on success, None if invalid."""
s = str(value).strip()
if s in _DOTTED_TO_PREFIX:
return _DOTTED_TO_PREFIX[s]
try:
n = int(s)
if 1 <= n <= 30:
return n
except (ValueError, TypeError):
pass
return None