From 43401f2475dd1f6cc2e220908f0caba246ea854e Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 3 Jan 2019 16:59:53 +0100 Subject: [PATCH] New challenges management. (#741) --- .golangci.toml | 1 - README.md | 85 +++++++------- challenge/dns01/dns_challenge.go | 2 + challenge/resolver/solver_manager.go | 59 +--------- challenge/resolver/solver_manager_test.go | 44 ++----- cmd/cmd_dnshelp.go | 2 +- cmd/flags.go | 55 +++++---- cmd/lego/main.go | 7 ++ cmd/setup_challenges.go | 135 +++++++++++----------- e2e/challenges_test.go | 60 ++++------ e2e/dnschallenge/dns_challenges_test.go | 10 +- 11 files changed, 190 insertions(+), 270 deletions(-) diff --git a/.golangci.toml b/.golangci.toml index 8d052187..f0ee5e03 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -38,7 +38,6 @@ exclude = [ "Error return value of (.+) is not checked", "exported (type|method|function) (.+) should have comment or be unexported", - "possible misuse of unsafe.Pointer", "cyclomatic complexity (.+) of func `NewDNSChallengeProviderByName` is high (.+)", # providers/dns/dns_providers.go "string `(lego\\.wtf|manhattan)` has (\\d+) occurrences, make it a constant", #providers/dns/gcloud/googlecloud_test.go diff --git a/README.md b/README.md index 7bb27126..35ee4251 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Let's Encrypt client and ACME library written in Go ### Binaries -To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases) -and put the binary somewhere convenient. lego does not assume anything about the location you run it from. +To get the binary just download the latest release for your OS/Arch from [the release page](https://github.com/xenolf/lego/releases) and put the binary somewhere convenient. +lego does not assume anything about the location you run it from. ### From Docker @@ -55,7 +55,7 @@ go get -u github.com/xenolf/lego/cmd/lego Please keep in mind that CLI switches and APIs are still subject to change. -When using the standard `--path` option, all certificates and account configurations are saved to a folder *.lego* in the current working directory. +When using the standard `--path` option, all certificates and account configurations are saved to a folder `.lego` in the current working directory. ## Usage @@ -75,30 +75,31 @@ COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --domains value, -d value Add a domain to the process. Can be specified multiple times. - --server value, -s value 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") - --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. - --email value, -m value Email used for registration and recovery contact. - --csr value, -c value Certificate signing request filename, if an external CSR is to be used - --eab Use External Account Binding for account registration. Requires --kid and --hmac. - --kid value Key identifier from External CA. Used for External Account Binding. - --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. - --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048") - --filename value Filename of the generated certificate - --path value Directory to use for storing the data (default: "./.lego") - --exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01", "tls-alpn-01". - --http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) - --webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge - --memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. - --http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port - --tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port - --dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage. - --dns-disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers. - --dns-resolvers value Set the resolvers to use for performing recursive DNS queries. 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 value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. The default is 10 seconds. (default: 0) - --pem Generate a .pem file by concatenating the .key and .crt files together. - --help, -h show help - --version, -v print the version + --domains value, -d value Add a domain to the process. Can be specified multiple times. + --server value, -s value 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") + --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. + --email value, -m value Email used for registration and recovery contact. + --csr value, -c value Certificate signing request filename, if an external CSR is to be used. + --eab Use External Account Binding for account registration. Requires --kid and --hmac. + --kid value Key identifier from External CA. Used for External Account Binding. + --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. + --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384. (default: "rsa2048") + --filename value (deprecated) Filename of the generated certificate. + --path value Directory to use for storing the data. (default: "./.lego") + --http Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges. + --http.port value Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port. (default: ":80") + --http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. + --http.memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. + --tls Use the TLS challenge to solve challenges. Can be mixed with other types of challenges. + --tls.port value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port. (default: ":443") + --dns value Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage. + --dns.disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers. + --dns.resolvers value Set the resolvers to use for performing recursive DNS queries. 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 value Set the HTTP timeout value to a specific value in seconds. (default: 0) + --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10) + --pem Generate a .pem file by concatenating the .key and .crt files together. + --help, -h show help + --version, -v print the version ``` ### Sudo @@ -107,14 +108,14 @@ The CLI does not require root permissions but needs to bind to port 80 and 443 f To run the CLI without sudo, you have four options: - Use setcap 'cap_net_bind_service=+ep' /path/to/program -- Pass the `--http` or/and the `--tls` option and specify a custom port to bind to. In this case you have to forward port 80/443 to these custom ports (see [Port Usage](#port-usage)). -- Pass the `--webroot` option and specify the path to your webroot folder. In this case the challenge will be written in a file in `.well-known/acme-challenge/` inside your webroot. +- Pass the `--http.port` or/and the `--tls.port` option and specify a custom port to bind to. In this case you have to forward port 80/443 to these custom ports (see [Port Usage](#port-usage)). +- Pass the `--http.webroot` option and specify the path to your webroot folder. In this case the challenge will be written in a file in `.well-known/acme-challenge/` inside your webroot. - Pass the `--dns` option and specify a DNS provider. ### Port Usage By default lego assumes it is able to bind to ports 80 and 443 to solve challenges. -If this is not possible in your environment, you can use the `--http` and `--tls` options to instruct +If this is not possible in your environment, you can use the `--http.port` and `--tls.port` options to instruct lego to listen on that interface:port for any incoming challenges. If you are using this option, make sure you proxy all of the following traffic to these ports. @@ -131,13 +132,14 @@ This traffic redirection is only needed as long as lego solves challenges. As so ### CLI Example -Assumes the `lego` binary has permission to bind to ports 80 and 443. You can get a pre-built binary from the [releases](https://github.com/xenolf/lego/releases) page. +Assumes the `lego` binary has permission to bind to ports 80 and 443. +You can get a pre-built binary from the [releases](https://github.com/xenolf/lego/releases) page. If your environment does not allow you to bind to these ports, please read [Port Usage](#port-usage). Obtain a certificate: ```bash -lego --email="foo@bar.com" --domains="example.com" run +lego --email="foo@bar.com" --domains="example.com" --http run ``` (Find your certificate in the `.lego` folder of current working directory.) @@ -145,13 +147,13 @@ lego --email="foo@bar.com" --domains="example.com" run To renew the certificate: ```bash -lego --email="foo@bar.com" --domains="example.com" renew +lego --email="foo@bar.com" --domains="example.com" --http renew ``` To renew the certificate only if it expires within 30 days ```bash -lego --email="foo@bar.com" --domains="example.com" renew --days 30 +lego --email="foo@bar.com" --domains="example.com" --http renew --days 30 ``` Obtain a certificate using the DNS challenge and AWS Route 53: @@ -160,17 +162,16 @@ Obtain a certificate using the DNS challenge and AWS Route 53: AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run ``` -Note that `--dns=foo` implies `--exclude=http-01`. lego will not attempt other challenges if you've told it to use DNS instead. - Obtain a certificate given a certificate signing request (CSR) generated by something else: ```bash -lego --email="foo@bar.com" --csr=/path/to/csr.pem run +lego --email="foo@bar.com" --http --csr=/path/to/csr.pem run ``` (lego will infer the domains to be validated based on the contents of the CSR, so make sure the CSR's Common Name and optional SubjectAltNames are set correctly.) -lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead: +lego defaults to communicating with the production Let's Encrypt ACME server. +If you'd like to test something without issuing real certificates, consider using the staging endpoint instead: ```bash lego --server=https://acme-staging-v02.api.letsencrypt.org/directory … @@ -193,6 +194,8 @@ import ( "github.com/xenolf/lego/certcrypto" "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/challenge/http01" + "github.com/xenolf/lego/challenge/tlsalpn01" "github.com/xenolf/lego/lego" "github.com/xenolf/lego/registration" ) @@ -243,10 +246,12 @@ func main() { // because we aren't running as root and can't bind a listener to port 80 and 443 // (used later when we attempt to pass challenges). Keep in mind that you still // need to proxy challenge traffic to port 5002 and 5001. - if err = client.Challenge.SetHTTP01Address(":5002"); err != nil { + err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) + if err != nil { log.Fatal(err) } - if err = client.Challenge.SetTLSALPN01Address(":5001"); err != nil { + err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) + if err != nil { log.Fatal(err) } diff --git a/challenge/dns01/dns_challenge.go b/challenge/dns01/dns_challenge.go index d0ff1ec3..c9ef2ee3 100644 --- a/challenge/dns01/dns_challenge.go +++ b/challenge/dns01/dns_challenge.go @@ -140,6 +140,8 @@ func (c *Challenge) Solve(authz acme.Authorization) error { // CleanUp cleans the challenge. func (c *Challenge) CleanUp(authz acme.Authorization) error { + log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz)) + chlng, err := challenge.FindChallenge(challenge.DNS01, authz) if err != nil { return err diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go index c6feea82..55faf77a 100644 --- a/challenge/resolver/solver_manager.go +++ b/challenge/resolver/solver_manager.go @@ -3,7 +3,6 @@ package resolver import ( "errors" "fmt" - "net" "sort" "strconv" "time" @@ -21,7 +20,7 @@ type byType []acme.Challenge func (a byType) Len() int { return len(a) } func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a byType) Less(i, j int) bool { return a[i].Type < a[j].Type } +func (a byType) Less(i, j int) bool { return a[i].Type > a[j].Type } type SolverManager struct { core *api.Core @@ -29,55 +28,12 @@ type SolverManager struct { } func NewSolversManager(core *api.Core) *SolverManager { - solvers := map[challenge.Type]solver{ - challenge.HTTP01: http01.NewChallenge(core, validate, &http01.ProviderServer{}), - challenge.TLSALPN01: tlsalpn01.NewChallenge(core, validate, &tlsalpn01.ProviderServer{}), - } - return &SolverManager{ - solvers: solvers, + solvers: map[challenge.Type]solver{}, core: core, } } -// SetHTTP01Address specifies a custom interface:port to be used for HTTP based challenges. -// If this option is not used, the default port 80 and all interfaces will be used. -// To only specify a port and no interface use the ":port" notation. -// -// NOTE: This REPLACES any custom HTTP provider previously set by calling -// c.SetProvider with the default HTTP challenge provider. -func (c *SolverManager) SetHTTP01Address(iface string) error { - host, port, err := net.SplitHostPort(iface) - if err != nil { - return err - } - - if chlng, ok := c.solvers[challenge.HTTP01]; ok { - chlng.(*http01.Challenge).SetProvider(http01.NewProviderServer(host, port)) - } - - return nil -} - -// SetTLSALPN01Address specifies a custom interface:port to be used for TLS based challenges. -// If this option is not used, the default port 443 and all interfaces will be used. -// To only specify a port and no interface use the ":port" notation. -// -// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling -// c.SetProvider with the default TLS-ALPN challenge provider. -func (c *SolverManager) SetTLSALPN01Address(iface string) error { - host, port, err := net.SplitHostPort(iface) - if err != nil { - return err - } - - if chlng, ok := c.solvers[challenge.TLSALPN01]; ok { - chlng.(*tlsalpn01.Challenge).SetProvider(tlsalpn01.NewProviderServer(host, port)) - } - - return nil -} - // SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge. func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error { c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p) @@ -96,18 +52,15 @@ func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.Cha return nil } -// Exclude explicitly removes challenges from the pool for solving. -func (c *SolverManager) Exclude(challenges []challenge.Type) { - // Loop through all challenges and delete the requested one if found. - for _, chlg := range challenges { - delete(c.solvers, chlg) - } +// Remove Remove a challenge type from the available solvers. +func (c *SolverManager) Remove(chlgType challenge.Type) { + delete(c.solvers, chlgType) } // Checks all challenges from the server in order and returns the first matching solver. func (c *SolverManager) chooseSolver(authz acme.Authorization) solver { // Allow to have a deterministic challenge order - sort.Sort(sort.Reverse(byType(authz.Challenges))) + sort.Sort(byType(authz.Challenges)) domain := challenge.GetTargetedDomain(authz) for _, chlg := range authz.Challenges { diff --git a/challenge/resolver/solver_manager_test.go b/challenge/resolver/solver_manager_test.go index ae22b373..2f47715b 100644 --- a/challenge/resolver/solver_manager_test.go +++ b/challenge/resolver/solver_manager_test.go @@ -5,54 +5,30 @@ import ( "crypto/rsa" "fmt" "io/ioutil" - "net" "net/http" - "reflect" + "sort" "testing" - "unsafe" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme/api" - "github.com/xenolf/lego/challenge" - "github.com/xenolf/lego/challenge/http01" "github.com/xenolf/lego/platform/tester" "gopkg.in/square/go-jose.v2" ) -func TestSolverManager_SetHTTP01Address(t *testing.T) { - _, apiURL, tearDown := tester.SetupFakeAPI() - defer tearDown() +func TestByType(t *testing.T) { + challenges := []acme.Challenge{ + {Type: "dns-01"}, {Type: "tlsalpn-01"}, {Type: "http-01"}, + } - keyBits := 32 // small value keeps test fast - key, err := rsa.GenerateKey(rand.Reader, keyBits) - require.NoError(t, err, "Could not generate test key") + sort.Sort(byType(challenges)) - core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) - require.NoError(t, err) + expected := []acme.Challenge{ + {Type: "tlsalpn-01"}, {Type: "http-01"}, {Type: "dns-01"}, + } - solversManager := NewSolversManager(core) - - optPort := "1234" - optHost := "" - - err = solversManager.SetHTTP01Address(net.JoinHostPort(optHost, optPort)) - require.NoError(t, err) - - require.IsType(t, &http01.Challenge{}, solversManager.solvers[challenge.HTTP01]) - httpSolver := solversManager.solvers[challenge.HTTP01].(*http01.Challenge) - - httpProviderServer := (*http01.ProviderServer)(unsafe.Pointer(reflect.ValueOf(httpSolver).Elem().FieldByName("provider").InterfaceData()[1])) - assert.Equal(t, net.JoinHostPort(optHost, optPort), httpProviderServer.GetAddress()) - - // test setting different host - optHost = "127.0.0.1" - err = solversManager.SetHTTP01Address(net.JoinHostPort(optHost, optPort)) - require.NoError(t, err) - - httpProviderServer = (*http01.ProviderServer)(unsafe.Pointer(reflect.ValueOf(httpSolver).Elem().FieldByName("provider").InterfaceData()[1])) - assert.Equal(t, net.JoinHostPort(optHost, optPort), httpProviderServer.GetAddress()) + assert.Equal(t, expected, challenges) } func TestValidate(t *testing.T) { diff --git a/cmd/cmd_dnshelp.go b/cmd/cmd_dnshelp.go index 027d7ade..2caa0a71 100644 --- a/cmd/cmd_dnshelp.go +++ b/cmd/cmd_dnshelp.go @@ -107,7 +107,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tglesys:\tGLESYS_POLLING_INTERVAL, GLESYS_PROPAGATION_TIMEOUT, GLESYS_TTL, GLESYS_HTTP_TIMEOUT") fmt.Fprintln(w, "\tgodaddy:\tGODADDY_POLLING_INTERVAL, GODADDY_PROPAGATION_TIMEOUT, GODADDY_TTL, GODADDY_HTTP_TIMEOUT, GODADDY_SEQUENCE_INTERVAL") fmt.Fprintln(w, "\thostingde:\tHOSTINGDE_POLLING_INTERVAL, HOSTINGDE_PROPAGATION_TIMEOUT, HOSTINGDE_TTL, HOSTINGDE_HTTP_TIMEOUT") - fmt.Fprintln(w, "\thttpreq:\t,HTTPREQ_POLLING_INTERVAL, HTTPREQ_PROPAGATION_TIMEOUT, HTTPREQ_HTTP_TIMEOUT") + fmt.Fprintln(w, "\thttpreq:\tHTTPREQ_POLLING_INTERVAL, HTTPREQ_PROPAGATION_TIMEOUT, HTTPREQ_HTTP_TIMEOUT") fmt.Fprintln(w, "\tiij:\tIIJ_POLLING_INTERVAL, IIJ_PROPAGATION_TIMEOUT, IIJ_TTL") fmt.Fprintln(w, "\tinwx:\tINWX_POLLING_INTERVAL, INWX_PROPAGATION_TIMEOUT, INWX_TTL, INWX_SANDBOX") fmt.Fprintln(w, "\tlightsail:\tLIGHTSAIL_POLLING_INTERVAL, LIGHTSAIL_PROPAGATION_TIMEOUT") diff --git a/cmd/flags.go b/cmd/flags.go index f5783dba..dbca98ab 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -13,8 +13,8 @@ func CreateFlags(defaultPath string) []cli.Flag { }, cli.StringFlag{ Name: "server, s", - Value: lego.LEDirectoryProduction, 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: "accept-tos, a", @@ -26,7 +26,7 @@ func CreateFlags(defaultPath string) []cli.Flag { }, cli.StringFlag{ Name: "csr, c", - Usage: "Certificate signing request filename, if an external CSR is to be used", + Usage: "Certificate signing request filename, if an external CSR is to be used.", }, cli.BoolFlag{ Name: "eab", @@ -43,56 +43,63 @@ func CreateFlags(defaultPath string) []cli.Flag { cli.StringFlag{ Name: "key-type, k", Value: "rsa2048", - Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384", + Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384.", }, cli.StringFlag{ Name: "filename", - Usage: "Filename of the generated certificate", + Usage: "(deprecated) Filename of the generated certificate.", }, cli.StringFlag{ Name: "path", - Usage: "Directory to use for storing the data", + Usage: "Directory to use for storing the data.", Value: defaultPath, }, - cli.StringSliceFlag{ - Name: "exclude, x", - Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"dns-01\", \"tls-alpn-01\".", - }, - cli.IntFlag{ - Name: "http-timeout", - Usage: "Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds.", + cli.BoolFlag{ + Name: "http", + Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.", }, cli.StringFlag{ - Name: "webroot", - Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge", + Name: "http.port", + Usage: "Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port.", + Value: ":80", + }, + cli.StringFlag{ + Name: "http.webroot", + Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge.", }, cli.StringSliceFlag{ - Name: "memcached-host", + Name: "http.memcached-host", Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.", }, - cli.StringFlag{ - Name: "http", - Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", + cli.BoolFlag{ + Name: "tls", + Usage: "Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.", }, cli.StringFlag{ - Name: "tls", - Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port", + Name: "tls.port", + Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port.", + Value: ":443", }, cli.StringFlag{ Name: "dns", - Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.", + Usage: "Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.", }, cli.BoolFlag{ - Name: "dns-disable-cp", + Name: "dns.disable-cp", Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.", }, cli.StringSliceFlag{ - Name: "dns-resolvers", + Name: "dns.resolvers", Usage: "Set the resolvers to use for performing recursive DNS queries. 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: "http-timeout", + Usage: "Set the HTTP timeout value to a specific value in seconds.", + }, cli.IntFlag{ Name: "dns-timeout", - Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. The default is 10 seconds.", + Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries.", + Value: 10, }, cli.BoolFlag{ Name: "pem", diff --git a/cmd/lego/main.go b/cmd/lego/main.go index 9d0d0af8..6a80fbf4 100644 --- a/cmd/lego/main.go +++ b/cmd/lego/main.go @@ -3,8 +3,10 @@ package main import ( + "fmt" "os" "path/filepath" + "runtime" "github.com/urfave/cli" "github.com/xenolf/lego/cmd" @@ -20,7 +22,12 @@ func main() { app.Name = "lego" app.HelpName = "lego" app.Usage = "Let's Encrypt client written in Go" + app.EnableBashCompletion = true + app.Version = version + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH) + } defaultPath := "" cwd, err := os.Getwd() diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index f1b59084..57cba52c 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -1,12 +1,15 @@ package cmd import ( + "net" "strings" "time" "github.com/urfave/cli" "github.com/xenolf/lego/challenge" "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/challenge/http01" + "github.com/xenolf/lego/challenge/tlsalpn01" "github.com/xenolf/lego/lego" "github.com/xenolf/lego/log" "github.com/xenolf/lego/providers/dns" @@ -15,24 +18,22 @@ import ( ) func setupChallenges(ctx *cli.Context, client *lego.Client) { - if len(ctx.GlobalStringSlice("exclude")) > 0 { - excludedSolvers(ctx, client) + if !ctx.GlobalBool("http") && !ctx.GlobalBool("tls") && !ctx.GlobalIsSet("dns") { + log.Fatal("No challenge selected. You must specify at least one challenge: `--http`, `--tls`, `--dns`.") } - if ctx.GlobalIsSet("webroot") { - setupWebroot(client, ctx.GlobalString("webroot")) + if ctx.GlobalBool("http") { + err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx)) + if err != nil { + log.Fatal(err) + } } - if ctx.GlobalIsSet("memcached-host") { - setupMemcached(client, ctx.GlobalStringSlice("memcached-host")) - } - - if ctx.GlobalIsSet("http") { - setupHTTP(client, ctx.GlobalString("http")) - } - - if ctx.GlobalIsSet("tls") { - setupTLS(client, ctx.GlobalString("tls")) + if ctx.GlobalBool("tls") { + err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx)) + if err != nil { + log.Fatal(err) + } } if ctx.GlobalIsSet("dns") { @@ -40,65 +41,59 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) { } } -func excludedSolvers(ctx *cli.Context, client *lego.Client) { - var cc []challenge.Type - for _, s := range ctx.GlobalStringSlice("exclude") { - cc = append(cc, challenge.Type(s)) - } - client.Challenge.Exclude(cc) -} +func setupHTTPProvider(ctx *cli.Context) challenge.Provider { + switch { + case ctx.GlobalIsSet("http.webroot"): + ps, err := webroot.NewHTTPProvider(ctx.GlobalString("http.webroot")) + if err != nil { + log.Fatal(err) + } + return ps + case ctx.GlobalIsSet("http.memcached-host"): + ps, err := memcached.NewMemcachedProvider(ctx.GlobalStringSlice("http.memcached-host")) + if err != nil { + log.Fatal(err) + } + return ps + case ctx.GlobalIsSet("http.port"): + iface := ctx.GlobalString("http.port") + if !strings.Contains(iface, ":") { + log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") + } -func setupWebroot(client *lego.Client, path string) { - provider, err := webroot.NewHTTPProvider(path) - if err != nil { - log.Fatal(err) - } + host, port, err := net.SplitHostPort(iface) + if err != nil { + log.Fatal(err) + } - err = client.Challenge.SetHTTP01Provider(provider) - if err != nil { - log.Fatal(err) - } - - // --webroot=foo indicates that the user specifically want to do a HTTP challenge - // infer that the user also wants to exclude all other challenges - client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) -} - -func setupMemcached(client *lego.Client, hosts []string) { - provider, err := memcached.NewMemcachedProvider(hosts) - if err != nil { - log.Fatal(err) - } - - err = client.Challenge.SetHTTP01Provider(provider) - if err != nil { - log.Fatal(err) - } - - // --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge - // infer that the user also wants to exclude all other challenges - client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) -} - -func setupHTTP(client *lego.Client, iface string) { - if !strings.Contains(iface, ":") { - log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") - } - - err := client.Challenge.SetHTTP01Address(iface) - if err != nil { - log.Fatal(err) + return http01.NewProviderServer(host, port) + case ctx.GlobalBool("http"): + return http01.NewProviderServer("", "") + default: + log.Fatal("Invalid HTTP challenge options.") + return nil } } -func setupTLS(client *lego.Client, iface string) { - if !strings.Contains(iface, ":") { - log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") - } +func setupTLSProvider(ctx *cli.Context) challenge.Provider { + switch { + case ctx.GlobalIsSet("tls.port"): + iface := ctx.GlobalString("tls.port") + if !strings.Contains(iface, ":") { + log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") + } - err := client.Challenge.SetTLSALPN01Address(iface) - if err != nil { - log.Fatal(err) + host, port, err := net.SplitHostPort(iface) + if err != nil { + log.Fatal(err) + } + + return tlsalpn01.NewProviderServer(host, port) + case ctx.GlobalBool("tls"): + return tlsalpn01.NewProviderServer("", "") + default: + log.Fatal("Invalid HTTP challenge options.") + return nil } } @@ -108,11 +103,11 @@ func setupDNS(ctx *cli.Context, client *lego.Client) { log.Fatal(err) } - servers := ctx.GlobalStringSlice("dns-resolvers") + servers := ctx.GlobalStringSlice("dns.resolvers") err = client.Challenge.SetDNS01Provider(provider, dns01.CondOption(len(servers) > 0, - dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns-resolvers")))), - dns01.CondOption(ctx.GlobalIsSet("dns-disable-cp"), + dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns.resolvers")))), + dns01.CondOption(ctx.GlobalIsSet("dns.disable-cp"), dns01.DisableCompletePropagationRequirement()), dns01.CondOption(ctx.GlobalIsSet("dns-timeout"), dns01.AddDNSTimeout(time.Duration(ctx.GlobalInt("dns-timeout"))*time.Second)), diff --git a/e2e/challenges_test.go b/e2e/challenges_test.go index af038abb..c6df3133 100644 --- a/e2e/challenges_test.go +++ b/e2e/challenges_test.go @@ -13,7 +13,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xenolf/lego/certificate" - "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/http01" + "github.com/xenolf/lego/challenge/tlsalpn01" "github.com/xenolf/lego/e2e/loader" "github.com/xenolf/lego/lego" "github.com/xenolf/lego/registration" @@ -50,12 +51,10 @@ func TestChallengeHTTP_Run(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "tls-alpn-01", "-s", "https://localhost:14000/dir", "-d", "acme.wtf", - "--http", ":5002", - "--tls", ":5001", + "--http", + "--http.port", ":5002", "run") if len(output) > 0 { @@ -72,12 +71,10 @@ func TestChallengeTLS_Run_Domains(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-d", "acme.wtf", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "run") if len(output) > 0 { @@ -94,12 +91,10 @@ func TestChallengeTLS_Run_CSR(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-csr", "./fixtures/csr.raw", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "run") if len(output) > 0 { @@ -116,12 +111,10 @@ func TestChallengeTLS_Run_CSR_PEM(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-csr", "./fixtures/csr.cert", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "run") if len(output) > 0 { @@ -138,13 +131,11 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-d", "lego.wtf", "-d", "acme.lego.wtf", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "run") if len(output) > 0 { @@ -157,12 +148,10 @@ func TestChallengeTLS_Run_Revoke(t *testing.T) { output, err = load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-d", "lego.wtf", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "revoke") if len(output) > 0 { @@ -179,12 +168,10 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-d", "légô.wtf", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "run") if len(output) > 0 { @@ -197,12 +184,10 @@ func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { output, err = load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "dns-01", - "-x", "http-01", "-s", "https://localhost:14000/dir", "-d", "légô.wtf", - "--http", ":5002", - "--tls", ":5001", + "--tls", + "--tls.port", ":5001", "revoke") if len(output) > 0 { @@ -228,8 +213,7 @@ func TestChallengeHTTP_Client_Obtain(t *testing.T) { client, err := lego.NewClient(config) require.NoError(t, err) - client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) - err = client.Challenge.SetHTTP01Address(":5002") + err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) @@ -267,8 +251,7 @@ func TestChallengeTLS_Client_Obtain(t *testing.T) { client, err := lego.NewClient(config) require.NoError(t, err) - client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.HTTP01}) - err = client.Challenge.SetTLSALPN01Address(":5001") + err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) @@ -307,8 +290,7 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { client, err := lego.NewClient(config) require.NoError(t, err) - client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.HTTP01}) - err = client.Challenge.SetTLSALPN01Address(":5001") + err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001")) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) diff --git a/e2e/dnschallenge/dns_challenges_test.go b/e2e/dnschallenge/dns_challenges_test.go index f067f0e3..461f5cad 100644 --- a/e2e/dnschallenge/dns_challenges_test.go +++ b/e2e/dnschallenge/dns_challenges_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xenolf/lego/certificate" - "github.com/xenolf/lego/challenge" "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/e2e/loader" "github.com/xenolf/lego/lego" @@ -55,16 +54,12 @@ func TestChallengeDNS_Run(t *testing.T) { output, err := load.RunLego( "-m", "hubert@hubert.com", "--accept-tos", - "-x", "http-01", - "-x", "tls-alpn-01", - "--dns-disable-cp", - "--dns-resolvers", ":8053", "--dns", "exec", + "--dns.resolvers", ":8053", + "--dns.disable-cp", "-s", "https://localhost:15000/dir", "-d", "*.légo.acme", "-d", "légo.acme", - "--http", ":5004", - "--tls", ":5003", "run") if len(output) > 0 { @@ -100,7 +95,6 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) { err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{":8053"}), dns01.DisableCompletePropagationRequirement()) - client.Challenge.Exclude([]challenge.Type{challenge.HTTP01, challenge.TLSALPN01}) require.NoError(t, err) reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})