diff --git a/acme/client.go b/acme/client.go index eb36700d..f07d35fd 100644 --- a/acme/client.go +++ b/acme/client.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "encoding/json" "errors" + "fmt" "io/ioutil" "log" "net/http" @@ -112,6 +113,92 @@ func (c *Client) Register() (*RegistrationResource, error) { return reg, nil } +// AgreeToTos updates the Client registration and sends the agreement to +// the server. +func (c *Client) AgreeToTos() error { + c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL + jsonBytes, err := json.Marshal(&c.user.GetRegistration().Body) + if err != nil { + return err + } + + logger().Printf("Agreement: %s", string(jsonBytes)) + + resp, err := c.jwsPost(c.user.GetRegistration().URI, jsonBytes) + if err != nil { + return err + } + + logResponseBody(resp) + + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("The server returned %d but we expected %d", resp.StatusCode, http.StatusAccepted) + } + + logResponseHeaders(resp) + logResponseBody(resp) + + return nil +} + +// ObtainCertificates tries to obtain certificates from the CA server +// using the challenges it has configured. It also tries to do multiple +// certificate processings at the same time in parallel. +func (c *Client) ObtainCertificates(domains []string) error { + + resc, errc := make(chan *authorizationResource), make(chan error) + for _, domain := range domains { + go func(domain string) { + jsonBytes, err := json.Marshal(authorization{Identifier: identifier{Type: "dns", Value: domain}}) + if err != nil { + errc <- err + return + } + + resp, err := c.jwsPost(c.user.GetRegistration().NewAuthzURL, jsonBytes) + if err != nil { + errc <- err + return + } + + if resp.StatusCode != http.StatusCreated { + errc <- fmt.Errorf("Getting challenges for %s failed. Got status %d but expected %d", + domain, resp.StatusCode, http.StatusCreated) + } + + links := parseLinks(resp.Header["Link"]) + if links["next"] == "" { + logger().Fatalln("The server did not provide enough information to proceed.") + } + + var authz authorization + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&authz) + if err != nil { + errc <- err + } + + resc <- &authorizationResource{Body: authz, NewCertURL: links["next"], Domain: domain} + + }(domain) + } + + var responses []*authorizationResource + for i := 0; i < len(domains); i++ { + select { + case res := <-resc: + responses = append(responses, res) + case err := <-errc: + logger().Printf("%v", err) + } + } + + close(resc) + close(errc) + + return nil +} + func logResponseHeaders(resp *http.Response) { logger().Println(resp.Status) for k, v := range resp.Header { diff --git a/acme/messages.go b/acme/messages.go index 5f53b400..c8cdc36a 100644 --- a/acme/messages.go +++ b/acme/messages.go @@ -1,5 +1,7 @@ package acme +import "time" + type registrationMessage struct { Contact []string `json:"contact"` } @@ -15,6 +17,7 @@ type Registration struct { } `json:"key"` Recoverytoken string `json:"recoveryToken"` Contact []string `json:"contact"` + Agreement string `json:"agreement,omitempty"` } // RegistrationResource represents all important informations about a registration @@ -25,3 +28,29 @@ type RegistrationResource struct { NewAuthzURL string TosURL string } + +type authorizationResource struct { + Body authorization + Domain string + NewCertURL string +} + +type authorization struct { + Identifier identifier `json:"identifier"` + Status string `json:"status,omitempty"` + Expires time.Time `json:"expires,omitempty"` + Challenges []challenge `json:"challenges,omitempty"` + Combinations [][]int `json:"combinations,omitempty"` +} + +type identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + +type challenge struct { + Type string `json:"type"` + Status string `json:"status"` + URI string `json:"uri"` + Token string `json:"token"` +} diff --git a/cli.go b/cli.go index b45a9309..acccb33b 100644 --- a/cli.go +++ b/cli.go @@ -5,7 +5,6 @@ import ( "os" "github.com/codegangsta/cli" - "github.com/xenolf/lego/acme" ) // Logger is used to log errors; if nil, the default log.Logger is used. @@ -138,55 +137,3 @@ func main() { app.Run(os.Args) } - -func checkFolder(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - return os.MkdirAll(path, 0700) - } - return nil -} - -func run(c *cli.Context) { - err := checkFolder(c.GlobalString("config-dir")) - if err != nil { - logger().Fatalf("Cound not check/create path: %v", err) - } - - conf := NewConfiguration(c) - - //TODO: move to account struct? Currently MUST pass email. - if !c.GlobalIsSet("email") { - logger().Fatal("You have to pass an account (email address) to the program using --email or -m") - } - - acc := NewAccount(c.GlobalString("email"), conf) - client := acme.NewClient(c.GlobalString("server"), acc) - if acc.Registration == nil { - reg, err := client.Register() - if err != nil { - logger().Fatalf("Could not complete registration -> %v", err) - } - - acc.Registration = reg - acc.Save() - - logger().Print("!!!! HEADS UP !!!!") - logger().Printf(` - Your account credentials have been saved in your Let's Encrypt - configuration directory at "%s". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal. - - If you lose your account credentials, you can recover - them using the token - "%s". - You must write that down and put it in a safe place.`, c.GlobalString("config-dir"), reg.Body.Recoverytoken) - } - - if !c.GlobalIsSet("domains") { - logger().Fatal("Please specify --domains") - } - -} diff --git a/cli_handlers.go b/cli_handlers.go new file mode 100644 index 00000000..ba637885 --- /dev/null +++ b/cli_handlers.go @@ -0,0 +1,96 @@ +package main + +import ( + "bufio" + "os" + "strings" + + "github.com/codegangsta/cli" + "github.com/xenolf/lego/acme" +) + +func checkFolder(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return os.MkdirAll(path, 0700) + } + return nil +} + +func run(c *cli.Context) { + err := checkFolder(c.GlobalString("config-dir")) + if err != nil { + logger().Fatalf("Cound not check/create path: %v", err) + } + + conf := NewConfiguration(c) + + //TODO: move to account struct? Currently MUST pass email. + if !c.GlobalIsSet("email") { + logger().Fatal("You have to pass an account (email address) to the program using --email or -m") + } + + acc := NewAccount(c.GlobalString("email"), conf) + client := acme.NewClient(c.GlobalString("server"), acc) + if acc.Registration == nil { + reg, err := client.Register() + if err != nil { + logger().Fatalf("Could not complete registration -> %v", err) + } + + acc.Registration = reg + acc.Save() + + logger().Print("!!!! HEADS UP !!!!") + logger().Printf(` + Your account credentials have been saved in your Let's Encrypt + configuration directory at "%s". + You should make a secure backup of this folder now. This + configuration directory will also contain certificates and + private keys obtained from Let's Encrypt so making regular + backups of this folder is ideal. + + If you lose your account credentials, you can recover + them using the token + "%s". + You must write that down and put it in a safe place.`, c.GlobalString("config-dir"), reg.Body.Recoverytoken) + + } + + if acc.Registration.Body.Agreement == "" { + if !c.GlobalBool("agree-tos") { + reader := bufio.NewReader(os.Stdin) + logger().Printf("Please review the TOS at %s", acc.Registration.TosURL) + + for { + logger().Println("Do you accept the TOS? Y/n") + text, err := reader.ReadString('\n') + if err != nil { + logger().Fatalf("Could not read from console -> %v", err) + } + + text = strings.Trim(text, "\r\n") + + if text == "n" { + logger().Fatal("You did not accept the TOS. Unable to proceed.") + } + + if text == "Y" || text == "y" || text == "" { + err = client.AgreeToTos() + if err != nil { + logger().Fatalf("Could not agree to tos -> %v", err) + } + acc.Save() + break + } + + logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") + } + } + } + + if !c.GlobalIsSet("domains") { + logger().Fatal("Please specify --domains") + } + + client.ObtainCertificates(c.GlobalStringSlice("domains")) +}