package acme import ( "bytes" "encoding/json" "errors" "log" "net/http" "net/textproto" "regexp" "time" ) type Solver interface { Solve() } type NonceSigner interface { Sign([]byte) ([]byte, error) parseNonce(*http.Response) } type Client struct { directory Directory nonce chan string } // 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.directory) 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 } } // Get is used for // directory, authz, cert func Get(s NonceSigner, uri string, v interface{}) error { resp, err := http.Get(uri) if err != nil { return err } defer resp.Body.Close() s.parseNonce(resp) return json.NewDecoder(resp.Body).Decode(v) } // 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 // Post is used for // new-reg, new-authz, challenge, new-cert func Post(s NonceSigner, uri string, v interface{}) (*http.Response, error) { body, err := json.Marshal(v) if err != nil { return nil, err } signed, err := s.Sign(body) if err != nil { return nil, err } resp, err := http.Post(uri, "application/jose+json", bytes.NewReader(signed)) if err != nil { return nil, err } s.parseNonce(resp) if resp.StatusCode >= http.StatusBadRequest { return nil, handleError(resp) } log.Printf("%+v\n", link(resp)) return resp, nil } type Links map[string]string func link(r *http.Response) Links { links := make(Links) key := textproto.CanonicalMIMEHeaderKey("Link") reg := regexp.MustCompile(`^<(.*)>;rel="(.*)"`) for _, l := range r.Header[key] { re := reg.FindStringSubmatch(l) if len(re) == 3 { links[re[2]] = re[1] } } return links } 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") }