1package cmd
2
3import (
4	"bufio"
5	"fmt"
6	"os"
7	"strings"
8
9	"github.com/go-acme/lego/v4/certificate"
10	"github.com/go-acme/lego/v4/lego"
11	"github.com/go-acme/lego/v4/log"
12	"github.com/go-acme/lego/v4/registration"
13	"github.com/urfave/cli"
14)
15
16func createRun() cli.Command {
17	return cli.Command{
18		Name:  "run",
19		Usage: "Register an account, then create and install a certificate",
20		Before: func(ctx *cli.Context) error {
21			// we require either domains or csr, but not both
22			hasDomains := len(ctx.GlobalStringSlice("domains")) > 0
23			hasCsr := len(ctx.GlobalString("csr")) > 0
24			if hasDomains && hasCsr {
25				log.Fatal("Please specify either --domains/-d or --csr/-c, but not both")
26			}
27			if !hasDomains && !hasCsr {
28				log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)")
29			}
30			return nil
31		},
32		Action: run,
33		Flags: []cli.Flag{
34			cli.BoolFlag{
35				Name:  "no-bundle",
36				Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
37			},
38			cli.BoolFlag{
39				Name:  "must-staple",
40				Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.",
41			},
42			cli.StringFlag{
43				Name:  "run-hook",
44				Usage: "Define a hook. The hook is executed when the certificates are effectively created.",
45			},
46			cli.StringFlag{
47				Name:  "preferred-chain",
48				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.",
49			},
50			cli.StringFlag{
51				Name:  "always-deactivate-authorizations",
52				Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
53			},
54		},
55	}
56}
57
58const rootPathWarningMessage = `!!!! HEADS UP !!!!
59
60Your account credentials have been saved in your Let's Encrypt
61configuration directory at "%s".
62
63You should make a secure backup of this folder now. This
64configuration directory will also contain certificates and
65private keys obtained from Let's Encrypt so making regular
66backups of this folder is ideal.
67`
68
69func run(ctx *cli.Context) error {
70	accountsStorage := NewAccountsStorage(ctx)
71
72	account, client := setup(ctx, accountsStorage)
73	setupChallenges(ctx, client)
74
75	if account.Registration == nil {
76		reg, err := register(ctx, client)
77		if err != nil {
78			log.Fatalf("Could not complete registration\n\t%v", err)
79		}
80
81		account.Registration = reg
82		if err = accountsStorage.Save(account); err != nil {
83			log.Fatal(err)
84		}
85
86		fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath())
87	}
88
89	certsStorage := NewCertificatesStorage(ctx)
90	certsStorage.CreateRootFolder()
91
92	cert, err := obtainCertificate(ctx, client)
93	if err != nil {
94		// Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error.
95		// Due to us not returning partial certificate we can just exit here instead of at the end.
96		log.Fatalf("Could not obtain certificates:\n\t%v", err)
97	}
98
99	certsStorage.SaveResource(cert)
100
101	meta := map[string]string{
102		renewEnvAccountEmail: account.Email,
103		renewEnvCertDomain:   cert.Domain,
104		renewEnvCertPath:     certsStorage.GetFileName(cert.Domain, ".crt"),
105		renewEnvCertKeyPath:  certsStorage.GetFileName(cert.Domain, ".key"),
106	}
107
108	return launchHook(ctx.String("run-hook"), meta)
109}
110
111func handleTOS(ctx *cli.Context, client *lego.Client) bool {
112	// Check for a global accept override
113	if ctx.GlobalBool("accept-tos") {
114		return true
115	}
116
117	reader := bufio.NewReader(os.Stdin)
118	log.Printf("Please review the TOS at %s", client.GetToSURL())
119
120	for {
121		fmt.Println("Do you accept the TOS? Y/n")
122		text, err := reader.ReadString('\n')
123		if err != nil {
124			log.Fatalf("Could not read from console: %v", err)
125		}
126
127		text = strings.Trim(text, "\r\n")
128		switch text {
129		case "", "y", "Y":
130			return true
131		case "n", "N":
132			return false
133		default:
134			fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.")
135		}
136	}
137}
138
139func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) {
140	accepted := handleTOS(ctx, client)
141	if !accepted {
142		log.Fatal("You did not accept the TOS. Unable to proceed.")
143	}
144
145	if ctx.GlobalBool("eab") {
146		kid := ctx.GlobalString("kid")
147		hmacEncoded := ctx.GlobalString("hmac")
148
149		if kid == "" || hmacEncoded == "" {
150			log.Fatalf("Requires arguments --kid and --hmac.")
151		}
152
153		return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
154			TermsOfServiceAgreed: accepted,
155			Kid:                  kid,
156			HmacEncoded:          hmacEncoded,
157		})
158	}
159
160	return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
161}
162
163func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) {
164	bundle := !ctx.Bool("no-bundle")
165
166	domains := ctx.GlobalStringSlice("domains")
167	if len(domains) > 0 {
168		// obtain a certificate, generating a new private key
169		request := certificate.ObtainRequest{
170			Domains:                        domains,
171			Bundle:                         bundle,
172			MustStaple:                     ctx.Bool("must-staple"),
173			PreferredChain:                 ctx.String("preferred-chain"),
174			AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
175		}
176		return client.Certificate.Obtain(request)
177	}
178
179	// read the CSR
180	csr, err := readCSRFile(ctx.GlobalString("csr"))
181	if err != nil {
182		return nil, err
183	}
184
185	// obtain a certificate for this CSR
186	return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
187		CSR:                            csr,
188		Bundle:                         bundle,
189		PreferredChain:                 ctx.String("preferred-chain"),
190		AlwaysDeactivateAuthorizations: ctx.Bool("always-deactivate-authorizations"),
191	})
192}
193