diff --git a/dns.go b/dns.go
index b356dad..b63d284 100644
--- a/dns.go
+++ b/dns.go
@@ -3,7 +3,6 @@ package main
import (
// Standard
"fmt"
- "maps"
"slices"
"strings"
)
@@ -21,6 +20,17 @@ type DNSRecord struct {
CNAME string
}
+type DNSEntry struct {
+ ID string `json:".id"`
+ Disabled string `json:"disabled"`
+ Name string `json:"name"`
+ TTL string `json:"ttl"`
+ Type string `json:"type"`
+
+ Address string `json:"address,omitempty"`
+ CNAME string `json:"cname,omitempty"`
+}
+
type DomainPart struct {
Record []DNSRecord
Subparts map[string]*DomainPart `json:",omitempty"`
@@ -93,125 +103,19 @@ func (r DNSRecord) NameReversed() string {
return strings.Join(parts, ".")
}
-func BuildRecordsTree(records []DNSRecord) *DomainPart {
- topPart := new(DomainPart)
- topPart.Subparts = make(map[string]*DomainPart)
+func (r DNSRecord) toDNSEntry() (e DNSEntry) {
+ e.ID = r.ID
+ e.Disabled = r.Disabled
+ e.Name = r.Name
+ e.TTL = r.TTL
+ e.Type = r.Type
- for _, record := range records {
- curPart := topPart
-
- currentDomainNameSplit := strings.Split(record.Name, ".")
- slices.Reverse(currentDomainNameSplit)
-
- for _, part := range currentDomainNameSplit {
- if nextDomainPart, found := curPart.Subparts[part]; !found {
- newPart := new(DomainPart)
- newPart.Record = []DNSRecord{record}
- newPart.Subparts = make(map[string]*DomainPart)
- curPart.Subparts[strings.ToLower(part)] = newPart
- curPart = newPart
- } else {
- nextDomainPart.Record = append(nextDomainPart.Record, record)
- curPart = nextDomainPart
- }
- }
+ switch(r.Type) {
+ case "A", "AAAA":
+ e.Address = r.ParsedValue
+ case "CNAME":
+ e.CNAME = r.ParsedValue
}
- return topPart
-}
-
-type HTMLElement struct {
- Header bool
- HTML string
-}
-
-func (dp *DomainPart) ToHTMLElements(parts []string) []HTMLElement {
- var lines []HTMLElement
-
- sortedParts := slices.Sorted(maps.Keys(dp.Subparts))
-
- for _, part := range sortedParts {
- subpart := dp.Subparts[part]
- newParts := append(parts, part)
-
- reversedParts := make([]string, len(newParts))
- copy(reversedParts, newParts)
- slices.Reverse(reversedParts)
- fqdn := strings.Join(reversedParts, ".")
-
- mostSpecificPart := reversedParts[0]
- restPart := ""
- if len(reversedParts) > 1 {
- restPart = strings.Join(reversedParts[1:], ".")
- mostSpecificPart += "."
- }
-
- if len(subpart.Subparts) != 0 {
- topmost := ""
- if len(newParts) == 1 {
- topmost = "top-most"
- }
- html := fmt.Sprintf(`
-
-

-

-
%s%s
-
-
-
- `,
- topmost, // .top-most
- restPart, // data-top
- fqdn, // data-self
- (len(newParts)-1)*32, // margin-left
- VERSION, // images/
- VERSION, // images/
- mostSpecificPart, // innerText
- restPart,
- )
- lines = append(lines, HTMLElement{Header: true, HTML: html})
- subLines := subpart.ToHTMLElements(newParts)
- lines = append(lines, subLines...)
- }
-
- }
-
- for _, part := range sortedParts {
- subpart := dp.Subparts[part]
- newParts := append(parts, part)
-
- reversedParts := make([]string, len(newParts))
- copy(reversedParts, newParts)
- slices.Reverse(reversedParts)
- //fqdn := strings.Join(reversedParts, ".")
-
- mostSpecificPart := reversedParts[0]
- restPart := ""
- if len(reversedParts) > 1 {
- restPart = strings.Join(reversedParts[1:], ".")
- mostSpecificPart += "."
- }
-
- if len(subpart.Subparts) == 0 {
- for _, rec := range subpart.Record {
- html := fmt.Sprintf(
- `
-
-
- %s
- `,
- (len(newParts)-1)*32,
- restPart, // data-top
- mostSpecificPart,
- restPart,
- rec.Type,
- rec.String(),
- )
- lines = append(lines, HTMLElement{Header: false, HTML: html})
- }
- }
-
- }
-
- return lines
+ return
}
diff --git a/routeros_device.go b/routeros_device.go
index 6d8334b..e394abd 100644
--- a/routeros_device.go
+++ b/routeros_device.go
@@ -6,6 +6,7 @@ import (
"crypto/tls"
"encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -105,8 +106,8 @@ func (dev *RouterosDevice) GetIdentity() (identity string, err error) { // {{{
return
} // }}}
-func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) {
- entries = []DNSRecord{}
+func (dev *RouterosDevice) StaticDNSEntries() (entries []*DNSRecord, err error) {
+ entries = []*DNSRecord{}
var body []byte
body, err = dev.query("GET", "/ip/dns/static", []byte{})
@@ -119,6 +120,26 @@ func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) {
return
}
+ for _, entry := range entries {
+ entry.ParsedValue = entry.String()
+ }
+
+ return
+}
+func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (err error) {
+ req, _ := json.Marshal(record)
+
+ _, err = dev.query("PATCH", "/ip/dns/static/"+record.ID, req)
+ if err != nil {
+ rosError := struct{ Detail string }{}
+ if jsonError := json.Unmarshal([]byte(err.Error()), &rosError); jsonError == nil {
+ logger.Error("routeros", "error", jsonError)
+ err = errors.New(rosError.Detail)
+ return
+ }
+ return
+ }
+
return
}
diff --git a/static/js/dns.mjs b/static/js/dns.mjs
index f12cc32..330e87f 100644
--- a/static/js/dns.mjs
+++ b/static/js/dns.mjs
@@ -212,7 +212,7 @@ class Folder {
// Records are refreshed.
this.divRecords.replaceChildren()
for (const rec of Array.from(this.records))
- this.divRecords.append(...rec.toElements())
+ this.divRecords.append(...rec.render())
return this.div
}// }}}
@@ -228,6 +228,7 @@ class Record {
this.divValue = null
this.divSeparator = null
}// }}}
+
id() {// {{{
return this.data['.id']
}// }}}
@@ -247,18 +248,12 @@ class Record {
return this.data.Type.toUpperCase()
}// }}}
value() {// {{{
- switch (this.type()) {
- case 'A':
- case 'AAAA':
- return this.data.Address
-
- case 'CNAME':
- return this.data.CNAME
- }
+ return this.data.ParsedValue
}// }}}
labels() {// {{{
return this.name().split('.')
}// }}}
+
copy(el, text) {// {{{
el.classList.add('copy')
navigator.clipboard.writeText(text)
@@ -267,24 +262,10 @@ class Record {
edit() {// {{{
new RecordDialog(this).show()
}// }}}
- set(key, value) {
- if (key != 'Value') {
- this.data[key] = value
- return
- }
-
- switch(this.data.type.toUpperCase()) {
- case 'A':
- case 'AAAA':
- this.data.Address = value
- break
-
- case 'CNAME':
- this.data.CNAME = value
- break
- }
- }
- toElements() {// {{{
+ set(key, value) {// {{{
+ this.data[key] = value
+ }// }}}
+ render() {// {{{
if (this.divFQDN === null) {
this.imgIcon = document.createElement('img')
this.divFQDN = document.createElement('div')
@@ -323,17 +304,30 @@ class Record {
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator]
}// }}}
+ save() {// {{{
+ fetch('/record/save', {
+ method: 'POST',
+ body: JSON.stringify(this.data),
+ })
+ .then(data => data.json())
+ .then(json => {
+ if (!json.OK) {
+ alert(json.Error)
+ return
+ }
+ })
+ }// }}}
}
class RecordDialog {
- constructor(record) {
+ constructor(record) {// {{{
this.record = record
- }
+ }// }}}
- show() {
- const dlg = document.createElement('dialog')
- dlg.id = "record-dialog"
- dlg.innerHTML = `
+ show() {// {{{
+ this.dlg = document.createElement('dialog')
+ this.dlg.id = "record-dialog"
+ this.dlg.innerHTML = `
Name
@@ -356,23 +350,33 @@ class RecordDialog {
`
- dlg.querySelector('.name').value = this.record.name()
- dlg.querySelector('.type').value = this.record.type()
- dlg.querySelector('.value').value = this.record.value()
- dlg.querySelector('.ttl').value = this.record.ttl()
+ this.dlg.querySelector('.name').value = this.record.name()
+ this.dlg.querySelector('.type').value = this.record.type()
+ this.dlg.querySelector('.value').value = this.record.value()
+ this.dlg.querySelector('.ttl').value = this.record.ttl();
- dlg.querySelector('.save').addEventListener('click', ()=>this.save())
- dlg.querySelector('.close').addEventListener('click', ()=>dlg.close())
+ ['.name', '.type', '.value', '.ttl'].forEach(v =>
+ this.dlg.querySelector(v).addEventListener('keydown', event => this.enterKeyHandler(event))
+ )
- dlg.addEventListener('close', ()=>dlg.remove())
- document.body.appendChild(dlg)
- dlg.showModal()
- }
+ this.dlg.querySelector('.save').addEventListener('click', () => this.save())
+ this.dlg.querySelector('.close').addEventListener('click', () => this.dlg.close())
- save() {
- this.record.set('Name', dlg.querySelector('.name').value)
- this.record.set('Type', dlg.querySelector('.type').value)
- this.record.set('Value', dlg.querySelector('.value').value)
- this.record.set('TTL', dlg.querySelector('.ttl').value)
- }
+ this.dlg.addEventListener('close', () => this.dlg.remove())
+ document.body.appendChild(this.dlg)
+ this.dlg.showModal()
+ }// }}}
+ enterKeyHandler(event) {// {{{
+ if (event.key == "Enter")
+ this.save()
+ }// }}}
+ save() {// {{{
+ this.record.set('Name', this.dlg.querySelector('.name').value)
+ this.record.set('Type', this.dlg.querySelector('.type').value)
+ this.record.set('ParsedValue', this.dlg.querySelector('.value').value)
+ this.record.set('TTL', this.dlg.querySelector('.ttl').value)
+ this.record.render()
+ this.record.save()
+ this.dlg.close()
+ }// }}}
}
diff --git a/webserver.go b/webserver.go
index 6ea9550..9344da4 100644
--- a/webserver.go
+++ b/webserver.go
@@ -6,12 +6,11 @@ import (
// Standard
"embed"
- _ "encoding/json"
+ "encoding/json"
"fmt"
+ "io"
"net/http"
"os"
- "slices"
- _ "html/template"
)
var (
@@ -33,6 +32,7 @@ func registerWebserverHandlers() {
}
http.HandleFunc("/", rootHandler)
+ http.HandleFunc("/record/save", actionRecordSave)
}
func startWebserver() {
@@ -56,31 +56,14 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
return
}
- var entries []DNSRecord
+ var entries []*DNSRecord
entries, err = device.StaticDNSEntries()
if err != nil {
w.Write([]byte(err.Error()))
return
}
- slices.SortFunc(entries, SortDNSRecord)
data["DNSRecords"] = entries
-
- /*
- tree := BuildRecordsTree(entries)
- htmlElements := tree.ToHTMLElements([]string{})
- data["TreeData"] = tree
-
- var html string
- for _, el := range htmlElements {
- html += el.HTML
- }
-
- data["Tree"] = template.HTML(html)
-
- j, _ := json.Marshal(tree)
- os.WriteFile("/tmp/tree.json", j, 0644)
- */
data["VERSION"] = VERSION
page.Data = data
@@ -95,3 +78,36 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
htmlEngine.StaticResource(w, r)
}
+
+func httpError(w http.ResponseWriter, err error) {
+ resp := struct {
+ OK bool
+ Error string
+ }{
+ false,
+ err.Error(),
+ }
+
+ j, _ := json.Marshal(resp)
+ w.Write(j)
+}
+
+func actionRecordSave(w http.ResponseWriter, r *http.Request) {
+ body, _ := io.ReadAll(r.Body)
+ var record DNSRecord
+ err := json.Unmarshal(body, &record)
+ if err != nil {
+ httpError(w, err)
+ return
+ }
+
+ err = device.UpdateDNSEntry(record.toDNSEntry())
+ if err != nil {
+ httpError(w, err)
+ logger.Error("webserver", "op", "record_save", "error", err)
+ return
+ }
+
+ j, _ := json.Marshal(struct{ OK bool }{true})
+ w.Write(j)
+}