mirror of
https://github.com/go-acme/lego
synced 2026-03-14 14:35:48 +01:00
feat: use file configuration
This commit is contained in:
parent
45c92d75be
commit
0fe1f47b1d
11 changed files with 992 additions and 5 deletions
|
|
@ -258,6 +258,15 @@ linters:
|
|||
text: Function 'renewForDomains' has too many statements
|
||||
linters:
|
||||
- funlen
|
||||
- path: cmd/internal/root/process_obtain.go
|
||||
text: (cognitive|cyclomatic) complexity \d+ of func `obtain` is high
|
||||
linters:
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- path: cmd/internal/root/process_renew.go
|
||||
text: cyclomatic complexity \d+ of func `renewForDomains` is high
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: providers/dns/cpanel/cpanel.go
|
||||
text: cyclomatic complexity 13 of func `\(\*DNSProvider\)\.CleanUp` is high
|
||||
linters:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/flags"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/root"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
|
|
@ -14,10 +16,11 @@ func CreateRootCommand() *cli.Command {
|
|||
Usage: "ACME client written in Go",
|
||||
EnableShellCompletion: true,
|
||||
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
||||
setUpLogger(cmd)
|
||||
setUpLogger(cmd, nil)
|
||||
|
||||
return ctx, nil
|
||||
},
|
||||
Action: rootRun,
|
||||
Flags: flags.CreateRootFlags(),
|
||||
Commands: CreateCommands(),
|
||||
}
|
||||
|
|
@ -35,3 +38,39 @@ func CreateCommands() []*cli.Command {
|
|||
createMigrate(),
|
||||
}
|
||||
}
|
||||
|
||||
func rootRun(ctx context.Context, cmd *cli.Command) error {
|
||||
filename, err := getConfigurationPath(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := configuration.ReadConfiguration(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
setUpLogger(cmd, cfg.Log)
|
||||
|
||||
configuration.ApplyDefaults(cfg)
|
||||
|
||||
err = configuration.Validate(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set effective User Agent.
|
||||
cfg.UserAgent = getUserAgent(cmd, cfg.UserAgent)
|
||||
|
||||
return root.Process(ctx, cfg)
|
||||
}
|
||||
|
||||
func getConfigurationPath(cmd *cli.Command) (string, error) {
|
||||
configPath := cmd.String(flags.FlgConfig)
|
||||
|
||||
if configPath != "" {
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
return configuration.FindDefaultConfigurationFile()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,19 @@ import (
|
|||
)
|
||||
|
||||
func CreateRootFlags() []cli.Flag {
|
||||
return createLogFlags()
|
||||
flags := []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: FlgConfig,
|
||||
Aliases: []string{FlgConfig},
|
||||
Sources: cli.EnvVars(toEnvName(FlgConfig)),
|
||||
Usage: "Path to the configuration file.",
|
||||
Local: true,
|
||||
},
|
||||
}
|
||||
|
||||
flags = append(flags, createLogFlags()...)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func CreateRunFlags() []cli.Flag {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,11 @@ const (
|
|||
FlgLogFormat = "log.format"
|
||||
)
|
||||
|
||||
// Flag names related to the configuration file.
|
||||
const (
|
||||
FlgConfig = "config"
|
||||
)
|
||||
|
||||
// Flag names related to the specific run command.
|
||||
const (
|
||||
FlgPrivateKey = "private-key"
|
||||
|
|
|
|||
42
cmd/internal/root/process.go
Normal file
42
cmd/internal/root/process.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
)
|
||||
|
||||
func Process(ctx context.Context, cfg *configuration.Configuration) error {
|
||||
archiver := storage.NewArchiver(cfg.Storage)
|
||||
|
||||
err := archiver.Accounts(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = archiver.Certificates(cfg.Certificates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return obtain(ctx, cfg)
|
||||
}
|
||||
|
||||
// NOTE(ldez): this is partially a duplication with flags parsing, but the errors are slightly different.
|
||||
func parseAddress(address string) (string, string, error) {
|
||||
if !strings.Contains(address, ":") {
|
||||
return "", "", fmt.Errorf("the address only accepts 'interface:port' or ':port' for its argument: '%s'",
|
||||
address)
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("could not split address '%s': %w", address, err)
|
||||
}
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
205
cmd/internal/root/process_challenges.go
Normal file
205
cmd/internal/root/process_challenges.go
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/challenge"
|
||||
"github.com/go-acme/lego/v5/challenge/dns01"
|
||||
"github.com/go-acme/lego/v5/challenge/dnspersist01"
|
||||
"github.com/go-acme/lego/v5/challenge/http01"
|
||||
"github.com/go-acme/lego/v5/challenge/tlsalpn01"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/providers/dns"
|
||||
"github.com/go-acme/lego/v5/providers/http/memcached"
|
||||
"github.com/go-acme/lego/v5/providers/http/s3"
|
||||
"github.com/go-acme/lego/v5/providers/http/webroot"
|
||||
)
|
||||
|
||||
func setupChallenges(client *lego.Client, chlgConfig *configuration.Challenge, networkStack challenge.NetworkStack) {
|
||||
if chlgConfig.HTTP != nil {
|
||||
err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(chlgConfig.HTTP, networkStack), http01.SetDelay(chlgConfig.HTTP.Delay))
|
||||
if err != nil {
|
||||
log.Fatal("Could not set HTTP challenge provider.", log.ErrorAttr(err))
|
||||
}
|
||||
}
|
||||
|
||||
if chlgConfig.TLS != nil {
|
||||
err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(chlgConfig.TLS, networkStack), tlsalpn01.SetDelay(chlgConfig.TLS.Delay))
|
||||
if err != nil {
|
||||
log.Fatal("Could not set TLS challenge provider.", log.ErrorAttr(err))
|
||||
}
|
||||
}
|
||||
|
||||
if chlgConfig.DNS != nil {
|
||||
err := setupDNS(client, chlgConfig.DNS, networkStack)
|
||||
if err != nil {
|
||||
log.Fatal("Could not set DNS challenge provider.", log.ErrorAttr(err))
|
||||
}
|
||||
}
|
||||
|
||||
if chlgConfig.DNSPersist != nil {
|
||||
err := setupDNSPersist(client, chlgConfig.DNSPersist, networkStack)
|
||||
if err != nil {
|
||||
log.Fatal("Could not set DNS-PERSIST challenge provider.", log.ErrorAttr(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupHTTPProvider(chlg *configuration.HTTPChallenge, networkStack challenge.NetworkStack) challenge.Provider {
|
||||
switch {
|
||||
case chlg.Webroot != "":
|
||||
ps, err := webroot.NewHTTPProvider(chlg.Webroot)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create the webroot provider.",
|
||||
slog.String("webRoot", chlg.Webroot),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return ps
|
||||
|
||||
case len(chlg.MemcachedHosts) > 0:
|
||||
ps, err := memcached.NewMemcachedProvider(chlg.MemcachedHosts)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create the memcached provider.",
|
||||
slog.String("memcachedHosts", strings.Join(chlg.MemcachedHosts, ", ")),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return ps
|
||||
|
||||
case chlg.S3Bucket != "":
|
||||
ps, err := s3.NewHTTPProvider(chlg.S3Bucket)
|
||||
if err != nil {
|
||||
log.Fatal("Could not create the S3 provider.",
|
||||
slog.String("bucket", chlg.S3Bucket),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
}
|
||||
|
||||
return ps
|
||||
|
||||
case chlg.Address != "":
|
||||
host, port, err := parseAddress(chlg.Address)
|
||||
if err != nil {
|
||||
log.Fatal("Could not split host and port.", slog.String("iface", chlg.Address), log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
srv := http01.NewProviderServerWithOptions(http01.Options{
|
||||
Network: networkStack.Network("tcp"),
|
||||
Address: net.JoinHostPort(host, port),
|
||||
})
|
||||
|
||||
if header := chlg.ProxyHeader; header != "" {
|
||||
srv.SetProxyHeader(header)
|
||||
}
|
||||
|
||||
return srv
|
||||
|
||||
default:
|
||||
srv := http01.NewProviderServerWithOptions(http01.Options{
|
||||
Network: networkStack.Network("tcp"),
|
||||
Address: net.JoinHostPort("", ":80"),
|
||||
})
|
||||
|
||||
if header := chlg.ProxyHeader; header != "" {
|
||||
srv.SetProxyHeader(header)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
}
|
||||
|
||||
func setupTLSProvider(chlg *configuration.TLSChallenge, networkStack challenge.NetworkStack) challenge.Provider {
|
||||
switch {
|
||||
case chlg.Address != "":
|
||||
host, port, err := parseAddress(chlg.Address)
|
||||
if err != nil {
|
||||
log.Fatal("Could not split host and port.", slog.String("iface", chlg.Address), log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
return tlsalpn01.NewProviderServerWithOptions(tlsalpn01.Options{
|
||||
Network: networkStack.Network("tcp"),
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
default:
|
||||
return tlsalpn01.NewProviderServerWithOptions(tlsalpn01.Options{
|
||||
Network: networkStack.Network("tcp"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupDNS(client *lego.Client, chlg *configuration.DNSChallenge, networkStack challenge.NetworkStack) error {
|
||||
provider, err := dns.NewDNSChallengeProviderByName(chlg.Provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &dns01.Options{RecursiveNameservers: chlg.Resolvers}
|
||||
|
||||
if chlg.DNSTimeout > 0 {
|
||||
opts.Timeout = time.Duration(chlg.DNSTimeout) * time.Second
|
||||
}
|
||||
|
||||
opts.NetworkStack = networkStack
|
||||
|
||||
dns01.SetDefaultClient(dns01.NewClient(opts))
|
||||
|
||||
return client.Challenge.SetDNS01Provider(provider,
|
||||
dns01.LazyCondOption(chlg.Propagation != nil, func() dns01.ChallengeOption {
|
||||
if chlg.Propagation.Wait > 0 {
|
||||
return dns01.PropagationWait(chlg.Propagation.Wait, true)
|
||||
}
|
||||
|
||||
return dns01.CombineOptions(
|
||||
dns01.CondOptions(chlg.Propagation.DisableAuthoritativeNameservers,
|
||||
dns01.DisableAuthoritativeNssPropagationRequirement(),
|
||||
),
|
||||
dns01.CondOptions(chlg.Propagation.DisableRecursiveNameservers,
|
||||
dns01.DisableRecursiveNSsPropagationRequirement(),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func setupDNSPersist(client *lego.Client, chlg *configuration.DNSPersistChallenge, networkStack challenge.NetworkStack) error {
|
||||
opts := &dns01.Options{RecursiveNameservers: chlg.Resolvers}
|
||||
|
||||
if chlg.DNSTimeout > 0 {
|
||||
opts.Timeout = time.Duration(chlg.DNSTimeout) * time.Second
|
||||
}
|
||||
|
||||
opts.NetworkStack = networkStack
|
||||
|
||||
dnspersist01.SetDefaultClient(dnspersist01.NewClient(opts))
|
||||
|
||||
return client.Challenge.SetDNSPersist01(
|
||||
dnspersist01.WithIssuerDomainName(chlg.IssuerDomainName),
|
||||
dnspersist01.CondOptions(!chlg.PersistUntil.IsZero(),
|
||||
dnspersist01.WithPersistUntil(chlg.PersistUntil),
|
||||
),
|
||||
dnspersist01.LazyCondOption(chlg.Propagation != nil, func() dnspersist01.ChallengeOption {
|
||||
if chlg.Propagation.Wait > 0 {
|
||||
return dnspersist01.PropagationWait(chlg.Propagation.Wait, true)
|
||||
}
|
||||
|
||||
return dnspersist01.CombineOptions(
|
||||
dnspersist01.CondOptions(chlg.Propagation.DisableAuthoritativeNameservers,
|
||||
dnspersist01.DisableAuthoritativeNssPropagationRequirement(),
|
||||
),
|
||||
dnspersist01.CondOptions(chlg.Propagation.DisableRecursiveNameservers,
|
||||
dnspersist01.DisableRecursiveNSsPropagationRequirement(),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
185
cmd/internal/root/process_obtain.go
Normal file
185
cmd/internal/root/process_obtain.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/challenge"
|
||||
"github.com/go-acme/lego/v5/cmd/internal"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
)
|
||||
|
||||
func obtain(ctx context.Context, cfg *configuration.Configuration) error {
|
||||
networkStack := getNetworkStack(cfg)
|
||||
|
||||
for accountID, challengesInfo := range createCertificatesMapping(cfg) {
|
||||
accountConfig := cfg.Accounts[accountID]
|
||||
|
||||
keyType, err := certcrypto.GetKeyType(accountConfig.KeyType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverConfig := configuration.GetServerConfig(cfg, accountID)
|
||||
|
||||
accountsStorage, err := storage.NewAccountsStorage(storage.AccountsStorageConfig{
|
||||
BasePath: cfg.Storage,
|
||||
Server: serverConfig.URL,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := accountsStorage.Get(ctx, keyType, accountConfig.Email, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lazyClient := sync.OnceValues(func() (*lego.Client, error) {
|
||||
client, errC := lego.NewClient(newClientConfig(serverConfig, account, keyType, cfg.UserAgent))
|
||||
if errC != nil {
|
||||
return nil, errC
|
||||
}
|
||||
|
||||
if client.GetServerMetadata().ExternalAccountRequired && accountConfig.ExternalAccountBinding != nil {
|
||||
return nil, errors.New("server requires External Account Binding (EAB)")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
})
|
||||
|
||||
if account.Registration == nil {
|
||||
client, errC := lazyClient()
|
||||
if errC != nil {
|
||||
return fmt.Errorf("set up client: %w", errC)
|
||||
}
|
||||
|
||||
var reg *acme.ExtendedAccount
|
||||
|
||||
reg, errC = registerAccount(ctx, client, accountConfig)
|
||||
if errC != nil {
|
||||
return fmt.Errorf("could not complete registration: %w", errC)
|
||||
}
|
||||
|
||||
account.Registration = reg
|
||||
|
||||
if errC = accountsStorage.Save(keyType, account); errC != nil {
|
||||
return fmt.Errorf("could not save the account file: %w", errC)
|
||||
}
|
||||
|
||||
fmt.Printf(storage.RootPathWarningMessage, accountsStorage.GetRootPath())
|
||||
}
|
||||
|
||||
certsStorage := storage.NewCertificatesStorage(cfg.Storage)
|
||||
|
||||
for challengeID, certIDs := range challengesInfo {
|
||||
chlgConfig := cfg.Challenges[challengeID]
|
||||
|
||||
lazySetup := sync.OnceValues(func() (*lego.Client, error) {
|
||||
client, errC := lazyClient()
|
||||
if errC != nil {
|
||||
return nil, fmt.Errorf("set up client: %w", errC)
|
||||
}
|
||||
|
||||
client.Challenge.RemoveAll()
|
||||
|
||||
setupChallenges(client, chlgConfig, networkStack)
|
||||
|
||||
return client, nil
|
||||
})
|
||||
|
||||
for _, certID := range certIDs {
|
||||
certConfig := cfg.Certificates[certID]
|
||||
|
||||
// Renew
|
||||
if certsStorage.ExistsFile(certID, storage.ExtResource) {
|
||||
err = renewCertificate(ctx, lazyClient, certID, certConfig, certsStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Run
|
||||
err := runCertificate(ctx, lazySetup, certConfig, certsStorage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createCertificatesMapping creates a mapping of account -> challenge -> certificate IDs.
|
||||
func createCertificatesMapping(cfg *configuration.Configuration) map[string]map[string][]string {
|
||||
// Accounts -> Challenges -> Certificates
|
||||
certsMappings := make(map[string]map[string][]string)
|
||||
|
||||
for certID, certDesc := range cfg.Certificates {
|
||||
if _, ok := certsMappings[certDesc.Account]; !ok {
|
||||
certsMappings[certDesc.Account] = make(map[string][]string)
|
||||
}
|
||||
|
||||
certsMappings[certDesc.Account][certDesc.Challenge] = append(certsMappings[certDesc.Account][certDesc.Challenge], certID)
|
||||
}
|
||||
|
||||
return certsMappings
|
||||
}
|
||||
|
||||
func getNetworkStack(cfg *configuration.Configuration) challenge.NetworkStack {
|
||||
switch cfg.NetworkStack {
|
||||
case "ipv4only", "ipv4":
|
||||
return challenge.IPv4Only
|
||||
|
||||
case "ipv6only", "ipv6":
|
||||
return challenge.IPv6Only
|
||||
|
||||
default:
|
||||
return challenge.DualStack
|
||||
}
|
||||
}
|
||||
|
||||
func newClientConfig(serverConfig *configuration.Server, account registration.User, keyType certcrypto.KeyType, ua string) *lego.Config {
|
||||
config := lego.NewConfig(account)
|
||||
config.CADirURL = serverConfig.URL
|
||||
config.UserAgent = ua
|
||||
|
||||
config.Certificate = lego.CertificateConfig{KeyType: keyType}
|
||||
|
||||
if serverConfig.OverallRequestLimit > 0 {
|
||||
config.Certificate.OverallRequestLimit = serverConfig.OverallRequestLimit
|
||||
}
|
||||
|
||||
if serverConfig.CertTimeout > 0 {
|
||||
config.Certificate.Timeout = time.Duration(serverConfig.CertTimeout) * time.Second
|
||||
}
|
||||
|
||||
if serverConfig.HTTPTimeout > 0 {
|
||||
config.HTTPClient.Timeout = time.Duration(serverConfig.HTTPTimeout) * time.Second
|
||||
}
|
||||
|
||||
if serverConfig.TLSSkipVerify {
|
||||
defaultTransport, ok := config.HTTPClient.Transport.(*http.Transport)
|
||||
if ok { // This is always true because the default client used by the CLI defined the transport.
|
||||
tr := defaultTransport.Clone()
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
config.HTTPClient.Transport = tr
|
||||
}
|
||||
}
|
||||
|
||||
config.HTTPClient = internal.NewRetryableClient(config.HTTPClient)
|
||||
|
||||
return config
|
||||
}
|
||||
73
cmd/internal/root/process_register.go
Normal file
73
cmd/internal/root/process_register.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/go-acme/lego/v5/registration"
|
||||
"github.com/go-acme/lego/v5/registration/zerossl"
|
||||
)
|
||||
|
||||
func registerAccount(ctx context.Context, client *lego.Client, accountConfig *configuration.Account) (*acme.ExtendedAccount, error) {
|
||||
accepted := handleTOS(client, accountConfig)
|
||||
if !accepted {
|
||||
return nil, errors.New("you did not accept the TOS: unable to proceed")
|
||||
}
|
||||
|
||||
if accountConfig.ExternalAccountBinding != nil {
|
||||
return client.Registration.RegisterWithExternalAccountBinding(ctx, registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: accountConfig.ExternalAccountBinding.KID,
|
||||
HmacEncoded: accountConfig.ExternalAccountBinding.HmacKey,
|
||||
})
|
||||
} else if zerossl.IsZeroSSL(accountConfig.Server) {
|
||||
return registration.RegisterWithZeroSSL(ctx, client.Registration, accountConfig.Email)
|
||||
}
|
||||
|
||||
return client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
}
|
||||
|
||||
func handleTOS(client *lego.Client, accountConfig *configuration.Account) bool {
|
||||
// metadata items are optional, and termsOfService too.
|
||||
urlTOS := client.GetServerMetadata().TermsOfService
|
||||
if urlTOS == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a global acceptance override
|
||||
if accountConfig.AcceptsTermsOfService {
|
||||
return true
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
log.Warn("Please review the TOS", slog.String("url", urlTOS))
|
||||
|
||||
for {
|
||||
fmt.Println("Do you accept the TOS? Y/n")
|
||||
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatal("Could not read from the console", log.ErrorAttr(err))
|
||||
}
|
||||
|
||||
text = strings.Trim(text, "\r\n")
|
||||
switch text {
|
||||
case "", "y", "Y":
|
||||
return true
|
||||
case "n", "N":
|
||||
return false
|
||||
default:
|
||||
fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.")
|
||||
}
|
||||
}
|
||||
}
|
||||
328
cmd/internal/root/process_renew.go
Normal file
328
cmd/internal/root/process_renew.go
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v5/acme/api"
|
||||
"github.com/go-acme/lego/v5/certcrypto"
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
"github.com/go-acme/lego/v5/log"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
type lzSetUp func() (*lego.Client, error)
|
||||
|
||||
func renewCertificate(ctx context.Context, lazyClient lzSetUp, certID string, certConfig *configuration.Certificate, certsStorage *storage.CertificatesStorage) error {
|
||||
// CSR
|
||||
if certConfig.CSR != "" {
|
||||
return renewForCSR(ctx, lazyClient, certID, certConfig, certsStorage)
|
||||
}
|
||||
|
||||
// Domains
|
||||
return renewForDomains(ctx, lazyClient, certID, certConfig, certsStorage)
|
||||
}
|
||||
|
||||
func renewForDomains(ctx context.Context, lazyClient lzSetUp, certID string, certConfig *configuration.Certificate, certsStorage *storage.CertificatesStorage) error {
|
||||
certificates, err := certsStorage.ReadCertificate(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading the certificate for %q: %w", certID, err)
|
||||
}
|
||||
|
||||
cert := certificates[0]
|
||||
|
||||
if cert.IsCA {
|
||||
return fmt.Errorf("certificate bundle for %q starts with a CA certificate", certID)
|
||||
}
|
||||
|
||||
ariRenewalTime, replacesCertID, err := getARIInfo(ctx, lazyClient, certID, certConfig.Renew, cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certDomains := certcrypto.ExtractDomains(cert)
|
||||
|
||||
renewalDomains := slices.Clone(certConfig.Domains)
|
||||
|
||||
if ariRenewalTime == nil && sameDomains(certDomains, renewalDomains) &&
|
||||
!isInRenewalPeriod(cert, certID, certConfig.Renew.Days, time.Now()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
log.Info("acme: Trying renewal.",
|
||||
log.CertNameAttr(certID),
|
||||
slog.Any("time-remaining", log.FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))),
|
||||
)
|
||||
|
||||
client, err := lazyClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("set up client: %w", err)
|
||||
}
|
||||
|
||||
randomSleep()
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: renewalDomains,
|
||||
MustStaple: certConfig.MustStaple,
|
||||
NotBefore: certConfig.NotBefore,
|
||||
NotAfter: certConfig.NotAfter,
|
||||
Bundle: !certConfig.NoBundle,
|
||||
PreferredChain: certConfig.PreferredChain,
|
||||
EnableCommonName: certConfig.EnableCommonName,
|
||||
Profile: certConfig.Profile,
|
||||
AlwaysDeactivateAuthorizations: certConfig.AlwaysDeactivateAuthorizations,
|
||||
}
|
||||
|
||||
if certConfig.Renew != nil && certConfig.Renew.ReuseKey {
|
||||
request.PrivateKey, err = certsStorage.ReadPrivateKey(certID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if replacesCertID != "" {
|
||||
request.ReplacesCertID = replacesCertID
|
||||
}
|
||||
|
||||
certRes, err := client.Certificate.Obtain(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not obtain the certificate for %q: %w", certID, err)
|
||||
}
|
||||
|
||||
certRes.ID = certID
|
||||
|
||||
err = certsStorage.Save(certRes, &storage.SaveOptions{PEM: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save the resource: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renewForCSR(ctx context.Context, lazyClient lzSetUp, certID string, certConfig *configuration.Certificate, certsStorage *storage.CertificatesStorage) error {
|
||||
csr, err := storage.ReadCSRFile(certConfig.CSR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read CSR file %q: %w", certConfig.CSR, err)
|
||||
}
|
||||
|
||||
certificates, err := certsStorage.ReadCertificate(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CSR: error while reading the certificate for domains %q: %w",
|
||||
strings.Join(certcrypto.ExtractDomainsCSR(csr), ","), err)
|
||||
}
|
||||
|
||||
cert := certificates[0]
|
||||
|
||||
if cert.IsCA {
|
||||
return fmt.Errorf("certificate bundle for %q starts with a CA certificate", certID)
|
||||
}
|
||||
|
||||
ariRenewalTime, replacesCertID, err := getARIInfo(ctx, lazyClient, certID, certConfig.Renew, cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CSR: %w", err)
|
||||
}
|
||||
|
||||
if ariRenewalTime == nil && sameDomainsCertificate(cert, csr) &&
|
||||
!isInRenewalPeriod(cert, certID, certConfig.Renew.Days, time.Now()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
log.Info("acme: Trying renewal.",
|
||||
log.CertNameAttr(certID),
|
||||
slog.Any("time-remaining", log.FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))),
|
||||
)
|
||||
|
||||
client, err := lazyClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("set up client: %w", err)
|
||||
}
|
||||
|
||||
request := certificate.ObtainForCSRRequest{
|
||||
CSR: csr,
|
||||
NotBefore: certConfig.NotBefore,
|
||||
NotAfter: certConfig.NotAfter,
|
||||
Bundle: !certConfig.NoBundle,
|
||||
PreferredChain: certConfig.PreferredChain,
|
||||
EnableCommonName: certConfig.EnableCommonName,
|
||||
Profile: certConfig.Profile,
|
||||
AlwaysDeactivateAuthorizations: certConfig.AlwaysDeactivateAuthorizations,
|
||||
}
|
||||
|
||||
if replacesCertID != "" {
|
||||
request.ReplacesCertID = replacesCertID
|
||||
}
|
||||
|
||||
certRes, err := client.Certificate.ObtainForCSR(ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CSR: could not obtain the certificate: %w", err)
|
||||
}
|
||||
|
||||
certRes.ID = certID
|
||||
|
||||
err = certsStorage.Save(certRes, &storage.SaveOptions{PEM: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("CSR: could not save the resource: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isInRenewalPeriod(cert *x509.Certificate, domain string, days int, now time.Time) bool {
|
||||
dueDate := getDueDate(cert, days, now)
|
||||
|
||||
if dueDate.Before(now) || dueDate.Equal(now) {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Infof(
|
||||
log.LazySprintf("Skip renewal: The certificate expires at %s, the renewal can be performed in %s.",
|
||||
cert.NotAfter.Format(time.RFC3339),
|
||||
log.FormattableDuration(dueDate.Sub(now)),
|
||||
),
|
||||
log.CertNameAttr(domain),
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getDueDate(x509Cert *x509.Certificate, days int, now time.Time) time.Time {
|
||||
if days == 0 {
|
||||
lifetime := x509Cert.NotAfter.Sub(x509Cert.NotBefore)
|
||||
|
||||
var divisor int64 = 3
|
||||
if lifetime.Round(24*time.Hour).Hours()/24.0 <= 10 {
|
||||
divisor = 2
|
||||
}
|
||||
|
||||
return x509Cert.NotAfter.Add(-1 * time.Duration(lifetime.Nanoseconds()/divisor))
|
||||
}
|
||||
|
||||
if days < 0 {
|
||||
// if the number of days is negative: always renew the certificate.
|
||||
return now
|
||||
}
|
||||
|
||||
return x509Cert.NotAfter.Add(-1 * time.Duration(days) * 24 * time.Hour)
|
||||
}
|
||||
|
||||
func getARIInfo(ctx context.Context, lazyClient lzSetUp, certID string, renewConfig *configuration.RenewConfiguration, cert *x509.Certificate) (*time.Time, string, error) {
|
||||
// renewConfig and renewConfig.ARI cannot be nil: they are always defined in the default.
|
||||
if renewConfig == nil || renewConfig.ARI == nil || renewConfig.ARI.Disable {
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
client, err := lazyClient()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("set up client: %w", err)
|
||||
}
|
||||
|
||||
ariRenewalTime := getARIRenewalTime(ctx, renewConfig.ARI.WaitToRenewDuration, cert, certID, client)
|
||||
if ariRenewalTime != nil {
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Figure out if we need to sleep before renewing.
|
||||
if ariRenewalTime.After(now) {
|
||||
log.Info("Sleeping until renewal time",
|
||||
log.CertNameAttr(certID),
|
||||
slog.Duration("sleep", ariRenewalTime.Sub(now)),
|
||||
slog.Time("renewalTime", *ariRenewalTime),
|
||||
)
|
||||
|
||||
time.Sleep(ariRenewalTime.Sub(now))
|
||||
}
|
||||
}
|
||||
|
||||
replacesCertID, err := api.MakeARICertID(cert)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error while constructing the ARI CertID for domain %q: %w", certID, err)
|
||||
}
|
||||
|
||||
return ariRenewalTime, replacesCertID, nil
|
||||
}
|
||||
|
||||
// getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint.
|
||||
func getARIRenewalTime(ctx context.Context, willingToSleep time.Duration, cert *x509.Certificate, domain string, client *lego.Client) *time.Time {
|
||||
renewalInfo, err := client.Certificate.GetRenewalInfo(ctx, cert)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrNoARI) {
|
||||
log.Warn("acme: the server does not advertise a renewal info endpoint.",
|
||||
log.CertNameAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn("acme: calling renewal info endpoint",
|
||||
log.CertNameAttr(domain),
|
||||
log.ErrorAttr(err),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
renewalTime := renewalInfo.ShouldRenewAt(now, willingToSleep)
|
||||
if renewalTime == nil {
|
||||
log.Info("acme: renewalInfo endpoint indicates that renewal is not needed.", log.CertNameAttr(domain))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("acme: renewalInfo endpoint indicates that renewal is needed.", log.CertNameAttr(domain))
|
||||
|
||||
if renewalInfo.ExplanationURL != "" {
|
||||
log.Info("acme: renewalInfo endpoint provided an explanation.",
|
||||
log.CertNameAttr(domain),
|
||||
slog.String("explanationURL", renewalInfo.ExplanationURL),
|
||||
)
|
||||
}
|
||||
|
||||
return renewalTime
|
||||
}
|
||||
|
||||
func randomSleep() {
|
||||
// https://github.com/go-acme/lego/issues/1656
|
||||
// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
// https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472
|
||||
const jitter = 8 * time.Minute
|
||||
|
||||
sleepTime := time.Duration(rand.Int64N(int64(jitter)))
|
||||
|
||||
log.Info("renewal: random delay.", slog.Duration("sleep", sleepTime))
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
func sameDomainsCertificate(cert *x509.Certificate, csr *x509.CertificateRequest) bool {
|
||||
return sameDomains(certcrypto.ExtractDomains(cert), certcrypto.ExtractDomainsCSR(csr))
|
||||
}
|
||||
|
||||
func sameDomains(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aClone := slices.Clone(a)
|
||||
sort.Strings(aClone)
|
||||
|
||||
bClone := slices.Clone(b)
|
||||
sort.Strings(bClone)
|
||||
|
||||
return slices.Equal(aClone, bClone)
|
||||
}
|
||||
78
cmd/internal/root/process_run.go
Normal file
78
cmd/internal/root/process_run.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v5/certificate"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/configuration"
|
||||
"github.com/go-acme/lego/v5/cmd/internal/storage"
|
||||
"github.com/go-acme/lego/v5/lego"
|
||||
)
|
||||
|
||||
func runCertificate(ctx context.Context, lazySetup lzSetUp, certConfig *configuration.Certificate, certsStorage *storage.CertificatesStorage) error {
|
||||
client, err := lazySetup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certRes, err := obtainCertificate(ctx, client, certConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = certsStorage.Save(certRes, &storage.SaveOptions{PEM: true})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not save the resource: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func obtainCertificate(ctx context.Context, client *lego.Client, certConfig *configuration.Certificate) (*certificate.Resource, error) {
|
||||
domains := certConfig.Domains
|
||||
|
||||
if len(domains) > 0 {
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
MustStaple: certConfig.MustStaple,
|
||||
NotBefore: certConfig.NotBefore,
|
||||
NotAfter: certConfig.NotAfter,
|
||||
Bundle: !certConfig.NoBundle,
|
||||
PreferredChain: certConfig.PreferredChain,
|
||||
EnableCommonName: certConfig.EnableCommonName,
|
||||
Profile: certConfig.Profile,
|
||||
AlwaysDeactivateAuthorizations: certConfig.AlwaysDeactivateAuthorizations,
|
||||
}
|
||||
|
||||
// NOTE(ldez): I didn't add an option to set a private key as the file.
|
||||
// I didn't find a use case for it when using the file configuration.
|
||||
// Maybe this can be added in the future.
|
||||
|
||||
return client.Certificate.Obtain(ctx, request)
|
||||
}
|
||||
|
||||
// read the CSR
|
||||
csr, err := storage.ReadCSRFile(certConfig.CSR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// obtain a certificate for this CSR
|
||||
request := certificate.ObtainForCSRRequest{
|
||||
CSR: csr,
|
||||
NotBefore: certConfig.NotBefore,
|
||||
NotAfter: certConfig.NotAfter,
|
||||
Bundle: !certConfig.NoBundle,
|
||||
PreferredChain: certConfig.PreferredChain,
|
||||
EnableCommonName: certConfig.EnableCommonName,
|
||||
Profile: certConfig.Profile,
|
||||
AlwaysDeactivateAuthorizations: certConfig.AlwaysDeactivateAuthorizations,
|
||||
}
|
||||
|
||||
// NOTE(ldez): I didn't add an option to set a private key as the file.
|
||||
// I didn't find a use case for it when using the file configuration.
|
||||
// Maybe this can be added in the future.
|
||||
|
||||
return client.Certificate.ObtainForCSR(ctx, request)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -15,12 +16,22 @@ import (
|
|||
|
||||
const rfc3339NanoNatural = "2006-01-02T15:04:05.000000000Z07:00"
|
||||
|
||||
func setUpLogger(cmd *cli.Command) {
|
||||
level := getLogLeveler(cmd.String(flags.FlgLogLevel))
|
||||
func setUpLogger(cmd *cli.Command, logCfg *configuration.Log) {
|
||||
cfg := &configuration.Log{}
|
||||
|
||||
if logCfg == nil {
|
||||
cfg.Level = cmd.String(flags.FlgLogLevel)
|
||||
cfg.Format = cmd.String(flags.FlgLogFormat)
|
||||
} else {
|
||||
cfg.Level = cmp.Or(logCfg.Level, cmd.String(flags.FlgLogLevel))
|
||||
cfg.Format = cmp.Or(logCfg.Format, cmd.String(flags.FlgLogFormat))
|
||||
}
|
||||
|
||||
level := getLogLeveler(cfg.Level)
|
||||
|
||||
var logger *slog.Logger
|
||||
|
||||
switch cmd.String(flags.FlgLogFormat) {
|
||||
switch cfg.Format {
|
||||
case configuration.LogFormatJSON:
|
||||
opts := &slog.HandlerOptions{
|
||||
Level: level,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue