Files
Charon/backend/internal/api/handlers/certificate_handler.go
GitHub Actions af8384046c chore: implement instruction compliance remediation
- Replace Go interface{} with any (Go 1.18+ standard)
- Add database indexes to frequently queried model fields
- Add JSDoc documentation to frontend API client methods
- Remove deprecated docker-compose version keys
- Add concurrency groups to all 25 GitHub Actions workflows
- Add YAML front matter and fix H1→H2 headings in docs

Coverage: Backend 85.5%, Frontend 87.73%
Security: No vulnerabilities detected

Refs: docs/plans/instruction_compliance_spec.md
2025-12-21 04:08:42 +00:00

219 lines
6.4 KiB
Go

package handlers
import (
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/Wikid82/charon/backend/internal/logger"
"github.com/Wikid82/charon/backend/internal/services"
"github.com/Wikid82/charon/backend/internal/util"
)
// BackupServiceInterface defines the contract for backup service operations
type BackupServiceInterface interface {
CreateBackup() (string, error)
ListBackups() ([]services.BackupFile, error)
DeleteBackup(filename string) error
GetBackupPath(filename string) (string, error)
RestoreBackup(filename string) error
GetAvailableSpace() (int64, error)
}
type CertificateHandler struct {
service *services.CertificateService
backupService BackupServiceInterface
notificationService *services.NotificationService
// Rate limiting for notifications
notificationMu sync.Mutex
lastNotificationTime map[uint]time.Time
}
func NewCertificateHandler(service *services.CertificateService, backupService BackupServiceInterface, ns *services.NotificationService) *CertificateHandler {
return &CertificateHandler{
service: service,
backupService: backupService,
notificationService: ns,
lastNotificationTime: make(map[uint]time.Time),
}
}
func (h *CertificateHandler) List(c *gin.Context) {
certs, err := h.service.ListCertificates()
if err != nil {
logger.Log().WithError(err).Error("failed to list certificates")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list certificates"})
return
}
c.JSON(http.StatusOK, certs)
}
type UploadCertificateRequest struct {
Name string `form:"name" binding:"required"`
Certificate string `form:"certificate"` // PEM content
PrivateKey string `form:"private_key"` // PEM content
}
func (h *CertificateHandler) Upload(c *gin.Context) {
// Handle multipart form
name := c.PostForm("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
return
}
// Read files
certFile, err := c.FormFile("certificate_file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "certificate_file is required"})
return
}
keyFile, err := c.FormFile("key_file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "key_file is required"})
return
}
// Open and read content
certSrc, err := certFile.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open cert file"})
return
}
defer func() {
if err := certSrc.Close(); err != nil {
logger.Log().WithError(err).Warn("failed to close certificate file")
}
}()
keySrc, err := keyFile.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open key file"})
return
}
defer func() {
if err := keySrc.Close(); err != nil {
logger.Log().WithError(err).Warn("failed to close key file")
}
}()
// Read to string
// Limit size to avoid DoS (e.g. 1MB)
certBytes := make([]byte, 1024*1024)
n, _ := certSrc.Read(certBytes)
certPEM := string(certBytes[:n])
keyBytes := make([]byte, 1024*1024)
n, _ = keySrc.Read(keyBytes)
keyPEM := string(keyBytes[:n])
cert, err := h.service.UploadCertificate(name, certPEM, keyPEM)
if err != nil {
logger.Log().WithError(err).Error("failed to upload certificate")
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to upload certificate"})
return
}
// Send Notification
if h.notificationService != nil {
h.notificationService.SendExternal(c.Request.Context(),
"cert",
"Certificate Uploaded",
fmt.Sprintf("Certificate %s uploaded", util.SanitizeForLog(cert.Name)),
map[string]any{
"Name": util.SanitizeForLog(cert.Name),
"Domains": util.SanitizeForLog(cert.Domains),
"Action": "uploaded",
},
)
}
c.JSON(http.StatusCreated, cert)
}
func (h *CertificateHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
// Validate ID range
if id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
// Check if certificate is in use before proceeding
inUse, err := h.service.IsCertificateInUse(uint(id))
if err != nil {
logger.Log().WithError(err).WithField("certificate_id", id).Error("failed to check certificate usage")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check certificate usage"})
return
}
if inUse {
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
return
}
// Create backup before deletion
if h.backupService != nil {
// Check disk space before backup (require at least 100MB free)
if availableSpace, err := h.backupService.GetAvailableSpace(); err != nil {
logger.Log().WithError(err).Warn("unable to check disk space, proceeding with backup")
} else if availableSpace < 100*1024*1024 {
logger.Log().WithField("available_bytes", availableSpace).Warn("low disk space, skipping backup")
c.JSON(http.StatusInsufficientStorage, gin.H{"error": "insufficient disk space for backup"})
return
}
if _, err := h.backupService.CreateBackup(); err != nil {
logger.Log().WithError(err).Error("failed to create backup before deletion")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create backup before deletion"})
return
}
}
// Proceed with deletion
if err := h.service.DeleteCertificate(uint(id)); err != nil {
if err == services.ErrCertInUse {
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
return
}
logger.Log().WithError(err).WithField("certificate_id", id).Error("failed to delete certificate")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete certificate"})
return
}
// Send Notification with rate limiting (1 per cert per 10 seconds)
if h.notificationService != nil {
h.notificationMu.Lock()
lastTime, exists := h.lastNotificationTime[uint(id)]
if !exists || time.Since(lastTime) > 10*time.Second {
h.lastNotificationTime[uint(id)] = time.Now()
h.notificationMu.Unlock()
h.notificationService.SendExternal(c.Request.Context(),
"cert",
"Certificate Deleted",
fmt.Sprintf("Certificate ID %d deleted", id),
map[string]any{
"ID": id,
"Action": "deleted",
},
)
} else {
h.notificationMu.Unlock()
logger.Log().WithField("certificate_id", id).Debug("notification rate limited")
}
}
c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
}