aboutsummaryrefslogtreecommitdiff
path: root/_www
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-05-22 15:57:52 +0700
committerShulhan <ms@kilabit.info>2022-05-22 16:14:35 +0700
commita676f04d2f8c1981fc953cc2e87adf059e23089a (patch)
treed86300df47d53abdf7ccb0342f96ddd3a677fbd4 /_www
parentbb08b5bb6ad6cf3e1c409db7d69ace4364e7bc8d (diff)
downloadrescached-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.html254
l---------_www/doc/CHANGELOG.adoc1
l---------_www/doc/README.adoc1
-rw-r--r--_www/doc/benchmark.adoc125
-rw-r--r--_www/doc/html.tmpl34
-rw-r--r--_www/doc/images/Screenshot_wui_environment.pngbin0 -> 158861 bytes
-rw-r--r--_www/doc/images/Screenshot_wui_frontpage.pngbin0 -> 344669 bytes
-rw-r--r--_www/doc/images/Screenshot_wui_hosts_blocks.pngbin0 -> 198326 bytes
-rw-r--r--_www/doc/images/Screenshot_wui_hosts_d.pngbin0 -> 125782 bytes
-rw-r--r--_www/doc/images/Screenshot_wui_zone_d.pngbin0 -> 91905 bytes
-rw-r--r--_www/doc/index.adoc35
-rw-r--r--_www/doc/rescached.cfg.adoc263
-rw-r--r--_www/doc/resolver.adoc460
-rw-r--r--_www/environment/index.html553
-rw-r--r--_www/hosts.d/index.html415
-rw-r--r--_www/index.css155
-rw-r--r--_www/index.html391
-rw-r--r--_www/index.js32
-rw-r--r--_www/rescached.js596
-rw-r--r--_www/zone.d/index.html934
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
new file mode 100644
index 0000000..0e8c60d
--- /dev/null
+++ b/_www/doc/images/Screenshot_wui_environment.png
Binary files differ
diff --git a/_www/doc/images/Screenshot_wui_frontpage.png b/_www/doc/images/Screenshot_wui_frontpage.png
new file mode 100644
index 0000000..0ee949b
--- /dev/null
+++ b/_www/doc/images/Screenshot_wui_frontpage.png
Binary files differ
diff --git a/_www/doc/images/Screenshot_wui_hosts_blocks.png b/_www/doc/images/Screenshot_wui_hosts_blocks.png
new file mode 100644
index 0000000..7eaebe2
--- /dev/null
+++ b/_www/doc/images/Screenshot_wui_hosts_blocks.png
Binary files differ
diff --git a/_www/doc/images/Screenshot_wui_hosts_d.png b/_www/doc/images/Screenshot_wui_hosts_d.png
new file mode 100644
index 0000000..b096fa0
--- /dev/null
+++ b/_www/doc/images/Screenshot_wui_hosts_d.png
Binary files differ
diff --git a/_www/doc/images/Screenshot_wui_zone_d.png b/_www/doc/images/Screenshot_wui_zone_d.png
new file mode 100644
index 0000000..52e7913
--- /dev/null
+++ b/_www/doc/images/Screenshot_wui_zone_d.png
Binary files differ
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>