2896 lines
74 KiB
HTML
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: "☑",
|
|
cross: "☒",
|
|
timer: "⏳",
|
|
bin: "✂",
|
|
warn: "⚠",
|
|
page: "🗎",
|
|
nsec: "🔒",
|
|
nsec3: "🔐",
|
|
clip: "📎",
|
|
};
|
|
|
|
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,"'")}'></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;'> </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}`)
|
|
+ " "
|
|
+ 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>
|