package acme import ( "crypto/rsa" "encoding/json" "errors" "io/ioutil" "log" "net/http" "net/url" "regexp" "strconv" "time" "github.com/mgutz/ansi" ) // Client ... type Client struct { Directory nonce chan string } var ( errNoNonces = errors.New("out of nonces") errTimedOut = errors.New("timed out") errContentType = errors.New("unknown content type") errChallengeType = errors.New("unknown challenge") ) // Nonce implements jose nonce provider func (c Client) Nonce() (string, error) { select { case nonce := <-c.nonce: return nonce, nil default: return "", errNoNonces } } func (c Client) replyNonce(r *http.Response) { if rn := r.Header.Get("Replay-Nonce"); 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) } // 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)) signed, err := s.Sign(body, c) if err != nil { return nil, err } resp, err := http.Post(uri, "application/json", signed) if err != nil { return nil, err } defer c.replyNonce(resp) log.Println(ansi.Color("STATUS", "yellow"), resp.Status) switch resp.Header.Get("Content-Type") { case "application/problem+json": defer resp.Body.Close() var p Problem if err = json.NewDecoder(resp.Body).Decode(&p); err != nil { return resp, err } if err, ok := urnErrors[p.Type]; ok { p.Err = err } return resp, p case "application/json": defer resp.Body.Close() return resp, json.NewDecoder(resp.Body).Decode(v) case "application/pkix-cert": return resp, nil default: return resp, errContentType } } type Links map[string]string type nextStep struct { Link Links Location *url.URL RetryAfter time.Duration } var linksRe = regexp.MustCompile(`^<(.*)>;rel="(.*)"`) func parseHeader(r *http.Response) nextStep { var ns nextStep if lo, err := r.Location(); err == nil { 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.String(), 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.String(), a, r) } } return err } func pickChallenge(c []Challenge) (int, Challenge) { for i, ch := range c { if canSolve[ch.Type] { return i, ch } } return -1, Challenge{} } func (c *Client) Authorize(s Signer, altnames []string) error { ident := Identifier{ Type: IdentDNS, Value: altnames[0], } r := &Authorization{ Resource: ResNewAuthz, Identifier: ident, } resp, err := c.post(c.NewAuthz, s, r) if err != nil { return err } switch resp.StatusCode { case http.StatusCreated: n, ch := pickChallenge(r.Challenges) if n < 0 { return errors.New("can't solve any challenges") } ka, _ := s.Thumb(ch.Token) ans := &Challenge{ Resource: ResChallenge, Type: ch.Type, KeyAuthorization: ka, } _, err = c.post(ch.URI, s, ans) var sol Solver switch ch.Type { case ChallengeHTTP: sol = &httpChallenge{Addr: "localhost:8080", Challenge: *ans} default: return errChallengeType } if err := Solve(sol, time.Minute); err != nil { return err } ns := parseHeader(resp) done := make(chan bool) errc := make(chan error) log.Println(ansi.Color("NextStep", "green"), ns) ticker := time.NewTicker(time.Second) defer ticker.Stop() go func() { for range ticker.C { err := c.queryStatus(ns.Location.String(), n, done) if err != nil { errc <- err return } } }() select { case <-done: case err = <-errc: case <-time.After(30 * time.Second): return errTimedOut } } return err } func (c *Client) queryStatus(url string, n int, done chan bool) error { r := &Authorization{} resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() defer c.replyNonce(resp) err = json.NewDecoder(resp.Body).Decode(r) if err != nil { return err } if r.Challenges[n].Status == StatusValid { done <- true } return nil } func (c *Client) Cert(s Signer, altnames []string, key *rsa.PrivateKey) ([]byte, error) { csr, err := NewCSR(altnames, key) if err != nil { return nil, err } r := &CSR{ Resource: ResNewCert, CSR: csr, } resp, err := c.post(c.NewCert, s, r) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) } //////////////////////////////////////////////////////////////////////// // Register // 1. new-reg // arg: contact // StatusCreated (201) or StatusConflict (409) // Response: Location, Link // 2. reg // arg: contact, tos // StatusOk // Authorize // Certificate