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 := `