mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 08:44:25 +00:00
295 lines
8.4 KiB
Go
295 lines
8.4 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/acme/autocert"
|
|
)
|
|
|
|
// certGenerationMutex protects concurrent certificate generation for the same host
|
|
var certGenerationMutex sync.Mutex
|
|
|
|
// generateSelfSignedCert generates a self-signed certificate for the given host.
|
|
// Returns the certificate and private key in PEM format.
|
|
func generateSelfSignedCert(host string) (certPEM, keyPEM []byte, err error) {
|
|
// Generate private key
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
|
|
}
|
|
|
|
// Create certificate template
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year
|
|
|
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
|
|
}
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{
|
|
Organization: []string{"ResolveSpec Self-Signed"},
|
|
CommonName: host,
|
|
},
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
// Add host as DNS name or IP address
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
template.IPAddresses = []net.IP{ip}
|
|
} else {
|
|
template.DNSNames = []string{host}
|
|
}
|
|
|
|
// Create certificate
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
|
|
}
|
|
|
|
// Encode certificate to PEM
|
|
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
|
|
// Encode private key to PEM
|
|
privBytes, err := x509.MarshalECPrivateKey(priv)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})
|
|
|
|
return certPEM, keyPEM, nil
|
|
}
|
|
|
|
// sanitizeHostname converts a hostname to a safe filename by replacing invalid characters.
|
|
func sanitizeHostname(host string) string {
|
|
// Replace any character that's not alphanumeric, dot, or dash with underscore
|
|
safe := ""
|
|
for _, r := range host {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
|
|
safe += string(r)
|
|
} else {
|
|
safe += "_"
|
|
}
|
|
}
|
|
return safe
|
|
}
|
|
|
|
// getCertDirectory returns the directory path for storing self-signed certificates.
|
|
// Creates the directory if it doesn't exist.
|
|
func getCertDirectory() (string, error) {
|
|
// Use a consistent directory in the user's cache directory
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
// Fallback to current directory if cache dir is not available
|
|
cacheDir = "."
|
|
}
|
|
|
|
certDir := filepath.Join(cacheDir, "resolvespec", "certs")
|
|
|
|
// Create directory if it doesn't exist
|
|
if err := os.MkdirAll(certDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create certificate directory: %w", err)
|
|
}
|
|
|
|
return certDir, nil
|
|
}
|
|
|
|
// isCertificateValid checks if a certificate file exists and is not expired.
|
|
func isCertificateValid(certFile string) bool {
|
|
// Check if file exists
|
|
certData, err := os.ReadFile(certFile)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Parse certificate
|
|
block, _ := pem.Decode(certData)
|
|
if block == nil {
|
|
return false
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Check if certificate is expired or will expire in the next 30 days
|
|
now := time.Now()
|
|
expiryThreshold := now.Add(30 * 24 * time.Hour)
|
|
|
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
|
return false
|
|
}
|
|
|
|
// Renew if expiring soon
|
|
if expiryThreshold.After(cert.NotAfter) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// saveCertToFiles saves certificate and key PEM data to persistent files.
|
|
// Returns the file paths for the certificate and key.
|
|
func saveCertToFiles(certPEM, keyPEM []byte, host string) (certFile, keyFile string, err error) {
|
|
// Get certificate directory
|
|
certDir, err := getCertDirectory()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Sanitize hostname for safe file naming
|
|
safeHost := sanitizeHostname(host)
|
|
|
|
// Use consistent file names based on host
|
|
certFile = filepath.Join(certDir, fmt.Sprintf("%s-cert.pem", safeHost))
|
|
keyFile = filepath.Join(certDir, fmt.Sprintf("%s-key.pem", safeHost))
|
|
|
|
// Write certificate
|
|
if err := os.WriteFile(certFile, certPEM, 0600); err != nil {
|
|
return "", "", fmt.Errorf("failed to write certificate: %w", err)
|
|
}
|
|
|
|
// Write key
|
|
if err := os.WriteFile(keyFile, keyPEM, 0600); err != nil {
|
|
return "", "", fmt.Errorf("failed to write private key: %w", err)
|
|
}
|
|
|
|
return certFile, keyFile, nil
|
|
}
|
|
|
|
// setupAutoTLS configures automatic TLS certificate management using Let's Encrypt.
|
|
// Returns a TLS config that can be used with http.Server.
|
|
func setupAutoTLS(domains []string, email, cacheDir string) (*tls.Config, error) {
|
|
if len(domains) == 0 {
|
|
return nil, fmt.Errorf("at least one domain must be specified for AutoTLS")
|
|
}
|
|
|
|
// Set default cache directory
|
|
if cacheDir == "" {
|
|
cacheDir = "./certs-cache"
|
|
}
|
|
|
|
// Create cache directory if it doesn't exist
|
|
if err := os.MkdirAll(cacheDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create certificate cache directory: %w", err)
|
|
}
|
|
|
|
// Create autocert manager
|
|
m := &autocert.Manager{
|
|
Prompt: autocert.AcceptTOS,
|
|
Cache: autocert.DirCache(cacheDir),
|
|
HostPolicy: autocert.HostWhitelist(domains...),
|
|
Email: email,
|
|
}
|
|
|
|
// Create TLS config
|
|
tlsConfig := m.TLSConfig()
|
|
tlsConfig.MinVersion = tls.VersionTLS13
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// configureTLS configures TLS for the server based on the provided configuration.
|
|
// Returns the TLS config and certificate/key file paths (if applicable).
|
|
func configureTLS(cfg Config) (*tls.Config, string, string, error) {
|
|
// Option 1: Certificate files provided
|
|
if cfg.SSLCert != "" && cfg.SSLKey != "" {
|
|
// Validate that files exist
|
|
if _, err := os.Stat(cfg.SSLCert); os.IsNotExist(err) {
|
|
return nil, "", "", fmt.Errorf("SSL certificate file not found: %s", cfg.SSLCert)
|
|
}
|
|
if _, err := os.Stat(cfg.SSLKey); os.IsNotExist(err) {
|
|
return nil, "", "", fmt.Errorf("SSL key file not found: %s", cfg.SSLKey)
|
|
}
|
|
|
|
// Return basic TLS config - cert/key will be loaded by ListenAndServeTLS
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
return tlsConfig, cfg.SSLCert, cfg.SSLKey, nil
|
|
}
|
|
|
|
// Option 2: Auto TLS (Let's Encrypt)
|
|
if cfg.AutoTLS {
|
|
tlsConfig, err := setupAutoTLS(cfg.AutoTLSDomains, cfg.AutoTLSEmail, cfg.AutoTLSCacheDir)
|
|
if err != nil {
|
|
return nil, "", "", fmt.Errorf("failed to setup AutoTLS: %w", err)
|
|
}
|
|
return tlsConfig, "", "", nil
|
|
}
|
|
|
|
// Option 3: Self-signed certificate
|
|
if cfg.SelfSignedSSL {
|
|
host := cfg.Host
|
|
if host == "" || host == "0.0.0.0" {
|
|
host = "localhost"
|
|
}
|
|
|
|
// Sanitize hostname for safe file naming
|
|
safeHost := sanitizeHostname(host)
|
|
|
|
// Lock to prevent concurrent certificate generation for the same host
|
|
certGenerationMutex.Lock()
|
|
defer certGenerationMutex.Unlock()
|
|
|
|
// Get certificate directory
|
|
certDir, err := getCertDirectory()
|
|
if err != nil {
|
|
return nil, "", "", fmt.Errorf("failed to get certificate directory: %w", err)
|
|
}
|
|
|
|
// Check for existing valid certificates
|
|
certFile := filepath.Join(certDir, fmt.Sprintf("%s-cert.pem", safeHost))
|
|
keyFile := filepath.Join(certDir, fmt.Sprintf("%s-key.pem", safeHost))
|
|
|
|
// If valid certificates exist, reuse them
|
|
if isCertificateValid(certFile) {
|
|
// Verify key file also exists
|
|
if _, err := os.Stat(keyFile); err == nil {
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
return tlsConfig, certFile, keyFile, nil
|
|
}
|
|
}
|
|
|
|
// Generate new certificates
|
|
certPEM, keyPEM, err := generateSelfSignedCert(host)
|
|
if err != nil {
|
|
return nil, "", "", fmt.Errorf("failed to generate self-signed certificate: %w", err)
|
|
}
|
|
|
|
certFile, keyFile, err = saveCertToFiles(certPEM, keyPEM, host)
|
|
if err != nil {
|
|
return nil, "", "", fmt.Errorf("failed to save self-signed certificate: %w", err)
|
|
}
|
|
|
|
tlsConfig := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
return tlsConfig, certFile, keyFile, nil
|
|
}
|
|
|
|
return nil, "", "", nil
|
|
}
|