forked from TrueCloudLab/lego
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:
parent
0dc1b5b7bf
commit
fdf059fbbd
6 changed files with 106 additions and 1 deletions
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
34
acme/http_challenge_webroot.go
Normal file
34
acme/http_challenge_webroot.go
Normal 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
4
cli.go
|
@ -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",
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue