package providers import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // CertigoACMEProxyProvider ist der Provider für certigo-acmeproxy type CertigoACMEProxyProvider struct { baseURL string } func NewCertigoACMEProxyProvider() *CertigoACMEProxyProvider { return &CertigoACMEProxyProvider{} } func (p *CertigoACMEProxyProvider) GetName() string { return "certigo-acmeproxy" } func (p *CertigoACMEProxyProvider) GetDisplayName() string { return "Certigo ACME Proxy" } func (p *CertigoACMEProxyProvider) GetDescription() string { return "ACME DNS-01 Challenge Responder für Let's Encrypt und andere ACME CAs" } func (p *CertigoACMEProxyProvider) ValidateConfig(settings map[string]interface{}) error { baseURL, ok := settings["baseURL"].(string) if !ok || strings.TrimSpace(baseURL) == "" { return fmt.Errorf("baseURL ist erforderlich") } // Entferne trailing slash falls vorhanden baseURL = strings.TrimSuffix(baseURL, "/") // Validiere URL-Format if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { return fmt.Errorf("baseURL muss mit http:// oder https:// beginnen") } return nil } func (p *CertigoACMEProxyProvider) TestConnection(settings map[string]interface{}) error { // Validiere zuerst die Konfiguration if err := p.ValidateConfig(settings); err != nil { return err } baseURL, _ := settings["baseURL"].(string) baseURL = strings.TrimSuffix(baseURL, "/") // Teste Verbindung über Health Check url := fmt.Sprintf("%s/health", baseURL) client := &http.Client{ Timeout: 5 * time.Second, } resp, err := client.Get(url) if err != nil { return fmt.Errorf("acme-proxy nicht erreichbar: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("acme-proxy antwortet mit Status %d", resp.StatusCode) } // Prüfe Response Body body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("fehler beim Lesen der Health-Check-Response: %v", err) } var healthResponse struct { Status string `json:"status"` } if err := json.Unmarshal(body, &healthResponse); err != nil { return fmt.Errorf("ungültige Health-Check-Response: %v", err) } if healthResponse.Status != "ok" { return fmt.Errorf("acme-proxy meldet Status: %s", healthResponse.Status) } return nil } // GetRequiredSettings gibt die erforderlichen Einstellungen zurück func (p *CertigoACMEProxyProvider) GetRequiredSettings() []SettingField { return []SettingField{ { Name: "baseURL", Label: "Base URL", Type: "text", Required: true, Description: "Base URL des certigo-acmeproxy Services (z.B. http://localhost:8080)", }, } } // RegisterChallengeDomain registriert eine neue Challenge-Domain beim ACME Proxy func (p *CertigoACMEProxyProvider) RegisterChallengeDomain(settings map[string]interface{}) (*ChallengeDomainResponse, error) { if err := p.ValidateConfig(settings); err != nil { return nil, err } baseURL, _ := settings["baseURL"].(string) baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/register", baseURL) client := &http.Client{ Timeout: 10 * time.Second, } req, err := http.NewRequest("POST", url, nil) if err != nil { return nil, fmt.Errorf("fehler beim Erstellen des Requests: %v", err) } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("fehler beim Senden des Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("fehler beim Lesen der Response: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("acme-proxy Fehler (Status %d): %s", resp.StatusCode, string(body)) } var registerResponse ChallengeDomainResponse if err := json.Unmarshal(body, ®isterResponse); err != nil { return nil, fmt.Errorf("fehler beim Parsen der Response: %v", err) } return ®isterResponse, nil } // UpdateChallengeToken setzt oder aktualisiert den ACME Challenge Token func (p *CertigoACMEProxyProvider) UpdateChallengeToken(username, password, token string, settings map[string]interface{}) error { if err := p.ValidateConfig(settings); err != nil { return err } baseURL, _ := settings["baseURL"].(string) baseURL = strings.TrimSuffix(baseURL, "/") url := fmt.Sprintf("%s/update", baseURL) requestBody := map[string]string{ "txt": token, } jsonData, err := json.Marshal(requestBody) if err != nil { return fmt.Errorf("fehler beim Erstellen des Request-Body: %v", err) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("fehler beim Erstellen des Requests: %v", err) } req.Header.Set("Content-Type", "application/json") // Basic Authentication auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) req.Header.Set("Authorization", "Basic "+auth) client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Do(req) if err != nil { return fmt.Errorf("fehler beim Senden des Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("fehler beim Lesen der Response: %v", err) } if resp.StatusCode == http.StatusUnauthorized { return fmt.Errorf("ungültige Authentifizierung") } if resp.StatusCode == http.StatusBadRequest { return fmt.Errorf("ungültige Anfrage: %s", string(body)) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("acme-proxy Fehler (Status %d): %s", resp.StatusCode, string(body)) } return nil } // ChallengeDomainResponse enthält die Response von /register type ChallengeDomainResponse struct { Username string `json:"username"` Password string `json:"password"` Fulldomain string `json:"fulldomain"` Subdomain string `json:"subdomain"` } // SignCSR signiert einen CSR (für ACME nicht direkt verwendet, aber Interface erfordert es) func (p *CertigoACMEProxyProvider) SignCSR(csrPEM string, settings map[string]interface{}) (*SignCSRResult, error) { return nil, fmt.Errorf("certigo-acmeproxy unterstützt keine direkte CSR-Signierung. Verwenden Sie ACME für Zertifikatsanfragen.") } // GetCertificate ruft ein Zertifikat ab (für ACME nicht direkt verwendet, aber Interface erfordert es) func (p *CertigoACMEProxyProvider) GetCertificate(certificateID string, settings map[string]interface{}) (string, error) { return "", fmt.Errorf("certigo-acmeproxy unterstützt keinen direkten Zertifikat-Abruf. Verwenden Sie ACME für Zertifikatsanfragen.") }