Repository: xenolf/lego (go)

Issue:
--dns flag ignored after core refactor
Just got the latest code (42941ccea6b431ebff203d4cb520991fb7b47951), and tried to run the lego command I usually run to update a cert via dns challenge, but it failed with a bunch of errors related to the tls solver (appearing to ignore the `--dns` flag). I rolled back to the previous commit (4e842a5eb6dcb9520e03db70cd5896f1df14b72a), and everything worked without errors as normal.

This is the command I ran (with a couple `example.com` domain names in place of the dozen or so real domain names I used):
```
export AWS_DEFAULT_REGION=us-west-2
export AWS_ACCESS_KEY_ID=my-id
export AWS_SECRET_ACCESS_KEY=my-key

lego -a --dns=route53 --path=/home/me/my-certs --email=me@example.com --domains=example.com --domains=www.example.com run
```

And this is the output from the failure with the latest code:
```
2018/12/08 15:52:45 [INFO] [example.com, www.example.com] acme: Obtaining bundled SAN certificate
2018/12/08 15:52:46 [INFO] [www.example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz/ABC123
2018/12/08 15:52:46 [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz/DEF456
2018/12/08 15:52:46 [INFO] [www.example.com] acme: Could not find solver for: tls-sni-01
2018/12/08 15:52:46 [INFO] [www.example.com] acme: use tls-alpn-01 solver
2018/12/08 15:52:46 [INFO] [example.com] acme: Could not find solver for: tls-sni-01
2018/12/08 15:52:46 [INFO] [example.com] acme: use tls-alpn-01 solver
2018/12/08 15:52:46 [INFO] [www.example.com] acme: Trying to solve TLS-ALPN-01
2018/12/08 15:52:49 [INFO] [example.com] acme: Trying to solve TLS-ALPN-01
2018/12/08 15:52:51 Could not obtain certificates: acme: Error -> One or more domains had a problem:
[www.example.com] [www.example.com] acme: error presenting token: could not start HTTPS server for challenge -> listen tcp :443: bind: permission denied
[example.com] [example.com] acme: error presenting token: could not start HTTPS server for challenge -> listen tcp :443: bind: permission denied
```

Pull request description:
New challenges management
Add new challenges management:

CLI:
- Remove `--exclude`, `-x` 
- `--http`:  [NEW] enable HTTP challenge
- `--http.port`: previously named `--http`
- `--http.webroot`: previously named `--webroot`
- `--http.memcached-host`: previously named `--memcached-host`
- `--tls`:  [NEW] enable TLS challenge
- `--tls.port`:  previously named `--tls`
- `--dns.disable-cp`:  previously named `--dns-disable-cp`
- `--dns.resolvers`: previously named `--dns-resolvers`

Lib:
- remove `SetHTTP01Address`
- remove `SetTLSALPN01Address`
- remove `Exclude`
- add `Remove`

Fixes #730 

Patch:
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/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.BoolFlag{
+			Name:  "http",
+			Usage: "Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.",
 		},
-		cli.IntFlag{
-			Name:  "http-timeout",
-			Usage: "Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds.",
+		cli.StringFlag{
+			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:  "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.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 setupWebroot(client *lego.Client, path string) {
-	provider, err := webroot.NewHTTPProvider(path)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	err = client.Challenge.SetHTTP01Provider(provider)
-	if err != nil {
-		log.Fatal(err)
+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.")
+		}
+
+		host, port, err := net.SplitHostPort(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
 	}
-
-	// --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)
-	}
-}
-
-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.")
-	}
-
-	err := client.Challenge.SetTLSALPN01Address(iface)
-	if err != nil {
-		log.Fatal(err)
+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.")
+		}
+
+		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)),


Test patch:
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()
-
-	keyBits := 32 // small value keeps test fast
-	key, err := rsa.GenerateKey(rand.Reader, keyBits)
-	require.NoError(t, err, "Could not generate test key")
-
-	core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key)
-	require.NoError(t, err)
-
-	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)
+func TestByType(t *testing.T) {
+	challenges := []acme.Challenge{
+		{Type: "dns-01"}, {Type: "tlsalpn-01"}, {Type: "http-01"},
+	}
 
-	httpProviderServer := (*http01.ProviderServer)(unsafe.Pointer(reflect.ValueOf(httpSolver).Elem().FieldByName("provider").InterfaceData()[1]))
-	assert.Equal(t, net.JoinHostPort(optHost, optPort), httpProviderServer.GetAddress())
+	sort.Sort(byType(challenges))
 
-	// test setting different host
-	optHost = "127.0.0.1"
-	err = solversManager.SetHTTP01Address(net.JoinHostPort(optHost, optPort))
-	require.NoError(t, err)
+	expected := []acme.Challenge{
+		{Type: "tlsalpn-01"}, {Type: "http-01"}, {Type: "dns-01"},
+	}
 
-	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/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})