diff options
Diffstat (limited to '_www/zone.d/index.html')
| -rw-r--r-- | _www/zone.d/index.html | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/_www/zone.d/index.html b/_www/zone.d/index.html new file mode 100644 index 0000000..77bfb8c --- /dev/null +++ b/_www/zone.d/index.html @@ -0,0 +1,513 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | zone.d</title> + + <style> + h4 { + border-bottom: 1px solid silver; + } + .nav-left { + padding: 0; + width: 13em; + float: left; + } + .nav-left .item { + margin: 1em 0; + cursor: pointer; + color: rgb(0, 100, 200); + } + .nav-left > input { + width: 12em; + } + .content { + float: left; + width: calc(100% - 14em); + } + .action-delete { + margin-left: 1em; + } + .actions { + padding: 1em; + } + .actions button { + width: 100%; + } + + .rr_form { + margin: 1em 0px; + padding: 10px 10px 0px 10px; + border: 1px solid silver; + } + .rr_form > * > .input > input.name { + width: auto; + } + + .rr { + font-family: monospace; + width: 100%; + padding: 1em 0px; + } + .rr.header { + font-weight: bold; + } + .rr > .name { + width: 20em; + display: inline-block; + word-wrap: break-word; + } + .rr > .type { + width: 4em; + display: inline-block; + } + .rr > .ttl { + width: 6em; + display: inline-block; + } + .rr > .value { + display: inline-block; + word-wrap: break-word; + } + </style> + </head> + <body onload="main()"> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/hosts_blocks/"> Hosts blocks </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/" class="active"> zone.d </a> + </nav> + <div id="notif"></div> + + <div class="nav-left"> + <h3>Zone files</h3> + <div id="ZoneFiles"></div> + + <label for="newZoneFile"> New zone file: </label> + <input id="newZoneFile" /> + <button onclick="createZoneFile()">Create</button> + </div> + + <div class="content"> + <div id="activeZone"></div> + + <div id="activeZone_soa" style="display: none"> + <h4>SOA record</h4> + + <div class="input"> + <label for="soa_mname"> Name server </label> + <input id="soa_mname" oninput="updateSOA('MName', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_mname_info')">?</span> + <div id="soa_mname_info" class="input-info" style="display: none"> + The domain-name of the name server that was the original or primary source + of data for this zone. It should be domain-name where the rescached run. + </div> + </div> + + <div class="input"> + <label for="soa_rname">Admin email</label> + <input id="soa_rname" oninput="updateSOA('RName', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_rname_info')">?</span> + <div id="soa_rname_info" class="input-info" style="display: none"> + Email address of the administrator responsible for this zone. The "@" on + email address is replaced with dot, and if there is a dot before "@" it + should be escaped with "\". For example, "dns.admin@domain.tld" would be + written as "dns\.admin.domain.tld".' + </div> + </div> + + <div class="input"> + <label for="soa_serial">Serial</label> + <input + id="soa_serial" + type="number" + min="0" + oninput="updateSOA('Serial', parseInt(this.value))" + /> + <span class="input-info-toggler" onclick="toggleInfo('soa_serial_info')">?</span> + <div id="soa_serial_info" class="input-info" style="display: none"> + Serial number for this zone. If a secondary name server observes an increase + in this number, the server will assume that the zone has been updated and + initiate a zone transfer. + </div> + </div> + + <div class="input"> + <label for="soa_refresh">Refresh</label> + <input + id="soa_refresh" + type="number" + min="0" + oninput="updateSOA('Refresh', parseInt(this.value))" + /> + <span class="input-info-toggler" onclick="toggleInfo('soa_refresh_info')">?</span> + <div id="soa_refresh_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should query the zone + for the SOA record, to detect zone changes. Recommendation for small and + stable zones is 86400 seconds (24 hours). + </div> + </div> + + <div class="input"> + <label for="soa_retry">Retry</label> + <input + id="soa_retry" + type="number" + min="0" + oninput="updateSOA('Retry', parseInt(this.value))" + /> + <span class="input-info-toggler" onclick="toggleInfo('soa_retry_info')">?</span> + <div id="soa_retry_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should retry to request + the serial number from the zone if the zone does not respond. It must be + less than Refresh. Recommendation for small and stable zones is 7200 seconds + (2 hours). + </div> + </div> + + <div class="input"> + <label for="soa_expire">Expire</label> + <input + id="soa_expire" + type="number" + min="0" + oninput="updateSOA('Expire', parseInt(this.value))" + /> + <span class="input-info-toggler" onclick="toggleInfo('soa_expire_info')">?</span> + <div class="soa_expire_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should stop answering + request for this zone if the zone does not respond. This value must be + bigger than the sum of Refresh and Retry. Recommendation for small and + stable zones is 3600000 seconds (1000 hours). + </div> + </div> + + <div class="input"> + <label for="soa_minimum"> Minimum </label> + <input + id="soa_minimum" + type="number" + min="0" + oninput="updateSOA('Minimum', parseInt(this.value))" + /> + <span class="input-info-toggler" onclick="toggleInfo('soa_expire_info')">?</span> + <div id="soa_minimum_info" class="input-info" style="display: none"> + Time to live for purposes of negative caching. Recommendation for small and + stable zones is 1800 seconds (30 min). + </div> + </div> + + <div class="actions"> + <button onclick="saveSOA()">Save</button> + </div> + </div> + + <div id="activeZone_records" style="display: none"> + <h4>List records</h4> + <div class="rr header"> + <span class="name"> Name </span> + <span class="type"> Type </span> + <span class="value"> Value </span> + </div> + <div id="list_records"></div> + </div> + + <div id="activeZone_form" class="rr_form" style="display: none"> + <div class="input"> + <label for="rr_type"> Type: </label> + <select id="rr_type" oninput="onSelectRRType(this.value)"> + <option value="1">A</option> + <option value="2">NS</option> + <option value="5">CNAME</option> + <option value="12">PTR</option> + <option value="15">MX</option> + <option value="16">TXT</option> + <option value="28">AAAA</option> + </select> + </div> + + <div id="activeZone_form_default"> + <div class="input"> + <label> Name: </label> + <input class="name" oninput="updateNewRR('Name', this.value)" /> + <span></span> + </div> + <div class="input"> + <label> Value: </label> + <input oninput="updateNewRR('Value', this.value)" /> + </div> + </div> + + <div id="activeZone_form_ptr" style="display: none"> + <div class="input"> + <label> Name: </label> + <input oninput="updateNewRR('Name', this.value)" /> + </div> + <div class="input"> + <label> Value: </label> + <input class="name" oninput="updateNewRR('Value', this.value)" /> + <span></span> + </div> + </div> + + <div id="activeZone_form_mx" style="display: none"> + <div class="input"> + <label> Name: </label> + <input class="name" oninput="updateNewRR('Name', this.value)" /> + <span></span> + </div> + <div class="input"> + <label> Preference: </label> + <input + type="number" + min="1" + max="65535" + oninput="updateNewRR('Value.Preference', this.value)" + /> + </div> + <div class="input"> + <label> Exchange: </label> + <input oninput="updateNewRR('Value.Exchange', this.value)" /> + </div> + </div> + + <div class="actions"> + <button class="create" onclick="createRR()">Create</button> + </div> + </div> + </div> + + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null + let activeZone = null + let newRR = { + Name: "", + Value: "", + } + + async function main() { + resc = new Rescached("") + + let res = await resc.getEnvironment() + if (res.code != 200) { + notifError(res.message) + return + } + renderZoneFiles(resc.env.ZoneFiles) + resetActiveZone() + } + + async function createZoneFile() { + let name = document.getElementById("newZoneFile").value + if (name === "") { + notifError("The zone file name must not be empty") + return + } + let res = await resc.ZoneFileCreate(name) + if (res.code != 200) { + notifError(res.message) + return + } + renderZoneFiles(resc.env.ZoneFiles) + } + + async function deleteZoneFile() { + let res = await resc.ZoneFileDelete(activeZone.Name) + if (res.code != 200) { + notifError(res.message) + return + } + renderZoneFiles(resc.env.ZoneFiles) + resetActiveZone() + notifInfo(res.message) + } + + async function createRR() { + let res = await resc.ZoneFileRecordCreate(activeZone.Name, newRR) + if (res.code != 200) { + notifError(res.message) + return + } + activeZone.Records = res.data + renderActiveZoneRecords() + notifInfo(res.message) + } + + async function deleteRR(name, idx) { + let rr = activeZone.Records[name][idx] + console.log("deleteRR: ", rr) + let res = await resc.ZoneFileRecordDelete(activeZone.Name, rr) + if (res.code != 200) { + notifError(res.message) + return + } + activeZone.Records = res.data + renderActiveZoneRecords() + notifInfo(res.message) + } + + function onSelectRRType(v) { + let formDefault = document.getElementById("activeZone_form_default") + let formPTR = document.getElementById("activeZone_form_ptr") + let formMX = document.getElementById("activeZone_form_mx") + + newRR.Type = v + newRR.Value = "" + + if (v == 12) { + // PTR + formDefault.style.display = "none" + formPTR.style.display = "block" + formPTR.children[1].children[2].innerText = "." + activeZone.Name + formMX.style.display = "none" + } else if (v == 15) { + formDefault.style.display = "none" + formPTR.style.display = "none" + formMX.style.display = "block" + formMX.children[0].children[2].innerText = "." + activeZone.Name + newRR.Value = { + Name: "", + Exchange: "", + Preference: 0, + } + } else { + formDefault.style.display = "block" + formDefault.children[0].children[2].innerText = "." + activeZone.Name + formPTR.style.display = "none" + formMX.style.display = "none" + } + } + + function renderZoneFiles(zoneFiles) { + let wrapper = document.getElementById("ZoneFiles") + out = "" + for (let name in zoneFiles) { + if (!zoneFiles.hasOwnProperty(name)) { + continue + } + let zoneFile = zoneFiles[name] + out += ` + <div class="item"> + <span onclick="setActiveZone('${zoneFile.Name}')"> + ${zoneFile.Name} + </span> + </div>` + } + wrapper.innerHTML = out + } + + function renderActiveZone() { + let w = document.getElementById("activeZone") + w.innerHTML = ` + <h3> + ${activeZone.Name} + <button + class="action-delete" + onclick="deleteZoneFile()" + > + Delete + </button> + </h3> + ` + } + + function renderActiveZoneSOA() { + const w = document.getElementById("activeZone_soa") + w.style.display = "block" + const soa = activeZone.SOA.Value + document.getElementById("soa_mname").value = soa.MName + document.getElementById("soa_rname").value = soa.RName + document.getElementById("soa_serial").value = soa.Serial + document.getElementById("soa_refresh").value = soa.Refresh + document.getElementById("soa_retry").value = soa.Retry + document.getElementById("soa_expire").value = soa.Expire + document.getElementById("soa_minimum").value = soa.Minimum + } + + function renderActiveZoneRecords() { + let el = document.getElementById("activeZone_records") + el.style.display = "block" + let w = document.getElementById("list_records") + out = "" + for (const [name, listRR] of Object.entries(activeZone.Records)) { + listRR.forEach((rr, idx) => { + out += ` + <div class="rr"> + <span class="name"> + ${rr.Name} + </span> + <span class="type"> + ${resc.GetRRTypeName(rr.Type)} + </span> + <span class="value"> + ${rr.Value} + </span> + <button onclick="deleteRR('${rr.Name}', ${idx})"> + X + </button> + </div> + ` + }) + } + w.innerHTML = out + } + + function renderActiveZoneForm() { + let form = document.getElementById("activeZone_form") + form.style.display = "block" + document.getElementById("rr_type").value = 1 + onSelectRRType(1) + } + + function resetActiveZone() { + document.getElementById("activeZone").innerHTML = ` + <p>Select one of the zone file to manage.</p> + ` + document.getElementById("activeZone_soa").style.display = "none" + document.getElementById("activeZone_records").style.display = "none" + document.getElementById("activeZone_form").style.display = "none" + activeZone = null + } + + async function saveSOA() { + console.log("saveSOA: ", activeZone.SOA.Value) + let rr = activeZone.SOA + rr.Type = 6 + let res = await resc.ZoneFileRecordCreate(activeZone.Name, rr) + if (res.code != 200) { + notifError(res.message) + return + } + notifInfo(res.message) + } + + function setActiveZone(name) { + activeZone = resc.env.ZoneFiles[name] + console.log("setActiveZone: ", activeZone) + renderActiveZone() + renderActiveZoneSOA() + renderActiveZoneRecords() + renderActiveZoneForm() + } + + function updateNewRR(k, v) { + newRR[k] = v + } + + function updateSOA(k, v) { + activeZone.SOA.Value[k] = v + } + </script> + </body> +</html> |
