DHCP option validate (#1718)

* Fix form validation

* Add DHCP option validation

* More options + placeholder text
This commit is contained in:
Tim Wilkinson 2024-11-25 22:29:18 -08:00 committed by GitHub
parent 9250d68a3b
commit f0270c4f37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 224 additions and 132 deletions

View File

@ -148,7 +148,7 @@
if (d) {
setTimeout(function() {
let invalid = false;
const f = document.querySelectorAll(m, "form");
const f = m.querySelectorAll("form");
for (let i = 0; i < f.length; i++) {
if (!f[i].checkValidity()) {
invalid = true;

View File

@ -161,67 +161,67 @@ if (f) {
f.close();
}
const dhcpOptionTypes = {
"1": "netmask",
"2": "time-offset",
"3": "router",
"6": "dns-server",
"7": "log-server",
"9": "lpr-server",
"13": "boot-file-size",
"15": "domain-name",
"16": "swap-server",
"17": "root-path",
"18": "extension-path",
"19": "ip-forward-enable",
"20": "non-local-source-routing",
"21": "policy-filter",
"22": "max-datagram-reassembly",
"23": "default-ttl",
"26": "mtu",
"27": "all-subnets-local",
"31": "router-discovery",
"32": "router-solicitation",
"33": "static-route",
"34": "trailer-encapsulation",
"35": "arp-timeout",
"36": "ethernet-encap",
"37": "tcp-ttl",
"38": "tcp-keepalive",
"40": "nis-domain",
"41": "nis-server",
"42": "ntp-server",
"44": "netbios-ns",
"45": "netbios-dd",
"46": "netbios-nodetype",
"47": "netbios-scope",
"48": "x-windows-fs",
"49": "x-windows-dm",
"58": "T1",
"59": "T2",
"60": "vendor-class",
"64": "nis+-domain",
"65": "nis+-server",
"66": "tftp-server",
"67": "bootfile-name",
"68": "mobile-ip-home",
"69": "smtp-server",
"70": "pop3-server",
"71": "nntp-server",
"74": "irc-server",
"77": "user-class",
"80": "rapid-commit",
"93": "client-arch",
"94": "client-interface-id",
"97": "client-machine-id",
"100": "posix-timezone",
"101": "tzdb-timezone",
"108": "ipv6-only",
"119": "domain-search",
"120": "sip-server",
"121": "classless-static-route",
"125": "vendor-id-encap",
"150": "tftp-server-address",
"255": "server-ip-address"
"1": ["netmask", "mask"],
"2": ["time-offset", "int32"],
"3": ["router", "ip"],
"6": ["dns-server", "ips"],
"7": ["log-server", "ip"],
"9": ["lpr-server", "ip"],
"13": ["boot-file-size", "1...65535"],
"15": ["domain-name", "text"],
"16": ["swap-server", "ip"],
"17": ["root-path", "text"],
"18": ["extension-path", "text"],
"19": ["ip-forward-enable", "flag"],
"20": ["non-local-source-routing", "flag"],
"21": ["policy-filter", "ipips"],
"22": ["max-datagram-reassembly", "(57[6-9]|5[89]\\d|[6-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])", "576...65535"],
"23": ["default-ttl", "1...255"],
"26": ["mtu", "(6[89]|[7-9]\\d|[1-9]\\d{2,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])"],
"27": ["all-subnets-local", "flag"],
"31": ["router-discovery", "flag"],
"32": ["router-solicitation", "ip"],
"33": ["static-route", "ipips"],
"34": ["trailer-encapsulation", "flag"],
"35": ["arp-timeout", "uint32"],
"36": ["ethernet-encap", "flag"],
"37": ["tcp-ttl", "1...255"],
"38": ["tcp-keepalive", "uint32"],
"40": ["nis-domain", "text"],
"41": ["nis-server", "ip"],
"42": ["ntp-server", "ip"],
"44": ["netbios-ns", "ip"],
"45": ["netbios-dd", "ip"],
"46": ["netbios-nodetype", "1|2|4|8", "1, 2, 4 or 8"],
"47": ["netbios-scope", "?"],
"48": ["x-windows-fs", "ip"],
"49": ["x-windows-dm", "ip"],
"58": ["T1", "time"],
"59": ["T2", "time"],
"60": ["vendor-class", "text"],
"64": ["nis+-domain", "text"],
"65": ["nis+-server", "ip"],
"66": ["tftp-server", "ip"],
"67": ["bootfile-name", "text"],
"68": ["mobile-ip-home", "ip"],
"69": ["smtp-server", "ip"],
"70": ["pop3-server", "ip"],
"71": ["nntp-server", "ip"],
"74": ["irc-server", "ip"],
"77": ["user-class", "text"],
"80": ["rapid-commit", "flag"],
"93": ["client-arch", "uint16"],
"94": ["client-interface-id", "?"],
"97": ["client-machine-id", "?"],
"100": ["posix-timezone", "?"],
"101": ["tzdb-timezone", "?"],
"108": ["ipv6-only", "?"],
"119": ["domain-search", "text"],
"120": ["sip-server", "ip"],
"121": ["classless-static-route", "((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}/([1-9]|[12]\\d|3[0-2]),((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}(,((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}/([1-9]|[12]\\d|3[0-2]),((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4})*", "IP Address/CIDR,IP Address..."],
"125": ["vendor-id-encap", "?"],
"150": ["tftp-server-address", "ip"],
"255": ["server-ip-address", "ip"]
};
%}
<div class="dialog">
@ -305,87 +305,91 @@ const dhcpOptionTypes = {
{{_R("dialog-advanced")}}
<div>
{% if (includeAdvanced) { %}
<div class="dhcp-tags">
<div class="cols">
<div>
<div class="o">Tags</div>
<div class="m">Tags for advanced options</div>
<form>
<div class="dhcp-tags">
<div class="cols">
<div>
<div class="o">Tags</div>
<div class="m">Tags for advanced options</div>
</div>
<button>+</button>
</div>
<button>+</button>
</div>
<div class="dhcptag-label adr">
<div class="row">
<div>tag</div>
<div>type</div>
<div>match</div>
<div></div>
</div>
<div></div>
</div>
<div class="list noborder">{%
for (let i = 0; i < length(advtags); i++) {
const t = advtags[i];
%}<div class="tag adr">
<div class="dhcptag-label adr">
<div class="row">
<input name="tag_name" type="text" required value="{{t.name}}">
<select name="tag_type" required>
<option value="">-</option>
<option value="vendorclass" {{t.type == "vendorclass" ? "selected": ""}}>Vendor Class</option>
<option value="userclass" {{t.type == "userclass" ? "selected": ""}}>User Class</option>
<option value="mac" {{t.type == "mac" ? "selected": ""}}>MAC Address</option>
<option value="circuitid" {{t.type == "circuitid" ? "selected": ""}}>Agent Circuit ID</option>
<option value="remoteid" {{t.type == "remoteid" ? "selected": ""}}>Agent Remote ID</option>
<option value="subscriberid" {{t.type == "subscriberid" ? "selected": ""}}>Subscriber-ID</option>
</select>
<input name="tag_match" type="text" required value="{{t.match}}">
<div>tag</div>
<div>type</div>
<div>match</div>
<div></div>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
<div class="dhcp-options">
<div class="cols">
<div>
<div class="o">Options</div>
<div class="m">Advanced options</div>
<div></div>
</div>
<button>+</button>
<div class="list noborder">{%
for (let i = 0; i < length(advtags); i++) {
const t = advtags[i];
%}<div class="tag adr">
<div class="row">
<input name="tag_name" type="text" required value="{{t.name}}">
<select name="tag_type" required>
<option value="">-</option>
<option value="vendorclass" {{t.type == "vendorclass" ? "selected": ""}}>Vendor Class</option>
<option value="userclass" {{t.type == "userclass" ? "selected": ""}}>User Class</option>
<option value="mac" {{t.type == "mac" ? "selected": ""}}>MAC Address</option>
<option value="circuitid" {{t.type == "circuitid" ? "selected": ""}}>Agent Circuit ID</option>
<option value="remoteid" {{t.type == "remoteid" ? "selected": ""}}>Agent Remote ID</option>
<option value="subscriberid" {{t.type == "subscriberid" ? "selected": ""}}>Subscriber-ID</option>
</select>
<input name="tag_match" type="text" required value="{{t.match}}">
<div></div>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
<div class="dhcpoption-label adr">
<div class="row">
<div>tag</div>
<div>option</div>
<div>value</div>
<div>always</div>
</div>
<div></div>
</div>
<div class="list noborder">{%
for (let i = 0; i < length(advoptions); i++) {
const o = advoptions[i];
%}<div class="option adr">
<div class="row">
<select name="option_name">
<option value="{{o.name}}" selected>{{o.name}}</option>
</select>
<input name="option_type" type="text" required list="dhcp-option-type-list" value="{{dhcpOptionTypes[o.type] ? `${o.type}: ${dhcpOptionTypes[o.type]}` : o.type}}" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?">
<input name="option_value" type="text" required value="{{o.value}}">
<label><input name="option_always" type="checkbox" {{o.always ? "checked" : ""}}></label>
</form>
<form>
<div class="dhcp-options">
<div class="cols">
<div>
<div class="o">Options</div>
<div class="m">Advanced options</div>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
<button>+</button>
</div>
<div class="dhcpoption-label adr">
<div class="row">
<div>tag</div>
<div>option</div>
<div>value</div>
<div>always</div>
</div>
<div></div>
</div>
<div class="list noborder">{%
for (let i = 0; i < length(advoptions); i++) {
const o = advoptions[i];
%}<div class="option adr">
<div class="row">
<select name="option_name">
<option value="{{o.name}}" selected>{{o.name}}</option>
</select>
<input name="option_type" type="text" required list="dhcp-option-type-list" value="{{dhcpOptionTypes[o.type][0] ? `${o.type}: ${dhcpOptionTypes[o.type][0]}` : o.type}}" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?">
<input name="option_value" type="text" required value="{{o.value}}">
<label><input name="option_always" type="checkbox" {{o.always ? "checked" : ""}}></label>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
</form>
{% } %}
</div>
</div>
<datalist id="dhcp-option-type-list">
{%
for (let k in dhcpOptionTypes) {
print(`<option value="${k}: ${dhcpOptionTypes[k]}"></option>`);
print(`<option value="${k}: ${dhcpOptionTypes[k][0]}"></option>`);
}
%}
</datalist>
@ -552,8 +556,41 @@ const dhcpOptionTypes = {
options[i].innerHTML = "<option value=''>[all]</option>" + tagnames.map(t => `<option value="${t}" ${t === v ? "selected": ""}>${t}</option>`).join("");
}
}
function refreshTags()
{
const tags = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row");
for (let i = 0; i < tags.length; i++) {
const type = htmx.find(tags[i], "select[name=tag_type]");
const match = htmx.find(tags[i], "input[name=tag_match]");
switch (type.value) {
case "mac":
match.pattern = "(?:(?:[0-9a-fA-F]{2}|\\*):){5}(?:[0-9a-fA-F]{2}|\\*)";
match.placeholder = "*:*:*:*:*:*";
break;
case "circuitid":
case "remoteid":
match.pattern = "(?:(?:[0-9a-fA-F]{2}|\\*):)*(?:[0-9a-fA-F]{2}|\\*)";
match.placeholder = "12:3a:bc";
break;
case "subscriberid":
match.pattern = "(?:[ -\\[\\]-~]|\\\\[\\\\benrt])+";
match.placeholder = "";
break;
case "-":
case "vendorclass":
case "userclass":
default:
match.pattern = ".+";
match.placeholder = "";
break;
}
}
}
function updateTags()
{
if (!htmx.closest(htmx.find("#ctrl-modal .dialog .dhcp-tags"), "form").checkValidity()) {
return;
}
const advtags = [];
const tags = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row");
for (let i = 0; i < tags.length; i++) {
@ -570,8 +607,59 @@ const dhcpOptionTypes = {
});
}
const dhcpOptionTypes = {%print(dhcpOptionTypes);%};
const dhcpOptionTypesPatterns = {
"?": [".*",],
ip: ["((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}", "IP Address"],
ips: ["((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}(,((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4})*", "IP Addresses"],
ipips: ["((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4} ((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}(,((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4} ((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4})*", "IP IP list"],
mask: ["((128|192|224|240|248|252|254)\\.0\\.0\\.0)|(255\\.(((0|128|192|224|240|248|252|254)\\.0\\.0)|(255\\.(((0|128|192|224|240|248|252|254)\\.0)|255\\.(0|128|192|224|240|248|252|254)))))", "Network mask"],
mac: ["(?:(?:[0-9a-fA-F]{2}|\\*):){5}(?:[0-9a-fA-F]{2}|\\*)", "xx:xx:xx:xx:xx:xx"],
flag: ["0|1", "0 or 1"],
text: [".+", "Text..."],
"1...65535": ["([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])", "1...65535"],
uint16: ["(\\d|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])", "0...65535"],
int32: ["-?\\d+", "Integer"],
uint32: ["\\d+", "Unsigned integer"],
"1...255": ["([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])", "1...255"],
"time": ["\d+[sSmMhHdDwWmM]", "Time (s, m, h, d, w, m)"]
};
function refreshOptions()
{
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row");
for (let i = 0; i < options.length; i++) {
const name = htmx.find(options[i], "select[name=option_name]");
const type = htmx.find(options[i], "input[name=option_type]");
const value = htmx.find(options[i], "input[name=option_value]");
const always = htmx.find(options[i], "input[name=option_always]");
if (!type.validity.valid) {
value.pattern = "";
}
else {
const types = dhcpOptionTypes[`${parseInt(type.value)}`];
const pat = dhcpOptionTypesPatterns[types[1]];
if (pat) {
value.pattern = pat[0];
value.placeholder = pat[1];
}
else {
value.pattern = types[1];
value.placeholder = types[2] || "";
}
}
if (name.value === "") {
always.checked = false;
always.disabled = true;
}
else {
always.disabled = false;
}
}
}
function updateOptions()
{
if (!htmx.closest(htmx.find("#ctrl-modal .dialog .dhcp-options"), "form").checkValidity()) {
return;
}
const advoptions = [];
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row");
for (let i = 0; i < options.length; i++) {
@ -584,7 +672,7 @@ const dhcpOptionTypes = {
advoptions.push({ name: name.value, type: tvalue, value: value.value, always: !!always.checked });
}
if (dhcpOptionTypes[tvalue]) {
type.value = `${tvalue}: ${dhcpOptionTypes[tvalue]}`;
type.value = `${tvalue}: ${dhcpOptionTypes[tvalue][0]}`;
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
@ -631,6 +719,7 @@ const dhcpOptionTypes = {
});
htmx.on("#ctrl-modal .dialog .dhcp-tags", "change", _ => {
refreshAdvOptions();
refreshTags();
updateTags();
});
htmx.on("#ctrl-modal .dialog .dhcp-options", "click", event => {
@ -642,7 +731,7 @@ const dhcpOptionTypes = {
function addNewOption()
{
const item = document.createElement("div");
item.innerHTML = `<div class="option adr"><div class="row"><select name="option_name"><option value="">[all]</option></select> <input name="option_type" type="text" required list="dhcp-option-type-list" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?"> <input name="option_value" type="text" required value=""> <label><input name="option_always" type="checkbox"></label></div><button>-</button></div>`;
item.innerHTML = `<div class="option adr"><div class="row"><select name="option_name"><option value="">[all]</option></select> <input name="option_type" type="text" required list="dhcp-option-type-list" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?"> <input name="option_value" type="text" required value=""> <label><input name="option_always" type="checkbox" disabled></label></div><button>-</button></div>`;
const fc = item.firstChild;
htmx.find("#ctrl-modal .dialog .dhcp-options .list").appendChild(fc);
refreshAdvOptions();
@ -670,8 +759,11 @@ const dhcpOptionTypes = {
}
});
htmx.on("#ctrl-modal .dialog .dhcp-options", "change", _ => {
refreshOptions();
updateOptions();
});
refreshTags();
refreshOptions();
refreshAdvOptions();
{% } %}
})();