From f0270c4f3793c02bc443efd2e6cae0cf5ca17eff Mon Sep 17 00:00:00 2001 From: Tim Wilkinson <tim.j.wilkinson@gmail.com> Date: Mon, 25 Nov 2024 22:29:18 -0800 Subject: [PATCH] DHCP option validate (#1718) * Fix form validation * Add DHCP option validation * More options + placeholder text --- files/app/main/app.ut | 2 +- files/app/main/status/e/dhcp.ut | 354 ++++++++++++++++++++------------ 2 files changed, 224 insertions(+), 132 deletions(-) diff --git a/files/app/main/app.ut b/files/app/main/app.ut index e7d2f017..a347a9b7 100755 --- a/files/app/main/app.ut +++ b/files/app/main/app.ut @@ -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; diff --git a/files/app/main/status/e/dhcp.ut b/files/app/main/status/e/dhcp.ut index 67599791..de9eb416 100755 --- a/files/app/main/status/e/dhcp.ut +++ b/files/app/main/status/e/dhcp.ut @@ -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(); {% } %} })();