aboutsummaryrefslogtreecommitdiff
path: root/provider.go
diff options
context:
space:
mode:
authorDimitri Sokolyuk <demon@dim13.org>2015-12-31 14:38:27 +0100
committerDimitri Sokolyuk <demon@dim13.org>2015-12-31 14:38:27 +0100
commit69f81d8942d31e7ae9c8d25740e572d20638c5cd (patch)
tree5d6e9645f699d4b377dc8175d94db0e7b802afc9 /provider.go
parent3e786c1c793ad6f2854a03431385b59dc4c27eae (diff)
Rename client into provider
Diffstat (limited to 'provider.go')
-rw-r--r--provider.go322
1 files changed, 322 insertions, 0 deletions
diff --git a/provider.go b/provider.go
new file mode 100644
index 0000000..c0121a1
--- /dev/null
+++ b/provider.go
@@ -0,0 +1,322 @@
+package acme
+
+import (
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/json"
+ "errors"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strconv"
+ "time"
+
+ "github.com/mgutz/ansi"
+)
+
+// Provider ...
+type Provider 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 (p Provider) Nonce() (string, error) {
+ select {
+ case nonce := <-p.nonce:
+ return nonce, nil
+ default:
+ return "", errNoNonces
+ }
+}
+
+func (p Provider) replyNonce(r *http.Response) {
+ if rn := r.Header.Get("Replay-Nonce"); rn != "" {
+ p.nonce <- rn
+ }
+}
+
+// NewProvider fetches directory and initializes nonce
+func NewProvider(directory string) (*Provider, error) {
+ p := &Provider{nonce: make(chan string, 10)}
+ resp, err := http.Get(directory)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ defer p.replyNonce(resp)
+ return p, json.NewDecoder(resp.Body).Decode(&p.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 (p *Provider) 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, p)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := http.Post(uri, "application/json", signed)
+ if err != nil {
+ return nil, err
+ }
+ defer p.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 (p *Provider) Register(a *Account) error {
+ r := &Registration{
+ Resource: ResNewReg,
+ Contact: a.contact,
+ }
+ resp, err := p.post(p.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 = p.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 = p.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 (p *Provider) Authorize(s ThumbSigner, domain string) error {
+ ident := Identifier{
+ Type: IdentDNS,
+ Value: domain,
+ }
+ r := &Authorization{
+ Resource: ResNewAuthz,
+ Identifier: ident,
+ }
+ resp, err := p.post(p.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, _ := KeyAuthorization(s, ch.Token)
+ ans := &Challenge{
+ Resource: ResChallenge,
+ Type: ch.Type,
+ KeyAuthorization: ka,
+ }
+ _, err = p.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("NEXT", "green"), ns)
+ ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+ go func() {
+ for range ticker.C {
+ err := p.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 (p *Provider) 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 p.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 (p *Provider) Cert(s Signer, altnames []string, key *rsa.PrivateKey) (*x509.Certificate, error) {
+ csr, err := NewCSR(altnames, key)
+ if err != nil {
+ return nil, err
+ }
+ r := &CSR{
+ Resource: ResNewCert,
+ CSR: csr,
+ }
+ resp, err := p.post(p.NewCert, s, r)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ns := parseHeader(resp)
+ log.Println(ansi.Color("NEXT", "green"), ns)
+ der, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ return x509.ParseCertificate(der)
+}