package acme import ( "crypto/x509" "encoding/json" "errors" "io" "io/ioutil" "log" "net/http" "regexp" "time" ) // Provider ... type Provider struct { Directory nonces chan string http.Client } var ( errNoNonces = errors.New("out of nonces") errTimedOut = errors.New("timed out") errContentType = errors.New("unknown content type") errChallengeType = errors.New("unknown challenge") errStatus = errors.New("unexpected status") ) // Nonce implements jose nonce provider func (p Provider) Nonce() (string, error) { select { case nonce := <-p.nonces: return nonce, nil default: return "", errNoNonces } } // NewProvider fetches directory and initializes nonce func NewProvider(directory string) (*Provider, error) { p := &Provider{ nonces: make(chan string, 10), Client: http.Client{ Timeout: time.Duration(5 * time.Second), }, } resp, err := p.Get(directory) if err != nil { return nil, err } _, err = p.parseJson(resp, &p.Directory) if err != nil { return nil, err } return p, nil } // 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 (p *Provider) post(uri string, s Signer, v interface{}) (*http.Response, error) { log.Println("post", uri) signed, err := s.Sign(v, p) if err != nil { return nil, err } return p.Post(uri, "application/jose+json", signed) } type nextStep struct { Link map[string]string Location string } var linksRe = regexp.MustCompile(`^<(.*)>;rel="(.*)"`) func (p *Provider) parseHeader(resp *http.Response) nextStep { var ns nextStep if lo, _ := resp.Location(); lo != nil { ns.Location = lo.String() } ns.Link = make(map[string]string) for _, li := range resp.Header["Link"] { re := linksRe.FindStringSubmatch(li) if len(re) == 3 { ns.Link[re[2]] = re[1] } } if rn := resp.Header.Get("Replay-Nonce"); rn != "" { p.nonces <- rn } return ns } func (p *Provider) parseJson(resp *http.Response, v interface{}) (nextStep, error) { ns := p.parseHeader(resp) defer resp.Body.Close() switch resp.Header.Get("Content-Type") { case "application/problem+json": return ns, problem(resp.Body) case "application/json": return ns, json.NewDecoder(resp.Body).Decode(v) } return ns, errContentType } func (p *Provider) parseCert(resp *http.Response) (*x509.Certificate, nextStep, error) { ns := p.parseHeader(resp) defer resp.Body.Close() switch resp.Header.Get("Content-Type") { case "application/problem+json": return nil, ns, problem(resp.Body) case "application/pkix-cert": c, err := cert(resp.Body) return c, ns, err } return nil, ns, errContentType } func problem(r io.Reader) error { var p Problem if err := json.NewDecoder(r).Decode(&p); err != nil { return err } p.Err = urnErrors[p.Type] return p } func cert(r io.Reader) (*x509.Certificate, error) { der, err := ioutil.ReadAll(r) if err != nil { return nil, err } return x509.ParseCertificate(der) } /* 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 (p *Provider) Register(s Signer, c Contacts) error { r := &Registration{ Resource: ResNewReg, Contact: c, } resp, err := p.post(p.NewReg, s, r) if err != nil { return err } ns, err := p.parseJson(resp, r) if err != nil && err.(Problem).Err != ErrMalformed { return err } r = &Registration{ Resource: ResReg, Contact: c, Agreement: ns.Link["terms-of-service"], } resp, err = p.post(ns.Location, s, r) if err != nil { return err } _, err = p.parseJson(resp, 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 (p *Provider) Authorize(s ThumbSigner, d *Desire) error { for _, domain := range d.altnames { r := &Authorization{ Resource: ResNewAuthz, Identifier: NewIdent(domain), } resp, err := p.post(p.NewAuthz, s, r) if err != nil { return err } _, err = p.parseJson(resp, r) if err != nil { return err } if resp.StatusCode != http.StatusCreated { return errStatus } n, ch := pickChallenge(r.Challenges) if n < 0 { return errors.New("can't solve any challenges") } ka, _ := KeyAuthorization(s, ch.Token) ans := &Challenge{ Resource: ResChallenge, Type: ch.Type, KeyAuthorization: ka, } resp, err = p.post(ch.URI, s, ans) if err != nil { return err } ns, err := p.parseJson(resp, ans) if err != nil { return err } 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 } for { done, err := p.queryStatus(ns.Location) if err != nil { return err } if done { break } } if err != nil { return err } } return nil } func (p *Provider) queryStatus(url string) (bool, error) { r := &Challenge{} resp, err := p.Get(url) if err != nil { return false, err } _, err = p.parseJson(resp, r) if err != nil { return false, err } return r.Status == StatusValid, nil } func (p *Provider) Cert(s Signer, d *Desire) error { csr, err := NewCSR(d.altnames, d.key) if err != nil { return err } r := &CSR{ Resource: ResNewCert, CSR: csr, } resp, err := p.post(p.NewCert, s, r) if err != nil { return err } crt, ns, err := p.parseCert(resp) if err != nil { return err } d.cert = append(d.cert, crt) resp, err = p.Get(ns.Link["up"]) if err != nil { return err } crt, _, err = p.parseCert(resp) if err != nil { return err } d.cert = append(d.cert, crt) return nil }