90 lines
2.4 KiB
Go
90 lines
2.4 KiB
Go
package lib
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/libdns/cloudflare"
|
|
"github.com/mholt/acmez/v3/acme"
|
|
)
|
|
|
|
// TLSConfig holds configuration for automatic TLS via Let's Encrypt DNS-01.
|
|
type TLSConfig struct {
|
|
Domain string // e.g. "use1.clavitor.ai"
|
|
CFToken string // Cloudflare API token for DNS-01 challenge
|
|
DataDir string // directory to store certs
|
|
Email string // ACME account email
|
|
}
|
|
|
|
// LoadTLSConfig reads TLS configuration from environment variables.
|
|
func LoadTLSConfig() TLSConfig {
|
|
return TLSConfig{
|
|
Domain: os.Getenv("TLS_DOMAIN"),
|
|
CFToken: os.Getenv("CF_API_TOKEN"),
|
|
DataDir: os.Getenv("TLS_CERT_DIR"),
|
|
Email: os.Getenv("TLS_EMAIL"),
|
|
}
|
|
}
|
|
|
|
// ListenAndServeTLS starts an HTTPS server on the given address using
|
|
// Let's Encrypt certificates obtained via Cloudflare DNS-01 challenge.
|
|
// Falls back to plain HTTP if TLS is not configured.
|
|
func ListenAndServeTLS(addr string, handler http.Handler, cfg TLSConfig) error {
|
|
if cfg.Domain == "" || cfg.CFToken == "" {
|
|
log.Printf("TLS not configured (missing TLS_DOMAIN or CF_API_TOKEN), serving plain HTTP")
|
|
return http.ListenAndServe(addr, handler)
|
|
}
|
|
|
|
if cfg.DataDir == "" {
|
|
cfg.DataDir = "/opt/clavitor/certs"
|
|
}
|
|
if cfg.Email == "" {
|
|
cfg.Email = "ops@clavitor.ai"
|
|
}
|
|
|
|
// Configure certmagic
|
|
certmagic.DefaultACME.Agreed = true
|
|
certmagic.DefaultACME.Email = cfg.Email
|
|
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
|
|
DNSManager: certmagic.DNSManager{
|
|
DNSProvider: &cloudflare.Provider{
|
|
APIToken: cfg.CFToken,
|
|
},
|
|
},
|
|
}
|
|
|
|
magic := certmagic.NewDefault()
|
|
magic.Storage = &certmagic.FileStorage{Path: cfg.DataDir}
|
|
|
|
// Obtain/renew cert
|
|
ctx := context.Background()
|
|
if err := magic.ManageSync(ctx, []string{cfg.Domain}); err != nil {
|
|
return fmt.Errorf("certmagic manage %s: %w", cfg.Domain, err)
|
|
}
|
|
|
|
tlsConfig := magic.TLSConfig()
|
|
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
|
|
|
|
server := &http.Server{
|
|
Addr: addr,
|
|
Handler: handler,
|
|
TLSConfig: tlsConfig,
|
|
}
|
|
|
|
log.Printf("Clavitor listening on https://0.0.0.0%s (%s)", addr, cfg.Domain)
|
|
// TLS certs are managed by certmagic, pass empty cert/key paths
|
|
return server.ListenAndServeTLS("", "")
|
|
}
|
|
|
|
// ChallengeError wraps ACME errors with actionable context.
|
|
func ChallengeError(err error) string {
|
|
if acmeErr, ok := err.(acme.Problem); ok {
|
|
return fmt.Sprintf("ACME error: %s (type: %s)", acmeErr.Detail, acmeErr.Type)
|
|
}
|
|
return err.Error()
|
|
}
|