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 (
|
||||
// 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(`
|
||||
<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
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
set(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() {// {{{
|
||||
}// }}}
|
||||
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 = `
|
||||
<div>Name</div>
|
||||
<input type="text" class="name">
|
||||
|
||||
|
|
@ -356,23 +350,33 @@ class RecordDialog {
|
|||
</div>
|
||||
`
|
||||
|
||||
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()
|
||||
}// }}}
|
||||
}
|
||||
|
|
|
|||
58
webserver.go
58
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,32 +56,15 @@ 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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue