diff options
Diffstat (limited to 'vendor/github.com/mholt/acmez/acme/client.go')
-rw-r--r-- | vendor/github.com/mholt/acmez/acme/client.go | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/vendor/github.com/mholt/acmez/acme/client.go b/vendor/github.com/mholt/acmez/acme/client.go new file mode 100644 index 0000000000..5037905b68 --- /dev/null +++ b/vendor/github.com/mholt/acmez/acme/client.go @@ -0,0 +1,240 @@ +// Copyright 2020 Matthew Holt +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package acme full implements the ACME protocol specification as +// described in RFC 8555: https://tools.ietf.org/html/rfc8555. +// +// It is designed to work smoothly in large-scale deployments with +// high resilience to errors and intermittent network or server issues, +// with retries built-in at every layer of the HTTP request stack. +// +// NOTE: This is a low-level API. Most users will want the mholt/acmez +// package which is more concerned with configuring challenges and +// implementing the order flow. However, using this package directly +// is recommended for advanced use cases having niche requirements. +// See the examples in the examples/plumbing folder for a tutorial. +package acme + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "go.uber.org/zap" +) + +// Client facilitates ACME client operations as defined by the spec. +// +// Because the client is synchronized for concurrent use, it should +// not be copied. +// +// Many errors that are returned by a Client are likely to be of type +// Problem as long as the ACME server returns a structured error +// response. This package wraps errors that may be of type Problem, +// so you can access the details using the conventional Go pattern: +// +// var problem Problem +// if errors.As(err, &problem) { +// log.Printf("Houston, we have a problem: %+v", problem) +// } +// +// All Problem errors originate from the ACME server. +type Client struct { + // The ACME server's directory endpoint. + Directory string + + // Custom HTTP client. + HTTPClient *http.Client + + // Augmentation of the User-Agent header. Please set + // this so that CAs can troubleshoot bugs more easily. + UserAgent string + + // Delay between poll attempts. Only used if server + // does not supply a Retry-Afer header. Default: 250ms + PollInterval time.Duration + + // Maximum duration for polling. Default: 5m + PollTimeout time.Duration + + // An optional logger. Default: no logs + Logger *zap.Logger + + mu sync.Mutex // protects all unexported fields + dir Directory + nonces *stack +} + +// GetDirectory retrieves the directory configured at c.Directory. It is +// NOT necessary to call this to provision the client. It is only useful +// if you want to access a copy of the directory yourself. +func (c *Client) GetDirectory(ctx context.Context) (Directory, error) { + if err := c.provision(ctx); err != nil { + return Directory{}, err + } + return c.dir, nil +} + +func (c *Client) provision(ctx context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.nonces == nil { + c.nonces = new(stack) + } + + err := c.provisionDirectory(ctx) + if err != nil { + return fmt.Errorf("provisioning client: %w", err) + } + + return nil +} + +func (c *Client) provisionDirectory(ctx context.Context) error { + // don't get directory again if we already have it; + // checking any one of the required fields will do + if c.dir.NewNonce != "" { + return nil + } + if c.Directory == "" { + return fmt.Errorf("missing directory URL") + } + // prefer cached version if it's recent enough + directoriesMu.Lock() + defer directoriesMu.Unlock() + if dir, ok := directories[c.Directory]; ok { + if time.Since(dir.retrieved) < 12*time.Hour { + c.dir = dir.Directory + return nil + } + } + _, err := c.httpReq(ctx, http.MethodGet, c.Directory, nil, &c.dir) + if err != nil { + return err + } + directories[c.Directory] = cachedDirectory{c.dir, time.Now()} + return nil +} + +func (c *Client) nonce(ctx context.Context) (string, error) { + nonce := c.nonces.pop() + if nonce != "" { + return nonce, nil + } + + if c.dir.NewNonce == "" { + return "", fmt.Errorf("directory missing newNonce endpoint") + } + + resp, err := c.httpReq(ctx, http.MethodHead, c.dir.NewNonce, nil, nil) + if err != nil { + return "", fmt.Errorf("fetching new nonce from server: %w", err) + } + + return resp.Header.Get(replayNonce), nil +} + +func (c *Client) pollInterval() time.Duration { + if c.PollInterval == 0 { + return defaultPollInterval + } + return c.PollInterval +} + +func (c *Client) pollTimeout() time.Duration { + if c.PollTimeout == 0 { + return defaultPollTimeout + } + return c.PollTimeout +} + +// Directory acts as an index for the ACME server as +// specified in the spec: "In order to help clients +// configure themselves with the right URLs for each +// ACME operation, ACME servers provide a directory +// object." §7.1.1 +type Directory struct { + NewNonce string `json:"newNonce"` + NewAccount string `json:"newAccount"` + NewOrder string `json:"newOrder"` + NewAuthz string `json:"newAuthz,omitempty"` + RevokeCert string `json:"revokeCert"` + KeyChange string `json:"keyChange"` + Meta *DirectoryMeta `json:"meta,omitempty"` +} + +// DirectoryMeta is optional extra data that may be +// included in an ACME server directory. §7.1.1 +type DirectoryMeta struct { + TermsOfService string `json:"termsOfService,omitempty"` + Website string `json:"website,omitempty"` + CAAIdentities []string `json:"caaIdentities,omitempty"` + ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"` +} + +// stack is a simple thread-safe stack. +type stack struct { + stack []string + stackMu sync.Mutex +} + +func (s *stack) push(v string) { + if v == "" { + return + } + s.stackMu.Lock() + defer s.stackMu.Unlock() + if len(s.stack) >= 64 { + return + } + s.stack = append(s.stack, v) +} + +func (s *stack) pop() string { + s.stackMu.Lock() + defer s.stackMu.Unlock() + n := len(s.stack) + if n == 0 { + return "" + } + v := s.stack[n-1] + s.stack = s.stack[:n-1] + return v +} + +// Directories seldom (if ever) change in practice, and +// client structs are often ephemeral, so we can cache +// directories to speed things up a bit for the user. +// Keyed by directory URL. +var ( + directories = make(map[string]cachedDirectory) + directoriesMu sync.Mutex +) + +type cachedDirectory struct { + Directory + retrieved time.Time +} + +// replayNonce is the header field that contains a new +// anti-replay nonce from the server. +const replayNonce = "Replay-Nonce" + +const ( + defaultPollInterval = 250 * time.Millisecond + defaultPollTimeout = 5 * time.Minute +) |