package acme import ( "encoding/json" "errors" "io/ioutil" "net/http" "regexp" "time" ) const ( // LE1 Let's Encrypt V1 LE1 = `https://acme-v01.api.letsencrypt.org/directory` // LES Let's Encrypt Staging LES = `https://acme-staging.api.letsencrypt.org/directory` ) type Resource string const ( ResNewReg Resource = "new-reg" ResRecoverReg Resource = "recover-reg" ResNewAuthz Resource = "new-authz" ResNewCert Resource = "new-cert" ResRevokeCert Resource = "revoke-cert" ResReg Resource = "reg" ResAuthz Resource = "authz" ResChallenge Resource = "challenge" ResCert Resource = "cert" ) // Directory ... type Directory struct { NewReg string `json:"new-reg"` RecoverReg string `json:"recover-reg"` NewAuthz string `json:"new-authz"` NewCert string `json:"new-cert"` RevokeCert string `json:"revoke-cert"` } // Provider ... type Provider struct { Directory nonces chan string http.Client http.Transport } 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" timeout = time.Second * 30 poll = time.Second ) // RoundTrip extracts nonces from HTTP reponse func (p Provider) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := p.Transport.RoundTrip(req) if err != nil { return nil, err } nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { return nil, errNoNonces } if len(p.nonces) == cap(p.nonces) { <-p.nonces // drop oldest } p.nonces <- nonce return resp, nil } // Nonce implements jose nonce provider func (p Provider) Nonce() (string, error) { select { case nonce := <-p.nonces: return nonce, nil case <-time.After(timeout): return "", errNoNonces } } // DialProvider fetches directory and initializes first nonce func DialProvider(directory string) (*Provider, error) { p := &Provider{nonces: make(chan string, 100)} p.Client = http.Client{ Transport: p, Timeout: time.Duration(timeout), } 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() switch resp.Header.Get("Content-Type") { case mimeJson: return json.NewDecoder(resp.Body).Decode(v) case mimeProblem: return problem(resp.Body) default: return errContentType } } func parseCert(resp *http.Response) ([]byte, error) { defer resp.Body.Close() switch resp.Header.Get("Content-Type") { case mimePkix: return ioutil.ReadAll(resp.Body) case mimeProblem: return nil, problem(resp.Body) default: return nil, errContentType } }