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 }