diff --git a/README.md b/README.md index 63525a64..7208a1c2 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,11 @@ When using the standard `--path` option, all certificates and account configurat #### Sudo The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges. -To run the CLI without sudo, you have two options: +To run the CLI without sudo, you have three 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. #### Port Usage By default lego assumes it is able to bind to ports 80 and 443 to solve challenges. @@ -87,6 +88,7 @@ GLOBAL OPTIONS: --rsa-key-size, -B "2048" Size of the RSA key. --path "${CWD}/.lego" Directory to use for storing the data --exclude, -x [--exclude option --exclude option] Explicitly disallow solvers by name from being used. Solvers: "http-01", "tls-sni-01". + --webroot Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge --http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port --tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port --dns Solve a DNS challenge using the specified provider. Disables all other solvers. diff --git a/cli.go b/cli.go index 27113aa6..83360a45 100644 --- a/cli.go +++ b/cli.go @@ -116,6 +116,10 @@ func main() { Name: "exclude, x", Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\".", }, + 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", + }, cli.StringFlag{ Name: "http", Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", diff --git a/cli_handlers.go b/cli_handlers.go index 28c96d8b..0ef89441 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -16,6 +16,7 @@ import ( "github.com/xenolf/lego/providers/dns/dnsimple" "github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/route53" + "github.com/xenolf/lego/providers/http/webroot" ) func checkFolder(path string) error { @@ -53,6 +54,18 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { client.ExcludeChallenges(conf.ExcludedSolvers()) } + if c.GlobalIsSet("webroot") { + provider, err := webroot.NewHTTPProviderWebroot(c.GlobalString("webroot")) + if err != nil { + logger().Fatal(err) + } + + client.SetChallengeProvider(acme.HTTP01, provider) + + // --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.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) + } if c.GlobalIsSet("http") { if strings.Index(c.GlobalString("http"), ":") == -1 { logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") diff --git a/providers/http/webroot/webroot.go b/providers/http/webroot/webroot.go new file mode 100644 index 00000000..823f1260 --- /dev/null +++ b/providers/http/webroot/webroot.go @@ -0,0 +1,58 @@ +// Package webroot implements a HTTP provider for solving the HTTP-01 challenge using web server's root path. +package webroot + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + "github.com/xenolf/lego/acme" +) + +// HTTPProviderWebroot implements ChallengeProvider for `http-01` challenge +type HTTPProviderWebroot struct { + path string +} + +// NewHTTPProviderWebroot returns a HTTPProviderWebroot instance with a configured webroot path +func NewHTTPProviderWebroot(path string) (*HTTPProviderWebroot, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("Webroot path does not exist") + } + + c := &HTTPProviderWebroot{ + path: path, + } + + return c, nil +} + +// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path +func (w *HTTPProviderWebroot) Present(domain, token, keyAuth string) error { + var err error + + challengeFilePath := path.Join(w.path, acme.HTTP01ChallengePath(token)) + err = os.MkdirAll(path.Dir(challengeFilePath), 0777) + if err != nil { + return fmt.Errorf("Could not create required directories in webroot for HTTP challenge -> %v", err) + } + + err = ioutil.WriteFile(challengeFilePath, []byte(keyAuth), 0777) + if err != nil { + return fmt.Errorf("Could not write file in webroot for HTTP challenge -> %v", err) + } + + return nil +} + +// CleanUp removes the file created for the challenge +func (w *HTTPProviderWebroot) CleanUp(domain, token, keyAuth string) error { + var err error + err = os.Remove(path.Join(w.path, acme.HTTP01ChallengePath(token))) + if err != nil { + return fmt.Errorf("Could not remove file in webroot after HTTP challenge -> %v", err) + } + + return nil +} diff --git a/providers/http/webroot/webroot_test.go b/providers/http/webroot/webroot_test.go new file mode 100644 index 00000000..755d947b --- /dev/null +++ b/providers/http/webroot/webroot_test.go @@ -0,0 +1,46 @@ +package webroot + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestHTTPProviderWebRoot(t *testing.T) { + webroot := "webroot" + domain := "domain" + token := "token" + keyAuth := "keyAuth" + challengeFilePath := webroot + "/.well-known/acme-challenge/" + token + + os.MkdirAll(webroot+"/.well-known/acme-challenge", 0777) + defer os.RemoveAll(webroot) + + provider, err := NewHTTPProviderWebroot(webroot) + if err != nil { + t.Errorf("Webroot provider error: got %v, want nil", err) + } + + err = provider.Present(domain, token, keyAuth) + if err != nil { + t.Errorf("Webroot provider present() error: got %v, want nil", err) + } + + if _, err := os.Stat(challengeFilePath); os.IsNotExist(err) { + t.Error("Challenge file was not created in webroot") + } + + data, err := ioutil.ReadFile(challengeFilePath) + if err != nil { + t.Errorf("Webroot provider ReadFile() error: got %v, want nil", err) + } + dataStr := string(data) + if dataStr != keyAuth { + t.Errorf("Challenge file content: got %q, want %q", dataStr, keyAuth) + } + + err = provider.CleanUp(domain, token, keyAuth) + if err != nil { + t.Errorf("Webroot provider CleanUp() error: got %v, want nil", err) + } +}