package acme import ( "encoding/json" "errors" "io/ioutil" "log" "net/http" "regexp" "strconv" "time" "github.com/mgutz/ansi" ) type Links map[string]string // Client ... type Client struct { Directory nonce chan string } type nextStep struct { Link Links Location string RetryAfter time.Duration } func (c Client) replyNonce(r *http.Response) { if rn := r.Header.Get("Replay-Nonce"); rn != "" { log.Println(ansi.Color("NONCE", "blue"), rn) c.nonce <- rn } } // NewClient fetches directory and initializes nonce func NewClient(directory string) (*Client, error) { c := &Client{nonce: make(chan string, 10)} resp, err := http.Get(directory) if err != nil { return nil, err } defer resp.Body.Close() defer c.replyNonce(resp) return c, json.NewDecoder(resp.Body).Decode(&c.Directory) } var errNoNonces = errors.New("out of nonces") // 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(uri string, s Signer, v interface{}) (*http.Response, error) { body, err := json.Marshal(v) if err != nil { return nil, err } log.Println(ansi.Color("POST", "red+b"), uri, string(body)) s.Init(c) signed, err := s.Sign(body) if err != nil { return nil, err } resp, err := http.Post(uri, "application/json", signed) if err != nil { return nil, err } defer resp.Body.Close() defer c.replyNonce(resp) log.Println(ansi.Color("STATUS", "yellow"), resp.Status) if resp.StatusCode >= http.StatusBadRequest { var p Problem err = json.NewDecoder(resp.Body).Decode(&p) if err != nil { return resp, err } if err, ok := urnErrors[p.Type]; ok { p.Err = err } return resp, p } // DEBUG body, err = ioutil.ReadAll(resp.Body) if err != nil { return resp, err } log.Println(ansi.Color("RESP", "red"), string(body)) return resp, json.Unmarshal(body, v) //return json.NewDecoder(resp.Body).Decode(v) } var linksRe = regexp.MustCompile(`^<(.*)>;rel="(.*)"`) func parseHeader(r *http.Response) nextStep { var ns nextStep if lo := r.Header.Get("Location"); lo != "" { ns.Location = lo } ns.Link = make(Links) for _, li := range r.Header["Link"] { re := linksRe.FindStringSubmatch(li) if len(re) == 3 { ns.Link[re[2]] = re[1] } } if ra := r.Header.Get("Retry-After"); ra != "" { n, err := strconv.Atoi(ra) if err == nil { ns.RetryAfter = time.Second * time.Duration(n) } } log.Println(ansi.Color("NEXT", "cyan"), ns) return ns } /* 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) error { r := &Registration{ Resource: ResNewReg, Contact: a.Contact, } resp, err := c.post(c.NewReg, a, r) if err != nil && err.(Problem).Err != ErrMalformed { return err } ns := parseHeader(resp) switch resp.StatusCode { case http.StatusConflict: // Query Location r = &Registration{Resource: ResRegister} resp, err = c.post(ns.Location, a, r) if err != nil { return err } fallthrough case http.StatusCreated: // Agree to TOS if tos := ns.Link["terms-of-service"]; tos != "" { r = &Registration{ Resource: ResRegister, Contact: a.Contact, Agreement: tos, } _, err = c.post(ns.Location, a, r) } } return err } func (c *Client) Authorize(a *Account, domain []string) error { ident := Identifier{ Type: IdentDNS, Value: domain[0], } r := &Authorization{ Resource: ResNewAuthz, Identifier: ident, } resp, err := c.post(c.NewAuthz, a, r) if err != nil { return err } switch resp.StatusCode { case http.StatusCreated: for _, ch := range r.Challenges { if ch.Type == ChallengeHTTP { ans := &Challenge{ Resource: ResChallenge, Type: ch.Type, Token: ch.Token, } _, err = c.post(ch.URI, a, ans) } } } return err } //////////////////////////////////////////////////////////////////////// // Register // 1. new-reg // arg: contact // StatusCreated (201) or StatusConflict (409) // Response: Location, Link // 2. reg // arg: contact, tos // StatusOk // Authorize // Certificate