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 #### Sudo
The CLI does not require root permissions but needs to bind to port 80 and 443 for certain challenges. 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 - 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 `--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 #### Port Usage
By default lego assumes it is able to bind to ports 80 and 443 to solve challenges. 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. --rsa-key-size, -B "2048" Size of the RSA key.
--path "${CWD}/.lego" Directory to use for storing the data --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". --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 --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 --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. --dns Enable the DNS challenge for solving using a provider.

View file

@ -12,6 +12,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -116,6 +117,21 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider)
return nil 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. // 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. // 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. // To only specify a port and no interface use the ":port" notation.

View file

@ -3,6 +3,7 @@ package acme
import ( import (
"crypto/rsa" "crypto/rsa"
"io/ioutil" "io/ioutil"
"os"
"strings" "strings"
"testing" "testing"
) )
@ -54,3 +55,48 @@ func TestHTTPChallengeInvalidPort(t *testing.T) {
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) 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", Name: "exclude, x",
Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"tls-sni-01\".", 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{ cli.StringFlag{
Name: "http", Name: "http",
Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", 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()) client.ExcludeChallenges(conf.ExcludedSolvers())
} }
if c.GlobalIsSet("webroot") {
client.SetWebRoot(c.GlobalString("webroot"))
}
if c.GlobalIsSet("http") { if c.GlobalIsSet("http") {
client.SetHTTPAddress(c.GlobalString("http")) client.SetHTTPAddress(c.GlobalString("http"))
} }