Records update
This commit is contained in:
parent
97058d036d
commit
2ae93b6fd4
4 changed files with 136 additions and 191 deletions
142
dns.go
142
dns.go
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
// Standard
|
// Standard
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
@ -21,6 +20,17 @@ type DNSRecord struct {
|
||||||
CNAME string
|
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 {
|
type DomainPart struct {
|
||||||
Record []DNSRecord
|
Record []DNSRecord
|
||||||
Subparts map[string]*DomainPart `json:",omitempty"`
|
Subparts map[string]*DomainPart `json:",omitempty"`
|
||||||
|
|
@ -93,125 +103,19 @@ func (r DNSRecord) NameReversed() string {
|
||||||
return strings.Join(parts, ".")
|
return strings.Join(parts, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildRecordsTree(records []DNSRecord) *DomainPart {
|
func (r DNSRecord) toDNSEntry() (e DNSEntry) {
|
||||||
topPart := new(DomainPart)
|
e.ID = r.ID
|
||||||
topPart.Subparts = make(map[string]*DomainPart)
|
e.Disabled = r.Disabled
|
||||||
|
e.Name = r.Name
|
||||||
|
e.TTL = r.TTL
|
||||||
|
e.Type = r.Type
|
||||||
|
|
||||||
for _, record := range records {
|
switch(r.Type) {
|
||||||
curPart := topPart
|
case "A", "AAAA":
|
||||||
|
e.Address = r.ParsedValue
|
||||||
currentDomainNameSplit := strings.Split(record.Name, ".")
|
case "CNAME":
|
||||||
slices.Reverse(currentDomainNameSplit)
|
e.CNAME = r.ParsedValue
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return topPart
|
return
|
||||||
}
|
|
||||||
|
|
||||||
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(`
|
|
||||||
<div class="top %s" data-top="%s" data-self="%s" style="padding-left: %dpx">
|
|
||||||
<img class="folder closed" src="/images/%s/icon_folder.svg">
|
|
||||||
<img class="folder open" src="/images/%s/icon_folder_open.svg">
|
|
||||||
<span>%s</span><span>%s</span>
|
|
||||||
</div>
|
|
||||||
<div class="type"></div>
|
|
||||||
<div class="value"></div>
|
|
||||||
`,
|
|
||||||
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(
|
|
||||||
`
|
|
||||||
<div class="record" style="padding-left: %dpx" data-top="%s"><div><span>%s</span><span>%s</span></div></div>
|
|
||||||
<div class="type"><div>%s</div></div>
|
|
||||||
<div class="value">%s</div>
|
|
||||||
`,
|
|
||||||
(len(newParts)-1)*32,
|
|
||||||
restPart, // data-top
|
|
||||||
mostSpecificPart,
|
|
||||||
restPart,
|
|
||||||
rec.Type,
|
|
||||||
rec.String(),
|
|
||||||
)
|
|
||||||
lines = append(lines, HTMLElement{Header: false, HTML: html})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -105,8 +106,8 @@ func (dev *RouterosDevice) GetIdentity() (identity string, err error) { // {{{
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) {
|
func (dev *RouterosDevice) StaticDNSEntries() (entries []*DNSRecord, err error) {
|
||||||
entries = []DNSRecord{}
|
entries = []*DNSRecord{}
|
||||||
|
|
||||||
var body []byte
|
var body []byte
|
||||||
body, err = dev.query("GET", "/ip/dns/static", []byte{})
|
body, err = dev.query("GET", "/ip/dns/static", []byte{})
|
||||||
|
|
@ -119,6 +120,26 @@ func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) {
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ class Folder {
|
||||||
// Records are refreshed.
|
// Records are refreshed.
|
||||||
this.divRecords.replaceChildren()
|
this.divRecords.replaceChildren()
|
||||||
for (const rec of Array.from(this.records))
|
for (const rec of Array.from(this.records))
|
||||||
this.divRecords.append(...rec.toElements())
|
this.divRecords.append(...rec.render())
|
||||||
|
|
||||||
return this.div
|
return this.div
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
@ -228,6 +228,7 @@ class Record {
|
||||||
this.divValue = null
|
this.divValue = null
|
||||||
this.divSeparator = null
|
this.divSeparator = null
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
id() {// {{{
|
id() {// {{{
|
||||||
return this.data['.id']
|
return this.data['.id']
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
@ -247,18 +248,12 @@ class Record {
|
||||||
return this.data.Type.toUpperCase()
|
return this.data.Type.toUpperCase()
|
||||||
}// }}}
|
}// }}}
|
||||||
value() {// {{{
|
value() {// {{{
|
||||||
switch (this.type()) {
|
return this.data.ParsedValue
|
||||||
case 'A':
|
|
||||||
case 'AAAA':
|
|
||||||
return this.data.Address
|
|
||||||
|
|
||||||
case 'CNAME':
|
|
||||||
return this.data.CNAME
|
|
||||||
}
|
|
||||||
}// }}}
|
}// }}}
|
||||||
labels() {// {{{
|
labels() {// {{{
|
||||||
return this.name().split('.')
|
return this.name().split('.')
|
||||||
}// }}}
|
}// }}}
|
||||||
|
|
||||||
copy(el, text) {// {{{
|
copy(el, text) {// {{{
|
||||||
el.classList.add('copy')
|
el.classList.add('copy')
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
|
|
@ -267,24 +262,10 @@ class Record {
|
||||||
edit() {// {{{
|
edit() {// {{{
|
||||||
new RecordDialog(this).show()
|
new RecordDialog(this).show()
|
||||||
}// }}}
|
}// }}}
|
||||||
set(key, value) {
|
set(key, value) {// {{{
|
||||||
if (key != 'Value') {
|
this.data[key] = value
|
||||||
this.data[key] = value
|
}// }}}
|
||||||
return
|
render() {// {{{
|
||||||
}
|
|
||||||
|
|
||||||
switch(this.data.type.toUpperCase()) {
|
|
||||||
case 'A':
|
|
||||||
case 'AAAA':
|
|
||||||
this.data.Address = value
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'CNAME':
|
|
||||||
this.data.CNAME = value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toElements() {// {{{
|
|
||||||
if (this.divFQDN === null) {
|
if (this.divFQDN === null) {
|
||||||
this.imgIcon = document.createElement('img')
|
this.imgIcon = document.createElement('img')
|
||||||
this.divFQDN = document.createElement('div')
|
this.divFQDN = document.createElement('div')
|
||||||
|
|
@ -323,17 +304,30 @@ class Record {
|
||||||
|
|
||||||
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator]
|
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 {
|
class RecordDialog {
|
||||||
constructor(record) {
|
constructor(record) {// {{{
|
||||||
this.record = record
|
this.record = record
|
||||||
}
|
}// }}}
|
||||||
|
|
||||||
show() {
|
show() {// {{{
|
||||||
const dlg = document.createElement('dialog')
|
this.dlg = document.createElement('dialog')
|
||||||
dlg.id = "record-dialog"
|
this.dlg.id = "record-dialog"
|
||||||
dlg.innerHTML = `
|
this.dlg.innerHTML = `
|
||||||
<div>Name</div>
|
<div>Name</div>
|
||||||
<input type="text" class="name">
|
<input type="text" class="name">
|
||||||
|
|
||||||
|
|
@ -356,23 +350,33 @@ class RecordDialog {
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
dlg.querySelector('.name').value = this.record.name()
|
this.dlg.querySelector('.name').value = this.record.name()
|
||||||
dlg.querySelector('.type').value = this.record.type()
|
this.dlg.querySelector('.type').value = this.record.type()
|
||||||
dlg.querySelector('.value').value = this.record.value()
|
this.dlg.querySelector('.value').value = this.record.value()
|
||||||
dlg.querySelector('.ttl').value = this.record.ttl()
|
this.dlg.querySelector('.ttl').value = this.record.ttl();
|
||||||
|
|
||||||
dlg.querySelector('.save').addEventListener('click', ()=>this.save())
|
['.name', '.type', '.value', '.ttl'].forEach(v =>
|
||||||
dlg.querySelector('.close').addEventListener('click', ()=>dlg.close())
|
this.dlg.querySelector(v).addEventListener('keydown', event => this.enterKeyHandler(event))
|
||||||
|
)
|
||||||
|
|
||||||
dlg.addEventListener('close', ()=>dlg.remove())
|
this.dlg.querySelector('.save').addEventListener('click', () => this.save())
|
||||||
document.body.appendChild(dlg)
|
this.dlg.querySelector('.close').addEventListener('click', () => this.dlg.close())
|
||||||
dlg.showModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
this.dlg.addEventListener('close', () => this.dlg.remove())
|
||||||
this.record.set('Name', dlg.querySelector('.name').value)
|
document.body.appendChild(this.dlg)
|
||||||
this.record.set('Type', dlg.querySelector('.type').value)
|
this.dlg.showModal()
|
||||||
this.record.set('Value', dlg.querySelector('.value').value)
|
}// }}}
|
||||||
this.record.set('TTL', dlg.querySelector('.ttl').value)
|
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()
|
||||||
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
webserver.go
58
webserver.go
|
|
@ -6,12 +6,11 @@ import (
|
||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"embed"
|
"embed"
|
||||||
_ "encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
_ "html/template"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -33,6 +32,7 @@ func registerWebserverHandlers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", rootHandler)
|
http.HandleFunc("/", rootHandler)
|
||||||
|
http.HandleFunc("/record/save", actionRecordSave)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWebserver() {
|
func startWebserver() {
|
||||||
|
|
@ -56,32 +56,15 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []DNSRecord
|
var entries []*DNSRecord
|
||||||
entries, err = device.StaticDNSEntries()
|
entries, err = device.StaticDNSEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.SortFunc(entries, SortDNSRecord)
|
|
||||||
data["DNSRecords"] = entries
|
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
|
data["VERSION"] = VERSION
|
||||||
page.Data = data
|
page.Data = data
|
||||||
|
|
||||||
|
|
@ -95,3 +78,36 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
htmlEngine.StaticResource(w, r)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue