refactor(cli): storage and flags management (#2812)

This commit is contained in:
Ludovic Fernandez 2026-01-28 19:24:50 +01:00 committed by GitHub
commit ab4e321904
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1741 additions and 808 deletions

View file

@ -1,115 +0,0 @@
package cmd
import (
"os"
"path/filepath"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCertificatesStorage_MoveToArchive(t *testing.T) {
domain := "example.com"
storage := CertificatesStorage{
rootPath: t.TempDir(),
archivePath: t.TempDir(),
}
domainFiles := generateTestFiles(t, storage.rootPath, domain)
err := storage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.NoFileExists(t, file)
}
root, err := os.ReadDir(storage.rootPath)
require.NoError(t, err)
require.Empty(t, root)
archive, err := os.ReadDir(storage.archivePath)
require.NoError(t, err)
require.Len(t, archive, len(domainFiles))
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
}
func TestCertificatesStorage_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
domain := "example.com"
storage := CertificatesStorage{
rootPath: t.TempDir(),
archivePath: t.TempDir(),
}
domainFiles := generateTestFiles(t, storage.rootPath, "example.org")
err := storage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.FileExists(t, file)
}
root, err := os.ReadDir(storage.rootPath)
require.NoError(t, err)
assert.Len(t, root, len(domainFiles))
archive, err := os.ReadDir(storage.archivePath)
require.NoError(t, err)
assert.Empty(t, archive)
}
func TestCertificatesStorage_MoveToArchive_ambiguousDomain(t *testing.T) {
domain := "example.com"
storage := CertificatesStorage{
rootPath: t.TempDir(),
archivePath: t.TempDir(),
}
domainFiles := generateTestFiles(t, storage.rootPath, domain)
otherDomainFiles := generateTestFiles(t, storage.rootPath, domain+".example.org")
err := storage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.NoFileExists(t, file)
}
for _, file := range otherDomainFiles {
assert.FileExists(t, file)
}
root, err := os.ReadDir(storage.rootPath)
require.NoError(t, err)
require.Len(t, root, len(otherDomainFiles))
archive, err := os.ReadDir(storage.archivePath)
require.NoError(t, err)
require.Len(t, archive, len(domainFiles))
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
}
func generateTestFiles(t *testing.T, dir, domain string) []string {
t.Helper()
var filenames []string
for _, ext := range []string{issuerExt, certExt, keyExt, pemExt, pfxExt, resourceExt} {
filename := filepath.Join(dir, domain+ext)
err := os.WriteFile(filename, []byte("test"), 0o666)
require.NoError(t, err)
filenames = append(filenames, filename)
}
return filenames
}

View file

@ -1,31 +0,0 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
)
func Before(ctx context.Context, cmd *cli.Command) (context.Context, error) {
if cmd.String(flgPath) == "" {
log.Fatal(fmt.Sprintf("Could not determine the current working directory. Please pass --%s.", flgPath))
}
err := createNonExistingFolder(cmd.String(flgPath))
if err != nil {
log.Fatal("Could not check/create the path.",
slog.String("flag", flgPath),
slog.String("filepath", cmd.String(flgPath)),
log.ErrorAttr(err),
)
}
if cmd.String(flgServer) == "" {
log.Fatal(fmt.Sprintf("Could not determine the current working server. Please pass --%s.", flgServer))
}
return ctx, nil
}

View file

@ -17,12 +17,16 @@ func createDNSHelp() *cli.Command {
Name: "dnshelp",
Usage: "Shows additional help for the '--dns' global option",
Action: dnsHelp,
Flags: []cli.Flag{
&cli.StringFlag{
Name: flgCode,
Aliases: []string{"c"},
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
Flags: createDNSHelpFlags(),
}
}
func createDNSHelpFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: flgCode,
Aliases: []string{"c"},
Usage: fmt.Sprintf("DNS code: %s", allDNSCodes()),
},
}
}

View file

