gldnsmerge #1

Merged
stryan merged 3 commits from gldnsmerge into master 2024-06-26 22:56:15 -04:00
8 changed files with 269 additions and 186 deletions

View File

@ -1,19 +0,0 @@
kind: pipeline
name: default
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init --recursive
- name: build-container
image: plugins/docker
settings:
repo: git.saintnet.tech/stryan/tildewatch
registry: git.saintnet.tech
password:
from_secret: build_pass
username:
from_secret: build_username
dockerfile: Containerfile
tags: latest
layers: true

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
config.yaml
env
.envrc
tildewatch

21
go.mod
View File

@ -3,29 +3,28 @@ module git.saintnet.tech/stryan/tildewatch
go 1.20
require (
github.com/charmbracelet/log v0.2.2
github.com/go-chi/chi v1.5.4
github.com/go-chi/chi/v5 v5.0.10
github.com/go-gandi/go-gandi v0.6.0
golang.org/x/exp v0.0.0-20230724220655-d98519c11495
github.com/charmbracelet/log v0.4.0
github.com/go-chi/chi/v5 v5.0.14
github.com/go-gandi/go-gandi v0.7.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/smarty/assertions v1.15.1 // indirect
golang.org/x/sys v0.10.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
golang.org/x/sys v0.21.0 // indirect
moul.io/http2curl v1.0.0 // indirect
)

39
go.sum
View File

@ -1,14 +1,14 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/charmbracelet/log v0.2.2 h1:CaXgos+ikGn5tcws5Cw3paQuk9e/8bIwuYGhnkqQFjo=
github.com/charmbracelet/log v0.2.2/go.mod h1:Zs11hKpb8l+UyX4y1srwZIGW+MPCXJHIty3MB9l/sno=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@ -18,33 +18,30 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk=
github.com/smarty/assertions v1.15.1/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stryan/go-gandi v0.0.0-20230725010359-2d3115fcccde h1:HNiHYiVE8tjdx8NRgRyXWj7HltWZT4cPcHI3zZmWJGM=
github.com/stryan/go-gandi v0.0.0-20230725010359-2d3115fcccde/go.mod h1:jRMLmlWJUFaFHHP7mSm5pYFrjoU2PU0mYKEHWdczksU=
golang.org/x/exp v0.0.0-20230724220655-d98519c11495 h1:zKGKw2WlGb8oPoRGqQ2PT8g2YoCN1w/YbbQjHXCdUWE=
golang.org/x/exp v0.0.0-20230724220655-d98519c11495/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

131
main.go
View File

