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 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 } } }