Files
Charon/backend/internal/services/certificate_service.go
Wikid82 c97c16a752 feat: add Settings and Setup pages for user management
- Implemented Settings page for changing user passwords with validation and feedback.
- Created Setup page for initial admin account setup with form handling and navigation.
- Added API service layer for handling requests related to proxy hosts, remote servers, and import functionality.
- Introduced mock data for testing purposes and set up testing framework with vitest.
- Configured Tailwind CSS for styling and Vite for development and build processes.
- Added scripts for Dockerfile validation, Python syntax checking, and Sourcery integration.
- Implemented release and coverage scripts for better CI/CD practices.
2025-11-19 22:54:35 -05:00

106 lines
2.6 KiB
Go

package services
import (
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// CertificateInfo represents parsed certificate details.
type CertificateInfo struct {
Domain string `json:"domain"`
Issuer string `json:"issuer"`
ExpiresAt time.Time `json:"expires_at"`
Status string `json:"status"` // "valid", "expiring", "expired"
}
// CertificateService manages certificate retrieval and parsing.
type CertificateService struct {
dataDir string
}
// NewCertificateService creates a new certificate service.
func NewCertificateService(dataDir string) *CertificateService {
return &CertificateService{
dataDir: dataDir,
}
}
// ListCertificates scans the Caddy data directory for certificates.
// It looks in certificates/acme-v02.api.letsencrypt.org-directory/ and others.
func (s *CertificateService) ListCertificates() ([]CertificateInfo, error) {
certs := []CertificateInfo{}
certRoot := filepath.Join(s.dataDir, "certificates")
// Walk through the certificate directory
err := filepath.Walk(certRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
// If directory doesn't exist yet (fresh install), just return empty
if os.IsNotExist(err) {
return nil
}
return err
}
// We only care about .crt files
if !info.IsDir() && strings.HasSuffix(info.Name(), ".crt") {
cert, err := s.parseCertificate(path)
if err != nil {
// Log error but continue scanning other certs
fmt.Printf("failed to parse cert %s: %v\n", path, err)
return nil
}
certs = append(certs, *cert)
}
return nil
})
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("walk certificates: %w", err)
}
return certs, nil
}
func (s *CertificateService) parseCertificate(path string) (*CertificateInfo, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
block, _ := pem.Decode(data)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse certificate: %w", err)
}
status := "valid"
now := time.Now()
if now.After(cert.NotAfter) {
status = "expired"
} else if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {
status = "expiring"
}
// Domain is usually the CommonName or the first SAN
domain := cert.Subject.CommonName
if domain == "" && len(cert.DNSNames) > 0 {
domain = cert.DNSNames[0]
}
return &CertificateInfo{
Domain: domain,
Issuer: cert.Issuer.CommonName,
ExpiresAt: cert.NotAfter,
Status: status,
}, nil
}