pebble/cmd/pebble-client/main.go

318 lines
6.4 KiB
Go

package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"github.com/letsencrypt/pebble/cmd"
"gopkg.in/square/go-jose.v1"
)
const (
version = "0.0.1"
userAgentBase = "pebble-client"
locale = "en-us"
)
func userAgent() string {
return fmt.Sprintf(
"%s %s (%s; %s)",
userAgentBase, version, runtime.GOOS, runtime.GOARCH)
}
type client struct {
server *url.URL
directory map[string]interface{}
email string
acctID string
http *http.Client
signer jose.Signer
nonce string
}
func newClient(server, email string) (*client, error) {
url, err := url.Parse(server)
if err != nil {
return nil, err
}
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
signer, err := jose.NewSigner(jose.RS256, privKey)
if err != nil {
return nil, err
}
c := &client{
server: url,
email: email,
http: &http.Client{},
signer: signer,
}
err = c.updateDirectory()
if err != nil {
return nil, err
}
err = c.updateNonce()
if err != nil {
return nil, err
}
signer.SetNonceSource(c)
err = c.register()
if err != nil {
return nil, err
}
return c, nil
}
func (c *client) sign(data []byte) (*jose.JsonWebSignature, error) {
signed, err := c.signer.Sign(data)
if err != nil {
return nil, err
}
return signed, nil
}
func (c *client) updateDirectory() error {
fmt.Printf("Requesting directory from %q\n", c.server.String())
respBody, _, err := c.getAPI(c.server.String())
if err != nil {
return err
}
var directory map[string]interface{}
err = json.Unmarshal(respBody, &directory)
if err != nil {
return err
}
c.directory = directory
return nil
}
func (c *client) updateNonce() error {
nonceURL := c.directory["new-nonce"].(string)
if nonceURL == "" {
return fmt.Errorf("Missing \"new-nonce\" entry in server directory")
}
fmt.Printf("Requesting nonce from %q\n", nonceURL)
before := c.nonce
_, _, err := c.getAPI(nonceURL)
if err != nil {
return err
}
after := c.nonce
if before == after {
return fmt.Errorf("Did not recieve a fresh nonce from new-nonce URL")
}
return nil
}
func (c *client) register() error {
regURL := c.directory["new-reg"].(string)
if regURL == "" {
return fmt.Errorf("Missing \"new-reg\" entry in server directory")
}
fmt.Printf("Registering new account with %q\n", regURL)
reqBody := struct {
ToSAgreed bool `json:"terms-of-service-agreed"`
Contact []string
Resource string
}{
ToSAgreed: true,
Contact: []string{"mailto:" + c.email},
Resource: "new-reg",
}
reqBodyStr, err := json.Marshal(&reqBody)
if err != nil {
return err
}
_, resp, err := c.postAPI(regURL, reqBodyStr)
if err != nil {
return err
}
locHeader := resp.Header.Get("Location")
if locHeader == "" {
return fmt.Errorf("No 'location' header with account URL in response")
}
c.acctID = locHeader
return nil
}
// Nonce satisfies the JWS "NonceSource" interface
func (c *client) Nonce() (string, error) {
n := c.nonce
err := c.updateNonce()
if err != nil {
return n, err
}
return n, nil
}
func (c *client) doReq(req *http.Request) ([]byte, *http.Response, error) {
resp, err := c.http.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
if n := resp.Header.Get("Replay-Nonce"); n != "" {
c.nonce = n
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
return respBody, resp, nil
}
func (c *client) getAPI(url string) ([]byte, *http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, err
}
req.Header.Set("User-Agent", userAgent())
req.Header.Set("Accept-Language", locale)
return c.doReq(req)
}
func (c *client) postAPI(url string, body []byte) ([]byte, *http.Response, error) {
signedBody, err := c.sign(body)
if err != nil {
return nil, nil, err
}
bodyBuf := bytes.NewBuffer([]byte(signedBody.FullSerialize()))
req, err := http.NewRequest("POST", url, bodyBuf)
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", "application/jose+json")
req.Header.Set("User-Agent", userAgent())
req.Header.Set("Accept-Language", locale)
return c.doReq(req)
}
func (c *client) endpoints() []string {
res := make([]string, 0, len(c.directory))
for k := range c.directory {
res = append(res, k)
}
return res
}
func (c *client) readEndpoint() (string, error) {
var endpoint string
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("$> Enter a directory endpoint to POST: ")
for scanner.Scan() {
line := scanner.Text()
if line == "" || line == "exit" || line == "q" {
break
}
if _, ok := c.directory[line]; !ok {
fmt.Printf("Unknown directory endpoint: %q.\nAvailable choices: %s\n",
line, strings.Join(c.endpoints(), ", "))
fmt.Printf("$> Enter a directory endpoint to POST: ")
continue
}
endpoint = c.directory[line].(string)
break
}
if err := scanner.Err(); err != nil {
return endpoint, err
}
return strings.TrimSpace(endpoint), nil
}
func (c *client) readJSON() ([]byte, error) {
var jsonBuf string
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("$> Enter JSON body, empty line to finish : \n")
for scanner.Scan() {
line := scanner.Text()
if line == "" || line == "exit" || line == "q" {
break
}
jsonBuf += line
}
var indented bytes.Buffer
err := json.Indent(&indented, []byte(jsonBuf), "", " ")
return indented.Bytes(), err
}
func (c *client) repl() error {
for {
endpoint, err := c.readEndpoint()
if err != nil {
return err
}
if endpoint == "" {
break
}
body, err := c.readJSON()
if err != nil {
return err
}
respBody, resp, err := c.postAPI(endpoint, body)
if err != nil {
return err
}
var indented bytes.Buffer
err = json.Indent(&indented, respBody, "", " ")
fmt.Printf("Response:\n%#v\n\n%s\n", resp, indented.String())
}
fmt.Println("Goodbye")
return nil
}
func main() {
server := flag.String("server", "http://localhost:14000/dir", "Directory address for Pebble server")
email := flag.String("email", "", "Email address for ACME registration contact")
flag.Parse()
fmt.Println("welcome to the pebble shell")
c, err := newClient(*server, *email)
cmd.FailOnError(err,
fmt.Sprintf("Failed to make new pebble client with email %q", *email))
fmt.Printf("Your account ID is %q\n", c.acctID)
fmt.Println("Starting REPL environment...\n\n")
err = c.repl()
cmd.FailOnError(err, "REPL error")
}