package main import ( "context" "crypto/x509" "database/sql" "encoding/asn1" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "io" "log" "net/http" "strings" "time" "github.com/google/uuid" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" "certigo-addon-backend/providers" ) // OID zu Name Mapping (OpenSSL Format) var oidToName = map[string]string{ "2.5.29.15": "X509v3 Key Usage", "2.5.29.17": "X509v3 Subject Alternative Name", "2.5.29.19": "X509v3 Basic Constraints", "2.5.29.31": "X509v3 CRL Distribution Points", "2.5.29.32": "X509v3 Certificate Policies", "2.5.29.35": "X509v3 Authority Key Identifier", "2.5.29.37": "X509v3 Extended Key Usage", "2.5.29.14": "X509v3 Subject Key Identifier", "1.3.6.1.5.5.7.1.1": "Authority Information Access", "1.3.6.1.5.5.7.48.1": "OCSP", "1.3.6.1.5.5.7.48.2": "CA Issuers", } // Extended Key Usage OIDs (OpenSSL Format) var extendedKeyUsageOIDs = map[string]string{ "1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.3": "Code Signing", "1.3.6.1.5.5.7.3.4": "E-mail Protection", "1.3.6.1.5.5.7.3.8": "Time Stamping", "1.3.6.1.5.5.7.3.9": "OCSP Signing", "1.3.6.1.5.5.7.3.5": "IPsec End System", "1.3.6.1.5.5.7.3.6": "IPsec Tunnel", "1.3.6.1.5.5.7.3.7": "IPsec User", } // Key Usage Flags var keyUsageFlags = map[int]string{ 0: "Digital Signature", 1: "Content Commitment", 2: "Key Encipherment", 3: "Data Encipherment", 4: "Key Agreement", 5: "Key Cert Sign", 6: "CRL Sign", 7: "Encipher Only", 8: "Decipher Only", } func getExtensionName(oid string) string { if name, ok := oidToName[oid]; ok { return name } return "Unknown Extension" } func parseExtensionValue(oid string, value []byte, csr *x509.CertificateRequest) (string, []string) { switch oid { case "2.5.29.37": // Extended Key Usage return parseExtendedKeyUsage(value) case "2.5.29.15": // Key Usage return parseKeyUsage(value) case "2.5.29.19": // Basic Constraints return parseBasicConstraints(value) case "2.5.29.17": // Subject Alternative Name return parseSubjectAlternativeName(csr) default: return hex.EncodeToString(value), nil } } func parseSubjectAlternativeName(csr *x509.CertificateRequest) (string, []string) { var parts []string // DNS Names for _, dns := range csr.DNSNames { parts = append(parts, fmt.Sprintf("DNS:%s", dns)) } // Email Addresses for _, email := range csr.EmailAddresses { parts = append(parts, fmt.Sprintf("email:%s", email)) } // IP Addresses for _, ip := range csr.IPAddresses { parts = append(parts, fmt.Sprintf("IP:%s", ip.String())) } // URIs for _, uri := range csr.URIs { parts = append(parts, fmt.Sprintf("URI:%s", uri.String())) } if len(parts) > 0 { return strings.Join(parts, ", "), parts } return "No Subject Alternative Name", nil } func parseExtendedKeyUsage(value []byte) (string, []string) { var oids []asn1.ObjectIdentifier _, err := asn1.Unmarshal(value, &oids) if err != nil { return hex.EncodeToString(value), nil } var purposes []string for _, oid := range oids { oidStr := oid.String() if purpose, ok := extendedKeyUsageOIDs[oidStr]; ok { purposes = append(purposes, purpose) } else { purposes = append(purposes, oidStr) } } if len(purposes) > 0 { // Format wie OpenSSL: jede Purpose auf eigener Zeile return strings.Join(purposes, "\n "), purposes } return hex.EncodeToString(value), nil } func parseKeyUsage(value []byte) (string, []string) { var bits asn1.BitString _, err := asn1.Unmarshal(value, &bits) if err != nil { return hex.EncodeToString(value), nil } var usages []string for i := 0; i < len(bits.Bytes)*8 && i < 9; i++ { if bits.At(i) == 1 { if usage, ok := keyUsageFlags[i]; ok { usages = append(usages, usage) } } } if len(usages) > 0 { return strings.Join(usages, ", "), usages } return "No key usage specified", nil } func parseBasicConstraints(value []byte) (string, []string) { var constraints struct { IsCA bool `asn1:"optional"` MaxPathLen int `asn1:"optional,default:-1"` } _, err := asn1.Unmarshal(value, &constraints) if err != nil { return hex.EncodeToString(value), nil } var parts []string if constraints.IsCA { parts = append(parts, "CA: true") } else { parts = append(parts, "CA: false") } if constraints.MaxPathLen >= 0 { parts = append(parts, fmt.Sprintf("Path Length: %d", constraints.MaxPathLen)) } return strings.Join(parts, ", "), parts } type HealthResponse struct { Status string `json:"status"` Message string `json:"message"` Time string `json:"time"` } type StatsResponse struct { Spaces int `json:"spaces"` FQDNs int `json:"fqdns"` CSRs int `json:"csrs"` Certificates int `json:"certificates"` } type Space struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` CreatedAt string `json:"createdAt"` } type CreateSpaceRequest struct { Name string `json:"name"` Description string `json:"description"` } type FQDN struct { ID string `json:"id"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` Description string `json:"description"` CreatedAt string `json:"createdAt"` } type CreateFQDNRequest struct { FQDN string `json:"fqdn"` Description string `json:"description"` } type Extension struct { ID string `json:"id"` OID string `json:"oid"` Name string `json:"name"` Critical bool `json:"critical"` Value string `json:"value"` Description string `json:"description"` Purposes []string `json:"purposes,omitempty"` } type CSR struct { ID string `json:"id"` FQDNID string `json:"fqdnId"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` CSRPEM string `json:"csrPem"` Subject string `json:"subject"` PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` SignatureAlgorithm string `json:"signatureAlgorithm"` KeySize int `json:"keySize"` DNSNames []string `json:"dnsNames"` EmailAddresses []string `json:"emailAddresses"` IPAddresses []string `json:"ipAddresses"` URIs []string `json:"uris"` Extensions []Extension `json:"extensions"` CreatedAt string `json:"createdAt"` } var db *sql.DB func initDB() { var err error // SQLite Connection String mit Timeout und WAL Mode für bessere Concurrency // _busy_timeout erhöht die Wartezeit bei Locks db, err = sql.Open("sqlite3", "./spaces.db?_foreign_keys=1&_journal_mode=WAL&_timeout=10000&_busy_timeout=10000") if err != nil { log.Fatal("Fehler beim Öffnen der Datenbank:", err) } // Setze Connection Pool Settings db.SetMaxOpenConns(1) // SQLite unterstützt nur eine Verbindung gleichzeitig db.SetMaxIdleConns(1) // Teste die Verbindung mit Retry log.Println("Teste Datenbank-Verbindung...") maxRetries := 5 for i := 0; i < maxRetries; i++ { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) err := db.PingContext(ctx) cancel() if err != nil { if i < maxRetries-1 { log.Printf("Datenbank-Verbindung fehlgeschlagen, versuche erneut (%d/%d)...", i+1, maxRetries) time.Sleep(time.Second * 2) continue } log.Fatal("Fehler beim Verbinden mit der Datenbank nach mehreren Versuchen:", err) } log.Println("Datenbank-Verbindung erfolgreich") break } // Aktiviere Foreign Keys (auch über Connection String, aber zur Sicherheit nochmal) log.Println("Aktiviere Foreign Keys...") ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "PRAGMA foreign_keys = ON") cancel() if err != nil { log.Fatal("Fehler beim Aktivieren der Foreign Keys:", err) } // Prüfe und bereinige WAL-Dateien falls nötig log.Println("Führe WAL-Checkpoint aus...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") cancel() if err != nil { log.Printf("Warnung: WAL-Checkpoint fehlgeschlagen: %v", err) } // Erstelle Tabelle falls sie nicht existiert log.Println("Erstelle spaces-Tabelle...") createTableSQL := ` CREATE TABLE IF NOT EXISTS spaces ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der Tabelle:", err) } // Erstelle FQDN-Tabelle log.Println("Erstelle fqdns-Tabelle...") createFQDNTableSQL := ` CREATE TABLE IF NOT EXISTS fqdns ( id TEXT PRIMARY KEY, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createFQDNTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der FQDN-Tabelle:", err) } // Erstelle CSR-Tabelle log.Println("Erstelle csrs-Tabelle...") createCSRTableSQL := ` CREATE TABLE IF NOT EXISTS csrs ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, csr_pem TEXT NOT NULL, subject TEXT, public_key_algorithm TEXT, signature_algorithm TEXT, key_size INTEGER, dns_names TEXT, email_addresses TEXT, ip_addresses TEXT, uris TEXT, extensions TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createCSRTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der CSR-Tabelle:", err) } // Füge Extensions-Spalte hinzu, falls sie nicht existiert (für bestehende Datenbanken) // Prüfe zuerst, ob die Spalte bereits existiert log.Println("Prüfe Extensions-Spalte...") var columnExists bool ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) rows, err := db.QueryContext(ctx, "PRAGMA table_info(csrs)") cancel() if err == nil { defer rows.Close() for rows.Next() { var cid int var name string var dataType string var notNull int var defaultValue interface{} var pk int if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err == nil { if name == "extensions" { columnExists = true break } } } rows.Close() } // Füge Spalte nur hinzu, wenn sie nicht existiert if !columnExists { log.Println("Füge Extensions-Spalte hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE csrs ADD COLUMN extensions TEXT") cancel() if err != nil { // Ignoriere "duplicate column" Fehler, da die Spalte möglicherweise zwischenzeitlich hinzugefügt wurde if !strings.Contains(err.Error(), "duplicate column") { log.Printf("Fehler beim Hinzufügen der Extensions-Spalte: %v", err) } } else { log.Println("Extensions-Spalte zur csrs-Tabelle hinzugefügt") } } else { log.Println("Extensions-Spalte existiert bereits") } // Erstelle Zertifikat-Tabelle log.Println("Erstelle certificates-Tabelle...") createCertificateTableSQL := ` CREATE TABLE IF NOT EXISTS certificates ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, csr_id TEXT NOT NULL, certificate_id TEXT NOT NULL, provider_id TEXT NOT NULL, certificate_pem TEXT, status TEXT NOT NULL, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, FOREIGN KEY (csr_id) REFERENCES csrs(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createCertificateTableSQL) cancel() if err != nil { if strings.Contains(err.Error(), "database is locked") { log.Fatal("Datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden (z.B. andere go run main.go Instanzen).") } log.Fatal("Fehler beim Erstellen der Zertifikat-Tabelle:", err) } log.Println("Datenbank erfolgreich initialisiert") } func healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } response := HealthResponse{ Status: "ok", Message: "Backend ist erreichbar", Time: time.Now().Format(time.RFC3339), } json.NewEncoder(w).Encode(response) } func getStatsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } var spacesCount, fqdnsCount, csrsCount, certificatesCount int // Zähle Spaces err := db.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&spacesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Spaces: %v", err) return } // Zähle FQDNs err = db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&fqdnsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der FQDNs: %v", err) return } // Zähle CSRs err = db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&csrsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der CSRs: %v", err) return } // Zähle Zertifikate err = db.QueryRow("SELECT COUNT(*) FROM certificates").Scan(&certificatesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Zertifikate: %v", err) return } response := StatsResponse{ Spaces: spacesCount, FQDNs: fqdnsCount, CSRs: csrsCount, Certificates: certificatesCount, } json.NewEncoder(w).Encode(response) } func getSpacesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Verwende Prepared Statement für bessere Performance und Sicherheit stmt, err := db.Prepare("SELECT id, name, description, created_at FROM spaces ORDER BY created_at DESC") if err != nil { http.Error(w, "Fehler beim Vorbereiten der Abfrage", http.StatusInternalServerError) log.Printf("Fehler beim Vorbereiten der Abfrage: %v", err) return } defer stmt.Close() rows, err := stmt.Query() if err != nil { http.Error(w, "Fehler beim Abrufen der Spaces", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Spaces: %v", err) return } defer rows.Close() spaces := make([]Space, 0) for rows.Next() { var space Space var createdAt time.Time var description sql.NullString err := rows.Scan(&space.ID, &space.Name, &description, &createdAt) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { space.Description = description.String } else { space.Description = "" } space.CreatedAt = createdAt.Format(time.RFC3339) spaces = append(spaces, space) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } // Stelle sicher, dass immer ein Array zurückgegeben wird if spaces == nil { spaces = []Space{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(spaces) } func createSpaceHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } var req CreateSpaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { http.Error(w, "Name is required", http.StatusBadRequest) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err := db.Exec( "INSERT INTO spaces (id, name, description, created_at) VALUES (?, ?, ?, ?)", id, req.Name, req.Description, createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des Space", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des Space: %v", err) return } newSpace := Space{ ID: id, Name: req.Name, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newSpace) } func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) id := vars["id"] if id == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe ob der Space existiert var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", id).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDNs vorhanden sind var fqdnCount int err = db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", id).Scan(&fqdnCount) if err != nil { http.Error(w, "Fehler beim Prüfen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der FQDNs: %v", err) return } // Prüfe Query-Parameter für Mitlöschen deleteFqdns := r.URL.Query().Get("deleteFqdns") == "true" if fqdnCount > 0 && !deleteFqdns { http.Error(w, "Space enthält noch FQDNs. Bitte löschen Sie zuerst die FQDNs oder wählen Sie die Option zum Mitlöschen.", http.StatusConflict) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche FQDNs zuerst, wenn gewünscht if deleteFqdns && fqdnCount > 0 { _, err = tx.Exec("DELETE FROM fqdns WHERE space_id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } log.Printf("Gelöscht: %d FQDNs für Space %s", fqdnCount, id) } // Lösche den Space result, err := tx.Exec("DELETE FROM spaces WHERE id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des Space: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } if rowsAffected == 0 { tx.Rollback() http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Space erfolgreich gelöscht"}) } func getSpaceFqdnCountHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } var count int err := db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", spaceID).Scan(&count) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDN-Anzahl", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDN-Anzahl: %v", err) return } json.NewEncoder(w).Encode(map[string]int{"count": count}) } func getFqdnsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe ob der Space existiert var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } rows, err := db.Query("SELECT id, space_id, fqdn, description, created_at FROM fqdns WHERE space_id = ? ORDER BY created_at DESC", spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDNs: %v", err) return } defer rows.Close() var fqdns []FQDN for rows.Next() { var fqdn FQDN var createdAt time.Time var description sql.NullString err := rows.Scan(&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &description, &createdAt) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { fqdn.Description = description.String } else { fqdn.Description = "" } fqdn.CreatedAt = createdAt.Format(time.RFC3339) fqdns = append(fqdns, fqdn) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if fqdns == nil { fqdns = []FQDN{} } json.NewEncoder(w).Encode(fqdns) } func createFqdnHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe ob der Space existiert var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } var req CreateFQDNRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.FQDN == "" { http.Error(w, "FQDN is required", http.StatusBadRequest) return } // Prüfe ob der FQDN bereits existiert (case-insensitive) var fqdnExists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE LOWER(fqdn) = LOWER(?))", req.FQDN).Scan(&fqdnExists) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if fqdnExists { http.Error(w, "Dieser FQDN existiert bereits", http.StatusConflict) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec( "INSERT INTO fqdns (id, space_id, fqdn, description, created_at) VALUES (?, ?, ?, ?, ?)", id, spaceID, req.FQDN, req.Description, createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des FQDN: %v", err) return } newFqdn := FQDN{ ID: id, SpaceID: spaceID, FQDN: req.FQDN, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newFqdn) } func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["id"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "Space ID und FQDN ID sind erforderlich", http.StatusBadRequest) return } // Prüfe ob der FQDN existiert und zum Space gehört var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE id = ? AND space_id = ?)", fqdnID, spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if !exists { http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für diesen FQDN (falls CASCADE nicht funktioniert) _, err = tx.Exec("DELETE FROM csrs WHERE fqdn_id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche den FQDN result, err := tx.Exec("DELETE FROM fqdns WHERE id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des FQDN: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } if rowsAffected == 0 { tx.Rollback() http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("FQDN %s und zugehörige CSRs erfolgreich gelöscht", fqdnID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "FQDN erfolgreich gelöscht"}) } func deleteAllFqdnsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) return } // Prüfe ob der Space existiert var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für alle FQDNs dieses Spaces _, err = tx.Exec("DELETE FROM csrs WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche alle FQDNs des Spaces result, err := tx.Exec("DELETE FROM fqdns WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und zugehörige CSRs aus Space %s", rowsAffected, spaceID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der FQDNs var totalCount int err := db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&totalCount) if err != nil { http.Error(w, "Fehler beim Zählen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der FQDNs: %v", err) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine FQDNs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs _, err = tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } // Lösche alle FQDNs result, err := tx.Exec("DELETE FROM fqdns") if err != nil { http.Error(w, "Fehler beim Löschen aller FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und alle zugehörigen CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der CSRs var totalCount int err := db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&totalCount) if err != nil { http.Error(w, "Fehler beim Zählen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der CSRs: %v", err) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine CSRs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomare Operation tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche alle CSRs result, err := tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func uploadCSRHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Parse multipart form err := r.ParseMultipartForm(10 << 20) // 10 MB max if err != nil { http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest) return } spaceID := r.FormValue("spaceId") fqdnName := r.FormValue("fqdn") if spaceID == "" || fqdnName == "" { http.Error(w, "spaceId und fqdn sind erforderlich", http.StatusBadRequest) return } // Prüfe ob Space existiert var spaceExists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&spaceExists) if err != nil || !spaceExists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDN existiert und zum Space gehört var fqdnID string err = db.QueryRow("SELECT id FROM fqdns WHERE fqdn = ? AND space_id = ?", fqdnName, spaceID).Scan(&fqdnID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "FQDN nicht gefunden oder gehört nicht zu diesem Space", http.StatusNotFound) } else { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) } return } // Hole die CSR-Datei file, header, err := r.FormFile("csr") if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } defer file.Close() // Lese den Dateiinhalt csrBytes := make([]byte, header.Size) _, err = io.ReadFull(file, csrBytes) if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } csrPEM := string(csrBytes) // Parse CSR block, _ := pem.Decode(csrBytes) if block == nil { http.Error(w, "Ungültiges PEM-Format", http.StatusBadRequest) return } csr, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { http.Error(w, "Fehler beim Parsen des CSR: "+err.Error(), http.StatusBadRequest) return } // Extrahiere Informationen subject := csr.Subject.String() publicKeyAlgorithm := csr.PublicKeyAlgorithm.String() signatureAlgorithm := csr.SignatureAlgorithm.String() // Bestimme Key Size keySize := 0 if csr.PublicKey != nil { switch pub := csr.PublicKey.(type) { case interface{ Size() int }: keySize = pub.Size() * 8 // Convert bytes to bits } } // Extrahiere SANs dnsNames := csr.DNSNames emailAddresses := csr.EmailAddresses ipAddresses := make([]string, len(csr.IPAddresses)) for i, ip := range csr.IPAddresses { ipAddresses[i] = ip.String() } uris := make([]string, len(csr.URIs)) for i, uri := range csr.URIs { uris[i] = uri.String() } // Extrahiere Extensions extensions := make([]Extension, 0) for _, ext := range csr.Extensions { oidStr := ext.Id.String() name := getExtensionName(oidStr) description, purposes := parseExtensionValue(oidStr, ext.Value, csr) extension := Extension{ ID: ext.Id.String(), OID: oidStr, Name: name, Critical: ext.Critical, Value: hex.EncodeToString(ext.Value), Description: description, Purposes: purposes, } extensions = append(extensions, extension) } // Konvertiere Slices zu JSON-Strings für DB dnsNamesJSON, _ := json.Marshal(dnsNames) emailAddressesJSON, _ := json.Marshal(emailAddresses) ipAddressesJSON, _ := json.Marshal(ipAddresses) urisJSON, _ := json.Marshal(uris) extensionsJSON, _ := json.Marshal(extensions) // Generiere eindeutige ID csrID := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec(` INSERT INTO csrs ( id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, csrID, fqdnID, spaceID, fqdnName, csrPEM, subject, publicKeyAlgorithm, signatureAlgorithm, keySize, string(dnsNamesJSON), string(emailAddressesJSON), string(ipAddressesJSON), string(urisJSON), string(extensionsJSON), createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des CSR: %v", err) return } newCSR := CSR{ ID: csrID, FQDNID: fqdnID, SpaceID: spaceID, FQDN: fqdnName, CSRPEM: csrPEM, Subject: subject, PublicKeyAlgorithm: publicKeyAlgorithm, SignatureAlgorithm: signatureAlgorithm, KeySize: keySize, DNSNames: dnsNames, EmailAddresses: emailAddresses, IPAddresses: ipAddresses, URIs: uris, Extensions: extensions, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newCSR) } func getCSRByFQDNHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "spaceId und fqdnId sind erforderlich", http.StatusBadRequest) return } // Prüfe ob nur der neueste CSR gewünscht ist latestOnly := r.URL.Query().Get("latest") == "true" if latestOnly { // Hole nur den neuesten CSR var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON, extensionsJSON sql.NullString err := db.QueryRow(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { if err == sql.ErrNoRows { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(nil) return } http.Error(w, "Fehler beim Abrufen des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des CSR: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON.String), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON.String), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON.String), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON.String), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csr) } else { // Hole alle CSRs für diesen FQDN rows, err := db.Query(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der CSRs: %v", err) return } defer rows.Close() var csrs []CSR for rows.Next() { var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON string var extensionsJSON sql.NullString err := rows.Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) csrs = append(csrs, csr) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if csrs == nil { csrs = []CSR{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csrs) } } func swaggerUIHandler(w http.ResponseWriter, r *http.Request) { html := ` Certigo Addon API - Swagger UI
` w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func openAPIHandler(w http.ResponseWriter, r *http.Request) { // Lese die OpenAPI YAML Datei openAPIContent := `openapi: 3.0.3 info: title: Certigo Addon API description: API für die Verwaltung von Spaces, FQDNs und Certificate Signing Requests (CSRs) version: 1.0.0 contact: name: Certigo Addon servers: - url: http://localhost:8080/api description: Local development server paths: /health: get: summary: System Health Check description: Prüft den Systemstatus des Backends tags: [System] responses: '200': description: System ist erreichbar content: application/json: schema: $ref: '#/components/schemas/HealthResponse' /stats: get: summary: Statistiken abrufen description: Ruft Statistiken über die Anzahl der Spaces, FQDNs und CSRs ab tags: [System] responses: '200': description: Statistiken erfolgreich abgerufen content: application/json: schema: $ref: '#/components/schemas/StatsResponse' /spaces: get: summary: Alle Spaces abrufen description: Ruft eine Liste aller Spaces ab tags: [Spaces] responses: '200': description: Liste der Spaces content: application/json: schema: type: array items: $ref: '#/components/schemas/Space' post: summary: Space erstellen description: Erstellt einen neuen Space tags: [Spaces] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSpaceRequest' responses: '201': description: Space erfolgreich erstellt content: application/json: schema: $ref: '#/components/schemas/Space' '400': description: Ungültige Anfrage /spaces/{id}: delete: summary: Space löschen description: Löscht einen Space. Wenn der Space FQDNs enthält, muss der Parameter deleteFqdns=true gesetzt werden. tags: [Spaces] parameters: - name: id in: path required: true schema: type: string format: uuid - name: deleteFqdns in: query required: false schema: type: boolean default: false description: Wenn true, werden alle FQDNs des Spaces mitgelöscht responses: '200': description: Space erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '404': description: Space nicht gefunden '409': description: Space enthält noch FQDNs /spaces/{id}/fqdns/count: get: summary: FQDN-Anzahl abrufen description: Ruft die Anzahl der FQDNs für einen Space ab tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Anzahl der FQDNs content: application/json: schema: $ref: '#/components/schemas/CountResponse' /spaces/{id}/fqdns: get: summary: Alle FQDNs eines Spaces abrufen description: Ruft alle FQDNs für einen Space ab tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Liste der FQDNs content: application/json: schema: type: array items: $ref: '#/components/schemas/FQDN' '404': description: Space nicht gefunden post: summary: FQDN erstellen description: Erstellt einen neuen FQDN innerhalb eines Spaces tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateFQDNRequest' responses: '201': description: FQDN erfolgreich erstellt content: application/json: schema: $ref: '#/components/schemas/FQDN' '400': description: Ungültige Anfrage '404': description: Space nicht gefunden '409': description: FQDN existiert bereits in diesem Space delete: summary: Alle FQDNs eines Spaces löschen description: Löscht alle FQDNs eines Spaces tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Alle FQDNs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' /spaces/{id}/fqdns/{fqdnId}: delete: summary: FQDN löschen description: Löscht einen einzelnen FQDN tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid responses: '200': description: FQDN erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '404': description: FQDN nicht gefunden /fqdns: delete: summary: Alle FQDNs global löschen description: Löscht alle FQDNs aus allen Spaces. Erfordert confirm=true Query-Parameter. tags: [FQDNs] parameters: - name: confirm in: query required: true schema: type: boolean description: Muss true sein, um die Operation auszuführen responses: '200': description: Alle FQDNs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' '400': description: Bestätigung erforderlich /csrs: delete: summary: Alle CSRs global löschen description: Löscht alle CSRs aus allen Spaces. Erfordert confirm=true Query-Parameter. tags: [CSRs] parameters: - name: confirm in: query required: true schema: type: string description: Muss "true" sein, um die Operation auszuführen example: "true" responses: '200': description: Alle CSRs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' '400': description: Bestätigung erforderlich /spaces/{spaceId}/fqdns/{fqdnId}/csr: post: summary: CSR hochladen description: Lädt einen CSR (Certificate Signing Request) im PEM-Format hoch tags: [CSRs] parameters: - name: spaceId in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid requestBody: required: true content: multipart/form-data: schema: type: object required: [csr, spaceId, fqdn] properties: csr: type: string format: binary description: CSR-Datei im PEM-Format spaceId: type: string description: ID des Spaces fqdn: type: string description: Name des FQDNs responses: '201': description: CSR erfolgreich hochgeladen content: application/json: schema: $ref: '#/components/schemas/CSR' '400': description: Ungültige Anfrage oder ungültiges CSR-Format '404': description: Space oder FQDN nicht gefunden get: summary: CSR(s) abrufen description: Ruft CSR(s) für einen FQDN ab. Mit latest=true wird nur der neueste CSR zurückgegeben. tags: [CSRs] parameters: - name: spaceId in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid - name: latest in: query required: false schema: type: boolean default: false description: Wenn true, wird nur der neueste CSR zurückgegeben responses: '200': description: CSR(s) erfolgreich abgerufen content: application/json: schema: oneOf: - $ref: '#/components/schemas/CSR' - type: array items: $ref: '#/components/schemas/CSR' '404': description: FQDN nicht gefunden components: schemas: HealthResponse: type: object properties: status: type: string example: "ok" message: type: string example: "Backend ist erreichbar" time: type: string format: date-time example: "2024-01-15T10:30:00Z" StatsResponse: type: object properties: spaces: type: integer example: 5 fqdns: type: integer example: 12 csrs: type: integer example: 7 Space: type: object properties: id: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" name: type: string example: "Mein Space" description: type: string example: "Beschreibung des Spaces" createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" CreateSpaceRequest: type: object required: [name] properties: name: type: string example: "Mein Space" description: type: string example: "Beschreibung des Spaces" FQDN: type: object properties: id: type: string format: uuid example: "660e8400-e29b-41d4-a716-446655440000" spaceId: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" fqdn: type: string example: "example.com" description: type: string example: "Beschreibung des FQDN" createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" CreateFQDNRequest: type: object required: [fqdn] properties: fqdn: type: string example: "example.com" description: type: string example: "Beschreibung des FQDN" Extension: type: object properties: id: type: string example: "2.5.29.37" oid: type: string example: "2.5.29.37" name: type: string example: "X509v3 Extended Key Usage" critical: type: boolean example: false value: type: string example: "301406082b0601050507030106082b06010505070302" description: type: string example: "TLS Web Server Authentication" purposes: type: array items: type: string example: ["TLS Web Server Authentication", "TLS Web Client Authentication"] CSR: type: object properties: id: type: string format: uuid example: "770e8400-e29b-41d4-a716-446655440000" fqdnId: type: string format: uuid example: "660e8400-e29b-41d4-a716-446655440000" spaceId: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" fqdn: type: string example: "example.com" csrPem: type: string example: "-----BEGIN CERTIFICATE REQUEST-----" subject: type: string example: "CN=example.com" publicKeyAlgorithm: type: string example: "RSA" signatureAlgorithm: type: string example: "SHA256-RSA" keySize: type: integer example: 2048 dnsNames: type: array items: type: string example: ["example.com", "www.example.com"] emailAddresses: type: array items: type: string example: ["admin@example.com"] ipAddresses: type: array items: type: string example: ["192.168.1.1"] uris: type: array items: type: string example: ["https://example.com"] extensions: type: array items: $ref: '#/components/schemas/Extension' createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" MessageResponse: type: object properties: message: type: string example: "Operation erfolgreich" CountResponse: type: object properties: count: type: integer example: 5 DeleteResponse: type: object properties: message: type: string example: "Alle FQDNs erfolgreich gelöscht" deletedCount: type: integer example: 5` w.Header().Set("Content-Type", "application/x-yaml") w.Write([]byte(openAPIContent)) } func main() { log.Println("Starte certigo-addon Backend...") // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() defer func() { log.Println("Schließe Datenbankverbindung...") db.Close() }() log.Println("Datenbank initialisiert") // Initialisiere Provider pm := providers.GetManager() pm.RegisterProvider(providers.NewDummyCAProvider()) pm.RegisterProvider(providers.NewAutoDNSProvider()) pm.RegisterProvider(providers.NewHetznerProvider()) r := mux.NewRouter() // Swagger UI Route r.HandleFunc("/swagger", swaggerUIHandler).Methods("GET") r.HandleFunc("/api/openapi.yaml", openAPIHandler).Methods("GET") // API Routes api := r.PathPrefix("/api").Subrouter() api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS") api.HandleFunc("/stats", getStatsHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", getSpacesHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces", createSpaceHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{id}", deleteSpaceHandler).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns/count", getSpaceFqdnCountHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", getFqdnsHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", createFqdnHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns", deleteAllFqdnsHandler).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{id}/fqdns/{fqdnId}", deleteFqdnHandler).Methods("DELETE", "OPTIONS") api.HandleFunc("/fqdns", deleteAllFqdnsGlobalHandler).Methods("DELETE", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", uploadCSRHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", getCSRByFQDNHandler).Methods("GET", "OPTIONS") api.HandleFunc("/csrs", deleteAllCSRsHandler).Methods("DELETE", "OPTIONS") // Provider Routes api.HandleFunc("/providers", getProvidersHandler).Methods("GET", "OPTIONS") api.HandleFunc("/providers/{id}", getProviderHandler).Methods("GET", "OPTIONS") api.HandleFunc("/providers/{id}/enabled", setProviderEnabledHandler).Methods("PUT", "OPTIONS") api.HandleFunc("/providers/{id}/config", updateProviderConfigHandler).Methods("PUT", "OPTIONS") api.HandleFunc("/providers/{id}/test", testProviderConnectionHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr/sign", signCSRHandler).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates", getCertificatesHandler).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates/{certId}/refresh", refreshCertificateHandler).Methods("POST", "OPTIONS") // Start server port := ":8080" log.Printf("Server läuft auf Port %s", port) log.Fatal(http.ListenAndServe(port, r)) } // Provider Handlers func getProvidersHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } pm := providers.GetManager() allProviders := pm.GetAllProviders() // Definiere feste Reihenfolge der Provider providerOrder := []string{"dummy-ca", "autodns", "hetzner"} // Erstelle Map für schnellen Zugriff providerMap := make(map[string]providers.ProviderInfo) for id, provider := range allProviders { config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, Settings: provider.GetRequiredSettings(), } providerMap[id] = providerInfo } // Sortiere nach definierter Reihenfolge var providerInfos []providers.ProviderInfo for _, id := range providerOrder { if providerInfo, exists := providerMap[id]; exists { providerInfos = append(providerInfos, providerInfo) delete(providerMap, id) } } // Füge alle anderen Provider hinzu, die nicht in der Liste sind for _, providerInfo := range providerMap { providerInfos = append(providerInfos, providerInfo) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(providerInfos) } func getProviderHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) id := vars["id"] pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, Settings: provider.GetRequiredSettings(), } // Füge aktuelle Konfigurationswerte hinzu (ohne Passwörter) safeSettings := make(map[string]interface{}) for key, value := range config.Settings { // Verstecke Passwörter und API Keys in der Antwort if key == "password" || key == "apiKey" { if str, ok := value.(string); ok && str != "" { safeSettings[key] = "***" } else { safeSettings[key] = value } } else { safeSettings[key] = value } } // Konvertiere zu JSON für die Response safeSettingsJSON, _ := json.Marshal(safeSettings) var safeSettingsMap map[string]interface{} json.Unmarshal(safeSettingsJSON, &safeSettingsMap) response := map[string]interface{}{ "id": providerInfo.ID, "name": providerInfo.Name, "displayName": providerInfo.DisplayName, "description": providerInfo.Description, "enabled": providerInfo.Enabled, "settings": providerInfo.Settings, "config": safeSettingsMap, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func setProviderEnabledHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) id := vars["id"] var req struct { Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() if err := pm.SetProviderEnabled(id, req.Enabled); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Provider-Status erfolgreich aktualisiert", "enabled": req.Enabled, }) } func updateProviderConfigHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() config, _ := pm.GetProviderConfig(id) config.Settings = req.Settings if err := pm.UpdateProviderConfig(id, config); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Konfiguration erfolgreich aktualisiert", }) } func testProviderConnectionHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } if err := provider.TestConnection(req.Settings); err != nil { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": err.Error(), }) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Verbindung erfolgreich", }) } func signCSRHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] var req struct { ProviderID string `json:"providerId"` CSRID string `json:"csrId,omitempty"` // Optional: spezifischer CSR, sonst neuester } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.ProviderID == "" { http.Error(w, "providerId ist erforderlich", http.StatusBadRequest) return } // Hole neuesten CSR für den FQDN var csrPEM string var csrID string err := db.QueryRow(` SELECT id, csr_pem FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan(&csrID, &csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Kein CSR für diesen FQDN gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Laden des CSR: %v", err) return } // Wenn spezifischer CSR angefordert wurde if req.CSRID != "" && req.CSRID != csrID { err := db.QueryRow(` SELECT csr_pem FROM csrs WHERE id = ? AND fqdn_id = ? AND space_id = ? `, req.CSRID, fqdnID, spaceID).Scan(&csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "CSR nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) return } csrID = req.CSRID } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(req.ProviderID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(req.ProviderID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Signiere CSR result, err := provider.SignCSR(csrPEM, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Signieren des CSR: %v", err), http.StatusInternalServerError) log.Printf("Fehler beim Signieren des CSR: %v", err) return } // Speichere das Zertifikat in der DB certID := uuid.New().String() createdAt := time.Now() _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, csrID, result.OrderID, req.ProviderID, result.CertificatePEM, result.Status, createdAt) if err != nil { log.Printf("Fehler beim Speichern des Zertifikats: %v", err) // Weiterhin erfolgreich zurückgeben, auch wenn Speichern fehlschlägt } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": result.Message, "certificateId": certID, "orderId": result.OrderID, "status": result.Status, "csrId": csrID, }) } func getCertificatesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] // Hole alle Zertifikate für diesen FQDN rows, err := db.Query(` SELECT id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at FROM certificates WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Laden der Zertifikate", http.StatusInternalServerError) log.Printf("Fehler beim Laden der Zertifikate: %v", err) return } defer rows.Close() var certificates []map[string]interface{} for rows.Next() { var id, csrID, certID, providerID, certPEM, status, createdAt string err := rows.Scan(&id, &csrID, &certID, &providerID, &certPEM, &status, &createdAt) if err != nil { log.Printf("Fehler beim Scannen der Zertifikat-Zeile: %v", err) continue } certificates = append(certificates, map[string]interface{}{ "id": id, "csrId": csrID, "certificateId": certID, "providerId": providerID, "certificatePEM": certPEM, "status": status, "createdAt": createdAt, }) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(certificates) } func refreshCertificateHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] certID := vars["certId"] // Hole Zertifikat aus DB var certificateID, providerID string err := db.QueryRow(` SELECT certificate_id, provider_id FROM certificates WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certID, fqdnID, spaceID).Scan(&certificateID, &providerID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Zertifikat nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des Zertifikats", http.StatusInternalServerError) return } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(providerID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(providerID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Rufe Zertifikat von CA ab certPEM, err := provider.GetCertificate(certificateID, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Abrufen des Zertifikats: %v", err), http.StatusInternalServerError) return } // Aktualisiere Zertifikat in DB _, err = db.Exec(` UPDATE certificates SET certificate_pem = ? WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certPEM, certID, fqdnID, spaceID) if err != nil { log.Printf("Fehler beim Aktualisieren des Zertifikats: %v", err) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "certificatePEM": certPEM, "certificateId": certificateID, }) }