package handlers import ( "context" "encoding/base64" "net" "net/http" "strings" "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/models" "github.com/TwiN/go-away" "github.com/gin-gonic/gin" ) var profanityDetector = goaway.NewProfanityDetector() type WebHandler struct { db *database.DB config *config.Config } func NewWebHandler(db *database.DB, cfg *config.Config) *WebHandler { return &WebHandler{ db: db, config: cfg, } } func (h *WebHandler) Index(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{ "baseDomain": h.config.BaseDomain, "spaceSubdomain": h.config.SpaceSubdomain, "zone": h.config.GetZone(), }) } func (h *WebHandler) ClaimSpace(c *gin.Context) { var req models.CreateSpaceRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) return } subdomain := strings.ToLower(strings.TrimSpace(req.Subdomain)) if !isValidSubdomain(subdomain) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subdomain format"}) return } if profanityDetector.IsProfane(subdomain) { c.JSON(http.StatusBadRequest, gin.H{"error": "Subdomain contains inappropriate content"}) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() exists, err := h.db.SubdomainExists(ctx, subdomain) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check subdomain availability"}) return } if exists { c.JSON(http.StatusConflict, gin.H{"error": "Subdomain already taken"}) return } space, err := h.db.CreateSpace(ctx, subdomain) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create space"}) return } resp := models.CreateSpaceResponse{ Token: space.Token, Subdomain: space.Subdomain, FQDN: space.GetFQDN(h.config.GetZone()), CreatedAt: space.CreatedAt, } c.JSON(http.StatusCreated, resp) } func (h *WebHandler) CheckSubdomain(c *gin.Context) { subdomain := strings.ToLower(strings.TrimSpace(c.Query("subdomain"))) if subdomain == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subdomain parameter required"}) return } if profanityDetector.IsProfane(subdomain) { c.JSON(http.StatusOK, gin.H{ "available": false, "subdomain": subdomain, "reason": "inappropriate", }) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() exists, err := h.db.SubdomainExists(ctx, subdomain) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check subdomain"}) return } c.JSON(http.StatusOK, gin.H{ "available": !exists, "subdomain": subdomain, }) } func isValidSubdomain(subdomain string) bool { if len(subdomain) < 3 || len(subdomain) > 63 { return false } if subdomain[0] == '-' || subdomain[len(subdomain)-1] == '-' { return false } for _, ch := range subdomain { if !((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-') { return false } } return true } type DynDNSHandler struct { db *database.DB dns *dns.Client config *config.Config } func NewDynDNSHandler(db *database.DB, dnsClient *dns.Client, cfg *config.Config) *DynDNSHandler { return &DynDNSHandler{ db: db, dns: dnsClient, config: cfg, } } func (h *DynDNSHandler) Update(c *gin.Context) { token, err := extractBasicAuthPassword(c) if err != nil { c.String(http.StatusUnauthorized, "badauth") return } hostname := c.Query("hostname") myip := c.Query("myip") if hostname == "" { c.String(http.StatusBadRequest, "nohost") return } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() space, err := h.db.GetSpaceByToken(ctx, token) if err != nil { c.String(http.StatusServiceUnavailable, "911") return } if space == nil { c.String(http.StatusUnauthorized, "badauth") return } expectedFQDN := space.GetFQDN(h.config.GetZone()) if hostname != expectedFQDN { c.String(http.StatusBadRequest, "nohost") return } if myip == "" { myip = c.ClientIP() } if net.ParseIP(myip) == nil { c.String(http.StatusBadRequest, "dnserr") return } zone := h.config.GetZone() err = h.dns.AddARecord(zone, space.Subdomain, myip, 300) if err != nil { c.String(http.StatusServiceUnavailable, "911") return } err = h.dns.AddWildcardARecord(zone, space.Subdomain, myip, 300) if err != nil { c.String(http.StatusServiceUnavailable, "911") return } if space.LastIP == myip { c.String(http.StatusOK, "nochg %s", myip) return } err = h.db.UpdateSpaceIP(ctx, token, myip) if err != nil { c.String(http.StatusServiceUnavailable, "911") return } c.String(http.StatusOK, "good %s", myip) } func extractBasicAuthPassword(c *gin.Context) (string, error) { auth := c.GetHeader("Authorization") if auth == "" { return "", nil } const prefix = "Basic " if !strings.HasPrefix(auth, prefix) { return "", nil } decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) if err != nil { return "", err } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { return "", nil } return parts[1], nil }