Development
This commit is contained in:
parent
8766c6c9a2
commit
ee31a18ac6
43 changed files with 54 additions and 48 deletions
224
docker/routlin-dash/app/sanitize.py
Normal file
224
docker/routlin-dash/app/sanitize.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue