From 36baaf0cafc61723ebf4888b948e6d5bbabf4a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Fri, 20 Feb 2026 13:26:21 +0100 Subject: [PATCH] Initial commit --- config.go | 40 +++ dns.go | 109 +++++++ go.mod | 10 + go.sum | 6 + main.go | 82 ++++++ routeros_device.go | 583 ++++++++++++++++++++++++++++++++++++++ static/css/index.css | 12 + views/layouts/main.gotmpl | 27 ++ views/pages/index.gotmpl | 7 + webserver.go | 85 ++++++ 10 files changed, 961 insertions(+) create mode 100644 config.go create mode 100644 dns.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 routeros_device.go create mode 100644 static/css/index.css create mode 100644 views/layouts/main.gotmpl create mode 100644 views/pages/index.gotmpl create mode 100644 webserver.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..34df751 --- /dev/null +++ b/config.go @@ -0,0 +1,40 @@ +package main + +import ( + // Standard + "encoding/json" + "os" +) + +type Config struct { + Network struct { + Address string + Port int + } + + Logging struct { + URL string + System string + Instance string + LogDir string + } + + Device struct { + Address string + Port int + Username string + Password string + Timeout int + } +} + +func readConfig() (config Config, err error) { + var configData []byte + configData, err = os.ReadFile(flagConfig) + if err != nil { + return + } + + err = json.Unmarshal(configData, &config) + return +} diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..a4dddb7 --- /dev/null +++ b/dns.go @@ -0,0 +1,109 @@ +package main + +import ( + // Standard + "fmt" + "slices" + "strings" +) + +type DNSRecord struct { + ID string `json:".id"` + Address string + Disabled string + Dynamic string + Name string + TTL string + Type string +} + +type RecordsTree struct { + Record DNSRecord + Children []RecordsTree +} + +func SortDNSRecord(a, b DNSRecord) int { + aComponents := strings.Split(a.Name, ".") + bComponents := strings.Split(b.Name, ".") + + slices.Reverse(aComponents) + slices.Reverse(bComponents) + + for i, aComp := range aComponents { + if i >= len(bComponents) { + return -1 + } + + bComp := bComponents[i] + if aComp > bComp { + return 1 + } + + if aComp < bComp { + return -1 + } + + } + + return 0 +} + +func (r DNSRecord) Parts() int { + return len(strings.Split(r.Name, ".")) +} + +func (r DNSRecord) Part(numParts int) string { + splitName := strings.Split(r.Name, ".") + slices.Reverse(splitName) + + parts := []string{} + for i := range numParts { + if i >= len(splitName) { + break + } + parts = append(parts, splitName[i]) + } + + slices.Reverse(parts) + return strings.Join(parts, ".") +} + +func (r DNSRecord) NameReversed() string { + parts := strings.Split(r.Name, ".") + slices.Reverse(parts) + return strings.Join(parts, ".") +} + +func (rt *RecordsTree) BuildTree(records []DNSRecord, startAt, partsLevel int) (tree RecordsTree) { + tree = RecordsTree{} + + for i := startAt; i < len(records); i++ { + // current: bar.foo.n44.se + current := records[i] + currentPart := current.Part(partsLevel) + + for j := i; j < len(records); j++ { + // next: baz.bar.foo.n44.se + next := records[j] + nextPart := next.Part(partsLevel) + fmt.Printf("%04d %04d: %s ?= %s\n", i, j, currentPart, nextPart) + + if currentPart == nextPart { + fmt.Printf("FOUND SOMETHING!\n") + nextTree := RecordsTree{ + Record: next, + } + + tree.Children = append(tree.Children, nextTree) + } + + // Doesn't match anymore, return to other loop after all that are processed + if currentPart != nextPart { + i = j + break + } + } + } + + return tree +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a71c22c --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module routeros_dns + +go 1.25.4 + +require ( + git.gibonuddevalla.se/go/html_template v1.0.0 + git.gibonuddevalla.se/go/vlog v1.0.0 +) + +require git.gibonuddevalla.se/go/wrappederror v0.3.5 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..22df342 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +git.gibonuddevalla.se/go/html_template v1.0.0 h1:0YoqKiWMxYUsYZomqFlXLR/h0UNBicqw+VOsPyotcD4= +git.gibonuddevalla.se/go/html_template v1.0.0/go.mod h1:DGqGMulbZyGoRqWXInXISb6GQLswuUrTaX1WhH7jx/w= +git.gibonuddevalla.se/go/vlog v1.0.0 h1:6iu7Wy3V3vTSg1usLSZWX9ipA+SY3FV4pi7HrHqagyc= +git.gibonuddevalla.se/go/vlog v1.0.0/go.mod h1:rjS9ZINhZF+Bhrb+fdD4aEKOnLxFYU3PJFMx7nOi4c0= +git.gibonuddevalla.se/go/wrappederror v0.3.5 h1:/EzrdXETlZfNpS6TcK1Ix6BaV+Fl7qcGoxUM0GkrIN8= +git.gibonuddevalla.se/go/wrappederror v0.3.5/go.mod h1:j4w320Hk1wvhOPjUaK4GgLvmtnjUUM5yVu6JFO1OCSc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fc40787 --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + // External + "git.gibonuddevalla.se/go/vlog" + + // Standard + "flag" + "log/slog" + "os" + "path" +) + +const VERSION = "v1" + +var ( + flagVersion bool + flagDev bool + flagDebug bool + flagConfig string + + config Config + initLogger *slog.Logger + logger *slog.Logger + + device RouterosDevice +) + +func init() {// {{{ + initLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})) + + confDir, err := os.UserConfigDir() + if err != nil { + initLogger.Error("application", "error", err) + os.Exit(1) + } + defConfFilename := path.Join(confDir, "routeros_dns.json") + + flag.BoolVar(&flagVersion, "version", false, "Print version and exit") + flag.BoolVar(&flagDev, "dev", false, "Load each static file from disk") + flag.BoolVar(&flagDebug, "debug", false, "Debugging log level") + flag.StringVar(&flagConfig, "config", defConfFilename, "Path to filename for configuration") + flag.Parse() + + config, err = readConfig() + if err != nil { + initLogger.Error("application", "error", err) + os.Exit(1) + } + + logger = initLogging(config) +}// }}} +func initLogging(config Config) (*slog.Logger) {// {{{ + opts := slog.HandlerOptions{} + if flagDebug { + opts.Level = slog.LevelDebug + } + handler := vlog.New( + os.Stdout, + opts, + config.Logging.LogDir, + config.Logging.URL, + "routeros-dns", + config.Logging.System, + config.Logging.Instance, + ) + return slog.New(handler) +}// }}} + +func main() { + initLogger.Info("application", "version", VERSION) + + device.Host = config.Device.Address + device.Port = config.Device.Port + device.Username = config.Device.Username + device.Password = config.Device.Password + device.Timeout = config.Device.Timeout + device.Init() + + registerWebserverHandlers() + startWebserver() +} diff --git a/routeros_device.go b/routeros_device.go new file mode 100644 index 0000000..6d8334b --- /dev/null +++ b/routeros_device.go @@ -0,0 +1,583 @@ +package main + +import ( + // Standard + "bytes" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type RouterosDevice struct { + Host string + Port int + Username string + Password string + Timeout int + + httpClient *http.Client + auth string + added time.Time +} + +type RouterosIDType int + +const ( + IPv4 RouterosIDType = iota + IPv6 +) + +type RouterosID struct { + Type RouterosIDType + ID string +} + +func (dev *RouterosDevice) Init() { // {{{ + cred := fmt.Sprintf("%s:%s", dev.Username, dev.Password) + dev.auth = base64.StdEncoding.EncodeToString([]byte(cred)) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + client := http.Client{ + Transport: tr, + Timeout: time.Duration(dev.Timeout) * time.Second, + } + + dev.httpClient = &client + dev.added = time.Now() +} // }}} + +// query sends a RouterOS REST API query and returns the unparsed body. +func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{ + url := fmt.Sprintf("https://%s/rest%s", dev.Host, path) + logger.Info("URL", "method", method, "url", url) + + var request *http.Request + request, err = http.NewRequest(method, url, bytes.NewBuffer(reqBody)) + if err != nil { + return + } + request.Header.Add("Authorization", "Basic "+dev.auth) + + var response *http.Response + response, err = dev.httpClient.Do(request) + if err != nil { + if response == nil { + logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody), "error", err) + } else { + logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody), "code", response.StatusCode) + } + return + } else { + logger.Debug("REST call", "op", "request", "method", method, "path", path, "body", string(reqBody)) + } + + body, _ = io.ReadAll(response.Body) + + if response.StatusCode < 200 || response.StatusCode >= 300 { + err = fmt.Errorf("%s", body) + logger.Debug("REST call", "op", "response", "method", method, "path", path, "code", response.StatusCode, "body", string(body)) + return + } + + logger.Debug("REST call", "op", "response", "method", method, "path", path, "code", response.StatusCode, "body", string(body)) + return +} // }}} +func (dev *RouterosDevice) GetIdentity() (identity string, err error) { // {{{ + var body []byte + body, err = dev.query("GET", "/system/identity", []byte{}) + + name := struct{ Name string }{} + err = json.Unmarshal(body, &name) + if err != nil { + return + } + + identity = name.Name + return +} // }}} + +func (dev *RouterosDevice) StaticDNSEntries() (entries []DNSRecord, err error) { + entries = []DNSRecord{} + + var body []byte + body, err = dev.query("GET", "/ip/dns/static", []byte{}) + if err != nil { + return + } + + err = json.Unmarshal(body, &entries) + if err != nil { + return + } + + return +} + +/* +// FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router +func (dev *RouterosDevice) FillPeerDetails(peer *Peer) (err error) { // {{{ + var body []byte + + // Retrieve the wireguard peer to get the IP address + body, err = dev.query("GET", "/interface/wireguard/peers?public-key="+strings.TrimSpace(peer.PublicKey), []byte{}) + if err != nil { + return + } + + devPeer := []struct { + ID string `json:".id"` + AllowedAddress string `json:"allowed-address"` + Comment string + }{} + + err = json.Unmarshal(body, &devPeer) + if err != nil { + return + } + + if len(devPeer) != 1 { + err = fmt.Errorf("Invalid number of peers returned (%d peers)", len(devPeer)) + return + } + peer.routerosID = devPeer[0].ID + peer.AllowedIPs = strings.Split(devPeer[0].AllowedAddress, ",") + peer.Comment = devPeer[0].Comment + + var row *sqlx.Row + row = service.Db.Conn.QueryRowx(` + SELECT + dev.config_template, + p.id, p.user_id, p.routeros_device_id, p.public_key, p.description, + _webservice.pgp_sym_decrypt( + DECODE(p.private_key, 'base64'), + $3, + 'compress-algo=1, cipher-algo=aes256' + ) AS private_key + + FROM public.peer p + INNER JOIN routeros_device dev ON p.routeros_device_id = dev.id + WHERE + p.public_key = $1 OR p.id = $2 + `, + peer.PublicKey, + peer.ID, + config.Application.Secret, + ) + if err != nil { + return + } + + var dbPeer Peer + err = row.StructScan(&dbPeer) + if err != nil { + return + } + + peer.ConfigTemplate = dbPeer.ConfigTemplate + peer.PrivateKey = dbPeer.PrivateKey + peer.PublicKey = dbPeer.PublicKey + + return +} // }}} +func (dev *RouterosDevice) GetPeerStatus(peer *Peer, fillAllowedIPs bool) (allowedIPs []string, listID []RouterosID, exist bool, timeLeft int, err error) { // {{{ + var body []byte + + // Retrieve the wireguard peer to get the IP address + body, err = dev.query("GET", "/interface/wireguard/peers?public-key="+strings.TrimSpace(peer.PublicKey), []byte{}) + if err != nil { + return + } + + devPeer := []struct { + ID string `json:".id"` + AllowedAddress string `json:"allowed-address"` + Comment string + }{} + + err = json.Unmarshal(body, &devPeer) + if err != nil { + return + } + + if len(devPeer) != 1 { + err = fmt.Errorf("Invalid number of peers returned (%d peers)", len(devPeer)) + return + } + exist = true + peer.routerosID = devPeer[0].ID + + // Finding if it is open or not + prefix := regexp.MustCompile("(?i)([0-9.]+/32|[0-9a-f:]/128)$") + rxpTime := regexp.MustCompile("(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?") + allowedAddresses := strings.Split(devPeer[0].AllowedAddress, ",") + for _, ip := range allowedAddresses { + if !prefix.Match([]byte(ip)) { + logger.Debug("peer", "public-key", peer.PublicKey, "allowed-address", ip, "error", "Invalid address, must end in /30 or /128") + continue + } + allowedIPs = append(allowedIPs, ip) + + ipParts := strings.Split(ip, "/") + listIDEntry := RouterosID{} + if len(ip) > 4 && ip[len(ip)-4:] == "/128" { + listIDEntry.Type = IPv6 + body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{}) + } else if len(ip) > 3 && ip[len(ip)-3:] == "/32" { + listIDEntry.Type = IPv4 + body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{}) + } else { + err = fmt.Errorf("Invalid IP (%s)", ip) + return + } + if err != nil { + return + } + + listEntry := []struct { + ID string `json:".id"` + Disabled string + Timeout string + }{} + err = json.Unmarshal(body, &listEntry) + if err != nil || len(listEntry) != 1 { + continue + } + listIDEntry.ID = listEntry[0].ID + listID = append(listID, listIDEntry) + + // Overwrite earlier timeout, as these should be the same value anyway. + timeLeft = 0 + time := rxpTime.FindStringSubmatch(listEntry[0].Timeout) + if i, err := strconv.Atoi(time[1]); err == nil { + timeLeft = i * 86400 + } + if i, err := strconv.Atoi(time[2]); err == nil { + timeLeft += i * 3600 + } + if i, err := strconv.Atoi(time[3]); err == nil { + timeLeft += i * 60 + } + if i, err := strconv.Atoi(time[4]); err == nil { + timeLeft += i + } + } + + if fillAllowedIPs { + peer.AllowedIPs = allowedIPs + } + + return +} // }}} +func (dev *RouterosDevice) IsCachedIPInAddressList(peer *Peer) (inList bool, timeLeft int, err error) { // {{{ + var body []byte + + // Finding if it is open or not + prefix := regexp.MustCompile("(?i)([0-9.]+/32|[0-9a-f:]/128)$") + rxpTime := regexp.MustCompile("(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?") + allowedAddresses := peer.CachedAddresses + for _, ip := range allowedAddresses { + // This indicates existence of key and address in GetPeerStatus, + // but can't here since it is too expensive to check device wireguard peers. + inList = true + + if !prefix.Match([]byte(ip)) { + logger.Debug("peer", "public-key", peer.PublicKey, "allowed-address", ip, "error", "Invalid address, must end in /30 or /128") + continue + } + + ipParts := strings.Split(ip, "/") + listIDEntry := RouterosID{} + if len(ip) > 4 && ip[len(ip)-4:] == "/128" { + listIDEntry.Type = IPv6 + body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{}) + } else if len(ip) > 3 && ip[len(ip)-3:] == "/32" { + listIDEntry.Type = IPv4 + body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{}) + } else { + err = fmt.Errorf("Invalid IP (%s)", ip) + return + } + if err != nil { + return + } + + listEntry := []struct { + ID string `json:".id"` + Disabled string + Timeout string + }{} + err = json.Unmarshal(body, &listEntry) + if err != nil || len(listEntry) != 1 { + continue + } + listIDEntry.ID = listEntry[0].ID + + // Overwrite earlier timeout, as these should be the same value anyway. + timeLeft = 0 + time := rxpTime.FindStringSubmatch(listEntry[0].Timeout) + if i, err := strconv.Atoi(time[1]); err == nil { + timeLeft = i * 86400 + } + if i, err := strconv.Atoi(time[2]); err == nil { + timeLeft += i * 3600 + } + if i, err := strconv.Atoi(time[3]); err == nil { + timeLeft += i * 60 + } + if i, err := strconv.Atoi(time[4]); err == nil { + timeLeft += i + } + } + + return +} // }}} +func (dev *RouterosDevice) ClosePeer(sess *session.T, peer Peer) (err error) { // {{{ + var body []byte + + var listID []RouterosID + for _, ip := range peer.CachedAddresses { + ipParts := strings.Split(ip, "/") + listIDEntry := RouterosID{} + if len(ip) > 4 && ip[len(ip)-4:] == "/128" { + listIDEntry.Type = IPv6 + body, err = dev.query("GET", "/ipv6/firewall/address-list?list=Wireguard-Authenticated&address="+ip, []byte{}) + } else if len(ip) > 3 && ip[len(ip)-3:] == "/32" { + listIDEntry.Type = IPv4 + body, err = dev.query("GET", "/ip/firewall/address-list?list=Wireguard-Authenticated&address="+ipParts[0], []byte{}) + } else { + err = fmt.Errorf("Invalid IP (%s)", ip) + return + } + if err != nil { + return + } + + listEntry := []struct { + ID string `json:".id"` + Disabled string + Timeout string + }{} + err = json.Unmarshal(body, &listEntry) + if err != nil || len(listEntry) != 1 { + continue + } + listIDEntry.ID = listEntry[0].ID + listID = append(listID, listIDEntry) + } + + for _, listID := range listID { + switch listID.Type { + case IPv4: + body, err = dev.query("DELETE", "/ip/firewall/address-list/"+listID.ID, []byte{}) + case IPv6: + body, err = dev.query("DELETE", "/ipv6/firewall/address-list/"+listID.ID, []byte{}) + } + if err != nil { + return + } + if string(body) != "" { + devError := struct { + Error int + Message string + Detail string + }{} + err = json.Unmarshal(body, &devError) + if err != nil { + return + } + if devError.Message != "" { + err = fmt.Errorf("%s", devError.Message) + return + } + } + } + + return +} // }}} +func (dev *RouterosDevice) OpenPeer(sess *session.T, peer Peer) (err error) { // {{{ + allowedIPs := peer.CachedAddresses + + var body []byte + for _, ip := range allowedIPs { + req, _ := json.Marshal(struct { + List string `json:"list"` + Address string `json:"address"` + Timeout string `json:"timeout"` + Comment string `json:"comment"` + }{ + "Wireguard-Authenticated", + ip, + "08:00:00", + sess.Username + ", " + peer.Description, + }, + ) + + if len(ip) > 4 && ip[len(ip)-4:] == "/128" { + body, err = dev.query("PUT", "/ipv6/firewall/address-list", req) + } else if len(ip) > 3 && ip[len(ip)-3:] == "/32" { + body, err = dev.query("PUT", "/ip/firewall/address-list", req) + } else { + err = fmt.Errorf("Invalid IP (%s)", ip) + return + } + if err != nil { + return + } + devError := struct { + Error int + Message string + Detail string + }{} + err = json.Unmarshal(body, &devError) + if err != nil { + return + } + if devError.Message != "" { + err = fmt.Errorf("%s", devError.Message) + return + } + } + + return +} // }}} +func (dev *RouterosDevice) GetPeers() (err error) { // {{{ + dev.Peers = []*Peer{} + + var rows *sqlx.Rows + rows, err = service.Db.Conn.Queryx( + ` + SELECT + p.id, + p.user_id, + p.routeros_device_id, + p.description, + p.public_key, + _webservice.pgp_sym_decrypt( + DECODE(p.private_key, 'base64'), + $2, + 'compress-algo=1, cipher-algo=aes256' + ) AS private_key, + cached_addresses + FROM public.peer p + INNER JOIN _webservice.user u ON p.user_id = u.id + WHERE + routeros_device_id = $1 + ORDER BY + u.name ASC, + p.description ASC + `, + dev.ID, + config.Application.Secret, + ) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var peer Peer + if err = rows.StructScan(&peer); err != nil { + return + } + dev.Peers = append(dev.Peers, &peer) + } + + return +} // }}} +*/ +/* +func (dev *RouterosDevice) UpdatePeer(peer Peer) (err error) { // {{{ + allowedIPs := peer.AllowedIPs + + _, err = service.Db.Conn.Exec( + `UPDATE public.peer + SET + description = $2, + user_id = $3, + cached_addresses = $4 + WHERE + id = $1`, + peer.ID, + peer.Description, + peer.UserID, + peer.CachedAddresses, + ) + if err != nil { + return + } + + err = dev.FillPeerDetails(&peer) + if err != nil { + return + } + + // This isn't necessarily set + if allowedIPs == nil { + return + } + + req := struct { + AllowedAddress string `json:"allowed-address"` + }{ + strings.Join(allowedIPs, ","), + } + reqJSON, _ := json.Marshal(req) + + _, err = dev.query("PATCH", "/interface/wireguard/peers/"+peer.routerosID, reqJSON) + if err != nil { + return + } + + return +} // }}} +func (dev *RouterosDevice) CreatePeer(peer Peer) (err error) { // {{{ + req := struct { + Interface string `json:"interface"` + PublicKey string `json:"public-key"` + AllowedAddresses string `json:"allowed-address"` + Comment string `json:"comment"` + }{ + dev.WireguardInterface, + peer.PublicKey, + strings.Join(peer.AllowedIPs, ","), + peer.Comment, + } + reqJSON, _ := json.Marshal(req) + _, err = dev.query("PUT", "/interface/wireguard/peers", reqJSON) + return +} // }}} +func (dev *RouterosDevice) DeletePeer(peer Peer) (err error) { // {{{ + if err = dev.FillPeerDetails(&peer); err != nil { + return + } + _, err = dev.query("DELETE", "/interface/wireguard/peers/"+peer.routerosID, []byte{}) + return +} // }}} +func (dev *RouterosDevice) AllowedIPs() (peers []*PeerIPs, err error) { // {{{ + var body []byte + body, err = dev.query("GET", "/interface/wireguard/peers?interface="+dev.WireguardInterface, nil) + if err != nil { + return + } + + err = json.Unmarshal(body, &peers) + if err != nil { + return + } + + for _, peer := range peers { + peer.AllowedIPs = strings.Split(peer.AllowedAddress, ",") + } + + return +} // }}} +*/ diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..b310509 --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,12 @@ +html { + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +[onClick] { + cursor: pointer; +} + diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl new file mode 100644 index 0000000..92a8d15 --- /dev/null +++ b/views/layouts/main.gotmpl @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + +
+ {{ block "page" . }}{{ end }} +
+ + + diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl new file mode 100644 index 0000000..2b95ea6 --- /dev/null +++ b/views/pages/index.gotmpl @@ -0,0 +1,7 @@ +{{ define "page" }} +

{{ .Data.Identity }}

+ +{{ range .Data.DNSRecords }} +
{{ .NameReversed }} +{{ end }} +{{ end }} diff --git a/webserver.go b/webserver.go new file mode 100644 index 0000000..844caaa --- /dev/null +++ b/webserver.go @@ -0,0 +1,85 @@ +package main + +import ( + // External + "git.gibonuddevalla.se/go/html_template" + + // Standard + "embed" + "encoding/json" + "fmt" + "net/http" + "os" + "slices" +) + +var ( + htmlEngine HTMLTemplate.Engine + + //go:embed static + staticFS embed.FS + + //go:embed views + viewFS embed.FS +) + +func registerWebserverHandlers() { + var err error + htmlEngine, err = HTMLTemplate.NewEngine(viewFS, staticFS, flagDev) + if err != nil { + logger.Error("webserver", "error", err) + os.Exit(1) + } + + http.HandleFunc("/", rootHandler) +} + +func startWebserver() { + listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) + logger.Info("webserver", "listen", listen) + http.ListenAndServe(listen, nil) +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + page := HTMLTemplate.SimplePage{} + page.Layout = "main" + page.Page = "index" + + var err error + data := make(map[string]any) + + data["Identity"], err = device.GetIdentity() + if err != nil { + w.Write([]byte(err.Error())) + return + } + + var entries []DNSRecord + entries, err = device.StaticDNSEntries() + if err != nil { + w.Write([]byte(err.Error())) + return + } + + slices.SortFunc(entries, SortDNSRecord) + data["DNSRecords"] = entries + + tree := RecordsTree{} + tree = tree.BuildTree(entries, 0, 1) + + j, _ := json.Marshal(tree) + os.WriteFile("/tmp/tree.json", j, 0644) + + page.Data = data + + err = htmlEngine.Render(page, w, r) + if err != nil { + w.Write([]byte(err.Error())) + return + } + return + } + + htmlEngine.StaticResource(w, r) +}