Files
certigo/backend/main.go
2025-11-20 13:29:13 +01:00

2687 lines
77 KiB
Go

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, &notNull, &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 := `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Certigo Addon API - Swagger UI</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/api/openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
};
</script>
</body>
</html>`
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,
})
}