package acme import ( "crypto/x509" "encoding/json" "errors" "log" "net/http" "regexp" "time" ) // Provider ... type Provider struct { Directory nonces chan string http.Client http.Transport poll time.Duration } var ( errNoNonces = errors.New("out of nonces") errContentType = errors.New("unknown content type") errChalType = errors.New("unknown challenge") errStatus = errors.New("unexpected status") ) const ( mimeJson = "application/json" mimeJose = "application/jose+json" mimeProblem = "application/problem+json" mimePkix = "application/pkix-cert" ) // RoundTrip implements RoundTipper func (p Provider) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := p.Transport.RoundTrip(req) if err != nil { return nil, err } if nonce := resp.Header.Get("Replay-Nonce"); nonce != "" { if len(p.nonces) == cap(p.nonces) { <-p.nonces // drop oldest } p.nonces <- nonce } // not sure if it belongs here if resp.Header.Get("Content-Type") == mimeProblem { defer resp.Body.Close() return nil, problem(resp.Body) } return resp, nil } // Nonce implements jose nonce provider func (p Provider) Nonce() (string, error) { select { case nonce := <-p.nonces: return nonce, nil default: return "", errNoNonces } } // DialProvider fetches directory and initializes nonce func DialProvider(directory string) (*Provider, error) { p := &Provider{ nonces: make(chan string, 3), poll: time.Second, } p.Client = http.Client{ Transport: p, Timeout: time.Duration(5 * time.Second), } resp, err := p.Get(directory) if err != nil { return nil, err } return p, parseJson(resp, &p.Directory) } func (p *Provider) post(uri string, s Signer, v interface{}) (*http.Response, error) { msg, err := json.Marshal(v) if err != nil { return nil, err } signed, err := s.Sign(msg, p) if err != nil { return nil, err } return p.Post(uri, mimeJose, signed) } type nextStep struct { Link map[string]string Location string } var linksRe = regexp.MustCompile(`^<(.*)>;rel="(.*)"`) func 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] } } return ns } func parseJson(resp *http.Response, v interface{}) error { defer resp.Body.Close() if resp.Header.Get("Content-Type") != mimeJson { return errContentType } return json.NewDecoder(resp.Body).Decode(v) } func parseCert(resp *http.Response) (*x509.Certificate, error) { defer resp.Body.Close() if resp.Header.Get("Content-Type") != mimePkix { return nil, errContentType } return readCert(resp.Body) } func (p *Provider) Register(s Signer, c Contacts) error { // first step: new-reg r := &Registration{ Resource: ResNewReg, Contact: c, } resp, err := p.post(p.NewReg, s, r) if err != nil && err.(Problem).Err != ErrMalformed { return err } ns := parseHeader(resp) // second step: reg, agree to tos r = &Registration{ Resource: ResReg, Agreement: ns.Link["terms-of-service"], } resp, err = p.post(ns.Location, s, r) if err != nil { return err } resp.Body.Close() return nil } func (p *Provider) solve(s Signer, ch Challenge) error { ka, err := s.KeyAuth(ch.Token) if err != nil { return err } log.Println("prevare solver", ch.Type) err = ch.Solve(ch.Token, ka) if err != nil { return err } defer ch.Solved() log.Println("poke solver", ch.Type) r := &Challenge{ Resource: ResChallenge, Type: ch.Type, KeyAuthorization: ka, } resp, err := p.post(ch.URI, s, r) if err != nil { return err } ns := parseHeader(resp) log.Println("start polling") if err := p.pollStatus(ns.Location); err != nil { return err } return nil } func (p *Provider) authz(s Signer, domain string, sol map[ChalType]Solver) error { // first step: pocke r := &Authorization{ Resource: ResNewAuthz, Identifier: Identifier{ Type: IdentDNS, Value: domain, }, } resp, err := p.post(p.NewAuthz, s, r) if err != nil { return err } err = parseJson(resp, r) if err != nil { return err } // second step: choose and start solver for _, ch := range r.Supported(sol) { if err = p.solve(s, ch); err != nil { return err } } return nil } func (p *Provider) Authz(s Signer, d *Desire) error { for _, domain := range d.altnames { if err := p.authz(s, domain, d.solver); err != nil { return err } } return nil } func (p *Provider) pollStatus(uri string) error { t := time.NewTicker(p.poll) defer t.Stop() for range t.C { done, err := p.queryStatus(uri) if err != nil { return err } if done { return nil } } return nil } func (p *Provider) queryStatus(uri string) (bool, error) { resp, err := p.Get(uri) if err != nil { return false, err } r := new(Challenge) err = parseJson(resp, r) if err != nil { return false, err } log.Println("status", r.Status) if r.Err != nil { return false, r.Err } return r.Status == StatusValid, nil } func (p *Provider) Bundle(s Signer, d *Desire) error { // first step: post csr csr, err := d.CSR() 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, err := parseCert(resp) if err != nil { return err } d.cert = append(d.cert, crt) ns := parseHeader(resp) // second step: cet CA resp, err = p.Get(ns.Link["up"]) if err != nil { return err } crt, err = parseCert(resp) if err != nil { return err } d.cert = append(d.cert, crt) return nil }