gldnsmerge #1
19
.drone.yml
19
.drone.yml
@ -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
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
config.yaml
|
config.yaml
|
||||||
env
|
.envrc
|
||||||
tildewatch
|
tildewatch
|
||||||
|
128
main.go
128
main.go
@ -2,20 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"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"
|
"github.com/go-gandi/go-gandi/config"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,34 +47,33 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug := os.Getenv("WATCH_DEBUG")
|
debug := os.Getenv("WATCH_DEBUG")
|
||||||
watchusers, err := loadConfig(file)
|
watchconf, err := loadConfig(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, v := range watchusers.Users {
|
|
||||||
if v.Hostname == "" {
|
|
||||||
log.Fatal("bad config", "user", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := config.Config{
|
conf := config.Config{
|
||||||
APIKey: apikey,
|
APIKey: apikey,
|
||||||
}
|
}
|
||||||
|
|
||||||
a := &Watch{
|
a := &Tilde{
|
||||||
Tildes: make(map[string]Registration),
|
|
||||||
debug: debug != "",
|
debug: debug != "",
|
||||||
domain: domain,
|
domain: domain,
|
||||||
conf: conf,
|
conf: conf,
|
||||||
}
|
}
|
||||||
for _, v := range watchusers.Users {
|
for _, v := range watchconf.Users {
|
||||||
a.Tildes[v.Secret] = Registration{
|
err := a.Add(&v)
|
||||||
Domain: v.Hostname,
|
if err != nil {
|
||||||
Update: false,
|
log.Fatal(err)
|
||||||
IPAddr: "",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Info(a.Tildes)
|
for _, v := range watchconf.Hosts {
|
||||||
|
err := a.Add(&v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info(a)
|
||||||
server := &http.Server{Addr: fmt.Sprintf("0.0.0.0:%v", port), Handler: a.server()}
|
server := &http.Server{Addr: fmt.Sprintf("0.0.0.0:%v", port), Handler: a.server()}
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
@ -97,8 +91,8 @@ func main() {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
err := server.ListenAndServe()
|
err := server.ListenAndServe()
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Warn("error shutting down web server", "error", err)
|
log.Warn(err)
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
log.Info("watch web server shutdown")
|
log.Info("watch web server shutdown")
|
||||||
@ -118,93 +112,3 @@ func main() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
log.Info("shut down")
|
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 successfully", "subdomain", reg.Domain)
|
|
||||||
}
|
|
||||||
toUp := watch.Tildes[token]
|
|
||||||
toUp.Update = false
|
|
||||||
watch.Tildes[token] = toUp
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
90
registration.go
Normal file
90
registration.go
Normal 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("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
|
||||||
|
}
|
128
tilde.go
Normal file
128
tilde.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
types.go
24
types.go
@ -1,31 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/go-gandi/go-gandi/config"
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Hostname string `yaml:"hostname"`
|
|
||||||
Secret string `yaml:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Users []User `yaml:"users"`
|
Users []UserRegistration `yaml:"users"`
|
||||||
|
Hosts []HostRegistration `yaml:"hosts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
IPAddr string `json:"ipaddr"`
|
IPAddr string `json:"ipaddr"`
|
||||||
Secret string `json:"secret"`
|
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
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user