@ -2,20 +2,15 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-gandi/go-gandi"
"github.com/go-gandi/go-gandi/config"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v2"
)
@ -52,34 +47,36 @@ func main() {
}
debug := os.Getenv("WATCH_DEBUG")
watchusers, err := loadConfig(file)
watchconf, err := loadConfig(file)
if err != nil {
log.Fatal(err)
}
for _, v := range watchusers.Users {
if v.Hostname == "" {
log.Fatal("bad config", "user", v)
}
}
conf := config.Config{
APIKey: apikey,
}
a := &Watch{
Tildes: make(map[string]Registration),
a := &Tilde{
debug: debug != "",
domain: domain,
conf: conf,
}
for _, v := range watchusers.Users {
a.Tildes[v.Secret] = Registration{
Domain: v.Hostname,
Update: false,
IPAddr: "",
count := 0
for _, v := range watchconf.Users {
err := a.Add(&v)
if err != nil {
log.Fatal(err, "reg", count)
}
count++
}
count = 0
for _, v := range watchconf.Hosts {
err := a.Add(&v)
if err != nil {
log.Fatal(err, "reg", count)
}
count++
}
log.Info(a.Tildes)
server := &http.Server{Addr: fmt.Sprintf("0.0.0.0:%v", port), Handler: a.server()}
var wg sync.WaitGroup
c := make(chan os.Signal, 1)
@ -97,8 +94,8 @@ func main() {
wg.Add(1)
go func() {
err := server.ListenAndServe()
if err != nil {
log.Warn("error shutting down web server", "error", err)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Warn(err)
}
wg.Done()
log.Info("watch web server shutdown")
@ -118,93 +115,3 @@ func main() {
wg.Wait()
log.Info("shut down")
}
func (watch *Watch) server() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Post("/submit", func(w http.ResponseWriter, r *http.Request) {
var payload Request
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
log.Warn("error parsing ip submission", "error", err)
w.WriteHeader(http.StatusBadRequest)
return
}
found := false
for i, v := range watch.Tildes {
if payload.Secret == i && v.IPAddr != payload.IPAddr {
cur := watch.Tildes[i]
watch.Tildes[i] = Registration{
IPAddr: payload.IPAddr,
Update: true,
Domain: cur.Domain,
}
found = true
}
}
if !found {
w.WriteHeader(http.StatusUnauthorized)
}
w.WriteHeader(http.StatusOK)
})
return r
}
func (watch *Watch) run(ctx context.Context) error {
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-ticker.C:
for i, v := range watch.Tildes {
if v.Update {
err := watch.runUpdate(i, v)
if err != nil {
log.Warn("error running update", "error", err)
}
}
}
case <-ctx.Done():
ticker.Stop()
return nil
}
}
}
func (watch *Watch) runUpdate(token string, reg Registration) error {
if reg.Domain == "" || reg.IPAddr == "" {
return fmt.Errorf("invalid registration for update %v: %v", reg.Domain, reg.IPAddr)
}
if watch.debug {
log.Info("would run updates", "subdomain", reg.Domain)
return nil
}
log.Info("updating registration", "subdomain", reg.Domain, "ip", reg.IPAddr)
dnsclient := gandi.NewLiveDNSClient(watch.conf)
log.Info("getting domain records", "domain", watch.domain, "subdomain", reg.Domain)
curr, err := dnsclient.GetDomainRecordsByName(watch.domain, reg.Domain)
if err != nil {
return fmt.Errorf("error getting domain records: %w", err)
}
if len(curr) == 0 {
log.Info("no records found for subdomain, not forcing creationg", "subdomain", reg.Domain)
}
for _, v := range curr {
if v.RrsetType == "A" {
if slices.Contains(v.RrsetValues, reg.IPAddr) {
log.Info("not updating, already set", "host", reg.Domain, "ip", reg.IPAddr)
} else {
_, err := dnsclient.UpdateDomainRecordByNameAndType(watch.domain, reg.Domain, "A", 900, []string{reg.IPAddr})
if err != nil {
return fmt.Errorf("error updating record %w", err)
}
log.Info("updating domain succesfully", "subdomain", reg.Domain)
}
toUp := watch.Tildes[token]
toUp.Update = false
watch.Tildes[token] = toUp
}
}
return nil
}

90
registration.go Normal file
View File

@ -0,0 +1,90 @@
package main
import (
"io"
"net/http"
"strings"
"github.com/go-gandi/go-gandi/config"
)
type Registration interface {
Valid() bool
Update(config.Config) (string, error)
HasSecret(string) bool
Domain() string
IP() string
}
type UserRegistration struct {
Subdomain string `yaml:"subdomain"`
Secret string `yaml:"secret"`
ip string
update bool
}
func (u *UserRegistration) Valid() bool {
return u.Subdomain != "" && u.Secret != ""
}
func (u *UserRegistration) Update(c config.Config) (string, error) {
if u.update {
u.update = false
return u.ip, nil
}
return "", nil
}
func (u *UserRegistration) HasSecret(s string) bool {
return u.Secret == s
}
func (u *UserRegistration) Domain() string {
return u.Subdomain
}
func (u *UserRegistration) IP() string {
return u.ip
}
type HostRegistration struct {
Subdomain string `yaml:"subdomain"`
ipddr string
}
func (h HostRegistration) Valid() bool {
return h.Subdomain != ""
}
func (h *HostRegistration) Update(_ config.Config) (string, error) {
resp, err := http.Get("https://icanhazip.com")
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var result string
dynip := strings.TrimSpace(string(body))
if dynip != h.ipddr {
h.ipddr = dynip
result = dynip
}
return result, nil
}
func (h *HostRegistration) HasSecret(_ string) bool {
return false
}
func (h *HostRegistration) Domain() string {
return h.Subdomain
}
func (h *HostRegistration) IP() string {
return h.ipddr
}

129
tilde.go Normal file
View File

@ -0,0 +1,129 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"time"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-gandi/go-gandi"
"github.com/go-gandi/go-gandi/config"
)
type Tilde struct {
regs []Registration
debug bool
domain string
conf config.Config
}
var errRegNotFound = errors.New("registration not found")
func (t *Tilde) Add(r Registration) error {
if !r.Valid() {
return errors.New("invalid registration")
}
_, err := r.Update(t.conf)
if err != nil {
return err
}
t.regs = append(t.regs, r)
return nil
}
func (t *Tilde) UpdateReg(secret string, ip string) error {
index := slices.IndexFunc(t.regs, func(ele Registration) bool { return ele.HasSecret(secret) })
if index == -1 {
return errRegNotFound
}
reg, ok := t.regs[index].(*UserRegistration)
if !ok {
return errors.New("error getting user reg")
}
reg.ip = ip
reg.update = true
return nil
}
func (t *Tilde) Sync(cnf config.Config) error {
for _, reg := range t.regs {
result, err := reg.Update(cnf)
if err != nil {
return err
}
if result != "" {
log.Info("updating registration", "subdomain", reg.Domain(), "ip", reg.IP())
dnsclient := gandi.NewLiveDNSClient(cnf)
log.Info("getting domain records")
curr, err := dnsclient.GetDomainRecordsByName(t.domain, reg.Domain())
if err != nil {
return fmt.Errorf("error getting domain records: %w", err)
}
if len(curr) == 0 {
log.Info("no records found for subdomain, not forcing creation", "subdomain", reg.Domain())
}
for _, v := range curr {
if v.RrsetType == "A" {
if slices.Contains(v.RrsetValues, reg.IP()) {
log.Info("not updating, already set", "host", reg.Domain(), "ip", reg.IP())
} else {
_, err := dnsclient.UpdateDomainRecordByNameAndType(t.domain, reg.Domain(), "A", 900, []string{reg.IP()})
if err != nil {
return fmt.Errorf("error updating record %w", err)
}
log.Info("updating domain successfully", "subdomain", reg.Domain())
}
}
}
}
}
return nil
}
func (watch *Tilde) server() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Post("/submit", func(w http.ResponseWriter, r *http.Request) {
var payload Request
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
log.Warn("error parsing ip submission", "error", err)
w.WriteHeader(http.StatusBadRequest)
return
}
err = watch.UpdateReg(payload.Secret, payload.IPAddr)
if err != nil {
if errors.Is(err, errRegNotFound) {
w.WriteHeader(http.StatusUnauthorized)
}
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
})
return r
}
func (watch *Tilde) run(ctx context.Context) error {
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-ticker.C:
err := watch.Sync(watch.conf)
if err != nil {
log.Warn(err)
}
case <-ctx.Done():
ticker.Stop()
return nil
}
}
}

View File

@ -1,31 +1,11 @@
package main
import "github.com/go-gandi/go-gandi/config"
type User struct {
Hostname string `yaml:"hostname"`
Secret string `yaml:"secret"`
}
type Config struct {
Users []User `yaml:"users"`
Users []UserRegistration `yaml:"users"`
Hosts []HostRegistration `yaml:"hosts"`
}
type Request struct {
IPAddr string `json:"ipaddr"`
Secret string `json:"secret"`
}
type Registration struct {
IPAddr string
Domain string
Update bool
}
type Watch struct {
Tildes map[string]Registration
debug bool
domain string
conf config.Config
}