206 lines
4.5 KiB
Go
206 lines
4.5 KiB
Go
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")
|
|
}
|