Make solvers configurable.

Allows selecting which solvers are available, and specifying options for them.
This commit is contained in:
Tommie Gannert 2015-12-05 21:01:08 +00:00
parent 039b7c50dc
commit b2c88d7a5d
5 changed files with 68 additions and 15 deletions

View file

@ -16,8 +16,13 @@ import (
"time" "time"
) )
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 is an optional custom logger.
var Logger *log.Logger Logger *log.Logger
)
// logf writes a log entry. It uses Logger if not // logf writes a log entry. It uses Logger if not
// nil, otherwise it uses the default log.Logger. // 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 // 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 // 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. // 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. // If optSolvers is nil, the value of DefaultSolvers is used. If given explicitly,
func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client, error) { // 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() privKey := user.GetPrivateKey()
if privKey == nil { if privKey == nil {
return nil, errors.New("private key was nil") return nil, errors.New("private key was nil")
@ -92,9 +100,31 @@ func NewClient(caDirURL string, user User, keyBits int, optPort string) (*Client
// Add all available solvers with the right index as per ACME // Add all available solvers with the right index as per ACME
// spec to this map. Otherwise they won`t be found. // spec to this map. Otherwise they won`t be found.
solvers := make(map[string]solver) solvers := make(map[string]solver)
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} 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} 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 return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
} }

View file

@ -27,8 +27,7 @@ func TestNewClient(t *testing.T) {
w.Write(data) w.Write(data)
})) }))
caURL, optPort := ts.URL, "1234" client, err := NewClient(ts.URL, user, keyBits, nil)
client, err := NewClient(caURL, user, keyBits, optPort)
if err != nil { if err != nil {
t.Fatalf("Could not create client: %v", err) 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 { if expected, actual := 2, len(client.solvers); actual != expected {
t.Fatalf("Expected %d solver(s), got %d", expected, actual) 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) httpSolver, ok := client.solvers["http-01"].(*httpChallenge)
if !ok { if !ok {

6
cli.go
View file

@ -74,9 +74,9 @@ func main() {
Usage: "Directory to use for storing the data", Usage: "Directory to use for storing the data",
Value: defaultPath, Value: defaultPath,
}, },
cli.StringFlag{ cli.StringSliceFlag{
Name: "port", Name: "solvers, S",
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", Usage: "Add an explicit solver for challenges. Solvers: \"http-01[:port]\", \"tls-sni-01[:port]\".",
}, },
} }

View file

@ -33,7 +33,7 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
//TODO: move to account struct? Currently MUST pass email. //TODO: move to account struct? Currently MUST pass email.
acc := NewAccount(c.GlobalString("email"), conf) 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 { if err != nil {
logger().Fatal("Could not create client:", err) logger().Fatal("Could not create client:", err)
} }

View file

@ -24,8 +24,8 @@ func (c *Configuration) RsaBits() int {
return c.context.GlobalInt("rsa-key-size") return c.context.GlobalInt("rsa-key-size")
} }
func (c *Configuration) OptPort() string { func (c *Configuration) Solvers() []string {
return c.context.GlobalString("port") return c.context.GlobalStringSlice("solvers")
} }
// ServerPath returns the OS dependent path to the data for a specific CA // ServerPath returns the OS dependent path to the data for a specific CA