package main import ( "FeatherDDNS/cache" "FeatherDDNS/dns" _ "FeatherDDNS/dns" "FeatherDDNS/ratelimit" "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) type Config struct { ListenPort string `json:"listenPort"` AdminToken string `json:"adminToken"` RateLimit RateLimit `json:"ratelimit"` ApiTokens ApiTokens `json:"apiTokens"` Clients []Client `json:"clients"` } type RateLimit struct { MaxTokens int `json:"maxTokens"` RefillRate int `json:"refillRate"` TokensPerHit int `json:"tokensPerHit"` } type ApiTokens struct { Cloudflare string `json:"cloudflare"` } type Client struct { FQDN string `json:"fqdn"` Token string `json:"token"` DNSProvider string `json:"dnsProvider"` ZoneId string `json:"zoneId"` } var config Config // Should have CF_API_TOKEN env set func main() { fmt.Println("Starting Feather DDNS...") startUS := time.Now().UnixMicro() loadConfig() gin.SetMode(gin.ReleaseMode) rl := ratelimit.NewRateLimiter( config.RateLimit.MaxTokens, time.Duration(config.RateLimit.RefillRate)*time.Second, config.RateLimit.TokensPerHit, ) defer rl.Stop() engine := gin.New() engine.Use(gin.Logger(), gin.Recovery()) engine.Use(rl.Middleware()) engine.GET("/ping", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) engine.GET("/token/generate", func(ctx *gin.Context) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{ "message": err.Error(), }) return } ctx.JSON(http.StatusOK, gin.H{ "token": base64.URLEncoding.EncodeToString(b), }) }) engine.GET("/reload", func(ctx *gin.Context) { authHeader := ctx.GetHeader("Authorization") authToken := strings.TrimPrefix(authHeader, "Bearer ") if authToken == config.AdminToken { loadConfig() log.Println("Configuration has been reloaded") ctx.JSON(http.StatusOK, gin.H{ "message": "Configuration has been reloaded", }) return } ctx.JSON(http.StatusUnauthorized, gin.H{ "message": "Unauthorized", }) }) engine.GET("/update", func(ctx *gin.Context) { fqdn := ctx.Query("fqdn") authHeader := ctx.GetHeader("Authorization") authToken := strings.TrimPrefix(authHeader, "Bearer ") client, err := getClient(fqdn) if err != nil { log.Println("Error: " + err.Error()) ctx.JSON(http.StatusUnauthorized, gin.H{ "message": "Unauthorized", }) return } if authToken == client.Token { var ip string if strings.TrimSpace(ctx.GetHeader("X-Source-IP")) == "" { ip = ctx.ClientIP() } else { ip = strings.TrimSpace(ctx.GetHeader("X-Source-IP")) fmt.Println("Using IP from header") } fmt.Printf("Request to update %s to IP %s\n", fqdn, ip) if !cache.GetCache().NeedsUpdate(fqdn, client.DNSProvider, ip) { ctx.JSON(http.StatusOK, gin.H{ "message": "IP unchanged, skipped", }) return } switch client.DNSProvider { case "cloudflare": { err = dns.UpdateCloudflare(config.ApiTokens.Cloudflare, client.ZoneId, client.FQDN, ip) break } } if err != nil { log.Println("Error: " + err.Error()) ctx.JSON(http.StatusInternalServerError, gin.H{ "message": "Internal Server Error, check logs for more info", }) return } cache.GetCache().Set(fqdn, client.DNSProvider, ip) ctx.JSON(http.StatusOK, gin.H{ "message": "Updated " + fqdn + " to resolve to " + ip, }) return } ctx.JSON(http.StatusUnauthorized, gin.H{ "message": "Unauthorized", }) }) usElapsed := time.Now().UnixMicro() - startUS fmt.Println("Feather DDNS started in " + strconv.Itoa(int(usElapsed)) + "us") fmt.Println("Running on port " + config.ListenPort) err := engine.Run(":" + config.ListenPort) if err != nil { panic(err) } } func loadConfig() { config = Config{} var cfgFile *os.File var err error if os.Getenv("FEATHERDDNS_DEBUG") == "true" { cfgFile, err = os.Open("./config.json") } else { cfgFile, err = os.Open("/etc/featherddns/config.json") } if err != nil { panic(err) } defer cfgFile.Close() rawJson, err := io.ReadAll(cfgFile) if err != nil { panic(err) } err = json.Unmarshal(rawJson, &config) if err != nil { panic(err) } } func getClient(fqdn string) (*Client, error) { for _, client := range config.Clients { if client.FQDN == fqdn { return &client, nil } } return &Client{}, errors.New("client not found") }