Added a --webroot option for HTTP challenge

When using this option, the challenge will be written in a file in
".well-known/acme-challenge/" inside the given webroot folder.
This allows lego to work without binding any port at all.
This commit is contained in:
Adrien Carbonne 2016-02-10 12:19:29 +01:00
parent 0dc1b5b7bf
commit fdf059fbbd
6 changed files with 106 additions and 1 deletions

View file

@ -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 `--webport` 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 Enable the DNS challenge for solving using a provider.

View file

@ -12,6 +12,7 @@ import (
"io/ioutil"
"log"
"net"
"os"
"regexp"
"strconv"
"strings"
@ -116,6 +117,21 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider)
return nil
}
// SetWebRoot specifies a custom folder path to be used for HTTP based challenges.
// If this option is used, lego will not bind any port to listen to,
// instead it will write the challenge in a file into path/.well-known/acme-challenge/
func (c *Client) SetWebRoot(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return err
}
if chlng, ok := c.solvers[HTTP01]; ok {
chlng.(*httpChallenge).provider = &httpChallengeWebRoot{path: path}
}
return nil
}
// SetHTTPAddress 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.

View file

@ -3,6 +3,7 @@ package acme
import (
"crypto/rsa"
"io/ioutil"
"os"
"strings"
"testing"
)
@ -54,3 +55,48 @@ func TestHTTPChallengeInvalidPort(t *testing.T) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
}
}
func TestHTTPChallengeWebRoot(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 512)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
challengeFilePath := "webroot/.well-known/acme-challenge/" + chlng.Token
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 {
return err
}
dataStr := string(data)
if dataStr != chlng.KeyAuthorization {
t.Errorf("Challenge file content: got %q, want %q", dataStr, chlng.KeyAuthorization)
}
return nil
}
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &httpChallengeWebRoot{path: "webroot"}}
os.MkdirAll("webroot/.well-known/acme-challenge", 0777)
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
t.Errorf("Solve error: got %v, want nil", err)
}
defer os.RemoveAll("webroot")
}
func TestHTTPChallengeWebRootInvalidPath(t *testing.T) {
privKey, _ := generatePrivateKey(rsakey, 128)
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &httpChallengeWebRoot{path: "/invalid-path-123456"}}
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
t.Errorf("Solve error: got %v, want error", err)
} else if want := "Could not write file in webroot"; !strings.Contains(err.Error(), want) {
t.Errorf("Solve error: got %q, want content %q", err.Error(), want)
}
}

View file

@ -0,0 +1,34 @@
package acme
import (
"fmt"
"io/ioutil"
"os"
"path"
)
// httpChallengeWebRoot implements ChallengeProvider for `http-01` challenge
type httpChallengeWebRoot struct {
path string
}
// Present makes the token available at `HTTP01ChallengePath(token)`
func (w *httpChallengeWebRoot) Present(domain, token, keyAuth string) error {
var err error
err = ioutil.WriteFile(path.Join(w.path, HTTP01ChallengePath(token)), []byte(keyAuth), 0777)
if err != nil {
return fmt.Errorf("Could not write file in webroot for HTTP challenge -> %v", err)
}
return nil
}
func (w *httpChallengeWebRoot) CleanUp(domain, token, keyAuth string) error {
var err error
err = os.Remove(path.Join(w.path, HTTP01ChallengePath(token)))
if err != nil {
return fmt.Errorf("Could not remove file in webroot after HTTP challenge -> %v", err)
}
return nil
}

4
cli.go
View file

@ -102,6 +102,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",

View file

@ -43,6 +43,9 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
client.ExcludeChallenges(conf.ExcludedSolvers())
}
if c.GlobalIsSet("webroot") {
client.SetWebRoot(c.GlobalString("webroot"))
}
if c.GlobalIsSet("http") {
client.SetHTTPAddress(c.GlobalString("http"))
}