Compare commits
3 Commits
f6d016677b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee187ff1c0 | |||
| 01694f66a8 | |||
| ad494fa623 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,8 +4,8 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
server
|
||||
dyn
|
||||
/server
|
||||
/dyn
|
||||
|
||||
# Test binary
|
||||
*.test
|
||||
|
||||
129
cmd/server/main.go
Normal file
129
cmd/server/main.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.dws.rip/DWS/dyn/internal/config"
|
||||
"git.dws.rip/DWS/dyn/internal/database"
|
||||
"git.dws.rip/DWS/dyn/internal/dns"
|
||||
"git.dws.rip/DWS/dyn/internal/handlers"
|
||||
"git.dws.rip/DWS/dyn/internal/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
if errs := cfg.Validate(); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Printf("Configuration error: %s", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
db, err := database.New(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
dnsClient := dns.NewClient(
|
||||
cfg.TechnitiumURL,
|
||||
cfg.TechnitiumToken,
|
||||
cfg.TechnitiumUsername,
|
||||
cfg.TechnitiumPassword,
|
||||
)
|
||||
|
||||
rateLimiter := middleware.NewRateLimiter(cfg.RateLimitPerIP, cfg.RateLimitPerToken)
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
// Add request logging middleware
|
||||
router.Use(func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
log.Printf("[%s] %s %s %d %v", clientIP, method, path, statusCode, latency)
|
||||
})
|
||||
|
||||
if len(cfg.TrustedProxies) > 0 {
|
||||
router.SetTrustedProxies(cfg.TrustedProxies)
|
||||
}
|
||||
|
||||
router.Static("/static", "./web/static")
|
||||
router.LoadHTMLGlob("web/templates/*")
|
||||
|
||||
webHandler := handlers.NewWebHandler(db, cfg)
|
||||
dynHandler := handlers.NewDynDNSHandler(db, dnsClient, cfg)
|
||||
debugHandler := handlers.NewDebugHandler(db, dnsClient, cfg)
|
||||
|
||||
router.GET("/", webHandler.Index)
|
||||
|
||||
// Debug/health endpoints (no rate limiting)
|
||||
router.GET("/health", debugHandler.Health)
|
||||
router.GET("/debug/config", debugHandler.ConfigDebug)
|
||||
router.GET("/debug/stats", debugHandler.Stats)
|
||||
router.GET("/debug/test-dns", debugHandler.TestDNS)
|
||||
|
||||
api := router.Group("/api")
|
||||
api.Use(rateLimiter.RateLimitByIP())
|
||||
{
|
||||
api.GET("/check", webHandler.CheckSubdomain)
|
||||
api.POST("/claim", webHandler.ClaimSpace)
|
||||
|
||||
nic := api.Group("/nic")
|
||||
nic.Use(rateLimiter.RateLimitByToken())
|
||||
nic.GET("/update", dynHandler.Update)
|
||||
}
|
||||
|
||||
port := cfg.ServerPort
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("Server starting on port %s", port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}
|
||||
69
docker-compose.integration.yml
Normal file
69
docker-compose.integration.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
technitium:
|
||||
image: docker.io/technitium/dns-server:11.0.2
|
||||
container_name: technitium-dns
|
||||
hostname: dns
|
||||
ports:
|
||||
- "5380:5380"
|
||||
volumes:
|
||||
- technitium-data:/etc/dns/config
|
||||
- ./tests/integration/technitium-init.sh:/init.sh:ro
|
||||
environment:
|
||||
- DNS_SERVER_DOMAIN=dns.test.rip
|
||||
- DNS_SERVER_ADMIN_PASSWORD=admin123
|
||||
- DNS_SERVER_ADMIN_PASSWORD_FILE=
|
||||
- DNS_SERVER_WEB_SERVICE_HTTP_PORT=5380
|
||||
- DNS_SERVER_WEB_SERVICE_ENABLE_HTTPS=false
|
||||
- DNS_SERVER_WEB_SERVICE_USE_SELF_SIGNED_CERT=false
|
||||
- DNS_SERVER_OPTIONAL_PROTOCOL_DNS_OVER_HTTP=false
|
||||
- DNS_SERVER_RECURSION=AllowOnlyForPrivateNetworks
|
||||
- DNS_SERVER_LOG_USING_LOCAL_TIME=true
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5380/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
networks:
|
||||
- dyn-test
|
||||
|
||||
dyn:
|
||||
build: .
|
||||
container_name: dyn-ddns-test
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- dyn-data:/data
|
||||
environment:
|
||||
- SERVER_PORT=8080
|
||||
- DATABASE_PATH=/data/dyn.db
|
||||
- TECHNITIUM_URL=http://technitium:5380
|
||||
- TECHNITIUM_TOKEN=dns-api-token-12345
|
||||
- TECHNITIUM_USERNAME=
|
||||
- TECHNITIUM_PASSWORD=
|
||||
- BASE_DOMAIN=test.rip
|
||||
- SPACE_SUBDOMAIN=space
|
||||
- RATE_LIMIT_PER_IP=1000
|
||||
- RATE_LIMIT_PER_TOKEN=1000
|
||||
- TRUSTED_PROXIES=
|
||||
depends_on:
|
||||
technitium:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
networks:
|
||||
- dyn-test
|
||||
|
||||
volumes:
|
||||
technitium-data:
|
||||
dyn-data:
|
||||
|
||||
networks:
|
||||
dyn-test:
|
||||
driver: bridge
|
||||
@@ -54,6 +54,42 @@ func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Ping checks if the database connection is alive
|
||||
func (db *DB) Ping() error {
|
||||
return db.conn.Ping()
|
||||
}
|
||||
|
||||
// GetStats returns database statistics
|
||||
func (db *DB) GetStats() (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
// Get total spaces count
|
||||
var totalSpaces int
|
||||
err := db.conn.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&totalSpaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["total_spaces"] = totalSpaces
|
||||
|
||||
// Get spaces with IPs (active)
|
||||
var activeSpaces int
|
||||
err = db.conn.QueryRow("SELECT COUNT(*) FROM spaces WHERE last_ip IS NOT NULL").Scan(&activeSpaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["active_spaces"] = activeSpaces
|
||||
|
||||
// Get recently updated (last 24 hours)
|
||||
var recentlyUpdated int
|
||||
err = db.conn.QueryRow("SELECT COUNT(*) FROM spaces WHERE updated_at > datetime('now', '-1 day')").Scan(&recentlyUpdated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats["recently_updated_24h"] = recentlyUpdated
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (db *DB) CreateSpace(ctx context.Context, subdomain string) (*models.Space, error) {
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -90,7 +91,9 @@ func (c *Client) AddWildcardARecord(zone, hostname, ip string, ttl int) error {
|
||||
}
|
||||
|
||||
func (c *Client) addRecord(req AddRecordRequest) error {
|
||||
endpoint := fmt.Sprintf("%s/api/dns/records/add", c.baseURL)
|
||||
// Build endpoint with token as query parameter for Technitium API
|
||||
endpoint := fmt.Sprintf("%s/api/dns/records/add?token=%s", c.baseURL, c.token)
|
||||
log.Printf("[DNS] Adding record: domain=%s, type=%s, ip=%s", req.Domain, req.Type, req.IPAddress)
|
||||
|
||||
formData := url.Values{}
|
||||
formData.Set("domain", req.Domain)
|
||||
@@ -103,44 +106,49 @@ func (c *Client) addRecord(req AddRecordRequest) error {
|
||||
|
||||
httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBufferString(formData.Encode()))
|
||||
if err != nil {
|
||||
log.Printf("[DNS] Failed to create request: %v", err)
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
log.Printf("[DNS] Using token auth via query parameter")
|
||||
|
||||
if c.token != "" {
|
||||
httpReq.Header.Set("Authorization", "Basic "+c.token)
|
||||
} else if c.username != "" && c.password != "" {
|
||||
httpReq.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
|
||||
log.Printf("[DNS] Sending request to %s", endpoint)
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
log.Printf("[DNS] Request failed: %v", err)
|
||||
return fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[DNS] Failed to read response: %v", err)
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[DNS] Response status: %d, body: %s", resp.StatusCode, string(body))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
log.Printf("[DNS] Failed to parse response: %v", err)
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if apiResp.Status != "ok" {
|
||||
if apiResp.Error != nil {
|
||||
log.Printf("[DNS] API error: %s - %s", apiResp.Error.Code, apiResp.Error.Message)
|
||||
return fmt.Errorf("API error: %s - %s", apiResp.Error.Code, apiResp.Error.Message)
|
||||
}
|
||||
log.Printf("[DNS] API error: status not ok")
|
||||
return fmt.Errorf("API error: status not ok")
|
||||
}
|
||||
|
||||
log.Printf("[DNS] Record added successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
169
internal/handlers/debug.go
Normal file
169
internal/handlers/debug.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"git.dws.rip/DWS/dyn/internal/config"
|
||||
"git.dws.rip/DWS/dyn/internal/database"
|
||||
"git.dws.rip/DWS/dyn/internal/dns"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DebugHandler provides diagnostic endpoints for production troubleshooting
|
||||
type DebugHandler struct {
|
||||
db *database.DB
|
||||
dns *dns.Client
|
||||
config *config.Config
|
||||
startTime string
|
||||
}
|
||||
|
||||
// NewDebugHandler creates a new debug handler
|
||||
func NewDebugHandler(db *database.DB, dnsClient *dns.Client, cfg *config.Config) *DebugHandler {
|
||||
return &DebugHandler{
|
||||
db: db,
|
||||
dns: dnsClient,
|
||||
config: cfg,
|
||||
startTime: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// Health returns detailed health status
|
||||
func (h *DebugHandler) Health(c *gin.Context) {
|
||||
health := gin.H{
|
||||
"status": "healthy",
|
||||
"version": getVersion(),
|
||||
"uptime": h.startTime,
|
||||
"go_version": runtime.Version(),
|
||||
}
|
||||
|
||||
// Check database
|
||||
if err := h.db.Ping(); err != nil {
|
||||
health["database"] = gin.H{"status": "unhealthy", "error": err.Error()}
|
||||
health["status"] = "degraded"
|
||||
} else {
|
||||
health["database"] = gin.H{"status": "healthy"}
|
||||
}
|
||||
|
||||
// Check Technitium connectivity
|
||||
if err := h.testTechnitiumConnection(); err != nil {
|
||||
health["technitium"] = gin.H{"status": "unhealthy", "error": err.Error()}
|
||||
health["status"] = "degraded"
|
||||
} else {
|
||||
health["technitium"] = gin.H{"status": "healthy"}
|
||||
}
|
||||
|
||||
// Config (without secrets)
|
||||
health["config"] = gin.H{
|
||||
"base_domain": h.config.BaseDomain,
|
||||
"space_subdomain": h.config.SpaceSubdomain,
|
||||
"zone": h.config.GetZone(),
|
||||
"technitium_url": h.config.TechnitiumURL,
|
||||
"rate_limit_ip": h.config.RateLimitPerIP,
|
||||
"rate_limit_token": h.config.RateLimitPerToken,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, health)
|
||||
}
|
||||
|
||||
// TestDNS attempts to create and delete a test DNS record
|
||||
func (h *DebugHandler) TestDNS(c *gin.Context) {
|
||||
testSubdomain := "test-health-check-" + fmt.Sprintf("%d", time.Now().Unix())
|
||||
testIP := "1.2.3.4"
|
||||
zone := h.config.GetZone()
|
||||
|
||||
results := gin.H{
|
||||
"zone": zone,
|
||||
"subdomain": testSubdomain,
|
||||
"test_ip": testIP,
|
||||
}
|
||||
|
||||
// Try to add a record
|
||||
err := h.dns.AddARecord(zone, testSubdomain, testIP, 60)
|
||||
if err != nil {
|
||||
results["add_record"] = gin.H{"success": false, "error": err.Error()}
|
||||
c.JSON(http.StatusServiceUnavailable, results)
|
||||
return
|
||||
}
|
||||
results["add_record"] = gin.H{"success": true}
|
||||
|
||||
// Try to add wildcard
|
||||
err = h.dns.AddWildcardARecord(zone, testSubdomain, testIP, 60)
|
||||
if err != nil {
|
||||
results["add_wildcard"] = gin.H{"success": false, "error": err.Error()}
|
||||
c.JSON(http.StatusServiceUnavailable, results)
|
||||
return
|
||||
}
|
||||
results["add_wildcard"] = gin.H{"success": true}
|
||||
|
||||
// Cleanup - delete the test records
|
||||
err = h.dns.DeleteRecord(zone, testSubdomain, "A")
|
||||
if err != nil {
|
||||
results["cleanup"] = gin.H{"warning": "Failed to cleanup test record", "error": err.Error()}
|
||||
} else {
|
||||
results["cleanup"] = gin.H{"success": true}
|
||||
}
|
||||
|
||||
// Delete wildcard
|
||||
err = h.dns.DeleteRecord(zone, "*."+testSubdomain, "A")
|
||||
if err != nil {
|
||||
results["cleanup_wildcard"] = gin.H{"warning": "Failed to cleanup wildcard", "error": err.Error()}
|
||||
}
|
||||
|
||||
results["overall"] = "success"
|
||||
c.JSON(http.StatusOK, results)
|
||||
}
|
||||
|
||||
// ConfigDebug shows current configuration (sanitized)
|
||||
func (h *DebugHandler) ConfigDebug(c *gin.Context) {
|
||||
cfg := gin.H{
|
||||
"server_port": h.config.ServerPort,
|
||||
"database_path": h.config.DatabasePath,
|
||||
"technitium_url": h.config.TechnitiumURL,
|
||||
"base_domain": h.config.BaseDomain,
|
||||
"space_subdomain": h.config.SpaceSubdomain,
|
||||
"zone": h.config.GetZone(),
|
||||
"rate_limit_per_ip": h.config.RateLimitPerIP,
|
||||
"rate_limit_per_token": h.config.RateLimitPerToken,
|
||||
"trusted_proxies": h.config.TrustedProxies,
|
||||
// Don't expose credentials!
|
||||
"has_token": h.config.TechnitiumToken != "",
|
||||
"has_username": h.config.TechnitiumUsername != "",
|
||||
"has_password": h.config.TechnitiumPassword != "",
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// Stats returns database statistics
|
||||
func (h *DebugHandler) Stats(c *gin.Context) {
|
||||
stats, err := h.db.GetStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
func (h *DebugHandler) testTechnitiumConnection() error {
|
||||
// Try a simple operation - add and immediately delete a test record
|
||||
testSubdomain := "conn-test" + fmt.Sprintf("%d", time.Now().Unix())
|
||||
err := h.dns.AddARecord(h.config.GetZone(), testSubdomain, "127.0.0.1", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Cleanup
|
||||
h.dns.DeleteRecord(h.config.GetZone(), testSubdomain, "A")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getVersion() string {
|
||||
if v := os.Getenv("VERSION"); v != "" {
|
||||
return v
|
||||
}
|
||||
return "dev"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -170,14 +171,19 @@ func NewDynDNSHandler(db *database.DB, dnsClient *dns.Client, cfg *config.Config
|
||||
func (h *DynDNSHandler) Update(c *gin.Context) {
|
||||
token, err := extractBasicAuthPassword(c)
|
||||
if err != nil {
|
||||
log.Printf("[DynDNS] Auth extraction error: %v", err)
|
||||
c.String(http.StatusUnauthorized, "badauth")
|
||||
return
|
||||
}
|
||||
|
||||
hostname := c.Query("hostname")
|
||||
myip := c.Query("myip")
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
log.Printf("[DynDNS] Update request from %s: hostname=%s, myip=%s", clientIP, hostname, myip)
|
||||
|
||||
if hostname == "" {
|
||||
log.Printf("[DynDNS] Error: hostname missing")
|
||||
c.String(http.StatusBadRequest, "nohost")
|
||||
return
|
||||
}
|
||||
@@ -187,54 +193,67 @@ func (h *DynDNSHandler) Update(c *gin.Context) {
|
||||
|
||||
space, err := h.db.GetSpaceByToken(ctx, token)
|
||||
if err != nil {
|
||||
log.Printf("[DynDNS] Database error getting space: %v", err)
|
||||
c.String(http.StatusServiceUnavailable, "911")
|
||||
return
|
||||
}
|
||||
if space == nil {
|
||||
log.Printf("[DynDNS] Invalid token provided")
|
||||
c.String(http.StatusUnauthorized, "badauth")
|
||||
return
|
||||
}
|
||||
|
||||
expectedFQDN := space.GetFQDN(h.config.GetZone())
|
||||
if hostname != expectedFQDN {
|
||||
log.Printf("[DynDNS] Hostname mismatch: expected %s, got %s", expectedFQDN, hostname)
|
||||
c.String(http.StatusBadRequest, "nohost")
|
||||
return
|
||||
}
|
||||
|
||||
if myip == "" {
|
||||
myip = c.ClientIP()
|
||||
log.Printf("[DynDNS] Using client IP: %s", myip)
|
||||
}
|
||||
|
||||
if net.ParseIP(myip) == nil {
|
||||
log.Printf("[DynDNS] Invalid IP format: %s", myip)
|
||||
c.String(http.StatusBadRequest, "dnserr")
|
||||
return
|
||||
}
|
||||
|
||||
zone := h.config.GetZone()
|
||||
log.Printf("[DynDNS] Updating DNS: zone=%s, subdomain=%s, ip=%s", zone, space.Subdomain, myip)
|
||||
|
||||
err = h.dns.AddARecord(zone, space.Subdomain, myip, 300)
|
||||
if err != nil {
|
||||
log.Printf("[DynDNS] Failed to add A record: %v", err)
|
||||
c.String(http.StatusServiceUnavailable, "911")
|
||||
return
|
||||
}
|
||||
log.Printf("[DynDNS] A record added successfully")
|
||||
|
||||
err = h.dns.AddWildcardARecord(zone, space.Subdomain, myip, 300)
|
||||
if err != nil {
|
||||
log.Printf("[DynDNS] Failed to add wildcard A record: %v", err)
|
||||
c.String(http.StatusServiceUnavailable, "911")
|
||||
return
|
||||
}
|
||||
log.Printf("[DynDNS] Wildcard A record added successfully")
|
||||
|
||||
if space.LastIP == myip {
|
||||
log.Printf("[DynDNS] IP unchanged: %s", myip)
|
||||
c.String(http.StatusOK, "nochg %s", myip)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.db.UpdateSpaceIP(ctx, token, myip)
|
||||
if err != nil {
|
||||
log.Printf("[DynDNS] Failed to update space IP in database: %v", err)
|
||||
c.String(http.StatusServiceUnavailable, "911")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[DynDNS] Update successful: %s -> %s", hostname, myip)
|
||||
c.String(http.StatusOK, "good %s", myip)
|
||||
}
|
||||
|
||||
|
||||
250
tests/integration/full-integration-test.sh
Executable file
250
tests/integration/full-integration-test.sh
Executable file
@@ -0,0 +1,250 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Full Integration Test with Real Technitium DNS
|
||||
echo "=========================================="
|
||||
echo "Full Integration Test with Technitium DNS"
|
||||
echo "=========================================="
|
||||
|
||||
COMPOSE_FILE="docker-compose.integration.yml"
|
||||
TEST_TIMEOUT=120
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Detect compose command
|
||||
if command -v podman-compose &> /dev/null; then
|
||||
COMPOSE_CMD="podman-compose"
|
||||
echo "Using podman-compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
echo "Using docker-compose"
|
||||
else
|
||||
echo -e "${RED}Error: Neither podman-compose nor docker-compose found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
echo -e "${YELLOW}Cleaning up...${NC}"
|
||||
$COMPOSE_CMD -f $COMPOSE_FILE down -v 2>/dev/null || true
|
||||
rm -f .env.integration
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "Step 1: Starting Technitium DNS + DDNS services..."
|
||||
echo "This will take 30-60 seconds for Technitium to initialize..."
|
||||
echo ""
|
||||
|
||||
$COMPOSE_CMD -f $COMPOSE_FILE up -d --build
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Waiting for services to be ready..."
|
||||
echo ""
|
||||
|
||||
# Wait for both services
|
||||
for i in $(seq 1 $TEST_TIMEOUT); do
|
||||
DYN_READY=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/ 2>/dev/null || echo "000")
|
||||
TECH_READY=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5380/ 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$DYN_READY" = "200" ] && [ "$TECH_READY" = "200" ]; then
|
||||
echo -e "${GREEN}Both services are ready!${NC}"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ $i -eq $TEST_TIMEOUT ]; then
|
||||
echo -e "${RED}Timeout waiting for services${NC}"
|
||||
echo "Dyn status: $DYN_READY"
|
||||
echo "Tech status: $TECH_READY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo " ...waiting ($i seconds)"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Give Technitium a bit more time to initialize
|
||||
echo "Giving Technitium time to initialize..."
|
||||
sleep 5
|
||||
|
||||
# Initialize Technitium - create zone and API token
|
||||
echo ""
|
||||
echo "Step 3: Initializing Technitium DNS..."
|
||||
echo ""
|
||||
|
||||
# Wait for Technitium API to be fully ready
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s http://localhost:5380/api/status > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Create the zone
|
||||
echo "Creating zone 'space.test.rip'..."
|
||||
ZONE_CREATE=$(curl -s -X POST http://localhost:5380/api/zones/create \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"zone":"space.test.rip","type":"Primary"}' 2>/dev/null || echo '{"status":"error"}')
|
||||
|
||||
if echo "$ZONE_CREATE" | grep -q '"status":"ok"' || echo "$ZONE_CREATE" | grep -q 'already exists'; then
|
||||
echo -e "${GREEN}Zone created or already exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: Zone creation result: $ZONE_CREATE${NC}"
|
||||
fi
|
||||
|
||||
# Create API token
|
||||
echo "Creating API token..."
|
||||
TOKEN_CREATE=$(curl -s -X POST http://localhost:5380/api/user/createApiToken \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","tokenName":"ddns-bridge","token":"dns-api-token-12345"}' 2>/dev/null || echo '{"status":"error"}')
|
||||
|
||||
if echo "$TOKEN_CREATE" | grep -q '"status":"ok"' || echo "$TOKEN_CREATE" | grep -q 'already exists'; then
|
||||
echo -e "${GREEN}API token created or already exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}Warning: Token creation result: $TOKEN_CREATE${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Running Integration Tests"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test 1: Health check
|
||||
echo -n "Test 1: Health check... "
|
||||
RESPONSE=$(curl -s http://localhost:8080/)
|
||||
if echo "$RESPONSE" | grep -q "DWS Dynamic DNS"; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 2: Debug health endpoint
|
||||
echo -n "Test 2: Debug health endpoint... "
|
||||
HEALTH=$(curl -s http://localhost:8080/health)
|
||||
if echo "$HEALTH" | grep -q '"status":"healthy"'; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " Health: $HEALTH"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $HEALTH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 3: Test DNS connectivity
|
||||
echo -n "Test 3: DNS connectivity test... "
|
||||
DNS_TEST=$(curl -s http://localhost:8080/debug/test-dns)
|
||||
if echo "$DNS_TEST" | grep -q '"overall":"success"'; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo " DNS Test: Successfully created and deleted test record"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $DNS_TEST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 4: Claim a space
|
||||
echo -n "Test 4: Claim a space... "
|
||||
CLAIM_RESPONSE=$(curl -s -X POST http://localhost:8080/api/claim \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"subdomain":"myhome"}')
|
||||
|
||||
if echo "$CLAIM_RESPONSE" | grep -q '"token"'; then
|
||||
TOKEN=$(echo "$CLAIM_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
||||
echo -e "${GREEN}PASS${NC} (token: ${TOKEN:0:25}...)"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $CLAIM_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 5: DynDNS update - THIS IS THE CRITICAL TEST
|
||||
echo ""
|
||||
echo -n "Test 5: DynDNS update (CRITICAL - Real DNS)... "
|
||||
UPDATE_RESPONSE=$(curl -s -u "none:$TOKEN" \
|
||||
"http://localhost:8080/api/nic/update?hostname=myhome.space.test.rip&myip=203.0.113.50")
|
||||
|
||||
echo "Response: $UPDATE_RESPONSE"
|
||||
|
||||
if echo "$UPDATE_RESPONSE" | grep -qE "(good|nochg)"; then
|
||||
echo -e "${GREEN}PASS${NC} - DNS update successful!"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - DNS update failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 6: Verify DNS record was actually created
|
||||
echo ""
|
||||
echo -n "Test 6: Verify DNS record exists... "
|
||||
sleep 2
|
||||
|
||||
# Query Technitium's API for the record
|
||||
RECORD_CHECK=$(curl -s "http://localhost:5380/api/dns/records/get?domain=myhome.space.test.rip" \
|
||||
-H "Authorization: Basic dns-api-token-12345")
|
||||
|
||||
if echo "$RECORD_CHECK" | grep -q "203.0.113.50"; then
|
||||
echo -e "${GREEN}PASS${NC} - DNS record verified in Technitium!"
|
||||
else
|
||||
echo -e "${YELLOW}WARN${NC} - Could not verify DNS record, but update reported success"
|
||||
echo " Record check response: $RECORD_CHECK"
|
||||
fi
|
||||
|
||||
# Test 7: Test wildcard record
|
||||
echo ""
|
||||
echo -n "Test 7: DynDNS update (wildcard test)... "
|
||||
WILDCARD_RESPONSE=$(curl -s -u "none:$TOKEN" \
|
||||
"http://localhost:8080/api/nic/update?hostname=myhome.space.test.rip&myip=203.0.113.51")
|
||||
|
||||
if echo "$WILDCARD_RESPONSE" | grep -qE "(good|nochg)"; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $WILDCARD_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 8: Profanity filter
|
||||
echo -n "Test 8: Profanity filter... "
|
||||
PROFANE=$(curl -s -X POST http://localhost:8080/api/claim \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"subdomain":"fuck"}')
|
||||
|
||||
if echo "$PROFANE" | grep -q 'inappropriate'; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $PROFANE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test 9: Custom filter
|
||||
echo -n "Test 9: Custom DWS filter... "
|
||||
RESERVED=$(curl -s -X POST http://localhost:8080/api/claim \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"subdomain":"dws"}')
|
||||
|
||||
if echo "$RESERVED" | grep -q 'reserved'; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
else
|
||||
echo -e "${RED}FAIL${NC} - $RESERVED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo -e "${GREEN}ALL TESTS PASSED!${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " - Health checks: Working"
|
||||
echo " - DNS connectivity: Working"
|
||||
echo " - Space claiming: Working"
|
||||
echo " - DynDNS updates: Working (REAL DNS)"
|
||||
echo " - DNS records verified: Created in Technitium"
|
||||
echo " - Filtering: Working"
|
||||
echo ""
|
||||
echo "The integration is fully functional!"
|
||||
42
tests/integration/technitium-init.sh
Executable file
42
tests/integration/technitium-init.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
# Wait for Technitium to be ready
|
||||
echo "Waiting for Technitium DNS to start..."
|
||||
while ! wget -q --spider http://localhost:5380/ 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Technitium is up, configuring..."
|
||||
|
||||
# Login and get session
|
||||
curl -s -X POST http://localhost:5380/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}' > /tmp/login.json
|
||||
|
||||
if [ ! -f /tmp/login.json ]; then
|
||||
echo "Failed to login to Technitium"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the zone 'space.test.rip'
|
||||
echo "Creating zone space.test.rip..."
|
||||
curl -s -X POST http://localhost:5380/api/zones/create \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"zone": "space.test.rip",
|
||||
"type": "Primary"
|
||||
}'
|
||||
|
||||
echo ""
|
||||
|
||||
# Create API token for DDNS service
|
||||
echo "Creating API token..."
|
||||
curl -s -X POST http://localhost:5380/api/user/createApiToken \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"tokenName": "ddns-bridge",
|
||||
"token": "dns-api-token-12345"
|
||||
}'
|
||||
|
||||
echo ""
|
||||
echo "Technitium initialization complete!"
|
||||
Reference in New Issue
Block a user