Development
This commit is contained in:
parent
6c3abca58c
commit
96f6e32c8f
9 changed files with 294 additions and 166 deletions
|
|
@ -17,41 +17,21 @@ 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',
|
||||
VALIDATION_FLAGS = {
|
||||
'VALIDATION_IPV4_FORMAT': 1 << 0,
|
||||
'VALIDATION_IPV6_FORMAT': 1 << 1,
|
||||
'VALIDATION_SUBNET': 1 << 2,
|
||||
'VALIDATION_ADDRESS': 1 << 3,
|
||||
'VALIDATION_MAC': 1 << 4,
|
||||
'VALIDATION_URL': 1 << 5,
|
||||
'VALIDATION_PORT': 1 << 6,
|
||||
'VALIDATION_DASH_NAME': 1 << 7,
|
||||
'VALIDATION_NETWORK_NAME': 1 << 8,
|
||||
'VALIDATION_DOMAIN_NAME': 1 << 9,
|
||||
'VALIDATION_TIME24H': 1 << 10,
|
||||
'VALIDATION_RANGE_INT': 1 << 11,
|
||||
'VALIDATION_ENDPOINT': 1 << 12,
|
||||
'VALIDATION_IPV4_CIDR': 1 << 13,
|
||||
}
|
||||
|
||||
# Utilities ===========================================================
|
||||
|
|
@ -102,11 +82,10 @@ def js_str(value):
|
|||
def parse_validation(s):
|
||||
if not s:
|
||||
return 0
|
||||
resolved = _COMPAT_VALIDATION.get(s, s)
|
||||
result = 0
|
||||
for token in resolved.split('|'):
|
||||
for token in s.split('|'):
|
||||
token = token.strip()
|
||||
val = _VALIDATION_FLAGS.get(token)
|
||||
val = VALIDATION_FLAGS.get(token)
|
||||
if val is None:
|
||||
print(f'[factory] WARNING: unknown validation token "{token}" in "{s}"', file=sys.stderr)
|
||||
continue
|
||||
|
|
@ -128,26 +107,6 @@ def _encode_field_validations(fields):
|
|||
|
||||
|
||||
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};}
|
||||
|
|
@ -172,33 +131,34 @@ function _ipv6(s){
|
|||
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;i<g.length;i++){if(g[i].length>2)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;i<lb.length;i++){if(!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(lb[i]))return _err('Invalid URL format');}if(rest[0]===':'){var pm=rest.slice(1).match(/^\d+/);if(!pm)return _par('');if(parseInt(pm[0])<1||parseInt(pm[0])>65535)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;i<lb.length;i++){var l=lb[i];if(l[0]==='-'||l[l.length-1]==='-')return _err('Invalid domain format');}return _ok();}
|
||||
if(flag===F_T24H){if(!s)return _par('');if(/[^0-9:]/.test(s))return _err('Digits and colon only');if(s.length<5)return _par('');return /^([01]\d|2[0-3]):[0-5]\d$/.test(s)?_ok():_err('Must be HH:MM in 24-hour format (e.g. 02:30)');}
|
||||
if(flag===F_RNGINT){if(s===''||s===null||s===undefined)return _par('');if(/[^0-9]/.test(s))return _err('Digits only');var n=parseInt(s,10);var mn=(arg1!==''&&arg1!=null)?parseInt(arg1,10):0;var mx=(arg2!==''&&arg2!=null)?parseInt(arg2,10):null;if(n<mn||(mx!==null&&n>mx)){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 _checkDomain(s){
|
||||
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;i<lb.length;i++){var l=lb[i];if(l[0]==='-'||l[l.length-1]==='-')return _err('Invalid domain format');}
|
||||
return _ok();
|
||||
}
|
||||
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<flags.length;i++){
|
||||
if(!(validation&flags[i]))continue;
|
||||
var r=_checkFlag(s,flags[i]);
|
||||
if(r.ok)return r;
|
||||
if(r.partial)anyPartial=true;
|
||||
else if(!firstMsg)firstMsg=r.msg;
|
||||
}
|
||||
function _acc(r){if(r.ok)return r;if(r.partial)anyPartial=true;else if(!firstMsg)firstMsg=r.msg;return null;}
|
||||
var t;
|
||||
if(validation&1){t=_acc(function(){var rv=_ipv4(s);if(rv==='ok')return _ok();if(rv==='partial'||rv==='empty')return _par('');if(rv==='badchar')return _err('Invalid character');if(rv==='badrange')return _err('Octet out of range');return _err('Invalid format');}());if(t)return t;}
|
||||
if(validation&2){t=_acc(function(){var rv=_ipv6(s);if(rv==='ok')return _ok();if(rv==='partial'||rv==='empty')return _par('');if(rv==='badchar')return _err('Invalid character');return _err('Invalid format');}());if(t)return t;}
|
||||
if(validation&4){t=_acc(function(){if(!arg1)return _par('');var prefix=parseInt(arg1,10);if(isNaN(prefix)||prefix<1||prefix>30)return _err('Prefix must be 1-30');var rv=_ipv4(s);if(rv!=='ok')return(rv==='partial'||rv==='empty')?_par(''):(rv==='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(t)return t;}
|
||||
if(validation&8){t=_acc(function(){var rv=_ipv4(s);if(rv!=='ok')return(rv==='partial'||rv==='empty')?_par(''):(rv==='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();}());if(t)return t;}
|
||||
if(validation&16){t=_acc(function(){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;i<g.length;i++){if(g[i].length>2)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(t)return t;}
|
||||
if(validation&32){t=_acc(function(){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;i<lb.length;i++){if(!/^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?$/.test(lb[i]))return _err('Invalid URL format');}if(rest[0]===':'){var pm=rest.slice(1).match(/^\d+/);if(!pm)return _par('');if(parseInt(pm[0])<1||parseInt(pm[0])>65535)return _err('Invalid URL format');}return _ok();}());if(t)return t;}
|
||||
if(validation&64){t=_acc(function(){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(t)return t;}
|
||||
if(validation&128){t=_acc(function(){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(t)return t;}
|
||||
if(validation&256){t=_acc(function(){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(t)return t;}
|
||||
if(validation&512){t=_acc(_checkDomain(s));if(t)return t;}
|
||||
if(validation&1024){t=_acc(function(){if(!s)return _par('');if(/[^0-9:]/.test(s))return _err('Digits and colon only');if(s.length<5)return _par('');return /^([01]\d|2[0-3]):[0-5]\d$/.test(s)?_ok():_err('Must be HH:MM in 24-hour format (e.g. 02:30)');}());if(t)return t;}
|
||||
if(validation&2048){t=_acc(function(){if(s===''||s===null||s===undefined)return _par('');if(/[^0-9]/.test(s))return _err('Digits only');var n=parseInt(s,10);var mn=(arg1!==''&&arg1!=null)?parseInt(arg1,10):0;var mx=(arg2!==''&&arg2!=null)?parseInt(arg2,10):null;if(n<mn||(mx!==null&&n>mx)){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(t)return t;}
|
||||
if(validation&4096){t=_acc(function(){if(!s)return _par('');if(/^[0-9.]+$/.test(s)){var rv=_ipv4(s);return rv==='ok'?_ok():(rv==='partial'||rv==='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 _checkDomain(s.slice(0,s.lastIndexOf(':')));}return _checkDomain(s);}());if(t)return t;}
|
||||
if(validation&8192){t=_acc(function(){if(!s)return _par('');var slash=s.indexOf('/');if(slash===-1){var rv=_ipv4(s);return(rv==='ok'||rv==='partial'||rv==='empty')?_par(''):(rv==='badchar'?_err('Invalid character'):rv==='badrange'?_err('Octet out of range'):_err('Invalid format'));}var rv=_ipv4(s.slice(0,slash));if(rv!=='ok')return rv==='badchar'?_err('Invalid character'):rv==='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(t)return t;}
|
||||
return anyPartial?_par(''):_err(firstMsg||'Invalid');
|
||||
}
|
||||
var lines=value.split('\n'),hasPartial=false,seen={},hasContent=false;
|
||||
|
|
@ -215,7 +175,7 @@ for(var i=0;i<lines.length;i++){
|
|||
if(!hasContent)return _par('');
|
||||
if(hasPartial)return _par('');
|
||||
return _ok();"""
|
||||
return f'function bigValidate(value,validation,collisions,dedup,arg1,arg2){{{decls}{body}\n}}'
|
||||
return f'function bigValidate(value,validation,collisions,dedup,arg1,arg2){{{body}\n}}'
|
||||
|
||||
|
||||
def get_worker_id(datasource):
|
||||
|
|
@ -826,18 +786,20 @@ def build_field(item, tokens):
|
|||
validate_attr = f' data-validate="{_vmask}"' if _vmask else ''
|
||||
depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else ''
|
||||
extra_attrs = ''.join(f' {e(ak)}="{e(str(av))}"' for ak, av in item.get('attrs', {}).items())
|
||||
existing_ids = apply_tokens(item.get('existing_ids', ''), tokens)
|
||||
existing_attr = f' data-existing-ids="{e(existing_ids)}"' if existing_ids else ''
|
||||
if _vmask:
|
||||
return (
|
||||
f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||
f'<div class="field-wrap"><input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
||||
f' placeholder="{placeholder}" class="form-input"{readonly}{validate_attr}{depends_attr}{extra_attrs}/>'
|
||||
f' placeholder="{placeholder}" class="form-input"{readonly}{validate_attr}{depends_attr}{extra_attrs}{existing_attr}/>'
|
||||
f'<p class="form-hint field-dyn-hint hidden"></p></div>'
|
||||
f'{hint_html}</div>'
|
||||
)
|
||||
return (
|
||||
f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
||||
f' placeholder="{placeholder}" class="form-input"{readonly}{validate_attr}{depends_attr}{extra_attrs}/>'
|
||||
f' placeholder="{placeholder}" class="form-input"{readonly}{validate_attr}{depends_attr}{extra_attrs}{existing_attr}/>'
|
||||
f'{hint_html}</div>'
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ def blocklists_edit():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
|
||||
# Blocklist name must be unique - it is the lookup key for VLAN use_blocklists references
|
||||
err = validate.check_blocklist_name_unique(items, fields['name'], exclude_idx=idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
items[idx].update({
|
||||
'name': fields['name'],
|
||||
'description': fields['description'],
|
||||
|
|
@ -134,8 +141,10 @@ def addblocklist_add():
|
|||
cfg = load_config()
|
||||
blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
||||
|
||||
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
||||
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
||||
# Blocklist name must be unique - it is the lookup key for VLAN use_blocklists references
|
||||
err = validate.check_blocklist_name_unique(blocklists, fields['name'])
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ def _parse_entry():
|
|||
protocol = sanitize.filtervalue(request.form.get('protocol', ''), validate.VALID_PROTOCOLS)
|
||||
src_raw = request.form.get('src_ip_or_subnet', '').strip()
|
||||
dst_raw = request.form.get('dst_ip_or_subnet', '').strip()
|
||||
dst_port_raw = request.form.get('dst_port', '').strip()
|
||||
dst_port_min_raw = request.form.get('dst_port_min', '').strip()
|
||||
dst_port_max_raw = request.form.get('dst_port_max', '').strip()
|
||||
|
||||
if not protocol:
|
||||
flash(f'The configuration has not been saved because the protocol is invalid. '
|
||||
|
|
@ -56,19 +57,31 @@ def _parse_entry():
|
|||
flash(f'The configuration has not been saved because "{dst_raw}" is not a valid IP address or subnet.', 'error')
|
||||
return None, True
|
||||
|
||||
dst_port = ''
|
||||
if dst_port_raw:
|
||||
dst_port = validate.port(dst_port_raw)
|
||||
if not dst_port:
|
||||
flash(f'The configuration has not been saved because "{dst_port_raw}" is not a valid port number (1-65535).', 'error')
|
||||
dst_port_min = ''
|
||||
if dst_port_min_raw:
|
||||
dst_port_min = validate.port(dst_port_min_raw)
|
||||
if not dst_port_min:
|
||||
flash(f'The configuration has not been saved because "{dst_port_min_raw}" is not a valid port number (1-65535).', 'error')
|
||||
return None, True
|
||||
|
||||
dst_port_max = ''
|
||||
if dst_port_max_raw:
|
||||
dst_port_max = validate.port(dst_port_max_raw)
|
||||
if not dst_port_max:
|
||||
flash(f'The configuration has not been saved because "{dst_port_max_raw}" is not a valid port number (1-65535).', 'error')
|
||||
return None, True
|
||||
|
||||
if dst_port_min and dst_port_max and int(dst_port_min) > int(dst_port_max):
|
||||
flash('Port range min must not be greater than max.', 'error')
|
||||
return None, True
|
||||
|
||||
return {
|
||||
'description': description,
|
||||
'protocol': protocol,
|
||||
'src_ip_or_subnet': src,
|
||||
'dst_ip_or_subnet': dst,
|
||||
'dst_port': dst_port,
|
||||
'dst_port_min': dst_port_min,
|
||||
'dst_port_max': dst_port_max,
|
||||
'enabled': True,
|
||||
}, None
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
{
|
||||
"label": "Protocol",
|
||||
"field": "protocol",
|
||||
"class": "col-mono"
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Source",
|
||||
|
|
@ -39,9 +39,14 @@
|
|||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "Dest Port",
|
||||
"field": "dst_port",
|
||||
"class": "col-mono"
|
||||
"label": "Port Min",
|
||||
"field": "dst_port_min",
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Port Max",
|
||||
"field": "dst_port_max",
|
||||
"class": "col-mono col-narrow"
|
||||
},
|
||||
{
|
||||
"label": "Status",
|
||||
|
|
@ -75,8 +80,12 @@
|
|||
"input_type": "text"
|
||||
},
|
||||
{
|
||||
"col": "dst_port",
|
||||
"input_type": "text"
|
||||
"col": "dst_port_min",
|
||||
"input_type": "number"
|
||||
},
|
||||
{
|
||||
"col": "dst_port_max",
|
||||
"input_type": "number"
|
||||
},
|
||||
{
|
||||
"col": "enabled",
|
||||
|
|
@ -112,36 +121,53 @@
|
|||
"input_type": "text",
|
||||
"placeholder": "e.g. Allow Chromecast"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Protocol",
|
||||
"name": "protocol",
|
||||
"input_type": "select",
|
||||
"options": "%PROTOCOL_OPTIONS%"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Source",
|
||||
"name": "src_ip_or_subnet",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.20.0/24"
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.20.100 or 192.168.20.0/24",
|
||||
"hint": "You may allow either a single device IP or an entire subnet to contact dest."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Destination",
|
||||
"name": "dst_ip_or_subnet",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_FORMAT",
|
||||
"placeholder": "e.g. 192.168.10.100"
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_IPV4_CIDR",
|
||||
"placeholder": "e.g. 192.168.10.200 or 192.168.10.0/24",
|
||||
"hint": "You may allow either a single device IP or an entire subnet to be reached by source."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Dest Port",
|
||||
"name": "dst_port",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_PORT",
|
||||
"placeholder": "e.g. 8009"
|
||||
"type": "field_row",
|
||||
"cols": 3,
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Protocol",
|
||||
"name": "protocol",
|
||||
"input_type": "select",
|
||||
"options": "%PROTOCOL_OPTIONS%"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Port Min",
|
||||
"name": "dst_port_min",
|
||||
"input_type": "number",
|
||||
"min": 1,
|
||||
"max": 65535,
|
||||
"hint": "This exception only applies to traffic over this port range and protocol."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Port Max",
|
||||
"name": "dst_port_max",
|
||||
"input_type": "number",
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
|
|
|
|||
|
|
@ -218,25 +218,37 @@ def vlans_addedit():
|
|||
existing = vlans[edit_idx]
|
||||
is_vpn = existing.get('is_vpn', False)
|
||||
|
||||
if is_vpn and mdns_reflection:
|
||||
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||
# VPN VLANs do not support mDNS reflection
|
||||
err = validate.check_mdns_vpn(is_vpn, mdns_reflection)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# VLAN 1 maps to the physical LAN interface; its ID is fixed
|
||||
current_id = existing.get('vlan_id')
|
||||
if current_id == 1 and vlan_id != 1:
|
||||
flash('VLAN 1 is the physical interface and cannot change its ID.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if vlan_id != current_id and any(
|
||||
v.get('vlan_id') == vlan_id for i, v in enumerate(vlans) if i != edit_idx
|
||||
):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
# VLAN ID must be unique across all VLANs (used as 802.1Q tag and interface name)
|
||||
err = validate.check_vlan_id_unique(vlans, vlan_id, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if radius_default and any(i != edit_idx and v.get('radius_default') for i, v in enumerate(vlans)):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
# VLAN name must be unique - it is used as the change-history lookup key
|
||||
err = validate.check_vlan_name_unique(vlans, name, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# Only one VLAN may be the RADIUS default (used for dynamic VLAN assignment)
|
||||
if radius_default:
|
||||
err = validate.check_radius_default_unique(vlans, exclude_idx=edit_idx)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(existing)
|
||||
existing.update({
|
||||
'name': name,
|
||||
|
|
@ -269,18 +281,31 @@ def vlans_addedit():
|
|||
else:
|
||||
is_vpn = 'is_vpn' in request.form
|
||||
|
||||
if is_vpn and mdns_reflection:
|
||||
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||
# VPN VLANs do not support mDNS reflection
|
||||
err = validate.check_mdns_vpn(is_vpn, mdns_reflection)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if any(v.get('vlan_id') == vlan_id for v in vlans):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
# VLAN ID must be unique across all VLANs (used as 802.1Q tag and interface name)
|
||||
err = validate.check_vlan_id_unique(vlans, vlan_id)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if radius_default and any(v.get('radius_default') for v in vlans):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
# VLAN name must be unique - it is used as the change-history lookup key
|
||||
err = validate.check_vlan_name_unique(vlans, name)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
# Only one VLAN may be the RADIUS default (used for dynamic VLAN assignment)
|
||||
if radius_default:
|
||||
err = validate.check_radius_default_unique(vlans)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'name': name,
|
||||
'vlan_id': vlan_id,
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@
|
|||
"name": "name",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_DASH_NAME",
|
||||
"existing_ids": "%EXISTING_VLAN_NAMES_JSON%",
|
||||
"hint": "Lowercase letters, digits, hyphens. E.g. iot"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ def physicalinterface_save():
|
|||
flash('Both WAN and LAN interfaces are required.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if wan == lan:
|
||||
flash('WAN and LAN interfaces must be different.', 'error')
|
||||
# WAN and LAN must be distinct physical interfaces
|
||||
err = validate.check_wan_lan_unique(wan, lan)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
|
|
@ -51,9 +53,11 @@ def physicalinterface_save():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
available = _get_system_interfaces()
|
||||
# Interfaces must exist on this system (checked against physical-only interface list)
|
||||
for iface in (wan, lan):
|
||||
if available and iface not in available:
|
||||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||
err = validate.check_interface_exists(iface, available)
|
||||
if err:
|
||||
flash(err, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
|
|
|
|||
|
|
@ -789,7 +789,8 @@ def collect_tokens():
|
|||
tokens['VLAN_FILTER_OPTIONS'] = filter_opts
|
||||
tokens['VLAN_NAMES_AS_OPTIONS'] = json.dumps([{'value': n, 'label': n} for n in vlan_names])
|
||||
tokens['VPN_VLAN_COUNT'] = str(sum(1 for v in vlans if v.get('is_vpn')))
|
||||
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([v.get('vlan_id') for v in vlans])
|
||||
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([v.get('vlan_id') for v in vlans])
|
||||
tokens['EXISTING_VLAN_NAMES_JSON'] = json.dumps([v.get('name') for v in vlans])
|
||||
_dv = next((v for v in vlans if v.get('radius_default')), None)
|
||||
tokens['RADIUS_DEFAULT_VLAN'] = f'"{_dv["name"]}" (VLAN {_dv["vlan_id"]})' if _dv else 'none set'
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -253,6 +253,79 @@ def _check_banned_ipv6(ip_str):
|
|||
raise ValueError(f"IPv6 wildcard must have 1-7 prefix groups: {ip_str!r}")
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Cross-VLAN uniqueness checks (callable independently by action.py)
|
||||
# ===================================================================
|
||||
|
||||
def check_vlan_name_unique(vlans, name, exclude_idx=None):
|
||||
"""Return error string if name is already used by another VLAN, else None."""
|
||||
for i, v in enumerate(vlans):
|
||||
if exclude_idx is not None and i == exclude_idx:
|
||||
continue
|
||||
if v.get('name') == name:
|
||||
return f'VLAN name "{name}" is already in use (VLAN ID {v.get("vlan_id")}).'
|
||||
return None
|
||||
|
||||
|
||||
def check_vlan_id_unique(vlans, vlan_id, exclude_idx=None):
|
||||
"""Return error string if vlan_id is already used by another VLAN, else None."""
|
||||
for i, v in enumerate(vlans):
|
||||
if exclude_idx is not None and i == exclude_idx:
|
||||
continue
|
||||
if v.get('vlan_id') == vlan_id:
|
||||
return f'VLAN ID {vlan_id} is already in use (VLAN "{v.get("name")}").'
|
||||
return None
|
||||
|
||||
|
||||
def check_radius_default_unique(vlans, exclude_idx=None):
|
||||
"""Return error string if another VLAN (not exclude_idx) is already the RADIUS default, else None."""
|
||||
for i, v in enumerate(vlans):
|
||||
if exclude_idx is not None and i == exclude_idx:
|
||||
continue
|
||||
if v.get('radius_default'):
|
||||
return f'VLAN "{v.get("name", "?")}" is already the RADIUS default. Only one VLAN may be the default.'
|
||||
return None
|
||||
|
||||
|
||||
def check_mdns_vpn(is_vpn, mdns_reflection):
|
||||
"""Return error string if mDNS reflection is enabled on a VPN VLAN, else None."""
|
||||
if is_vpn and mdns_reflection:
|
||||
return 'mDNS reflection is not supported on VPN VLANs.'
|
||||
return None
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Blocklist checks (callable independently by action.py)
|
||||
# ===================================================================
|
||||
|
||||
def check_blocklist_name_unique(blocklists, name, exclude_idx=None):
|
||||
"""Return error string if name is already used by another blocklist entry, else None."""
|
||||
for i, b in enumerate(blocklists):
|
||||
if exclude_idx is not None and i == exclude_idx:
|
||||
continue
|
||||
if b.get('name') == name:
|
||||
return f'A blocklist named "{name}" already exists.'
|
||||
return None
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# Physical interface checks (callable independently by action.py)
|
||||
# ===================================================================
|
||||
|
||||
def check_wan_lan_unique(wan, lan):
|
||||
"""Return error string if WAN and LAN resolve to the same interface, else None."""
|
||||
if wan and lan and wan == lan:
|
||||
return 'WAN and LAN interfaces must be different.'
|
||||
return None
|
||||
|
||||
|
||||
def check_interface_exists(iface, available):
|
||||
"""Return error string if iface is absent from the available set (when non-empty), else None."""
|
||||
if available and iface not in available:
|
||||
return f"Interface '{iface}' does not exist on this system."
|
||||
return None
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# VLAN / interface helpers (shared with core.py apply logic)
|
||||
# ===================================================================
|
||||
|
|
@ -300,9 +373,7 @@ def derive_interface(vlan, data):
|
|||
def validate_config(data):
|
||||
"""Validate config.json structure and content. Returns list of error strings."""
|
||||
errors = []
|
||||
seen_vlan_ids = {}
|
||||
seen_interfaces = {}
|
||||
seen_names = {}
|
||||
seen_listen_ports = {}
|
||||
|
||||
# Pre-compute per-VLAN vlan_ids and interface names without mutating data
|
||||
|
|
@ -340,13 +411,15 @@ def validate_config(data):
|
|||
available_interfaces = set(os.listdir('/sys/class/net'))
|
||||
except Exception:
|
||||
pass
|
||||
if available_interfaces:
|
||||
if wan not in available_interfaces:
|
||||
errors.append(f"network_interfaces.wan_interface: '{wan}' does not exist on this system.")
|
||||
if lan not in available_interfaces:
|
||||
errors.append(f"network_interfaces.lan_interface: '{lan}' does not exist on this system.")
|
||||
if wan == lan:
|
||||
errors.append(f"network_interfaces.wan_interface and network_interfaces.lan_interface must be different (both set to '{wan}').")
|
||||
err = check_interface_exists(wan, available_interfaces)
|
||||
if err:
|
||||
errors.append(f"network_interfaces.wan_interface: {err}")
|
||||
err = check_interface_exists(lan, available_interfaces)
|
||||
if err:
|
||||
errors.append(f"network_interfaces.lan_interface: {err}")
|
||||
err = check_wan_lan_unique(wan, lan)
|
||||
if err:
|
||||
errors.append(f"network_interfaces: {err}")
|
||||
|
||||
# Blocklist library =============================================
|
||||
blocklists_by_name = {}
|
||||
|
|
@ -359,8 +432,11 @@ def validate_config(data):
|
|||
if bl.get("format") and bl["format"] not in VALID_BLOCKLIST_FORMATS:
|
||||
errors.append(f"{label}: format must be one of: {', '.join(sorted(VALID_BLOCKLIST_FORMATS))}.")
|
||||
if name:
|
||||
if name in blocklists_by_name:
|
||||
errors.append(f"{label}: duplicate blocklist name '{name}'.")
|
||||
err = check_blocklist_name_unique(
|
||||
data.get("dns_blocking", {}).get("blocklists", []), name, exclude_idx=idx
|
||||
)
|
||||
if err:
|
||||
errors.append(f"{label}: {err}")
|
||||
else:
|
||||
blocklists_by_name[name] = bl
|
||||
|
||||
|
|
@ -375,17 +451,13 @@ def validate_config(data):
|
|||
if vlan_id is None or not isinstance(vlan_id, int) or not (1 <= vlan_id <= 4094):
|
||||
errors.append(f"vlan '{name}': vlan_id must be an integer 1-4094 (got {vlan_id!r}).")
|
||||
|
||||
if name in seen_names:
|
||||
errors.append(f"{label}: duplicate vlan name '{name}' "
|
||||
f"(also used by id={seen_names[name]}).")
|
||||
else:
|
||||
seen_names[name] = vlan_id
|
||||
err = check_vlan_name_unique(_all_vlans, name, exclude_idx=i)
|
||||
if err:
|
||||
errors.append(f"{label}: {err}")
|
||||
|
||||
if vlan_id in seen_vlan_ids:
|
||||
errors.append(f"{label}: duplicate vlan_id {vlan_id} "
|
||||
f"(also used by '{seen_vlan_ids[vlan_id]}').")
|
||||
else:
|
||||
seen_vlan_ids[vlan_id] = name
|
||||
err = check_vlan_id_unique(_all_vlans, vlan_id, exclude_idx=i)
|
||||
if err:
|
||||
errors.append(f"{label}: {err}")
|
||||
|
||||
if iface in seen_interfaces:
|
||||
errors.append(f"{label}: duplicate interface '{iface}' "
|
||||
|
|
@ -393,8 +465,9 @@ def validate_config(data):
|
|||
else:
|
||||
seen_interfaces[iface] = name
|
||||
|
||||
if vlan.get("mdns_reflection") is True and is_wg(vlan):
|
||||
errors.append(f"{label}: mdns_reflection must be false for WireGuard interfaces.")
|
||||
err = check_mdns_vpn(is_wg(vlan), vlan.get('mdns_reflection'))
|
||||
if err:
|
||||
errors.append(f"{label}: {err}")
|
||||
|
||||
if is_wg(vlan):
|
||||
# vpn_information =======================================
|
||||
|
|
@ -702,14 +775,28 @@ def validate_config(data):
|
|||
if not ipv4_or_cidr(dst):
|
||||
errors.append(f"{label}: dst_ip_or_subnet '{dst}' is not a valid "
|
||||
f"IPv4 address or network.")
|
||||
if r.get("dst_port") is not None:
|
||||
nat_check_port(f"{label} dst_port", r.get("dst_port"))
|
||||
if r.get("dst_port_min"):
|
||||
nat_check_port(f"{label} dst_port_min", r.get("dst_port_min"))
|
||||
if r.get("dst_port_max"):
|
||||
nat_check_port(f"{label} dst_port_max", r.get("dst_port_max"))
|
||||
min_p, max_p = r.get("dst_port_min", ""), r.get("dst_port_max", "")
|
||||
if min_p and max_p:
|
||||
try:
|
||||
if int(min_p) > int(max_p):
|
||||
errors.append(f"{label}: dst_port_min {min_p} is greater than dst_port_max {max_p}.")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# radius_default uniqueness check ===============================
|
||||
defaults = [v["name"] for v in data.get("vlans", []) if v.get("radius_default") is True]
|
||||
if len(defaults) > 1:
|
||||
errors.append(f"Multiple VLANs have radius_default: true ({', '.join(defaults)}). "
|
||||
f"Only one VLAN may be the RADIUS default.")
|
||||
_rd_idx = next(
|
||||
(i for i, v in enumerate(data.get('vlans', [])) if v.get('radius_default')), None
|
||||
)
|
||||
if _rd_idx is not None:
|
||||
err = check_radius_default_unique(data.get('vlans', []), exclude_idx=_rd_idx)
|
||||
if err:
|
||||
defaults = [v.get('name', '?') for v in data.get('vlans', []) if v.get('radius_default')]
|
||||
errors.append(f"Multiple VLANs have radius_default: true ({', '.join(defaults)}). "
|
||||
f"Only one VLAN may be the RADIUS default.")
|
||||
|
||||
# RADIUS requires multiple VLANs ================================
|
||||
non_wg_vlans = [v for v in data.get("vlans", []) if not is_wg(v)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue