diff --git a/docker/routlin-dash/app/factory.py b/docker/routlin-dash/app/factory.py index 373c1e3..2e0021f 100644 --- a/docker/routlin-dash/app/factory.py +++ b/docker/routlin-dash/app/factory.py @@ -17,6 +17,43 @@ LEVEL_RANK = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3} STANDARD_INPUT_TYPES = {'text', 'password', 'number', 'checkbox', 'select', 'textarea'} +_VALIDATION_FLAGS = { + 'VALIDATION_IPV4_FORMAT': 1, + 'VALIDATION_IPV6_FORMAT': 2, + 'VALIDATION_SUBNET': 4, + 'VALIDATION_ADDRESS': 8, + 'VALIDATION_MAC': 16, + 'VALIDATION_URL': 32, + 'VALIDATION_PORT': 64, + 'VALIDATION_DASH_NAME': 128, + 'VALIDATION_NETWORK_NAME': 256, + 'VALIDATION_DOMAIN_NAME': 512, + 'VALIDATION_TIME24H': 1024, + 'VALIDATION_RANGE_INT': 2048, + 'VALIDATION_ENDPOINT': 4096, + 'VALIDATION_IPV4_CIDR': 8192, +} + +_COMPAT_VALIDATION = { + 'ipv4': 'VALIDATION_IPV4_FORMAT', + 'ipv6': 'VALIDATION_IPV6_FORMAT', + 'ip': 'VALIDATION_IPV4_FORMAT|VALIDATION_IPV6_FORMAT', + 'ipv4cidr': 'VALIDATION_IPV4_CIDR', + 'mac': 'VALIDATION_MAC', + 'url': 'VALIDATION_URL', + 'port': 'VALIDATION_PORT', + 'dashname': 'VALIDATION_DASH_NAME', + 'networkname': 'VALIDATION_NETWORK_NAME', + 'domainname': 'VALIDATION_DOMAIN_NAME', + 'time_24h': 'VALIDATION_TIME24H', + 'vlan_id': 'VALIDATION_RANGE_INT', + 'positive_int': 'VALIDATION_RANGE_INT', + 'endpoint': 'VALIDATION_ENDPOINT', + 'ip_in_subnet': 'VALIDATION_ADDRESS', + 'address': 'VALIDATION_ADDRESS', + 'subnet': 'VALIDATION_SUBNET', +} + # Utilities =========================================================== def e(text): @@ -62,6 +99,124 @@ def js_str(value): return json.dumps(str(value)) +def parse_validation(s): + if not s: + return 0 + resolved = _COMPAT_VALIDATION.get(s, s) + result = 0 + for token in resolved.split('|'): + token = token.strip() + val = _VALIDATION_FLAGS.get(token) + if val is None: + print(f'[factory] WARNING: unknown validation token "{token}" in "{s}"', file=sys.stderr) + continue + result |= val + return result + + +def _encode_field_validations(fields): + out = [] + for f in fields: + f2 = dict(f) + raw = f2.get('validate', '') + if not raw and f2.get('input_type') == 'number': + raw = 'VALIDATION_RANGE_INT' + if raw and isinstance(raw, str): + f2['validate'] = parse_validation(raw) + out.append(f2) + return out + + +def build_big_validate(): + _JS_NAMES = { + 'VALIDATION_IPV4_FORMAT': 'F_IPV4', + 'VALIDATION_IPV6_FORMAT': 'F_IPV6', + 'VALIDATION_SUBNET': 'F_SUBNET', + 'VALIDATION_ADDRESS': 'F_ADDR', + 'VALIDATION_MAC': 'F_MAC', + 'VALIDATION_URL': 'F_URL', + 'VALIDATION_PORT': 'F_PORT', + 'VALIDATION_DASH_NAME': 'F_DASH', + 'VALIDATION_NETWORK_NAME': 'F_NET', + 'VALIDATION_DOMAIN_NAME': 'F_DOMAIN', + 'VALIDATION_TIME24H': 'F_T24H', + 'VALIDATION_RANGE_INT': 'F_RNGINT', + 'VALIDATION_ENDPOINT': 'F_ENDPT', + 'VALIDATION_IPV4_CIDR': 'F_IPV4C', + } + decls = ''.join( + f'var {_JS_NAMES[k]}={_VALIDATION_FLAGS[k]};' + for k in _VALIDATION_FLAGS + ) + body = r""" +function _ok(){return{ok:true,msg:'',partial:false};} +function _par(m){return{ok:false,msg:m||'',partial:true};} +function _err(m){return{ok:false,msg:m||'Invalid',partial:false};} +function _ipv4(s){ + if(!s)return'empty'; + if(/[^0-9.]/.test(s))return'badchar'; + if(/\.\./.test(s)||s[0]==='.')return'badstruct'; + var p=s.split('.'); + if(p.length>4)return'badstruct'; + for(var i=0;i
{hint}
' if hint else '' return ( diff --git a/docker/routlin-dash/app/pages/dhcp/content.json b/docker/routlin-dash/app/pages/dhcp/content.json index 3d6625c..9ecf296 100644 --- a/docker/routlin-dash/app/pages/dhcp/content.json +++ b/docker/routlin-dash/app/pages/dhcp/content.json @@ -108,12 +108,12 @@ { "col": "hostname", "input_type": "text", - "validate": "networkname" + "validate": "VALIDATION_NETWORK_NAME" }, { "col": "mac", "input_type": "text", - "validate": "mac" + "validate": "VALIDATION_MAC" }, { "col": "ip", @@ -171,7 +171,7 @@ "label": "Hostname", "name": "hostname", "input_type": "text", - "validate": "networkname", + "validate": "VALIDATION_NETWORK_NAME", "placeholder": "e.g. nas" }, { @@ -179,7 +179,7 @@ "label": "MAC Address", "name": "mac", "input_type": "text", - "validate": "mac", + "validate": "VALIDATION_MAC", "placeholder": "e.g. aa:bb:cc:dd:ee:ff" }, { diff --git a/docker/routlin-dash/app/pages/dnsblocking/content.json b/docker/routlin-dash/app/pages/dnsblocking/content.json index e736d4a..b372cde 100644 --- a/docker/routlin-dash/app/pages/dnsblocking/content.json +++ b/docker/routlin-dash/app/pages/dnsblocking/content.json @@ -49,7 +49,7 @@ { "col": "name", "input_type": "text", - "validate": "dashname" + "validate": "VALIDATION_DASH_NAME" }, { "col": "description", @@ -63,7 +63,7 @@ { "col": "url", "input_type": "text", - "validate": "url" + "validate": "VALIDATION_URL" } ] }, @@ -92,7 +92,7 @@ "label": "Name", "name": "name", "input_type": "text", - "validate": "dashname", + "validate": "VALIDATION_DASH_NAME", "placeholder": "e.g. steven-black" }, { @@ -114,7 +114,7 @@ "label": "Source URL", "name": "url", "input_type": "text", - "validate": "url", + "validate": "VALIDATION_URL", "placeholder": "https://..." }, { @@ -172,7 +172,7 @@ "label": "Daily Refresh Time", "name": "daily_execute_time_24hr_local", "input_type": "text", - "validate": "time_24h", + "validate": "VALIDATION_TIME24H", "value": "%GENERAL_DAILY_EXECUTE_TIME%", "placeholder": "e.g. 02:30", "hint": "24-hour local time for the daily blocklist refresh." diff --git a/docker/routlin-dash/app/pages/dnsserver/content.json b/docker/routlin-dash/app/pages/dnsserver/content.json index 8c4da82..c71a9b4 100644 --- a/docker/routlin-dash/app/pages/dnsserver/content.json +++ b/docker/routlin-dash/app/pages/dnsserver/content.json @@ -30,7 +30,7 @@ "name": "upstream_servers", "item_placeholder": "e.g. 1.1.1.1", "add_label": "Add Provider", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT|VALIDATION_IPV6_FORMAT", "hint": "DNS resolvers queried for external hostnames. Supports IPv4 and IPv6.", "items": "%DNS_UPSTREAM_SERVERS_JSON%" }, diff --git a/docker/routlin-dash/app/pages/hostoverrides/content.json b/docker/routlin-dash/app/pages/hostoverrides/content.json index 991bba1..9f2ca4e 100644 --- a/docker/routlin-dash/app/pages/hostoverrides/content.json +++ b/docker/routlin-dash/app/pages/hostoverrides/content.json @@ -54,12 +54,12 @@ { "col": "host", "input_type": "text", - "validate": "domainname" + "validate": "VALIDATION_DOMAIN_NAME" }, { "col": "ip", "input_type": "text", - "validate": "ipv4" + "validate": "VALIDATION_IPV4_FORMAT" }, { "col": "enabled", @@ -100,7 +100,7 @@ "label": "Hostname", "name": "host", "input_type": "text", - "validate": "domainname", + "validate": "VALIDATION_DOMAIN_NAME", "placeholder": "e.g. server.home.local" }, { @@ -108,7 +108,7 @@ "label": "Resolves To", "name": "ip", "input_type": "text", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT", "placeholder": "e.g. 192.168.1.100" }, { diff --git a/docker/routlin-dash/app/pages/intervlan/content.json b/docker/routlin-dash/app/pages/intervlan/content.json index e47e1aa..4dc306d 100644 --- a/docker/routlin-dash/app/pages/intervlan/content.json +++ b/docker/routlin-dash/app/pages/intervlan/content.json @@ -124,7 +124,7 @@ "label": "Source", "name": "src_ip_or_subnet", "input_type": "text", - "validate": "ipv4cidr", + "validate": "VALIDATION_IPV4_CIDR", "placeholder": "e.g. 192.168.20.0/24" }, { @@ -132,7 +132,7 @@ "label": "Destination", "name": "dst_ip_or_subnet", "input_type": "text", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT", "placeholder": "e.g. 192.168.10.100" }, { @@ -140,7 +140,7 @@ "label": "Dest Port", "name": "dst_port", "input_type": "text", - "validate": "port", + "validate": "VALIDATION_PORT", "placeholder": "e.g. 8009" }, { diff --git a/docker/routlin-dash/app/pages/networklayout/content.json b/docker/routlin-dash/app/pages/networklayout/content.json index 72b99d0..ff58c50 100644 --- a/docker/routlin-dash/app/pages/networklayout/content.json +++ b/docker/routlin-dash/app/pages/networklayout/content.json @@ -142,7 +142,7 @@ "input_type": "number", "min": 1, "max": 4094, - "validate": "vlan_id", + "validate": "VALIDATION_RANGE_INT", "existing_ids": "%EXISTING_VLAN_IDS_JSON%", "hint": "Unique integer 1-4094. Sets the 802.1Q tag and interface name." }, @@ -151,7 +151,7 @@ "label": "VLAN Name", "name": "name", "input_type": "text", - "validate": "dashname", + "validate": "VALIDATION_DASH_NAME", "hint": "Lowercase letters, digits, hyphens. E.g. iot" }, { @@ -184,7 +184,7 @@ { "label": "IP Address", "name": "ip", - "valtype": "address", + "validate": "VALIDATION_ADDRESS", "attrs": { "data-dep-subnet": "[name='subnet']", "data-dep-mask": ".subnet-prefix-input" @@ -200,7 +200,7 @@ { "label": "Hostname", "name": "hostname", - "validate": "networkname", + "validate": "VALIDATION_NETWORK_NAME", "placeholder": "Optional" } ] @@ -223,7 +223,7 @@ "label": "DNS Server(s)", "name": "dns_server", "override_name": "dns_server_override", - "validate": "ip_in_subnet", + "validate": "VALIDATION_ADDRESS", "hint": "DNS server(s) advertised to clients via DHCP." }, { @@ -231,7 +231,7 @@ "label": "NTP Server(s)", "name": "ntp_server", "override_name": "ntp_server_override", - "validate": "ip_in_subnet", + "validate": "VALIDATION_ADDRESS", "hint": "NTP server(s) advertised to clients via DHCP." }, { @@ -239,7 +239,7 @@ "label": "Domain", "name": "dhcp_domain", "input_type": "text", - "validate": "networkname", + "validate": "VALIDATION_NETWORK_NAME", "value": "lan", "hint": "Local domain name advertised to clients via DHCP (e.g. lan, home.arpa, corp). Avoid \"local\" per RFC 6762." } diff --git a/docker/routlin-dash/app/pages/physicalinterfaces/content.json b/docker/routlin-dash/app/pages/physicalinterfaces/content.json index cf8a3be..ba388e9 100644 --- a/docker/routlin-dash/app/pages/physicalinterfaces/content.json +++ b/docker/routlin-dash/app/pages/physicalinterfaces/content.json @@ -131,7 +131,7 @@ "label": "MAC Address", "name": "mac", "input_type": "text", - "validate": "mac", + "validate": "VALIDATION_MAC", "value": "", "hint": "Factory default: none" } diff --git a/docker/routlin-dash/app/pages/portforwarding/content.json b/docker/routlin-dash/app/pages/portforwarding/content.json index 99ad8c1..264def2 100644 --- a/docker/routlin-dash/app/pages/portforwarding/content.json +++ b/docker/routlin-dash/app/pages/portforwarding/content.json @@ -124,7 +124,7 @@ "label": "Ext Port", "name": "dest_port", "input_type": "text", - "validate": "port", + "validate": "VALIDATION_PORT", "placeholder": "e.g. 25565" }, { @@ -132,7 +132,7 @@ "label": "NAT IP", "name": "nat_ip", "input_type": "text", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT", "placeholder": "e.g. 192.168.1.50" }, { @@ -140,7 +140,7 @@ "label": "NAT Port", "name": "nat_port", "input_type": "text", - "validate": "port", + "validate": "VALIDATION_PORT", "placeholder": "e.g. 25565" }, { diff --git a/docker/routlin-dash/app/pages/vpn/content.json b/docker/routlin-dash/app/pages/vpn/content.json index 617eeea..bd0f4c9 100644 --- a/docker/routlin-dash/app/pages/vpn/content.json +++ b/docker/routlin-dash/app/pages/vpn/content.json @@ -96,7 +96,7 @@ { "col": "name", "input_type": "text", - "validate": "dashname" + "validate": "VALIDATION_DASH_NAME" }, { "col": "split_tunnel", @@ -141,7 +141,7 @@ "label": "Name", "name": "peer_name", "input_type": "text", - "validate": "dashname", + "validate": "VALIDATION_DASH_NAME", "placeholder": "e.g. laptop", "hint": "Friendly name for this peer." }, @@ -157,7 +157,7 @@ "label": "Assigned IP", "name": "peer_ip", "input_type": "text", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT", "placeholder": "e.g. 192.168.40.2", "hint": "Static IP assigned to this peer within the VPN subnet." }, @@ -219,7 +219,7 @@ "label": "Server Endpoint", "name": "vpn_server_endpoint", "input_type": "text", - "validate": "endpoint", + "validate": "VALIDATION_ENDPOINT", "value": "%VPN_SERVER_ENDPOINT%", "placeholder": "e.g. vpn.example.com", "hint": "Publicly reachable hostname or IP of this server, embedded in client config files." @@ -229,7 +229,7 @@ "label": "Domain", "name": "vpn_domain", "input_type": "text", - "validate": "dashname", + "validate": "VALIDATION_DASH_NAME", "value": "%VPN_DOMAIN%", "placeholder": "e.g. local", "hint": "DNS search domain pushed to VPN clients." @@ -239,7 +239,7 @@ "label": "DNS Override", "name": "vpn_dns_server", "input_type": "text", - "validate": "ipv4", + "validate": "VALIDATION_IPV4_FORMAT", "value": "%VPN_DNS_SERVER%", "placeholder": "Leave blank to use gateway IP (%VPN_GATEWAY%)", "hint": "Explicit DNS server pushed to peers. Defaults to the gateway IP." diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index d031dce..c2bd59f 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -10,7 +10,6 @@ from factory import LEVEL_RANK, e, client_level, passes, build_items, build_snap PAGES_DIR = os.path.join(APP_DIR, 'pages') NAVBAR_FILE = os.path.join(APP_DIR, 'navbar.json') CSS_FILE = os.path.join(DATA_DIR, 'styles.css') -VALIDATION_FILE = os.path.join(DATA_DIR, 'validation.js') COMMON_JS_FILE = os.path.join(DATA_DIR, 'common.js') BLOCKLISTS_DIR = os.path.join(CONFIGS_DIR, 'blocklists') HEALTH_FILE = os.path.join(CONFIGS_DIR, '.health') @@ -1076,11 +1075,7 @@ def build_nav_item(item, active_view, level, in_dropdown=False, inherited_req=No # Inline JavaScript ================================================= def _inline_js(page_name=None): - try: - with open(VALIDATION_FILE) as f: - val_js = f.read() - except Exception: - val_js = '' + big_validate_js = factory.build_big_validate() try: with open(COMMON_JS_FILE) as f: app_js = f.read() @@ -1094,7 +1089,7 @@ def _inline_js(page_name=None): page_js = f.read() except Exception: pass - return val_js + '\n' + app_js + ('\n' + page_js if page_js else '') + return big_validate_js + '\n' + app_js + ('\n' + page_js if page_js else '') # Routes ============================================================