@ -10,6 +10,7 @@ import (
"strings"
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/urfave/cli/v3"
)
@ -23,24 +24,23 @@ func createList() *cli.Command {
Name: "list",
Usage: "Display certificates and accounts information.",
Action: list,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgAccounts,
Aliases: []string{"a"},
Usage: "Display accounts.",
},
&cli.BoolFlag{
Name: flgNames,
Aliases: []string{"n"},
Usage: "Display certificate common names only.",
},
// fake email, needed by NewAccountsStorage
&cli.StringFlag{
Name: flgEmail,
Value: "",
Hidden: true,
},
Flags: createListFlags(),
}
}
func createListFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: flgAccounts,
Aliases: []string{"a"},
Usage: "Display accounts.",
},
&cli.BoolFlag{
Name: flgNames,
Aliases: []string{"n"},
Usage: "Display certificate names only.",
},
CreatePathFlag(false),
}
}
@ -55,7 +55,7 @@ func list(ctx context.Context, cmd *cli.Command) error {
}
func listCertificates(_ context.Context, cmd *cli.Command) error {
certsStorage := NewCertificatesStorage(cmd)
certsStorage := storage.NewCertificatesReader(cmd.String(flgPath))
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
if err != nil {
@ -77,7 +77,7 @@ func listCertificates(_ context.Context, cmd *cli.Command) error {
}
for _, filename := range matches {
if strings.HasSuffix(filename, issuerExt) {
if strings.HasSuffix(filename, storage.ExtIssuer) {
continue
}
@ -111,7 +111,7 @@ func listCertificates(_ context.Context, cmd *cli.Command) error {
}
func listAccount(_ context.Context, cmd *cli.Command) error {
accountsStorage := NewAccountsStorage(cmd)
accountsStorage := newAccountsStorage(cmd)
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
@ -131,7 +131,7 @@ func listAccount(_ context.Context, cmd *cli.Command) error {
return err
}
var account Account
var account storage.Account
err = json.Unmarshal(data, &account)
if err != nil {

View file

@ -17,6 +17,8 @@ import (
"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/hook"
"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"
@ -60,101 +62,109 @@ func createRenew() *cli.Command {
return ctx, nil
},
Flags: []cli.Flag{
&cli.IntFlag{
Name: flgRenewDays,
Value: 30,
Usage: "The number of days left on a certificate to renew it.",
},
// TODO(ldez): in v5, remove this flag, use this behavior as default.
&cli.BoolFlag{
Name: flgRenewDynamic,
Value: false,
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
},
&cli.BoolFlag{
Name: flgARIDisable,
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
},
&cli.DurationFlag{
Name: flgARIWaitToRenewDuration,
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
},
&cli.BoolFlag{
Name: flgReuseKey,
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
&cli.BoolFlag{
Name: flgNoBundle,
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.TimestampFlag{
Name: flgNotAfter,
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.StringFlag{
Name: flgPreferredChain,
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRenewHook,
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
&cli.DurationFlag{
Name: flgRenewHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
},
&cli.BoolFlag{
Name: flgNoRandomSleep,
Usage: "Do not add a random sleep before the renewal." +
" We do not recommend using this flag if you are doing your renewals in an automated way.",
},
&cli.BoolFlag{
Name: flgForceCertDomains,
Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.",
},
},
Flags: createRenewFlags(),
}
}
func createRenewFlags() []cli.Flag {
flags := CreateFlags()
flags = append(flags,
&cli.IntFlag{
Name: flgRenewDays,
Value: 30,
Usage: "The number of days left on a certificate to renew it.",
},
// TODO(ldez): in v5, remove this flag, use this behavior as default.
&cli.BoolFlag{
Name: flgRenewDynamic,
Value: false,
Usage: "Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.",
},
&cli.BoolFlag{
Name: flgARIDisable,
Usage: "Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.",
},
&cli.DurationFlag{
Name: flgARIWaitToRenewDuration,
Usage: "The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint.",
},
&cli.BoolFlag{
Name: flgReuseKey,
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
},
&cli.BoolFlag{
Name: flgNoBundle,
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.TimestampFlag{
Name: flgNotAfter,
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.StringFlag{
Name: flgPreferredChain,
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRenewHook,
Usage: "Define a hook. The hook is executed only when the certificates are effectively renewed.",
},
&cli.DurationFlag{
Name: flgRenewHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
},
&cli.BoolFlag{
Name: flgNoRandomSleep,
Usage: "Do not add a random sleep before the renewal." +
" We do not recommend using this flag if you are doing your renewals in an automated way.",
},
&cli.BoolFlag{
Name: flgForceCertDomains,
Usage: "Check and ensure that the cert's domain list matches those passed in the domains argument.",
},
)
return flags
}
func renew(ctx context.Context, cmd *cli.Command) error {
account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd))
account, keyType := setupAccount(ctx, cmd, newAccountsStorage(cmd))
if account.Registration == nil {
log.Fatal("The account is not registered. Use 'run' to register a new account.", slog.String("email", account.Email))
}
certsStorage := NewCertificatesStorage(cmd)
certsStorage := newCertificatesStorage(cmd)
bundle := !cmd.Bool(flgNoBundle)
meta := map[string]string{
hookEnvAccountEmail: account.Email,
hook.EnvAccountEmail: account.Email,
}
// CSR
@ -166,14 +176,14 @@ func renew(ctx context.Context, cmd *cli.Command) error {
return renewForDomains(ctx, cmd, account, keyType, certsStorage, bundle, meta)
}
func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
func renewForDomains(ctx context.Context, cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
domains := cmd.StringSlice(flgDomains)
domain := domains[0]
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, certExt)
certificates, err := certsStorage.ReadCertificate(domain, storage.ExtCert)
if err != nil {
log.Fatal("Error while loading the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
}
@ -234,7 +244,7 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, ke
var privateKey crypto.PrivateKey
if cmd.Bool(flgReuseKey) {
keyBytes, errR := certsStorage.ReadFile(domain, keyExt)
keyBytes, errR := certsStorage.ReadFile(domain, storage.ExtKey)
if errR != nil {
log.Fatal("Error while loading the private key.",
log.DomainAttr(domain),
@ -293,10 +303,10 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, ke
addPathToMetadata(meta, domain, certRes, certsStorage)
return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
}
func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
func renewForCSR(ctx context.Context, cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error {
csr, err := readCSRFile(cmd.String(flgCSR))
if err != nil {
log.Fatal("Could not read CSR file.",
@ -314,7 +324,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyTyp
// load the cert resource from files.
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
certificates, err := certsStorage.ReadCertificate(domain, certExt)
certificates, err := certsStorage.ReadCertificate(domain, storage.ExtCert)
if err != nil {
log.Fatal("Error while loading the certificate.",
log.DomainAttr(domain),
@ -392,7 +402,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyTyp
addPathToMetadata(meta, domain, certRes, certsStorage)
return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
return hook.Launch(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta)
}
func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool {

View file

@ -5,6 +5,7 @@ import (
"log/slog"
"github.com/go-acme/lego/v5/acme"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
)
@ -20,28 +21,36 @@ func createRevoke() *cli.Command {
Name: "revoke",
Usage: "Revoke a certificate",
Action: revoke,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgKeep,
Aliases: []string{"k"},
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
&cli.UintFlag{
Name: flgReason,
Usage: "Identifies the reason for the certificate revocation." +
" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." +
" Valid values are:" +
" 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged)," +
" 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL)," +
" 9 (privilegeWithdrawn), or 10 (aACompromise).",
Value: acme.CRLReasonUnspecified,
},
},
Flags: createRevokeFlags(),
}
}
func createRevokeFlags() []cli.Flag {
flags := CreateFlags()
flags = append(flags,
&cli.BoolFlag{
Name: flgKeep,
Aliases: []string{"k"},
Usage: "Keep the certificates after the revocation instead of archiving them.",
},
&cli.UintFlag{
Name: flgReason,
Usage: "Identifies the reason for the certificate revocation." +
" See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1." +
" Valid values are:" +
" 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged)," +
" 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL)," +
" 9 (privilegeWithdrawn), or 10 (aACompromise).",
Value: acme.CRLReasonUnspecified,
},
)
return flags
}
func revoke(ctx context.Context, cmd *cli.Command) error {
account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd))
account, keyType := setupAccount(ctx, cmd, newAccountsStorage(cmd))
if account.Registration == nil {
log.Fatal("Account is not registered. Use 'run' to register a new account.", slog.String("email", account.Email))
@ -49,13 +58,13 @@ func revoke(ctx context.Context, cmd *cli.Command) error {
client := newClient(cmd, account, keyType)
certsStorage := NewCertificatesStorage(cmd)
certsStorage := newCertificatesStorage(cmd)
certsStorage.CreateRootFolder()
for _, domain := range cmd.StringSlice(flgDomains) {
log.Info("Trying to revoke the certificate.", log.DomainAttr(domain))
certBytes, err := certsStorage.ReadFile(domain, certExt)
certBytes, err := certsStorage.ReadFile(domain, storage.ExtCert)
if err != nil {
log.Fatal("Error while revoking the certificate.", log.DomainAttr(domain), log.ErrorAttr(err))
}

View file

@ -10,6 +10,8 @@ import (
"time"
"github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/cmd/internal/hook"
"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/go-acme/lego/v5/registration"
@ -50,58 +52,66 @@ func createRun() *cli.Command {
return ctx, nil
},
Action: run,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: flgNoBundle,
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.TimestampFlag{
Name: flgNotAfter,
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.StringFlag{
Name: flgPrivateKey,
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
},
&cli.StringFlag{
Name: flgPreferredChain,
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRunHook,
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
&cli.DurationFlag{
Name: flgRunHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
Flags: createRunFlags(),
}
}
func createRunFlags() []cli.Flag {
flags := CreateFlags()
flags = append(flags,
&cli.BoolFlag{
Name: flgNoBundle,
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
},
&cli.BoolFlag{
Name: flgMustStaple,
Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate." +
" Only works if the CSR is generated by lego.",
},
&cli.TimestampFlag{
Name: flgNotBefore,
Usage: "Set the notBefore field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
}
&cli.TimestampFlag{
Name: flgNotAfter,
Usage: "Set the notAfter field in the certificate (RFC3339 format)",
Config: cli.TimestampConfig{
Layouts: []string{time.RFC3339},
},
},
&cli.StringFlag{
Name: flgPrivateKey,
Usage: "Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.",
},
&cli.StringFlag{
Name: flgPreferredChain,
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
" If no match, the default offered chain will be used.",
},
&cli.StringFlag{
Name: flgProfile,
Usage: "If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.",
},
&cli.StringFlag{
Name: flgAlwaysDeactivateAuthorizations,
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
},
&cli.StringFlag{
Name: flgRunHook,
Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
},
&cli.DurationFlag{
Name: flgRunHookTimeout,
Usage: "Define the timeout for the hook execution.",
Value: 2 * time.Minute,
},
)
return flags
}
const rootPathWarningMessage = `!!!! HEADS UP !!!!
@ -116,7 +126,7 @@ server. Making regular backups of this folder is ideal.
`
func run(ctx context.Context, cmd *cli.Command) error {
accountsStorage := NewAccountsStorage(cmd)
accountsStorage := newAccountsStorage(cmd)
account, keyType := setupAccount(ctx, cmd, accountsStorage)
@ -136,7 +146,7 @@ func run(ctx context.Context, cmd *cli.Command) error {
fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
}
certsStorage := NewCertificatesStorage(cmd)
certsStorage := newCertificatesStorage(cmd)
certsStorage.CreateRootFolder()
cert, err := obtainCertificate(ctx, cmd, client)
@ -149,12 +159,12 @@ func run(ctx context.Context, cmd *cli.Command) error {
certsStorage.SaveResource(cert)
meta := map[string]string{
hookEnvAccountEmail: account.Email,
hook.EnvAccountEmail: account.Email,
}
addPathToMetadata(meta, cert.Domain, cert, certsStorage)
return launchHook(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta)
return hook.Launch(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta)
}
func handleTOS(cmd *cli.Command, client *lego.Client) bool {
@ -231,7 +241,7 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien
if cmd.IsSet(flgPrivateKey) {
var err error
request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey))
request.PrivateKey, err = storage.LoadPrivateKey(cmd.String(flgPrivateKey))
if err != nil {
return nil, fmt.Errorf("load private key: %w", err)
}
@ -260,7 +270,7 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien
if cmd.IsSet(flgPrivateKey) {
var err error
request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey))
request.PrivateKey, err = storage.LoadPrivateKey(cmd.String(flgPrivateKey))
if err != nil {
return nil, fmt.Errorf("load private key: %w", err)
}

View file

@ -6,53 +6,75 @@ import (
"path/filepath"
"github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/go-acme/lego/v5/lego"
"github.com/urfave/cli/v3"
"software.sslmate.com/src/go-pkcs12"
)
// Flag names.
// Flag names related to the account and domains.
const (
flgDomains = "domains"
flgAcceptTOS = "accept-tos"
flgEmail = "email"
flgCSR = "csr"
flgEAB = "eab"
flgKID = "kid"
flgHMAC = "hmac"
)
// Flag names related to the output.
const (
flgFilename = "filename"
flgPath = "path"
flgPEM = "pem"
flgPFX = "pfx"
flgPFXPass = "pfx.pass"
flgPFXFormat = "pfx.format"
)
// Flag names related to the ACME client.
const (
flgServer = "server"
flgDisableCommonName = "disable-cn"
flgKeyType = "key-type"
flgHTTPTimeout = "http-timeout"
flgTLSSkipVerify = "tls-skip-verify"
flgCertTimeout = "cert.timeout"
flgOverallRequestLimit = "overall-request-limit"
flgUserAgent = "user-agent"
)
// Flag names related to HTTP-01 challenge.
const (
flgHTTP = "http"
flgHTTPPort = "http.port"
flgHTTPDelay = "http.delay"
flgHTTPProxyHeader = "http.proxy-header"
flgHTTPWebroot = "http.webroot"
flgHTTPMemcachedHost = "http.memcached-host"
flgHTTPS3Bucket = "http.s3-bucket"
)
// Flag names related to TLS-ALPN-01 challenge.
const (
flgTLS = "tls"
flgTLSPort = "tls.port"
flgTLSDelay = "tls.delay"
)
// Flag names related to DNS-01 challenge.
const (
flgDomains = "domains"
flgServer = "server"
flgAcceptTOS = "accept-tos"
flgEmail = "email"
flgDisableCommonName = "disable-cn"
flgCSR = "csr"
flgEAB = "eab"
flgKID = "kid"
flgHMAC = "hmac"
flgKeyType = "key-type"
flgFilename = "filename"
flgPath = "path"
flgHTTP = "http"
flgHTTPPort = "http.port"
flgHTTPDelay = "http.delay"
flgHTTPProxyHeader = "http.proxy-header"
flgHTTPWebroot = "http.webroot"
flgHTTPMemcachedHost = "http.memcached-host"
flgHTTPS3Bucket = "http.s3-bucket"
flgTLS = "tls"
flgTLSPort = "tls.port"
flgTLSDelay = "tls.delay"
flgDNS = "dns"
flgDNSDisableCP = "dns.disable-cp"
flgDNSPropagationWait = "dns.propagation-wait"
flgDNSPropagationDisableANS = "dns.propagation-disable-ans"
flgDNSPropagationRNS = "dns.propagation-rns"
flgDNSResolvers = "dns.resolvers"
flgHTTPTimeout = "http-timeout"
flgTLSSkipVerify = "tls-skip-verify"
flgDNSTimeout = "dns-timeout"
flgPEM = "pem"
flgPFX = "pfx"
flgPFXPass = "pfx.pass"
flgPFXFormat = "pfx.format"
flgCertTimeout = "cert.timeout"
flgOverallRequestLimit = "overall-request-limit"
flgUserAgent = "user-agent"
)
// Environment variable names.
const (
envEAB = "LEGO_EAB"
envEABHMAC = "LEGO_EAB_HMAC"
@ -65,78 +87,53 @@ const (
envServer = "LEGO_SERVER"
)
func CreateFlags(defaultPath string) []cli.Flag {
if defaultPath == "" {
cwd, err := os.Getwd()
if err == nil {
defaultPath = filepath.Join(cwd, ".lego")
}
}
func CreateACMEClientFlags() []cli.Flag {
return []cli.Flag{
&cli.StringSliceFlag{
Name: flgDomains,
Aliases: []string{"d"},
Usage: "Add a domain to the process. Can be specified multiple times.",
},
&cli.StringFlag{
Name: flgServer,
Aliases: []string{"s"},
Sources: cli.EnvVars(envServer),
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
},
&cli.BoolFlag{
Name: flgAcceptTOS,
Aliases: []string{"a"},
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
&cli.StringFlag{
Name: flgEmail,
Aliases: []string{"m"},
Sources: cli.EnvVars(envEmail),
Usage: "Email used for registration and recovery contact.",
Name: flgServer,
Aliases: []string{"s"},
Sources: cli.EnvVars(envServer),
Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.",
Value: lego.LEDirectoryProduction,
Required: true,
},
&cli.BoolFlag{
Name: flgDisableCommonName,
Usage: "Disable the use of the common name in the CSR.",
},
&cli.StringFlag{
Name: flgCSR,
Aliases: []string{"c"},
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
&cli.BoolFlag{
Name: flgEAB,
Sources: cli.EnvVars(envEAB),
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
&cli.StringFlag{
Name: flgKID,
Sources: cli.EnvVars(envEABKID),
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgHMAC,
Sources: cli.EnvVars(envEABHMAC),
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgKeyType,
Aliases: []string{"k"},
Value: "ec256",
Usage: "Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384.",
},
&cli.StringFlag{
Name: flgFilename,
Usage: "(deprecated) Filename of the generated certificate.",
&cli.IntFlag{
Name: flgHTTPTimeout,
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
&cli.BoolFlag{
Name: flgTLSSkipVerify,
Usage: "Skip the TLS verification of the ACME server.",
},
&cli.IntFlag{
Name: flgCertTimeout,
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
},
&cli.IntFlag{
Name: flgOverallRequestLimit,
Usage: "ACME overall requests limit.",
Value: certificate.DefaultOverallRequestLimit,
},
&cli.StringFlag{
Name: flgPath,
Sources: cli.EnvVars(envPath),
Usage: "Directory to use for storing the data.",
Value: defaultPath,
Name: flgUserAgent,
Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli",
},
}
}
func CreateHTTPChallengeFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: flgHTTP,
Usage: "Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.",
@ -169,6 +166,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
Name: flgHTTPS3Bucket,
Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.",
},
}
}
func CreateTLSChallengeFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: flgTLS,
Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.",
@ -183,6 +185,11 @@ func CreateFlags(defaultPath string) []cli.Flag {
Usage: "Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge.",
Value: 0,
},
}
}
func CreateDNSChallengeFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: flgDNS,
Usage: "Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.",
@ -210,19 +217,21 @@ func CreateFlags(defaultPath string) []cli.Flag {
" Supported: host:port." +
" The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.",
},
&cli.IntFlag{
Name: flgHTTPTimeout,
Usage: "Set the HTTP timeout value to a specific value in seconds.",
},
&cli.BoolFlag{
Name: flgTLSSkipVerify,
Usage: "Skip the TLS verification of the ACME server.",
},
&cli.IntFlag{
Name: flgDNSTimeout,
Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries.",
Value: 10,
},
}
}
func CreateOutputFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: flgFilename,
Usage: "(deprecated) Filename of the generated certificate.",
},
CreatePathFlag(true),
&cli.BoolFlag{
Name: flgPEM,
Usage: "Generate an additional .pem (base64) file by concatenating the .key and .crt files together.",
@ -244,19 +253,102 @@ func CreateFlags(defaultPath string) []cli.Flag {
Value: "RC2",
Sources: cli.EnvVars(envPFXFormat),
},
&cli.IntFlag{
Name: flgCertTimeout,
Usage: "Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates.",
Value: 30,
}
}
func CreatePathFlag(forceCreation bool) cli.Flag {
return &cli.StringFlag{
Name: flgPath,
Sources: cli.NewValueSourceChain(cli.EnvVar(envPath), &defaultPathValueSource{}),
Usage: "Directory to use for storing the data.",
Validator: func(s string) error {
if !forceCreation {
return nil
}
err := storage.CreateNonExistingFolder(s)
if err != nil {
return fmt.Errorf("could not check/create the path %q: %w", s, err)
}
return nil
},
&cli.IntFlag{
Name: flgOverallRequestLimit,
Usage: "ACME overall requests limit.",
Value: certificate.DefaultOverallRequestLimit,
Required: true,
}
}
func CreateAccountFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: flgAcceptTOS,
Aliases: []string{"a"},
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
},
&cli.StringFlag{
Name: flgUserAgent,
Usage: "Add to the user-agent sent to the CA to identify an application embedding lego-cli",
Name: flgEmail,
Aliases: []string{"m"},
Sources: cli.EnvVars(envEmail),
Usage: "Email used for registration and recovery contact.",
},
&cli.StringFlag{
Name: flgCSR,
Aliases: []string{"c"},
Usage: "Certificate signing request filename, if an external CSR is to be used.",
},
&cli.BoolFlag{
Name: flgEAB,
Sources: cli.EnvVars(envEAB),
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
&cli.StringFlag{
Name: flgKID,
Sources: cli.EnvVars(envEABKID),
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
&cli.StringFlag{
Name: flgHMAC,
Sources: cli.EnvVars(envEABHMAC),
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
}
}
func CreateFlags() []cli.Flag {
flags := []cli.Flag{
&cli.StringSliceFlag{
Name: flgDomains,
Aliases: []string{"d"},
Usage: "Add a domain to the process. Can be specified multiple times.",
},
}
flags = append(flags, CreateAccountFlags()...)
flags = append(flags, CreateACMEClientFlags()...)
flags = append(flags, CreateOutputFlags()...)
flags = append(flags, CreateHTTPChallengeFlags()...)
flags = append(flags, CreateTLSChallengeFlags()...)
flags = append(flags, CreateDNSChallengeFlags()...)
return flags
}
// defaultPathValueSource gets the default path based on the current working directory.
// The field value is only here because clihelp/generator.
type defaultPathValueSource struct{}
func (p *defaultPathValueSource) String() string {
return "default path"
}
func (p *defaultPathValueSource) GoString() string {
return "&defaultPathValueSource{}"
}
func (p *defaultPathValueSource) Lookup() (string, bool) {
cwd, err := os.Getwd()
if err != nil {
return "", false
}
return filepath.Join(cwd, ".lego"), true
}

View file

@ -1,104 +1,25 @@
package cmd
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/cmd/internal/hook"
"github.com/go-acme/lego/v5/cmd/internal/storage"
)
const (
hookEnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
hookEnvCertDomain = "LEGO_CERT_DOMAIN"
hookEnvCertPath = "LEGO_CERT_PATH"
hookEnvCertKeyPath = "LEGO_CERT_KEY_PATH"
hookEnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
hookEnvCertPEMPath = "LEGO_CERT_PEM_PATH"
hookEnvCertPFXPath = "LEGO_CERT_PFX_PATH"
)
func launchHook(ctx context.Context, hook string, timeout time.Duration, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
parts := strings.Fields(hook)
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create pipe: %w", err)
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
}
go func() {
<-ctxCmd.Done()
if ctxCmd.Err() != nil {
_ = cmd.Process.Kill()
_ = stdout.Close()
}
}()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
err = cmd.Wait()
if err != nil {
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return fmt.Errorf("wait command: %w", err)
}
return nil
}
func metaToEnv(meta map[string]string) []string {
var envs []string
for k, v := range meta {
envs = append(envs, k+"="+v)
}
return envs
}
func addPathToMetadata(meta map[string]string, domain string, certRes *certificate.Resource, certsStorage *CertificatesStorage) {
meta[hookEnvCertDomain] = domain
meta[hookEnvCertPath] = certsStorage.GetFileName(domain, certExt)
meta[hookEnvCertKeyPath] = certsStorage.GetFileName(domain, keyExt)
meta[hook.EnvCertDomain] = domain
meta[hook.EnvCertPath] = certsStorage.GetFileName(domain, storage.ExtCert)
meta[hook.EnvCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtKey)
if certRes.IssuerCertificate != nil {
meta[hookEnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, issuerExt)
meta[hook.EnvIssuerCertKeyPath] = certsStorage.GetFileName(domain, storage.ExtIssuer)
}
if certsStorage.pem {
meta[hookEnvCertPEMPath] = certsStorage.GetFileName(domain, pemExt)
if certsStorage.IsPEM() {
meta[hook.EnvCertPEMPath] = certsStorage.GetFileName(domain, storage.ExtPEM)
}
if certsStorage.pfx {
meta[hookEnvCertPFXPath] = certsStorage.GetFileName(domain, pfxExt)
if certsStorage.IsPFX() {
meta[hook.EnvCertPFXPath] = certsStorage.GetFileName(domain, storage.ExtPFX)
}
}

84
cmd/internal/hook/hook.go Normal file
View file

@ -0,0 +1,84 @@
package hook
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
const (
EnvAccountEmail = "LEGO_ACCOUNT_EMAIL"
EnvCertDomain = "LEGO_CERT_DOMAIN"
EnvCertPath = "LEGO_CERT_PATH"
EnvCertKeyPath = "LEGO_CERT_KEY_PATH"
EnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH"
EnvCertPEMPath = "LEGO_CERT_PEM_PATH"
EnvCertPFXPath = "LEGO_CERT_PFX_PATH"
)
func Launch(ctx context.Context, hook string, timeout time.Duration, meta map[string]string) error {
if hook == "" {
return nil
}
ctxCmd, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
parts := strings.Fields(hook)
cmd := exec.CommandContext(ctxCmd, parts[0], parts[1:]...)
cmd.Env = append(os.Environ(), metaToEnv(meta)...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("create pipe: %w", err)
}
cmd.Stderr = cmd.Stdout
err = cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
}
go func() {
<-ctxCmd.Done()
if ctxCmd.Err() != nil {
_ = cmd.Process.Kill()
_ = stdout.Close()
}
}()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
err = cmd.Wait()
if err != nil {
if errors.Is(ctxCmd.Err(), context.DeadlineExceeded) {
return errors.New("hook timed out")
}
return fmt.Errorf("wait command: %w", err)
}
return nil
}
func metaToEnv(meta map[string]string) []string {
var envs []string
for k, v := range meta {
envs = append(envs, k+"="+v)
}
return envs
}

View file

@ -1,19 +1,20 @@
package cmd
package hook
import (
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_launchHook(t *testing.T) {
err := launchHook(t.Context(), "echo foo", 1*time.Second, map[string]string{})
func Test_Launch(t *testing.T) {
err := Launch(t.Context(), "echo foo", 1*time.Second, map[string]string{})
require.NoError(t, err)
}
func Test_launchHook_errors(t *testing.T) {
func Test_Launch_errors(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
@ -54,8 +55,18 @@ func Test_launchHook_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := launchHook(t.Context(), test.hook, test.timeout, map[string]string{})
err := Launch(t.Context(), test.hook, test.timeout, map[string]string{})
require.EqualError(t, err, test.expected)
})
}
}
func Test_metaToEnv(t *testing.T) {
env := metaToEnv(map[string]string{
"foo": "bar",
})
expected := []string{"foo=bar"}
assert.Equal(t, expected, env)
}

View file

@ -1,4 +1,4 @@
package cmd
package storage
import (
"crypto"
@ -13,6 +13,10 @@ type Account struct {
key crypto.PrivateKey
}
func NewAccount(email string, key crypto.PrivateKey) *Account {
return &Account{Email: email, key: key}
}
/** Implementation of the registration.User interface **/
// GetEmail returns the email address for the account.

View file

@ -1,10 +1,11 @@
package cmd
package storage
import (
"context"
"crypto"
"encoding/json"
"encoding/pem"
"fmt"
"log/slog"
"net/url"
"os"
@ -15,7 +16,6 @@ import (
"github.com/go-acme/lego/v5/lego"
"github.com/go-acme/lego/v5/log"
"github.com/go-acme/lego/v5/registration"
"github.com/urfave/cli/v3"
)
const userIDPlaceholder = "noemail@example.com"
@ -26,6 +26,14 @@ const (
accountFileName = "account.json"
)
type AccountsStorageConfig struct {
Email string
BasePath string
Server string
UserAgent string
}
// AccountsStorage A storage for account data.
//
// rootPath:
@ -63,32 +71,28 @@ type AccountsStorage struct {
userID string
email string
rootPath string
rootUserPath string
keysPath string
accountFilePath string
cmd *cli.Command
server *url.URL
userAgent string
}
// NewAccountsStorage Creates a new AccountsStorage.
func NewAccountsStorage(cmd *cli.Command) *AccountsStorage {
// TODO: move to account struct?
email := cmd.String(flgEmail)
func NewAccountsStorage(config AccountsStorageConfig) (*AccountsStorage, error) {
email := config.Email
userID := email
if userID == "" {
userID = userIDPlaceholder
}
serverURL, err := url.Parse(cmd.String(flgServer))
serverURL, err := url.Parse(config.Server)
if err != nil {
log.Fatal("URL parsing",
slog.String("flag", flgServer),
slog.String("serverURL", cmd.String(flgServer)),
log.ErrorAttr(err),
)
return nil, fmt.Errorf("invalid server URL %q: %w", config.Server, err)
}
rootPath := filepath.Join(cmd.String(flgPath), baseAccountsRootFolderName)
rootPath := filepath.Join(config.BasePath, baseAccountsRootFolderName)
serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host)
accountsPath := filepath.Join(rootPath, serverPath)
rootUserPath := filepath.Join(accountsPath, userID)
@ -97,20 +101,19 @@ func NewAccountsStorage(cmd *cli.Command) *AccountsStorage {
userID: userID,
email: email,
rootPath: rootPath,
rootUserPath: rootUserPath,
keysPath: filepath.Join(rootUserPath, baseKeysFolderName),
accountFilePath: filepath.Join(rootUserPath, accountFileName),
cmd: cmd,
}
server: serverURL,
userAgent: config.UserAgent,
}, nil
}
func (s *AccountsStorage) ExistsAccountFilePath() bool {
accountFile := filepath.Join(s.rootUserPath, accountFileName)
if _, err := os.Stat(accountFile); os.IsNotExist(err) {
if _, err := os.Stat(s.accountFilePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatal("Could not read the account file.",
slog.String("filepath", accountFile),
slog.String("filepath", s.accountFilePath),
log.ErrorAttr(err),
)
}
@ -122,10 +125,6 @@ func (s *AccountsStorage) GetRootPath() string {
return s.rootPath
}
func (s *AccountsStorage) GetRootUserPath() string {
return s.rootUserPath
}
func (s *AccountsStorage) GetUserID() string {
return s.userID
}
@ -165,7 +164,7 @@ func (s *AccountsStorage) LoadAccount(ctx context.Context, privateKey crypto.Pri
account.key = privateKey
if account.Registration == nil || account.Registration.Body.Status == "" {
reg, err := tryRecoverRegistration(ctx, s.cmd, privateKey)
reg, err := s.tryRecoverRegistration(ctx, privateKey)
if err != nil {
log.Fatal("Could not load the account file. Registration is nil.",
slog.String("userID", s.GetUserID()),
@ -210,7 +209,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva
return privateKey
}
privateKey, err := loadPrivateKey(accKeyPath)
privateKey, err := LoadPrivateKey(accKeyPath)
if err != nil {
log.Fatal("Could not load an RSA private key from the file.",
slog.String("filepath", accKeyPath),
@ -222,7 +221,7 @@ func (s *AccountsStorage) GetPrivateKey(keyType certcrypto.KeyType) crypto.Priva
}
func (s *AccountsStorage) createKeysFolder() {
if err := createNonExistingFolder(s.keysPath); err != nil {
if err := CreateNonExistingFolder(s.keysPath); err != nil {
log.Fatal("Could not check/create the directory for the account.",
slog.String("userID", s.GetUserID()),
log.ErrorAttr(err),
@ -230,6 +229,39 @@ func (s *AccountsStorage) createKeysFolder() {
}
}
func (s *AccountsStorage) tryRecoverRegistration(ctx context.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = s.server.String()
config.UserAgent = s.userAgent
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
reg, err := client.Registration.ResolveAccountByKey(ctx)
if err != nil {
return nil, err
}
return reg, nil
}
func LoadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
if err != nil {
return nil, err
}
return privateKey, nil
}
func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.PrivateKey, error) {
privateKey, err := certcrypto.GeneratePrivateKey(keyType)
if err != nil {
@ -251,36 +283,3 @@ func generatePrivateKey(file string, keyType certcrypto.KeyType) (crypto.Private
return privateKey, nil
}
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
keyBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
privateKey, err := certcrypto.ParsePEMPrivateKey(keyBytes)
if err != nil {
return nil, err
}
return privateKey, nil
}
func tryRecoverRegistration(ctx context.Context, cmd *cli.Command, privateKey crypto.PrivateKey) (*registration.Resource, error) {
// couldn't load account but got a key. Try to look the account up.
config := lego.NewConfig(&Account{key: privateKey})
config.CADirURL = cmd.String(flgServer)
config.UserAgent = getUserAgent(cmd)
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
reg, err := client.Registration.ResolveAccountByKey(ctx)
if err != nil {
return nil, err
}
return reg, nil
}

View file

@ -0,0 +1,223 @@
package storage
import (
"crypto"
"crypto/rsa"
"os"
"path/filepath"
"strings"
"testing"
"github.com/go-acme/lego/v5/acme"
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/registration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAccountsStorage_GetUserID(t *testing.T) {
testCases := []struct {
desc string
email string
expected string
}{
{
desc: "with email",
email: "test@example.com",
expected: "test@example.com",
},
{
desc: "without email",
expected: "noemail@example.com",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
storage, err := NewAccountsStorage(AccountsStorageConfig{
Email: test.email,
BasePath: t.TempDir(),
})
require.NoError(t, err)
assert.Equal(t, test.email, storage.GetEmail())
assert.Equal(t, test.expected, storage.GetUserID())
})
}
}
func TestAccountsStorage_ExistsAccountFilePath(t *testing.T) {
testCases := []struct {
desc string
setup func(t *testing.T, storage *AccountsStorage)
assert assert.BoolAssertionFunc
}{
{
desc: "an account file exists",
setup: func(t *testing.T, storage *AccountsStorage) {
t.Helper()
err := os.MkdirAll(filepath.Dir(storage.accountFilePath), 0o755)
require.NoError(t, err)
err = os.WriteFile(storage.accountFilePath, []byte("test"), 0o644)
require.NoError(t, err)
},
assert: assert.True,
},
{
desc: "no account file",
assert: assert.False,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
storage, err := NewAccountsStorage(AccountsStorageConfig{
BasePath: t.TempDir(),
})
require.NoError(t, err)
if test.setup != nil {
test.setup(t, storage)
}
test.assert(t, storage.ExistsAccountFilePath())
})
}
}
func TestAccountsStorage_GetRootPath(t *testing.T) {
basePath := t.TempDir()
storage, err := NewAccountsStorage(AccountsStorageConfig{
BasePath: basePath,
})
require.NoError(t, err)
assert.Truef(t, strings.HasPrefix(storage.GetRootPath(), basePath),
"%s is not prefixed with %s", storage.GetRootPath(), basePath)
rootPath, err := filepath.Rel(basePath, storage.GetRootPath())
require.NoError(t, err)
assert.Equal(t, baseAccountsRootFolderName, rootPath)
}
func TestAccountsStorage_Save(t *testing.T) {
basePath := t.TempDir()
storage, err := NewAccountsStorage(AccountsStorageConfig{
Email: "test@example.com",
BasePath: basePath,
})
require.NoError(t, err)
account := &Account{
Email: "account@example.com",
Registration: &registration.Resource{
Body: acme.Account{
Status: "valid",
Contact: []string{"contact@example.com"},
TermsOfServiceAgreed: true,
Orders: "https://ame.example.com/orders/123456",
OnlyReturnExisting: true,
ExternalAccountBinding: []byte(`"EAB"`),
},
URI: "https://ame.example.com",
},
key: crypto.PrivateKey(""),
}
err = os.MkdirAll(filepath.Dir(storage.accountFilePath), 0o755)
require.NoError(t, err)
err = storage.Save(account)
require.NoError(t, err)
require.FileExists(t, storage.accountFilePath)
accountFilePath, err := filepath.Rel(basePath, storage.accountFilePath)
require.NoError(t, err)
assert.Equal(t, filepath.Join(baseAccountsRootFolderName, "test@example.com", accountFileName), accountFilePath)
file, err := os.ReadFile(storage.accountFilePath)
require.NoError(t, err)
expected, err := os.ReadFile(filepath.Join("testdata", accountFileName))
require.NoError(t, err)
assert.JSONEq(t, string(expected), string(file))
}
func TestAccountsStorage_LoadAccount(t *testing.T) {
storage, err := NewAccountsStorage(AccountsStorageConfig{
Email: "test@example.com",
BasePath: t.TempDir(),
})
require.NoError(t, err)
storage.accountFilePath = filepath.Join("testdata", accountFileName)
account := storage.LoadAccount(t.Context(), "")
expected := &Account{
Email: "account@example.com",
Registration: &registration.Resource{
Body: acme.Account{
Status: "valid",
Contact: []string{"contact@example.com"},
TermsOfServiceAgreed: true,
Orders: "https://ame.example.com/orders/123456",
OnlyReturnExisting: true,
ExternalAccountBinding: []byte(`"EAB"`),
},
URI: "https://ame.example.com",
},
key: crypto.PrivateKey(""),
}
assert.Equal(t, expected, account)
}
func TestAccountsStorage_GetPrivateKey(t *testing.T) {
testCases := []struct {
desc string
basePath string
}{
{
desc: "create a new private key",
},
{
desc: "existing private key",
basePath: "testdata",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
if test.basePath == "" {
test.basePath = t.TempDir()
}
storage, err := NewAccountsStorage(AccountsStorageConfig{
Email: "test@example.com",
BasePath: test.basePath,
})
require.NoError(t, err)
expectedPath := filepath.Join(test.basePath, baseAccountsRootFolderName, "test@example.com", baseKeysFolderName, "test@example.com.key")
privateKey := storage.GetPrivateKey(certcrypto.RSA4096)
assert.FileExists(t, expectedPath)
assert.IsType(t, &rsa.PrivateKey{}, privateKey)
})
}
}

View file

@ -0,0 +1,38 @@
package storage
import (
"os"
"path/filepath"
)
const (
ExtIssuer = ".issuer.crt"
ExtCert = ".crt"
ExtKey = ".key"
ExtPEM = ".pem"
ExtPFX = ".pfx"
ExtResource = ".json"
)
const (
baseCertificatesFolderName = "certificates"
baseArchivesFolderName = "archives"
)
func getCertificatesRootPath(basePath string) string {
return filepath.Join(basePath, baseCertificatesFolderName)
}
func getCertificatesArchivePath(basePath string) string {
return filepath.Join(basePath, baseArchivesFolderName)
}
func CreateNonExistingFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0o700)
} else if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,93 @@
package storage
import (
"crypto/x509"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/log"
"golang.org/x/net/idna"
)
type CertificatesReader struct {
rootPath string
}
func NewCertificatesReader(basePath string) *CertificatesReader {
return &CertificatesReader{
rootPath: getCertificatesRootPath(basePath),
}
}
func (s *CertificatesReader) ReadResource(domain string) certificate.Resource {
raw, err := s.ReadFile(domain, ExtResource)
if err != nil {
log.Fatal("Error while loading the metadata.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
}
var resource certificate.Resource
if err = json.Unmarshal(raw, &resource); err != nil {
log.Fatal("Error while marshaling the metadata.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
}
return resource
}
func (s *CertificatesReader) ExistsFile(domain, extension string) bool {
filePath := s.GetFileName(domain, extension)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatal("File stat", slog.String("filepath", filePath), log.ErrorAttr(err))
}
return true
}
func (s *CertificatesReader) ReadFile(domain, extension string) ([]byte, error) {
return os.ReadFile(s.GetFileName(domain, extension))
}
func (s *CertificatesReader) GetRootPath() string {
return s.rootPath
}
func (s *CertificatesReader) GetFileName(domain, extension string) string {
filename := sanitizedDomain(domain) + extension
return filepath.Join(s.rootPath, filename)
}
func (s *CertificatesReader) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
content, err := s.ReadFile(domain, extension)
if err != nil {
return nil, err
}
// The input may be a bundle or a single certificate.
return certcrypto.ParsePEMBundle(content)
}
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
if err != nil {
log.Fatal("Could not sanitize the domain.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
}
return safe
}

View file

@ -0,0 +1,141 @@
package storage
import (
"path/filepath"
"testing"
"github.com/go-acme/lego/v5/certificate"
"github.com/stretchr/testify/assert"
)
func TestNewCertificatesWriter_ReadResource(t *testing.T) {
reader := NewCertificatesReader("testdata")
resource := reader.ReadResource("example.com")
expected := certificate.Resource{
Domain: "example.com",
CertURL: "https://acme.example.org/cert/123",
CertStableURL: "https://acme.example.org/cert/456",
}
assert.Equal(t, expected, resource)
}
func TestNewCertificatesWriter_ExistsFile(t *testing.T) {
reader := NewCertificatesReader("testdata")
testCases := []struct {
desc string
domain string
extension string
assert assert.BoolAssertionFunc
}{
{
desc: "exists",
domain: "example.com",
extension: ExtResource,
assert: assert.True,
},
{
desc: "not exists",
domain: "example.org",
extension: ExtResource,
assert: assert.False,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
test.assert(t, reader.ExistsFile(test.domain, test.extension))
})
}
}
func TestNewCertificatesWriter_ReadFile(t *testing.T) {
reader := NewCertificatesReader("testdata")
data, err := reader.ReadFile("example.com", ExtResource)
assert.NoError(t, err)
assert.NotEmpty(t, data)
}
func TestNewCertificatesWriter_GetRootPath(t *testing.T) {
basePath := t.TempDir()
reader := NewCertificatesReader(basePath)
rootPath := reader.GetRootPath()
expected := filepath.Join(basePath, baseCertificatesFolderName)
assert.Equal(t, expected, rootPath)
}
func TestNewCertificatesWriter_GetFileName(t *testing.T) {
testCases := []struct {
desc string
domain string
extension string
expected string
}{
{
desc: "simple",
domain: "example.com",
extension: ExtCert,
expected: "example.com.crt",
},
{
desc: "hyphen",
domain: "test-acme.example.com",
extension: ExtResource,
expected: "test-acme.example.com.json",
},
{
desc: "wildcard",
domain: "*.example.com",
extension: ExtKey,
expected: "_.example.com.key",
},
{
desc: "colon",
domain: "acme:test.example.com",
extension: ExtResource,
expected: "acme-test.example.com.json",
},
{
desc: "IDN",
domain: "测试.com",
extension: ExtResource,
expected: "xn--0zwm56d.com.json",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
basePath := t.TempDir()
reader := NewCertificatesReader(basePath)
filename := reader.GetFileName(test.domain, test.extension)
expected := filepath.Join(basePath, baseCertificatesFolderName, test.expected)
assert.Equal(t, expected, filename)
})
}
}
func TestNewCertificatesWriter_ReadCertificate(t *testing.T) {
reader := NewCertificatesReader("testdata")
cert, err := reader.ReadCertificate("example.org", ExtCert)
assert.NoError(t, err)
assert.NotEmpty(t, cert)
}

View file

@ -1,4 +1,4 @@
package cmd
package storage
import (
"bytes"
@ -17,26 +17,23 @@ import (
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
"golang.org/x/net/idna"
"software.sslmate.com/src/go-pkcs12"
)
const (
baseCertificatesFolderName = "certificates"
baseArchivesFolderName = "archives"
)
const filePerm os.FileMode = 0o600
const (
issuerExt = ".issuer.crt"
certExt = ".crt"
keyExt = ".key"
pemExt = ".pem"
pfxExt = ".pfx"
resourceExt = ".json"
)
type CertificatesWriterConfig struct {
BasePath string
// CertificatesStorage a certificates' storage.
PEM bool
PFX bool
PFXFormat string
PFXPassword string
Filename string // TODO(ldez): remove
}
// CertificatesWriter a writer of certificate files.
//
// rootPath:
//
@ -49,39 +46,42 @@ const (
// ./.lego/archives/
// │ └── archived certificates directory
// └── "path" option
type CertificatesStorage struct {
type CertificatesWriter struct {
rootPath string
archivePath string
pem bool
pem bool
pfx bool
pfxPassword string
pfxFormat string
filename string // Deprecated
pfxPassword string
filename string // TODO(ldez): remove
}
// NewCertificatesStorage create a new certificates storage.
func NewCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
pfxFormat := cmd.String(flgPFXFormat)
switch pfxFormat {
case "DES", "RC2", "SHA256":
default:
log.Fatal("Invalid PFX format.", slog.String("format", pfxFormat))
// NewCertificatesWriter create a new certificates storage writer.
func NewCertificatesWriter(config CertificatesWriterConfig) (*CertificatesWriter, error) {
if config.PFX {
switch config.PFXFormat {
case "DES", "RC2", "SHA256":
default:
return nil, fmt.Errorf("invalid PFX format: %s", config.PFXFormat)
}
}
return &CertificatesStorage{
rootPath: filepath.Join(cmd.String(flgPath), baseCertificatesFolderName),
archivePath: filepath.Join(cmd.String(flgPath), baseArchivesFolderName),
pem: cmd.Bool(flgPEM),
pfx: cmd.Bool(flgPFX),
pfxPassword: cmd.String(flgPFXPass),
pfxFormat: pfxFormat,
filename: cmd.String(flgFilename),
}
return &CertificatesWriter{
rootPath: getCertificatesRootPath(config.BasePath),
archivePath: getCertificatesArchivePath(config.BasePath),
pem: config.PEM,
pfx: config.PFX,
pfxPassword: config.PFXPassword,
pfxFormat: config.PFXFormat,
filename: config.Filename,
}, nil
}
func (s *CertificatesStorage) CreateRootFolder() {
err := createNonExistingFolder(s.rootPath)
func (s *CertificatesWriter) CreateRootFolder() {
err := CreateNonExistingFolder(s.rootPath)
if err != nil {
log.Fatal("Could not check/create the root folder",
slog.String("filepath", s.rootPath),
@ -90,8 +90,8 @@ func (s *CertificatesStorage) CreateRootFolder() {
}
}
func (s *CertificatesStorage) CreateArchiveFolder() {
err := createNonExistingFolder(s.archivePath)
func (s *CertificatesWriter) CreateArchiveFolder() {
err := CreateNonExistingFolder(s.archivePath)
if err != nil {
log.Fatal("Could not check/create the archive folder.",
slog.String("filepath", s.archivePath),
@ -100,16 +100,12 @@ func (s *CertificatesStorage) CreateArchiveFolder() {
}
}
func (s *CertificatesStorage) GetRootPath() string {
return s.rootPath
}
func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
func (s *CertificatesWriter) SaveResource(certRes *certificate.Resource) {
domain := certRes.Domain
// We store the certificate, private key and metadata in different files
// as web servers would not be able to work with a combined file.
err := s.WriteFile(domain, certExt, certRes.Certificate)
err := s.writeFile(domain, ExtCert, certRes.Certificate)
if err != nil {
log.Fatal("Unable to save Certificate.",
log.DomainAttr(domain),
@ -118,7 +114,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
}
if certRes.IssuerCertificate != nil {
err = s.WriteFile(domain, issuerExt, certRes.IssuerCertificate)
err = s.writeFile(domain, ExtIssuer, certRes.IssuerCertificate)
if err != nil {
log.Fatal("Unable to save IssuerCertificate.",
log.DomainAttr(domain),
@ -129,7 +125,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
// if we were given a CSR, we don't know the private key
if certRes.PrivateKey != nil {
err = s.WriteCertificateFiles(domain, certRes)
err = s.writeCertificateFiles(domain, certRes)
if err != nil {
log.Fatal("Unable to save PrivateKey.", log.DomainAttr(domain), log.ErrorAttr(err))
}
@ -146,7 +142,7 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
)
}
err = s.WriteFile(domain, resourceExt, jsonBytes)
err = s.writeFile(domain, ExtResource, jsonBytes)
if err != nil {
log.Fatal("Unable to save CertResource.",
log.DomainAttr(domain),
@ -155,85 +151,59 @@ func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) {
}
}
func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource {
raw, err := s.ReadFile(domain, resourceExt)
func (s *CertificatesWriter) MoveToArchive(domain string) error {
baseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))
matches, err := filepath.Glob(baseFilename + ".*")
if err != nil {
log.Fatal("Error while loading the metadata.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
return err
}
var resource certificate.Resource
if err = json.Unmarshal(raw, &resource); err != nil {
log.Fatal("Error while marshaling the metadata.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
for _, oldFile := range matches {
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+ExtIssuer {
continue
}
date := strconv.FormatInt(time.Now().Unix(), 10)
filename := date + "." + filepath.Base(oldFile)
newFile := filepath.Join(s.archivePath, filename)
err = os.Rename(oldFile, newFile)
if err != nil {
return err
}
}
return resource
return nil
}
func (s *CertificatesStorage) ExistsFile(domain, extension string) bool {
filePath := s.GetFileName(domain, extension)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
} else if err != nil {
log.Fatal("File stat", slog.String("filepath", filePath), log.ErrorAttr(err))
}
return true
func (s *CertificatesWriter) GetArchivePath() string {
return s.archivePath
}
func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) {
return os.ReadFile(s.GetFileName(domain, extension))
func (s *CertificatesWriter) IsPEM() bool {
return s.pem
}
func (s *CertificatesStorage) GetFileName(domain, extension string) string {
filename := sanitizedDomain(domain) + extension
return filepath.Join(s.rootPath, filename)
func (s *CertificatesWriter) IsPFX() bool {
return s.pfx
}
func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) {
content, err := s.ReadFile(domain, extension)
if err != nil {
return nil, err
}
// The input may be a bundle or a single certificate.
return certcrypto.ParsePEMBundle(content)
}
func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error {
var baseFileName string
if s.filename != "" {
baseFileName = s.filename
} else {
baseFileName = sanitizedDomain(domain)
}
filePath := filepath.Join(s.rootPath, baseFileName+extension)
return os.WriteFile(filePath, data, filePerm)
}
func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *certificate.Resource) error {
err := s.WriteFile(domain, keyExt, certRes.PrivateKey)
func (s *CertificatesWriter) writeCertificateFiles(domain string, certRes *certificate.Resource) error {
err := s.writeFile(domain, ExtKey, certRes.PrivateKey)
if err != nil {
return fmt.Errorf("unable to save key file: %w", err)
}
if s.pem {
err = s.WriteFile(domain, pemExt, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
err = s.writeFile(domain, ExtPEM, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil))
if err != nil {
return fmt.Errorf("unable to save PEM file: %w", err)
}
}
if s.pfx {
err = s.WritePFXFile(domain, certRes)
err = s.writePFXFile(domain, certRes)
if err != nil {
return fmt.Errorf("unable to save PFX file: %w", err)
}
@ -242,7 +212,7 @@ func (s *CertificatesStorage) WriteCertificateFiles(domain string, certRes *cert
return nil
}
func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.Resource) error {
func (s *CertificatesWriter) writePFXFile(domain string, certRes *certificate.Resource) error {
certPemBlock, _ := pem.Decode(certRes.Certificate)
if certPemBlock == nil {
return fmt.Errorf("unable to parse Certificate for domain %s", domain)
@ -273,33 +243,23 @@ func (s *CertificatesStorage) WritePFXFile(domain string, certRes *certificate.R
return fmt.Errorf("unable to encode PFX data for domain %s: %w", domain, err)
}
return s.WriteFile(domain, pfxExt, pfxBytes)
return s.writeFile(domain, ExtPFX, pfxBytes)
}
func (s *CertificatesStorage) MoveToArchive(domain string) error {
baseFilename := filepath.Join(s.rootPath, sanitizedDomain(domain))
matches, err := filepath.Glob(baseFilename + ".*")
if err != nil {
return err
func (s *CertificatesWriter) writeFile(domain, extension string, data []byte) error {
var baseFileName string
if s.filename != "" {
baseFileName = s.filename
} else {
baseFileName = sanitizedDomain(domain)
}
for _, oldFile := range matches {
if strings.TrimSuffix(oldFile, filepath.Ext(oldFile)) != baseFilename && oldFile != baseFilename+issuerExt {
continue
}
filePath := filepath.Join(s.rootPath, baseFileName+extension)
date := strconv.FormatInt(time.Now().Unix(), 10)
filename := date + "." + filepath.Base(oldFile)
newFile := filepath.Join(s.archivePath, filename)
log.Info("Writing file.",
slog.String("filepath", filePath))
err = os.Rename(oldFile, newFile)
if err != nil {
return err
}
}
return nil
return os.WriteFile(filePath, data, filePerm)
}
func getCertificateChain(certRes *certificate.Resource) ([]*x509.Certificate, error) {
@ -339,16 +299,3 @@ func getPFXEncoder(pfxFormat string) (*pkcs12.Encoder, error) {
return encoder, nil
}
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
if err != nil {
log.Fatal("Could not sanitize the domain.",
log.DomainAttr(domain),
log.ErrorAttr(err),
)
}
return safe
}

View file

@ -0,0 +1,285 @@
package storage
import (
"os"
"path/filepath"
"regexp"
"testing"
"github.com/go-acme/lego/v5/certificate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCertificatesWriter_CreateRootFolder(t *testing.T) {
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: t.TempDir(),
})
require.NoError(t, err)
require.NoDirExists(t, writer.rootPath)
writer.CreateRootFolder()
require.DirExists(t, writer.rootPath)
}
func TestCertificatesWriter_CreateArchiveFolder(t *testing.T) {
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: t.TempDir(),
})
require.NoError(t, err)
require.NoDirExists(t, writer.GetArchivePath())
writer.CreateArchiveFolder()
require.DirExists(t, writer.GetArchivePath())
}
func TestCertificatesWriter_SaveResource(t *testing.T) {
basePath := t.TempDir()
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: basePath,
})
require.NoError(t, err)
err = os.MkdirAll(writer.rootPath, 0o755)
require.NoError(t, err)
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.crt"))
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.issuer"))
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.key"))
require.NoFileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
writer.SaveResource(&certificate.Resource{
Domain: "example.com",
CertURL: "https://acme.example.org/cert/123",
CertStableURL: "https://acme.example.org/cert/456",
PrivateKey: []byte("PrivateKey"),
Certificate: []byte("Certificate"),
IssuerCertificate: []byte("IssuerCertificate"),
CSR: []byte("CSR"),
})
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.crt"))
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.issuer.crt"))
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.key"))
require.FileExists(t, filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
assertCertificateFileContent(t, basePath, "example.com.crt")
assertCertificateFileContent(t, basePath, "example.com.issuer.crt")
assertCertificateFileContent(t, basePath, "example.com.key")
actual, err := os.ReadFile(filepath.Join(basePath, baseCertificatesFolderName, "example.com.json"))
require.NoError(t, err)
expected, err := os.ReadFile(filepath.Join("testdata", baseCertificatesFolderName, "example.com.json"))
require.NoError(t, err)
assert.JSONEq(t, string(expected), string(actual))
}
func TestCertificatesWriter_MoveToArchive(t *testing.T) {
domain := "example.com"
certStorage := setupCertificatesWriter(t)
domainFiles := generateTestFiles(t, certStorage.rootPath, domain)
err := certStorage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.NoFileExists(t, file)
}
root, err := os.ReadDir(certStorage.rootPath)
require.NoError(t, err)
require.Empty(t, root)
archive, err := os.ReadDir(certStorage.GetArchivePath())
require.NoError(t, err)
require.Len(t, archive, len(domainFiles))
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
}
func TestCertificatesWriter_MoveToArchive_noFileRelatedToDomain(t *testing.T) {
domain := "example.com"
certStorage := setupCertificatesWriter(t)
domainFiles := generateTestFiles(t, certStorage.rootPath, "example.org")
err := certStorage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.FileExists(t, file)
}
root, err := os.ReadDir(certStorage.rootPath)
require.NoError(t, err)
assert.Len(t, root, len(domainFiles))
archive, err := os.ReadDir(certStorage.GetArchivePath())
require.NoError(t, err)
assert.Empty(t, archive)
}
func TestCertificatesWriter_MoveToArchive_ambiguousDomain(t *testing.T) {
domain := "example.com"
certStorage := setupCertificatesWriter(t)
domainFiles := generateTestFiles(t, certStorage.rootPath, domain)
otherDomainFiles := generateTestFiles(t, certStorage.rootPath, domain+".example.org")
err := certStorage.MoveToArchive(domain)
require.NoError(t, err)
for _, file := range domainFiles {
assert.NoFileExists(t, file)
}
for _, file := range otherDomainFiles {
assert.FileExists(t, file)
}
root, err := os.ReadDir(certStorage.rootPath)
require.NoError(t, err)
require.Len(t, root, len(otherDomainFiles))
archive, err := os.ReadDir(certStorage.GetArchivePath())
require.NoError(t, err)
require.Len(t, archive, len(domainFiles))
assert.Regexp(t, `\d+\.`+regexp.QuoteMeta(domain), archive[0].Name())
}
func TestCertificatesWriter_GetArchivePath(t *testing.T) {
basePath := t.TempDir()
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: basePath,
})
require.NoError(t, err)
assert.Equal(t, filepath.Join(basePath, baseArchivesFolderName), writer.GetArchivePath())
}
func TestCertificatesWriter_IsPEM(t *testing.T) {
testCases := []struct {
desc string
pem bool
assert assert.BoolAssertionFunc
}{
{
desc: "PEM enable",
pem: true,
assert: assert.True,
},
{
desc: "PEM disable",
pem: false,
assert: assert.False,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: t.TempDir(),
PEM: test.pem,
})
require.NoError(t, err)
test.assert(t, writer.IsPEM())
})
}
}
func TestCertificatesWriter_IsPFX(t *testing.T) {
testCases := []struct {
desc string
pfx bool
assert assert.BoolAssertionFunc
}{
{
desc: "PFX enable",
pfx: true,
assert: assert.True,
},
{
desc: "PFX disable",
pfx: false,
assert: assert.False,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
writer, err := NewCertificatesWriter(CertificatesWriterConfig{
BasePath: t.TempDir(),
PFX: test.pfx,
PFXFormat: "DES",
})
require.NoError(t, err)
test.assert(t, writer.IsPFX())
})
}
}
func assertCertificateFileContent(t *testing.T, basePath, filename string) {
t.Helper()
actual, err := os.ReadFile(filepath.Join(basePath, baseCertificatesFolderName, filename))
require.NoError(t, err)
expected, err := os.ReadFile(filepath.Join("testdata", baseCertificatesFolderName, filename))
require.NoError(t, err)
assert.Equal(t, string(expected), string(actual))
}
func setupCertificatesWriter(t *testing.T) *CertificatesWriter {
t.Helper()
basePath := t.TempDir()
writer, err := NewCertificatesWriter(
CertificatesWriterConfig{
BasePath: basePath,
},
)
require.NoError(t, err)
writer.CreateRootFolder()
writer.CreateArchiveFolder()
return writer
}
func generateTestFiles(t *testing.T, dir, domain string) []string {
t.Helper()
var filenames []string
for _, ext := range []string{ExtIssuer, ExtCert, ExtKey, ExtPEM, ExtPFX, ExtResource} {
filename := filepath.Join(dir, domain+ext)
err := os.WriteFile(filename, []byte("test"), 0o666)
require.NoError(t, err)
filenames = append(filenames, filename)
}
return filenames
}

View file

@ -0,0 +1,16 @@
{
"email": "account@example.com",
"registration": {
"body": {
"status": "valid",
"contact": [
"contact@example.com"
],
"termsOfServiceAgreed": true,
"orders": "https://ame.example.com/orders/123456",
"onlyReturnExisting": true,
"externalAccountBinding": "EAB"
},
"uri": "https://ame.example.com"
}
}

View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJJwIBAAKCAgEAxF6apvKKtm33tZbeYwsS2v3BAR6Q1UM4PzyTnckyjlqQ+9p6
IX3ZTDfatAw4D7Xzw3LQ8YwsNBJPRmx3SFtcbpWsuRtFFtnNuPlxw4UcM2QhWaq1
55R6TMn8dcD+TgQWX5yG3dJBAAHNwtsxbGHjlafu6DHqFrhE63O7Pa0yV/Ld7lyg
CrbIfEEroKPZxXco5VyxH+HceAhBqYe2j7ACWy5G3VlFS2zM3tor5u15bO7XGJAv
Q1jiIG05TWxm0cfSq6IiJ3vaWlvwEmHFBxSmB/6/IGblw0FKLf2lUOYma87xJeS4
2TjPYw+cZRqSYJeUBpXGuoGZq600PvpNwoPlT9umxFfAIOSLcKFzEqMG73sJVgO6
TgDl+YYdkWQ0iCBmanUkkUUw11ESO3jIk5Wx7MSljbN21FIhMXOjzlY73Hx7YrlE
Vz9IVARXp1cnr0rZtGx/WfIt8JXwt2FGiXsjb4BkVBfq7unofpgT1QBajYdu9sCO
z1C26ctgByXldsHH/7UdS9O/5zMA/WJeHzYxC8GIMt/LSxOOH98FAvDCdnTE8r1b
GJb0vEwPUNbjA1rzmx62K/4WsS/aCnxGsAhGFePky/NsuXOqGAXEtbWYhWR6IjJg
RELa28RNwT61x67xuc0bz0H0/aaFZ0brIR6td4igyAOb6TxvbT7Esmm3Ua0CAwEA
AQKCAf8TS1+/VmRKw5cJaASZQJD8LpA+ZIdgbP9U6Isia/E899CCKaabVREEzp+H
OL/TQYOyEwwl6sdZaiRf7m/J+wi3hiTBRnaDZc+CIjtt00PyF4Ibr9G5oKIU6XUM
REy4cV+aBWTE3Qs3MAngQuRdOAX9OEfesByLMqOS9Rrm4M7qJBpH20gtJ8iLMdwX
iilRx4ruYN5o9MhSZWmYF1sniBAVqmKumCqZUlKAMKDkmavefl4t9/zUPsAglumU
T3TeimFQLSfy4Jr2ReruWZk9e1PBTffIXqKq7zWCFEKz60uzwj8qoWJNML30wrtM
bu183vHAKgr6SF8zfyAFhkPO7LCbNvApOp+DhaOHzL9fx+T0toFnlgXlQ8YWQAjI
Cc6Z8uyKK+R6p4YdhNEB0SUYP8uNNtgmwzhxzf63A5H+XC2r43uveepgN1XtgKbf
GuHKwye7FcKs5L+OTMlUEaWXl0FXp1cRUUAxiOdqQggP6UBFfiuCSId5dl9P7+hY
FdE7By/3bPK5exdBz5bLcVBowrK67bNfwzFBTZAPhrYQENe45zl0FpWiBEWBHTqY
EDxoOqXgzPtrfarT+1MbfOBEQDhZarEc1i8HgUeXUpdC8MR074sN+knMeBBsRw84
YwVMXPhPgvJbo12cmnW434aKaw6VXLLfE+uO1F/Q3QI2zNAVAoIBAQDLdaqR3gL/
HV4MM3o++1UmN7KZdrCQS9WLRR479/2nSfA3WvCyZW6+qg330RJaDkEO1vyyaaRA
c7EIfHy8Ej8Lc9pde0m/1Xl1fZ2p/Tw9e31gAglBHsQlPtrahbTQCwpDXQR5oQwm
pk6tfBD83g+hxqTj0a6N/uL8IkviruhI8wniqD690cyPxCDRNdeY3Kf1RGOBatZH
VB83f7n6aSrtP0vJreWPx1Nr4LGjS7WcLI7fMeXu+uB45pXOGKbg1B47HQNAArQF
732SCOa0E4YpoweDJjIjI/8JSLehqlM+Dfr8RlC2SrjkSPS+vQqLzrZJe4iUqAMd
2yBuYD5lQmuzAoIBAQD3FDm8VkBucjxPF3bUZS5gkD8PAzcWBbrU0RVBkA+yZQJF
6iZb9nY6owVkzVhtTHM/ejV+cNp6zpMd4MiGZ+yc3OUmskoBNeQIj8WxPYDZc9Bl
b1BMdEmy4wz4haZoic8SEfnbPmxh+Rt6y08qkucPWswTJu6+A+kK2cr+Pk2AUkZ7
pNgugytLjmFVVaDisgmm/8B6RnFSYuz1yBkgzcBqULNtayUUrTIQTcaAej7Vs/NT
umVB5B3gdIGMr1xpu7Z2QgErTzOnpg2rCBDW80Dhl8o6OG6f74u/gSUrbJrPPGYl
5bKGpijj9D3SYXXfxa+sOZMgcCs1+PwbxL39Ix0fAoIBAQDHH2XMVMgB/i/pKQhf
U4NGYAR/hVXQIyffocmxT0gEzaw3wN0I+5SjLbN18jxPvtuVNnsh8Zo7Kf79GzjI
p+LjxoLUMrE++iJhBoujrp+iXJWbvOJpxT6aZSWz8F/BrMximUqj7yYBPYqK532I
vZv27H68KJ75gZeMw9QZCq1zl8j490hQZmAZ5A1qM5PJm0sWE9R++Jy2OnJC0tKl
bQACKYx22aZuTNosHkA8XQBk1IcPkbpDZW0DZkj+58mLCI59tCtWHk8p7/WpUuTX
ILSRU2kqxdsT4UrdPznZEuVdOjmFZRvhDMhfQ7ekZUdJBQoKaMiHFNfYBHl9DNyE
JblZAoIBAEZL9NlG2PITgmEmVeK8HuPOZoKI8aVMYAmoqxmKOU0SPAFUSzGi/6RT
OXeijOQb+jY3OP0Ocrq6B1Va3PKQottGZdQKqc+KW6Sr8x0oWH6F2ubhMsFt3IIO
42PZ8qyPeOC1SJc+PWvqigz3x0Bfp2nQ9XsFequJRUaXDJAlfbtirTcEgZVKMIlA
qySyRiH5cZGX9lVTsW41QyHymmOg5nvZFhOthlFJrZLB7hYjsbjvh+1sfN6wXme7
/hfe1LYoeBNRWC/QSuwJ7J2an9/oOa91lk3WPHM4nlQQFFk0fx8zGgTyQ0bYA45H
sUcn/3d5MTAY+WkjQMgMXP4xjbR1xxUCggEAYzuIlT+zxIocmIFtilzQG5fWCU3Z
SYYuP9tRyoRHvTc9N0BojIe34GoQhsqMhlu2u7NlDIDbSQvHtjmi33J0x3Y2uUYy
64CcsrNUnN8tOka8jdcshDOhxsUKo08u/+ZNKpTzD4GfgUKvbz4WcZAg8PJTBSjQ
XFkboKvTBh926THByobwRL/spPAZTVCydtq8G9cVxaaMq1OQJn2KSPZ/dsmWvCBp
zEKwt7g4fFVlQCekJ2tB95LwkyKhjaiqEefyViGM5rtBc4n7KEDKn2L2n+5jBLvD
Gb7FwCYpVTtf3futX2xruSie98VhTrDjDB6sSP3iQ9knayk2Okx7o3PoeQ==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1 @@
Certificate

View file

@ -0,0 +1 @@
IssuerCertificate

View file

@ -0,0 +1,5 @@
{
"domain": "example.com",
"certUrl": "https://acme.example.org/cert/123",
"certStableUrl": "https://acme.example.org/cert/456"
}

View file

@ -0,0 +1 @@
PrivateKey

View file

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBkzCCATmgAwIBAgIQZrrrYlA0GzvmKXjrMwLQNDAKBggqhkjOPQQDAjASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYwMDAw
WjASMRAwDgYDVQQKEwdBY21lIENvMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
NkulPSW9ljKP9q4IRK1lEAp/AvNeWHOcpWgy9c76XkUm05aulJk3Zra1VewY3dq4
XjMPIe/YVwmriOjaw1uuE6NvMG0wDgYDVR0PAQH/BAQDAgKEMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKh2BJ5EzUfChYGB
wwhWa+8sTLALMBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMAoGCCqGSM49BAMCA0gA
MEUCIBbwUdYyeJTRQJBxY8s4IUAr7KhyT2+/WuusIy3+FpsTAiEA+7X2Vikcrerk
XWwaukHVTQhirl0SHh2QegsQzM9IzTM=
-----END CERTIFICATE-----

View file

@ -19,8 +19,6 @@ func main() {
Usage: "Let's Encrypt client written in Go",
Version: getVersion(),
EnableShellCompletion: true,
Flags: cmd.CreateFlags(""),
Before: cmd.Before,
Commands: cmd.CreateCommands(),
}
@ -28,8 +26,6 @@ func main() {
fmt.Printf("lego version %s %s/%s\n", cmd.Version, runtime.GOOS, runtime.GOARCH)
}
app.Commands = cmd.CreateCommands()
err := app.Run(context.Background(), os.Args)
if err != nil {
log.Fatal("Error", log.ErrorAttr(err))

View file

@ -15,6 +15,7 @@ import (
"github.com/go-acme/lego/v5/acme"
"github.com/go-acme/lego/v5/certcrypto"
"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/go-acme/lego/v5/registration"
@ -22,10 +23,8 @@ import (
"github.com/urfave/cli/v3"
)
const filePerm os.FileMode = 0o600
// setupClient creates a new client with challenge settings.
func setupClient(cmd *cli.Command, account *Account, keyType certcrypto.KeyType) *lego.Client {
func setupClient(cmd *cli.Command, account *storage.Account, keyType certcrypto.KeyType) *lego.Client {
client := newClient(cmd, account, keyType)
setupChallenges(cmd, client)
@ -33,15 +32,15 @@ func setupClient(cmd *cli.Command, account *Account, keyType certcrypto.KeyType)
return client
}
func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) {
func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *storage.AccountsStorage) (*storage.Account, certcrypto.KeyType) {
keyType := getKeyType(cmd)
privateKey := accountsStorage.GetPrivateKey(keyType)
var account *Account
var account *storage.Account
if accountsStorage.ExistsAccountFilePath() {
account = accountsStorage.LoadAccount(ctx, privateKey)
} else {
account = &Account{Email: accountsStorage.GetEmail(), key: privateKey}
account = storage.NewAccount(accountsStorage.GetEmail(), privateKey)
}
return account, keyType
@ -123,16 +122,6 @@ func getUserAgent(cmd *cli.Command) string {
return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", cmd.String(flgUserAgent), cmd.Version))
}
func createNonExistingFolder(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, 0o700)
} else if err != nil {
return err
}
return nil
}
func readCSRFile(filename string) (*x509.CertificateRequest, error) {
bytes, err := os.ReadFile(filename)
if err != nil {

52
cmd/storages.go Normal file
View file

@ -0,0 +1,52 @@
package cmd
import (
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
)
// CertificatesStorage a certificates' storage.
type CertificatesStorage struct {
*storage.CertificatesWriter
*storage.CertificatesReader
}
// newCertificatesStorage create a new certificates storage.
func newCertificatesStorage(cmd *cli.Command) *CertificatesStorage {
basePath := cmd.String(flgPath)
config := storage.CertificatesWriterConfig{
BasePath: basePath,
PEM: cmd.Bool(flgPEM),
PFX: cmd.Bool(flgPFX),
PFXFormat: cmd.String(flgPFXPass),
PFXPassword: cmd.String(flgPFXFormat),
Filename: cmd.String(flgFilename),
}
writer, err := storage.NewCertificatesWriter(config)
if err != nil {
log.Fatal("Certificates storage initialization", log.ErrorAttr(err))
}
return &CertificatesStorage{
CertificatesWriter: writer,
CertificatesReader: storage.NewCertificatesReader(basePath),
}
}
// newAccountsStorage Creates a new AccountsStorage.
func newAccountsStorage(cmd *cli.Command) *storage.AccountsStorage {
accountsStorage, err := storage.NewAccountsStorage(storage.AccountsStorageConfig{
Email: cmd.String(flgEmail),
BasePath: cmd.String(flgPath),
Server: cmd.String(flgServer),
UserAgent: getUserAgent(cmd),
})
if err != nil {
log.Fatal("Accounts storage initialization", log.ErrorAttr(err))
}
return accountsStorage
}

View file

@ -19,18 +19,40 @@ COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
"""
[[command]]
title = "lego help run"
content = """
NAME:
lego run - Register an account, then create and install a certificate
USAGE:
lego run
OPTIONS:
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") [$LEGO_SERVER]
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
--disable-cn Disable the use of the common name in the CSR.
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
--disable-cn Disable the use of the common name in the CSR.
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
--tls-skip-verify Skip the TLS verification of the ACME server.
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
--overall-request-limit int ACME overall requests limit. (default: 18)
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
--filename string (deprecated) Filename of the generated certificate.
--path string Directory to use for storing the data. (default: "./.lego") [$LEGO_PATH]
--path string Directory to use for storing the data. [$LEGO_PATH]
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
@ -47,42 +69,20 @@ GLOBAL OPTIONS:
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
--tls-skip-verify Skip the TLS verification of the ACME server.
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
--overall-request-limit int ACME overall requests limit. (default: 18)
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
--not-before time Set the notBefore field in the certificate (RFC3339 format)
--not-after time Set the notAfter field in the certificate (RFC3339 format)
--private-key string Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
--run-hook string Define a hook. The hook is executed when the certificates are effectively created.
--run-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
--help, -h show help
"""
[[command]]
title = "lego help run"
content = """
NAME:
lego run - Register an account, then create and install a certificate
USAGE:
lego run
OPTIONS:
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
--not-before time Set the notBefore field in the certificate (RFC3339 format)
--not-after time Set the notAfter field in the certificate (RFC3339 format)
--private-key string Path to private key (in PEM encoding) for the certificate. By default, the private key is generated.
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
--run-hook string Define a hook. The hook is executed when the certificates are effectively created.
--run-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
--help, -h show help
"""
[[command]]
title = "lego help renew"
content = """
@ -93,23 +93,61 @@ USAGE:
lego renew
OPTIONS:
--days int The number of days left on a certificate to renew it. (default: 30)
--dynamic Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.
--ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.
--ari-wait-to-renew-duration duration The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
--reuse-key Used to indicate you want to reuse your current private key for the new certificate.
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
--not-before time Set the notBefore field in the certificate (RFC3339 format)
--not-after time Set the notAfter field in the certificate (RFC3339 format)
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
--renew-hook string Define a hook. The hook is executed only when the certificates are effectively renewed.
--renew-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
--no-random-sleep Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way.
--force-cert-domains Check and ensure that the cert's domain list matches those passed in the domains argument.
--help, -h show help
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
--disable-cn Disable the use of the common name in the CSR.
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
--tls-skip-verify Skip the TLS verification of the ACME server.
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
--overall-request-limit int ACME overall requests limit. (default: 18)
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
--filename string (deprecated) Filename of the generated certificate.
--path string Directory to use for storing the data. [$LEGO_PATH]
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
--http.proxy-header string Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host")
--http.webroot string Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
--http.memcached-host string [ --http.memcached-host string ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.
--http.s3-bucket string Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
--tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.
--tls.port string Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443")
--tls.delay duration Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge. (default: 0s)
--dns string Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.
--dns.disable-cp (deprecated) use dns.propagation-disable-ans instead.
--dns.propagation-disable-ans By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
--days int The number of days left on a certificate to renew it. (default: 30)
--dynamic Compute dynamically, based on the lifetime of the certificate(s), when to renew: use 1/3rd of the lifetime left, or 1/2 of the lifetime for short-lived certificates). This supersedes --days and will be the default behavior in Lego v5.
--ari-disable Do not use the renewalInfo endpoint (RFC9773) to check if a certificate should be renewed.
--ari-wait-to-renew-duration duration The maximum duration you're willing to sleep for a renewal time returned by the renewalInfo endpoint. (default: 0s)
--reuse-key Used to indicate you want to reuse your current private key for the new certificate.
--no-bundle Do not create a certificate bundle by adding the issuers certificate to the new certificate.
--must-staple Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.
--not-before time Set the notBefore field in the certificate (RFC3339 format)
--not-after time Set the notAfter field in the certificate (RFC3339 format)
--preferred-chain string If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.
--profile string If the CA offers multiple certificate profiles (draft-ietf-acme-profiles), choose this one.
--always-deactivate-authorizations string Force the authorizations to be relinquished even if the certificate request was successful.
--renew-hook string Define a hook. The hook is executed only when the certificates are effectively renewed.
--renew-hook-timeout duration Define the timeout for the hook execution. (default: 2m0s)
--no-random-sleep Do not add a random sleep before the renewal. We do not recommend using this flag if you are doing your renewals in an automated way.
--force-cert-domains Check and ensure that the cert's domain list matches those passed in the domains argument.
--help, -h show help
"""
[[command]]
@ -122,9 +160,47 @@ USAGE:
lego revoke
OPTIONS:
--keep, -k Keep the certificates after the revocation instead of archiving them.
--reason uint Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. Valid values are: 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged), 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL), 9 (privilegeWithdrawn), or 10 (aACompromise). (default: 0)
--help, -h show help
--domains string, -d string [ --domains string, -d string ] Add a domain to the process. Can be specified multiple times.
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--email string, -m string Email used for registration and recovery contact. [$LEGO_EMAIL]
--csr string, -c string Certificate signing request filename, if an external CSR is to be used.
--eab Use External Account Binding for account registration. Requires --kid and --hmac. [$LEGO_EAB]
--kid string Key identifier from External CA. Used for External Account Binding. [$LEGO_EAB_KID]
--hmac string MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. [$LEGO_EAB_HMAC]
--server string, -s string CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. [$LEGO_SERVER]
--disable-cn Disable the use of the common name in the CSR.
--key-type string, -k string Key type to use for private keys. Supported: rsa2048, rsa3072, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
--http-timeout int Set the HTTP timeout value to a specific value in seconds. (default: 0)
--tls-skip-verify Skip the TLS verification of the ACME server.
--cert.timeout int Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
--overall-request-limit int ACME overall requests limit. (default: 18)
--user-agent string Add to the user-agent sent to the CA to identify an application embedding lego-cli
--filename string (deprecated) Filename of the generated certificate.
--path string Directory to use for storing the data. [$LEGO_PATH]
--pem Generate an additional .pem (base64) file by concatenating the .key and .crt files together.
--pfx Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together. [$LEGO_PFX]
--pfx.pass string The password used to encrypt the .pfx (PCKS#12) file. (default: "changeit") [$LEGO_PFX_PASSWORD]
--pfx.format string The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256. (default: "RC2") [$LEGO_PFX_FORMAT]
--http Use the HTTP-01 challenge to solve challenges. Can be mixed with other types of challenges.
--http.port string Set the port and interface to use for HTTP-01 based challenges to listen on. Supported: interface:port or :port. (default: ":80")
--http.delay duration Delay between the starts of the HTTP server (use for HTTP-01 based challenges) and the validation of the challenge. (default: 0s)
--http.proxy-header string Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host")
--http.webroot string Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
--http.memcached-host string [ --http.memcached-host string ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.
--http.s3-bucket string Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
--tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.
--tls.port string Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443")
--tls.delay duration Delay between the start of the TLS listener (use for TLSALPN-01 based challenges) and the validation of the challenge. (default: 0s)
--dns string Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.
--dns.disable-cp (deprecated) use dns.propagation-disable-ans instead.
--dns.propagation-disable-ans By setting this flag to true, disables the need to await propagation of the TXT record to all authoritative name servers.
--dns.propagation-rns By setting this flag to true, use all the recursive nameservers to check the propagation of the TXT record.
--dns.propagation-wait duration By setting this flag, disables all the propagation checks of the TXT record and uses a wait duration instead. (default: 0s)
--dns.resolvers string [ --dns.resolvers string ] Set the resolvers to use for performing (recursive) CNAME resolving and apex domain determination. For DNS-01 challenge verification, the authoritative DNS server is queried directly. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
--dns-timeout int Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name server queries. (default: 10)
--keep, -k Keep the certificates after the revocation instead of archiving them.
--reason uint Identifies the reason for the certificate revocation. See https://www.rfc-editor.org/rfc/rfc5280.html#section-5.3.1. Valid values are: 0 (unspecified), 1 (keyCompromise), 2 (cACompromise), 3 (affiliationChanged), 4 (superseded), 5 (cessationOfOperation), 6 (certificateHold), 8 (removeFromCRL), 9 (privilegeWithdrawn), or 10 (aACompromise). (default: 0)
--help, -h show help
"""
[[command]]
@ -138,7 +214,8 @@ USAGE:
OPTIONS:
--accounts, -a Display accounts.
--names, -n Display certificate common names only.
--names, -n Display certificate names only.
--path string Directory to use for storing the data. [$LEGO_PATH]
--help, -h show help
"""

View file

@ -66,13 +66,14 @@ func TestChallengeHTTP_Run(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", testDomain1,
"--http",
"--http.port", ":5002",
"run")
)
if err != nil {
t.Fatal(err)
}
@ -82,13 +83,14 @@ func TestChallengeTLS_Run_Domains(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", testDomain1,
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
@ -98,13 +100,14 @@ func TestChallengeTLS_Run_IP(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", "127.0.0.1",
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
@ -116,13 +119,14 @@ func TestChallengeTLS_Run_CSR(t *testing.T) {
csrPath := createTestCSRFile(t, true)
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-csr", csrPath,
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
@ -134,13 +138,14 @@ func TestChallengeTLS_Run_CSR_PEM(t *testing.T) {
csrPath := createTestCSRFile(t, false)
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-csr", csrPath,
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
@ -150,6 +155,7 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
@ -157,12 +163,13 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) {
"-d", testDomain3,
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
err = load.RunLego(t.Context(),
"revoke",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
@ -179,25 +186,27 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", testDomain4,
"--tls",
"--tls.port", ":5001",
"run")
)
if err != nil {
t.Fatal(err)
}
err = load.RunLego(t.Context(),
"revoke",
"-m", testEmail1,
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", testDomain4,
"--tls",
"--tls.port", ":5001",
"revoke")
)
if err != nil {
t.Fatal(err)
}

View file

@ -59,6 +59,7 @@ func TestChallengeDNS_Run(t *testing.T) {
loader.CleanLegoFiles(t.Context())
err := load.RunLego(t.Context(),
"run",
"--accept-tos",
"--dns", "exec",
"--dns.resolvers", ":8053",
@ -66,7 +67,7 @@ func TestChallengeDNS_Run(t *testing.T) {
"-s", "https://localhost:15000/dir",
"-d", testDomain2,
"-d", testDomain1,
"run")
)
if err != nil {
t.Fatal(err)
}

View file

@ -86,14 +86,12 @@ func generate(ctx context.Context) error {
// createStubApp Construct cli app, very similar to cmd/lego/main.go.
// Notable differences:
// - substitute "." for CWD in default config path, as the user will very likely see a different path
// - do not include version information, because we're likely running against a snapshot
// - skip DNS help and provider list, as initialization takes time, and we don't generate `lego dns --help` here.
func createStubApp() *cli.Command {
return &cli.Command{
Name: "lego",
Usage: "Let's Encrypt client written in Go",
Flags: cmd.CreateFlags("./.lego"),
Commands: cmd.CreateCommands(),
}
}