package gateway import ( "encoding/json" "net/http" "strings" "time" "github.com/DeBrosOfficial/network/pkg/client" "go.uber.org/zap" ) // ACMERequest represents the request body for ACME DNS-01 challenges // from the lego httpreq provider type ACMERequest struct { FQDN string `json:"fqdn"` // e.g., "_acme-challenge.example.com." Value string `json:"value"` // The challenge token } // acmePresentHandler handles DNS-01 challenge presentation // POST /v1/internal/acme/present // Creates a TXT record in the dns_records table for ACME validation func (g *Gateway) acmePresentHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req ACMERequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { g.logger.Error("Failed to decode ACME present request", zap.Error(err)) http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.FQDN == "" || req.Value == "" { http.Error(w, "fqdn and value are required", http.StatusBadRequest) return } // Normalize FQDN (ensure trailing dot for DNS format) fqdn := strings.TrimSuffix(req.FQDN, ".") fqdn = strings.ToLower(fqdn) + "." // Add trailing dot for DNS format g.logger.Info("ACME DNS-01 challenge: presenting TXT record", zap.String("fqdn", fqdn), zap.String("value_prefix", req.Value[:min(10, len(req.Value))]+"..."), ) // Insert TXT record into dns_records db := g.client.Database() ctx := client.WithInternalAuth(r.Context()) // First, delete any existing ACME challenge for this FQDN (in case of retry) deleteQuery := `DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'TXT' AND namespace = 'acme'` _, _ = db.Query(ctx, deleteQuery, fqdn) // Insert new TXT record insertQuery := `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, is_active, created_at, updated_at, created_by) VALUES (?, 'TXT', ?, 60, 'acme', TRUE, datetime('now'), datetime('now'), 'system')` _, err := db.Query(ctx, insertQuery, fqdn, req.Value) if err != nil { g.logger.Error("Failed to insert ACME TXT record", zap.Error(err)) http.Error(w, "Failed to create DNS record", http.StatusInternalServerError) return } g.logger.Info("ACME TXT record created", zap.String("fqdn", fqdn), ) // Give DNS a moment to propagate (CoreDNS reads from RQLite) time.Sleep(100 * time.Millisecond) w.WriteHeader(http.StatusOK) } // acmeCleanupHandler handles DNS-01 challenge cleanup // POST /v1/internal/acme/cleanup // Removes the TXT record after ACME validation completes func (g *Gateway) acmeCleanupHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req ACMERequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { g.logger.Error("Failed to decode ACME cleanup request", zap.Error(err)) http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.FQDN == "" { http.Error(w, "fqdn is required", http.StatusBadRequest) return } // Normalize FQDN (ensure trailing dot for DNS format) fqdn := strings.TrimSuffix(req.FQDN, ".") fqdn = strings.ToLower(fqdn) + "." // Add trailing dot for DNS format g.logger.Info("ACME DNS-01 challenge: cleaning up TXT record", zap.String("fqdn", fqdn), ) // Delete TXT record from dns_records db := g.client.Database() ctx := client.WithInternalAuth(r.Context()) deleteQuery := `DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'TXT' AND namespace = 'acme'` _, err := db.Query(ctx, deleteQuery, fqdn) if err != nil { g.logger.Error("Failed to delete ACME TXT record", zap.Error(err)) http.Error(w, "Failed to delete DNS record", http.StatusInternalServerError) return } g.logger.Info("ACME TXT record deleted", zap.String("fqdn", fqdn), ) w.WriteHeader(http.StatusOK) } // min returns the smaller of two integers func min(a, b int) int { if a < b { return a } return b }