Nathanwoodburn.github.io/dnsedit.html
2022-07-18 22:07:22 +10:00

2896 lines
74 KiB
HTML

<!--- (c) Copyright 2019-2021, James Stevens ... see LICENSE for details --->
<!--- Alternative license arrangements are possible, contact me for more information --->
<html>
<head>
<title>PowerDNS WebUI - Login</title>
</head>
<script language="Javascript">
const debugAPI = false;
</script>
<style type='text/css'>
.fullvis { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: visible; opacity: 1; text-align: center; }
.fadein { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: visible; opacity: 1; transition: opacity 0.5s linear; }
.fadeout { font-family:Verdana,Arial,Helvetica,sans-serif; font-size:14pt; visibility: hidden; opacity: 0; transition: visibility 0s 1s, opacity 1s linear; }
#topSpan{
background-color: #666688;
height: 55px;
line-height: 55px;
margin-top: 0px;
padding-left: 0px;
padding-top: 1px;
position: -webkit-sticky;
position: sticky;
top: 0px;
}
html {
color: white;
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
background-color:#666688;
scrollbar-base-color:#666699;
scrollbar-arrow-color:#ddddff;
}
th {
color: black;
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
background-color:#a0a0cc;
font-weight: bold;
padding-left: 6px;
padding-right: 6px;
}
select {
color: black;
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 8pt;
}
input {
color: black;
background-color: white;
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 8pt;
}
.inputBad {
color: black;
background-color: #ffd0d0;
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 8pt;
}
body {
color: white;
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
margin-top: 0px;
background-color:#666688;
scrollbar-base-color:#666699;
scrollbar-arrow-color:#ddddff;
}
a {
color: #b0b0ff;
text-decoration:none;
}
a:hover {
text-decoration: underline;
}
form {
margin:0px;
}
.myBtn {
text-align: center;
display: inline-block;
background-color: #d0d0ff;
color: black;
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 10pt;
font-weight: bold;
border-radius: 25px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
margin-top: 0px;
margin-left: 10px;
margin-bottom: 2px;
margin-right: 2px;
}
.myBtn:hover {
box-shadow: 3px 3px #222244;
text-shadow: 1px 1px #ffffff;
}
.myBtn:active {
position: relative;
box-shadow: none;
text-shadow: none;
top: 1px;
left: 1px;
}
.dnskey {
color: #383855;
}
td {
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
vertical-align: top;
padding-left: 7px;
padding-right: 7px;
white-space: nowrap;
}
.dataCell {
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
vertical-align: top;
padding-left: 7px;
padding-right: 7px;
white-space: normal;
min-width: 50%;
overflow-wrap: anywhere;
}
.dataRow {
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size: 10pt;
cursor: pointer;
color: white;
background-color: #666688;
}
.dataRow:hover {
background-color: #444466;
}
.formPrompt {
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:10pt;
color: white;
text-align: right;
}
.msgPop {
position: absolute;
left: 10px;
top: 20px;
font-family:Verdana,Arial,Helvetica,sans-serif;
font-size:16pt;
width: 95%;
background: #ffd0d0;
text-align: center;
color: black;
padding: 5px;
border-radius: 10px;
}
.msgPopNo {
visibility: hidden;
opacity: 0; transition: visibility 0s 1s, opacity 1s linear;
}
.msgPopYes {
visibility: visible;
opacity: 1; transition: opacity 0.5s linear;
}
</style>
<script language="Javascript">
console.clear();
var gbl = {
server: null,
apikey: null,
with_https: true,
fast_zone_list: false,
zone_list: "zones",
default_ttl: 86400,
state: { state: "login" },
tick: "&#x2611;",
cross: "&#x2612;",
timer: "&#x23f3;",
bin: "&#x2702;",
warn: "&#x26a0;",
page: "&#x1F5CE;",
nsec: "&#x1F512;",
nsec3: "&#x1F510;",
clip: "&#x1F4CE;",
};
var ctx = { }; // will store the context of what the user is up to
//=============================================================================================
// Some system constants
//=============================================================================================
// add a trailing '.' for these record types, if the user forgets
const autoAddDot = { rrMX: true, rrNS: true, rrCNAME: true, };
const pdnsCookies = { "pdns_fast_zone_list":1, "pdns_with_https":1,"pdns_server":1 };
// regex for an FQDN hostname
const fqdnCheck = /^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])\.)*(xn--|)[A-Za-z0-9\-]+[.]$/;
// regex for adding a host name
const hostnameCheck = /^([a-zA-Z0-9_*]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])(\.([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_]))*$/
// regex's for RR validation - just add more here and they will work
const validations = {
rrAAAA: /^(([0-9A-F]{1,4}:){7,7}[0-9A-F]{1,4}|([0-9A-F]{1,4}:){1,7}:|([0-9A-F]{1,4}:){1,6}:[0-9A-F]{1,4}|([0-9A-F]{1,4}:){1,5}(:[0-9A-F]{1,4}){1,2}|([0-9A-F]{1,4}:){1,4}(:[0-9A-F]{1,4}){1,3}|([0-9A-F]{1,4}:){1,3}(:[0-9A-F]{1,4}){1,4}|([0-9A-F]{1,4}:){1,2}(:[0-9A-F]{1,4}){1,5}|[0-9A-F]{1,4}:((:[0-9A-F]{1,4}){1,6})|:((:[0-9A-F]{1,4}){1,7}|:)|fe80:(:[0-9A-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9A-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/i,
rrA: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
rrMX: /^[1-9][0-9]{0,5} (([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\-_]*[a-zA-Z0-9_])\.)+([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\-_]*[A-Za-z0-9_])[.]$/,
rrNS: fqdnCheck,
rrCNAME: fqdnCheck,
rrCATALOG: fqdnCheck,
rrDS: /^(([123456]?[0-9]{1,4}|[0-9]1,4) ([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]) [1234] [0-9A-F ]{40,100})$/i,
};
// validation regexp for metadata fields
const trueOrFalse = /^[01]$/;
const validateMeta = {
"API-RECTIFY": trueOrFalse,
"IXFR": trueOrFalse,
"NSEC3NARROW": trueOrFalse,
"PRESIGNED": trueOrFalse,
"PUBLISH-CDNSKEY": trueOrFalse,
"SLAVE-RENOTIFY": trueOrFalse,
"SOA-EDIT": /^(INCREMENT-WEEKS|INCEPTION-EPOCH|INCEPTION-INCREMENT|INCEPTION)$/i,
"SOA-EDIT-API": /^(DEFAULT|INCREASE|EPOCH|SOA-EDIT|SOA-EDIT-INCREASE)$/i,
"SOA-EDIT-DNSUPDATE": /^(DEFAULT|INCREASE|EPOCH|SOA-EDIT|SOA-EDIT-INCREASE)$/i,
};
// Descriptions for "DS" record algorythm numbers from IANA
const dsAlg = [ "Reserved","SHA1","SHA256","GOST","SHA384"];
// drop downs for metadata items
const metaPickLists = {
"PUBLISH-CDNSKEY" : { "Enabled":1, "Disabled":0 },
"API-RECTIFY" : { "Enabled":1, "Disabled":0 },
"PRESIGNED" : { "Enabled":1, "Disabled":0 },
"NSEC3NARROW" : { "Enabled":1, "Disabled":0 },
"IXFR" : { "Enabled":1, "Disabled":0 },
"eSLAVE-RENOTIFY" : { "Enabled":1, "Disabled":0 },
"SOA-EDIT" : [ "INCREMENT-WEEKS","INCEPTION-EPOCH","INCEPTION-INCREMENT","INCEPTION" ],
"SOA-EDIT-API": [ "DEFAULT","INCREASE","EPOCH","SOA-EDIT","SOA-EDIT-INCREASE" ],
"SOA-EDIT-DNSUPDATE": [ "DEFAULT","INCREASE","EPOCH","SOA-EDIT","SOA-EDIT-INCREASE" ],
};
// these appear as metadata items but are handled as zone properties - lol
const blockedMeta = {
"SOA-EDIT": { name: "soa_edit", list: false, delete: false },
"SOA-EDIT-API": { name: "soa_edit_api", list: false, delete: true },
"NSEC3PARAM": { name: "nsec3param", list: false, delete: true },
"TSIG-ALLOW-AXFR": { name: "master_tsig_key_ids", list: true, delete: true },
"AXFR-MASTER-TSIG": { name: "slave_tsig_key_ids", list: true, delete: true },
};
// All Meta Data Items
const allMetaItems = [
'ALLOW-AXFR-FROM',
'ALLOW-DNSUPDATE-FROM',
'ALSO-NOTIFY',
'AXFR-MASTER-TSIG',
'AXFR-SOURCE',
'FORWARD-DNSUPDATE',
'GSS-ACCEPTOR-PRINCIPAL',
'GSS-ALLOW-AXFR-PRINCIPAL',
'IXFR',
'LUA-AXFR-SCRIPT',
'NOTIFY-DNSUPDATE',
'NSEC3NARROW',
'NSEC3PARAM',
'PRESIGNED',
'PUBLISH-CDNSKEY',
'PUBLISH-CDS',
'SOA-EDIT',
'SOA-EDIT-API',
'SOA-EDIT-DNSUPDATE',
'TSIG-ALLOW-AXFR',
'TSIG-ALLOW-DNSUPDATE',
];
//=============================================================================================
// Some library functions
//=============================================================================================
function callApi(sfx,callback,inData)
{
document.body.style.cursor="progress";
if (debugAPI) {
console.log("API>>>",sfx);
console.log("API>>>",inData);
}
gbl.forms.forEach(i => i.style.display = "none");
if ((inData != null)&&(inData.method == "PUT"))
msg(`Requesting ... ${gbl.timer}`);
else
msg(`Loading ... ${gbl.timer}`);
let p = "https";
if (!gbl.with_https) p = "http";
let url = `${p}://${gbl.server}/api/v1/servers/localhost/${sfx}`;
let okResp = 200;
let httpCmd = {
headers: { 'X-API-Key': gbl.apikey },
method: 'GET',
};
if (inData != null) {
httpCmd.method = inData.method;
httpCmd.body = inData.json;
if ("okResp" in inData) okResp = inData.okResp;
}
fetch(url,httpCmd).then(response => {
if (debugAPI) console.log("API-Resp>>>",response);
if (response.status != okResp) {
response.text().then(
data => {
try {
e = JSON.parse(data);
errMsg(e.error);
}
catch {
errMsg(`ERROR: ${response.status} ${response.statusText}`)
}
},
() => errMsg(`ERROR: ${response.status} ${response.statusText}`)
);
msg("");
if ((inData != null)&&("callErr" in inData)) return callback(null);
return;
}
response.text().then(data => {
if ((inData != null)&&(inData.noData)) {
msg("");
callback(true);
} else {
let param = data;
try {
param = JSON.parse(data); }
catch {
param = data; }
msg("");
callback(param);
}
});
})
.catch(err => errMsg(`ERROR: Failed to connect to PowerDNS`))
document.body.style.cursor="auto";
}
// This one function is danallison/downloadString.js
// https://gist.github.com/danallison/3ec9d5314788b337b682
function downloadFile(text, fileType, fileName) {
var blob = new Blob([text], { type: fileType });
var a = document.createElement('a');
a.download = fileName;
a.href = URL.createObjectURL(blob);
a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
a.style.display = "none";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
msg("");
}
// end of -> danallison/downloadString.js
function setState(title,state)
{
gbl.state = state;
if (!("param" in gbl)) window.history.pushState(state,title,gbl.pathname);
document.title = title;
delete gbl.param;
}
function nameToCatalog(name) {
/**
* Secure Hash Algorithm (SHA1)
* http://www.webtoolkit.info/
**/
function SHA1(in_msg,msg_len) {
function rotate_left(n,s) {
var t4 = ( n<<s ) | (n>>>(32-s));
return t4;
};
function cvt_hex(val) {
var str='';
var i;
var v;
for( i=7; i>=0; i-- ) {
v = (val>>>(i*4))&0x0f;
str += v.toString(16);
}
return str;
};
var blockstart;
var i, j;
var W = new Array(80);
var H0 = 0x67452301;
var H1 = 0xEFCDAB89;
var H2 = 0x98BADCFE;
var H3 = 0x10325476;
var H4 = 0xC3D2E1F0;
var A, B, C, D, E;
var temp;
let msg = new Uint8Array(in_msg);
var word_array = new Array();
for( i=0; i<msg_len-3; i+=4 ) {
j = msg[i]<<24 | msg[i+1]<<16 | msg[i+2]<<8 | msg[i+3];
word_array.push( j );
}
switch( msg_len % 4 ) {
case 0:
i = 0x080000000;
break;
case 1:
i = msg[msg_len-1]<<24 | 0x0800000;
break;
case 2:
i = msg[msg_len-2]<<24 | msg[msg_len-1]<<16 | 0x08000;
break;
case 3:
i = msg[msg_len-3]<<24 | msg[msg_len-2]<<16 | msg[msg_len-1]<<8 | 0x80;
break;
}
word_array.push( i );
while( (word_array.length % 16) != 14 ) word_array.push( 0 );
word_array.push( msg_len>>>29 );
word_array.push( (msg_len<<3)&0x0ffffffff );
for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ) {
for( i=0; i<16; i++ ) W[i] = word_array[blockstart+i];
for( i=16; i<=79; i++ ) W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
A = H0;
B = H1;
C = H2;
D = H3;
E = H4;
for( i= 0; i<=19; i++ ) {
temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
E = D;
D = C;
C = rotate_left(B,30);
B = A;
A = temp;
}
for( i=20; i<=39; i++ ) {
temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
E = D;
D = C;
C = rotate_left(B,30);
B = A;
A = temp;
}
for( i=40; i<=59; i++ ) {
temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
E = D;
D = C;
C = rotate_left(B,30);
B = A;
A = temp;
}
for( i=60; i<=79; i++ ) {
temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
E = D;
D = C;
C = rotate_left(B,30);
B = A;
A = temp;
}
H0 = (H0 + A) & 0x0ffffffff;
H1 = (H1 + B) & 0x0ffffffff;
H2 = (H2 + C) & 0x0ffffffff;
H3 = (H3 + D) & 0x0ffffffff;
H4 = (H4 + E) & 0x0ffffffff;
}
var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
return temp.toLowerCase();
}
let buffer = new ArrayBuffer(name.length+2);
let msg = new Uint8Array(buffer);
let last_len = 99;
let idx = 0;
for(let frag of name.split(".")) {
msg[idx++] = frag.length;
for(let i=0;i<frag.length;i++) msg[idx++] = frag[i].toLowerCase().charCodeAt(0);
last_len = frag.length
}
if (last_len > 0) msg[idx++] = 0;
return SHA1(msg,idx);
}
//=============================================================================================
// Stats handling
//=============================================================================================
function loadStats()
{
callApi("statistics",showStats);
}
function showOneStats(data)
{
let item = null;
for(let i of data) if (i.name==this) item = i;
if (item == null) {
errMsg(`Could not find stats item '${this}'`);
return loadStats();
}
ctx.stats = data;
setState(`${gbl.server}: Stats: ${item.name}`, { state: "stats", server: gbl.server, stats: item.name });
let x = "<table width=100% border=0 cellspacing=1 cellpadding=0><tr>";
for (let i in item.value[0]) x += `<th>${i}</th>`;
x += "</tr>";
for(r in item.value) {
x += "<tr class=dataRow>";
for(i in item.value[r])
x += `<td>${item.value[r][i]}</td>`;
x += "</tr>";
}
x += "</table>";
gbl.bot.innerHTML = x;
topLine();
}
function oneStat(item)
{
callApi('statistics',showOneStats.bind(item));
}
function showStats(data)
{
data.sort((a,b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
ctx.stats = data;
setState(`${gbl.server}: Stats`, { state: "stats", server: gbl.server });
let x = "<table width=100% border=0 cellspacing=1 cellpadding=0>";
data.forEach(i => {
let clk=`onClick="oneStat('${i.name}');"`;
if (typeof(i.value) != "object") clk="";
x += `<tr ${clk} class=dataRow><td>${i.name}</td><td>${i.type}</td>`;
if (typeof(i.value) != "object") x += `<td>${i.value}</td></tr>`;
else {
if (i.value.length > 0) x += "<td>[Click to view]</td>";
else x += "<td></td>";
}
});
x += "</table>";
gbl.bot.innerHTML = x;
topLine()
}
//=============================================================================================
// Zone META data handling
//=============================================================================================
function doEditMeta(isDel)
{
let f = document.newMetaForm;
let dataIdx = parseInt(f.dataIdx.value);
let itemIdx = parseInt(f.itemIdx.value);
let metaValue = document.newMetaForm.metaValue.value;
let i = ctx.zoneMeta[dataIdx];
if ((i.kind in validateMeta)&&(!(validateMeta[i.kind].test(metaValue)))) {
errMsg(`Invalid value for ${i.kind}`);
document.newMetaForm.metaValue.focus();
document.newMetaForm.metaValue.select();
return false;
}
if (isDel) {
metaValue = "";
i.metadata.splice(itemIdx,1);
}
else {
i.metadata[itemIdx] = metaValue;
i.metadata.sort((a,b) => {
if (a < b) return -1;
if (b < a) return 1;
return 0;
});
}
if (i.kind in blockedMeta) {
let json = {}
json[blockedMeta[i.kind].name] = metaValue;
if (blockedMeta[i.kind].list) json[blockedMeta[i.kind].name] = i.metadata;
callApi(`zones/${ctx.zone}`, zoneMeta, { method: "PUT", okResp: 204, json: JSON.stringify(json) } );
}
else {
let hdr = { method: "PUT", okResp: 200, json: JSON.stringify({ metadata: i.metadata }) };
callApi(`zones/${ctx.zone}/metadata/${i.kind}`, zoneMeta, hdr);
}
return false; // prevent form submit
}
function editMetaItem(dataIdx,itemIdx)
{
let metaItem = ctx.zoneMeta[dataIdx].kind;
let metaValue = ctx.zoneMeta[dataIdx].metadata[itemIdx];
clickAddMeta(metaItem.toUpperCase(),metaValue,dataIdx,itemIdx);
}
function formAddMeta()
{
let f = document.newMetaForm;
let metaItem = f.metaItem.value;
let metaValue = [ f.metaValue.value ];
let hdr = { method: "POST", okResp: 201, callErr: true };
let url = `zones/${ctx.zone}/metadata`;
for(let i of ctx.zoneMeta)
if (i.kind.toUpperCase() == metaItem) {
if (i.metadata.includes(f.metaValue.value)) return zoneMeta();
metaValue = i.metadata;
metaValue.push(f.metaValue.value);
hdr.method = "PUT";
hdr.okResp = 200;
url = `${url}/${metaItem}`;
break;
}
let nxtFn = function(res) { if (res == null) clickAddMeta(metaItem.toUpperCase(),metaValue); else zoneMeta(); };
hdr.json = JSON.stringify( { type: "Metadata", kind:metaItem, metadata: metaValue } );
if (!(metaItem in blockedMeta)) callApi(url, nxtFn, hdr);
else {
let json = {}
json[blockedMeta[metaItem].name] = f.metaValue.value;
if (blockedMeta[metaItem].list) json[blockedMeta[metaItem].name] = metaValue;
callApi(`zones/${ctx.zone}`, nxtFn,
{ method: "PUT", callErr: true, okResp: 204, json: JSON.stringify(json) } );
}
return false;
}
function showDelete(item,val) {
if (!(item in blockedMeta)) return (val != null);
if (blockedMeta[item].delete) return (val != null);
return false;
}
function clickAddMeta(item,val,dataIdx,itemIdx)
{
var formCall = "formAddMeta();"
if (dataIdx != null) formCall = "doEditMeta(false);"
x = `<form method=post action="/none.cgi" name=newMetaForm onSubmit="return ${formCall}">`
if (dataIdx != null) x += `<input type=hidden name=dataIdx value=${dataIdx}>`
if (itemIdx != null) x += `<input type=hidden name=itemIdx value=${itemIdx}>`
x += '<table width=40% align=center>'
x += '<colgroup><col width=35%/><col width=65%/></colgroup>';
let addBtn = "Change";
if (val == null) {
addBtn = "Add Metadata";
x += "<tr><td colspan=2 align=center><h2>Add a New Metadata Item</h2></td></tr>"
+ "<tr><td class=formPrompt>Metadata: </td><td><select onChange='clickAddMeta(this.value.toUpperCase());' name=metaItem>";
allMetaItems.forEach(i => { c=""; if (i==item) c="selected"; x += `<option ${c}>${i}` });
x += '</select></td></tr>';
}
else {
x += `<tr><td colspan=2 align=center><h2>Edit a Metadata Item</h2></td></tr>`
+ `<tr><td class=formPrompt>Metadata Item: </td><td>${item}</td></tr>`
+ `<input type=hidden name=metaItem value="${item}">`;
}
x += '<tr><Td class=formPrompt>Value: </td><td>';
if (item in metaPickLists) {
x += "<select name=metaValue>";
if (Array.isArray(metaPickLists[item]))
metaPickLists[item].forEach(i => x += `<option>${i}`);
else
for(let i in metaPickLists[item]) x += `<option value=${metaPickLists[item][i]}>${i}`;
x += "</select>";
}
else
x += `<input size=50 name=metaValue></td></tr>`;
x += '<tr><td colspan=2><hr></td></tr>'
+ '<tr><td align=center colspan=2>'
+ '<span class=myBtn title="Cancel adding Metadata" onClick="zoneMeta();">Cancel</span>'
if (showDelete(item,val))
x += '<span title="Delete this Metadata Item" class=myBtn onClick="doEditMeta(true);">Delete</span>'
x += `<span title="Add this Metadata Record" class=myBtn onClick="${formCall}">${addBtn}</span></td></tr>`
+ '<input type=submit style="display: none;">'
+ '</table></form>';
gbl.bot.innerHTML = x;
if (val != null) document.newMetaForm.metaValue.value = val;
else {
if (item in metaPickLists)
document.newMetaForm.metaValue.selectedIndex = 0;
else
document.newMetaForm.metaValue.value = "";
}
document.newMetaForm.metaValue.focus();
}
function zoneMeta()
{
ctx.meta = true; topLine();
callApi(`zones/${ctx.zone}/metadata`,zoneMetaDetails);
return false;
}
function zoneMetaDetails(data)
{
data.sort((a,b) => {
if (a.kind < b.kind) return -1;
if (a.kind > b.kind) return 1;
return 0;
});
for(let i of data)
i.metadata.sort((a,b) => {
if (a < b) return -1;
if (b < a) return 1;
return 0;
});
setState(`${gbl.server}: ${ctx.zone}, META Data`,
{ state: "meta", server: gbl.server, zone: ctx.zone, meta:true });
ctx.zoneMeta = data;
let x = "<table align=center width=60% border=0 cellspacing=1 cellpadding=0>";
for(let i in data) {
for(let y in data[i].metadata) {
let td = `<td onClick="editMetaItem(${i},${y});">`;
x += `<tr class=dataRow>${td}${data[i].kind.toUpperCase()}</td>`;
x += `${td}${data[i].metadata[y]}</td></tr>`;
}
}
x += "</table>";
gbl.bot.innerHTML = x;
topLine();
}
//=============================================================================================
// Zone Details Handling
//=============================================================================================
function addNewRR()
{
ctx.rec.rrs.records.push({ content:"", disabled: false });
drawOneRecord();
clickData(ctx.rec.rrs.records.length-1);
}
function madeChange(res)
{
if (res)
loadThisZone();
else
errMsg("Update Failed");
}
function patchRRdata()
{
return JSON.stringify({
rrsets: [
{
name: ctx.rec.name,
ttl: ctx.rec.rrs.ttl,
type: ctx.rec.type,
changetype: "REPLACE",
records : ctx.rec.rrs.records,
}
]
});
}
function saveRRChanges(withSave)
{
delete ctx.edit;
topLine();
if (withSave)
callApi(`zones/${ctx.zone}`,madeChange,
{
method: "PATCH",
noData: true,
okResp: 204,
json: patchRRdata()
}
);
else
getZoneRecord();
}
function changeRR()
{
let newVal = document.rrForm.new.value;
if (newVal == "") {
if (ctx.rec.rrs.records[ctx.edit.dta].content != "") ctx.needSave = "saveRRChanges";
ctx.rec.rrs.records.splice(ctx.edit.dta,1);
drawOneRecord();
}
else {
if ((ctx.rec.type == "TXT")&&(newVal.substr(0,1)!='"')&&(newVal.substr(newVal.length-1)!='"'))
newVal = `"${newVal}"`;
s = document.getElementById(`rrs${ctx.edit.dta}`);
if (newVal != ctx.rec.rrs.records[ctx.edit.dta].content) {
idx = `rr${ctx.rec.type}`;
if ((idx in autoAddDot)&&(newVal.substr(newVal.length-1)!=".")) newVal += ".";
if (idx in validations) {
if (!validations[idx].test(newVal)) {
errMsg("Invalid value");
document.rrForm.new.focus();
document.rrForm.new.select();
return false;
}
}
ctx.rec.rrs.records[ctx.edit.dta].content = newVal;
s.innerHTML = newVal;
ctx.needSave = "saveRRChanges";
}
else
s.innerHTML = ctx.rec.rrs.records[ctx.edit.dta].content;
}
delete ctx.edit;
topLine();
return false; // cancel the form submit
}
function changeTTL()
{
let newVal = document.rrForm.new.value;
if (!validInt(newVal)) {
errMsg("Invalid TTL");
document.rrForm.new.focus();
document.rrForm.new.select();
return false;
}
if (newVal != ctx.rec.rrs.ttl) {
ctx.needSave = "saveRRChanges";
ctx.rec.rrs.ttl = newVal;
drawOneRecord();
}
delete ctx.edit;
topLine();
return false; // cancel the form submit
}
function clickTTL(idx)
{
if ("edit" in ctx) { if (("ttl" in ctx.edit)&&(ctx.edit.ttl == idx)) return; else cancelAllEdits(); }
let s = document.getElementById(`ttl${idx}`);
if (s.innerHTML.indexOf("<form") >= 0) return;
ctx.edit = { ttl: idx, };
let x = "<form onSubmit='return changeTTL();' name=rrForm>"
+ `<input onkeydown="keyEsc(event);" size=10 name=new value="${ctx.rec.rrs.ttl}"></form>`;
s.innerHTML = x;
document.rrForm.new.focus();
topLine();
}
function clickData(idx)
{
if ("edit" in ctx) { if (("dta" in ctx.edit)&&(ctx.edit.dta == idx)) return; else cancelAllEdits(); }
let s = document.getElementById(`rrs${idx}`);
if (s.innerHTML.indexOf("<form") >= 0) return;
let len = 50
if ((ctx.rec.type=="SOA")||(ctx.rec.type=="TXT")) len=100;
let x = "<form onSubmit='return changeRR();' name=rrForm>"
+ `<input onkeydown="keyEsc(event);" size=${len} name=new`
+ ` value='${ctx.rec.rrs.records[idx].content.replace(/'/g,"&apos;")}'></form>`;
ctx.edit = { dta: idx };
topLine();
s.innerHTML = x;
document.rrForm.new.focus();
}
function findZoneName(name)
{
let ret = "";
for(let n of ctx.zoneList) {
if (
(name.length >= n.name.length) &&
(n.name.length > ret.length) &&
((name==n.name)||(name.substr(name.length - n.name.length)==n.name))
)
ret = n.name;
}
return ret;
}
function changeName()
{
let z = null;
let name = document.rrForm.new.value;
if (name.substr(name.length-1)!=".") {
let t = findZoneName(`${name}.`);
if (t != null) {
z = t;
name += ".";
}
else {
name = `${name}.${ctx.zone}`;
z = ctx.zone;
}
}
else
z = findZoneName(name);
if (z != "") ctx.zone = z;
else {
errMsg("No matching zone");
document.rrForm.new.focus();
return false;
}
if (ctx.rec.name != name) {
ctx.rec.name = name;
ctx.rec.rrs.name = name;
ctx.needSave = "saveRRChanges";
}
delete ctx.edit;
topLine();
drawOneRecord();
return false; // block form submit
}
function clickRecName(idx)
{
if ("edit" in ctx) { if (("nme" in ctx.edit)&&(ctx.edit.nme == idx)) return; else cancelAllEdits(); }
let x = "<form onSubmit='return changeName();' name=rrForm>"
+ `<input onkeydown="keyEsc(event);" size=50 name=new value='${ctx.rec.name}'></form>`;
ctx.edit = { nme: idx, };
let s = document.getElementById(`nme${idx}`);
topLine();
s.innerHTML = x;
document.rrForm.new.focus();
}
function cancelAllEdits()
{
if (!("edit" in ctx)) return;
if ("nme" in ctx.edit) {
let s = document.getElementById(`nme${ctx.edit.nme}`);
s.innerHTML = ctx.rec.name;
}
if ("ttl" in ctx.edit) {
let s = document.getElementById(`ttl${ctx.edit.ttl}`);
s.innerHTML = ctx.rec.rrs.ttl;
}
if ("dta" in ctx.edit) {
if (ctx.rec.rrs.records[ctx.edit.dta].content == "") {
ctx.rec.rrs.records.pop();
if ((ctx.rec.loadedRows==0)&&(ctx.rec.rrs.records.length==0))
return loadThisZone();
drawOneRecord();
}
else {
let s = document.getElementById(`rrs${ctx.edit.dta}`);
s.innerHTML = ctx.rec.rrs.records[ctx.edit.dta].content;
}
}
delete ctx.edit;
topLine();
}
function deleteRRitem(idx)
{
if ("edit" in ctx) return cancelAllEdits();
ctx.rec.rrs.records.splice(idx,1);
ctx.needSave = "saveRRChanges";
topLine();
drawOneRecord();
}
function findRec(rrsets,rec)
{
for(let tag of rrsets)
if ((tag.name == rec.name)&&(tag.type == rec.type)) return tag;
return null;
}
function oneRecord(data)
{
if (data.name != ctx.zone) {
return errMsg("ERROR: Zone does not match loaded data");
}
let rrs = findRec(data.rrsets,ctx.rec);
if (rrs == null) {
if ("rrs" in ctx.rec) {
while ((ctx.rec.rrs.records.length > 0)&&(ctx.rec.rrs.records[ctx.rec.rrs.records.length-1].content == ""))
ctx.rec.rrs.records.pop();
}
return errMsg(`No matching data for ${ctx.rec.name}/${ctx.rec.type}`);
}
rrs.records.sort((a,b) => {
if (a.content < b.content) return -1;
if (a.content > b.content) return 1;
return 0;
})
ctx.rec.rrs = rrs;
ctx.rec.loadedRows = rrs.records.length;
drawOneRecord();
}
function drawOneRecord(rrs)
{
delete ctx.edit;
let x = `${gbl.server}: ${ctx.zone}: `;
if ("showName" in ctx.rec)
x += ctx.rec.showName;
else
x += ctx.rec.name;
setState(`${x}/${ctx.rec.type}`, { state: "record", server: gbl.server, zone: ctx.zone, name:ctx.rec.name, type: ctx.rec.type });
x = "<table width=100% border=0 cellspacing=1 cellpadding=0>";
x += '<colgroup><col width="10%" /><col width="25%" /><col width="99px" /><col /><col width="75%" /></colgroup>'
for(let idx in ctx.rec.rrs.records) {
let i = ctx.rec.rrs.records[idx];
x += "<tr class=dataRow>"
+ `<td onClick='deleteRRitem(${idx});' align=center>${gbl.bin}</td>`
+ `<td onClick='clickRecName(${idx});'><span id=nme${idx}>${ctx.rec.name}</span></td>`
+ `<td align=right onClick='clickTTL(${idx});'><span id=ttl${idx}>${ctx.rec.rrs.ttl}</span></td>`
+ `<td>${ctx.rec.rrs.type}</td>`
+ `<td class=dataCell onClick='clickData(${idx});'><span id=rrs${idx}>${i.content}</span></td></tr>`;
}
gbl.bot.innerHTML = `${x}</table>`;
topLine();
}
function clickZoneRecord(name,type)
{
if (ctx.zoneDetails.kind == "Slave") {
popMsg("Slave zone, editing is disabled");
delete gbl.param; return; }
ctx.rec = {
name: name,
type: type,
};
getZoneRecord();
}
function getZoneRecord()
{
delete ctx.edit;
delete ctx.needSave;
topLine();
callApi(`zones/${ctx.zone}`,oneRecord);
}
//=============================================================================================
// DNSSEC Stuff
//=============================================================================================
function showHideBits(formName)
{
let f = document.forms[formName];
let k = document.getElementById("key2Lens");
let c = document.getElementById("key1Lens");
let s = "table-row-group";
if ((f.keyAlg.value=="ecdsa256")||(f.keyAlg.value=="ecdsa384")) {
k.style.display = "none";
c.style.display = "none";
}
else {
if (formName == "addKeyForm") c.style.display = "table-row-group";
else {
if (f.keyPolicy.value == "CSK") {
c.style.display = "table-row-group";
k.style.display = "none";
}
else {
k.style.display = "table-row-group";
c.style.display = "none";
}
}
}
}
function addAlg(formName)
{
let keyAlg = [
"ecdsa256","ecdsa384","rsasha1","rsasha1-nsec3-sha1","rsasha256","rsasha512","dh","dsa",
"dsa-nsec3-sha1","ecc-gost","ed25519","ed448","gost","indirect","rsamd5"
]
let keyLens = ["1024","2048","4096"];
let x = `<tr><Td class=formPrompt>Key Algrythm :</td><td><select onChange='showHideBits("${formName}");' name=keyAlg>`;
keyAlg.forEach(i => { x += `<option value=${i}>${i.toUpperCase()}` } );
x += "</select></td></tr>";
let txt = "CSK";
if (formName=="addKeyForm") txt = "Key";
x += "<tbody style='display:none;' id='key1Lens'>"
+ `<tr><Td class=formPrompt>${txt} Length (bits) :</td><td><select name=cskBits>`;
keyLens.forEach(i => { x += `<option>${i}`; });
x += "</select></td></tr></tbody>";
x += "<tbody style='display:none;' id='key2Lens'>"
x += "<tr><Td class=formPrompt>KSK Length (bits) :</td><td><select name=kskBits>";
keyLens.forEach(i => { x += `<option>${i}`; });
x += "</select></td></tr>";
x += "<tr><Td class=formPrompt>ZSK Length (bits) :</td><td><select name=zskBits>";
keyLens.forEach(i => { x += `<option>${i}`; });
x += "</select></td></tr></tbody>";
return x;
}
function setNSEC3(delnsec3)
{
let nsec3 = "1 0 5 ";
let nxtFn = function() {
gbl.params = { state: "keys", server: gbl.server, zone: ctx.zone, keys:true };
gbl.state = gbl.params;
loadThisZone();
}
if (delnsec3 == null) {
let old = "0000000000";
let sfx="";
if (ctx.zoneDetails.nsec3param != "") {
i = ctx.zoneDetails.nsec3param.split(" ");
nsec3 = `${i[0]} ${i[1]} ${i[2]}`
old = i[3];
}
while(sfx.length < old.length)
sfx = sfx + Math.floor(Math.random() * 256).toString(16)
nsec3 = `${nsec3} ${sfx}`;
nxtFn = function() {
ctx.zoneDetails.nsec3param = nsec3;
callApi(`zones/${ctx.zone}/rectify`, zoneKeys, { method: "PUT" });
}
}
else nsec3 = "";
callApi(`zones/${ctx.zone}`, nxtFn,
{ method: "PUT", okResp: 204, json: JSON.stringify({ nsec3param: nsec3 }) } );
}
function doSignZone()
{
let nextFn = zoneKeys;
let keyAlg = document.signZoneForm.keyAlg.value;
let kskBits = parseInt(document.signZoneForm.kskBits.value);
let zskBits = parseInt(document.signZoneForm.zskBits.value);
let cskBits = parseInt(document.signZoneForm.cskBits.value);
let keyPolicy = document.signZoneForm.keyPolicy.value;
let keyDNSSEC = document.signZoneForm.keyDNSSEC.value;
if (keyAlg == "ecdsa256") {
kskBits = 256;
zskBits = 256;
cskBits = 256;
}
else if (keyAlg == "ecdsa384") {
kskBits = 384;
zskBits = 384;
cskBits = 384;
}
if (keyDNSSEC == "NSEC3") { nextFn = function() { setNSEC3(null); }; };
if (keyPolicy == "CSK") {
let json = { "keytype": "csk", "active": true, "algorithm": keyAlg, "bits": cskBits, };
callApi(`zones/${ctx.zone}/cryptokeys`,nextFn,
{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}
else {
let json_ksk = { "keytype": "ksk", "active": true, "algorithm": keyAlg, "bits": kskBits, }
callApi(`zones/${ctx.zone}/cryptokeys`, () => {
let json_zsk = { "keytype": "zsk", "active": true, "algorithm": keyAlg, "bits": zskBits, }
callApi(`zones/${ctx.zone}/cryptokeys`,nextFn,
{ method: "POST", okResp: 201, json: JSON.stringify(json_zsk) } );
},
{ method: "POST", okResp: 201, json: JSON.stringify(json_ksk) } );
}
return false; // prevent submit
}
function signZone()
{
let x = "<form onSubmit='return doSignZone();' name=signZoneForm><table align=center>";
x += "<tr><td><div style='height: 15px;'></div></td></tr>"
+ `<tr><th colspan=2>DNSSEC Sign: ${ctx.zone}</th></tr>`;
x += `<tr><td class=formPrompt>Key Policy: </td><td><select onChange="showHideBits('signZoneForm');" name=keyPolicy>`;
["KSK+ZSK", "CSK"].forEach(i => { x += `<option>${i}` } );
x += `</select></td></tr>${addAlg("signZoneForm")}`;
x += "<tr><td class=formPrompt>DNSSEC Policy: </td><td><select name=keyDNSSEC>";
["NSEC3","NSEC"].forEach(i => { x += `<option>${i}` } );
x += "</select></td></tr>";
x += "<tr><td><div style='height: 15px;'></div></td></tr>"
+ "<tr><td align=center colspan=2>"
+ btn("loadThisZone()","Cancel",`Do not sign ${ctx.zone}`)
+ btn("doSignZone()","Sign Zone",`DNSSEC Sign ${ctx.zone}`)
+ "</td></tr><tr><td><div style='height: 15px;'></div></td></tr>"
+ "<input type=submit style='display: none;'></form>";
x += "</table>";
x += "<P style='height: 25px;'>&nbsp;</p><table border=0 cellspacing=20 align=center>"
+ "<tr><Td colspan=2>Your friendly paper-clip helper buddy says ....</td></tr>"
+ `<tr><td><span style='font-size: 65pt'>${gbl.clip}</span></td><td>`
+ "<P>Unless you have a really good reason, you should probably sign your zone using <b>ECDSA256</b><P>"
+ "<b>NSEC</b> is more space & bandwidth efficient than <b>NSEC3</b>.<br><b>NSEC3</b> will enlarge your zone by typically twice as much as <b>NSEC</b>, but <b>NSEC</b> exposes all your DNS names in a linked list.<P>"
+ "<b>CSK</b> uses one key instead of two, but means your zone will double in size during key rollover.<br>With <b>KSK+ZSK</b> you have two keys to roll-over, but you can keep the same zone size during the process."
+ "</td></tr></table>"
gbl.bot.innerHTML = x;
}
function clickDS(txt)
{
let x = txt.split(" ");
if (x.length < 4) {
errMsg("Copy to clipboard failed");
return false;
}
navigator.clipboard.writeText(x[x.length-1]).then(
() => popMsg("Digest copied to clipboard"),
() => errMsg("Copy to clipboard failed")
);
return false;
}
function doAddKey()
{
let json = {
"keytype": document.addKeyForm.keyKind.value,
"active": (document.addKeyForm.keyActive.value=="true"),
"algorithm": document.addKeyForm.keyAlg.value,
"bits": parseInt(document.addKeyForm.cskBits.value),
};
if (json.algorithm == "ecdsa256") json.bits = 256;
else if (json.algorithm == "ecdsa384") json.bits = 384;
callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
return false // prevent submit
}
function clickAddKey()
{
let x = "<form onSubmit='return doAddKey();' name=addKeyForm><table align=center>"
+ "<tr><td><div style='height: 15px;'></div></td></tr>"
+ "<tr><th colspan=2>Add a DNSSEC Key</th></tr>"
+ "<tr><Td class=formPrompt>Key Type :</td><td><select name=keyKind>";
["ksk","zsk","csk"].forEach(i => { x += `<option value=${i}>${i.toUpperCase()}` } );
x += `</select></td></tr>${addAlg("addKeyForm")}`;
x += "<tr><Td class=formPrompt>Key Active :</td><td><select name=keyActive>";
["true","false"].forEach(i => { x += `<option>${i}`; });
x += "</select></td></tr>";
x += "<tr><td><div style='height: 15px;'></div></td></tr>"
+ "<tr><td align=center colspan=2>"
+ btn("cancelAddKey()","Cancel",`Do not add a key to the zone ${ctx.zone}`)
+ btn("doAddKey()","Add a Key",`Add a key to the zone ${ctx.zone}`)
+ "</td></tr><tr><td><div style='height: 15px;'></div></td></tr>"
+ "<input type=submit style='display: none;'></form>";
ctx.dnssec.t.style.display = "none";
ctx.dnssec.b.innerHTML = x;
}
function cancelAddKey()
{
ctx.dnssec.b.innerHTML = "";
ctx.dnssec.t.style.display = "block";
}
function activateZSK(a,b)
{
callApi(`zones/${ctx.zone}/cryptokeys/${ctx.zoneKeys[a].id}`,
() => {
callApi(`zones/${ctx.zone}/cryptokeys/${ctx.zoneKeys[b].id}`,zoneKeys,
{
method: "PUT",
okResp: 204,
json: '{ "active": true }'
} );
}, {
method: "PUT",
okResp: 204,
json: '{ "active": false }'
}
);
}
function startZSK(template)
{
let k = ctx.zoneKeys[template];
let json = {
"keytype": "zsk",
"active": false,
"algorithm": k.algorithm,
"bits": k.bits,
};
callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}
function startCSK(template)
{
let k = ctx.zoneKeys[template];
let json = {
"keytype": "csk",
"active": true,
"algorithm": k.algorithm,
"bits": k.bits,
}
callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}
function startKSK(template)
{
let k = ctx.zoneKeys[template];
let json = {
"keytype": "ksk",
"active": true,
"algorithm": k.algorithm,
"bits": k.bits,
}
callApi(`zones/${ctx.zone}/cryptokeys`,zoneKeys,
{ method: "POST", okResp: 201, json: JSON.stringify(json) } );
}
function toggleKey(id,act)
{
let p = { "active": (!(act)) };
callApi(`zones/${ctx.zone}/cryptokeys/${id}`,zoneKeys,
{ method: "PUT", okResp: 204, json: JSON.stringify(p) } );
}
function deleteAllKeys()
{
let nxtFn = deleteAllKeys;
let kId = ctx.zoneKeys[ctx.zoneKeys.length-1].id;
ctx.zoneKeys.pop();
if (ctx.zoneKeys.length == 0) {
if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != ""))
nxtFn = function() { setNSEC3(true); };
else
nxtFn = zoneKeys;
}
callApi(`zones/${ctx.zone}/cryptokeys/${kId}`,nxtFn, { method: "DELETE", okResp: 204 } );
}
function deleteKey(idx)
{
let k = ctx.zoneKeys[idx];
callApi(`zones/${ctx.zone}/cryptokeys/${k.id}`,zoneKeys, { method: "DELETE", okResp: 204 } );
}
function cancelKeyAction()
{
ctx.dnssec.b.innerHTML = "";
ctx.dnssec.t.style.display = "block";
delete ctx.dnssec.clickKey;
}
function clickDnssecKey(idx)
{
if (("clickKey" in ctx.dnssec)&&(ctx.dnssec.clickKey == idx)) {
ctx.dnssec.b.innerHTML = "";
ctx.dnssec.t.style.display = "block";
delete ctx.dnssec.clickKey;
return;
}
ctx.dnssec.clickKey = idx;
let sz = 150;
let k = ctx.zoneKeys[idx];
let x = "<table align=center>";
let txt = `Activate Key-${k.id}`;
if (k.active) txt = `Deactivate Key-${k.id}`;
x += "<tr><td>" + btn(`toggleKey(${k.id},${k.active})`,txt,txt,sz) + "</td></tr>"
+ "<tr><td>" + btn(`deleteKey(${idx})`,`Delete Key-${k.id}`,`Delete Key ${k.id}`,sz) + "</td></tr>"
+ "<tr><td>" + btn('cancelKeyAction()',"Cancel Action","Cancel an action on this key",sz) + "</td></tr>"
+ "</table>";
ctx.dnssec.t.style.display = "none";
ctx.dnssec.b.innerHTML = x
}
function showKeys(data)
{
data.sort((a,b) => {
if (a.keytype < b.keytype) return -1;
if (a.keytype > b.keytype) return 1;
if (a.active != b.active) {
if (a.active) return -1;
if (b.active) return 1;
}
if (a.id > b.id) return -1;
if (a.id < b.id) return 1;
return 0;
})
ctx.zoneKeys = data;
setState(`${gbl.server}: ${ctx.zone}, DNSSEC Keys`, { state: "keys", server: gbl.server, zone: ctx.zone, keys:true });
let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>";
x += "<tr><td width=100%><table width=100% border=0 cellspacing=1 cellpadding=0><colgroup>";
for(let i=0;i<5;i++) x += '<col width="50px">';
x += "</colgroup><tr>";
for (let i of ["Active","Id","Type/Flags","Bits","Algrythm",""])
x += `<th>${i}</th>`;
x += "</tr>";
let keyTypes = { num: 0 };
if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != "")) {
x += `<tr class=dataRow><td align=center>${gbl.tick}`
+ `</td><td colspan=3></td><td>NSEC3PARAM</td><td>${ctx.zoneDetails.nsec3param.toUpperCase()}</td></tr>`
}
for(let idx in data) {
i = data[idx];
x += `<tr onClick='clickDnssecKey(${idx});' class=dataRow><td align=center>`
let aflag = 0;
if (i.active) { aflag=1; x += gbl.tick; } else x += gbl.cross;
x += `</td><td align=center>${i.id}</td>`;
if (i.keytype in keyTypes) {
let k = keyTypes[i.keytype];
k.num++; k.active += aflag;
k.ids.push({ idx: parseInt(idx), id: i.id, act: i.active });
}
else {
keyTypes[i.keytype] = { ids: [], active: aflag, num: 1, }
keyTypes[i.keytype].ids.push({ idx: parseInt(idx), id: i.id, act: i.active });
keyTypes.num++;
}
x += `<td align=center>${i.keytype.toUpperCase()}/${i.flags}</td>`
+ `<td align=center>${i.bits}</td>`
+ `<td align=left>${i.algorithm}`
+ `</td><td class=dnskey>${i.dnskey.substr(1,50)}...</td></tr>`;
if ("ds" in i) {
for (let ds of i.ds) {
let dscol = ds.split(" ");
x += `<tr onClick="clickDS('${ds}');" class=dataRow><td></td><td><li>DS`
+ `</td><td>${dsAlg[parseInt(dscol[2])]}</td><td colspan=3>${ds}</td></tr>`;
}
}
x += "<tr><td><div style='height: 15px;'></div></td></tr>";
}
let sp = "<div style='height: 7px;'></div>";
let sz = 100;
x += "</table></td><td>"
+ btn('clickAddKey()',"Add Key",`Add a DNSSEC Key to ${ctx.zone}`,sz) + sp;
+ btn('deleteAllKeys()',"Unsign",`Remove all DNSSEC on ${ctx.zone}`,sz) + sp;
if (("nsec3param" in ctx.zoneDetails)&&(ctx.zoneDetails.nsec3param != "")) {
x += btn("setNSEC3(true)","Del NSEC3",`Switch ${ctx.zone} from NSEC3 to NSEC`,sz) + sp
+ btn("setNSEC3()","Roll NSEC3",`Change the NSEC3PARAM for ${ctx.zone}`,sz) + sp;
}
else
x += btn("setNSEC3()","Add NSEC3",`Switch this zone to NSEC3 ${ctx.zone}`,sz) + sp;
x += "</td></tr></table>";
sz = 200
x += "<div id='dnssecTop'><table align=center cellspacing=10 cellpadding=0 border=0>"
let notes = "If your zone is currently validating, after <b>every</b> Key Rollover step you must leave a few days<br>"
+ "for the internet to take-up your change. Probably a minimum of around 3 to 4 days";
let key_txt = null;
let zkey_txt = "";
if ((keyTypes.num==1)&&("csk" in keyTypes)) {
if (
([1,2].indexOf(keyTypes.csk.num) >= 0)&&
(keyTypes.csk.num == keyTypes.csk.active)
) {
let cid0 = keyTypes.csk.ids[0].idx;
let cid1 = keyTypes.csk.ids[keyTypes.csk.ids.length-1].idx;
let csk_rollover = [
`<tr><td>Step 1 of 2: </td><Td>${btn(`startCSK(${cid0})`,"Start CSK Rollover","Start a CSK Rollover",sz)}</td></tr>`,
`<tr><td>Step 2 of 2: </td><Td>${btn(`deleteKey(${cid1})`,"Complete CSK Rollover","Complete a CSK Rollover",sz)}</td></tr>`,
]
if (keyTypes.csk.num == 1) key_txt = csk_rollover[0];
else if (keyTypes.csk.num == 2) key_txt = csk_rollover[1];
}
}
else if ((keyTypes.num==2)&&("ksk" in keyTypes)&&("zsk" in keyTypes)) {
if (
([1,2].indexOf(keyTypes.ksk.num) >= 0)&&
([1,2].indexOf(keyTypes.zsk.num) >= 0)&&
(keyTypes.ksk.active == keyTypes.ksk.num)&&
(keyTypes.zsk.active == 1)
) {
let ksk_link = "<a target=_blank href='https://doc.powerdns.com/authoritative/guides/kskroll.html'>";
let zsk_link = "<a target=_blank href='https://doc.powerdns.com/authoritative/guides/zskroll.html'>";
let kid0 = keyTypes.ksk.ids[0].idx;
let kid1 = keyTypes.ksk.ids[keyTypes.ksk.ids.length-1].idx;
let zid0 = keyTypes.zsk.ids[0].idx;
let zid1 = keyTypes.zsk.ids[keyTypes.zsk.ids.length-1].idx;
let ksk_rollover = [
`<tr><td>${ksk_link}Step 1 of 2</a>: </td><Td>`
+ btn(`startKSK(${kid0})`,"Start KSK Rollover","Start a KSK Rollover",sz) + "</td></tr>",
`<tr><td>${ksk_link}Step 2 of 2</a>: </td><Td>`
+ btn(`deleteKey(${kid1})`,"Complete KSK Rollover","Complete the KSK Rollover",sz) + "</td></tr>",
];
let zsk_rollover = [
`<tr><td>${zsk_link}Step 1 of 3</a>: </td><Td>`
+ btn(`startZSK(${zid0})`,"Start ZSK Rollover","Start a ZSK Rollover",sz) + "</td></tr>",
`<tr><td>${zsk_link}Step 2 of 3</a>: </td><Td>`
+ btn(`activateZSK(${zid0},${zid1})`,"Activate ZSK Rollover","Activate the ZSK Rollover",sz) + "</td></tr>",
`<tr><td>${zsk_link}Step 3 of 3</a>: </td><Td>`
+ btn(`deleteKey(${zid1})`,"Complete ZSK Rollover","Complete the ZSK Rollover",sz) + "</td></tr>",
];
if (keyTypes.ksk.num == 1)
key_txt = ksk_rollover[0];
else if (keyTypes.ksk.num == 2) {
key_txt = ksk_rollover[1];
notes = "<u>If you have only just created a new KSK, wait a few days before changing your DS records</u>"
+ "<P>Once you have changed your DS reocrds, wait a few more days before completing the KSK rollover."
+ `<P>The DS records for Key-Id: ${keyTypes.ksk.ids[0].id}, should be the only ones present<br>`
+ "in the parent zone, for a few days, before completing the KSK rollover.";
}
if (keyTypes.zsk.num == 1)
zkey_txt = zsk_rollover[0];
else if (keyTypes.zsk.num == 2) {
if (keyTypes.zsk.ids[0].id < keyTypes.zsk.ids[1].id)
zkey_txt = zsk_rollover[1];
else
zkey_txt = zsk_rollover[2];
}
if (key_txt == null) zkey_txt = "";
}
}
x += "<tr><td colspan=2><hr></td></tr>"
if (key_txt == null)
x += "<tr><td><h3>DNSSEC Key Rollover State could not be detected</h3></td></tr>"
else
x += key_txt + zkey_txt;
x += "<tr><td colspan=2><hr></td></tr>"
x += "</table>";
if (notes != null) x += `<center>${notes}</center>`;
x += "</div><div id='dnssecBot'></div>"
gbl.bot.innerHTML = x;
topLine();
ctx.dnssec = {
t: document.getElementById("dnssecTop"),
b: document.getElementById("dnssecBot"),
}
}
function zoneKeys()
{
callApi(`zones/${ctx.zone}/cryptokeys`,showKeys);
}
//=============================================================================================
// Zone detils code
//=============================================================================================
function zoneKindChange(sel)
{
m = document.getElementById("asMaster");
s = document.getElementById("asSlave");
if (sel.value == "Master") {
m.style.display = "table-row-group";
s.style.display = "none";
}
else {
m.style.display = "none";
s.style.display = "table-row-group";
}
}
function zoneNotify()
{
callApi(`zones/${ctx.zone}/notify`, ret => {
if (ret!=null) popMsg(`${gbl.tick} Zone notified`); else errMsg("Notify failed");
}, { method: "PUT", callErr: true });
}
function zoneRectify()
{
callApi(`zones/${ctx.zone}/rectify`, ret => {
if (ret!=null) popMsg(`${gbl.tick} Zone rectified`); else errMsg("Rectify failed");
}, { method: "PUT", callErr: true });
}
function zoneRetrieve()
{
callApi(`zones/${ctx.zone}/axfr-retrieve`, ret => {
if (ret!=null) popMsg(`${gbl.tick} Zone retrieval requested`); else errMsg("Zone retrieval failed");
}, { method: "PUT", callErr: true });
}
function clickAddZone(isFresh)
{
ctx.addZone = true;
topLine();
gbl.bot.innerHTML = "";
gbl.addZ.style.display = "block";
if (isFresh) {
let f = document.newZoneForm;
f.zoneName.value = "";
f.zoneName.focus();
}
}
function dropZone()
{
let x = "<table align=center><tr><td align=center><h3>Are you sure you want to delete"
+ `<br>the zone '${ctx.zone}' and all its records ?</h3></td></tr>`
+ "<tr><td align=center>"
+ btn("loadThisZone();","Cancel",`Do not delete the zone ${ctx.zone}`)
+ "&nbsp;"
+ btn("doDropZone()",`Drop '${ctx.zone}'`,`Delete the zone ${ctx.zone} and ALL its records`);
gbl.bot.innerHTML = x;
}
function doDropZone()
{
callApi(`zones/${ctx.zone}`,zoneList, { method: "DELETE", noData: true, okResp: 204 });
}
function doNewName()
{
let f = document.newNameForm;
let rrType = f.rrType.value;
if (!validInt(f.rrTTL.value)) {
errMsg("Invalid TTL");
f.rrTTL.focus();
f.rrTTL.select();
return;
}
if ((f.rrName.value == "")||(f.rrName.value == "@"))
name = ctx.zone;
else {
if (!hostnameCheck.test(f.rrName.value)) {
errMsg("Invalid host name");
f.rrName.focus();
f.rrName.select();
return false;
}
name = `${f.rrName.value}.${ctx.zone}`;
}
gbl.addH.style.display = "none";
if (rrType=="CATALOG") {
name = f.rrName.value + ".";
catname = nameToCatalog(name) + `.zones.${ctx.zone}`;
ctx.rec = {
name: catname,
type: "PTR",
rrs: {
name: catname,
type: "PTR",
ttl: f.rrTTL.value,
records: [ { "content":name, "disabled":false } ],
},
};
return saveRRChanges(true);
}
if (findRec(ctx.zoneDetails.rrsets,{ name: name, type: rrType })!=null)
return clickZoneRecord(name,rrType);
ctx.rec = {
name: name,
type: rrType,
rrs: {
name: name,
type: rrType,
ttl: f.rrTTL.value,
records: [ ],
},
};
ctx.rec.loadedRows = 0;
ctx.needSave = "saveRRChanges";
addNewRR();
return false; // prevent submit
}
function zoneAddHost()
{
topLine();
gbl.bot.innerHTML = "";
gbl.addH.style.display = "block";
let f = document.newNameForm;
f.rrName.value = "";
f.rrName.focus();
}
function zoneDetails(data)
{
data.rrsets.sort((a,b) => {
if ((a.name == data.name)&&(b.name == data.name)) {
for(tag of ["SOA","NS","MX"]) {
if (a.type == tag) return -1;
if (b.type == tag) return 1;
}
}
else {
if (a.name == data.name) return -1;
if (b.name == data.name) return 1;
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
}
if (a.type < b.type) return -1;
if (a.type > b.type) return 1;
return 0;
})
ctx.zoneDetails = data;
if (("param" in gbl)&&("zone" in gbl.param)) {
if ("meta" in gbl.param) return zoneMeta();
if ("keys" in gbl.param) return zoneKeys();
if ((data.kind != "Slave")&&("name" in gbl.param)&&("type" in gbl.param)) {
return clickZoneRecord(gbl.param.name,gbl.param.type);
}
}
setState(`${gbl.server}: ${ctx.zone}`, { state: "zone", server: gbl.server, zone: ctx.zone });
let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>"
+ "<tr><td width=100%><table width=100% border=0 cellspacing=0 cellpadding=0>";
for(let i of data.rrsets) {
x += `<tr onClick="clickZoneRecord('${i.name}','${i.type}');" class=dataRow>`
+ `<td valign=top>${i.name.substr(0,i.name.length - ctx.zone.length - 1)}</td>`
+ `<td valign=top align=right>${i.ttl}</td>`
+ `<td valign=top>${i.type}</td><td class=dataCell valign=top>`;
if (i.type == "SOA") {
ctx.soaSplit = i.records[0].content.split(" ");
x += formatSOA(ctx.soaSplit);
}
else {
i.records.sort((a,b) => {
if (a.content < b.content) return -1;
if (a.content > b.content) return 1;
return 0;
})
for(let rrs of i.records)
x += `${rrs.content}<br>`;
}
x += "</td></tr>";
}
let sp = "<div style='height: 7px;'></div>";
let sz = 100;
x += "</table></td><td align=center>";
if (ctx.zoneDetails.kind != "Slave")
x += btn("zoneAddHost()","Add Host",`Add a host to the zone ${ctx.zone}`,sz) + sp;
x += btn("zoneAxfr()","Download",`Download ${ctx.zone} in RFC/AXFR format`,sz) + sp
+ btn('zoneMeta()',"Metadata",`Load the META data for ${ctx.zone}`,sz) + sp
+ btn('zoneNotify()',"Notify Zone",`Notify the slaves of ${ctx.zone}`,sz) + sp;
if (ctx.zoneDetails.dnssec)
x += btn('zoneRectify()',"Rectify Zone",`Rectify the data for ${ctx.zone}`,sz) + sp;
if (ctx.zoneDetails.kind == "Slave")
x += btn('zoneRetrieve()',"Retrieve Zone",`Retrieve ${ctx.zone} from its master(s)`,sz) + sp;
x += btn('dropZone()',"Drop Zone",`Delete ${ctx.zone} and ALL its records`,sz);
x += "<hr>";
if (ctx.zoneDetails.dnssec) {
x += btn('zoneKeys()',"DNSSEC Keys",`View DNSSEC keys for ${ctx.zone}`,sz) + sp;
}
else
x += btn('signZone()',"Sign Zone",`DNSSEC sign ${ctx.zone}`,sz) + sp;
x += "</td></tr></table>";
gbl.bot.innerHTML = x;
topLine();
}
function loadThisZone()
{
return zoneDetailsLoad(ctx.zone);
}
function zoneDetailsLoad(zone)
{
document.body.scrollTop = 0;
ctx = {
zone: zone,
zoneList: ctx.zoneList,
};
callApi(`zones/${ctx.zone}`,zoneDetails);
}
//=============================================================================================
// Search handling code
//=============================================================================================
function searchLoadRecord(zone,name,type)
{
gbl.param = { state: "record", server: gbl.server, zone: zone, name:name, type: type };
gbl.state = gbl.param;
title = `${gbl.server}, Search results - ${name}:${type}`
window.history.pushState(gbl.param,title,gbl.pathname);
zoneDetailsLoad(zone);
}
function searchResults(data)
{
if (data.length <= 0) {
popMsg("No search results");
return;
}
setState(`${gbl.server}: Search Results`, { state: "search", server: gbl.server, search: ctx.searchTerm });
ctx = { searchTerm: ctx.searchTerm }; topLine(); ctx = {};
let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>";
for(let i of data) {
x += "<tr class=dataRow";
if (i.object_type != "zone") {
x += ` onClick='ctx = { zone: "${i.zone}" }; `
+ ` searchLoadRecord("${i.zone}","${i.name}","${i.type}");'`
+ `><td valign=top>${i.name}</td>`
+ `<td valign=top align=right>${i.ttl}</td>`
+ `<td valign=top>${i.type}</td>`;
if (i.type == "SOA")
x += `<td valign=top>${formatSOA(i.content.split(" "))}</td>`;
else
x += `<td valign=top>${i.content}</td>`;
}
else {
x += ` onClick='return zoneDetailsLoad("${i.name}");'>`
+ `<td>${i.name}</td><td colspan=3>[Zone Apex]</td>`;
}
x += "</tr>";
}
x += "</table>";
gbl.bot.innerHTML = x;
}
function searchFormSubmit()
{
let s = document.searchForm.searchTerm.value;
ctx.searchTerm = s;
document.searchForm.searchTerm.value = "";
callApi(`search-data?q=${s}`,searchResults);
return false;
}
//=============================================================================================
// TSIGs
//=============================================================================================
function loadTsigList()
{
callApi("tsigkeys",showTsigList);
}
function clickTSIG(txt,name)
{
navigator.clipboard.writeText(txt).then(
() => popMsg(`${name} copied to clipboard`),
() => errMsg("Copy to clipboard failed")
);
}
function deleteTSIG(id)
{
callApi(`tsigkeys/${id}`,loadTsigList,{ method: "DELETE", okResp: 204 });
}
function formAddTsig()
{
let f = document.newTsigForm;
let json = {
name: f.tsigName.value,
algorithm: f.tsigAlg.value,
key: f.tsigKey.value,
};
if (json.name.substr(json.name.length-1)!=".")
json.name += ".";
if (!fqdnCheck.test(json.name)) {
errMsg("Invalid TSIG Name");
document.newTsigForm.tsigName.focus();
document.newTsigForm.tsigName.select();
return false;
}
callApi("tsigkeys",data => {
if (data == null) addTSIG(false); else showTSIG(data.id);
},
{ method: "POST", callErr: true, okResp: 201, json: JSON.stringify(json) });
return false; // prevent form submit
}
function regenTSIG(id,alg)
{
callApi(`tsigkeys/${id}`,() => {
callApi("tsigkeys",() => showTSIG(id),
{ method: "POST", okResp: 201, json: JSON.stringify({ name:id, algorithm: alg }) });
}
,{ method: "DELETE", okResp: 204 });
}
function addTSIG(withClr)
{
gbl.bot.innerHTML = "";
gbl.addT.style.display = "block";
let t = document.newTsigForm.tsigName;
let k = document.newTsigForm.tsigKey;
if (withClr) { t.value = ""; k.value = ""; }
t.focus();
}
function showTSIG(tsig)
{
callApi(`tsigkeys/${tsig}`,data => {
setState(`${gbl.server}: TSIG ${data.id}`, { state: "tsigdata", server: gbl.server, tsig: data.id });
ctx = {}; topLine();
let x = "<table style='margin-top: 50px;' align=center border=0 cellspacing=0 cellpadding=0>"
+ `<tr class=dataRow onClick=\"clickTSIG('${data.id}','Name');\"><td class=formPrompt>TSIG Name :</td><td>${data.id}</td></tr>`
+ `<tr><td class=formPrompt>TSIG Algorithm :</td><td>${data.algorithm.toUpperCase()}</td></tr>`
+ `<tr onClick=\"clickTSIG('${data.key}','Key');\" class=dataRow><td class=formPrompt>TSIG Key :</td><td>${data.key}</td></tr>`
+ "<tr><td align=center colspan=2><hr></td></tr>"
+ "<tr><td align=center colspan=2>"
+ btn("loadTsigList()","Back","Go back to the list of TSIGs")
+ btn(`regenTSIG('${data.id}','${data.algorithm}')`,"Rollover","Replace this TSIG Key, keeping the name")
+ btn(`deleteTSIG('${data.id}')`,"Delete","Delete this TSIG Key")
+ "</td></tr></table>";
gbl.bot.innerHTML = x;
topLine();
});
}
function showTsigList(data)
{
if (data.length <= 0) {
popMsg("No TSIGs found");
return;
}
data.sort((a,b) => {
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
});
if (("param" in gbl)&&("tsig" in gbl.param)&&(typeof(gbl.param.tsig) != "boolean"))
return showTSIG(gbl.param.tsig);
setState(`${gbl.server}: TSIGs`, { state: "tsig", server: gbl.server, tsig: true });
ctx = {}; topLine();
let x = "<table width=100% border=0 cellspacing=0 cellpadding=0>"
+ "<colgroup><col width=100%/><col/></colgroup><tr><Td>"
+ "<table align=center border=0 cellspacing=2 cellpadding=2>"
+ "<tr><th>TSIG Name</td><th>Algorithm</td></tr>";
for(let i of data)
x += `<tr class=dataRow onClick="showTSIG('${i.id}');"><td>${i.id}</td>`
+ `<td>${i.algorithm.toUpperCase()}</td></tr>`;
x += "</table></td><td>" + btn("addTSIG(true)","Add TSIG","Add a TSIG Key") + "</td></tr></table>";
gbl.bot.innerHTML = x;
}
//=============================================================================================
// Code to handle the list of zones
//=============================================================================================
function doMakeZone(data)
{
if (data != null)
zoneDetailsLoad(data.name);
else {
clickAddZone(false);
errMsg("Adding zone failed");
}
}
function doNewZone()
{
let name = document.newZoneForm.zoneName.value;
if (name.substr(name.length-1)!=".") name += ".";
if (!fqdnCheck.test(name)) {
errMsg("Invalid domain name");
document.newZoneForm.zoneName.focus();
document.newZoneForm.zoneName.select();
return false;
}
let kind = document.newZoneForm.zoneKind.value;
let ns = [];
let ip = [];
for(let i=0;i<4;i++) {
let x = document.newZoneForm[`ns${i}`];
let v = x.value;
if (v != "") {
if (v.substr(v.length-1)!=".") v += ".";
if (!fqdnCheck.test(v)) {
errMsg(`Invalid Host Name: ${v}`);
x.focus(); x.select();
return false;
}
ns.push(v);
}
x = document.newZoneForm[`ip${i}`];
if (x.value != "") {
if ((validations["rrA"].test(x.value))||(validations["rrA"].test(x.value)))
ip.push(x.value);
else {
errMsg(`Invalid IP Address: ${x.value}`);
x.focus(); x.select();
return false;
}
}
}
let data = {
name: name,
kind: kind,
masters: ip,
nameservers: ns,
};
callApi(gbl.zone_list,doMakeZone, { method: "POST", callErr: true, okResp: 201, json: JSON.stringify(data) } );
return false; // block form submit
}
function zoneListDetails(data)
{
data.sort((a,b) => {
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
});
ctx.zoneList = data;
if ("param" in gbl) {
if (gbl.param.state == "stats") {
if ("stats" in gbl.param) return oneStat(gbl.param.stats);
return loadStats();
}
if ("tsig" in gbl.param) return loadTsigList();
if ("search" in gbl.param) {
ctx.searchTerm = gbl.param.search;
return callApi(`search-data?q=${ctx.searchTerm}`,searchResults);
}
if ("zone" in gbl.param) return zoneDetailsLoad(gbl.param.zone);
}
setState(`${gbl.server}: Zone List`, { state: "zonelist", server: gbl.server });
let x = "<table border=0 cellspacing=0 cellpadding=0 width=100%><colgroup><col width=100%/><col/></colgroup>"
x += "<tr><td><table width=100% border=0 cellspacing=0 cellpadding=0>";
for(let i of data) {
x += `<tr class=dataRow onClick='zoneDetailsLoad("${i.name}")'>`
+ `<td>${i.name}</td><td>${i.kind}</td>`;
t = "";
hlp = "Zone is NOT dnssec signed";
if (i.dnssec) {
t = gbl.nsec;
hlp = "Zone is DNSSEC signed";
}
x += `<td title='${hlp}'>${t}</td><td>${i.serial}</td>`;
t = ""; hlp = "";
if (i.notified_serial != i.serial) {
t = gbl.timer;
hlp = "Zone has unnotified changes";
}
x += `<td title='${hlp}'>${t}</td></tr>`;
}
x += "</table></td><td>";
let sp = "<div style='height: 7px;'></div>";
let sz = 100;
x += btn("clickAddZone(true)","Add Zone","Add a new zone to the database",sz)+sp;
x += "</td></tr></table>";
topLine();
gbl.bot.innerHTML = x;
}
function zoneList()
{
document.body.scrollTop = 0;
ctx = { }; topLine();
callApi(gbl.zone_list,zoneListDetails);
return false;
}
//=============================================================================================
// Zone AXFR
//=============================================================================================
function zoneAxfrData(data)
{
let name = ctx.zone.replace(/\./g,"_");
name = `${name.substr(0,name.length-1)}.txt`;
downloadFile(data,"text/plain",name);
}
function zoneAxfr()
{
callApi(`zones/${ctx.zone}/export`,zoneAxfrData);
}
//=============================================================================================
// Common functions
//=============================================================================================
function validInt(x) {
return ((x != "") && (x != null) && (!(isNaN(x))));
}
function btn(call,txt,hlp,sz) {
let ex=""
if (sz != null) ex = `style='width: ${sz}px;'`
return `<span ${ex} title="${hlp}" class=myBtn onClick="${call}; return false;">${txt}</span>`;
}
function goLogin()
{
document.location = gbl.pathname;
}
function topLine()
{
if (!("zoneList" in ctx)) return;
gbl.forms.forEach(i => i.style.display = "none");
let y = "<table style='margin-top:10px;' border=0 cellspacing=0 cellpadding=0 width=100%><tr><td width=1 class=dataRow>"
+ `<font size=+1><a title='Open this page in a new window' target=_blank href='${window.origin}${gbl.pathname}?`;
let x = `'>${gbl.page}</a></font></td><td>`
+ btn("goLogin()",gbl.server,"Change server")
+ btn("zoneList()","Zones",`Reload the list of zones at ${gbl.server}`);
if ((gbl.state.state == "tsig")||(gbl.state.state == "tsigdata"))
x += btn("loadTsigList()","TSIGs","Show the list of TSIGs");
if ("searchTerm" in ctx) delete ctx.searchTerm;
if ("zone" in ctx) {
x += btn('loadThisZone()',ctx.zone.substr(0,ctx.zone.length-1),`Reload all records in ${ctx.zone}`);
if ("rec" in ctx) {
let name = "@";
if (ctx.rec.name != ctx.zone)
name = ctx.rec.name.substr(0,ctx.rec.name.length - ctx.zone.length - 1);
ctx.rec.showName = name;
x += btn('getZoneRecord()',`${name}/${ctx.rec.type}`,`Reload the ${ctx.rec.type} recods for ${name}`);
}
else if (gbl.state.state == "meta") {
x += btn('zoneMeta()',"META",`Reload the META data for ${ctx.zone}`);
}
else if ("zoneKeys" in ctx) {
x += btn('zoneKeys()',"DNSSEC",`Reload the DNSSEC Keys for ${ctx.zone}`);
}
}
else if (("state" in gbl)&&(gbl.state.state == "stats")) {
x += btn("loadStats()","Stats","View the server stats");
if ("stats" in gbl.state)
x += btn(`oneStat('${gbl.state.stats}')`,gbl.state.stats,`View the server stat - ${gbl.state.stats}`);
}
x += "</td><td width=100% align=center><span class='fullvis' id='showMsg'></span></td><td>";
if ("zone" in ctx) {
if ("edit" in ctx) {
if ("needCancel" in ctx.edit)
x += btn(`${ctx.edit.needCancel}(false)`,"Cancel","Cancel this edit");
else
x += btn("cancelAllEdits()","Cancel","Cancel this edit");
}
else {
if ("rec" in ctx)
x += btn("addNewRR()","Add RR",`Add another ${ctx.rec.type} record to ${ctx.rec.name}`);
if ("needSave" in ctx)
x += btn(`${ctx.needSave}(false)`,"Cancel","Abandom all changes")
+ btn(`${ctx.needSave}(true)`,"Save","Save your changes to the zone file");
if (gbl.state.state == "meta")
x += btn('clickAddMeta()',"Add Meta",`Add a META data record to ${ctx.zone}`);
}
}
if (gbl.state.state == "zonelist") {
x += btn("loadTsigList()","TSIGs","Show the list of TSIGs")
if (!("stats" in ctx))
x += btn("loadStats()","Stats","View the server stats");
}
x += "</td><td width=1 align=right><form name=searchForm onSubmit='return searchFormSubmit();'>"
+ "<input placeholder='Search' size=30 name=searchTerm></form></td>"
+ "</tr><table><hr>";
gbl.top.innerHTML = y + btoa(JSON.stringify(gbl.state)) + x;
}
function loadCookies()
{
let ckobj = {}
let cookies = document.cookie.split(";");
for(let i in cookies) {
ck = cookies[i];
while (ck.charAt(0) == ' ') ck = ck.substring(1);
if ((idx = ck.indexOf("=")) >= 0) {
let itm = ck.substr(0,idx);
if (itm in pdnsCookies) {
try { ckobj[itm] = atob(ck.substr(idx+1)); } catch(e) {}
}
}
}
let f = document.loginForm;
if ("pdns_server" in ckobj) f.server.value = ckobj["pdns_server"];
if ("pdns_with_https" in ckobj) f.with_https.checked = (ckobj["pdns_with_https"] == "true");
if ("pdns_fast_zone_list" in ckobj) f.fast_zone_list.checked = (ckobj["pdns_fast_zone_list"] == "true");
}
function startUp()
{
gbl.pathname = window.location.pathname;
let f = document.loginForm;
if (f.server.value == "") f.server.value = window.location.host;
f.with_https.checked = (window.location.protocol == "https:");
f.fast_zone_list.checked = gbl.fast_zone_list;
loadCookies()
window.addEventListener('popstate', e => {
if (e.state == null) goLogin();
gbl.param = e.state;
if ("server" in gbl.param) {
gbl.server = gbl.param.server;
zoneList();
return true;
}
delete gbl.param;
document.location = gbl.pathname;
return true;
});
gbl.top = document.getElementById("topSpan");
gbl.bot = document.getElementById("lowerSpan");
gbl.addH = document.getElementById("addName");
gbl.addZ = document.getElementById("addZone");
gbl.addT = document.getElementById("addTsig");
gbl.optM = document.getElementById("metaOpt");
gbl.forms = [ gbl.addH, gbl.addZ, gbl.addT ];
if (window.location.search != "") {
gbl.param = JSON.parse(atob(window.location.search.substr(1)));
if ("server" in gbl.param) {
gbl.state = gbl.param;
gbl.server = gbl.param.server;
zoneList();
}
}
document.newNameForm.rrTTL.value = gbl.default_ttl;
}
function initialLogin()
{
let f = document.loginForm;
gbl.server = f.server.value;
gbl.apikey = f.apikey.value;
gbl.with_https = f.with_https.checked;
gbl.fast_zone_list = f.fast_zone_list.checked;
gbl.pdns_server = gbl.server;
gbl.pdns_with_https = gbl.with_https;
gbl.pdns_fast_zone_list = gbl.fast_zone_list;
if (gbl.pdns_fast_zone_list) gbl.zone_list = "zones?dnssec=false"; else gbl.zone_list="zones";
let milliDays = 30 * 86400000; // 30 days in milliseconds
let d = new Date();
d.setTime(d.getTime() + milliDays);
let expires = "; samesite=strict; expires="+ d.toUTCString();
for(let i in pdnsCookies)
document.cookie = i + "= ; samesite=strict; expires = Thu, 01 Jan 1970 00:00:00 GMT"
for(let i in pdnsCookies)
document.cookie = i + "=" + btoa(gbl[i]) + expires;
zoneList();
return false; // block form submit
}
function formatSOA(i)
{
let soa = `${i[0]} ${i[1]}<br>`;
let idx = 2;
for(let tag of ["serial","refresh","retry","expire","minimum"]) {
soa += `<span style='display: inline-block; width:50px'></span>`;
soa += `<span style='display: inline-block; width:100px'>${i[idx++]}</span>;${tag}<br>`;
}
return soa;
}
function unerrMsg()
{
let m = document.getElementById("myMsgPop");
let t1 = m.innerHTML;
let t2 = gbl.lastErrMsg;
if (t2 == null) t2 = "";
if (t1 == t2) m.className = "msgPop msgPopNo";
delete gbl.lastErrMsg;
}
function errMsg(txt)
{
let m = document.getElementById("myMsgPop");
m.className = 'msgPop msgPopYes';
m.innerHTML = `${gbl.warn} ${txt}`;
gbl.lastErrMsg = m.innerHTML;
msg("");
setTimeout(unerrMsg,2500);
}
function unpopMsg()
{
let m = document.getElementById("showMsg");
let t1 = m.innerHTML;
let t2 = gbl.lastMsg;
if (t2 == null) t2 = "";
if (t1 == t2) m.className = "fadeout";
delete gbl.lastMsg;
}
function popMsg(txt)
{
msg(txt,"fadein");
gbl.lastMsg = document.getElementById("showMsg").innerHTML;
setTimeout(unpopMsg,2500);
}
function msg(myMsg,newClass)
{
let m = document.getElementById("showMsg");
if (!m) m = document.getElementById("lowerMsg");
if (!m) return;
if (newClass == null) newClass = "fullvis";
m.className = newClass;
m.innerHTML = myMsg;
delete gbl.lastMsg;
}
function keyEsc(e) {
if (e.key == "Escape") {
if ("edit" in ctx) return cancelAllEdits();
if ("addRR" in ctx) return loadThisZone();
if ("addZone" in ctx) return zoneList();
}
}
//=============================================================================================
</script>
<head>
<title>PowerDNS WebUI - Login</title>
</head>
<body onLoad="startUp();">
<div id="topSpan">
<table border=0 cellspacing=0 cellpadding=0 width=100%>
<tr><td><h1 style="text-align:center">PowerDNS WebUI</h1></td></tr>
</table>
</div>
<div id="lowerSpan">
<table align=center>
<form action="/" onSubmit="return initialLogin()" name=loginForm>
<tr><td class=formPrompt>Server:</td><td><input title="PowerDNS RestAPI, can include optional :[port] and sub-directory" name=server size=40 value=""></td></tr>
<tr><td class=formPrompt>API-Key:</td><td><input autocomplete="current-password" type=password size=40 name=apikey value=""></td></tr>
<tr><td class=formPrompt>HTTPS (SSL/TLS):</td><td><input title="Connect to API over HTTPS, not HTTP" type=checkbox name=with_https></td></tr>
<tr><td class=formPrompt>Fast Zone List:</td><td><input title="Fast load long zone lists by excluding some information" type=checkbox name=fast_zone_list></td></tr>
<tr><td align=center colspan=2><span title="Load zone list" class=myBtn onClick="initialLogin()">Load Zone List</span></td></tr>
</tr>
<input type=submit style="display: none;">
</form>
</table>
<P>
<div class='fullvis' id="lowerMsg"></div>
</div>
<div id=addZone style="display: none;">
<table width=40% align=center><tbody id="startBody">
<colgroup><col width=35%/><col width=65%/></colgroup>
<tr><td colspan=2 align=center><h2>Add a New Zone</h2></td></tr>
<form method=post action="/" name=newZoneForm onSubmit="return doNewZone();">
<tr><td class=formPrompt>Name :</td><td><input onkeydown="keyEsc(event);" name=zoneName></td></tr>
<tr><td class=formPrompt>Type :</td><td><select onChange="zoneKindChange(this);" name=zoneKind><option>Master<option>Slave<option>Native</select></td></tr>
</tbody><tbody id="asMaster">
<tr><td class=formPrompt>Name Servers :</td><td>
<input size=40 name=ns0><br>
<input size=40 name=ns1><br>
<input size=40 name=ns2><br>
<input size=40 name=ns3><br></td></tr>
</tbody><tbody id="asSlave" style="display: none;">
<tr><td class=formPrompt>Master's IPs :</td><td>
<input size=40 name=ip0><br>
<input size=40 name=ip1><br>
<input size=40 name=ip2><br>
<input size=40 name=ip3><br></td></tr>
</tbody>
<tr><td colspan=2><hr></td></tr>
<tr><td align=center colspan=2><span title="Cancel adding zone" class=myBtn onClick="zoneList();">Cancel</span><span title="Add new zone" class=myBtn onClick="doNewZone();">Add Zone</span></td></tr>
</tr>
<input type=submit style="display: none;">
</form>
</table>
</div>
<div id=addName style="display: none;">
<table width=40% align=center>
<colgroup><col width=35%/><col width=65%/></colgroup>
<tr><td colspan=2 align=center><h2>Add a New Host</h2></td></tr>
<form method=post action="/" name=newNameForm onSubmit="return doNewName();">
<tr><td class=formPrompt>Host name:</td><td><input onkeydown="keyEsc(event);" name=rrName></td></tr>
<tr><td class=formPrompt>RR Type:</td><td><select name=rrType>
<option>A<option>AAAA<option>MX<option>NS<option>TXT
<option value=CATALOG>Add Zone to Catalog
<option>A6<option>ADDR<option>AFSDB<option>ALIAS
<option>CAA<option>CDNSKEY<option>CDS<option>CERT
<option>CNAME<option>DHCID<option>DLV<option>DNAME<option>DNSKEY<option>DS
<option>EUI48<option>EUI64<option>HINFO<option>IPSECKEY<option>KEY
<option>KX<option>LOC<option>LUA<option>MAILA<option>MAILB<option>MB
<option>MG<option>MINFO<option>MR<option>MX<option>NAPTR<option>NS
<option>OPENPGPKEY<option>OPT<option>PTR
<option>RKEY<option>RP<option>RRSIG<option>SIG<option>SMIMEA<option>SOA
<option>SPF<option>SRV<option>SSHFP<option>TKEY<option>TLSA<option>TXT
<option>URI<option>WKS
</select></td></tr>
<tr><td class=formPrompt>TTL:</td><td><input value=86400 name=rrTTL></td></tr>
<tr><td colspan=2><hr></td></tr>
<tr><td align=center colspan=2><span class=myBtn title="Cancel adding RR" onClick="loadThisZone();">Cancel</span><span title="Add this RR" class=myBtn onClick="doNewName();">Add Record</span></td></tr>
</tr>
<input type=submit style="display: none;">
</form>
</table>
</div>
<div id=addTsig style="display: none;">
<table width=40% align=center>
<colgroup><col width=35%/><col width=65%/></colgroup>
<tr><td colspan=2 align=center><h2>Add a New TSIG Key</h2></td></tr>
<form method=post action="/" name=newTsigForm onSubmit="return formAddTsig();">
<tr><Td class=formPrompt>TSIG Name: </td><td><input size=50 name=tsigName></td></tr>
<tr><td class=formPrompt>TSIG Algrythm: </td><td><select name=tsigAlg>
<option value="hmac-sha512">HMAC-SHA512<option value="hmac-sha384">HMAC-SHA384<option value="hmac-sha256">HMAC-SHA256
<option value="hmac-sha224">HMAC-SHA224<option value="hmac-sha1">HMAC-SHA1<option value="hmac-md5">HMAC-MD5
</select></td></tr>
<tr><Td class=formPrompt>TSIG Key: </td><td><input placeholder='Leave blank, for auto-generated key' size=100 name=tsigKey></td></tr>
<tr><td colspan=2><hr></td></tr>
<tr><td align=center colspan=2><span class=myBtn title="Cancel adding a TSIG Key" onClick="loadTsigList();">Cancel</span><span title="Add this TSIG Key" class=myBtn onClick="formAddTsig();">Add TSIG</span></td></tr>
<input type=submit style="display: none;">
</form>
</table>
</div>
<div id=myMsgPop class='msgPop msgPopNo'></div>
</body></html>