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 --- client.go | 322 ------------------------------------------------------- cmd/acme/main.go | 10 +- provider.go | 322 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 327 deletions(-) delete mode 100644 client.go create mode 100644 provider.go diff --git a/client.go b/client.go deleted file mode 100644 index eae9f30..0000000 --- a/client.go +++ /dev/null @@ -1,322 +0,0 @@ -package acme - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/json" - "errors" - "io/ioutil" - "log" - "net/http" - "net/url" - "regexp" - "strconv" - "time" - - "github.com/mgutz/ansi" -) - -// Client ... -type Client 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 (c Client) Nonce() (string, error) { - select { - case nonce := <-c.nonce: - return nonce, nil - default: - return "", errNoNonces - } -} - -func (c Client) replyNonce(r *http.Response) { - if rn := r.Header.Get("Replay-Nonce"); rn != "" { - c.nonce <- rn - } -} - -// NewClient fetches directory and initializes nonce -func NewClient(directory string) (*Client, error) { - c := &Client{nonce: make(chan string, 10)} - resp, err := http.Get(directory) - if err != nil { - return nil, err - } - defer resp.Body.Close() - defer c.replyNonce(resp) - return c, json.NewDecoder(resp.Body).Decode(&c.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 (c *Client) 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, c) - if err != nil { - return nil, err - } - - resp, err := http.Post(uri, "application/json", signed) - if err != nil { - return nil, err - } - defer c.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 (c *Client) Register(a *Account) error { - r := &Registration{ - Resource: ResNewReg, - Contact: a.contact, - } - resp, err := c.post(c.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 = c.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 = c.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 (c *Client) Authorize(s ThumbSigner, domain string) error { - ident := Identifier{ - Type: IdentDNS, - Value: domain, - } - r := &Authorization{ - Resource: ResNewAuthz, - Identifier: ident, - } - resp, err := c.post(c.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 = c.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 := c.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 (c *Client) 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 c.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 (c *Client) 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 := c.post(c.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) -} diff --git a/cmd/acme/main.go b/cmd/acme/main.go index dc397b2..2243ac3 100644 --- a/cmd/acme/main.go +++ b/cmd/acme/main.go @@ -79,23 +79,23 @@ func main() { a.AddMail(des.account.Mail) a.AddPhone(des.account.Phone) log.Println(k, a) - c, err := acme.NewClient(des.provider.Directory) + p, err := acme.NewProvider(des.provider.Directory) if err != nil { log.Fatal(err) } - log.Println(k, c) + log.Println(k, p) - err = c.Register(a) + err = p.Register(a) if err != nil { log.Fatal("register", err) } - err = c.Authorize(a, des.Altnames[0]) + err = p.Authorize(a, des.Altnames[0]) if err != nil { log.Fatal("authz", err) } - crt, err := c.Cert(a, des.Altnames, des.key) + crt, err := p.Cert(a, des.Altnames, des.key) if err != nil { log.Fatal("cert", err) } 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