From 2ae93b6fd4219a3ec9dd31816685a245b7ab0076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 07:49:28 +0100 Subject: [PATCH] Records update --- dns.go | 142 ++++++++------------------------------------- routeros_device.go | 25 +++++++- static/js/dns.mjs | 102 ++++++++++++++++---------------- webserver.go | 58 +++++++++++------- 4 files changed, 136 insertions(+), 191 deletions(-) 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%s
-
%s
-
%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) +}