package acme import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "regexp" "time" ) // Link to the next stage type Link map[string]string // Client ... type Client struct { Dir Directory Link Link Location string nonce chan string RetryAfter time.Duration } // NewClient fetches directory and initializes nonce func NewClient(uri string) (*Client, error) { resp, err := http.Get(uri) if err != nil { return nil, err } defer resp.Body.Close() c := &Client{nonce: make(chan string, 10)} c.nonce <- replyNonce(resp) err = json.NewDecoder(resp.Body).Decode(&c.Dir) if err != nil { return nil, err } return c, nil } var errNoNonces = errors.New("No nonces available") // Nonce implements jose nonce provider func (c Client) Nonce() (string, error) { select { case nonce := <-c.nonce: return nonce, nil default: return "", errNoNonces } } // Important header fields // // Replay-Nonce each response, required for next request // Link links to next stage // Retry-After polling interval // Action Request Response // // Register POST new-reg 201 -> reg // Request challenges POST new-authz 201 -> authz // Answer challenges POST challenge 200 // Poll for status GET authz 200 // Request issuance POST new-cert 201 -> cert // Check for new cert GET cert 200 // request is used for // new-reg, new-authz, challenge, new-cert func (c *Client) post(url string, s Signer, v interface{}) error { body, err := json.Marshal(v) if err != nil { return err } log.Println(string(body)) signed, err := s.Sign(body, c) if err != nil { return err } resp, err := http.Post(url, "application/jose+json", signed) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return handleError(resp) } c.nonce <- replyNonce(resp) c.Link = links(resp) c.Location = location(resp) body, err = ioutil.ReadAll(resp.Body) if err != nil { return err } // DEBUG log.Println("RESPONSE", string(body)) return json.Unmarshal(body, v) //return json.NewDecoder(resp.Body).Decode(v) } func location(r *http.Response) string { return r.Header.Get("Location") } func links(r *http.Response) Link { link := make(Link) reg := regexp.MustCompile(`^<(.*)>;rel="(.*)"`) for _, l := range r.Header["Link"] { re := reg.FindStringSubmatch(l) if len(re) == 3 { link[re[2]] = re[1] } } return link } func retryAfter(r *http.Response) time.Duration { ra := r.Header.Get("Retry-After") if d, err := time.ParseDuration(ra + "s"); err == nil { return d } return time.Second } func replyNonce(r *http.Response) string { return r.Header.Get("Replay-Nonce") } /* directory . . .................................................... . . . . . . . . V "next" V "next" V V new-reg ---+----> new-authz ---+----> new-cert revoke-cert . | . | . ^ . | . | . | "revoke" V | V | V | reg* ----+ authz -----+ cert-----------+ . ^ | . | "up" | "up" V | V challenge cert-chain */ func (c *Client) Register(a *Account) (*Registration, error) { r := &Registration{ Resource: ResNewReg, Contact: a.Contact, } err := c.post(c.Dir.NewReg, a, r) return r, err } // Agree to TOS func (c *Client) Agree(a *Account) (*Registration, error) { r := &Registration{ Resource: ResRegister, Contact: a.Contact, Agreement: c.Link["terms-of-service"], } err := c.post(c.Location, a, r) return r, err } func (c *Client) Authorize(a *Account, domain string) (*Authorization, error) { r := &Authorization{ Resource: ResNewAuthz, Identifier: Identifier{ Type: IdentDNS, Value: domain, }, } err := c.post(c.Dir.NewAuthz, a, r) return r, err } func (c Client) String() string { return fmt.Sprintf("Link: %v, Location: %v", c.Link, c.Location) }