mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-23 17:28:53 +02:00
fix(autocert): ensure extra certificate registration and renewal scheduling
Extra providers were not being properly initialized during NewProvider(), causing certificate registration and renewal scheduling to be skipped. - Add ConfigExtra type with idx field for provider indexing - Add MergeExtraConfig() for inheriting main provider settings - Add setupExtraProviders() for recursive extra provider initialization - Refactor NewProvider to return error and call setupExtraProviders() - Add provider-scoped logger with "main" or "extra[N]" name - Add batch operations: ObtainCertIfNotExistsAll(), ObtainCertAll() - Add ForceExpiryAll() with completion tracking via WaitRenewalDone() - Add RenewMode (force/ifNeeded) for controlling renewal behavior - Add PrintCertExpiriesAll() for logging all provider certificate expiries Summary of staged changes: - config.go: Added ConfigExtra type, MergeExtraConfig(), recursive validation with path uniqueness checking - provider.go: Added provider indexing, scoped logger, batch cert operations, force renewal with completion tracking, RenewMode control - setup.go: New file with setupExtraProviders() for proper extra provider initialization - setup_test.go: New tests for extra provider setup - multi_cert_test.go: New tests for multi-certificate functionality - renew.go: Updated to use new provider API with error handling - state.go: Updated to handle NewProvider error return
This commit is contained in:
@@ -10,12 +10,15 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +27,368 @@ import (
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
)
|
||||
|
||||
// TestACMEServer implements a minimal ACME server for testing with request tracking.
|
||||
type TestACMEServer struct {
|
||||
server *httptest.Server
|
||||
caCert *x509.Certificate
|
||||
caKey *rsa.PrivateKey
|
||||
clientCSRs map[string]*x509.CertificateRequest
|
||||
orderDomains map[string][]string
|
||||
authzDomains map[string]string
|
||||
orderSeq int
|
||||
certRequestCount map[string]int
|
||||
renewalRequestCount map[string]int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newTestACMEServer(t *testing.T) *TestACMEServer {
|
||||
t.Helper()
|
||||
|
||||
// Generate CA certificate and key
|
||||
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test CA"},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"Test"},
|
||||
StreetAddress: []string{""},
|
||||
PostalCode: []string{""},
|
||||
},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
caCert, err := x509.ParseCertificate(caCertDER)
|
||||
require.NoError(t, err)
|
||||
|
||||
acme := &TestACMEServer{
|
||||
caCert: caCert,
|
||||
caKey: caKey,
|
||||
clientCSRs: make(map[string]*x509.CertificateRequest),
|
||||
orderDomains: make(map[string][]string),
|
||||
authzDomains: make(map[string]string),
|
||||
orderSeq: 0,
|
||||
certRequestCount: make(map[string]int),
|
||||
renewalRequestCount: make(map[string]int),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
acme.setupRoutes(mux)
|
||||
|
||||
acme.server = httptest.NewUnstartedServer(mux)
|
||||
acme.server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{caCert.Raw},
|
||||
PrivateKey: caKey,
|
||||
},
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
acme.server.StartTLS()
|
||||
return acme
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) Close() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) URL() string {
|
||||
return s.server.URL
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) httpClient() *http.Client {
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(s.caCert)
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: certPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) setupRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/acme/acme/directory", s.handleDirectory)
|
||||
mux.HandleFunc("/acme/new-nonce", s.handleNewNonce)
|
||||
mux.HandleFunc("/acme/new-account", s.handleNewAccount)
|
||||
mux.HandleFunc("/acme/new-order", s.handleNewOrder)
|
||||
mux.HandleFunc("/acme/authz/", s.handleAuthorization)
|
||||
mux.HandleFunc("/acme/chall/", s.handleChallenge)
|
||||
mux.HandleFunc("/acme/order/", s.handleOrder)
|
||||
mux.HandleFunc("/acme/cert/", s.handleCertificate)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
directory := map[string]any{
|
||||
"newNonce": s.server.URL + "/acme/new-nonce",
|
||||
"newAccount": s.server.URL + "/acme/new-account",
|
||||
"newOrder": s.server.URL + "/acme/new-order",
|
||||
"keyChange": s.server.URL + "/acme/key-change",
|
||||
"meta": map[string]any{
|
||||
"termsOfService": s.server.URL + "/terms",
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(directory)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleNewNonce(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-12345")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
account := map[string]any{
|
||||
"status": "valid",
|
||||
"contact": []string{"mailto:test@example.com"},
|
||||
"orders": s.server.URL + "/acme/orders",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", s.server.URL+"/acme/account/1")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-67890")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(account)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleNewOrder(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var jws struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
json.Unmarshal(body, &jws)
|
||||
payloadBytes, _ := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||
var orderReq struct {
|
||||
Identifiers []map[string]string `json:"identifiers"`
|
||||
}
|
||||
json.Unmarshal(payloadBytes, &orderReq)
|
||||
|
||||
domains := []string{}
|
||||
for _, id := range orderReq.Identifiers {
|
||||
domains = append(domains, id["value"])
|
||||
}
|
||||
sort.Strings(domains)
|
||||
domainKey := strings.Join(domains, ",")
|
||||
|
||||
s.mu.Lock()
|
||||
s.orderSeq++
|
||||
orderID := fmt.Sprintf("test-order-%d", s.orderSeq)
|
||||
authzID := fmt.Sprintf("test-authz-%d", s.orderSeq)
|
||||
s.orderDomains[orderID] = domains
|
||||
if len(domains) > 0 {
|
||||
s.authzDomains[authzID] = domains[0]
|
||||
}
|
||||
s.certRequestCount[domainKey]++
|
||||
s.mu.Unlock()
|
||||
|
||||
order := map[string]any{
|
||||
"status": "ready",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": orderReq.Identifiers,
|
||||
"authorizations": []string{s.server.URL + "/acme/authz/" + authzID},
|
||||
"finalize": s.server.URL + "/acme/order/" + orderID + "/finalize",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", s.server.URL+"/acme/order/"+orderID)
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-order")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
authzID := strings.TrimPrefix(r.URL.Path, "/acme/authz/")
|
||||
domain := s.authzDomains[authzID]
|
||||
if domain == "" {
|
||||
domain = "test.example.com"
|
||||
}
|
||||
authz := map[string]any{
|
||||
"status": "valid",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifier": map[string]string{"type": "dns", "value": domain},
|
||||
"challenges": []map[string]any{
|
||||
{
|
||||
"type": "dns-01",
|
||||
"status": "valid",
|
||||
"url": s.server.URL + "/acme/chall/test-chall-789",
|
||||
"token": "test-token-abc123",
|
||||
},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-authz")
|
||||
json.NewEncoder(w).Encode(authz)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
challenge := map[string]any{
|
||||
"type": "dns-01",
|
||||
"status": "valid",
|
||||
"url": r.URL.String(),
|
||||
"token": "test-token-abc123",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-chall")
|
||||
json.NewEncoder(w).Encode(challenge)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/finalize") {
|
||||
s.handleFinalize(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
orderID := strings.TrimPrefix(r.URL.Path, "/acme/order/")
|
||||
domains := s.orderDomains[orderID]
|
||||
if len(domains) == 0 {
|
||||
domains = []string{"test.example.com"}
|
||||
}
|
||||
certURL := s.server.URL + "/acme/cert/" + orderID
|
||||
order := map[string]any{
|
||||
"status": "valid",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": func() []map[string]string {
|
||||
out := make([]map[string]string, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
out = append(out, map[string]string{"type": "dns", "value": d})
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
"certificate": certURL,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-order-get")
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleFinalize(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
csr, err := s.extractCSRFromJWS(body)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid CSR: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orderID := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/acme/order/"), "/finalize")
|
||||
s.mu.Lock()
|
||||
s.clientCSRs[orderID] = csr
|
||||
|
||||
// Detect renewal: if we already have a certificate for these domains, it's a renewal
|
||||
domains := csr.DNSNames
|
||||
sort.Strings(domains)
|
||||
domainKey := strings.Join(domains, ",")
|
||||
|
||||
if s.certRequestCount[domainKey] > 1 {
|
||||
s.renewalRequestCount[domainKey]++
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
certURL := s.server.URL + "/acme/cert/" + orderID
|
||||
order := map[string]any{
|
||||
"status": "valid",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": func() []map[string]string {
|
||||
out := make([]map[string]string, 0, len(domains))
|
||||
for _, d := range domains {
|
||||
out = append(out, map[string]string{"type": "dns", "value": d})
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
"certificate": certURL,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", strings.TrimSuffix(r.URL.String(), "/finalize"))
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-finalize")
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) extractCSRFromJWS(jwsData []byte) (*x509.CertificateRequest, error) {
|
||||
var jws struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
if err := json.Unmarshal(jwsData, &jws); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var finalizeReq struct {
|
||||
CSR string `json:"csr"`
|
||||
}
|
||||
if err := json.Unmarshal(payloadBytes, &finalizeReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csrBytes, err := base64.RawURLEncoding.DecodeString(finalizeReq.CSR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x509.ParseCertificateRequest(csrBytes)
|
||||
}
|
||||
|
||||
func (s *TestACMEServer) handleCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
orderID := strings.TrimPrefix(r.URL.Path, "/acme/cert/")
|
||||
csr, exists := s.clientCSRs[orderID]
|
||||
if !exists {
|
||||
http.Error(w, "No CSR found for order", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test Cert"},
|
||||
Country: []string{"US"},
|
||||
},
|
||||
DNSNames: csr.DNSNames,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, s.caCert, csr.PublicKey, s.caKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.caCert.Raw})
|
||||
|
||||
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-cert")
|
||||
w.Write(append(certPEM, caPEM...))
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dnsproviders.InitProviders()
|
||||
m.Run()
|
||||
@@ -41,7 +406,7 @@ func TestCustomProvider(t *testing.T) {
|
||||
ACMEKeyPath: "certs/custom-acme.key",
|
||||
}
|
||||
|
||||
err := cfg.Validate()
|
||||
err := error(cfg.Validate())
|
||||
require.NoError(t, err)
|
||||
|
||||
user, legoCfg, err := cfg.GetLegoConfig()
|
||||
@@ -62,7 +427,8 @@ func TestCustomProvider(t *testing.T) {
|
||||
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing field 'ca_dir_url'")
|
||||
require.Contains(t, err.Error(), "missing field")
|
||||
require.Contains(t, err.Error(), "ca_dir_url")
|
||||
})
|
||||
|
||||
t.Run("custom provider with step-ca internal CA", func(t *testing.T) {
|
||||
@@ -76,7 +442,7 @@ func TestCustomProvider(t *testing.T) {
|
||||
ACMEKeyPath: "certs/internal-acme.key",
|
||||
}
|
||||
|
||||
err := cfg.Validate()
|
||||
err := error(cfg.Validate())
|
||||
require.NoError(t, err)
|
||||
|
||||
user, legoCfg, err := cfg.GetLegoConfig()
|
||||
@@ -86,9 +452,10 @@ func TestCustomProvider(t *testing.T) {
|
||||
require.Equal(t, "https://step-ca.internal:443/acme/acme/directory", legoCfg.CADirURL)
|
||||
require.Equal(t, "admin@internal.com", user.Email)
|
||||
|
||||
provider := autocert.NewProvider(cfg, user, legoCfg)
|
||||
provider, err := autocert.NewProvider(cfg, user, legoCfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, provider)
|
||||
require.Equal(t, autocert.ProviderCustom, provider.GetName())
|
||||
require.Equal(t, "main", provider.GetName())
|
||||
require.Equal(t, "certs/internal.crt", provider.GetCertPath())
|
||||
require.Equal(t, "certs/internal.key", provider.GetKeyPath())
|
||||
})
|
||||
@@ -119,7 +486,8 @@ func TestObtainCertFromCustomProvider(t *testing.T) {
|
||||
require.NotNil(t, user)
|
||||
require.NotNil(t, legoCfg)
|
||||
|
||||
provider := autocert.NewProvider(cfg, user, legoCfg)
|
||||
provider, err := autocert.NewProvider(cfg, user, legoCfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, provider)
|
||||
|
||||
// Test obtaining certificate
|
||||
@@ -161,7 +529,8 @@ func TestObtainCertFromCustomProvider(t *testing.T) {
|
||||
require.NotNil(t, user)
|
||||
require.NotNil(t, legoCfg)
|
||||
|
||||
provider := autocert.NewProvider(cfg, user, legoCfg)
|
||||
provider, err := autocert.NewProvider(cfg, user, legoCfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, provider)
|
||||
|
||||
err = provider.ObtainCert()
|
||||
@@ -178,330 +547,3 @@ func TestObtainCertFromCustomProvider(t *testing.T) {
|
||||
require.True(t, time.Now().After(x509Cert.NotBefore))
|
||||
})
|
||||
}
|
||||
|
||||
// testACMEServer implements a minimal ACME server for testing.
|
||||
type testACMEServer struct {
|
||||
server *httptest.Server
|
||||
caCert *x509.Certificate
|
||||
caKey *rsa.PrivateKey
|
||||
clientCSRs map[string]*x509.CertificateRequest
|
||||
orderID string
|
||||
}
|
||||
|
||||
func newTestACMEServer(t *testing.T) *testACMEServer {
|
||||
t.Helper()
|
||||
|
||||
// Generate CA certificate and key
|
||||
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test CA"},
|
||||
Country: []string{"US"},
|
||||
Province: []string{""},
|
||||
Locality: []string{"Test"},
|
||||
StreetAddress: []string{""},
|
||||
PostalCode: []string{""},
|
||||
},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
caCert, err := x509.ParseCertificate(caCertDER)
|
||||
require.NoError(t, err)
|
||||
|
||||
acme := &testACMEServer{
|
||||
caCert: caCert,
|
||||
caKey: caKey,
|
||||
clientCSRs: make(map[string]*x509.CertificateRequest),
|
||||
orderID: "test-order-123",
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
acme.setupRoutes(mux)
|
||||
|
||||
acme.server = httptest.NewUnstartedServer(mux)
|
||||
acme.server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{
|
||||
{
|
||||
Certificate: [][]byte{caCert.Raw},
|
||||
PrivateKey: caKey,
|
||||
},
|
||||
},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
acme.server.StartTLS()
|
||||
return acme
|
||||
}
|
||||
|
||||
func (s *testACMEServer) Close() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func (s *testACMEServer) URL() string {
|
||||
return s.server.URL
|
||||
}
|
||||
|
||||
func (s *testACMEServer) httpClient() *http.Client {
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(s.caCert)
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: certPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testACMEServer) setupRoutes(mux *http.ServeMux) {
|
||||
// ACME directory endpoint
|
||||
mux.HandleFunc("/acme/acme/directory", s.handleDirectory)
|
||||
|
||||
// ACME endpoints
|
||||
mux.HandleFunc("/acme/new-nonce", s.handleNewNonce)
|
||||
mux.HandleFunc("/acme/new-account", s.handleNewAccount)
|
||||
mux.HandleFunc("/acme/new-order", s.handleNewOrder)
|
||||
mux.HandleFunc("/acme/authz/", s.handleAuthorization)
|
||||
mux.HandleFunc("/acme/chall/", s.handleChallenge)
|
||||
mux.HandleFunc("/acme/order/", s.handleOrder)
|
||||
mux.HandleFunc("/acme/cert/", s.handleCertificate)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
directory := map[string]interface{}{
|
||||
"newNonce": s.server.URL + "/acme/new-nonce",
|
||||
"newAccount": s.server.URL + "/acme/new-account",
|
||||
"newOrder": s.server.URL + "/acme/new-order",
|
||||
"keyChange": s.server.URL + "/acme/key-change",
|
||||
"meta": map[string]interface{}{
|
||||
"termsOfService": s.server.URL + "/terms",
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(directory)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleNewNonce(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-12345")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleNewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
account := map[string]interface{}{
|
||||
"status": "valid",
|
||||
"contact": []string{"mailto:test@example.com"},
|
||||
"orders": s.server.URL + "/acme/orders",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", s.server.URL+"/acme/account/1")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-67890")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(account)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleNewOrder(w http.ResponseWriter, r *http.Request) {
|
||||
authzID := "test-authz-456"
|
||||
|
||||
order := map[string]interface{}{
|
||||
"status": "ready", // Skip pending state for simplicity
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||
"authorizations": []string{s.server.URL + "/acme/authz/" + authzID},
|
||||
"finalize": s.server.URL + "/acme/order/" + s.orderID + "/finalize",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", s.server.URL+"/acme/order/"+s.orderID)
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-order")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
authz := map[string]interface{}{
|
||||
"status": "valid", // Skip challenge validation for simplicity
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifier": map[string]string{"type": "dns", "value": "test.example.com"},
|
||||
"challenges": []map[string]interface{}{
|
||||
{
|
||||
"type": "dns-01",
|
||||
"status": "valid",
|
||||
"url": s.server.URL + "/acme/chall/test-chall-789",
|
||||
"token": "test-token-abc123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-authz")
|
||||
json.NewEncoder(w).Encode(authz)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
challenge := map[string]interface{}{
|
||||
"type": "dns-01",
|
||||
"status": "valid",
|
||||
"url": r.URL.String(),
|
||||
"token": "test-token-abc123",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-chall")
|
||||
json.NewEncoder(w).Encode(challenge)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/finalize") {
|
||||
s.handleFinalize(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
certURL := s.server.URL + "/acme/cert/" + s.orderID
|
||||
order := map[string]interface{}{
|
||||
"status": "valid",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||
"certificate": certURL,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-order-get")
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleFinalize(w http.ResponseWriter, r *http.Request) {
|
||||
// Read the JWS payload
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract CSR from JWS payload
|
||||
csr, err := s.extractCSRFromJWS(body)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid CSR: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the CSR for certificate generation
|
||||
s.clientCSRs[s.orderID] = csr
|
||||
|
||||
certURL := s.server.URL + "/acme/cert/" + s.orderID
|
||||
order := map[string]interface{}{
|
||||
"status": "valid",
|
||||
"expires": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
||||
"identifiers": []map[string]string{{"type": "dns", "value": "test.example.com"}},
|
||||
"certificate": certURL,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Location", strings.TrimSuffix(r.URL.String(), "/finalize"))
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-finalize")
|
||||
json.NewEncoder(w).Encode(order)
|
||||
}
|
||||
|
||||
func (s *testACMEServer) extractCSRFromJWS(jwsData []byte) (*x509.CertificateRequest, error) {
|
||||
// Parse the JWS structure
|
||||
var jws struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jwsData, &jws); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the payload
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(jws.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the finalize request
|
||||
var finalizeReq struct {
|
||||
CSR string `json:"csr"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(payloadBytes, &finalizeReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the CSR
|
||||
csrBytes, err := base64.RawURLEncoding.DecodeString(finalizeReq.CSR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the CSR
|
||||
csr, err := x509.ParseCertificateRequest(csrBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return csr, nil
|
||||
}
|
||||
|
||||
func (s *testACMEServer) handleCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract order ID from URL
|
||||
orderID := strings.TrimPrefix(r.URL.Path, "/acme/cert/")
|
||||
|
||||
// Get the CSR for this order
|
||||
csr, exists := s.clientCSRs[orderID]
|
||||
if !exists {
|
||||
http.Error(w, "No CSR found for order", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create certificate using the public key from the client's CSR
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test Cert"},
|
||||
Country: []string{"US"},
|
||||
},
|
||||
DNSNames: csr.DNSNames,
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// Use the public key from the CSR and sign with CA key
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, s.caCert, csr.PublicKey, s.caKey)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return certificate chain
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.caCert.Raw})
|
||||
|
||||
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||
w.Header().Set("Replay-Nonce", "test-nonce-cert")
|
||||
w.Write(append(certPEM, caPEM...))
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
)
|
||||
|
||||
func TestExtraCertKeyPathsUnique(t *testing.T) {
|
||||
t.Run("duplicate cert_path rejected", func(t *testing.T) {
|
||||
cfg := &autocert.Config{
|
||||
Provider: autocert.ProviderLocal,
|
||||
Extra: []autocert.Config{
|
||||
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||
{CertPath: "a.crt", KeyPath: "b.key"},
|
||||
},
|
||||
}
|
||||
require.Error(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("duplicate key_path rejected", func(t *testing.T) {
|
||||
cfg := &autocert.Config{
|
||||
Provider: autocert.ProviderLocal,
|
||||
Extra: []autocert.Config{
|
||||
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||
{CertPath: "b.crt", KeyPath: "a.key"},
|
||||
},
|
||||
}
|
||||
require.Error(t, cfg.Validate())
|
||||
})
|
||||
}
|
||||
90
internal/autocert/provider_test/multi_cert_test.go
Normal file
90
internal/autocert/provider_test/multi_cert_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
//nolint:errchkjson,errcheck
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
func buildMultiCertYAML(serverURL string) []byte {
|
||||
return fmt.Appendf(nil, `
|
||||
email: main@example.com
|
||||
domains: [main.example.com]
|
||||
provider: custom
|
||||
ca_dir_url: %s/acme/acme/directory
|
||||
cert_path: certs/main.crt
|
||||
key_path: certs/main.key
|
||||
extra:
|
||||
- email: extra1@example.com
|
||||
domains: [extra1.example.com]
|
||||
cert_path: certs/extra1.crt
|
||||
key_path: certs/extra1.key
|
||||
- email: extra2@example.com
|
||||
domains: [extra2.example.com]
|
||||
cert_path: certs/extra2.crt
|
||||
key_path: certs/extra2.key
|
||||
`, serverURL)
|
||||
}
|
||||
|
||||
func TestMultipleCertificatesLifecycle(t *testing.T) {
|
||||
acmeServer := newTestACMEServer(t)
|
||||
defer acmeServer.Close()
|
||||
|
||||
yamlConfig := buildMultiCertYAML(acmeServer.URL())
|
||||
var cfg autocert.Config
|
||||
cfg.HTTPClient = acmeServer.httpClient()
|
||||
|
||||
/* unmarshal yaml config with multiple certs */
|
||||
err := error(serialization.UnmarshalValidateYAML(yamlConfig, &cfg))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"main.example.com"}, cfg.Domains)
|
||||
require.Len(t, cfg.Extra, 2)
|
||||
require.Equal(t, []string{"extra1.example.com"}, cfg.Extra[0].Domains)
|
||||
require.Equal(t, []string{"extra2.example.com"}, cfg.Extra[1].Domains)
|
||||
|
||||
var provider *autocert.Provider
|
||||
|
||||
/* initialize autocert with multi-cert config */
|
||||
user, legoCfg, gerr := cfg.GetLegoConfig()
|
||||
require.NoError(t, gerr)
|
||||
provider, err = autocert.NewProvider(&cfg, user, legoCfg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, provider)
|
||||
|
||||
// Start renewal scheduler
|
||||
root := task.RootTask("test", false)
|
||||
defer root.Finish(nil)
|
||||
provider.ScheduleRenewalAll(root)
|
||||
|
||||
require.Equal(t, "custom", cfg.Provider)
|
||||
require.Equal(t, "custom", cfg.Extra[0].Provider)
|
||||
require.Equal(t, "custom", cfg.Extra[1].Provider)
|
||||
|
||||
/* track cert requests for all configs */
|
||||
os.MkdirAll("certs", 0755)
|
||||
defer os.RemoveAll("certs")
|
||||
|
||||
err = provider.ObtainCertIfNotExistsAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, acmeServer.certRequestCount["main.example.com"])
|
||||
require.Equal(t, 1, acmeServer.certRequestCount["extra1.example.com"])
|
||||
require.Equal(t, 1, acmeServer.certRequestCount["extra2.example.com"])
|
||||
|
||||
/* track renewal scheduling and requests */
|
||||
|
||||
// force renewal for all providers and wait for completion
|
||||
ok := provider.ForceExpiryAll()
|
||||
require.True(t, ok)
|
||||
provider.WaitRenewalDone(t.Context())
|
||||
|
||||
require.Equal(t, 1, acmeServer.renewalRequestCount["main.example.com"])
|
||||
require.Equal(t, 1, acmeServer.renewalRequestCount["extra1.example.com"])
|
||||
require.Equal(t, 1, acmeServer.renewalRequestCount["extra2.example.com"])
|
||||
}
|
||||
@@ -71,15 +71,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "a.internal.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -100,15 +103,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -129,15 +135,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "unknown.domain.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -159,8 +168,11 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(nil)
|
||||
require.NoError(t, err)
|
||||
@@ -182,8 +194,11 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: ""})
|
||||
require.NoError(t, err)
|
||||
@@ -204,15 +219,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "FOO.EXAMPLE.COM"})
|
||||
require.NoError(t, err)
|
||||
@@ -233,15 +251,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: " foo.example.com. "})
|
||||
require.NoError(t, err)
|
||||
@@ -262,15 +283,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert1, KeyPath: extraKey1},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.a.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -292,8 +316,11 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -317,7 +344,7 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert1, KeyPath: extraKey1},
|
||||
{CertPath: extraCert2, KeyPath: extraKey2},
|
||||
},
|
||||
@@ -325,8 +352,11 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.test.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -352,15 +382,18 @@ func TestGetCertBySNI(t *testing.T) {
|
||||
Provider: autocert.ProviderLocal,
|
||||
CertPath: mainCert,
|
||||
KeyPath: mainKey,
|
||||
Extra: []autocert.Config{
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: extraCert, KeyPath: extraKey},
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
p := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, p.Setup())
|
||||
p, err := autocert.NewProvider(cfg, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.LoadCert()
|
||||
require.NoError(t, err)
|
||||
|
||||
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user