From b2c88d7a5d1a94eb7a296219473206fafebc834d Mon Sep 17 00:00:00 2001 From: Tommie Gannert Date: Sat, 5 Dec 2015 21:01:08 +0000 Subject: [PATCH] Make solvers configurable. Allows selecting which solvers are available, and specifying options for them. --- acme/client.go | 44 +++++++++++++++++++++++++++++++++++++------- acme/client_test.go | 27 +++++++++++++++++++++++++-- cli.go | 6 +++--- cli_handlers.go | 2 +- configuration.go | 4 ++-- 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/acme/client.go b/acme/client.go index ef8570c5..777cc8e0 100644 --- a/acme/client.go +++ b/acme/client.go @@ -16,8 +16,13 @@ import ( "time" ) -// Logger is an optional custom logger. -var Logger *log.Logger +var ( + // DefaultSolvers is the set of solvers to use if none is given to NewClient. + DefaultSolvers = []string{"http-01", "tls-sni-01"} + + // Logger is an optional custom logger. + Logger *log.Logger +) // logf writes a log entry. It uses Logger if not // nil, otherwise it uses the default log.Logger. @@ -56,9 +61,12 @@ type Client struct { // the ACME directory located at caDirURL for the rest of its actions. It will // generate private keys for certificates of size keyBits. And, if the challenge // type requires it, the client will open a port at optPort to solve the challenge. -// If optPort is blank, the port required by the spec will be used, but you must -// forward the required port to optPort for the challenge to succeed. -func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client, error) { +// +// If optSolvers is nil, the value of DefaultSolvers is used. If given explicitly, +// it is a set of solver names to enable. The "http-01" and "tls-sni-01" solvers +// take an optional TCP port to listen on after a colon, e.g. "http-01:80". If +// the port is not specified, the port required by the spec will be used. +func NewClient(caDirURL string, user User, keyBits int, optSolvers []string) (*Client, error) { privKey := user.GetPrivateKey() if privKey == nil { return nil, errors.New("private key was nil") @@ -92,8 +100,30 @@ func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client // Add all available solvers with the right index as per ACME // spec to this map. Otherwise they won`t be found. solvers := make(map[string]solver) - solvers["http-01"] = &httpChallenge{jws: jws, validate: validate, optPort: optPort} - solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate, optPort: optPort} + if optSolvers == nil { + optSolvers = DefaultSolvers + } + for _, s := range optSolvers { + ss := strings.SplitN(s, ":", 2) + switch ss[0] { + case "http-01": + optPort := "" + if len(ss) > 1 { + optPort = ss[1] + } + solvers["http-01"] = &httpChallenge{jws: jws, validate: validate, optPort: optPort} + + case "tls-sni-01": + optPort := "" + if len(ss) > 1 { + optPort = ss[1] + } + solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate, optPort: optPort} + + default: + return nil, fmt.Errorf("unknown solver: %s", s) + } + } return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil } diff --git a/acme/client_test.go b/acme/client_test.go index 3718d466..e62e3731 100644 --- a/acme/client_test.go +++ b/acme/client_test.go @@ -27,8 +27,7 @@ func TestNewClient(t *testing.T) { w.Write(data) })) - caURL, optPort := ts.URL, "1234" - client, err := NewClient(caURL, user, keyBits, optPort) + client, err := NewClient(ts.URL, user, keyBits, nil) if err != nil { t.Fatalf("Could not create client: %v", err) } @@ -47,6 +46,30 @@ func TestNewClient(t *testing.T) { if expected, actual := 2, len(client.solvers); actual != expected { t.Fatalf("Expected %d solver(s), got %d", expected, actual) } +} + +func TestNewClientOptPort(t *testing.T) { + keyBits := 32 // small value keeps test fast + key, err := rsa.GenerateKey(rand.Reader, keyBits) + if err != nil { + t.Fatal("Could not generate test key:", err) + } + user := mockUser{ + email: "test@test.com", + regres: new(RegistrationResource), + privatekey: key, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) + w.Write(data) + })) + + optPort := "1234" + client, err := NewClient(ts.URL, user, keyBits, []string{"http-01:" + optPort}) + if err != nil { + t.Fatalf("Could not create client: %v", err) + } httpSolver, ok := client.solvers["http-01"].(*httpChallenge) if !ok { diff --git a/cli.go b/cli.go index 1074989f..ce262367 100644 --- a/cli.go +++ b/cli.go @@ -74,9 +74,9 @@ func main() { Usage: "Directory to use for storing the data", Value: defaultPath, }, - cli.StringFlag{ - Name: "port", - Usage: "Challenges will use this port to listen on. Please make sure to forward port 443 to this port on your machine. Otherwise use setcap on the binary", + cli.StringSliceFlag{ + Name: "solvers, S", + Usage: "Add an explicit solver for challenges. Solvers: \"http-01[:port]\", \"tls-sni-01[:port]\".", }, } diff --git a/cli_handlers.go b/cli_handlers.go index dafa25c9..c6ff4f5e 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -33,7 +33,7 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { //TODO: move to account struct? Currently MUST pass email. acc := NewAccount(c.GlobalString("email"), conf) - client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.OptPort()) + client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits(), conf.Solvers()) if err != nil { logger().Fatal("Could not create client:", err) } diff --git a/configuration.go b/configuration.go index e96896aa..3f39f281 100644 --- a/configuration.go +++ b/configuration.go @@ -24,8 +24,8 @@ func (c *Configuration) RsaBits() int { return c.context.GlobalInt("rsa-key-size") } -func (c *Configuration) OptPort() string { - return c.context.GlobalString("port") +func (c *Configuration) Solvers() []string { + return c.context.GlobalStringSlice("solvers") } // ServerPath returns the OS dependent path to the data for a specific CA