forked from TrueCloudLab/lego
Move CLI handlers to their own file
Implement Tos accept and start obtain certificates
This commit is contained in:
parent
14627c9d51
commit
7aab5562c1
4 changed files with 212 additions and 53 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -112,6 +113,92 @@ func (c *Client) Register() (*RegistrationResource, error) {
|
||||||
return reg, nil
|
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) {
|
func logResponseHeaders(resp *http.Response) {
|
||||||
logger().Println(resp.Status)
|
logger().Println(resp.Status)
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package acme
|
package acme
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type registrationMessage struct {
|
type registrationMessage struct {
|
||||||
Contact []string `json:"contact"`
|
Contact []string `json:"contact"`
|
||||||
}
|
}
|
||||||
|
@ -15,6 +17,7 @@ type Registration struct {
|
||||||
} `json:"key"`
|
} `json:"key"`
|
||||||
Recoverytoken string `json:"recoveryToken"`
|
Recoverytoken string `json:"recoveryToken"`
|
||||||
Contact []string `json:"contact"`
|
Contact []string `json:"contact"`
|
||||||
|
Agreement string `json:"agreement,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistrationResource represents all important informations about a registration
|
// RegistrationResource represents all important informations about a registration
|
||||||
|
@ -25,3 +28,29 @@ type RegistrationResource struct {
|
||||||
NewAuthzURL string
|
NewAuthzURL string
|
||||||
TosURL 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"`
|
||||||
|
}
|
||||||
|
|
53
cli.go
53
cli.go
|
@ -5,7 +5,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/codegangsta/cli"
|
"github.com/codegangsta/cli"
|
||||||
"github.com/xenolf/lego/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is used to log errors; if nil, the default log.Logger is used.
|
// Logger is used to log errors; if nil, the default log.Logger is used.
|
||||||
|
@ -138,55 +137,3 @@ func main() {
|
||||||
|
|
||||||
app.Run(os.Args)
|
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
96
cli_handlers.go
Normal file
96
cli_handlers.go
Normal file
|
@ -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"))
|
||||||
|
}
|
Loading…
Reference in a new issue