From 65882afcffc33dfdc604599d5de0a5f904b7f100 Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 24 Jul 2023 21:54:56 -0400 Subject: [PATCH] initial --- .gitignore | 3 + Containerfile | 12 +++ go.mod | 32 ++++++++ go.sum | 54 ++++++++++++++ main.go | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 31 ++++++++ 6 files changed, 335 insertions(+) create mode 100644 .gitignore create mode 100644 Containerfile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7461ea9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config.yaml +env +tildewatch diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..5944163 --- /dev/null +++ b/Containerfile @@ -0,0 +1,12 @@ +FROM golang:1.20 as builder +WORKDIR /go/src/app +COPY . . +RUN apt update && apt upgrade -y +RUN CGO_ENABLED=0 go build + +FROM alpine:latest as final +WORKDIR /app +RUN mkdir -p /lib64 +COPY --from=builder /go/src/app/tildewatch /app/ + +CMD ["/app/tildewatch"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..454caff --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +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 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/lipgloss v0.7.1 // 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-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/smarty/assertions v1.15.1 // indirect + golang.org/x/sys v0.10.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + moul.io/http2curl v1.0.0 // indirect +) + +replace github.com/go-gandi/go-gandi => github.com/stryan/go-gandi v0.0.0-20230725010359-2d3115fcccde diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d8a5527 --- /dev/null +++ b/go.sum @@ -0,0 +1,54 @@ +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/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-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= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +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-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/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/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/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= +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= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..33bbff6 --- /dev/null +++ b/main.go @@ -0,0 +1,203 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "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" +) + +func load_config(filename string) (*Config, error) { + u := &Config{} + yamlFile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(yamlFile, u) + if err != nil { + return nil, err + } + return u, nil +} + +func main() { + ctx, endFn := context.WithCancel(context.Background()) + domain, ok := os.LookupEnv("WATCH_DOMAIN") + if !ok { + log.Fatal("no domain specified") + } + apikey, ok := os.LookupEnv("WATCH_API") + if !ok { + log.Fatal("no Gandi api key") + } + port, ok := os.LookupEnv("WATCH_PORT") + if !ok { + port = "6283" + } + file, ok := os.LookupEnv("WATCH_FILE") + if !ok { + file = "./config.yaml" + } + + debug := os.Getenv("WATCH_DEBUG") + watchusers, err := load_config(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), + debug: debug != "", + domain: domain, + conf: conf, + } + for _, v := range watchusers.Users { + a.Tildes[v.Secret] = Registration{ + Domain: v.Hostname, + Update: false, + IPAddr: "", + } + } + 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) + signal.Notify(c, os.Interrupt) + go func() { + <-c + log.Info("trying to shutdown cleanly") + endFn() + server.Shutdown(ctx) + }() + + wg.Add(1) + go func() { + server.ListenAndServe() + wg.Done() + log.Info("watch web server shutdown") + }() + + wg.Add(1) + go func() { + a.run(ctx) + wg.Done() + log.Info("watch updater shutdown") + }() + log.Info("watch running") + wg.Wait() + log.Info("shut down") + + return +} + +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 { + for { + ticker := time.NewTicker(5 * time.Second) + select { + case <-ticker.C: + for i, v := range watch.Tildes { + if v.Update { + err := watch.run_update(i, v) + if err != nil { + log.Warn("error running update", "error", err) + } + } + } + + case <-ctx.Done(): + ticker.Stop() + return nil + } + } +} + +func (watch *Watch) run_update(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 +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..e84f596 --- /dev/null +++ b/types.go @@ -0,0 +1,31 @@ +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"` +} + +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 +}