From 69f81d8942d31e7ae9c8d25740e572d20638c5cd Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Thu, 31 Dec 2015 14:38:27 +0100 Subject: Rename client into provider --- provider.go | 322 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 provider.go (limited to 'provider.go') 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) +} -- cgit v1.2.3