package acme import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "net/http" "regexp" "strconv" "time" ) type Links map[string]string // Client ... type Client struct { Dir Directory Link Links 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.parseHeader(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 // Location next step // Content-Location cert // 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(s Signer, v interface{}) error { body, err := json.Marshal(v) if err != nil { return err } log.Println("POST", string(body)) signed, err := s.Sign(body, c) if err != nil { return err } resp, err := http.Post(c.Location, "application/jose+json", signed) if err != nil { return err } defer resp.Body.Close() c.parseHeader(resp) log.Println("STATUS", resp.Status) log.Println("HEADER", c) if resp.StatusCode >= http.StatusBadRequest { var p Problem err = json.NewDecoder(resp.Body).Decode(&p) if err != nil { return err } if err, ok := urnErrors[p.Type]; ok { p.Err = err } return p } // DEBUG body, err = ioutil.ReadAll(resp.Body) if err != nil { return err } log.Println("RESPONSE", string(body)) return json.Unmarshal(body, v) //return json.NewDecoder(resp.Body).Decode(v) } var linksRe = regexp.MustCompile(`^<(.*)>;rel="(.*)"`) func (c *Client) parseHeader(r *http.Response) { if rn := r.Header.Get("Replay-Nonce"); rn != "" { c.nonce <- rn } if lo := r.Header.Get("Location"); lo != "" { c.Location = lo } c.Link = make(Links) for _, li := range r.Header["Link"] { re := linksRe.FindStringSubmatch(li) if len(re) == 3 { c.Link[re[2]] = re[1] } } if ra := r.Header.Get("Retry-After"); ra != "" { n, err := strconv.Atoi(ra) if err == nil { c.RetryAfter = time.Second * time.Duration(n) } } } /* 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, } c.Location = c.Dir.NewReg return r, c.post(a, r) } // Agree to TOS func (c *Client) Agree(a *Account) (*Registration, error) { tos := c.Link["terms-of-service"] if tos == "" { return nil, errors.New("TOS agreed") } r := &Registration{ Resource: ResRegister, Contact: a.Contact, Agreement: tos, } log.Println("TOS", tos) return r, c.post(a, r) } func (c *Client) Authorize(a *Account, domain string) (*Authorization, error) { r := &Authorization{ Resource: ResNewAuthz, Identifier: Identifier{ Type: IdentDNS, Value: domain, }, } c.Location = c.Dir.NewAuthz return r, c.post(a, r) } func (c Client) String() string { return fmt.Sprintf("Location: %v Links: %v", c.Location, c.Link) } //////////////////////////////////////////////////////////////////////// // Register // 1. new-reg // arg: contact // StatusCreated (201) or StatusConflict (409) // Response: Location, Link // 2. reg // arg: contact, tos // StatusOk // Authorize // Certificate