211 lines
4.7 KiB
Go
211 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
func loadConfig(filename string) (*Config, error) {
|
|
u := &Config{}
|
|
yamlFile, err := os.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 := 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),
|
|
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()
|
|
err := server.Shutdown(ctx)
|
|
if err != nil {
|
|
log.Warn("error shutting down tilde server", "error", err)
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
err := server.ListenAndServe()
|
|
if err != nil {
|
|
log.Warn("error shutting down web server", "error", err)
|
|
}
|
|
wg.Done()
|
|
log.Info("watch web server shutdown")
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
err := a.run(ctx)
|
|
if err != nil {
|
|
log.Warn("error shutting down update server", "error", err)
|
|
}
|
|
|
|
wg.Done()
|
|
log.Info("watch updater shutdown")
|
|
}()
|
|
log.Info("watch running")
|
|
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 {
|
|
for {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
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
|
|
}
|