Push code
This commit is contained in:
205
main.go
Normal file
205
main.go
Normal file
@@ -0,0 +1,205 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user