diff options
| author | Shulhan <ms@kilabit.info> | 2022-05-22 15:57:52 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-05-22 16:14:35 +0700 |
| commit | a676f04d2f8c1981fc953cc2e87adf059e23089a (patch) | |
| tree | d86300df47d53abdf7ccb0342f96ddd3a677fbd4 /_www | |
| parent | bb08b5bb6ad6cf3e1c409db7d69ace4364e7bc8d (diff) | |
| download | rescached-a676f04d2f8c1981fc953cc2e87adf059e23089a.tar.xz | |
all: move the documentation under _www/doc directory
This also allow the latest/released documentation viewed on
the web user interface under /doc path.
While at it, reformat HTML and CSS files using js-beautify and
JavaScript files using clang-format [1].
[1] https://google.github.io/styleguide/jsguide.html#formatting
Diffstat (limited to '_www')
| -rw-r--r-- | _www/block.d/index.html | 254 | ||||
| l--------- | _www/doc/CHANGELOG.adoc | 1 | ||||
| l--------- | _www/doc/README.adoc | 1 | ||||
| -rw-r--r-- | _www/doc/benchmark.adoc | 125 | ||||
| -rw-r--r-- | _www/doc/html.tmpl | 34 | ||||
| -rw-r--r-- | _www/doc/images/Screenshot_wui_environment.png | bin | 0 -> 158861 bytes | |||
| -rw-r--r-- | _www/doc/images/Screenshot_wui_frontpage.png | bin | 0 -> 344669 bytes | |||
| -rw-r--r-- | _www/doc/images/Screenshot_wui_hosts_blocks.png | bin | 0 -> 198326 bytes | |||
| -rw-r--r-- | _www/doc/images/Screenshot_wui_hosts_d.png | bin | 0 -> 125782 bytes | |||
| -rw-r--r-- | _www/doc/images/Screenshot_wui_zone_d.png | bin | 0 -> 91905 bytes | |||
| -rw-r--r-- | _www/doc/index.adoc | 35 | ||||
| -rw-r--r-- | _www/doc/rescached.cfg.adoc | 263 | ||||
| -rw-r--r-- | _www/doc/resolver.adoc | 460 | ||||
| -rw-r--r-- | _www/environment/index.html | 553 | ||||
| -rw-r--r-- | _www/hosts.d/index.html | 415 | ||||
| -rw-r--r-- | _www/index.css | 155 | ||||
| -rw-r--r-- | _www/index.html | 391 | ||||
| -rw-r--r-- | _www/index.js | 32 | ||||
| -rw-r--r-- | _www/rescached.js | 596 | ||||
| -rw-r--r-- | _www/zone.d/index.html | 934 |
20 files changed, 2586 insertions, 1663 deletions
diff --git a/_www/block.d/index.html b/_www/block.d/index.html index 4707919..47ff4fa 100644 --- a/_www/block.d/index.html +++ b/_www/block.d/index.html @@ -2,141 +2,152 @@ <!-- SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info --> <!-- SPDX-License-Identifier: GPL-3.0-or-later --> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <link rel="icon" type="image/png" href="/favicon.png" /> - <link rel="stylesheet" href="/index.css" /> - <title>rescached | hosts blocks</title> - <style> - .block_source { - width: calc(100% - 2em); - overflow: auto; - } - .block_source input:disabled { - color: black; - } - .item { - width: 100%; - margin-bottom: 1em; - } - .item.header { - font-weight: bold; - margin-bottom: 1em; - border-bottom: 1px solid silver; - } - .item .is-enabled { - display: inline-block; - width: 4em; - vertical-align: top; - } - .item .info { - display: inline-block; - width: calc(100% - 6em); - } - .item .info button { - display: inline-block; - margin: 0; - padding: 4px; - } - .item .info input { - width: calc(100% - 4em); - } - </style> - </head> - <body onload="onLoad()"> - <nav class="menu"> - <a href="/"> rescached </a> - / - <a href="/environment/"> Environment </a> - / - <a href="/block.d/" class="active"> block.d </a> - / - <a href="/hosts.d/"> hosts.d </a> - / - <a href="/zone.d/"> zone.d </a> - </nav> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | hosts blocks</title> - <div id="notif"></div> + <style> + .block_source { + width: calc(100% - 2em); + overflow: auto; + } - <p>Configure the source of blocked hosts file.</p> + .block_source input:disabled { + color: black; + } - <div class="block_source"> - <div class="item header"> - <span class="is-enabled"> Enabled </span> - <span class="info"> Name </span> - </div> - </div> - <div id="HostsBlocks" class="block_source"></div> + .item { + width: 100%; + margin-bottom: 1em; + } - <div> - <button onclick="updateHostsBlocks()">Save</button> - </div> + .item.header { + font-weight: bold; + margin-bottom: 1em; + border-bottom: 1px solid silver; + } - <script src="/index.js"></script> - <script src="/rescached.js"></script> - <script> - let resc = null + .item .is-enabled { + display: inline-block; + width: 4em; + vertical-align: top; + } - function onLoad() { - resc = new Rescached("") - getEnvironment() - } + .item .info { + display: inline-block; + width: calc(100% - 6em); + } - async function getEnvironment() { - const res = await resc.getEnvironment() - if (res.code != 200) { - notifError(res.message) - return - } + .item .info button { + display: inline-block; + margin: 0; + padding: 4px; + } - let env = res.data - renderHostsBlocks(env.HostsBlocks) - } + .item .info input { + width: calc(100% - 4em); + } + </style> +</head> - function onCheckHostblock(key, val) { - resc.env.HostsBlocks[key].IsEnabled = val - } +<body onload="onLoad()"> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/block.d/" class="active"> block.d </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/"> zone.d </a> + / + <a href="/doc/"> Documentation </a> + </nav> - async function blockdUpdate(name) { - const res = await resc.BlockdUpdate(name) - if (res.code != 200) { - notifError("blockdUpdate: ", res.message) - return - } + <div id="notif"></div> - resc.env.HostsBlocks[name] = res.data + <p>Configure the source of blocked hosts file.</p> - notifInfo("The hosts blocks has been updated.") + <div class="block_source"> + <div class="item header"> + <span class="is-enabled"> Enabled </span> + <span class="info"> Name </span> + </div> + </div> + <div id="HostsBlocks" class="block_source"></div> - renderHostsBlocks(resc.env.HostsBlocks) - } + <div> + <button onclick="updateHostsBlocks()">Save</button> + </div> - async function updateHostsBlocks() { - const res = await resc.updateHostsBlocks(resc.env.HostsBlocks) - if (res.code != 200) { - notifError("updateHostsBlocks: ", res.message) - return - } + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null - renderHostsBlocks(res.data) - notifInfo("The hosts blocks has been updated.") - } + function onLoad() { + resc = new Rescached("") + getEnvironment() + } - function renderHostsBlocks(hostsBlocks) { - let parent = document.getElementById("HostsBlocks") - parent.innerHTML = "" + async function getEnvironment() { + const res = await resc.getEnvironment() + if (res.code != 200) { + notifError(res.message) + return + } - for (var k in hostsBlocks) { - if (!hostsBlocks.hasOwnProperty(k)) { - continue; - } + let env = res.data + renderHostsBlocks(env.HostsBlocks) + } - let hostsBlock = hostsBlocks[k] - let item = document.createElement("div") - item.classList.add("item") - item.innerHTML = ` + function onCheckHostblock(key, val) { + resc.env.HostsBlocks[key].IsEnabled = val + } + + async function blockdUpdate(name) { + const res = await resc.BlockdUpdate(name) + if (res.code != 200) { + notifError("blockdUpdate: ", res.message) + return + } + + resc.env.HostsBlocks[name] = res.data + + notifInfo("The hosts blocks has been updated.") + + renderHostsBlocks(resc.env.HostsBlocks) + } + + async function updateHostsBlocks() { + const res = await resc.updateHostsBlocks(resc.env.HostsBlocks) + if (res.code != 200) { + notifError("updateHostsBlocks: ", res.message) + return + } + + renderHostsBlocks(res.data) + notifInfo("The hosts blocks has been updated.") + } + + function renderHostsBlocks(hostsBlocks) { + let parent = document.getElementById("HostsBlocks") + parent.innerHTML = "" + + for (var k in hostsBlocks) { + if (!hostsBlocks.hasOwnProperty(k)) { + continue; + } + + let hostsBlock = hostsBlocks[k] + let item = document.createElement("div") + item.classList.add("item") + item.innerHTML = ` <span class="is-enabled"> <input type="checkbox" @@ -155,9 +166,10 @@ <div> Last updated at ${hostsBlock.LastUpdated} </div> </span>` - parent.appendChild(item) - } - } - </script> - </body> + parent.appendChild(item) + } + } + </script> +</body> + </html> diff --git a/_www/doc/CHANGELOG.adoc b/_www/doc/CHANGELOG.adoc new file mode 120000 index 0000000..dbe8cbb --- /dev/null +++ b/_www/doc/CHANGELOG.adoc @@ -0,0 +1 @@ +../../CHANGELOG.adoc
\ No newline at end of file diff --git a/_www/doc/README.adoc b/_www/doc/README.adoc new file mode 120000 index 0000000..35c2551 --- /dev/null +++ b/_www/doc/README.adoc @@ -0,0 +1 @@ +../../README.adoc
\ No newline at end of file diff --git a/_www/doc/benchmark.adoc b/_www/doc/benchmark.adoc new file mode 100644 index 0000000..baf856c --- /dev/null +++ b/_www/doc/benchmark.adoc @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later += Benchmark + +Commit: e670b34 +Build: normal + +Config options, + +---- +dir.hosts=/etc/rescached/hosts.d +dir.master=/etc/rescached/master.d +debug = 0 +---- + +== resolverbench + +Result of benchmarking with local blocked host file, + +---- +master ms 0 % ./resolverbench 127.0.0.1:53 scripts/hosts.block += Benchmarking with 27367 messages += Total: 27367 += Failed: 0 += Elapsed time: 1.053238347s +---- + +== dnstrace + +Result of benchmarking with 10000 query and 100 concurrent connections, + +---- +master ms 0 % dnstrace --recurse --codes --io-errors -s 127.0.0.1:53 -t A -n 10000 -c 100 redsift.io +Benchmarking 127.0.0.1:53 via udp with 100 conncurrent requests + +Total requests: 1000000 of 1000000 (100.0%) +DNS success codes: 1000000 + +DNS response codes + NOERROR: 1000000 + +Time taken for tests: 10.318186376s +Questions per second: 96916.3 + +DNS timings, 1000000 datapoints + min: 0s + mean: 1.017194ms + [+/-sd]: 770.525µs + max: 39.845887ms + +DNS distribution, 1000000 datapoints + LATENCY | | COUNT ++-------------+---------------------------------------------+--------+ + 131.071µs | | 1722 + 393.215µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 115890 + 655.359µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 185089 + 917.503µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 316551 + 1.179647ms | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 300305 + 1.441791ms | ▄▄▄▄ | 31218 + 1.703935ms | ▄▄ | 12005 + 1.966079ms | ▄ | 6387 + 2.228223ms | ▄ | 5007 + 2.490367ms | | 3196 + 2.752511ms | | 2573 + 3.014655ms | | 2486 + 3.276799ms | | 2012 + 3.538943ms | | 1814 + 3.801087ms | | 1806 + 4.063231ms | | 1512 + 4.325375ms | | 1099 + 4.587519ms | | 1077 + 4.849663ms | | 785 + 5.111807ms | | 759 + 5.373951ms | | 901 + 5.636095ms | | 765 + 5.898239ms | | 874 + 6.160383ms | | 654 + 6.422527ms | | 476 + 6.684671ms | | 351 + 6.946815ms | | 294 + 7.208959ms | | 245 + 7.471103ms | | 292 + 7.733247ms | | 261 + 7.995391ms | | 255 + 8.257535ms | | 132 + 8.650751ms | | 396 + 9.175039ms | | 193 + 9.699327ms | | 78 + 10.223615ms | | 51 + 10.747903ms | | 102 + 11.272191ms | | 23 + 11.796479ms | | 0 + 12.320767ms | | 0 + 12.845055ms | | 0 + 13.369343ms | | 0 + 13.893631ms | | 0 + 14.417919ms | | 0 + 14.942207ms | | 0 + 15.466495ms | | 0 + 15.990783ms | | 0 + 16.515071ms | | 0 + 17.301503ms | | 0 + 18.350079ms | | 0 + 19.398655ms | | 192 + 20.447231ms | | 112 + 21.495807ms | | 0 + 22.544383ms | | 0 + 23.592959ms | | 0 + 24.641535ms | | 12 + 25.690111ms | | 28 + 26.738687ms | | 14 + 27.787263ms | | 5 + 28.835839ms | | 0 + 29.884415ms | | 0 + 30.932991ms | | 0 + 31.981567ms | | 0 + 33.030143ms | | 0 + 34.603007ms | | 0 + 36.700159ms | | 0 + 38.797311ms | | 1 +---- + +== Credits + +- https://github.com/redsift/dnstrace[dnstrace] diff --git a/_www/doc/html.tmpl b/_www/doc/html.tmpl new file mode 100644 index 0000000..4eea130 --- /dev/null +++ b/_www/doc/html.tmpl @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- SPDX-FileCopyrightText: 2022 M. Shulhan <ms@kilabit.info --> +<!-- SPDX-License-Identifier: GPL-3.0-or-later --> +<html lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | doc</title> +</head> + +<body> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/block.d/"> block.d </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/"> zone.d </a> + / + <a href="/doc/" class="active"> Documentation </a> + </nav> + + <div class="page"> + <div class="container">{{.Body}}</div> + </div> +</body> + +</html> diff --git a/_www/doc/images/Screenshot_wui_environment.png b/_www/doc/images/Screenshot_wui_environment.png Binary files differnew file mode 100644 index 0000000..0e8c60d --- /dev/null +++ b/_www/doc/images/Screenshot_wui_environment.png diff --git a/_www/doc/images/Screenshot_wui_frontpage.png b/_www/doc/images/Screenshot_wui_frontpage.png Binary files differnew file mode 100644 index 0000000..0ee949b --- /dev/null +++ b/_www/doc/images/Screenshot_wui_frontpage.png diff --git a/_www/doc/images/Screenshot_wui_hosts_blocks.png b/_www/doc/images/Screenshot_wui_hosts_blocks.png Binary files differnew file mode 100644 index 0000000..7eaebe2 --- /dev/null +++ b/_www/doc/images/Screenshot_wui_hosts_blocks.png diff --git a/_www/doc/images/Screenshot_wui_hosts_d.png b/_www/doc/images/Screenshot_wui_hosts_d.png Binary files differnew file mode 100644 index 0000000..b096fa0 --- /dev/null +++ b/_www/doc/images/Screenshot_wui_hosts_d.png diff --git a/_www/doc/images/Screenshot_wui_zone_d.png b/_www/doc/images/Screenshot_wui_zone_d.png Binary files differnew file mode 100644 index 0000000..52e7913 --- /dev/null +++ b/_www/doc/images/Screenshot_wui_zone_d.png diff --git a/_www/doc/index.adoc b/_www/doc/index.adoc new file mode 100644 index 0000000..3c9ff89 --- /dev/null +++ b/_www/doc/index.adoc @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2022 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later += rescached documentation +Shulhan <ms@kilabit.info> + +link:CHANGELOG.html[CHANGELOG]:: Log for each release. + +link:README.html[rescached]:: Manual page for rescached program. + +link:rescached.cfg.html[rescached.cfg]:: Manual page for rescached +configuration. + +link:resolver.html[resolver]:: Manual page for resolver. + +link:benchmark.html[Benchmark]:: The latest benchmark of rescached server. + + +[#todo] +== TODO + +* zone.d rr add - check for duplicate value. + +* Prioritize the order of hosts file to be loaded: +** block.d +** hosts.d +** zone.d +** /etc/hosts + +* Generate unique ID for each RR in caches/zone for deletion. + +* Move repository to sr.ht + +* Implement DNS type 65 + +* Implement DNSSec diff --git a/_www/doc/rescached.cfg.adoc b/_www/doc/rescached.cfg.adoc new file mode 100644 index 0000000..2e19cac --- /dev/null +++ b/_www/doc/rescached.cfg.adoc @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later += RESCACHED.CONF(5) +:doctype: manpage +:man source: rescached.cfg +:man version: 2020.05.10 +:man manual: rescached.cfg + + +== NAME + +rescached.cfg - Configuration for rescached service + + +== SYNOPSIS + +/etc/rescached/rescached.cfg + + +== DESCRIPTION + +These file configure the behaviour of *rescached*(1) service. +This section will explain more about each option and how they effect +`rescached`. + +The configuration is using INI format where each options is grouped by header +in square bracket: + +* `[rescached]` +* `[dns "server"]` + + +== OPTIONS + +=== [rescached] + +This group of options contain the main configuration that related to +rescached. + +[#wui-listen] +==== wui.listen + +Format:: [host]:port +Default:: 127.0.0.1:5380 +Description:: The address to listen for web user interface. + +[#file-resolvconf] +==== file.resolvconf + +Format:: /any/path/to/file +Default:: /etc/rescached/resolv.conf +Description:: A path to dynamically generated *resolv.conf*(5) by +*resolvconf*(8). ++ +-- +If set, the nameserver values in referenced file will be used as "parent" name +server if no "parent" is defined in configuration file. + +To use this config, you must set either "dnsmasq_resolv", "pdnsd_resolv", or +"unbound_conf" in "/etc/resolvconf.conf" to point to +"/etc/rescached/resolv.conf". + +For example, +---- +resolv_conf=/etc/resolv.conf +name_servers=127.0.0.1 +dnsmasq_resolv=/etc/rescached/resolv.conf +#pdnsd_resolv=/etc/rescached/resolv.conf +#unbound_conf=/etc/rescached/resolv.conf +---- +-- + +[#debug] +==== debug + +Value:: +0::: log startup and errors. +1::: log startup, errors, request, response, caches, and exit status. +Format:: Number (0 or 1). +Default:: 0 +Description:: This option only used for debugging program or if user want to +monitor what kind of traffic goes in and out of rescached. + +[#dns_server] +=== [dns "server"] + +This group of options related to DNS server. + +[#parent] +==== parent + +Format:: + +---- +parent = "parent = " [ scheme "://"] ( ip-address / domain-name ) [ ":" port ] +scheme = ( "udp" / "https") +---- + +Default:: +* Address: udp://1.1.1.1 +* Port: 53 +Description:: List of parent DNS servers. ++ +When `rescached` receive a query from client (for example, your browser) and +when it does not have a cached answer for that query, it will forward the +query to one of the parent name servers. ++ +Using UDP as parent scheme, will automatically assume that the server also +capable of handling query in TCP. +This is required when client (for example, your browser) re-send the query +after receiving truncated UDP answer. +Any query received by `rescached` through TCP will forwarded to the parent +name server as TCP too, using the same address and port defined in one of UDP +parent. ++ +Please, do not use OpenDNS server. +If certain host-name not found (i.e. typo in host-name), OpenDNS will reply +with its own address, instead of replying with empty answer. +This will make `rescached` caching a false data and it may make your +application open or consume unintended resources. ++ +To check if your parent server reply the unknown host-name with no answer, use +*resolver*(1) tool. + +Example:: +---- +## Using UDP connection to forward request to parent name server. +parent = udp://1.1.1.1 + +## Using DNS over TLS to forward request to parent name server. +parent = https://1.1.1.1 + +## Using DNS over HTTPS to forward request to parent name server. +parent = https://kilabit.info/dns-query +---- + +[#listen] +==== listen + +Format:: <IP-ADDRESS>:<PORT> +Default:: 127.0.0.1:53 +Description:: Address in local network where `rescached` will listening for +query from client. ++ +If you want rescached to serve a query from another host in your local +network, change this value to `0.0.0.0:53`. + +[#http-port] +==== http.port + +Format:: Number +Default:: 443 +Description:: Port to serve DNS over HTTP. + +[#tls-port] +==== tls.port + +Format:: Number +Default:: 853 +Description:: Port to serve DNS over TLS. + +[#tls-certificate] +==== tls.certificate + +Format:: /path/to/file +Default:: (empty) +Description:: Path to certificate file to serve DNS over TLS and HTTPS. + + +[#tls-private_key] +==== tls.private_key + +Format:: /path/to/file +Default:: (empty) +Description:: Path to certificate private key file to serve DNS over TLS and +HTTPS. + +[#tls-allow_insecure] +==== tls.allow_insecure + +Format:: true | false +Default:: false +Description:: If its true, allow serving DoH and DoT with self-signed +certificate. + +[#doh-behind_proxy] +==== doh.behind_proxy + +Format:: true | false +Default:: false +Description:: If its true, serve DNS over HTTP only, even if +certificate files is defined. +This allow serving DNS request forwarded by another proxy server. + +[#cache-prune_delay] +==== cache.prune_delay + +Format:: Duration with time unit. Valid time units are "s", "m", "h". +Default:: 1h +Description:: Delay for pruning caches. ++ +Every N seconds/minutes/hours, rescached will traverse all +caches and remove response that has not been accessed less than +`cache.prune_threshold`. +Its value must be equal or greater than 1 hour (3600 seconds). + +[#cache-prune_threshold] +==== cache.prune_threshold + +Format:: Duration with time unit. Valid time units are "s", "m", "h". +Default:: -1h +Description:: The duration when the cache will be considered expired. +Its value must be negative and greater or equal than -1 hour (-3600 seconds). + +== FILES + +[#hosts-d] +=== /etc/rescached/hosts.d + +Path to hosts directory where rescached will load all hosts formatted files. + + +[#zone-d] +=== /etc/rescached/zone.d + +Path to zone directory where rescached will load all zone files. + + +== EXAMPLE + +Simple rescached configuration using dnscrypt-proxy that listen on port 54 as +parent resolver, with prune delay set to 60 seconds and threshold also to 60 +seconds. + +---- +[dns "server"] +parent=udp://127.0.0.1:54 +cache.prune_delay=60s +cache.prune_threshold=60s +---- + +Save the above script into `rescached.cfg` and run it, + + $ sudo rescached -config rescached.cfg + + +== AUTHOR + +`rescached` is developed by M. Shulhan (m.shulhan@gmail.com). + + +== LICENSE + +Copyright 2018, M. Shulhan (m.shulhan@gmail.com). +All rights reserved. + +Use of this source code is governed by a GPL-3.0 license that can be +found in the COPYING file. + + +== SEE ALSO + +*rescached*(1) diff --git a/_www/doc/resolver.adoc b/_www/doc/resolver.adoc new file mode 100644 index 0000000..de34a25 --- /dev/null +++ b/_www/doc/resolver.adoc @@ -0,0 +1,460 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later += RESOLVER(1) +:doctype: manpage +:man source: resolver +:man version: 2022.04.15 +:man manual: resolver + + +== NAME + +resolver - command line interface for DNS and rescached server. + + +== SYNOPSIS + +resolver [-insecure] [-ns nameserver] [-server] <command> [args...] + + +== DESCRIPTION + +resolver is a tool to resolve hostname to IP address or to query services +on hostname by type (MX, SOA, TXT, etc.) using standard DNS protocol with UDP, +DNS over TLS (DoT), or DNS over HTTPS (DoH). + + +== OPTIONS + +The following options affect the commands operation. + +`-insecure`:: ++ +-- +Ignore invalid server certificate when querying DoT, DoH, or rescached server. +-- + +`-ns <nameserver>`:: ++ +-- +This option define the parent DNS server where the resolver send the query. +Default to one of "nameserver" in `/etc/resolv.conf`. + +The nameserver is defined in the following format, + + ("udp"/"tcp"/"https") "://" (domain / ip-address) [":" port] + +Examples, + +* udp://194.233.68.184:53 for querying with UDP, +* tcp://194.233.68.184:53 for querying with TCP, +* https://194.233.68.184:853 for querying with DNS over TLS (DoT), and +* https://kilabit.info/dns-query for querying with DNS over HTTPS (DoH). +-- + +`-server <rescached-URL>`:: ++ +-- +Set the rescached HTTP server where commands will send. +The rescached-URL use HTTP scheme: + + ("http" / "https") "://" (domain / ip-address) [":" port] + +Default to "https://127.0.0.1:5380" if its empty. +-- + +== COMMANDS + +=== QUERY + +query <domain / ip-address> [type] [class]:: ++ +-- +Query the domain or IP address with optional type and/or class. + +Unless the option "-ns" is given, the query command will use the +nameserver defined in the system resolv.conf file. + +The "type" parameter define DNS record type to be queried. +List of valid types, + +* A (1) - a host Address (default) +* NS (2) - an authoritative Name Server +* CNAME (5) - the Canonical NAME for an alias +* SOA (6) - marks the Start of a zone of Authority +* MB (7) - a MailBox domain name +* MG (8) - a Mail Group member +* MR (9) - a Mail Rename domain name +* NULL (10) - a null resource record +* WKS (11) - a Well Known Service description +* PTR (12) - a domain name PoinTeR +* HINFO (13) - Host INFOrmation +* MINFO (14) - mailbox or mail list information +* MX (15) - Mail Exchange +* TXT (16) - TeXT strings +* AAAA (28) - a host address in IPv6 +* SRV (33) - a SerViCe record + +The "class" parameter is optional, its either IN (default), CS, or HS. +-- + + +=== MANAGING BLOCK.D + +block.d disable <name>:: ++ +-- +Disable specific hosts on block.d. +-- + +block.d enable <name>:: ++ +-- +Enable specific hosts on block.d. +-- + +block.d update <name>:: ++ +-- +Fetch the latest hosts file from remote block.d URL defined by +its name. +On success, the hosts file will be updated and the server will be +restarted. +-- + + +=== MANAGING CACHES + +caches:: ++ +-- +Fetch and print all caches from rescached server. +-- + + +caches search <string>:: ++ +-- +Search the domain name in rescached caches. +This command can also be used to inspect each DNS message on the caches. +-- + +caches remove <string>:: ++ +-- +Remove the domain name from rescached caches. +If the parameter is "all", it will remove all caches. +-- + + +=== MANAGING ENVIRONMENT + +env:: ++ +-- +Fetch the current server environment and print it as JSON format to stdout. +-- + +env update <path-to-file / "-">:: ++ +-- +Update the server environment from JSON formatted file. +If the argument is "-", the new environment is read from stdin. +If the environment is valid, the server will be restarted. +-- + + +=== MANAGING HOSTS.D + +hosts.d create <name>:: ++ +-- +Create new hosts file inside the hosts.d directory with specific file +name. +-- + +hosts.d delete <name>:: ++ +-- +Delete hosts file inside the hosts.d directory by file name. +-- + +hosts.d get <name>:: ++ +-- +Get the content of hosts file inside the hosts.d directory by file name. +-- + + +=== MANAGING RECORD IN HOSTS.D + +hosts.d rr add <name> <domain> <value>:: ++ +-- +Insert a new record and save it to the hosts file identified by +"name". +If the domain name already exists, the new record will be appended +instead of replaced. +-- + +hosts.d rr delete <name> <domain>:: ++ +-- +Delete record from hosts file "name" by domain name. +-- + + +=== MANAGING ZONE.D + +`zone.d`:: ++ +Fetch and print all zones in the server, including their SOA. + +zone.d create <name>:: ++ +Create new zone file inside the zone.d directory. + +zone.d delete <name>:: ++ +Delete zone file inside the zone.d directory. + + +=== MANAGING RECORD IN ZONE.D + +`zone.d rr get <zone>`:: + +Get and print all records in the zone. + +zone.d rr add <zone> <"@" | subdomain> <ttl> <type> <class> <value> ...:: ++ +-- +Add new record into the zone file. + +The domain name can be set to origin using "@" or empty string, subdomain +(without ending with "."), or fully qualified domain name (end with "."). + +If ttl is set to 0, it will default to 604800 (7 days). + +List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. + +List of valid class are IN, CS, HS. + +The value parameter can be more than one, for example, the MX record +we pass two parameters: + + <pref> <exchange> + +See the example below for more information. +-- + +`zone.d rr delete <zone> <"@" | subdomain> <type> <class> <value>`:: ++ +-- +Delete record from zone by its subdomain, type, class, and value. +-- + + +== EXIT STATUS + +Upon exit and success +resolver+ will return 0, or 1 otherwise. + + +== EXAMPLES + +=== QUERY + +Query the IPv4 address for kilabit.info, + + $ resolver query kilabit.info + +Query the mail exchange (MX) for domain kilabit.info, + + $ resolver query kilabit.info MX + +Query the IPv4 address for kilabit.info using 127.0.0.1 at port 53 as +name server, + + $ resolver -ns=udp://127.0.0.1:53 query kilabit.info + +Query the IPv4 address of domain name "kilabit.info" using DNS over TLS at +name server 194.233.68.184, + + $ resolver -insecure -ns=https://194.233.68.184 query kilabit.info + +Query the IPv4 records of domain name "kilabit.info" using DNS over HTTPS on +name server kilabit.info, + + $ resolver -ns=https://kilabit.info/dns-query query kilabit.info + +Inspect the rescached's caches on server at http://127.0.0.1:5380, + + $ resolver -server=http://127.0.0.1:5380 caches + + +=== MANAGING CACHES + +Search caches that contains "bit" on the domain name, + + $ resolver caches search bit + +Remove caches that contains domain name "kilabit.info", + + $ resolver caches remove kilabit.info + +Remove all caches in the server, + + $ resolver caches remove all + + +=== MANAGING ENVIRONMENT + +Fetch and print current server environment, + + $ resolver env + +Update the server environment from JSON file in /tmp/env.json, + + $ resolver env update /tmp/env.json + +Update the server environment by reading JSON from standard input, + + $ cat /tmp/env.json | resolver env update - + + +=== MANAGING HOSTS.D + +Create new hosts file named "myhosts" inside the hosts.d directory, + + $ resolver hosts.d create myhosts + OK + +Delete hosts file named "myhosts" inside the hosts.d directory, + + $ resolver hosts.d delete myhosts + OK + +Get the content of hosts file named "myhosts" inside the hosts.d directory, + + $ resolver hosts.d get myhosts + [ + { + "Value": "127.0.0.1", + "Name": "localhost", + "Type": 1, + "Class": 1, + "TTL": 604800 + }, + { + "Value": "::1", + "Name": "localhost", + "Type": 28, + "Class": 1, + "TTL": 604800 + } + ] + +=== MANAGING RECORD IN HOSTS.D + +Add new record "127.0.0.1 my.hosts" to hosts file named "hosts", + + $ resolver hosts.d rr add hosts my.hosts 127.0.0.1 + { + "Value": "127.0.0.1", + "Name": "my.hosts", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + +Delete record "my.hosts" from hosts file "hosts", + + $ resolver hosts.d rr delete hosts my.hosts + { + "Value": "127.0.0.1", + "Name": "my.hosts", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + +=== MANAGING ZONE.D + +Print all zone in the server, + + $ resolver zone.d + my.zone + SOA: {MName:my.zone RName: Serial:0 Refresh:0 Retry:0 Expire:0 Minimum:0} + + +=== MANAGING RECORD IN ZONE.D + +Assume that we have create zone "my.zone". + +Get all records in the zone "my.zone", + + $ resolver zone.d rr get my.zone + my.zone + 604800 MX IN map[Exchange:mail.my.zone Preference:10] + 604800 A IN 127.0.0.2 + 604800 A IN 127.0.0.3 + www.my.zone + 604800 A IN 192.168.1.2 + +Add IPv4 address "127.0.0.1" for domain my.zone, + + $ resolver zone.d rr add my.zone @ 0 A IN 127.0.0.1 + +or + + $ resolver zone.d rr add my.zone "" 0 A IN 127.0.0.1 + { + "Value": "127.0.0.1", + "Name": "my.zone", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + +and to delete the above record, + + $ resolver zone.d rr delete my.zone @ A IN 127.0.0.1 + OK + +Add subdomain "www" with IPv4 address "192.168.1.2" to zone "my.zone", + + $ resolver zone.d rr add my.zone www 0 A IN 192.168.1.2 + { + "Value": "192.168.1.2", + "Name": "www.my.zone", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + +and to delete the above record, + + $ resolver zone.d rr delete my.zone www A IN 192.168.1.2 + OK + +== AUTHOR + +This software is developed by M. Shulhan (ms@kilabit.info). + + +== LICENSE + +Copyright 2018, M. Shulhan (ms@kilabit.info). +All rights reserved. + +Use of this source code is governed by a GPL 3.0 license that can be +found in the COPYING file. + + +== LINKS + +Source code repository: https://github.com/shuLhan/rescached-go + + +== SEE ALSO + +*rescached*(1), *rescached.cfg*(5) diff --git a/_www/environment/index.html b/_www/environment/index.html index 9702d44..6698df4 100644 --- a/_www/environment/index.html +++ b/_www/environment/index.html @@ -2,333 +2,306 @@ <!-- SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info --> <!-- SPDX-License-Identifier: GPL-3.0-or-later --> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <link rel="icon" type="image/png" href="/favicon.png" /> - <link rel="stylesheet" href="/index.css" /> - <title>rescached | Environment</title> - <style> - .input > label { - width: 11em; - display: inline-block; - } - .input > input, - .input > select { - width: calc(100% - 13em); - display: inline-block; - } - .input { - margin-top: 1em; - } - .input-deletable { - width: 100%; - } - .input-deletable > input { - max-width: calc(100% - 6em); - } - .input-deletable > button { - width: 5em; - } - .input-checkbox { - width: calc(100% - 13em); - display: inline-block; - } - .input-checkbox input[type="checkbox"] { - width: auto; - } - .section-bottom { - margin: 2em 0px; - } - </style> - </head> - <body onload="onLoad()"> - <nav class="menu"> - <a href="/"> rescached </a> - / - <a href="/environment/" class="active"> Environment </a> - / - <a href="/block.d/"> block.d </a> - / - <a href="/hosts.d/"> hosts.d </a> - / - <a href="/zone.d/"> zone.d </a> - </nav> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | Environment</title> - <div id="notif"></div> + <style> + .input>label { + width: 11em; + display: inline-block; + } - <div class="environment"> - <p> - This page allow you to change the rescached environment. Upon save, the rescached service - will be restarted. - </p> + .input>input, + .input>select { + width: calc(100% - 13em); + display: inline-block; + } - <h3>rescached</h3> + .input { + margin-top: 1em; + } - <div class="input"> - <label for="FileResolvConf"> System resolv.conf </label> - <input id="FileResolvConf" oninput="onInput('FileResolvConf', this.value)" /> - <span class="input-info-toggler" onclick="toggleInfo('FileResolvConf_info')">?</span> - <div id="FileResolvConf_info" class="input-info" style="display: none"> - A path to dynamically generated resolv.conf(5) by resolvconf(8). If set, the - nameserver values in referenced file will replace 'parent' value and 'parent' will - become a fallback in case the referenced file being deleted or can not be parsed. - </div> - </div> + .input-deletable { + width: 100%; + } - <div class="input"> - <label for="Debug"> Debug level </label> - <input id="Debug" type="number" min="0" max="3" oninput="onInput('Debug', this.value)" /> - <span class="input-info-toggler" onclick="toggleInfo('Debug_info')">?</span> - <div id="Debug_info" class="input-info" style="display: none"> - This option only used for debugging program or if user want to monitor what kind of - traffic goes in and out of rescached. - </div> - </div> + .input-deletable>input { + max-width: calc(100% - 6em); + } - <h3>DNS server</h3> + .input-deletable>button { + width: 5em; + } - <div class="input"> - <label for="nameservers"> Parent name servers </label> - <span class="input-info-toggler" onclick="toggleInfo('nameservers_info')">?</span> - <div id="nameservers_info" class="input-info" style="display: none"> - List of parent DNS servers. - </div> - <div id="nameservers"></div> - <button onclick="handleAddNameserver()">Add</button> - </div> + .input-checkbox { + width: calc(100% - 13em); + display: inline-block; + } - <div class="input"> - <label for="ListenAddress"> Listen address </label> - <input id="ListenAddress" oninput="onInput('ListenAddress', this.value)" /> - <span class="input-info-toggler" onclick="toggleInfo('ListenAddress_info')">?</span> - <div id="ListenAddress_info" class="input-info" style="display: none"> - Address in local network where rescached will listening for query from client - through UDP and TCP. - <br /> - If you want rescached to serve a query from another host in your local network, - change this value to <tt>0.0.0.0:53</tt>. - </div> - </div> + .input-checkbox input[type="checkbox"] { + width: auto; + } - <div class="input"> - <label for="HTTPPort"> HTTP listen port </label> - <input - id="HTTPPort" - type="number" - min="0" - max="65535" - oninput="onInput('HTTPPort', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('HTTPPort_info')">?</span> - <div id="HTTPPort_info" class="input-info" style="display: none"> - Port to serve DNS over HTTP - </div> - </div> + .section-bottom { + margin: 2em 0px; + } + </style> +</head> - <div class="input"> - <label for="TLSPort"> TLS listen port </label> - <input - id="TLSPort" - type="number" - min="0" - max="65535" - oninput="onInput('TLSPort', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('TLSPort_info')">?</span> - <div id="TLSPort_info" class="input-info" style="display: none"> - Port to serve DNS over TLS - </div> - </div> +<body onload="onLoad()"> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/" class="active"> Environment </a> + / + <a href="/block.d/"> block.d </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/"> zone.d </a> + / + <a href="/doc/"> Documentation </a> + </nav> - <div class="input"> - <label for="TLSCertFile"> TLS certificate </label> - <input - id="TLSCertFile" - placeholder="/path/to/certificate" - oninput="onInput('TLSCertFile', this.value)" - /> - <span class="input-info-toggler" onclick="toggleInfo('TLSCertFile_info')">?</span> - <div id="TLSCertFile_info" class="input-info" style="display: none"> - Path to certificate file to serve DNS over TLS and HTTPS - </div> - </div> + <div id="notif"></div> - <div class="input"> - <label for="TLSPrivateKey"> TLS private key </label> - <input - id="TLSPrivateKey" - placeholder="/path/to/certificate/private.key" - oninput="onInput('TLSPrivateKey', this.value)" - /> - <span class="input-info-toggler" onclick="toggleInfo('TLSPrivateKey_info')">?</span> - <div id="TLSPrivateKey_info" class="input-info" style="display: none"> - Path to certificate private key file to serve DNS over TLS and HTTPS. - </div> - </div> + <div class="environment"> + <p> + This page allow you to change the rescached environment. Upon save, the rescached service + will be restarted. + </p> - <div class="input"> - <label for="TLSAllowInsecure"> TLS allow insecure </label> - <div class="input-checkbox"> - <input - id="TLSAllowInsecure" - type="checkbox" - oninput="onInput('TLSAllowInsecure', this.checked)" - /> - <span class="suffix"> Yes </span> - </div> - <span class="input-info-toggler" onclick="toggleInfo('TLSAllowInsecure_info')">?</span> - <div id="TLSAllowInsecure_info" class="input-info" style="display: none"> - If its true, allow serving DoH and DoT with self signed certificate. - </div> - </div> + <h3>rescached</h3> - <div class="input"> - <label for="DoHBehindProxy"> DoH behind proxy </label> - <div class="input-checkbox"> - <input - id="DoHBehindProxy" - type="checkbox" - oninput="onInput('DoHBehindProxy', this.checked)" - /> - <span class="suffix"> Yes </span> - </div> - <span class="input-info-toggler" onclick="toggleInfo('DoHBehindProxy_info')">?</span> - <div id="DoHBehindProxy_info" class="input-info" style="display: none"> - If its true, serve DNS over HTTP only, even if certificate files is defined. This - allow serving DNS request forwarded by another proxy server. - </div> - </div> + <div class="input"> + <label for="FileResolvConf"> System resolv.conf </label> + <input id="FileResolvConf" oninput="onInput('FileResolvConf', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('FileResolvConf_info')">?</span> + <div id="FileResolvConf_info" class="input-info" style="display: none"> + A path to dynamically generated resolv.conf(5) by resolvconf(8). If set, the + nameserver values in referenced file will replace 'parent' value and 'parent' will + become a fallback in case the referenced file being deleted or can not be parsed. + </div> + </div> - <div class="input"> - <label for="PruneDelay"> Prune delay </label> - <input - id="PruneDelay" - type="number" - min="3600" - max="36000" - oninput="onInput('PruneDelay', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('PruneDelay_info')">?</span> - <div id="PruneDelay_info" class="input-info" style="display: none"> - Delay for pruning caches. Every N seconds, rescached will traverse all caches and - remove response that has not been accessed less than cache.prune_threshold. Its - value must be equal or greater than 1 hour (3600 seconds). - </div> - </div> + <div class="input"> + <label for="Debug"> Debug level </label> + <input id="Debug" type="number" min="0" max="3" oninput="onInput('Debug', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('Debug_info')">?</span> + <div id="Debug_info" class="input-info" style="display: none"> + This option only used for debugging program or if user want to monitor what kind of + traffic goes in and out of rescached. + </div> + </div> - <div class="input"> - <label for="PruneThreshold"> Prune threshold </label> - <input - id="PruneThreshold" - type="number" - min="-36000" - max="-3600" - oninput="onInput('PruneThreshold', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('PruneThreshold_info')">?</span> - <div id="PruneThreshold_info" class="input-info" style="display: none"> - The duration when the cache will be considered expired. Its value must be negative - and greater or equal than -1 hour (-3600 seconds). - </div> - </div> + <h3>DNS server</h3> - <div class="section-bottom"> - <div> - <button onclick="handleSave()">Save</button> - </div> - </div> - </div> + <div class="input"> + <label for="nameservers"> Parent name servers </label> + <span class="input-info-toggler" onclick="toggleInfo('nameservers_info')">?</span> + <div id="nameservers_info" class="input-info" style="display: none"> + List of parent DNS servers. + </div> + <div id="nameservers"></div> + <button onclick="handleAddNameserver()">Add</button> + </div> - <script src="/index.js"></script> - <script src="/rescached.js"></script> - <script> - let resc = null + <div class="input"> + <label for="ListenAddress"> Listen address </label> + <input id="ListenAddress" oninput="onInput('ListenAddress', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('ListenAddress_info')">?</span> + <div id="ListenAddress_info" class="input-info" style="display: none"> + Address in local network where rescached will listening for query from client + through UDP and TCP. + <br /> + If you want rescached to serve a query from another host in your local network, + change this value to <tt>0.0.0.0:53</tt>. + </div> + </div> - async function getEnvironment() { - const res = await resc.getEnvironment() - if (res.code != 200) { - notifError(res.message) - return - } - const env = res.data + <div class="input"> + <label for="HTTPPort"> HTTP listen port </label> + <input id="HTTPPort" type="number" min="0" max="65535" oninput="onInput('HTTPPort', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('HTTPPort_info')">?</span> + <div id="HTTPPort_info" class="input-info" style="display: none"> + Port to serve DNS over HTTP + </div> + </div> - // Set all input values using env data. - document.getElementById("FileResolvConf").value = env.FileResolvConf - document.getElementById("Debug").value = env.Debug + <div class="input"> + <label for="TLSPort"> TLS listen port </label> + <input id="TLSPort" type="number" min="0" max="65535" oninput="onInput('TLSPort', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('TLSPort_info')">?</span> + <div id="TLSPort_info" class="input-info" style="display: none"> + Port to serve DNS over TLS + </div> + </div> - renderNameservers() + <div class="input"> + <label for="TLSCertFile"> TLS certificate </label> + <input id="TLSCertFile" placeholder="/path/to/certificate" oninput="onInput('TLSCertFile', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('TLSCertFile_info')">?</span> + <div id="TLSCertFile_info" class="input-info" style="display: none"> + Path to certificate file to serve DNS over TLS and HTTPS + </div> + </div> - document.getElementById("ListenAddress").value = env.ListenAddress - document.getElementById("HTTPPort").value = env.HTTPPort - document.getElementById("TLSPort").value = env.TLSPort - document.getElementById("TLSCertFile").value = env.TLSCertFile - document.getElementById("TLSPrivateKey").value = env.TLSPrivateKey + <div class="input"> + <label for="TLSPrivateKey"> TLS private key </label> + <input id="TLSPrivateKey" placeholder="/path/to/certificate/private.key" oninput="onInput('TLSPrivateKey', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('TLSPrivateKey_info')">?</span> + <div id="TLSPrivateKey_info" class="input-info" style="display: none"> + Path to certificate private key file to serve DNS over TLS and HTTPS. + </div> + </div> - document.getElementById("TLSAllowInsecure").checked = env.TLSAllowInsecure - document.getElementById("DoHBehindProxy").checked = env.DoHBehindProxy + <div class="input"> + <label for="TLSAllowInsecure"> TLS allow insecure </label> + <div class="input-checkbox"> + <input id="TLSAllowInsecure" type="checkbox" oninput="onInput('TLSAllowInsecure', this.checked)" /> + <span class="suffix"> Yes </span> + </div> + <span class="input-info-toggler" onclick="toggleInfo('TLSAllowInsecure_info')">?</span> + <div id="TLSAllowInsecure_info" class="input-info" style="display: none"> + If its true, allow serving DoH and DoT with self signed certificate. + </div> + </div> - document.getElementById("PruneDelay").value = env.PruneDelay - document.getElementById("PruneThreshold").value = env.PruneThreshold - } + <div class="input"> + <label for="DoHBehindProxy"> DoH behind proxy </label> + <div class="input-checkbox"> + <input id="DoHBehindProxy" type="checkbox" oninput="onInput('DoHBehindProxy', this.checked)" /> + <span class="suffix"> Yes </span> + </div> + <span class="input-info-toggler" onclick="toggleInfo('DoHBehindProxy_info')">?</span> + <div id="DoHBehindProxy_info" class="input-info" style="display: none"> + If its true, serve DNS over HTTP only, even if certificate files is defined. This + allow serving DNS request forwarded by another proxy server. + </div> + </div> - function handleAddNameserver() { - resc.env.NameServers.push("") - renderNameservers() - } + <div class="input"> + <label for="PruneDelay"> Prune delay </label> + <input id="PruneDelay" type="number" min="3600" max="36000" oninput="onInput('PruneDelay', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('PruneDelay_info')">?</span> + <div id="PruneDelay_info" class="input-info" style="display: none"> + Delay for pruning caches. Every N seconds, rescached will traverse all caches and + remove response that has not been accessed less than cache.prune_threshold. Its + value must be equal or greater than 1 hour (3600 seconds). + </div> + </div> - async function handleSave() { - console.log("handleSave: ", resc.env) - let res = await resc.updateEnvironment() - if (res.code !== 200) { - notifError(res.message) - return - } - notifInfo("Environment has been saved!") - } + <div class="input"> + <label for="PruneThreshold"> Prune threshold </label> + <input id="PruneThreshold" type="number" min="-36000" max="-3600" oninput="onInput('PruneThreshold', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('PruneThreshold_info')">?</span> + <div id="PruneThreshold_info" class="input-info" style="display: none"> + The duration when the cache will be considered expired. Its value must be negative + and greater or equal than -1 hour (-3600 seconds). + </div> + </div> - function onDeleteNameserver(x) { - resc.env.NameServers.splice(x, 1) - renderNameservers() - } + <div class="section-bottom"> + <div> + <button onclick="handleSave()">Save</button> + </div> + </div> + </div> - function onInput(key, value) { - console.log("onInput ", key, ": ", value) - resc.env[key] = value - } + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null - function onInputNameserver(x, newv) { - resc.env.NameServers[x] = newv - } + async function getEnvironment() { + const res = await resc.getEnvironment() + if (res.code != 200) { + notifError(res.message) + return + } + const env = res.data - function onLoad() { - resc = new Rescached("") - getEnvironment() - } + // Set all input values using env data. + document.getElementById("FileResolvConf").value = env.FileResolvConf + document.getElementById("Debug").value = env.Debug - function renderNameservers() { - let listNS = document.getElementById("nameservers") - listNS.innerHTML = "" + renderNameservers() - if (resc.env.NameServers === null) { - resc.env.NameServers = [] - return - } + document.getElementById("ListenAddress").value = env.ListenAddress + document.getElementById("HTTPPort").value = env.HTTPPort + document.getElementById("TLSPort").value = env.TLSPort + document.getElementById("TLSCertFile").value = env.TLSCertFile + document.getElementById("TLSPrivateKey").value = env.TLSPrivateKey - for (let x = 0; x < resc.env.NameServers.length; x++) { - let ns = resc.env.NameServers[x] + document.getElementById("TLSAllowInsecure").checked = env.TLSAllowInsecure + document.getElementById("DoHBehindProxy").checked = env.DoHBehindProxy - let el = document.createElement("div") - el.classList.add("input-deletable") - el.innerHTML = ` + document.getElementById("PruneDelay").value = env.PruneDelay + document.getElementById("PruneThreshold").value = env.PruneThreshold + } + + function handleAddNameserver() { + resc.env.NameServers.push("") + renderNameservers() + } + + async function handleSave() { + console.log("handleSave: ", resc.env) + let res = await resc.updateEnvironment() + if (res.code !== 200) { + notifError(res.message) + return + } + notifInfo("Environment has been saved!") + } + + function onDeleteNameserver(x) { + resc.env.NameServers.splice(x, 1) + renderNameservers() + } + + function onInput(key, value) { + console.log("onInput ", key, ": ", value) + resc.env[key] = value + } + + function onInputNameserver(x, newv) { + resc.env.NameServers[x] = newv + } + + function onLoad() { + resc = new Rescached("") + getEnvironment() + } + + function renderNameservers() { + let listNS = document.getElementById("nameservers") + listNS.innerHTML = "" + + if (resc.env.NameServers === null) { + resc.env.NameServers = [] + return + } + + for (let x = 0; x < resc.env.NameServers.length; x++) { + let ns = resc.env.NameServers[x] + + let el = document.createElement("div") + el.classList.add("input-deletable") + el.innerHTML = ` <input value="${ns}" oninput="onInputNameserver(${x}, this.value)"> <button onclick="onDeleteNameserver(${x})"> Delete </button>` - listNS.appendChild(el) - } - } - </script> - </body> + listNS.appendChild(el) + } + } + </script> +</body> + </html> diff --git a/_www/hosts.d/index.html b/_www/hosts.d/index.html index 6709801..c8f4b41 100644 --- a/_www/hosts.d/index.html +++ b/_www/hosts.d/index.html @@ -2,195 +2,205 @@ <!-- SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info --> <!-- SPDX-License-Identifier: GPL-3.0-or-later --> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <link rel="icon" type="image/png" href="/favicon.png" /> - <link rel="stylesheet" href="/index.css" /> - <title>rescached | hosts.d</title> - <style> - .nav-left { - padding: 0px; - width: 16em; - float: left; - } - .nav-left .item { - margin: 4px 0px; - } - #activeHostsFile { - float: left; - width: calc(100% - 17em); - } - .host { - font-family: monospace; - width: 100%; - } - .host.header { - margin: 1em 0px; - font-weight: bold; - border-bottom: 1px solid silver; - } - .host_name { - display: inline-block; - width: 18em; - word-wrap: break-word; - } - .host_value { - display: inline-block; - width: 10em; - } - </style> - </head> - <body onload="onLoad()"> - <nav class="menu"> - <a href="/"> rescached </a> - / - <a href="/environment/"> Environment </a> - / - <a href="/block.d/"> block.d </a> - / - <a href="/hosts.d/" class="active"> hosts.d </a> - / - <a href="/zone.d/"> zone.d </a> - </nav> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | hosts.d</title> - <div id="notif"></div> + <style> + .nav-left { + padding: 0px; + width: 16em; + float: left; + } - <div class="hosts_d"> - <div class="nav-left"> - <h3>Hosts files</h3> - <div id="HostsFiles"></div> + .nav-left .item { + margin: 4px 0px; + } - <br /> + #activeHostsFile { + float: left; + width: calc(100% - 17em); + } - <label> - <span>New hosts file:</span> - <br /> - <input id="newHostsFile" oninput="onInputNewHostsFile(this.value)" /> - </label> - <button onclick="onCreateHostsFile()">Create</button> - </div> + .host { + font-family: monospace; + width: 100%; + } - <div id="activeHostsFile"> - <p>Select one of the hosts file to manage.</p> - </div> - </div> + .host.header { + margin: 1em 0px; + font-weight: bold; + border-bottom: 1px solid silver; + } - <script src="/index.js"></script> - <script src="/rescached.js"></script> - <script> - let resc = null - let activeHostsFile = null - let newHostsFile = "" - let newRecord = { - Name: "", - Value: "", - } + .host_name { + display: inline-block; + width: 18em; + word-wrap: break-word; + } - async function getHostsFile(name) { - activeHostsFile = resc.env.HostsFiles[name] - if (typeof activeHostsFile.Records === "undefined") { - activeHostsFile.Records = [] - } - if (activeHostsFile.Records === null) { - activeHostsFile.Records = [] - } - if (activeHostsFile.Records.length === 0) { - const res = await resc.HostsFileGet(name) - activeHostsFile.Records = res.data - } - renderHostsFile(activeHostsFile) - newRecord.Name = "" - newRecord.Value = "" - } + .host_value { + display: inline-block; + width: 10em; + } + </style> +</head> - async function onCreateHostsFile() { - if (newHostsFile === "") { - notifError("Please fill the hosts file name first") - return - } +<body onload="onLoad()"> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/block.d/"> block.d </a> + / + <a href="/hosts.d/" class="active"> hosts.d </a> + / + <a href="/zone.d/"> zone.d </a> + / + <a href="/doc/"> Documentation </a> + </nav> - let res = await resc.HostsFileCreate(newHostsFile) + <div id="notif"></div> - if (res.code >= 400) { - notifError("ERROR: HostsFileCreate: " + res.message) - return - } - renderHostsFiles(resc.env.HostsFiles) - notifInfo(res.message) - resetInputs() - } + <div class="hosts_d"> + <div class="nav-left"> + <h3>Hosts files</h3> + <div id="HostsFiles"></div> - async function onDeleteActiveHostsFile() { - const res = await resc.HostsFileDelete(activeHostsFile.Name) - if (res.code != 200) { - notifError(res.message) - return - } - renderHostsFiles(resc.env.HostsFiles) - document.getElementById( - "activeHostsFile", - ).innerHTML = `<p>Select one of the hosts file to manage.</p>` - notifInfo(`Hosts file "${activeHostsFile.Name}" has been deleted`) - activeHostsFile = null - } + <br /> - async function onCreateRecord(hostsFile) { - if (hostsFile === "") { - notifError("invalid or empty hosts file name: " + hostsFile) - return - } - if (newRecord.Name === "") { - notifError("invalid or empty domain name") - return - } - if (newRecord.Value === "") { - notifError("invalid or empty IP address") - return - } - let res = await resc.HostsdRecordAdd(hostsFile, newRecord.Name, newRecord.Value) - if (res.code >= 400) { - notifError("failed to add record for " + hostsFile + ": " + res.message) - return - } - renderNewRecord(res.data) - } + <label> + <span>New hosts file:</span> + <br /> + <input id="newHostsFile" oninput="onInputNewHostsFile(this.value)" /> + </label> + <button onclick="onCreateHostsFile()">Create</button> + </div> - async function onDeleteRecord(domain) { - let res = await resc.HostsdRecordDelete(activeHostsFile.Name, domain) - if (res.code !== 200) { - notifError("Failed to delete record " + domain) - return - } - activeHostsFile = resc.env.HostsFiles[activeHostsFile.Name] - renderHostsFile(activeHostsFile) - } + <div id="activeHostsFile"> + <p>Select one of the hosts file to manage.</p> + </div> + </div> - function onInputNewHostsFile(v) { - newHostsFile = v - } + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null + let activeHostsFile = null + let newHostsFile = "" + let newRecord = { + Name: "", + Value: "", + } - function onInputNewRecord(k, v) { - newRecord[k] = v - } + async function getHostsFile(name) { + activeHostsFile = resc.env.HostsFiles[name] + if (typeof activeHostsFile.Records === "undefined") { + activeHostsFile.Records = [] + } + if (activeHostsFile.Records === null) { + activeHostsFile.Records = [] + } + if (activeHostsFile.Records.length === 0) { + const res = await resc.HostsFileGet(name) + activeHostsFile.Records = res.data + } + renderHostsFile(activeHostsFile) + newRecord.Name = "" + newRecord.Value = "" + } - async function onLoad() { - resc = new Rescached("") + async function onCreateHostsFile() { + if (newHostsFile === "") { + notifError("Please fill the hosts file name first") + return + } - let res = await resc.getEnvironment() - if (res.code != 200) { - notifError(res.message) - return - } + let res = await resc.HostsFileCreate(newHostsFile) - renderHostsFiles(res.data.HostsFiles) - resetInputs() - } + if (res.code >= 400) { + notifError("ERROR: HostsFileCreate: " + res.message) + return + } + renderHostsFiles(resc.env.HostsFiles) + notifInfo(res.message) + resetInputs() + } - function renderHostsFile(hf) { - let content = document.getElementById("activeHostsFile") - let innerHTML = ` + async function onDeleteActiveHostsFile() { + const res = await resc.HostsFileDelete(activeHostsFile.Name) + if (res.code != 200) { + notifError(res.message) + return + } + renderHostsFiles(resc.env.HostsFiles) + document.getElementById( + "activeHostsFile", + ).innerHTML = `<p>Select one of the hosts file to manage.</p>` + notifInfo(`Hosts file "${activeHostsFile.Name}" has been deleted`) + activeHostsFile = null + } + + async function onCreateRecord(hostsFile) { + if (hostsFile === "") { + notifError("invalid or empty hosts file name: " + hostsFile) + return + } + if (newRecord.Name === "") { + notifError("invalid or empty domain name") + return + } + if (newRecord.Value === "") { + notifError("invalid or empty IP address") + return + } + let res = await resc.HostsdRecordAdd(hostsFile, newRecord.Name, newRecord.Value) + if (res.code >= 400) { + notifError("failed to add record for " + hostsFile + ": " + res.message) + return + } + renderNewRecord(res.data) + } + + async function onDeleteRecord(domain) { + let res = await resc.HostsdRecordDelete(activeHostsFile.Name, domain) + if (res.code !== 200) { + notifError("Failed to delete record " + domain) + return + } + activeHostsFile = resc.env.HostsFiles[activeHostsFile.Name] + renderHostsFile(activeHostsFile) + } + + function onInputNewHostsFile(v) { + newHostsFile = v + } + + function onInputNewRecord(k, v) { + newRecord[k] = v + } + + async function onLoad() { + resc = new Rescached("") + + let res = await resc.getEnvironment() + if (res.code != 200) { + notifError(res.message) + return + } + + renderHostsFiles(res.data.HostsFiles) + resetInputs() + } + + function renderHostsFile(hf) { + let content = document.getElementById("activeHostsFile") + let innerHTML = ` <p> ${hf.Name} (${hf.Records.length} records) <button onclick="onDeleteActiveHostsFile()">Delete</button> @@ -210,51 +220,52 @@ </div> <div id="records"> ` - for (let x = 0; x < hf.Records.length; x++) { - let rr = hf.Records[x] - innerHTML += ` + for (let x = 0; x < hf.Records.length; x++) { + let rr = hf.Records[x] + innerHTML += ` <div class="host"> <span class="host_name"> ${rr.Name} </span> <span class="host_value"> ${rr.Value} </span> <button onclick="onDeleteRecord('${rr.Name}')">X</button> </div>` - } - innerHTML += "</div>" - content.innerHTML = innerHTML - } + } + innerHTML += "</div>" + content.innerHTML = innerHTML + } - function renderHostsFiles(hostsFiles) { - let parent = document.getElementById("HostsFiles") - parent.innerHTML = "" + function renderHostsFiles(hostsFiles) { + let parent = document.getElementById("HostsFiles") + parent.innerHTML = "" - for (let k in hostsFiles) { - if (!hostsFiles.hasOwnProperty(k)) { - continue - } - let hf = hostsFiles[k] - let item = document.createElement("div") - item.classList.add("item") - item.innerHTML = `<a href="#" onclick="getHostsFile('${k}')"> ${hf.Name} </a>` - parent.appendChild(item) - } - } + for (let k in hostsFiles) { + if (!hostsFiles.hasOwnProperty(k)) { + continue + } + let hf = hostsFiles[k] + let item = document.createElement("div") + item.classList.add("item") + item.innerHTML = `<a href="#" onclick="getHostsFile('${k}')"> ${hf.Name} </a>` + parent.appendChild(item) + } + } - // renderNewRecord prepend the new record on top of the list. - function renderNewRecord(rr) { - let div = document.getElementById("records") - innerHTML = ` + // renderNewRecord prepend the new record on top of the list. + function renderNewRecord(rr) { + let div = document.getElementById("records") + innerHTML = ` <div class="host"> <span class="host_name"> ${rr.Name} </span> <span class="host_value"> ${rr.Value} </span> <button onclick="onDeleteRecord('${rr.Name}')">X</button> </div>` - div.innerHTML = innerHTML + div.innerHTML - } + div.innerHTML = innerHTML + div.innerHTML + } + + function resetInputs() { + document.getElementById("newHostsFile").value = "" + newHostsFile = "" + } + </script> +</body> - function resetInputs() { - document.getElementById("newHostsFile").value = "" - newHostsFile = "" - } - </script> - </body> </html> diff --git a/_www/index.css b/_www/index.css index 150c58e..b336c71 100644 --- a/_www/index.css +++ b/_www/index.css @@ -3,140 +3,145 @@ html, body { - position: relative; - width: 100%; - height: 100%; + position: relative; + width: 100%; + height: 100%; } body { - background-color: floralwhite; - color: #333; - margin: 0; - padding: 8px; - box-sizing: border-box; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + background-color: floralwhite; + color: #333; + margin: 0; + padding: 8px; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } a { - color: rgb(0, 100, 200); - text-decoration: none; + color: rgb(0, 100, 200); + text-decoration: none; } a:hover { - text-decoration: underline; + text-decoration: underline; } a:visited { - color: rgb(0, 80, 160); + color: rgb(0, 80, 160); } input, button, select, textarea { - font-family: inherit; - font-size: inherit; - padding: 0.4em; - margin: 0.5em 0; - box-sizing: border-box; - border: 1px solid #ccc; - border-radius: 2px; + font-family: inherit; + font-size: inherit; + padding: 0.4em; + margin: 0.5em 0; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 2px; } input:disabled { - color: #ccc; + color: #ccc; } input[type="range"] { - height: 0; + height: 0; } button { - color: #333; - background-color: lavender; - outline: none; + color: #333; + background-color: lavender; + outline: none; } button:disabled { - color: #999; + color: #999; } button:not(:disabled):active { - background-color: #ddd; + background-color: #ddd; } button:focus { - border-color: #666; + border-color: #666; } h1, h2 { - color: #ff3e00; - text-transform: uppercase; - font-weight: 100; + color: #ff3e00; + text-transform: uppercase; + font-weight: 200; } body { - margin: 0 auto; - width: 800px; - padding: 1em; + margin: 0 auto; + width: 800px; + padding: 1em; } #notif { - position: fixed; - top: 1em; - width: 70%; + position: fixed; + top: 1em; + width: 70%; } -#notif > .error { - background-color: salmon; - padding: 1em; + +#notif>.error { + background-color: salmon; + padding: 1em; } -#notif > .info { - background-color: lightblue; - padding: 1em; + +#notif>.info { + background-color: lightblue; + padding: 1em; } nav.menu { - color: #ff3e00; - text-transform: uppercase; - font-weight: 100; - margin-bottom: 2em; + color: #ff3e00; + text-transform: uppercase; + margin-bottom: 2em; } + .active { - padding-bottom: 4px; - border-bottom: 4px solid #ff3e00; + padding-bottom: 4px; + border-bottom: 4px solid #ff3e00; } -.input > label { - width: 8em; - display: inline-block; +.input>label { + width: 8em; + display: inline-block; } -.input > input, -.input > select { - width: calc(100% - 11em); - display: inline-block; + +.input>input, +.input>select { + width: calc(100% - 11em); + display: inline-block; } -.input > .input-info-toggler { - border-radius: 50%; - border: 1px solid grey; - cursor: pointer; - display: inline-block; - font-size: 12px; - height: 14px; - line-height: 14px; - padding: 2px; - text-align: center; - width: 14px; + +.input>.input-info-toggler { + border-radius: 50%; + border: 1px solid grey; + cursor: pointer; + display: inline-block; + font-size: 12px; + height: 14px; + line-height: 14px; + padding: 2px; + text-align: center; + width: 14px; } -.input > .input-info { - background-color: #eee; - margin: 8px 0px; - padding: 1em; + +.input>.input-info { + background-color: #eee; + margin: 8px 0px; + padding: 1em; } @media (max-width: 900px) { - body { - width: calc(100% - 2em); - } + body { + width: calc(100% - 2em); + } } diff --git a/_www/index.html b/_www/index.html index 41b0d4b..55653f8 100644 --- a/_www/index.html +++ b/_www/index.html @@ -2,167 +2,183 @@ <!-- SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info --> <!-- SPDX-License-Identifier: GPL-3.0-or-later --> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <link rel="icon" type="image/png" href="/favicon.png" /> - <link rel="stylesheet" href="/index.css" /> - <title>rescached</title> - <style> - .message { - padding: 1em 0px; - border-bottom: 1px solid silver; - } - #summary { - margin: 1em 0; - } - #caches { - height: 20em; - overflow: auto; - font-family: monospace; - } - .RType { - width: 3em; - display: inline-block; - } - .rr { - border-bottom: 1px dashed silver; - margin-left: 1em; - width: 100%; - } - .rr.header { - font-weight: bold; - } - .rr span { - display: inline-block; - } - .kind { - width: 9em; - } - .type { - width: 5em; - } - .ttl { - width: 6em; - } - .value { - word-wrap: anywhere; - width: calc(100% - 24em); - } - </style> - </head> - <body onload="main()"> - <nav class="menu"> - <a href="/" class="active"> rescached </a> - / - <a href="/environment/"> Environment </a> - / - <a href="/block.d/"> block.d </a> - / - <a href="/hosts.d/"> hosts.d </a> - / - <a href="/zone.d/"> zone.d </a> - </nav> - <form id="form_search"> - <div class="search"> - Caches: - <input name="query" /> - <button onclick="doSearch()">Search</button> - <button onclick="doClearResult()">Clear result</button> - </div> - </form> - <div id="result"></div> - <div id="notif"></div> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached</title> - <div id="summary"></div> - <div id="caches"></div> + <style> + .message { + padding: 1em 0px; + border-bottom: 1px solid silver; + } - <script src="/index.js"></script> - <script src="/rescached.js"></script> - <script> - let resc = null - let cachePoller = null - let dateFmt = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - }) - let searchResults = [] + #summary { + margin: 1em 0; + } - async function main() { - resc = new Rescached("") - const res = await resc.Caches() - if (res.code != 200) { - notifError(`doSearch ${query}: ${res.message}`) - return - } - renderCaches(res.data) - cachePoller = setInterval(pollCaches, 10000) + #caches { + height: 20em; + overflow: auto; + font-family: monospace; + } - // Catch the enter key on input text search. - document.getElementById("form_search").addEventListener("submit", (e) => { - e.preventDefault() - }) - } + .RType { + width: 3em; + display: inline-block; + } - async function pollCaches() { - const res = await resc.Caches() - if (res.code != 200) { - notifError(`doSearch ${query}: ${res.message}`) - return - } - renderCaches(res.data) - } + .rr { + border-bottom: 1px dashed silver; + margin-left: 1em; + width: 100%; + } - async function doCacheRemove(idx, qname) { - const res = await resc.CacheRemove(qname) - if (res.code != 200) { - notifError(`doCacheRemove ${qname}: ${res.message}`) - return - } + .rr.header { + font-weight: bold; + } - notifInfo(`Record ${qname} has been removed from cache`) + .rr span { + display: inline-block; + } - // Remove the record from search result and re-render it. - searchResults.splice(idx, 1) - onSearchResult(searchResults) - } + .kind { + width: 9em; + } - async function doSearch() { - const query = document.getElementsByName("query")[0].value - console.log("doSearch: ", query) - const res = await resc.Search(query) - if (res.code != 200) { - notifError(`doSearch ${query}: ${res.message}`) - return - } - searchResults = res.data - onSearchResult(res.data) - } + .type { + width: 5em; + } - async function doClearResult() { - searchResults = [] - document.getElementById("result").innerHTML = "" - } + .ttl { + width: 6em; + } - function onSearchResult(dnsRecords) { - const elResult = document.getElementById("result") - elResult.innerHTML = "" + .value { + word-wrap: anywhere; + width: calc(100% - 24em); + } + </style> +</head> - if (dnsRecords.length === 0) { - elResult.innerHTML = "<div>No matches record found.</div>" - return - } +<body onload="main()"> + <nav class="menu"> + <a href="/" class="active"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/block.d/"> block.d </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/"> zone.d </a> + / + <a href="/doc/"> Documentation </a> + </nav> + <form id="form_search"> + <div class="search"> + Caches: + <input name="query" /> + <button onclick="doSearch()">Search</button> + <button onclick="doClearResult()">Clear result</button> + </div> + </form> + <div id="result"></div> + <div id="notif"></div> - for (let x = 0; x < dnsRecords.length; x++) { - const record = dnsRecords[x] - const divRecord = document.createElement("div") - divRecord.classList.add("message") - innerHTML = ` + <div id="summary"></div> + <div id="caches"></div> + + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null; + let cachePoller = null; + let dateFmt = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }); + let searchResults = []; + + async function main() { + resc = new Rescached(""); + const res = await resc.Caches(); + if (res.code != 200) { + notifError(`doSearch ${query}: ${res.message}`); + return; + } + renderCaches(res.data); + cachePoller = setInterval(pollCaches, 10000); + + // Catch the enter key on input text search. + document + .getElementById("form_search") + .addEventListener("submit", (e) => { + e.preventDefault(); + }); + } + + async function pollCaches() { + const res = await resc.Caches(); + if (res.code != 200) { + notifError(`doSearch ${query}: ${res.message}`); + return; + } + renderCaches(res.data); + } + + async function doCacheRemove(idx, qname) { + const res = await resc.CacheRemove(qname); + if (res.code != 200) { + notifError(`doCacheRemove ${qname}: ${res.message}`); + return; + } + + notifInfo(`Record ${qname} has been removed from cache`); + + // Remove the record from search result and re-render it. + searchResults.splice(idx, 1); + onSearchResult(searchResults); + } + + async function doSearch() { + const query = document.getElementsByName("query")[0].value; + console.log("doSearch: ", query); + const res = await resc.Search(query); + if (res.code != 200) { + notifError(`doSearch ${query}: ${res.message}`); + return; + } + searchResults = res.data; + onSearchResult(res.data); + } + + async function doClearResult() { + searchResults = []; + document.getElementById("result").innerHTML = ""; + } + + function onSearchResult(dnsRecords) { + const elResult = document.getElementById("result"); + elResult.innerHTML = ""; + + if (dnsRecords.length === 0) { + elResult.innerHTML = "<div>No matches record found.</div>"; + return; + } + + for (let x = 0; x < dnsRecords.length; x++) { + const record = dnsRecords[x]; + const divRecord = document.createElement("div"); + divRecord.classList.add("message"); + innerHTML = ` <div class="qname"> ${record.Question.Name} <button class="b-remove" @@ -175,57 +191,60 @@ <span class="ttl"> TTL </span> <span class="value"> Value </span> </div> - ` - if (record.Answer !== null && record.Answer.length > 0) { - innerHTML += renderRR(record.Answer, "Answer") - } - if (record.Authority !== null && record.Authority.length > 0) { - innerHTML += renderRR(record.Authority, "Authority") - } - if (record.Additional !== null && record.Additional.length > 0) { - innerHTML += renderRR(record.Additional, "Additional") - } + `; + if (record.Answer !== null && record.Answer.length > 0) { + innerHTML += renderRR(record.Answer, "Answer"); + } + if (record.Authority !== null && record.Authority.length > 0) { + innerHTML += renderRR(record.Authority, "Authority"); + } + if (record.Additional !== null && record.Additional.length > 0) { + innerHTML += renderRR(record.Additional, "Additional"); + } - divRecord.innerHTML = innerHTML - elResult.appendChild(divRecord) - } - } + divRecord.innerHTML = innerHTML; + elResult.appendChild(divRecord); + } + } - function renderCaches(answers) { - document.getElementById("summary").innerHTML = ` + function renderCaches(answers) { + document.getElementById("summary").innerHTML = ` Total caches: ${answers.length} - ` - let w = document.getElementById("caches") - let out = ` - ` - for (let x = answers.length - 1; x >= 0; x--) { - let answer = answers[x] - out += ` + `; + let w = document.getElementById("caches"); + let out = ` + `; + for (let x = answers.length - 1; x >= 0; x--) { + let answer = answers[x]; + out += ` <div class="cache"> - <span class="AccessedAt">${dateFmt.format(new Date(answer.AccessedAt * 1000))}</span> + <span class="AccessedAt">${dateFmt.format( + new Date(answer.AccessedAt * 1000) + )}</span> <span class="RType">${resc.GetRRTypeName(answer.RType)}</span> <span class="QName">${answer.QName}</span> - </div>` - } - w.innerHTML = out - } + </div>`; + } + w.innerHTML = out; + } - function renderRR(listRR, title) { - let innerHTML = "" + function renderRR(listRR, title) { + let innerHTML = ""; - for (let x = 0; x < listRR.length; x++) { - const rr = listRR[x] - innerHTML += ` + for (let x = 0; x < listRR.length; x++) { + const rr = listRR[x]; + innerHTML += ` <div class="rr"> <span class="kind"> ${title} </span> <span class="type"> ${getRRTypeName(rr.Type)} </span> <span class="ttl"> ${rr.TTL} </span> <span class="value"> ${JSON.stringify(rr.Value, null, 2)} </span> </div> - ` - } - return innerHTML - } - </script> - </body> + `; + } + return innerHTML; + } + </script> +</body> + </html> diff --git a/_www/index.js b/_www/index.js index f11cbbe..cb51265 100644 --- a/_www/index.js +++ b/_www/index.js @@ -2,30 +2,30 @@ // SPDX-License-Identifier: GPL-3.0-or-later function notifError(msg) { - displayNotif("error", msg) + displayNotif("error", msg); } function notifInfo(msg) { - displayNotif("info", msg) + displayNotif("info", msg); } function displayNotif(className, msg) { - let notif = document.getElementById("notif") - let el = document.createElement("div") - el.classList.add(className) - el.innerHTML = msg - notif.appendChild(el) + let notif = document.getElementById("notif"); + let el = document.createElement("div"); + el.classList.add(className); + el.innerHTML = msg; + notif.appendChild(el); - setTimeout(function () { - notif.removeChild(notif.children[0]) - }, 5000) + setTimeout(function () { + notif.removeChild(notif.children[0]); + }, 5000); } function toggleInfo(id) { - let el = document.getElementById(id) - if (el.style.display === "none") { - el.style.display = "block" - } else { - el.style.display = "none" - } + let el = document.getElementById(id); + if (el.style.display === "none") { + el.style.display = "block"; + } else { + el.style.display = "none"; + } } diff --git a/_www/rescached.js b/_www/rescached.js index 4c30486..d0bc20c 100644 --- a/_www/rescached.js +++ b/_www/rescached.js @@ -2,356 +2,346 @@ // SPDX-License-Identifier: GPL-3.0-or-later const RRTypes = { - 1: "A", - 2: "NS", - 3: "MD", - 4: "MF", - 5: "CNAME", - 6: "SOA", - 7: "MB", - 8: "MG", - 9: "MR", - 10: "NULL", - 11: "WKS", - 12: "PTR", - 13: "HINFO", - 14: "MINFO", - 15: "MX", - 16: "TXT", - 28: "AAAA", - 33: "SRV", - 41: "OPT", -} + 1: "A", + 2: "NS", + 3: "MD", + 4: "MF", + 5: "CNAME", + 6: "SOA", + 7: "MB", + 8: "MG", + 9: "MR", + 10: "NULL", + 11: "WKS", + 12: "PTR", + 13: "HINFO", + 14: "MINFO", + 15: "MX", + 16: "TXT", + 28: "AAAA", + 33: "SRV", + 41: "OPT", +}; -const contentTypeForm = "application/x-www-form-urlencoded" -const contentTypeJson = "application/json" +const contentTypeForm = "application/x-www-form-urlencoded"; +const contentTypeJson = "application/json"; -const paramNameName = "name" +const paramNameName = "name"; -const headerContentType = "Content-Type" +const headerContentType = "Content-Type"; function getRRTypeName(k) { - let v = RRTypes[k] - if (v === "") { - return k - } - return v + let v = RRTypes[k]; + if (v === "") { + return k; + } + return v; } class Rescached { - static nanoSeconds = 1000000000 - static apiBlockd = "/api/block.d" - static apiBlockdUpdate = "/api/block.d/update" - static apiCaches = "/api/caches" - static apiCachesSearch = "/api/caches/search" - static apiHostsd = "/api/hosts.d" - static apiHostsdRR = "/api/hosts.d/rr" - static apiZoned = "/api/zone.d" - static apiZonedRR = "/api/zone.d/rr" + static nanoSeconds = 1000000000; + static apiBlockd = "/api/block.d"; + static apiBlockdUpdate = "/api/block.d/update"; + static apiCaches = "/api/caches"; + static apiCachesSearch = "/api/caches/search"; + static apiHostsd = "/api/hosts.d"; + static apiHostsdRR = "/api/hosts.d/rr"; + static apiZoned = "/api/zone.d"; + static apiZonedRR = "/api/zone.d/rr"; - constructor(server) { - this.server = server - this.env = {} - } + constructor(server) { + this.server = server; + this.env = {}; + } - async BlockdUpdate(name) { - let params = new URLSearchParams() - params.set("name", name) + async BlockdUpdate(name) { + let params = new URLSearchParams(); + params.set("name", name); - const httpRes = await fetch(Rescached.apiBlockdUpdate, { - method: "POST", - headers: { - [headerContentType]: contentTypeForm, - }, - body: params.toString(), - }) - return await httpRes.json() - } + const httpRes = await fetch(Rescached.apiBlockdUpdate, { + method: "POST", + headers: { + [headerContentType]: contentTypeForm, + }, + body: params.toString(), + }); + return await httpRes.json(); + } - async Caches() { - const res = await fetch(this.server + Rescached.apiCaches, { - headers: { - Connection: "keep-alive", - }, - }) - return await res.json() - } + async Caches() { + const res = await fetch(this.server + Rescached.apiCaches, { + headers: { + Connection: "keep-alive", + }, + }); + return await res.json(); + } - async CacheRemove(qname) { - const res = await fetch( - this.server + Rescached.apiCaches + "?name=" + qname, - { - method: "DELETE", - }, - ) - return await res.json() - } + async CacheRemove(qname) { + const res = await fetch( + this.server + Rescached.apiCaches + "?name=" + qname, + { + method: "DELETE", + } + ); + return await res.json(); + } - async Search(query) { - console.log("Search: ", query) - const res = await fetch( - this.server + - Rescached.apiCachesSearch + - "?query=" + - query, - ) - return await res.json() - } + async Search(query) { + console.log("Search: ", query); + const res = await fetch( + this.server + Rescached.apiCachesSearch + "?query=" + query + ); + return await res.json(); + } - async getEnvironment() { - const httpRes = await fetch(this.server + "/api/environment") - const res = await httpRes.json() + async getEnvironment() { + const httpRes = await fetch(this.server + "/api/environment"); + const res = await httpRes.json(); - if (httpRes.status === 200) { - res.data.PruneDelay = - res.data.PruneDelay / Rescached.nanoSeconds - res.data.PruneThreshold = - res.data.PruneThreshold / - Rescached.nanoSeconds + if (httpRes.status === 200) { + res.data.PruneDelay = res.data.PruneDelay / Rescached.nanoSeconds; + res.data.PruneThreshold = res.data.PruneThreshold / Rescached.nanoSeconds; - for (let k in res.data.HostsFiles) { - if (!res.data.HostsFiles.hasOwnProperty(k)) { - continue - } - let hf = res.data.HostsFiles[k] - if (typeof hf.Records === "undefined") { - hf.Records = [] - } - } - this.env = res.data - } - return res - } + for (let k in res.data.HostsFiles) { + if (!res.data.HostsFiles.hasOwnProperty(k)) { + continue; + } + let hf = res.data.HostsFiles[k]; + if (typeof hf.Records === "undefined") { + hf.Records = []; + } + } + this.env = res.data; + } + return res; + } - GetRRTypeName(k) { - let v = RRTypes[k] - if (v === "") { - return k - } - return v - } + GetRRTypeName(k) { + let v = RRTypes[k]; + if (v === "") { + return k; + } + return v; + } - async HostsFileCreate(name) { - var params = new URLSearchParams() - params.set(paramNameName, name) + async HostsFileCreate(name) { + var params = new URLSearchParams(); + params.set(paramNameName, name); - const httpRes = await fetch(Rescached.apiHostsd, { - method: "POST", - headers: { - [headerContentType]: contentTypeForm, - }, - body: params.toString(), - }) - let res = await httpRes.json() - if (res.code === 200) { - this.env.HostsFiles[name] = { - Name: name, - Records: [], - } - } - return res - } + const httpRes = await fetch(Rescached.apiHostsd, { + method: "POST", + headers: { + [headerContentType]: contentTypeForm, + }, + body: params.toString(), + }); + let res = await httpRes.json(); + if (res.code === 200) { + this.env.HostsFiles[name] = { + Name: name, + Records: [], + }; + } + return res; + } - async HostsFileDelete(name) { - var params = new URLSearchParams() - params.set(paramNameName, name) + async HostsFileDelete(name) { + var params = new URLSearchParams(); + params.set(paramNameName, name); - var url = Rescached.apiHostsd + "?" + params.toString() - const httpRes = await fetch(url, { - method: "DELETE", - }) - const res = await httpRes.json() - if (httpRes.status === 200) { - delete this.env.HostsFiles[name] - } - return res - } + var url = Rescached.apiHostsd + "?" + params.toString(); + const httpRes = await fetch(url, { + method: "DELETE", + }); + const res = await httpRes.json(); + if (httpRes.status === 200) { + delete this.env.HostsFiles[name]; + } + return res; + } - async HostsFileGet(name) { - var params = new URLSearchParams() - params.set(paramNameName, name) + async HostsFileGet(name) { + var params = new URLSearchParams(); + params.set(paramNameName, name); - var url = Rescached.apiHostsd + "?" + params.toString() - const httpRes = await fetch(url) + var url = Rescached.apiHostsd + "?" + params.toString(); + const httpRes = await fetch(url); - let res = await httpRes.json() - if (httpRes.Status === 200) { - this.env.HostsFiles[name] = { - Name: name, - Records: res.data, - } - } - return res - } + let res = await httpRes.json(); + if (httpRes.Status === 200) { + this.env.HostsFiles[name] = { + Name: name, + Records: res.data, + }; + } + return res; + } - async HostsdRecordAdd(hostsFile, domain, value) { - let params = new URLSearchParams() - params.set("name", hostsFile) - params.set("domain", domain) - params.set("value", value) + async HostsdRecordAdd(hostsFile, domain, value) { + let params = new URLSearchParams(); + params.set("name", hostsFile); + params.set("domain", domain); + params.set("value", value); - const httpRes = await fetch(Rescached.apiHostsdRR, { - method: "POST", - headers: { - [headerContentType]: contentTypeForm, - }, - body: params.toString(), - }) - const res = await httpRes.json() - if (httpRes.Status === 200) { - let hf = this.env.HostsFiles[hostsFile] - hf.Records.push(res.data) - } - return res - } + const httpRes = await fetch(Rescached.apiHostsdRR, { + method: "POST", + headers: { + [headerContentType]: contentTypeForm, + }, + body: params.toString(), + }); + const res = await httpRes.json(); + if (httpRes.Status === 200) { + let hf = this.env.HostsFiles[hostsFile]; + hf.Records.push(res.data); + } + return res; + } - async HostsdRecordDelete(hostsFile, domain) { - let params = new URLSearchParams() - params.set("name", hostsFile) - params.set("domain", domain) + async HostsdRecordDelete(hostsFile, domain) { + let params = new URLSearchParams(); + params.set("name", hostsFile); + params.set("domain", domain); - const api = Rescached.apiHostsdRR + "?" + params.toString() + const api = Rescached.apiHostsdRR + "?" + params.toString(); - const httpRes = await fetch(api, { - method: "DELETE", - }) - const res = await httpRes.json() - if (httpRes.Status === 200) { - let hf = this.env.HostsFiles[hostsFile] - for (let x = 0; x < hf.Records.length; x++) { - if (hf.Records[x].Name === domain) { - hf.Records.splice(x, 1) - } - } - } - return res - } + const httpRes = await fetch(api, { + method: "DELETE", + }); + const res = await httpRes.json(); + if (httpRes.Status === 200) { + let hf = this.env.HostsFiles[hostsFile]; + for (let x = 0; x < hf.Records.length; x++) { + if (hf.Records[x].Name === domain) { + hf.Records.splice(x, 1); + } + } + } + return res; + } - async updateEnvironment() { - let got = {} + async updateEnvironment() { + let got = {}; - Object.assign(got, this.env) + Object.assign(got, this.env); - got.PruneDelay = got.PruneDelay * this.nanoSeconds - got.PruneThreshold = got.PruneThreshold * this.nanoSeconds + got.PruneDelay = got.PruneDelay * this.nanoSeconds; + got.PruneThreshold = got.PruneThreshold * this.nanoSeconds; - const httpRes = await fetch( - this.server + "/api/environment", - { - method: "POST", - headers: { - [headerContentType]: contentTypeJson, - }, - body: JSON.stringify(got), - }, - ) + const httpRes = await fetch(this.server + "/api/environment", { + method: "POST", + headers: { + [headerContentType]: contentTypeJson, + }, + body: JSON.stringify(got), + }); - return await httpRes.json() - } + return await httpRes.json(); + } - async updateHostsBlocks(hostsBlocks) { - const httpRes = await fetch(Rescached.apiBlockd, { - method: "POST", - headers: { - [headerContentType]: contentTypeJson, - }, - body: JSON.stringify(hostsBlocks), - }) - return await httpRes.json() - } + async updateHostsBlocks(hostsBlocks) { + const httpRes = await fetch(Rescached.apiBlockd, { + method: "POST", + headers: { + [headerContentType]: contentTypeJson, + }, + body: JSON.stringify(hostsBlocks), + }); + return await httpRes.json(); + } - async ZoneFileCreate(name) { - let params = new URLSearchParams() - params.set(paramNameName, name) + async ZoneFileCreate(name) { + let params = new URLSearchParams(); + params.set(paramNameName, name); - const httpRes = await fetch(Rescached.apiZoned, { - method: "POST", - headers: { - [headerContentType]: contentTypeForm, - }, - body: params.toString(), - }) - let res = await httpRes.json() - if (res.code == 200) { - this.env.Zones[name] = res.data - } - return res - } + const httpRes = await fetch(Rescached.apiZoned, { + method: "POST", + headers: { + [headerContentType]: contentTypeForm, + }, + body: params.toString(), + }); + let res = await httpRes.json(); + if (res.code == 200) { + this.env.Zones[name] = res.data; + } + return res; + } - async ZoneFileDelete(name) { - let params = new URLSearchParams() - params.set(paramNameName, name) + async ZoneFileDelete(name) { + let params = new URLSearchParams(); + params.set(paramNameName, name); - let url = Rescached.apiZoned + "?" + params.toString() - const httpRes = await fetch(url, { - method: "DELETE", - }) - let res = await httpRes.json() - if (res.code == 200) { - delete this.env.Zones[name] - } - return res - } + let url = Rescached.apiZoned + "?" + params.toString(); + const httpRes = await fetch(url, { + method: "DELETE", + }); + let res = await httpRes.json(); + if (res.code == 200) { + delete this.env.Zones[name]; + } + return res; + } - // ZonedRecords fetch the RR on specific zone. - async ZonedRecords(name) { - let params = new URLSearchParams() - params.set(paramNameName, name) + // ZonedRecords fetch the RR on specific zone. + async ZonedRecords(name) { + let params = new URLSearchParams(); + params.set(paramNameName, name); - let url = Rescached.apiZonedRR + "?" + params.toString() - const httpRes = await fetch(url) - let res = await httpRes.json() - return res - } + let url = Rescached.apiZonedRR + "?" + params.toString(); + const httpRes = await fetch(url); + let res = await httpRes.json(); + return res; + } - async ZonedRecordAdd(name, rr) { - let req = { - name: name, - type: getRRTypeName(rr.Type), - record: btoa(JSON.stringify(rr)), - } + async ZonedRecordAdd(name, rr) { + let req = { + name: name, + type: getRRTypeName(rr.Type), + record: btoa(JSON.stringify(rr)), + }; - const httpRes = await fetch(Rescached.apiZonedRR, { - method: "POST", - headers: { - [headerContentType]: contentTypeJson, - }, - body: JSON.stringify(req), - }) + const httpRes = await fetch(Rescached.apiZonedRR, { + method: "POST", + headers: { + [headerContentType]: contentTypeJson, + }, + body: JSON.stringify(req), + }); - let res = await httpRes.json() - if (httpRes.status === 200) { - let zf = this.env.Zones[name] - if (rr.Type == 6) { - // SOA. - zf.SOA = res.data - } else { - let rr = res.data - if (zf.Records == null) { - zf.Records = {} - } - zf.Records[rr.Name].push(rr) - } - } - return res - } + let res = await httpRes.json(); + if (httpRes.status === 200) { + let zf = this.env.Zones[name]; + if (rr.Type == 6) { + zf.SOA = res.data; + } else { + let rr = res.data; + if (zf.Records == null) { + zf.Records = {}; + } + zf.Records[rr.Name].push(rr); + } + } + return res; + } - async ZonedRecordDelete(zone, rr) { - let params = new URLSearchParams() - params.set(paramNameName, zone) - params.set("type", getRRTypeName(rr.Type)) - params.set("record", btoa(JSON.stringify(rr))) + async ZonedRecordDelete(zone, rr) { + let params = new URLSearchParams(); + params.set(paramNameName, zone); + params.set("type", getRRTypeName(rr.Type)); + params.set("record", btoa(JSON.stringify(rr))); - let api = Rescached.apiZonedRR + "?" + params.toString() + let api = Rescached.apiZonedRR + "?" + params.toString(); - const httpRes = await fetch(api, { - method: "DELETE", - }) + const httpRes = await fetch(api, { + method: "DELETE", + }); - let res = await httpRes.json() - if (httpRes.status === 200) { - this.env.Zones[zone].Records = res.data - } - return res - } + let res = await httpRes.json(); + if (httpRes.status === 200) { + this.env.Zones[zone].Records = res.data; + } + return res; + } } diff --git a/_www/zone.d/index.html b/_www/zone.d/index.html index ec7068e..18483bf 100644 --- a/_www/zone.d/index.html +++ b/_www/zone.d/index.html @@ -2,430 +2,423 @@ <!-- SPDX-FileCopyrightText: 2021 M. Shulhan <ms@kilabit.info --> <!-- SPDX-License-Identifier: GPL-3.0-or-later --> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - <link rel="icon" type="image/png" href="/favicon.png" /> - <link rel="stylesheet" href="/index.css" /> - <title>rescached | zone.d</title> - <style> - h4 { - border-bottom: 1px solid silver; - } - .nav-left { - padding: 0; - width: 13em; - float: left; - } - .nav-left .item { - margin: 1em 0; - cursor: pointer; - color: rgb(0, 100, 200); - } - .nav-left > input { - width: 12em; - } - .content { - float: left; - width: calc(100% - 14em); - } - .action-delete { - margin-left: 1em; - } - div.actions { - padding: 1em; - } - div.actions button { - width: 100%; - } +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <link rel="icon" type="image/png" href="/favicon.png" /> + <link rel="stylesheet" href="/index.css" /> + <title>rescached | zone.d</title> - .rr_form { - margin: 1em 0px; - padding: 10px 10px 0px 10px; - border: 1px solid silver; - } - .rr_form > * > .input > input.name { - width: auto; - } + <style> + h4 { + border-bottom: 1px solid silver; + } - .rr { - font-family: monospace; - width: 100%; - } - .rr > * { - vertical-align: middle; - } - .rr.header { - font-weight: bold; - } - .rr > .name { - width: 12em; - display: inline-block; - word-wrap: break-word; - } - .rr > .type { - width: 4em; - display: inline-block; - } - .rr > .value { - display: inline-block; - width: 16em; - word-wrap: break-word; - } - .rr > .actions { - display: inline-block; - } - </style> - </head> - <body onload="main()"> - <nav class="menu"> - <a href="/"> rescached </a> - / - <a href="/environment/"> Environment </a> - / - <a href="/block.d/"> block.d </a> - / - <a href="/hosts.d/"> hosts.d </a> - / - <a href="/zone.d/" class="active"> zone.d </a> - </nav> - <div id="notif"></div> + .nav-left { + padding: 0; + width: 13em; + float: left; + } - <div class="nav-left"> - <h3>Zone files</h3> - <div id="Zones"></div> + .nav-left .item { + margin: 1em 0; + cursor: pointer; + color: rgb(0, 100, 200); + } - <label for="newZoneFile"> New zone file: </label> - <input id="newZoneFile" /> - <button onclick="createZoneFile()">Create</button> - </div> + .nav-left>input { + width: 12em; + } - <div class="content"> - <div id="activeZone"></div> + .content { + float: left; + width: calc(100% - 14em); + } - <div id="activeZone_soa" style="display: none"> - <h4>SOA record</h4> + .action-delete { + margin-left: 1em; + } - <div class="input"> - <label for="soa_mname"> Name server </label> - <input id="soa_mname" oninput="updateSOA('MName', this.value)" /> - <span class="input-info-toggler" onclick="toggleInfo('soa_mname_info')">?</span> - <div id="soa_mname_info" class="input-info" style="display: none"> - The domain-name of the name server that was the original or primary source - of data for this zone. It should be domain-name where the rescached run. - </div> - </div> + div.actions { + padding: 1em; + } - <div class="input"> - <label for="soa_rname">Admin email</label> - <input id="soa_rname" oninput="updateSOA('RName', this.value)" /> - <span class="input-info-toggler" onclick="toggleInfo('soa_rname_info')">?</span> - <div id="soa_rname_info" class="input-info" style="display: none"> - Email address of the administrator responsible for this zone. The "@" on - email address is replaced with dot, and if there is a dot before "@" it - should be escaped with "\". For example, "dns.admin@domain.tld" would be - written as "dns\.admin.domain.tld".' - </div> - </div> + div.actions button { + width: 100%; + } - <div class="input"> - <label for="soa_serial">Serial</label> - <input - id="soa_serial" - type="number" - min="0" - oninput="updateSOA('Serial', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('soa_serial_info')">?</span> - <div id="soa_serial_info" class="input-info" style="display: none"> - Serial number for this zone. If a secondary name server observes an increase - in this number, the server will assume that the zone has been updated and - initiate a zone transfer. - </div> - </div> + .rr_form { + margin: 1em 0px; + padding: 10px 10px 0px 10px; + border: 1px solid silver; + } - <div class="input"> - <label for="soa_refresh">Refresh</label> - <input - id="soa_refresh" - type="number" - min="0" - oninput="updateSOA('Refresh', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('soa_refresh_info')">?</span> - <div id="soa_refresh_info" class="input-info" style="display: none"> - Number of seconds after which secondary name servers should query the zone - for the SOA record, to detect zone changes. Recommendation for small and - stable zones is 86400 seconds (24 hours). - </div> - </div> + .rr_form>*>.input>input.name { + width: auto; + } - <div class="input"> - <label for="soa_retry">Retry</label> - <input - id="soa_retry" - type="number" - min="0" - oninput="updateSOA('Retry', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('soa_retry_info')">?</span> - <div id="soa_retry_info" class="input-info" style="display: none"> - Number of seconds after which secondary name servers should retry to request - the serial number from the zone if the zone does not respond. It must be - less than Refresh. Recommendation for small and stable zones is 7200 seconds - (2 hours). - </div> - </div> + .rr { + font-family: monospace; + width: 100%; + } - <div class="input"> - <label for="soa_expire">Expire</label> - <input - id="soa_expire" - type="number" - min="0" - oninput="updateSOA('Expire', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('soa_expire_info')">?</span> - <div id="soa_expire_info" class="input-info" style="display: none"> - Number of seconds after which secondary name servers should stop answering - request for this zone if the zone does not respond. This value must be - bigger than the sum of Refresh and Retry. Recommendation for small and - stable zones is 3600000 seconds (1000 hours). - </div> - </div> + .rr>* { + vertical-align: middle; + } - <div class="input"> - <label for="soa_minimum"> Minimum </label> - <input - id="soa_minimum" - type="number" - min="0" - oninput="updateSOA('Minimum', parseInt(this.value))" - /> - <span class="input-info-toggler" onclick="toggleInfo('soa_minimum_info')">?</span> - <div id="soa_minimum_info" class="input-info" style="display: none"> - Time to live for purposes of negative caching. Recommendation for small and - stable zones is 1800 seconds (30 min). - </div> - </div> + .rr.header { + font-weight: bold; + } - <div class="actions"> - <button onclick="saveSOA()">Save</button> - </div> - </div> + .rr>.name { + width: 12em; + display: inline-block; + word-wrap: break-word; + } - <div id="activeZone_records" style="display: none"> - <h4>List records</h4> - <div class="rr header"> - <span class="name"> Name </span> - <span class="type"> Type </span> - <span class="value"> Value </span> - </div> - <div id="list_records"></div> - </div> + .rr>.type { + width: 4em; + display: inline-block; + } - <div id="activeZone_form" class="rr_form" style="display: none"> - <div class="input"> - <label for="rr_type"> Type: </label> - <select id="rr_type" oninput="onSelectRRType(this.value)"> - <option value="1">A</option> - <option value="2">NS</option> - <option value="5">CNAME</option> - <option value="12">PTR</option> - <option value="15">MX</option> - <option value="16">TXT</option> - <option value="28">AAAA</option> - </select> - </div> + .rr>.value { + display: inline-block; + width: 16em; + word-wrap: break-word; + } - <div id="activeZone_form_default"> - <div class="input"> - <label for="rr_name"> Name: </label> - <input id="rr_name" class="name" /> - <span></span> - </div> - <div class="input"> - <label for="rr_value"> Value: </label> - <input id="rr_value" /> - </div> - </div> + .rr>.actions { + display: inline-block; + } + </style> +</head> - <div id="activeZone_form_ptr" style="display: none"> - <div class="input"> - <label for="rr_ptr_name"> Name: </label> - <input id="rr_ptr_name" /> - </div> - <div class="input"> - <label for="rr_ptr_value"> Value: </label> - <input id="rr_ptr_value" class="name" /> - <span></span> - </div> - </div> +<body onload="main()"> + <nav class="menu"> + <a href="/"> rescached </a> + / + <a href="/environment/"> Environment </a> + / + <a href="/block.d/"> block.d </a> + / + <a href="/hosts.d/"> hosts.d </a> + / + <a href="/zone.d/" class="active"> zone.d </a> + / + <a href="/doc/"> Documentation </a> + </nav> + <div id="notif"></div> - <div id="activeZone_form_mx" style="display: none"> - <div class="input"> - <label for="rr_mx_name"> Name: </label> - <input id="rr_mx_name" class="name" /> - <span></span> - </div> - <div class="input"> - <label for="rr_mx_preference"> Preference: </label> - <input id="rr_mx_preference" type="number" min="1" max="65535" /> - </div> - <div class="input"> - <label for="rr_mx_exchange"> Exchange: </label> - <input id="rr_mx_exchange" /> - </div> - </div> + <div class="nav-left"> + <h3>Zone files</h3> + <div id="Zones"></div> - <div class="actions"> - <button class="create" onclick="createRR()">Create</button> - </div> - </div> - </div> + <label for="newZoneFile"> New zone file: </label> + <input id="newZoneFile" /> + <button onclick="createZoneFile()">Create</button> + </div> - <script src="/index.js"></script> - <script src="/rescached.js"></script> - <script> - let resc = null - let activeZone = null - let newRR = { - Name: "", - Value: "", - } + <div class="content"> + <div id="activeZone"></div> - async function main() { - resc = new Rescached("") + <div id="activeZone_soa" style="display: none"> + <h4>SOA record</h4> - let res = await resc.getEnvironment() - if (res.code != 200) { - notifError(res.message) - return - } - renderZones(resc.env.Zones) - resetActiveZone() - } + <div class="input"> + <label for="soa_mname"> Name server </label> + <input id="soa_mname" oninput="updateSOA('MName', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_mname_info')">?</span> + <div id="soa_mname_info" class="input-info" style="display: none"> + The domain-name of the name server that was the original or primary source + of data for this zone. It should be domain-name where the rescached run. + </div> + </div> - async function createZoneFile() { - let name = document.getElementById("newZoneFile").value - if (name === "") { - notifError("The zone file name must not be empty") - return - } - let res = await resc.ZoneFileCreate(name) - if (res.code != 200) { - notifError(res.message) - return - } - renderZones(resc.env.Zones) - } + <div class="input"> + <label for="soa_rname">Admin email</label> + <input id="soa_rname" oninput="updateSOA('RName', this.value)" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_rname_info')">?</span> + <div id="soa_rname_info" class="input-info" style="display: none"> + Email address of the administrator responsible for this zone. The "@" on + email address is replaced with dot, and if there is a dot before "@" it + should be escaped with "\". For example, "dns.admin@domain.tld" would be + written as "dns\.admin.domain.tld".' + </div> + </div> - async function deleteZoneFile() { - let res = await resc.ZoneFileDelete(activeZone.Name) - if (res.code != 200) { - notifError(res.message) - return - } - renderZones(resc.env.Zones) - resetActiveZone() - notifInfo(res.message) - } + <div class="input"> + <label for="soa_serial">Serial</label> + <input id="soa_serial" type="number" min="0" oninput="updateSOA('Serial', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_serial_info')">?</span> + <div id="soa_serial_info" class="input-info" style="display: none"> + Serial number for this zone. If a secondary name server observes an increase + in this number, the server will assume that the zone has been updated and + initiate a zone transfer. + </div> + </div> - async function createRR() { - newRR.Type = parseInt(document.getElementById("rr_type").value) - switch (newRR.Type) { - case 12: // PTR - newRR.Name = document.getElementById("rr_ptr_name").value - newRR.Value = document.getElementById("rr_ptr_value").value - break - case 15: // MX - newRR.Name = document.getElementById("rr_mx_name").value - newRR.Value = { - Preference: parseInt( - document.getElementById("rr_mx_preference").value, - ), - Exchange: document.getElementById("rr_mx_exchange").value, - } - break - default: - newRR.Name = document.getElementById("rr_name").value - newRR.Value = document.getElementById("rr_value").value - } - console.log("createRR: ", newRR) - let res = await resc.ZonedRecordAdd(activeZone.Name, newRR) - if (res.code != 200) { - notifError(res.message) - return - } - renderActiveZoneRecords() - notifInfo(res.message) - } + <div class="input"> + <label for="soa_refresh">Refresh</label> + <input id="soa_refresh" type="number" min="0" oninput="updateSOA('Refresh', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_refresh_info')">?</span> + <div id="soa_refresh_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should query the zone + for the SOA record, to detect zone changes. Recommendation for small and + stable zones is 86400 seconds (24 hours). + </div> + </div> - async function deleteRR(name, idx) { - let rr = activeZone.Records[name][idx] - let res = await resc.ZonedRecordDelete(activeZone.Name, rr) - if (res.code != 200) { - notifError(res.message) - return - } - renderActiveZoneRecords() - notifInfo(res.message) - } + <div class="input"> + <label for="soa_retry">Retry</label> + <input id="soa_retry" type="number" min="0" oninput="updateSOA('Retry', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_retry_info')">?</span> + <div id="soa_retry_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should retry to request + the serial number from the zone if the zone does not respond. It must be + less than Refresh. Recommendation for small and stable zones is 7200 seconds + (2 hours). + </div> + </div> - function onSelectRRType(v) { - let formDefault = document.getElementById("activeZone_form_default") - let formPTR = document.getElementById("activeZone_form_ptr") - let formMX = document.getElementById("activeZone_form_mx") + <div class="input"> + <label for="soa_expire">Expire</label> + <input id="soa_expire" type="number" min="0" oninput="updateSOA('Expire', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_expire_info')">?</span> + <div id="soa_expire_info" class="input-info" style="display: none"> + Number of seconds after which secondary name servers should stop answering + request for this zone if the zone does not respond. This value must be + bigger than the sum of Refresh and Retry. Recommendation for small and + stable zones is 3600000 seconds (1000 hours). + </div> + </div> - newRR.Type = parseInt(v) - newRR.Value = "" + <div class="input"> + <label for="soa_minimum"> Minimum </label> + <input id="soa_minimum" type="number" min="0" oninput="updateSOA('Minimum', parseInt(this.value))" /> + <span class="input-info-toggler" onclick="toggleInfo('soa_minimum_info')">?</span> + <div id="soa_minimum_info" class="input-info" style="display: none"> + Time to live for purposes of negative caching. Recommendation for small and + stable zones is 1800 seconds (30 min). + </div> + </div> - if (v == 12) { - // PTR - formDefault.style.display = "none" - formPTR.style.display = "block" - formPTR.children[1].children[2].innerText = "." + activeZone.Name - formMX.style.display = "none" - } else if (v == 15) { - formDefault.style.display = "none" - formPTR.style.display = "none" - formMX.style.display = "block" - formMX.children[0].children[2].innerText = "." + activeZone.Name - newRR.Value = { - Name: "", - Exchange: "", - Preference: 0, - } - } else { - formDefault.style.display = "block" - formDefault.children[0].children[2].innerText = "." + activeZone.Name - formPTR.style.display = "none" - formMX.style.display = "none" - } - } + <div class="actions"> + <button onclick="saveSOA()">Save</button> + </div> + </div> - function renderZones(zones) { - let wrapper = document.getElementById("Zones") - out = "" - for (let name in zones) { - if (!zones.hasOwnProperty(name)) { - continue - } - let zoneFile = zones[name] - out += ` + <div id="activeZone_records" style="display: none"> + <h4>List records</h4> + <div class="rr header"> + <span class="name"> Name </span> + <span class="type"> Type </span> + <span class="value"> Value </span> + </div> + <div id="list_records"></div> + </div> + + <div id="activeZone_form" class="rr_form" style="display: none"> + <div class="input"> + <label for="rr_type"> Type: </label> + <select id="rr_type" oninput="onSelectRRType(this.value)"> + <option value="1">A</option> + <option value="2">NS</option> + <option value="5">CNAME</option> + <option value="12">PTR</option> + <option value="15">MX</option> + <option value="16">TXT</option> + <option value="28">AAAA</option> + </select> + </div> + + <div id="activeZone_form_default"> + <div class="input"> + <label for="rr_name"> Name: </label> + <input id="rr_name" class="name" /> + <span></span> + </div> + <div class="input"> + <label for="rr_value"> Value: </label> + <input id="rr_value" /> + </div> + </div> + + <div id="activeZone_form_ptr" style="display: none"> + <div class="input"> + <label for="rr_ptr_name"> Name: </label> + <input id="rr_ptr_name" /> + </div> + <div class="input"> + <label for="rr_ptr_value"> Value: </label> + <input id="rr_ptr_value" class="name" /> + <span></span> + </div> + </div> + + <div id="activeZone_form_mx" style="display: none"> + <div class="input"> + <label for="rr_mx_name"> Name: </label> + <input id="rr_mx_name" class="name" /> + <span></span> + </div> + <div class="input"> + <label for="rr_mx_preference"> Preference: </label> + <input id="rr_mx_preference" type="number" min="1" max="65535" /> + </div> + <div class="input"> + <label for="rr_mx_exchange"> Exchange: </label> + <input id="rr_mx_exchange" /> + </div> + </div> + + <div class="actions"> + <button class="create" onclick="createRR()">Create</button> + </div> + </div> + </div> + + <script src="/index.js"></script> + <script src="/rescached.js"></script> + <script> + let resc = null + let activeZone = null + let newRR = { + Name: "", + Value: "", + } + + async function main() { + resc = new Rescached("") + + let res = await resc.getEnvironment() + if (res.code != 200) { + notifError(res.message) + return + } + renderZones(resc.env.Zones) + resetActiveZone() + } + + async function createZoneFile() { + let name = document.getElementById("newZoneFile").value + if (name === "") { + notifError("The zone file name must not be empty") + return + } + let res = await resc.ZoneFileCreate(name) + if (res.code != 200) { + notifError(res.message) + return + } + renderZones(resc.env.Zones) + } + + async function deleteZoneFile() { + let res = await resc.ZoneFileDelete(activeZone.Name) + if (res.code != 200) { + notifError(res.message) + return + } + renderZones(resc.env.Zones) + resetActiveZone() + notifInfo(res.message) + } + + async function createRR() { + newRR.Type = parseInt(document.getElementById("rr_type").value) + switch (newRR.Type) { + case 12: // PTR + newRR.Name = document.getElementById("rr_ptr_name").value + newRR.Value = document.getElementById("rr_ptr_value").value + break + case 15: // MX + newRR.Name = document.getElementById("rr_mx_name").value + newRR.Value = { + Preference: parseInt( + document.getElementById("rr_mx_preference").value, + ), + Exchange: document.getElementById("rr_mx_exchange").value, + } + break + default: + newRR.Name = document.getElementById("rr_name").value + newRR.Value = document.getElementById("rr_value").value + } + console.log("createRR: ", newRR) + let res = await resc.ZonedRecordAdd(activeZone.Name, newRR) + if (res.code != 200) { + notifError(res.message) + return + } + renderActiveZoneRecords() + notifInfo(res.message) + } + + async function deleteRR(name, idx) { + let rr = activeZone.Records[name][idx] + let res = await resc.ZonedRecordDelete(activeZone.Name, rr) + if (res.code != 200) { + notifError(res.message) + return + } + renderActiveZoneRecords() + notifInfo(res.message) + } + + function onSelectRRType(v) { + let formDefault = document.getElementById("activeZone_form_default") + let formPTR = document.getElementById("activeZone_form_ptr") + let formMX = document.getElementById("activeZone_form_mx") + + newRR.Type = parseInt(v) + newRR.Value = "" + + if (v == 12) { + // PTR + formDefault.style.display = "none" + formPTR.style.display = "block" + formPTR.children[1].children[2].innerText = "." + activeZone.Name + formMX.style.display = "none" + } else if (v == 15) { + formDefault.style.display = "none" + formPTR.style.display = "none" + formMX.style.display = "block" + formMX.children[0].children[2].innerText = "." + activeZone.Name + newRR.Value = { + Name: "", + Exchange: "", + Preference: 0, + } + } else { + formDefault.style.display = "block" + formDefault.children[0].children[2].innerText = "." + activeZone.Name + formPTR.style.display = "none" + formMX.style.display = "none" + } + } + + function renderZones(zones) { + let wrapper = document.getElementById("Zones") + out = "" + for (let name in zones) { + if (!zones.hasOwnProperty(name)) { + continue + } + let zoneFile = zones[name] + out += ` <div class="item"> <span onclick="setActiveZone('${zoneFile.Name}')"> ${zoneFile.Name} </span> </div>` - } - wrapper.innerHTML = out - } + } + wrapper.innerHTML = out + } - function renderActiveZone() { - let w = document.getElementById("activeZone") - w.innerHTML = ` + function renderActiveZone() { + let w = document.getElementById("activeZone") + w.innerHTML = ` <h3> ${activeZone.Name} <button @@ -436,32 +429,32 @@ </button> </h3> ` - } + } - function renderActiveZoneSOA() { - const w = document.getElementById("activeZone_soa") - w.style.display = "block" - const soa = activeZone.SOA - document.getElementById("soa_mname").value = soa.MName - document.getElementById("soa_rname").value = soa.RName - document.getElementById("soa_serial").value = soa.Serial - document.getElementById("soa_refresh").value = soa.Refresh - document.getElementById("soa_retry").value = soa.Retry - document.getElementById("soa_expire").value = soa.Expire - document.getElementById("soa_minimum").value = soa.Minimum - } + function renderActiveZoneSOA() { + const w = document.getElementById("activeZone_soa") + w.style.display = "block" + const soa = activeZone.SOA + document.getElementById("soa_mname").value = soa.MName + document.getElementById("soa_rname").value = soa.RName + document.getElementById("soa_serial").value = soa.Serial + document.getElementById("soa_refresh").value = soa.Refresh + document.getElementById("soa_retry").value = soa.Retry + document.getElementById("soa_expire").value = soa.Expire + document.getElementById("soa_minimum").value = soa.Minimum + } - function renderActiveZoneRecords() { - let el = document.getElementById("activeZone_records") - el.style.display = "block" - let w = document.getElementById("list_records") - out = "" - for (const [name, listRR] of Object.entries(activeZone.Records)) { - if (listRR == null || !Array.isArray(listRR)) { - continue - } - listRR.forEach((rr, idx) => { - out += ` + function renderActiveZoneRecords() { + let el = document.getElementById("activeZone_records") + el.style.display = "block" + let w = document.getElementById("list_records") + out = "" + for (const [name, listRR] of Object.entries(activeZone.Records)) { + if (listRR == null || !Array.isArray(listRR)) { + continue + } + listRR.forEach((rr, idx) => { + out += ` <div class="rr"> <span class="name"> ${rr.Name} @@ -479,72 +472,73 @@ </span> </div> ` - }) - } - w.innerHTML = out - } + }) + } + w.innerHTML = out + } - function renderRRValue(value) { - if (typeof value === "object") { - let w = "" - for (const [k, v] of Object.entries(value)) { - if (w.length > 0) { - w += "," - } - w += k + "=" + v - } - return w - } - return value - } + function renderRRValue(value) { + if (typeof value === "object") { + let w = "" + for (const [k, v] of Object.entries(value)) { + if (w.length > 0) { + w += "," + } + w += k + "=" + v + } + return w + } + return value + } - function renderActiveZoneForm() { - let form = document.getElementById("activeZone_form") - form.style.display = "block" - document.getElementById("rr_type").value = 1 - onSelectRRType(1) - } + function renderActiveZoneForm() { + let form = document.getElementById("activeZone_form") + form.style.display = "block" + document.getElementById("rr_type").value = 1 + onSelectRRType(1) + } - function resetActiveZone() { - document.getElementById("activeZone").innerHTML = ` + function resetActiveZone() { + document.getElementById("activeZone").innerHTML = ` <p>Select one of the zone file to manage.</p> ` - document.getElementById("activeZone_soa").style.display = "none" - document.getElementById("activeZone_records").style.display = "none" - document.getElementById("activeZone_form").style.display = "none" - activeZone = null - } + document.getElementById("activeZone_soa").style.display = "none" + document.getElementById("activeZone_records").style.display = "none" + document.getElementById("activeZone_form").style.display = "none" + activeZone = null + } + + async function saveSOA() { + console.log("saveSOA: ", activeZone.SOA.Value) + let rr = activeZone.SOA + rr.Type = 6 + let res = await resc.ZonedRecordAdd(activeZone.Name, rr) + if (res.code != 200) { + notifError(res.message) + return + } + notifInfo(res.message) + } - async function saveSOA() { - console.log("saveSOA: ", activeZone.SOA.Value) - let rr = activeZone.SOA - rr.Type = 6 - let res = await resc.ZonedRecordAdd(activeZone.Name, rr) - if (res.code != 200) { - notifError(res.message) - return - } - notifInfo(res.message) - } + async function setActiveZone(name) { + activeZone = resc.env.Zones[name] + let res = await resc.ZonedRecords(name) + if (res.code != 200) { + notifError(res.message) + return + } + activeZone.Records = res.data + console.log("setActiveZone: records: ", activeZone.Records) + renderActiveZone() + renderActiveZoneSOA() + renderActiveZoneRecords() + renderActiveZoneForm() + } - async function setActiveZone(name) { - activeZone = resc.env.Zones[name] - let res = await resc.ZonedRecords(name) - if (res.code != 200) { - notifError(res.message) - return - } - activeZone.Records = res.data - console.log("setActiveZone: records: ", activeZone.Records) - renderActiveZone() - renderActiveZoneSOA() - renderActiveZoneRecords() - renderActiveZoneForm() - } + function updateSOA(k, v) { + activeZone.SOA.Value[k] = v + } + </script> +</body> - function updateSOA(k, v) { - activeZone.SOA.Value[k] = v - } - </script> - </body> </html> |
