123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- // Copyright 2016 Google LLC. All Rights Reserved.
- //
- // 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 jsonclient provides a simple client for fetching and parsing
- // JSON CT structures from a log.
- package jsonclient
- import (
- "bytes"
- "context"
- "crypto"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "log"
- "math/rand"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
- ct "github.com/google/certificate-transparency-go"
- "github.com/google/certificate-transparency-go/x509"
- "golang.org/x/net/context/ctxhttp"
- "k8s.io/klog/v2"
- )
- const maxJitter = 250 * time.Millisecond
- type backoffer interface {
- // set adjusts/increases the current backoff interval (typically on retryable failure);
- // if the optional parameter is provided, this will be used as the interval if it is greater
- // than the currently set interval. Returns the current wait period so that it can be
- // logged along with any error message.
- set(*time.Duration) time.Duration
- // decreaseMultiplier reduces the current backoff multiplier, typically on success.
- decreaseMultiplier()
- // until returns the time until which the client should wait before making a request,
- // it may be in the past in which case it should be ignored.
- until() time.Time
- }
- // JSONClient provides common functionality for interacting with a JSON server
- // that uses cryptographic signatures.
- type JSONClient struct {
- uri string // the base URI of the server. e.g. https://ct.googleapis/pilot
- httpClient *http.Client // used to interact with the server via HTTP
- Verifier *ct.SignatureVerifier // nil for no verification (e.g. no public key available)
- logger Logger // interface to use for logging warnings and errors
- backoff backoffer // object used to store and calculate backoff information
- userAgent string // If set, this is sent as the UserAgent header.
- }
- // Logger is a simple logging interface used to log internal errors and warnings
- type Logger interface {
- // Printf formats and logs a message
- Printf(string, ...interface{})
- }
- // Options are the options for creating a new JSONClient.
- type Options struct {
- // Interface to use for logging warnings and errors, if nil the
- // standard library log package will be used.
- Logger Logger
- // PEM format public key to use for signature verification.
- PublicKey string
- // DER format public key to use for signature verification.
- PublicKeyDER []byte
- // UserAgent, if set, will be sent as the User-Agent header with each request.
- UserAgent string
- }
- // ParsePublicKey parses and returns the public key contained in opts.
- // If both opts.PublicKey and opts.PublicKeyDER are set, PublicKeyDER is used.
- // If neither is set, nil will be returned.
- func (opts *Options) ParsePublicKey() (crypto.PublicKey, error) {
- if len(opts.PublicKeyDER) > 0 {
- return x509.ParsePKIXPublicKey(opts.PublicKeyDER)
- }
- if opts.PublicKey != "" {
- pubkey, _ /* keyhash */, rest, err := ct.PublicKeyFromPEM([]byte(opts.PublicKey))
- if err != nil {
- return nil, err
- }
- if len(rest) > 0 {
- return nil, errors.New("extra data found after PEM key decoded")
- }
- return pubkey, nil
- }
- return nil, nil
- }
- type basicLogger struct{}
- func (bl *basicLogger) Printf(msg string, args ...interface{}) {
- log.Printf(msg, args...)
- }
- // RspError represents an error that occurred when processing a response from a server,
- // and also includes key details from the http.Response that triggered the error.
- type RspError struct {
- Err error
- StatusCode int
- Body []byte
- }
- // Error formats the RspError instance, focusing on the error.
- func (e RspError) Error() string {
- return e.Err.Error()
- }
- // New constructs a new JSONClient instance, for the given base URI, using the
- // given http.Client object (if provided) and the Options object.
- // If opts does not specify a public key, signatures will not be verified.
- func New(uri string, hc *http.Client, opts Options) (*JSONClient, error) {
- pubkey, err := opts.ParsePublicKey()
- if err != nil {
- return nil, fmt.Errorf("invalid public key: %v", err)
- }
- var verifier *ct.SignatureVerifier
- if pubkey != nil {
- var err error
- verifier, err = ct.NewSignatureVerifier(pubkey)
- if err != nil {
- return nil, err
- }
- }
- if hc == nil {
- hc = new(http.Client)
- }
- logger := opts.Logger
- if logger == nil {
- logger = &basicLogger{}
- }
- return &JSONClient{
- uri: strings.TrimRight(uri, "/"),
- httpClient: hc,
- Verifier: verifier,
- logger: logger,
- backoff: &backoff{},
- userAgent: opts.UserAgent,
- }, nil
- }
- // BaseURI returns the base URI that the JSONClient makes queries to.
- func (c *JSONClient) BaseURI() string {
- return c.uri
- }
- // GetAndParse makes a HTTP GET call to the given path, and attempts to parse
- // the response as a JSON representation of the rsp structure. Returns the
- // http.Response, the body of the response, and an error (which may be of
- // type RspError if the HTTP response was available).
- func (c *JSONClient) GetAndParse(ctx context.Context, path string, params map[string]string, rsp interface{}) (*http.Response, []byte, error) {
- if ctx == nil {
- return nil, nil, errors.New("context.Context required")
- }
- // Build a GET request with URL-encoded parameters.
- vals := url.Values{}
- for k, v := range params {
- vals.Add(k, v)
- }
- fullURI := fmt.Sprintf("%s%s?%s", c.uri, path, vals.Encode())
- klog.V(2).Infof("GET %s", fullURI)
- httpReq, err := http.NewRequest(http.MethodGet, fullURI, nil)
- if err != nil {
- return nil, nil, err
- }
- if len(c.userAgent) != 0 {
- httpReq.Header.Set("User-Agent", c.userAgent)
- }
- httpRsp, err := ctxhttp.Do(ctx, c.httpClient, httpReq)
- if err != nil {
- return nil, nil, err
- }
- // Read everything now so http.Client can reuse the connection.
- body, err := io.ReadAll(httpRsp.Body)
- httpRsp.Body.Close()
- if err != nil {
- return nil, nil, RspError{Err: fmt.Errorf("failed to read response body: %v", err), StatusCode: httpRsp.StatusCode, Body: body}
- }
- if httpRsp.StatusCode != http.StatusOK {
- return nil, nil, RspError{Err: fmt.Errorf("got HTTP Status %q", httpRsp.Status), StatusCode: httpRsp.StatusCode, Body: body}
- }
- if err := json.NewDecoder(bytes.NewReader(body)).Decode(rsp); err != nil {
- return nil, nil, RspError{Err: err, StatusCode: httpRsp.StatusCode, Body: body}
- }
- return httpRsp, body, nil
- }
- // PostAndParse makes a HTTP POST call to the given path, including the request
- // parameters, and attempts to parse the response as a JSON representation of
- // the rsp structure. Returns the http.Response, the body of the response, and
- // an error (which may be of type RspError if the HTTP response was available).
- func (c *JSONClient) PostAndParse(ctx context.Context, path string, req, rsp interface{}) (*http.Response, []byte, error) {
- if ctx == nil {
- return nil, nil, errors.New("context.Context required")
- }
- // Build a POST request with JSON body.
- postBody, err := json.Marshal(req)
- if err != nil {
- return nil, nil, err
- }
- fullURI := fmt.Sprintf("%s%s", c.uri, path)
- klog.V(2).Infof("POST %s", fullURI)
- httpReq, err := http.NewRequest(http.MethodPost, fullURI, bytes.NewReader(postBody))
- if err != nil {
- return nil, nil, err
- }
- if len(c.userAgent) != 0 {
- httpReq.Header.Set("User-Agent", c.userAgent)
- }
- httpReq.Header.Set("Content-Type", "application/json")
- httpRsp, err := ctxhttp.Do(ctx, c.httpClient, httpReq)
- // Read all of the body, if there is one, so that the http.Client can do Keep-Alive.
- var body []byte
- if httpRsp != nil {
- body, err = io.ReadAll(httpRsp.Body)
- httpRsp.Body.Close()
- }
- if err != nil {
- if httpRsp != nil {
- return nil, nil, RspError{StatusCode: httpRsp.StatusCode, Body: body, Err: err}
- }
- return nil, nil, err
- }
- if httpRsp.StatusCode == http.StatusOK {
- if err = json.Unmarshal(body, &rsp); err != nil {
- return nil, nil, RspError{StatusCode: httpRsp.StatusCode, Body: body, Err: err}
- }
- }
- return httpRsp, body, nil
- }
- // waitForBackoff blocks until the defined backoff interval or context has expired, if the returned
- // not before time is in the past it returns immediately.
- func (c *JSONClient) waitForBackoff(ctx context.Context) error {
- dur := time.Until(c.backoff.until().Add(time.Millisecond * time.Duration(rand.Intn(int(maxJitter.Seconds()*1000)))))
- if dur < 0 {
- dur = 0
- }
- backoffTimer := time.NewTimer(dur)
- select {
- case <-ctx.Done():
- return ctx.Err()
- case <-backoffTimer.C:
- }
- return nil
- }
- // PostAndParseWithRetry makes a HTTP POST call, but retries (with backoff) on
- // retryable errors; the caller should set a deadline on the provided context
- // to prevent infinite retries. Return values are as for PostAndParse.
- func (c *JSONClient) PostAndParseWithRetry(ctx context.Context, path string, req, rsp interface{}) (*http.Response, []byte, error) {
- if ctx == nil {
- return nil, nil, errors.New("context.Context required")
- }
- for {
- httpRsp, body, err := c.PostAndParse(ctx, path, req, rsp)
- if err != nil {
- // Don't retry context errors.
- if err == context.Canceled || err == context.DeadlineExceeded {
- return nil, nil, err
- }
- wait := c.backoff.set(nil)
- c.logger.Printf("Request to %s failed, backing-off %s: %s", c.uri, wait, err)
- } else {
- switch {
- case httpRsp.StatusCode == http.StatusOK:
- return httpRsp, body, nil
- case httpRsp.StatusCode == http.StatusRequestTimeout:
- // Request timeout, retry immediately
- c.logger.Printf("Request to %s timed out, retrying immediately", c.uri)
- case httpRsp.StatusCode == http.StatusServiceUnavailable:
- fallthrough
- case httpRsp.StatusCode == http.StatusTooManyRequests:
- var backoff *time.Duration
- // Retry-After may be either a number of seconds as a int or a RFC 1123
- // date string (RFC 7231 Section 7.1.3)
- if retryAfter := httpRsp.Header.Get("Retry-After"); retryAfter != "" {
- if seconds, err := strconv.Atoi(retryAfter); err == nil {
- b := time.Duration(seconds) * time.Second
- backoff = &b
- } else if date, err := time.Parse(time.RFC1123, retryAfter); err == nil {
- b := time.Until(date)
- backoff = &b
- }
- }
- wait := c.backoff.set(backoff)
- c.logger.Printf("Request to %s failed, backing-off for %s: got HTTP status %s", c.uri, wait, httpRsp.Status)
- default:
- return nil, nil, RspError{
- StatusCode: httpRsp.StatusCode,
- Body: body,
- Err: fmt.Errorf("got HTTP status %q", httpRsp.Status)}
- }
- }
- if err := c.waitForBackoff(ctx); err != nil {
- return nil, nil, err
- }
- }
- }
|