From f152d82386aa578ba88816c23982723cdd06c268 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sun, 31 May 2026 00:22:39 -0400 Subject: [PATCH] Development --- docker/routlin-dash/app/factory.py | 208 +++++++++++++++--- .../routlin-dash/app/pages/dhcp/content.json | 8 +- .../app/pages/dnsblocking/content.json | 10 +- .../app/pages/dnsserver/content.json | 2 +- .../app/pages/hostoverrides/content.json | 8 +- .../app/pages/intervlan/content.json | 6 +- .../app/pages/networklayout/content.json | 14 +- .../app/pages/physicalinterfaces/content.json | 2 +- .../app/pages/portforwarding/content.json | 6 +- .../routlin-dash/app/pages/vpn/content.json | 12 +- docker/routlin-dash/app/view_page.py | 9 +- 11 files changed, 219 insertions(+), 66 deletions(-) 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;i255||String(n)!==p[i])return'badrange';} + return(p.length===4&&p.every(function(x){return x!=='';}))? 'ok':'partial'; +} +function _ipv6(s){ + if(!s)return'empty'; + if(/[^0-9a-fA-F:]/.test(s))return'badchar'; + if(/:::/.test(s))return'badstruct'; + if((s.match(/::/g)||[]).length>1)return'badstruct'; + var parts=s.split(/::?/); + for(var i=0;i4)return'badstruct';} + var c=(s.match(/:/g)||[]).length,d=s.indexOf('::')!==-1; + if(d&&c>7)return'badstruct'; + return(c===7&&!d)||d?'ok':'partial'; +} +function _checkFlag(s,flag){ + if(flag===F_IPV4){var r=_ipv4(s);if(r==='ok')return _ok();if(r==='partial'||r==='empty')return _par('');if(r==='badchar')return _err('Invalid character');if(r==='badrange')return _err('Octet out of range');return _err('Invalid format');} + if(flag===F_IPV6){var r=_ipv6(s);if(r==='ok')return _ok();if(r==='partial'||r==='empty')return _par('');if(r==='badchar')return _err('Invalid character');return _err('Invalid format');} + if(flag===F_MAC){if(!s)return _par('');if(/[^0-9a-fA-F:]/.test(s))return _err('Invalid character');if(/::/.test(s))return _err('Invalid format');var g=s.split(':');if(g.length>6)return _err('Too many groups');for(var i=0;i2)return _err('Each group must be exactly 2 hex characters');}return(g.length===6&&g.every(function(x){return x.length===2;}))?_ok():_par('');} + if(flag===F_URL){if(!s)return _par('');if(/[^A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]/.test(s))return _err('Invalid character');var sl=s.toLowerCase();if('https://'.startsWith(sl)||'http://'.startsWith(sl))return _par('');var sep=sl.indexOf('://');if(sep===-1)return _err('Invalid URL format');var scheme=sl.slice(0,sep);if(scheme!=='http'&&scheme!=='https')return _err('Invalid URL format');var after=s.slice(sep+3);if(!after)return _par('');var he=after.search(/[/:?#]/),host=he===-1?after:after.slice(0,he),rest=he===-1?'':after.slice(he);if(!host)return _par('');if(/\.\./.test(host)||host[0]==='.'||host[host.length-1]==='.')return _err('Invalid URL format');var lb=host.split('.');for(var i=0;i65535)return _err('Invalid URL format');}return _ok();} + if(flag===F_PORT){if(!s)return _par('');if(/[^0-9]/.test(s))return _err('Digits only');var n=parseInt(s,10);return(n>=1&&n<=65535)?_ok():_err('Must be between 1 and 65535');} + if(flag===F_DASH){if(!s)return _par('');if(/[^a-z0-9-]/.test(s))return _err('Lowercase letters, digits and hyphens only');if(s[0]==='-'||/--/.test(s))return _err('No leading, trailing or consecutive hyphens');if(s[s.length-1]==='-')return _par('');return _ok();} + if(flag===F_NET){if(!s)return _par('');if(/[^a-zA-Z0-9_-]/.test(s))return _err('Letters, digits, hyphens and underscores only');if(s[0]==='-'||s[0]==='_')return _err('No leading, trailing or consecutive special characters');if(/[-_]{2,}/.test(s))return _err('No leading, trailing or consecutive special characters');if(s[s.length-1]==='-'||s[s.length-1]==='_')return _par('');return _ok();} + if(flag===F_DOMAIN){if(!s)return _par('');if(/[^a-zA-Z0-9.-]/.test(s))return _err('Letters, digits, hyphens and dots only');if(s[0]==='.')return _err('Invalid domain format');if(/\.\./.test(s))return _err('Invalid domain format');if(s[s.length-1]==='.')return _par('');var lb=s.split('.');for(var i=0;imx)){if(mn!=null&&mx!==null)return _err('Must be between '+mn+' and '+mx);return mn!=null?_err('Must be >= '+mn):_err('Must be <= '+mx);}return _ok();} + if(flag===F_ENDPT){if(!s)return _par('');if(/^[0-9.]+$/.test(s)){var r=_ipv4(s);return r==='ok'?_ok():(r==='partial'||r==='empty')?_par(''):_err('Invalid character');}if(s.indexOf(':')!==-1){var cc=(s.match(/:/g)||[]).length;if(cc>1){if(/:::/.test(s)||(s.match(/::/g)||[]).length>1)return _err('Invalid hostname or IP');if(/[^0-9a-fA-F:.]/.test(s))return _err('Invalid character');var col=s.replace(/[^:]/g,'').length;return(s.indexOf('::')!==-1||col===7)?_ok():_par('');}return _checkFlag(s.slice(0,s.lastIndexOf(':')),F_DOMAIN);}return _checkFlag(s,F_DOMAIN);} + if(flag===F_IPV4C){if(!s)return _par('');var slash=s.indexOf('/');if(slash===-1){var r=_ipv4(s);return(r==='ok'||r==='partial'||r==='empty')?_par(''):(r==='badchar'?_err('Invalid character'):r==='badrange'?_err('Octet out of range'):_err('Invalid format'));}var r=_ipv4(s.slice(0,slash));if(r!=='ok')return r==='badchar'?_err('Invalid character'):r==='badrange'?_err('Octet out of range'):_par('');var pfx=s.slice(slash+1);if(!pfx)return _par('');if(/[^0-9]/.test(pfx))return _err('Invalid character');var n=parseInt(pfx,10);return(n>=0&&n<=32)?_ok():_err('Prefix must be 0-32');} + if(flag===F_SUBNET){if(!arg1)return _par('');var prefix=parseInt(arg1,10);if(isNaN(prefix)||prefix<1||prefix>30)return _err('Prefix must be 1-30');var r=_ipv4(s);if(r!=='ok')return(r==='partial'||r==='empty')?_par(''):(r==='badchar'?_err('Invalid character'):_err('Invalid format'));var mB=prefix===0?0:((0xFFFFFFFF<<(32-prefix))>>>0);var ipN=s.split('.').reduce(function(a,o){return(a<<8|+o)>>>0;},0);return((ipN&(~mB>>>0))!==0)?_err('Host bits must be zero'):_ok();} + if(flag===F_ADDR){var r=_ipv4(s);if(r!=='ok')return(r==='partial'||r==='empty')?_par(''):(r==='badchar'?_err('Invalid character'):_err('Invalid format'));if(!arg1||!arg2)return _par('');var prefix=parseInt(arg1,10);if(isNaN(prefix)||prefix<1||prefix>30)return _par('');if(_ipv4(arg2)!=='ok')return _par('');var mB=prefix===0?0:((0xFFFFFFFF<<(32-prefix))>>>0);var snN=arg2.split('.').reduce(function(a,o){return(a<<8|+o)>>>0;},0);if((snN&(~mB>>>0))!==0)return _par('');var iPts=s.split('.').map(Number),sPts=arg2.split('.').map(Number);var ipN=((iPts[0]<<24)|(iPts[1]<<16)|(iPts[2]<<8)|iPts[3])>>>0,sN=((sPts[0]<<24)|(sPts[1]<<16)|(sPts[2]<<8)|sPts[3])>>>0;if((ipN&mB)!==(sN&mB))return _err('IP not in VLAN subnet');var hM=(~mB)>>>0,netN=(sN&mB)>>>0;if(ipN===netN)return _err('Network address not allowed');if(ipN===(netN|hM)>>>0)return _err('Broadcast address not allowed');return _ok();} + return _par(''); +} +function _checkLine(s){ + var anyPartial=false,firstMsg=''; + var flags=[F_IPV4,F_IPV6,F_SUBNET,F_ADDR,F_MAC,F_URL,F_PORT,F_DASH,F_NET,F_DOMAIN,F_T24H,F_RNGINT,F_ENDPT,F_IPV4C]; + for(var i=0;i{e(o["label"])}' for o in options ) - validate = item.get('validate', '') + validate_raw = item.get('validate', '') depends = item.get('depends', []) - validate_attr = f' data-validate="{e(validate)}"' if validate else '' + _vmask = parse_validation(validate_raw) if validate_raw else 0 + validate_attr = f' data-validate="{_vmask}"' if _vmask else '' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' - if validate: + if _vmask: return ( f'
' f'
' @@ -500,10 +656,11 @@ def build_field(item, tokens): if input_type == 'number': min_attr = f' min="{item["min"]}"' if 'min' in item else '' max_attr = f' max="{item["max"]}"' if 'max' in item else '' - validate = item.get('validate', 'positive_int') + validate_raw = item.get('validate', 'VALIDATION_RANGE_INT') depends = item.get('depends', []) existing_ids = apply_tokens(item.get('existing_ids', ''), tokens) - validate_attr = f' data-validate="{e(validate)}"' + _vmask = parse_validation(validate_raw) + validate_attr = f' data-validate="{_vmask}"' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' existing_attr = f' data-existing-ids="{e(existing_ids)}"' if existing_ids else '' dyn_hint_html = '' @@ -612,11 +769,12 @@ def build_field(item, tokens): ] return build_table_picker(name, label, current, picker_rows, headers, summary_config, action_btn_html) - validate = item.get('validate', '') + validate_raw = item.get('validate', '') depends = item.get('depends', []) - validate_attr = f' data-validate="{e(validate)}"' if validate else '' + _vmask = parse_validation(validate_raw) if validate_raw else 0 + validate_attr = f' data-validate="{_vmask}"' if _vmask else '' depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else '' - if validate: + if _vmask: return ( f'
' f'
{hint}

' if hint else '' - validate = e(item.get('validate', '')) + validate_raw = item.get('validate', '') + _vmask = parse_validation(validate_raw) if validate_raw else 0 try: items_list = json.loads(apply_tokens(item.get('items', '[]'), tokens)) @@ -654,7 +813,7 @@ def build_editable_list(item, tokens): '
' for v in items_list ) - validate_attr = f' data-validate="{validate}"' if validate else '' + validate_attr = f' data-validate="{_vmask}"' if _vmask else '' return ( f'
' f'
' @@ -843,7 +1002,7 @@ def build_table(item, tokens, inherited_req=None): thead += '' expanded_ra_fields = { - i: expand_fields(ra.get('fields', []), tokens) + i: _encode_field_validations(expand_fields(ra.get('fields', []), tokens)) for i, ra in enumerate(row_actions) if ra.get('method', 'post').lower() == 'inline_edit' } @@ -1174,20 +1333,18 @@ def build_item(item, tokens, inherited_req=None): f_name = e(f.get('name', '')) f_placeholder = e(f.get('placeholder', '')) f_required = 'true' if f.get('required') else 'false' - f_validate = f.get('validate', '') - f_valtype = f.get('valtype', '') - f_attrs = f.get('attrs', {}) + f_validate_raw = f.get('validate', '') or f.get('valtype', '') + f_attrs = f.get('attrs', {}) + _vmask = parse_validation(f_validate_raw) if f_validate_raw else 0 attr_str = f' data-field="{f_name}" data-required="{f_required}"' - if f_validate: - attr_str += f' data-validate="{e(f_validate)}"' - if f_valtype: - attr_str += f' data-valtype="{e(f_valtype)}"' + if _vmask: + attr_str += f' data-validate="{_vmask}"' for ak, av in f_attrs.items(): attr_str += f' {e(ak)}="{e(str(av))}"' inp = f'' - if f_validate or f_valtype: + if _vmask: field_inner = ( '
' + inp + @@ -1249,11 +1406,12 @@ def build_item(item, tokens, inherited_req=None): label = e(item.get('label', '')) name = e(item.get('name', '')) override_name = e(item.get('override_name', name + '_override')) - validate = e(item.get('validate', '')) - validate_attr = f' data-validate-lines="{validate}"' if validate else '' - dyn_hint_html = '' if validate else '' - wrap_open = '
' if validate else '' - wrap_close = '
' if validate else '' + validate_raw = item.get('validate', '') + _vmask = parse_validation(validate_raw) if validate_raw else 0 + validate_attr = f' data-validate-lines="{_vmask}"' if _vmask else '' + dyn_hint_html = '' if _vmask else '' + wrap_open = '
' if _vmask else '' + wrap_close = '
' if _vmask else '' hint = e(apply_tokens(item.get('hint', ''), tokens)) hint_html = f'

{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 ============================================================