vendor
This commit is contained in:
72
vendor/github.com/google/certificate-transparency-go/jsonclient/backoff.go
generated
vendored
Normal file
72
vendor/github.com/google/certificate-transparency-go/jsonclient/backoff.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2017 Google Inc. 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
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type backoff struct {
|
||||
mu sync.RWMutex
|
||||
multiplier uint
|
||||
notBefore time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
// maximum backoff is 2^(maxMultiplier-1) = 128 seconds
|
||||
maxMultiplier = 8
|
||||
)
|
||||
|
||||
func (b *backoff) set(override *time.Duration) time.Duration {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.notBefore.After(time.Now()) {
|
||||
if override != nil {
|
||||
// If existing backoff is set but override would be longer than
|
||||
// it then set it to that.
|
||||
notBefore := time.Now().Add(*override)
|
||||
if notBefore.After(b.notBefore) {
|
||||
b.notBefore = notBefore
|
||||
}
|
||||
}
|
||||
return time.Until(b.notBefore)
|
||||
}
|
||||
var wait time.Duration
|
||||
if override != nil {
|
||||
wait = *override
|
||||
} else {
|
||||
if b.multiplier < maxMultiplier {
|
||||
b.multiplier++
|
||||
}
|
||||
wait = time.Second * time.Duration(1<<(b.multiplier-1))
|
||||
}
|
||||
b.notBefore = time.Now().Add(wait)
|
||||
return wait
|
||||
}
|
||||
|
||||
func (b *backoff) decreaseMultiplier() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.multiplier > 0 {
|
||||
b.multiplier--
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backoff) until() time.Time {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.notBefore
|
||||
}
|
117
vendor/github.com/google/certificate-transparency-go/jsonclient/backoff_test.go
generated
vendored
Normal file
117
vendor/github.com/google/certificate-transparency-go/jsonclient/backoff_test.go
generated
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright 2017 Google Inc. 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
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testLeeway = 25 * time.Microsecond
|
||||
|
||||
func fuzzyTimeEquals(a, b time.Time, leeway time.Duration) bool {
|
||||
diff := math.Abs(float64(a.Sub(b).Nanoseconds()))
|
||||
if diff < float64(leeway.Nanoseconds()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fuzzyDurationEquals(a, b time.Duration, leeway time.Duration) bool {
|
||||
diff := math.Abs(float64(a.Nanoseconds() - b.Nanoseconds()))
|
||||
if diff < float64(leeway.Nanoseconds()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestBackoff(t *testing.T) {
|
||||
b := backoff{}
|
||||
|
||||
// Test that the interval increases as expected
|
||||
for i := uint(0); i < maxMultiplier; i++ {
|
||||
n := time.Now()
|
||||
interval := b.set(nil)
|
||||
if interval != time.Second*(1<<i) {
|
||||
t.Fatalf("backoff.set(nil)=%v; want %v", interval, time.Second*(1<<i))
|
||||
}
|
||||
expected := n.Add(interval)
|
||||
until := b.until()
|
||||
if !fuzzyTimeEquals(expected, until, time.Millisecond) {
|
||||
t.Fatalf("backoff.until()=%v; want %v (+ 0-250ms)", expected, until)
|
||||
}
|
||||
|
||||
// reset notBefore
|
||||
b.notBefore = time.Time{}
|
||||
}
|
||||
|
||||
// Test that multiplier doesn't go above maxMultiplier
|
||||
b.multiplier = maxMultiplier
|
||||
b.notBefore = time.Time{}
|
||||
interval := b.set(nil)
|
||||
if b.multiplier > maxMultiplier {
|
||||
t.Fatalf("backoff.multiplier=%v; want %v", b.multiplier, maxMultiplier)
|
||||
}
|
||||
if interval > time.Second*(1<<(maxMultiplier-1)) {
|
||||
t.Fatalf("backoff.set(nil)=%v; want %v", interval, 1<<(maxMultiplier-1)*time.Second)
|
||||
}
|
||||
|
||||
// Test decreaseMultiplier properly decreases the multiplier
|
||||
b.multiplier = 1
|
||||
b.notBefore = time.Time{}
|
||||
b.decreaseMultiplier()
|
||||
if b.multiplier != 0 {
|
||||
t.Fatalf("backoff.multiplier=%v; want %v", b.multiplier, 0)
|
||||
}
|
||||
|
||||
// Test decreaseMultiplier doesn't reduce multiplier below 0
|
||||
b.decreaseMultiplier()
|
||||
if b.multiplier != 0 {
|
||||
t.Fatalf("backoff.multiplier=%v; want %v", b.multiplier, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackoffOverride(t *testing.T) {
|
||||
b := backoff{}
|
||||
for _, tc := range []struct {
|
||||
notBefore time.Time
|
||||
override time.Duration
|
||||
expectedInterval time.Duration
|
||||
}{
|
||||
{
|
||||
notBefore: time.Now().Add(time.Hour),
|
||||
override: time.Second * 1800,
|
||||
expectedInterval: time.Hour,
|
||||
},
|
||||
{
|
||||
notBefore: time.Now().Add(time.Hour),
|
||||
override: time.Second * 7200,
|
||||
expectedInterval: 2 * time.Hour,
|
||||
},
|
||||
{
|
||||
notBefore: time.Time{},
|
||||
override: time.Second * 7200,
|
||||
expectedInterval: 2 * time.Hour,
|
||||
},
|
||||
} {
|
||||
b.multiplier = 0
|
||||
b.notBefore = tc.notBefore
|
||||
interval := b.set(&tc.override)
|
||||
if !fuzzyDurationEquals(tc.expectedInterval, interval, testLeeway) {
|
||||
t.Fatalf("backoff.set(%v)=%v; want %v", tc.override, interval, tc.expectedInterval)
|
||||
}
|
||||
}
|
||||
}
|
289
vendor/github.com/google/certificate-transparency-go/jsonclient/client.go
generated
vendored
Normal file
289
vendor/github.com/google/certificate-transparency-go/jsonclient/client.go
generated
vendored
Normal file
@ -0,0 +1,289 @@
|
||||
// Copyright 2016 Google Inc. 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"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"
|
||||
)
|
||||
|
||||
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. http://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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// 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{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAndParse makes a HTTP GET call to the given path, and attempta to parse
|
||||
// the response as a JSON representation of the rsp structure. Returns the
|
||||
// http.Response, the body of the response, and an error. Note that the
|
||||
// returned http.Response can be non-nil even when an error is returned,
|
||||
// in particular when the HTTP status is not OK or when the JSON parsing fails.
|
||||
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())
|
||||
httpReq, err := http.NewRequest(http.MethodGet, fullURI, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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 := ioutil.ReadAll(httpRsp.Body)
|
||||
httpRsp.Body.Close()
|
||||
if err != nil {
|
||||
return httpRsp, body, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if httpRsp.StatusCode != http.StatusOK {
|
||||
return httpRsp, body, fmt.Errorf("got HTTP Status %q", httpRsp.Status)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(bytes.NewReader(body)).Decode(rsp); err != nil {
|
||||
return httpRsp, body, err
|
||||
}
|
||||
|
||||
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. Note that the returned http.Response can be non-nil even when an
|
||||
// error is returned, in particular when the HTTP status is not OK or when the
|
||||
// JSON parsing fails.
|
||||
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)
|
||||
httpReq, err := http.NewRequest(http.MethodPost, fullURI, bytes.NewReader(postBody))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
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 = ioutil.ReadAll(httpRsp.Body)
|
||||
httpRsp.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return httpRsp, body, err
|
||||
}
|
||||
|
||||
if httpRsp.StatusCode == http.StatusOK {
|
||||
if err = json.Unmarshal(body, &rsp); err != nil {
|
||||
return httpRsp, body, 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
|
||||
// retriable 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 failed, backing-off for %s: %s", 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 timed out, retrying immediately")
|
||||
case httpRsp.StatusCode == http.StatusServiceUnavailable:
|
||||
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 := date.Sub(time.Now())
|
||||
backoff = &b
|
||||
}
|
||||
}
|
||||
wait := c.backoff.set(backoff)
|
||||
c.logger.Printf("Request failed, backing-off for %s: got HTTP status %s", wait, httpRsp.Status)
|
||||
default:
|
||||
return httpRsp, body, fmt.Errorf("got HTTP Status %q", httpRsp.Status)
|
||||
}
|
||||
}
|
||||
if err := c.waitForBackoff(ctx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
446
vendor/github.com/google/certificate-transparency-go/jsonclient/client_test.go
generated
vendored
Normal file
446
vendor/github.com/google/certificate-transparency-go/jsonclient/client_test.go
generated
vendored
Normal file
@ -0,0 +1,446 @@
|
||||
// Copyright 2016 Google Inc. 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/certificate-transparency-go/testdata"
|
||||
)
|
||||
|
||||
func publicKeyPEMToDER(key string) []byte {
|
||||
block, _ := pem.Decode([]byte(key))
|
||||
if block == nil {
|
||||
panic("failed to decode public key PEM")
|
||||
}
|
||||
if block.Type != "PUBLIC KEY" {
|
||||
panic("PEM does not have type 'PUBLIC KEY'")
|
||||
}
|
||||
return block.Bytes
|
||||
}
|
||||
|
||||
func TestNewJSONClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts Options
|
||||
errstr string
|
||||
}{
|
||||
{
|
||||
name: "invalid PublicKey",
|
||||
opts: Options{PublicKey: "bogus"},
|
||||
errstr: "no PEM block",
|
||||
},
|
||||
{
|
||||
name: "invalid PublicKeyDER",
|
||||
opts: Options{PublicKeyDER: []byte("bogus")},
|
||||
errstr: "asn1: structure error",
|
||||
},
|
||||
{
|
||||
name: "RSA PublicKey",
|
||||
opts: Options{PublicKey: testdata.RsaPublicKeyPEM},
|
||||
},
|
||||
{
|
||||
name: "RSA PublicKeyDER",
|
||||
opts: Options{PublicKeyDER: publicKeyPEMToDER(testdata.RsaPublicKeyPEM)},
|
||||
},
|
||||
{
|
||||
name: "ECDSA PublicKey",
|
||||
opts: Options{PublicKey: testdata.EcdsaPublicKeyPEM},
|
||||
},
|
||||
{
|
||||
name: "ECDSA PublicKeyDER",
|
||||
opts: Options{PublicKeyDER: publicKeyPEMToDER(testdata.EcdsaPublicKeyPEM)},
|
||||
},
|
||||
{
|
||||
name: "DSA PublicKey",
|
||||
opts: Options{PublicKey: testdata.DsaPublicKeyPEM},
|
||||
errstr: "Unsupported public key type",
|
||||
},
|
||||
{
|
||||
name: "DSA PublicKeyDER",
|
||||
opts: Options{PublicKeyDER: publicKeyPEMToDER(testdata.DsaPublicKeyPEM)},
|
||||
errstr: "Unsupported public key type",
|
||||
},
|
||||
{
|
||||
name: "PublicKey contains trailing garbage",
|
||||
opts: Options{PublicKey: testdata.RsaPublicKeyPEM + "bogus"},
|
||||
errstr: "extra data found",
|
||||
},
|
||||
{
|
||||
name: "PublicKeyDER contains trailing garbage",
|
||||
opts: Options{PublicKeyDER: append(publicKeyPEMToDER(testdata.RsaPublicKeyPEM), []byte("deadbeef")...)},
|
||||
errstr: "trailing data",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
client, err := New("http://127.0.0.1", nil, test.opts)
|
||||
if test.errstr != "" {
|
||||
if err == nil {
|
||||
t.Errorf("%v: New()=%p,nil; want error %q", test.name, client, test.errstr)
|
||||
} else if !strings.Contains(err.Error(), test.errstr) {
|
||||
t.Errorf("%v: New()=nil,%q; want error %q", test.name, err, test.errstr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%v: New()=nil,%q; want no error", test.name, err)
|
||||
} else if client == nil {
|
||||
t.Errorf("%v: New()=nil,nil; want client", test.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TestStruct struct {
|
||||
TreeSize int `json:"tree_size"`
|
||||
Timestamp int `json:"timestamp"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type TestParams struct {
|
||||
RespCode int `json:"rc"`
|
||||
}
|
||||
|
||||
func MockServer(t *testing.T, failCount int, retryAfter int) *httptest.Server {
|
||||
t.Helper()
|
||||
mu := sync.Mutex{}
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
switch r.URL.Path {
|
||||
case "/struct/path":
|
||||
fmt.Fprintf(w, `{"tree_size": 11, "timestamp": 99}`)
|
||||
case "/struct/params":
|
||||
var s TestStruct
|
||||
if r.Method == http.MethodGet {
|
||||
s.TreeSize, _ = strconv.Atoi(r.FormValue("tree_size"))
|
||||
s.Timestamp, _ = strconv.Atoi(r.FormValue("timestamp"))
|
||||
s.Data = r.FormValue("data")
|
||||
} else {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&s)
|
||||
if err != nil {
|
||||
panic("Failed to decode: " + err.Error())
|
||||
}
|
||||
defer r.Body.Close()
|
||||
}
|
||||
fmt.Fprintf(w, `{"tree_size": %d, "timestamp": %d, "data": "%s"}`, s.TreeSize, s.Timestamp, s.Data)
|
||||
case "/error":
|
||||
var params TestParams
|
||||
if r.Method == http.MethodGet {
|
||||
params.RespCode, _ = strconv.Atoi(r.FormValue("rc"))
|
||||
} else {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(¶ms)
|
||||
if err != nil {
|
||||
panic("Failed to decode: " + err.Error())
|
||||
}
|
||||
defer r.Body.Close()
|
||||
}
|
||||
http.Error(w, "error page", params.RespCode)
|
||||
case "/malformed":
|
||||
fmt.Fprintf(w, `{"tree_size": 11, "timestamp": 99`) // no closing }
|
||||
case "/retry":
|
||||
if failCount > 0 {
|
||||
failCount--
|
||||
if retryAfter != 0 {
|
||||
if retryAfter > 0 {
|
||||
w.Header().Add("Retry-After", strconv.Itoa(retryAfter))
|
||||
}
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(w, `{"tree_size": 11, "timestamp": 99}`)
|
||||
}
|
||||
case "/retry-rfc1123":
|
||||
if failCount > 0 {
|
||||
failCount--
|
||||
w.Header().Add("Retry-After", time.Now().Add(time.Duration(retryAfter)*time.Second).Format(time.RFC1123))
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
} else {
|
||||
fmt.Fprintf(w, `{"tree_size": 11, "timestamp": 99}`)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("Unhandled URL path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGetAndParse(t *testing.T) {
|
||||
rc := regexp.MustCompile
|
||||
tests := []struct {
|
||||
uri string
|
||||
params map[string]string
|
||||
status int
|
||||
result TestStruct
|
||||
errstr *regexp.Regexp
|
||||
wantBody bool
|
||||
}{
|
||||
{uri: "/short%", errstr: rc("invalid URL escape")},
|
||||
{uri: "/malformed", status: http.StatusOK, errstr: rc("unexpected EOF"), wantBody: true},
|
||||
{uri: "/error", params: map[string]string{"rc": "404"}, status: http.StatusNotFound, wantBody: true},
|
||||
{uri: "/error", params: map[string]string{"rc": "403"}, status: http.StatusForbidden, wantBody: true},
|
||||
{uri: "/struct/path", status: http.StatusOK, result: TestStruct{11, 99, ""}, wantBody: true},
|
||||
{
|
||||
uri: "/struct/params",
|
||||
status: http.StatusOK,
|
||||
params: map[string]string{"tree_size": "42", "timestamp": "88", "data": "abcd"},
|
||||
result: TestStruct{42, 88, "abcd"},
|
||||
wantBody: true,
|
||||
},
|
||||
}
|
||||
|
||||
ts := MockServer(t, -1, 0)
|
||||
defer ts.Close()
|
||||
|
||||
logClient, err := New(ts.URL, &http.Client{}, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
for _, test := range tests {
|
||||
var result TestStruct
|
||||
httpRsp, body, err := logClient.GetAndParse(ctx, test.uri, test.params, &result)
|
||||
if gotBody := (body != nil); gotBody != test.wantBody {
|
||||
t.Errorf("GetAndParse(%q) got body? %v, want? %v", test.uri, gotBody, test.wantBody)
|
||||
}
|
||||
if test.errstr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("GetAndParse(%q)=%+v,_,nil; want error matching %q", test.uri, result, test.errstr)
|
||||
} else if !test.errstr.MatchString(err.Error()) {
|
||||
t.Errorf("GetAndParse(%q)=nil,_,%q; want error matching %q", test.uri, err.Error(), test.errstr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if httpRsp.StatusCode != test.status {
|
||||
t.Errorf("GetAndParse('%s') got status %d; want %d", test.uri, httpRsp.StatusCode, test.status)
|
||||
}
|
||||
if test.status == http.StatusOK {
|
||||
if err != nil {
|
||||
t.Errorf("GetAndParse(%q)=nil,_,%q; want %+v", test.uri, err.Error(), result)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.result) {
|
||||
t.Errorf("GetAndParse(%q)=%+v,_,nil; want %+v", test.uri, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAndParse(t *testing.T) {
|
||||
rc := regexp.MustCompile
|
||||
tests := []struct {
|
||||
uri string
|
||||
request interface{}
|
||||
status int
|
||||
result TestStruct
|
||||
errstr *regexp.Regexp
|
||||
wantBody bool
|
||||
}{
|
||||
{uri: "/short%", errstr: rc("invalid URL escape")},
|
||||
{uri: "/struct/params", request: json.Number(`invalid`), errstr: rc("invalid number literal")},
|
||||
{uri: "/malformed", status: http.StatusOK, errstr: rc("unexpected end of JSON"), wantBody: true},
|
||||
{uri: "/error", request: TestParams{RespCode: 404}, status: http.StatusNotFound, wantBody: true},
|
||||
{uri: "/error", request: TestParams{RespCode: 403}, status: http.StatusForbidden, wantBody: true},
|
||||
{uri: "/struct/path", status: http.StatusOK, result: TestStruct{11, 99, ""}, wantBody: true},
|
||||
{
|
||||
uri: "/struct/params",
|
||||
status: http.StatusOK,
|
||||
request: TestStruct{42, 88, "abcd"},
|
||||
result: TestStruct{42, 88, "abcd"},
|
||||
wantBody: true,
|
||||
},
|
||||
}
|
||||
|
||||
ts := MockServer(t, -1, 0)
|
||||
defer ts.Close()
|
||||
|
||||
logClient, err := New(ts.URL, &http.Client{}, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
for _, test := range tests {
|
||||
var result TestStruct
|
||||
httpRsp, body, err := logClient.PostAndParse(ctx, test.uri, test.request, &result)
|
||||
if gotBody := (body != nil); gotBody != test.wantBody {
|
||||
t.Errorf("GetAndParse(%q) returned body %v, wanted %v", test.uri, gotBody, test.wantBody)
|
||||
}
|
||||
if test.errstr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("PostAndParse(%q)=%+v,nil; want error matching %q", test.uri, result, test.errstr)
|
||||
} else if !test.errstr.MatchString(err.Error()) {
|
||||
t.Errorf("PostAndParse(%q)=nil,%q; want error matching %q", test.uri, err.Error(), test.errstr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if httpRsp.StatusCode != test.status {
|
||||
t.Errorf("PostAndParse(%q) got status %d; want %d", test.uri, httpRsp.StatusCode, test.status)
|
||||
}
|
||||
if test.status == http.StatusOK {
|
||||
if err != nil {
|
||||
t.Errorf("PostAndParse(%q)=nil,%q; want %+v", test.uri, err.Error(), test.result)
|
||||
}
|
||||
if !reflect.DeepEqual(result, test.result) {
|
||||
t.Errorf("PostAndParse(%q)=%+v,nil; want %+v", test.uri, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mockBackoff is not safe for concurrent usage
|
||||
type mockBackoff struct {
|
||||
override time.Duration
|
||||
}
|
||||
|
||||
func (mb *mockBackoff) set(o *time.Duration) time.Duration {
|
||||
if o != nil {
|
||||
mb.override = *o
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (mb *mockBackoff) decreaseMultiplier() {}
|
||||
func (mb *mockBackoff) until() time.Time { return time.Time{} }
|
||||
|
||||
func TestPostAndParseWithRetry(t *testing.T) {
|
||||
tests := []struct {
|
||||
uri string
|
||||
request interface{}
|
||||
deadlineSecs int // -1 indicates no deadline
|
||||
retryAfter int // -1 indicates generate 503 with no Retry-After
|
||||
failCount int
|
||||
errstr string
|
||||
expectedBackoff time.Duration // 0 indicates no expected backoff override set
|
||||
}{
|
||||
{
|
||||
uri: "/error",
|
||||
request: TestParams{RespCode: 418},
|
||||
deadlineSecs: -1,
|
||||
retryAfter: 0,
|
||||
failCount: 0,
|
||||
errstr: "teapot",
|
||||
expectedBackoff: 0,
|
||||
},
|
||||
{
|
||||
uri: "/short%",
|
||||
request: nil,
|
||||
deadlineSecs: 0,
|
||||
retryAfter: 0,
|
||||
failCount: 0,
|
||||
errstr: "deadline exceeded",
|
||||
expectedBackoff: 0,
|
||||
},
|
||||
{
|
||||
uri: "/retry",
|
||||
request: nil,
|
||||
deadlineSecs: -1,
|
||||
retryAfter: 0,
|
||||
failCount: 1,
|
||||
errstr: "",
|
||||
expectedBackoff: 0,
|
||||
},
|
||||
{
|
||||
uri: "/retry",
|
||||
request: nil,
|
||||
deadlineSecs: -1,
|
||||
retryAfter: 5,
|
||||
failCount: 1,
|
||||
errstr: "",
|
||||
expectedBackoff: 5 * time.Second,
|
||||
},
|
||||
{
|
||||
uri: "/retry-rfc1123",
|
||||
request: nil,
|
||||
deadlineSecs: -1,
|
||||
retryAfter: 5,
|
||||
failCount: 1,
|
||||
errstr: "",
|
||||
expectedBackoff: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
ts := MockServer(t, test.failCount, test.retryAfter)
|
||||
defer ts.Close()
|
||||
|
||||
logClient, err := New(ts.URL, &http.Client{}, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mb := mockBackoff{}
|
||||
logClient.backoff = &mb
|
||||
ctx := context.Background()
|
||||
if test.deadlineSecs >= 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Duration(test.deadlineSecs)*time.Second))
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
var result TestStruct
|
||||
httpRsp, _, err := logClient.PostAndParseWithRetry(ctx, test.uri, test.request, &result)
|
||||
if test.errstr != "" {
|
||||
if err == nil {
|
||||
t.Errorf("PostAndParseWithRetry()=%+v,nil; want error %q", result, test.errstr)
|
||||
} else if !strings.Contains(err.Error(), test.errstr) {
|
||||
t.Errorf("PostAndParseWithRetry()=nil,%q; want error %q", err.Error(), test.errstr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("PostAndParseWithRetry()=nil,%q; want no error", err.Error())
|
||||
} else if httpRsp.StatusCode != http.StatusOK {
|
||||
t.Errorf("PostAndParseWithRetry() got status %d; want OK(404)", httpRsp.StatusCode)
|
||||
}
|
||||
if test.expectedBackoff > 0 && !fuzzyDurationEquals(test.expectedBackoff, mb.override, time.Second) {
|
||||
t.Errorf("Unexpected backoff override set: got: %s, wanted: %s", mb.override, test.expectedBackoff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextRequired(t *testing.T) {
|
||||
ts := MockServer(t, -1, 0)
|
||||
defer ts.Close()
|
||||
|
||||
logClient, err := New(ts.URL, &http.Client{}, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var result TestStruct
|
||||
_, _, err = logClient.GetAndParse(nil, "/struct/path", nil, &result)
|
||||
if err == nil {
|
||||
t.Errorf("GetAndParse() succeeded with empty Context")
|
||||
}
|
||||
_, _, err = logClient.PostAndParse(nil, "/struct/path", nil, &result)
|
||||
if err == nil {
|
||||
t.Errorf("PostAndParse() succeeded with empty Context")
|
||||
}
|
||||
_, _, err = logClient.PostAndParseWithRetry(nil, "/struct/path", nil, &result)
|
||||
if err == nil {
|
||||
t.Errorf("PostAndParseWithRetry() succeeded with empty Context")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user