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
|
#### 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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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",
|
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",
|
||||||
|
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue