Implement CAA quorum checking after failure (#1763)
When a CAA request to Unbound times out, fall back to checking CAA via Google Public DNS' HTTPS API, through multiple proxies so as to hit geographically distributed paths. All successful multipath responses must be identical in order to succeed, and at most one can fail. Fixes #1618
This commit is contained in:
parent
2c39c684cb
commit
35b6e83e81
|
@ -176,23 +176,27 @@
|
|||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/context",
|
||||
"Rev": "4876518f9e71663000c348837735820161a42df7"
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/context/ctxhttp",
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/http2",
|
||||
"Rev": "4876518f9e71663000c348837735820161a42df7"
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/http2/hpack",
|
||||
"Rev": "4876518f9e71663000c348837735820161a42df7"
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/timeseries",
|
||||
"Rev": "4876518f9e71663000c348837735820161a42df7"
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/trace",
|
||||
"Rev": "4876518f9e71663000c348837735820161a42df7"
|
||||
"Rev": "b797637b7aeeed133049c7281bfa31dcc9ca42d6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "google.golang.org/grpc",
|
||||
|
|
|
@ -82,12 +82,12 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
|
|||
return nil, &DNSError{dns.TypeCAA, "always.timeout", MockTimeoutError(), -1}
|
||||
case "reserved.com":
|
||||
record.Tag = "issue"
|
||||
record.Value = "symantec.com"
|
||||
record.Value = "ca.com"
|
||||
results = append(results, &record)
|
||||
case "critical.com":
|
||||
record.Flag = 1
|
||||
record.Tag = "issue"
|
||||
record.Value = "symantec.com"
|
||||
record.Value = "ca.com"
|
||||
results = append(results, &record)
|
||||
case "present.com", "present.servfail.com":
|
||||
record.Tag = "issue"
|
||||
|
@ -101,7 +101,7 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
|
|||
case "multi-crit-present.com":
|
||||
record.Flag = 1
|
||||
record.Tag = "issue"
|
||||
record.Value = "symantec.com"
|
||||
record.Value = "ca.com"
|
||||
results = append(results, &record)
|
||||
secondRecord := record
|
||||
secondRecord.Value = "letsencrypt.org"
|
||||
|
@ -129,6 +129,8 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
|
|||
record.Tag = "issue"
|
||||
record.Value = ";"
|
||||
results = append(results, &record)
|
||||
case "bad-local-resolver.com":
|
||||
return nil, DNSError{underlying: MockTimeoutError()}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
|
|
@ -46,6 +46,14 @@ func (d DNSError) Error() string {
|
|||
dns.TypeToString[d.recordType], d.hostname)
|
||||
}
|
||||
|
||||
// Timeout returns true if the underlying error was a timeout
|
||||
func (d DNSError) Timeout() bool {
|
||||
if netErr, ok := d.underlying.(*net.OpError); ok {
|
||||
return netErr.Timeout()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const detailDNSTimeout = "query timed out"
|
||||
const detailDNSNetFailure = "networking error"
|
||||
const detailServerFailure = "server failure at resolver"
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
package cdr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/metrics"
|
||||
)
|
||||
|
||||
// We have found a number of network operators which block or drop CAA
|
||||
// queries that pass through their network which leads to consistent
|
||||
// timeout failures from certain network perspectives. We have been
|
||||
// unable to find a network solution to this so we are required to
|
||||
// implement a multi-path resolution technique. This is a real hack and
|
||||
// to be honest probably not the best solution to this problem. Ideally
|
||||
// we would control our own distributed multi-path resolver but there
|
||||
// are no publicly available ones.
|
||||
//
|
||||
// This implementation talks to the Google Public DNS resolver over
|
||||
// multiple paths using HTTP proxies with geographically distributed
|
||||
// endpoints. In case the Google resolver encounters the same issues we do
|
||||
// multiple queries for the same name in parallel and we require a M of N
|
||||
// quorum of responses to return the SUCCESS return code. In order to prevent
|
||||
// the case where an attacker may be able to exploit the Google resolver in
|
||||
// some way we also require that the records returned from all requests are
|
||||
// the same (as far as I can tell the Google DNS implementation doesn't share
|
||||
// cache state between the distributed nodes so this should be safe).
|
||||
//
|
||||
// Since DNS isn't a super secure protocol and Google has recently introduced
|
||||
// a public HTTPS API for their DNS resolver so instead we use that.
|
||||
//
|
||||
// API reference:
|
||||
// https://developers.google.com/speed/public-dns/docs/dns-over-https#api_specification
|
||||
|
||||
var apiURI = "https://dns.google.com/resolve"
|
||||
|
||||
func parseAnswer(as []core.GPDNSAnswer) ([]*dns.CAA, error) {
|
||||
rrs := []*dns.CAA{}
|
||||
// only bother parsing out CAA records
|
||||
for _, a := range as {
|
||||
if a.Type != dns.TypeCAA {
|
||||
continue
|
||||
}
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s %d IN CAA %s", a.Name, a.TTL, a.Data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if caaRR, ok := rr.(*dns.CAA); ok {
|
||||
rrs = append(rrs, caaRR)
|
||||
}
|
||||
}
|
||||
return rrs, nil
|
||||
}
|
||||
|
||||
func createClient(proxy string) (*http.Client, string, error) {
|
||||
u, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(u),
|
||||
TLSHandshakeTimeout: 10 * time.Second, // Same as http.DefaultTransport, doesn't override context
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}, u.Host, nil
|
||||
}
|
||||
|
||||
// CAADistributedResolver holds state needed to talk to GPDNS
|
||||
type CAADistributedResolver struct {
|
||||
URI string
|
||||
Clients map[string]*http.Client
|
||||
stats metrics.Scope
|
||||
maxFailures int
|
||||
timeout time.Duration
|
||||
logger blog.Logger
|
||||
}
|
||||
|
||||
// New returns an initialized CAADistributedResolver which requires a M of N
|
||||
// quorum to succeed where N = len(proxies) and M = N - maxFailures
|
||||
func New(scope metrics.Scope, timeout time.Duration, maxFailures int, proxies []string, logger blog.Logger) (*CAADistributedResolver, error) {
|
||||
cdr := &CAADistributedResolver{
|
||||
Clients: make(map[string]*http.Client, len(proxies)),
|
||||
URI: apiURI,
|
||||
stats: scope,
|
||||
maxFailures: maxFailures,
|
||||
timeout: timeout,
|
||||
logger: logger,
|
||||
}
|
||||
for _, p := range proxies {
|
||||
c, h, err := createClient(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cdr.Clients[h] = c
|
||||
}
|
||||
return cdr, nil
|
||||
}
|
||||
|
||||
// queryCAA sends the query request to the GPD API. If the return code is
|
||||
// dns.RcodeSuccess the 'Answer' section is parsed for CAA records, otherwise
|
||||
// an error is returned. Unlike bdns.DNSResolver.LookupCAA it will not repeat
|
||||
// failed queries if the context has not expired as we expect to be running
|
||||
// multiple queries in parallel and only need a M of N quorum (we also expect
|
||||
// GPD to have quite good availability)
|
||||
func (cdr *CAADistributedResolver) queryCAA(ctx context.Context, req *http.Request, ic *http.Client) ([]*dns.CAA, error) {
|
||||
apiResp, err := ctxhttp.Do(ctx, ic, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = apiResp.Body.Close()
|
||||
}()
|
||||
body, err := ioutil.ReadAll(&io.LimitedReader{R: apiResp.Body, N: 1024})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.StatusCode != http.StatusOK {
|
||||
if string(body) != "" {
|
||||
return nil, fmt.Errorf("Unexpected HTTP status code %d, body: %s", apiResp.StatusCode, body)
|
||||
}
|
||||
return nil, fmt.Errorf("Unexpected HTTP status code %d", apiResp.StatusCode)
|
||||
}
|
||||
var respObj core.GPDNSResponse
|
||||
err = json.Unmarshal(body, &respObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if respObj.Status != dns.RcodeSuccess {
|
||||
if respObj.Comment != "" {
|
||||
return nil, fmt.Errorf("Query failed with %s: %s", dns.RcodeToString[respObj.Status], respObj.Comment)
|
||||
}
|
||||
return nil, fmt.Errorf("Query failed wtih %s", dns.RcodeToString[respObj.Status])
|
||||
}
|
||||
|
||||
return parseAnswer(respObj.Answer)
|
||||
}
|
||||
|
||||
type queryResult struct {
|
||||
records []*dns.CAA
|
||||
err error
|
||||
}
|
||||
|
||||
type caaSet []*dns.CAA
|
||||
|
||||
func (cs caaSet) Len() int { return len(cs) }
|
||||
func (cs caaSet) Less(i, j int) bool { return cs[i].Value < cs[j].Value } // sort by value...?
|
||||
func (cs caaSet) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] }
|
||||
|
||||
func marshalCanonicalCAASet(set []*dns.CAA) ([]byte, error) {
|
||||
var err error
|
||||
offset, size := 0, 0
|
||||
sortedSet := caaSet(set)
|
||||
sort.Sort(sortedSet)
|
||||
for _, rr := range sortedSet {
|
||||
size += dns.Len(rr)
|
||||
}
|
||||
tbh := make([]byte, size)
|
||||
for _, rr := range sortedSet {
|
||||
ttl := rr.Hdr.Ttl
|
||||
rr.Hdr.Ttl = 0 // only variable that should jitter
|
||||
offset, err = dns.PackRR(rr, tbh, offset, nil, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rr.Hdr.Ttl = ttl
|
||||
}
|
||||
return tbh, nil
|
||||
}
|
||||
|
||||
// LookupCAA performs a multipath CAA DNS lookup using GPDNS
|
||||
func (cdr *CAADistributedResolver) LookupCAA(ctx context.Context, domain string) ([]*dns.CAA, error) {
|
||||
query := make(url.Values)
|
||||
query.Add("name", domain)
|
||||
query.Add("type", strconv.Itoa(int(dns.TypeCAA))) // CAA
|
||||
rawQuery := query.Encode()
|
||||
|
||||
// min of ctx deadline and time.Now().Add(cdr.timeout)
|
||||
caaCtx, cancel := context.WithTimeout(ctx, cdr.timeout)
|
||||
defer cancel()
|
||||
results := make(chan queryResult, len(cdr.Clients))
|
||||
for addr, interfaceClient := range cdr.Clients {
|
||||
req, err := http.NewRequest("GET", cdr.URI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.URL.RawQuery = rawQuery
|
||||
go func(ic *http.Client, ia string) {
|
||||
started := time.Now()
|
||||
records, err := cdr.queryCAA(caaCtx, req, ic)
|
||||
cdr.stats.TimingDuration(fmt.Sprintf("CDR.GPDNS.Latency.%s", ia), time.Since(started))
|
||||
if err != nil {
|
||||
cdr.stats.Inc(fmt.Sprintf("CDR.GPDNS.Failures.%s", ia), 1)
|
||||
cdr.logger.Err(fmt.Sprintf("queryCAA failed [via %s]: %s", ia, err))
|
||||
}
|
||||
results <- queryResult{records, err}
|
||||
}(interfaceClient, addr)
|
||||
}
|
||||
// collect everything
|
||||
failed := 0
|
||||
var CAAs []*dns.CAA
|
||||
var canonicalSet []byte
|
||||
var err error
|
||||
for i := 0; i < len(cdr.Clients); i++ {
|
||||
r := <-results
|
||||
if r.err != nil {
|
||||
failed++
|
||||
if failed > cdr.maxFailures {
|
||||
cdr.stats.Inc("CDR.QuorumFailed", 1)
|
||||
cdr.logger.Err(fmt.Sprintf("%d out of %d CAA queries failed", len(cdr.Clients), failed))
|
||||
return nil, r.err
|
||||
}
|
||||
}
|
||||
if CAAs == nil {
|
||||
CAAs = r.records
|
||||
canonicalSet, err = marshalCanonicalCAASet(CAAs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
thisSet, err := marshalCanonicalCAASet(r.records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(r.records) != len(CAAs) || !bytes.Equal(thisSet, canonicalSet) {
|
||||
cdr.stats.Inc("CDR.MismatchedSet", 1)
|
||||
return nil, errors.New("mismatching CAA record sets were returned")
|
||||
}
|
||||
}
|
||||
}
|
||||
cdr.stats.Inc("CDR.Quorum", 1)
|
||||
return CAAs, nil
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package cdr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/metrics"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
var log = blog.UseMock()
|
||||
|
||||
func TestParseAnswer(t *testing.T) {
|
||||
as := []core.GPDNSAnswer{
|
||||
{"a", 257, 10, "0 issue \"ca.com\""},
|
||||
{"b", 1, 10, "1.1.1.1"},
|
||||
}
|
||||
|
||||
r, err := parseAnswer(as)
|
||||
test.AssertNotError(t, err, "Failed to parse records")
|
||||
test.AssertEquals(t, len(r), 1)
|
||||
test.AssertEquals(t, r[0].Hdr.Name, "a.")
|
||||
test.AssertEquals(t, r[0].Hdr.Ttl, uint32(10))
|
||||
test.AssertEquals(t, r[0].Flag, uint8(0))
|
||||
test.AssertEquals(t, r[0].Tag, "issue")
|
||||
test.AssertEquals(t, r[0].Value, "ca.com")
|
||||
}
|
||||
|
||||
func TestQueryCAA(t *testing.T) {
|
||||
testServ := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
|
||||
defer testServ.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", testServ.URL, nil)
|
||||
test.AssertNotError(t, err, "Failed to create request")
|
||||
query := make(url.Values)
|
||||
query.Add("name", "test-domain")
|
||||
query.Add("type", "257") // CAA
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
client := new(http.Client)
|
||||
cpr := CAADistributedResolver{logger: log}
|
||||
set, err := cpr.queryCAA(context.Background(), req, client)
|
||||
test.AssertNotError(t, err, "queryCAA failed")
|
||||
test.AssertEquals(t, len(set), 1)
|
||||
test.AssertEquals(t, set[0].Hdr.Name, "test-domain.")
|
||||
test.AssertEquals(t, set[0].Hdr.Ttl, uint32(10))
|
||||
test.AssertEquals(t, set[0].Flag, uint8(0))
|
||||
test.AssertEquals(t, set[0].Tag, "issue")
|
||||
test.AssertEquals(t, set[0].Value, "ca.com")
|
||||
}
|
||||
|
||||
func TestLookupCAA(t *testing.T) {
|
||||
testSrv := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
|
||||
defer testSrv.Close()
|
||||
|
||||
cpr := CAADistributedResolver{
|
||||
logger: log,
|
||||
Clients: map[string]*http.Client{
|
||||
"1.1.1.1": new(http.Client),
|
||||
"2.2.2.2": new(http.Client),
|
||||
"3.3.3.3": new(http.Client),
|
||||
},
|
||||
stats: metrics.NewNoopScope(),
|
||||
maxFailures: 1,
|
||||
timeout: time.Second,
|
||||
URI: testSrv.URL,
|
||||
}
|
||||
|
||||
set, err := cpr.LookupCAA(context.Background(), "test-domain")
|
||||
test.AssertNotError(t, err, "LookupCAA method failed")
|
||||
test.AssertEquals(t, len(set), 1)
|
||||
test.AssertEquals(t, set[0].Hdr.Name, "test-domain.")
|
||||
test.AssertEquals(t, set[0].Hdr.Ttl, uint32(10))
|
||||
test.AssertEquals(t, set[0].Flag, uint8(0))
|
||||
test.AssertEquals(t, set[0].Tag, "issue")
|
||||
test.AssertEquals(t, set[0].Value, "ca.com")
|
||||
|
||||
set, err = cpr.LookupCAA(context.Background(), "break")
|
||||
test.AssertError(t, err, "LookupCAA should've failed")
|
||||
test.Assert(t, set == nil, "LookupCAA returned non-nil CAA set")
|
||||
|
||||
set, err = cpr.LookupCAA(context.Background(), "break-rcode")
|
||||
test.AssertError(t, err, "LookupCAA should've failed")
|
||||
test.Assert(t, set == nil, "LookupCAA returned non-nil CAA set")
|
||||
|
||||
set, err = cpr.LookupCAA(context.Background(), "break-dns-quorum")
|
||||
test.AssertError(t, err, "LookupCAA should've failed")
|
||||
test.Assert(t, set == nil, "LookupCAA returned non-nil CAA set")
|
||||
}
|
||||
|
||||
type slightlyBrokenHandler struct {
|
||||
broken bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (sbh *slightlyBrokenHandler) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
sbh.mu.Lock()
|
||||
defer sbh.mu.Unlock()
|
||||
if sbh.broken {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
sbh.broken = true
|
||||
mocks.GPDNSHandler(w, r)
|
||||
}
|
||||
|
||||
func TestHTTPQuorum(t *testing.T) {
|
||||
sbh := &slightlyBrokenHandler{}
|
||||
testSrv := httptest.NewServer(http.HandlerFunc(sbh.Handler))
|
||||
defer testSrv.Close()
|
||||
|
||||
cpr := CAADistributedResolver{
|
||||
logger: log,
|
||||
Clients: map[string]*http.Client{
|
||||
"1.1.1.1": new(http.Client),
|
||||
"2.2.2.2": new(http.Client),
|
||||
"3.3.3.3": new(http.Client),
|
||||
},
|
||||
stats: metrics.NewNoopScope(),
|
||||
maxFailures: 1,
|
||||
timeout: time.Second,
|
||||
URI: testSrv.URL,
|
||||
}
|
||||
|
||||
set, err := cpr.LookupCAA(context.Background(), "test-domain")
|
||||
test.AssertError(t, err, "LookupCAA should've failed")
|
||||
test.Assert(t, set == nil, "LookupCAA returned non-nil CAA set")
|
||||
}
|
||||
|
||||
func TestMarshalCanonicalCAASet(t *testing.T) {
|
||||
a, b := new(dns.CAA), new(dns.CAA)
|
||||
a.Value, b.Value = "a", "b"
|
||||
setA := []*dns.CAA{a, b}
|
||||
setB := []*dns.CAA{b, a}
|
||||
canonA, err := marshalCanonicalCAASet(setA)
|
||||
test.AssertNotError(t, err, "marshalCanonicalCAASet failed")
|
||||
canonB, err := marshalCanonicalCAASet(setB)
|
||||
test.AssertNotError(t, err, "marshalCanonicalCAASet failed")
|
||||
test.Assert(t, bytes.Equal(canonA, canonB), "sets do not match")
|
||||
cRR := dns.Copy(b)
|
||||
c := cRR.(*dns.CAA)
|
||||
c.Value = "c"
|
||||
c.Hdr.Ttl = 100
|
||||
hashC, err := marshalCanonicalCAASet([]*dns.CAA{c, a})
|
||||
test.AssertNotError(t, err, "marshalCanonicalCAASet failed")
|
||||
test.AssertEquals(t, c.Hdr.Ttl, uint32(100))
|
||||
test.Assert(t, bytes.Equal(canonA, canonB), fmt.Sprintf("Mismatching sets had same bytes: %x == %x", hashC, canonB))
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/jmhodges/clock"
|
||||
|
||||
"github.com/letsencrypt/boulder/bdns"
|
||||
"github.com/letsencrypt/boulder/cdr"
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
bgrpc "github.com/letsencrypt/boulder/grpc"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
|
@ -50,12 +51,24 @@ func main() {
|
|||
cmd.FailOnError(err, "Failed to load credentials and create connection to service")
|
||||
caaClient = caaPB.NewCAACheckerClient(conn)
|
||||
}
|
||||
clk := clock.Default()
|
||||
scoped := metrics.NewStatsdScope(stats, "VA", "DNS")
|
||||
sbc := newGoogleSafeBrowsing(c.VA.GoogleSafeBrowsing)
|
||||
vai := va.NewValidationAuthorityImpl(pc, sbc, caaClient, stats, clk)
|
||||
var cdrClient *cdr.CAADistributedResolver
|
||||
if c.VA.CAADistributedResolver != nil {
|
||||
var err error
|
||||
cdrClient, err = cdr.New(
|
||||
scoped,
|
||||
c.VA.CAADistributedResolver.Timeout.Duration,
|
||||
c.VA.CAADistributedResolver.MaxFailures,
|
||||
c.VA.CAADistributedResolver.Proxies,
|
||||
logger,
|
||||
)
|
||||
cmd.FailOnError(err, "Failed to create CAADistributedResolver")
|
||||
}
|
||||
clk := clock.Default()
|
||||
vai := va.NewValidationAuthorityImpl(pc, sbc, caaClient, cdrClient, stats, clk)
|
||||
dnsTimeout, err := time.ParseDuration(c.Common.DNSTimeout)
|
||||
cmd.FailOnError(err, "Couldn't parse DNS timeout")
|
||||
scoped := metrics.NewStatsdScope(stats, "VA", "DNS")
|
||||
dnsTries := c.VA.DNSTries
|
||||
if dnsTries < 1 {
|
||||
dnsTries = 1
|
||||
|
|
|
@ -14,8 +14,9 @@ import (
|
|||
"time"
|
||||
|
||||
cfsslConfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/pkcs11key"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
)
|
||||
|
||||
// Config stores configuration parameters that applications
|
||||
|
@ -90,6 +91,8 @@ type Config struct {
|
|||
|
||||
CAAService *GRPCClientConfig
|
||||
|
||||
CAADistributedResolver *CAADistributedResolverConfig
|
||||
|
||||
// The number of times to try a DNS query (that has a temporary error)
|
||||
// before giving up. May be short-circuited by deadlines. A zero value
|
||||
// will be turned into 1.
|
||||
|
@ -525,3 +528,11 @@ type PortConfig struct {
|
|||
HTTPSPort int
|
||||
TLSPort int
|
||||
}
|
||||
|
||||
// CAADistributedResolverConfig specifies the HTTP client setup and interfaces
|
||||
// needed to resolve CAA addresses over multiple paths
|
||||
type CAADistributedResolverConfig struct {
|
||||
Timeout ConfigDuration
|
||||
MaxFailures int
|
||||
Proxies []string
|
||||
}
|
||||
|
|
|
@ -669,3 +669,27 @@ type FQDNSet struct {
|
|||
Issued time.Time
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// GPDNSAnswer represents a DNS record returned by the Google Public DNS API
|
||||
type GPDNSAnswer struct {
|
||||
Name string `json:"name"`
|
||||
Type uint16 `json:"type"`
|
||||
TTL int `json:"TTL"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// GPDNSAnswer represents a DNS record returned by the Google Public DNS API
|
||||
type GPDNSResponse struct {
|
||||
// Ignored fields
|
||||
// tc
|
||||
// rd
|
||||
// ra
|
||||
// ad
|
||||
// cd
|
||||
// question
|
||||
// additional
|
||||
// edns_client_subnet
|
||||
Status int `json:"Status"`
|
||||
Answer []GPDNSAnswer `json:"Answer"`
|
||||
Comment string `json:"Comment"`
|
||||
}
|
||||
|
|
|
@ -6,19 +6,24 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/cactus/go-statsd-client/statsd"
|
||||
"github.com/jmhodges/clock"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/square/go-jose"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
)
|
||||
|
||||
// StorageAuthority is a mock
|
||||
|
@ -410,3 +415,41 @@ func (m *Mailer) SendMail(to []string, subject, msg string) error {
|
|||
func (m *Mailer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GPDNSHandler mocks the Google Public DNS API
|
||||
func GPDNSHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Query().Get("name") {
|
||||
case "test-domain", "bad-local-resolver.com":
|
||||
resp := core.GPDNSResponse{
|
||||
Status: dns.RcodeSuccess,
|
||||
Answer: []core.GPDNSAnswer{
|
||||
{r.URL.Query().Get("name"), 257, 10, "0 issue \"ca.com\""},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
case "break":
|
||||
w.WriteHeader(400)
|
||||
case "break-rcode":
|
||||
data, err := json.Marshal(core.GPDNSResponse{Status: dns.RcodeServerFailure})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
case "break-dns-quorum":
|
||||
resp := core.GPDNSResponse{
|
||||
Status: dns.RcodeSuccess,
|
||||
Answer: []core.GPDNSAnswer{
|
||||
{r.URL.Query().Get("name"), 257, 10, strconv.Itoa(mrand.Int())},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -215,6 +215,12 @@
|
|||
"clientCertificatePath": "test/grpc-creds/client.pem",
|
||||
"clientKeyPath": "test/grpc-creds/key.pem"
|
||||
},
|
||||
"caaPublicResolver": {
|
||||
"timeout": "10s",
|
||||
"keepalive": "30s",
|
||||
"maxFailures": 1,
|
||||
"proxies": []
|
||||
},
|
||||
"amqp": {
|
||||
"serverURLFile": "test/secrets/amqp_url",
|
||||
"insecure": true,
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestIsSafeDomain(t *testing.T) {
|
|||
sbc.EXPECT().IsListed("bad.com").Return("bad", nil)
|
||||
sbc.EXPECT().IsListed("errorful.com").Return("", errors.New("welp"))
|
||||
sbc.EXPECT().IsListed("outofdate.com").Return("", safebrowsing.ErrOutOfDateHashes)
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, sbc, nil, stats, clock.NewFake())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, sbc, nil, nil, stats, clock.NewFake())
|
||||
|
||||
resp, err := va.IsSafeDomain(ctx, &core.IsSafeDomainRequest{Domain: "good.com"})
|
||||
if err != nil {
|
||||
|
@ -65,7 +65,7 @@ func TestIsSafeDomain(t *testing.T) {
|
|||
|
||||
func TestAllowNilInIsSafeDomain(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.NewFake())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.NewFake())
|
||||
|
||||
// Be cool with a nil SafeBrowsing. This will happen in prod when we have
|
||||
// flag mismatch between the VA and RA.
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/letsencrypt/boulder/bdns"
|
||||
"github.com/letsencrypt/boulder/cdr"
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
bgrpc "github.com/letsencrypt/boulder/grpc"
|
||||
|
@ -56,10 +57,12 @@ type ValidationAuthorityImpl struct {
|
|||
stats statsd.Statter
|
||||
clk clock.Clock
|
||||
caaClient caaPB.CAACheckerClient
|
||||
caaDR *cdr.CAADistributedResolver
|
||||
}
|
||||
|
||||
// NewValidationAuthorityImpl constructs a new VA
|
||||
func NewValidationAuthorityImpl(pc *cmd.PortConfig, sbc SafeBrowsing, caaClient caaPB.CAACheckerClient, stats statsd.Statter, clk clock.Clock) *ValidationAuthorityImpl {
|
||||
func NewValidationAuthorityImpl(pc *cmd.PortConfig, sbc SafeBrowsing, caaClient caaPB.CAACheckerClient,
|
||||
cdrClient *cdr.CAADistributedResolver, stats statsd.Statter, clk clock.Clock) *ValidationAuthorityImpl {
|
||||
logger := blog.Get()
|
||||
return &ValidationAuthorityImpl{
|
||||
SafeBrowsing: sbc,
|
||||
|
@ -70,6 +73,7 @@ func NewValidationAuthorityImpl(pc *cmd.PortConfig, sbc SafeBrowsing, caaClient
|
|||
stats: stats,
|
||||
clk: clk,
|
||||
caaClient: caaClient,
|
||||
caaDR: cdrClient,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -737,9 +741,44 @@ func newCAASet(CAAs []*dns.CAA) *CAASet {
|
|||
return &filtered
|
||||
}
|
||||
|
||||
type caaResult struct {
|
||||
records []*dns.CAA
|
||||
err error
|
||||
}
|
||||
|
||||
func parseResults(results []caaResult) (*CAASet, error) {
|
||||
// Return first result
|
||||
for _, res := range results {
|
||||
if res.err != nil {
|
||||
return nil, res.err
|
||||
}
|
||||
if len(res.records) > 0 {
|
||||
return newCAASet(res.records), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) parallelCAALookup(ctx context.Context, name string, lookuper func(context.Context, string) ([]*dns.CAA, error)) []caaResult {
|
||||
labels := strings.Split(name, ".")
|
||||
results := make([]caaResult, len(labels))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < len(labels); i++ {
|
||||
// Start the concurrent DNS lookup.
|
||||
wg.Add(1)
|
||||
go func(name string, r *caaResult) {
|
||||
r.records, r.err = lookuper(ctx, name)
|
||||
wg.Done()
|
||||
}(strings.Join(labels[i:], "."), &results[i])
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) getCAASet(ctx context.Context, hostname string) (*CAASet, error) {
|
||||
hostname = strings.TrimRight(hostname, ".")
|
||||
labels := strings.Split(hostname, ".")
|
||||
|
||||
// See RFC 6844 "Certification Authority Processing" for pseudocode.
|
||||
// Essentially: check CAA records for the FDQN to be issued, and all
|
||||
|
@ -749,38 +788,24 @@ func (va *ValidationAuthorityImpl) getCAASet(ctx context.Context, hostname strin
|
|||
// the RPC call.
|
||||
//
|
||||
// We depend on our resolver to snap CNAME and DNAME records.
|
||||
|
||||
type result struct {
|
||||
records []*dns.CAA
|
||||
err error
|
||||
results := va.parallelCAALookup(ctx, hostname, va.DNSResolver.LookupCAA)
|
||||
set, err := parseResults(results)
|
||||
if err == nil {
|
||||
return set, nil
|
||||
}
|
||||
results := make([]result, len(labels))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < len(labels); i++ {
|
||||
// Start the concurrent DNS lookup.
|
||||
wg.Add(1)
|
||||
go func(name string, r *result) {
|
||||
r.records, r.err = va.DNSResolver.LookupCAA(ctx, name)
|
||||
wg.Done()
|
||||
}(strings.Join(labels[i:], "."), &results[i])
|
||||
if va.caaDR == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Return the first result
|
||||
for _, res := range results {
|
||||
if res.err != nil {
|
||||
return nil, res.err
|
||||
}
|
||||
if len(res.records) > 0 {
|
||||
return newCAASet(res.records), nil
|
||||
// we have a CAADistributedResolver and one of the local lookups failed
|
||||
// so we talk to the Google Public DNS service over various proxies
|
||||
// instead if the initial error was a timeout
|
||||
if dnsErr, ok := err.(*bdns.DNSError); ok {
|
||||
if !dnsErr.Timeout() {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// no CAA records found
|
||||
return nil, nil
|
||||
results = va.parallelCAALookup(ctx, hostname, va.caaDR.LookupCAA)
|
||||
return parseResults(results)
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) checkCAARecords(ctx context.Context, identifier core.AcmeIdentifier) (present, valid bool, err error) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
|
@ -27,10 +28,12 @@ import (
|
|||
|
||||
"github.com/cactus/go-statsd-client/statsd"
|
||||
"github.com/jmhodges/clock"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/square/go-jose"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/letsencrypt/boulder/bdns"
|
||||
"github.com/letsencrypt/boulder/cdr"
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
|
@ -224,7 +227,7 @@ func TestHTTP(t *testing.T) {
|
|||
badPort = goodPort - 1
|
||||
}
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: badPort}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: badPort}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
_, prob := va.validateHTTP01(ctx, ident, chall)
|
||||
|
@ -233,7 +236,7 @@ func TestHTTP(t *testing.T) {
|
|||
}
|
||||
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
|
||||
|
||||
va = NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: goodPort}, nil, nil, stats, clock.Default())
|
||||
va = NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: goodPort}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
log.Clear()
|
||||
|
@ -316,7 +319,7 @@ func TestHTTPRedirectLookup(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
log.Clear()
|
||||
|
@ -373,7 +376,7 @@ func TestHTTPRedirectLoop(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
log.Clear()
|
||||
|
@ -392,7 +395,7 @@ func TestHTTPRedirectUserAgent(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
va.UserAgent = rejectUserAgent
|
||||
|
||||
|
@ -433,7 +436,7 @@ func TestTLSSNI(t *testing.T) {
|
|||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
|
@ -505,7 +508,7 @@ func TestTLSError(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
_, prob := va.validateTLSSNI01(ctx, ident, chall)
|
||||
|
@ -523,7 +526,7 @@ func TestValidateHTTP(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -549,7 +552,7 @@ func TestValidateHTTPResponseDocument(t *testing.T) {
|
|||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{HTTPPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -607,7 +610,7 @@ func TestValidateTLSSNI01(t *testing.T) {
|
|||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -625,7 +628,7 @@ func TestValidateTLSSNI01(t *testing.T) {
|
|||
|
||||
func TestValidateTLSSNINotSane(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default()) // no calls made
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default()) // no calls made
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -647,7 +650,7 @@ func TestValidateTLSSNINotSane(t *testing.T) {
|
|||
|
||||
func TestUpdateValidations(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -676,7 +679,7 @@ func TestUpdateValidations(t *testing.T) {
|
|||
|
||||
func TestCAATimeout(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
va.IssuerDomain = "letsencrypt.org"
|
||||
err := va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"})
|
||||
|
@ -721,19 +724,19 @@ func TestCAAChecking(t *testing.T) {
|
|||
}
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
va.IssuerDomain = "letsencrypt.org"
|
||||
for _, caaTest := range tests {
|
||||
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
|
||||
if err != nil {
|
||||
t.Errorf("CheckCAARecords error for %s: %s", caaTest.Domain, err)
|
||||
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
|
||||
}
|
||||
if present != caaTest.Present {
|
||||
t.Errorf("CheckCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
|
||||
t.Errorf("checkCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
|
||||
}
|
||||
if valid != caaTest.Valid {
|
||||
t.Errorf("CheckCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
|
||||
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -760,7 +763,7 @@ func TestCAAChecking(t *testing.T) {
|
|||
|
||||
func TestDNSValidationFailure(t *testing.T) {
|
||||
stats := mocks.NewStatter()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -799,7 +802,7 @@ func TestDNSValidationInvalid(t *testing.T) {
|
|||
}
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -813,7 +816,7 @@ func TestDNSValidationInvalid(t *testing.T) {
|
|||
|
||||
func TestDNSValidationNotSane(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -849,7 +852,7 @@ func TestDNSValidationNotSane(t *testing.T) {
|
|||
|
||||
func TestDNSValidationServFail(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -876,7 +879,7 @@ func TestDNSValidationServFail(t *testing.T) {
|
|||
func TestDNSValidationNoServer(t *testing.T) {
|
||||
c, _ := statsd.NewNoopClient()
|
||||
stats := metrics.NewNoopScope()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, c, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, c, clock.Default())
|
||||
va.DNSResolver = bdns.NewTestDNSResolverImpl(time.Second*5, []string{}, stats, clock.Default(), 1)
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -898,7 +901,7 @@ func TestDNSValidationNoServer(t *testing.T) {
|
|||
|
||||
func TestDNSValidationOK(t *testing.T) {
|
||||
stats := mocks.NewStatter()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -929,7 +932,7 @@ func TestDNSValidationOK(t *testing.T) {
|
|||
|
||||
func TestDNSValidationNoAuthorityOK(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -962,7 +965,7 @@ func TestDNSValidationNoAuthorityOK(t *testing.T) {
|
|||
// it asserts nothing; it is intended for coverage.
|
||||
func TestDNSValidationLive(t *testing.T) {
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -1021,7 +1024,7 @@ func TestCAAFailure(t *testing.T) {
|
|||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, stats, clock.Default())
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{TLSPort: port}, nil, nil, nil, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
mockRA := &MockRegistrationAuthority{}
|
||||
va.RA = mockRA
|
||||
|
@ -1038,6 +1041,41 @@ func TestCAAFailure(t *testing.T) {
|
|||
test.AssertEquals(t, core.StatusInvalid, mockRA.lastAuthz.Challenges[0].Status)
|
||||
}
|
||||
|
||||
func TestGetCAASetFallback(t *testing.T) {
|
||||
testSrv := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
|
||||
defer testSrv.Close()
|
||||
|
||||
stats, _ := statsd.NewNoopClient()
|
||||
caaDR, err := cdr.New(metrics.NewNoopScope(), time.Second, 1, []string{}, log)
|
||||
test.AssertNotError(t, err, "Failed to create CAADistributedResolver")
|
||||
caaDR.URI = testSrv.URL
|
||||
caaDR.Clients["1.1.1.1"] = new(http.Client)
|
||||
va := NewValidationAuthorityImpl(&cmd.PortConfig{}, nil, nil, caaDR, stats, clock.Default())
|
||||
va.DNSResolver = &bdns.MockDNSResolver{}
|
||||
|
||||
set, err := va.getCAASet(ctx, "bad-local-resolver.com")
|
||||
test.AssertNotError(t, err, "getCAASet failed to fail back to cdr on timeout")
|
||||
test.AssertEquals(t, len(set.Issue), 1)
|
||||
}
|
||||
|
||||
func TestParseResults(t *testing.T) {
|
||||
r := []caaResult{}
|
||||
s, err := parseResults(r)
|
||||
test.Assert(t, s == nil, "set is not nil")
|
||||
test.Assert(t, err == nil, "error is not nil")
|
||||
test.AssertNotError(t, err, "no error should be returned")
|
||||
r = []caaResult{{nil, errors.New("")}, {[]*dns.CAA{&dns.CAA{Value: "test"}}, nil}}
|
||||
s, err = parseResults(r)
|
||||
test.Assert(t, s == nil, "set is not nil")
|
||||
test.AssertEquals(t, err.Error(), "")
|
||||
expected := dns.CAA{Value: "other-test"}
|
||||
r = []caaResult{{[]*dns.CAA{&expected}, nil}, {[]*dns.CAA{&dns.CAA{Value: "test"}}, nil}}
|
||||
s, err = parseResults(r)
|
||||
test.AssertEquals(t, len(s.Unknown), 1)
|
||||
test.Assert(t, s.Unknown[0] == &expected, "Incorrect record returned")
|
||||
test.AssertNotError(t, err, "no error should be returned")
|
||||
}
|
||||
|
||||
func TestTruncateBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Case string
|
||||
|
|
|
@ -36,12 +36,7 @@
|
|||
// Contexts.
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// A Context carries a deadline, a cancelation signal, and other values across
|
||||
// API boundaries.
|
||||
|
@ -138,48 +133,6 @@ type Context interface {
|
|||
Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||
var Canceled = errors.New("context canceled")
|
||||
|
||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case background:
|
||||
return "context.Background"
|
||||
case todo:
|
||||
return "context.TODO"
|
||||
}
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||
// values, and has no deadline. It is typically used by the main function,
|
||||
// initialization, and tests, and as the top-level Context for incoming
|
||||
|
@ -201,247 +154,3 @@ func TODO() Context {
|
|||
// A CancelFunc does not wait for the work to stop.
|
||||
// After the first call, subsequent calls to a CancelFunc do nothing.
|
||||
type CancelFunc func()
|
||||
|
||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||
// context's Done channel is closed when the returned cancel function is called
|
||||
// or when the parent context's Done channel is closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
c := newCancelCtx(parent)
|
||||
propagateCancel(parent, c)
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// newCancelCtx returns an initialized cancelCtx.
|
||||
func newCancelCtx(parent Context) *cancelCtx {
|
||||
return &cancelCtx{
|
||||
Context: parent,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// propagateCancel arranges for child to be canceled when parent is.
|
||||
func propagateCancel(parent Context, child canceler) {
|
||||
if parent.Done() == nil {
|
||||
return // parent is never canceled
|
||||
}
|
||||
if p, ok := parentCancelCtx(parent); ok {
|
||||
p.mu.Lock()
|
||||
if p.err != nil {
|
||||
// parent has already been canceled
|
||||
child.cancel(false, p.err)
|
||||
} else {
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]bool)
|
||||
}
|
||||
p.children[child] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else {
|
||||
go func() {
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// parentCancelCtx follows a chain of parent references until it finds a
|
||||
// *cancelCtx. This function understands how each of the concrete types in this
|
||||
// package represents its parent.
|
||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||
for {
|
||||
switch c := parent.(type) {
|
||||
case *cancelCtx:
|
||||
return c, true
|
||||
case *timerCtx:
|
||||
return c.cancelCtx, true
|
||||
case *valueCtx:
|
||||
parent = c.Context
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeChild removes a context from its parent.
|
||||
func removeChild(parent Context, child canceler) {
|
||||
p, ok := parentCancelCtx(parent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.children != nil {
|
||||
delete(p.children, child)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// A canceler is a context type that can be canceled directly. The
|
||||
// implementations are *cancelCtx and *timerCtx.
|
||||
type canceler interface {
|
||||
cancel(removeFromParent bool, err error)
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||
// that implement canceler.
|
||||
type cancelCtx struct {
|
||||
Context
|
||||
|
||||
done chan struct{} // closed by the first cancel call.
|
||||
|
||||
mu sync.Mutex
|
||||
children map[canceler]bool // set to nil by the first cancel call
|
||||
err error // set to non-nil by the first cancel call
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Err() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *cancelCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithCancel", c.Context)
|
||||
}
|
||||
|
||||
// cancel closes c.done, cancels each of c's children, and, if
|
||||
// removeFromParent is true, removes c from its parent's children.
|
||||
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||
if err == nil {
|
||||
panic("context: internal error: missing cancel error")
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
c.mu.Unlock()
|
||||
return // already canceled
|
||||
}
|
||||
c.err = err
|
||||
close(c.done)
|
||||
for child := range c.children {
|
||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||
child.cancel(false, err)
|
||||
}
|
||||
c.children = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||
// context's Done channel is closed when the deadline expires, when the returned
|
||||
// cancel function is called, or when the parent context's Done channel is
|
||||
// closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
||||
// The current deadline is already sooner than the new one.
|
||||
return WithCancel(parent)
|
||||
}
|
||||
c := &timerCtx{
|
||||
cancelCtx: newCancelCtx(parent),
|
||||
deadline: deadline,
|
||||
}
|
||||
propagateCancel(parent, c)
|
||||
d := deadline.Sub(time.Now())
|
||||
if d <= 0 {
|
||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.err == nil {
|
||||
c.timer = time.AfterFunc(d, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||
// implement Done and Err. It implements cancel by stopping its timer then
|
||||
// delegating to cancelCtx.cancel.
|
||||
type timerCtx struct {
|
||||
*cancelCtx
|
||||
timer *time.Timer // Under cancelCtx.mu.
|
||||
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.deadline, true
|
||||
}
|
||||
|
||||
func (c *timerCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
||||
}
|
||||
|
||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||
c.cancelCtx.cancel(false, err)
|
||||
if removeFromParent {
|
||||
// Remove this timerCtx from its parent cancelCtx's children.
|
||||
removeChild(c.cancelCtx.Context, c)
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.timer != nil {
|
||||
c.timer.Stop()
|
||||
c.timer = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete:
|
||||
//
|
||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||
// return slowOperation(ctx)
|
||||
// }
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||
return WithDeadline(parent, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
return &valueCtx{parent, key, val}
|
||||
}
|
||||
|
||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||
// delegates all other calls to the embedded Context.
|
||||
type valueCtx struct {
|
||||
Context
|
||||
key, val interface{}
|
||||
}
|
||||
|
||||
func (c *valueCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
||||
}
|
||||
|
||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
|
||||
package ctxhttp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func nop() {}
|
||||
|
||||
var (
|
||||
testHookContextDoneBeforeHeaders = nop
|
||||
testHookDoReturned = nop
|
||||
testHookDidBodyClose = nop
|
||||
)
|
||||
|
||||
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
|
||||
// If the client is nil, http.DefaultClient is used.
|
||||
// If the context is canceled or times out, ctx.Err() will be returned.
|
||||
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
// TODO(djd): Respect any existing value of req.Cancel.
|
||||
cancel := make(chan struct{})
|
||||
req.Cancel = cancel
|
||||
|
||||
type responseAndError struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
result := make(chan responseAndError, 1)
|
||||
|
||||
// Make local copies of test hooks closed over by goroutines below.
|
||||
// Prevents data races in tests.
|
||||
testHookDoReturned := testHookDoReturned
|
||||
testHookDidBodyClose := testHookDidBodyClose
|
||||
|
||||
go func() {
|
||||
resp, err := client.Do(req)
|
||||
testHookDoReturned()
|
||||
result <- responseAndError{resp, err}
|
||||
}()
|
||||
|
||||
var resp *http.Response
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
testHookContextDoneBeforeHeaders()
|
||||
close(cancel)
|
||||
// Clean up after the goroutine calling client.Do:
|
||||
go func() {
|
||||
if r := <-result; r.resp != nil {
|
||||
testHookDidBodyClose()
|
||||
r.resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
return nil, ctx.Err()
|
||||
case r := <-result:
|
||||
var err error
|
||||
resp, err = r.resp, r.err
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
c := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(cancel)
|
||||
case <-c:
|
||||
// The response's Body is closed.
|
||||
}
|
||||
}()
|
||||
resp.Body = ¬ifyingReader{resp.Body, c}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Get issues a GET request via the Do function.
|
||||
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request via the Do function.
|
||||
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// Post issues a POST request via the Do function.
|
||||
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
return Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// PostForm issues a POST request via the Do function.
|
||||
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
|
||||
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||
}
|
||||
|
||||
// notifyingReader is an io.ReadCloser that closes the notify channel after
|
||||
// Close is called or a Read fails on the underlying ReadCloser.
|
||||
type notifyingReader struct {
|
||||
io.ReadCloser
|
||||
notify chan<- struct{}
|
||||
}
|
||||
|
||||
func (r *notifyingReader) Read(p []byte) (int, error) {
|
||||
n, err := r.ReadCloser.Read(p)
|
||||
if err != nil && r.notify != nil {
|
||||
close(r.notify)
|
||||
r.notify = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *notifyingReader) Close() error {
|
||||
err := r.ReadCloser.Close()
|
||||
if r.notify != nil {
|
||||
close(r.notify)
|
||||
r.notify = nil
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.7
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context" // standard library's context, as of Go 1.7
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
todo = context.TODO()
|
||||
background = context.Background()
|
||||
)
|
||||
|
||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||
var Canceled = context.Canceled
|
||||
|
||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = context.DeadlineExceeded
|
||||
|
||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||
// context's Done channel is closed when the returned cancel function is called
|
||||
// or when the parent context's Done channel is closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
ctx, f := context.WithCancel(parent)
|
||||
return ctx, CancelFunc(f)
|
||||
}
|
||||
|
||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||
// context's Done channel is closed when the deadline expires, when the returned
|
||||
// cancel function is called, or when the parent context's Done channel is
|
||||
// closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||
ctx, f := context.WithDeadline(parent, deadline)
|
||||
return ctx, CancelFunc(f)
|
||||
}
|
||||
|
||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete:
|
||||
//
|
||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||
// return slowOperation(ctx)
|
||||
// }
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||
return WithDeadline(parent, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
return context.WithValue(parent, key, val)
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.7
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case background:
|
||||
return "context.Background"
|
||||
case todo:
|
||||
return "context.TODO"
|
||||
}
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
|
||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||
var Canceled = errors.New("context canceled")
|
||||
|
||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||
|
||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||
// context's Done channel is closed when the returned cancel function is called
|
||||
// or when the parent context's Done channel is closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
c := newCancelCtx(parent)
|
||||
propagateCancel(parent, c)
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// newCancelCtx returns an initialized cancelCtx.
|
||||
func newCancelCtx(parent Context) *cancelCtx {
|
||||
return &cancelCtx{
|
||||
Context: parent,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// propagateCancel arranges for child to be canceled when parent is.
|
||||
func propagateCancel(parent Context, child canceler) {
|
||||
if parent.Done() == nil {
|
||||
return // parent is never canceled
|
||||
}
|
||||
if p, ok := parentCancelCtx(parent); ok {
|
||||
p.mu.Lock()
|
||||
if p.err != nil {
|
||||
// parent has already been canceled
|
||||
child.cancel(false, p.err)
|
||||
} else {
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]bool)
|
||||
}
|
||||
p.children[child] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else {
|
||||
go func() {
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// parentCancelCtx follows a chain of parent references until it finds a
|
||||
// *cancelCtx. This function understands how each of the concrete types in this
|
||||
// package represents its parent.
|
||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||
for {
|
||||
switch c := parent.(type) {
|
||||
case *cancelCtx:
|
||||
return c, true
|
||||
case *timerCtx:
|
||||
return c.cancelCtx, true
|
||||
case *valueCtx:
|
||||
parent = c.Context
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeChild removes a context from its parent.
|
||||
func removeChild(parent Context, child canceler) {
|
||||
p, ok := parentCancelCtx(parent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.children != nil {
|
||||
delete(p.children, child)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// A canceler is a context type that can be canceled directly. The
|
||||
// implementations are *cancelCtx and *timerCtx.
|
||||
type canceler interface {
|
||||
cancel(removeFromParent bool, err error)
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||
// that implement canceler.
|
||||
type cancelCtx struct {
|
||||
Context
|
||||
|
||||
done chan struct{} // closed by the first cancel call.
|
||||
|
||||
mu sync.Mutex
|
||||
children map[canceler]bool // set to nil by the first cancel call
|
||||
err error // set to non-nil by the first cancel call
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Err() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *cancelCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithCancel", c.Context)
|
||||
}
|
||||
|
||||
// cancel closes c.done, cancels each of c's children, and, if
|
||||
// removeFromParent is true, removes c from its parent's children.
|
||||
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||
if err == nil {
|
||||
panic("context: internal error: missing cancel error")
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
c.mu.Unlock()
|
||||
return // already canceled
|
||||
}
|
||||
c.err = err
|
||||
close(c.done)
|
||||
for child := range c.children {
|
||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||
child.cancel(false, err)
|
||||
}
|
||||
c.children = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||
// context's Done channel is closed when the deadline expires, when the returned
|
||||
// cancel function is called, or when the parent context's Done channel is
|
||||
// closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
||||
// The current deadline is already sooner than the new one.
|
||||
return WithCancel(parent)
|
||||
}
|
||||
c := &timerCtx{
|
||||
cancelCtx: newCancelCtx(parent),
|
||||
deadline: deadline,
|
||||
}
|
||||
propagateCancel(parent, c)
|
||||
d := deadline.Sub(time.Now())
|
||||
if d <= 0 {
|
||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.err == nil {
|
||||
c.timer = time.AfterFunc(d, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||
// implement Done and Err. It implements cancel by stopping its timer then
|
||||
// delegating to cancelCtx.cancel.
|
||||
type timerCtx struct {
|
||||
*cancelCtx
|
||||
timer *time.Timer // Under cancelCtx.mu.
|
||||
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.deadline, true
|
||||
}
|
||||
|
||||
func (c *timerCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
||||
}
|
||||
|
||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||
c.cancelCtx.cancel(false, err)
|
||||
if removeFromParent {
|
||||
// Remove this timerCtx from its parent cancelCtx's children.
|
||||
removeChild(c.cancelCtx.Context, c)
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.timer != nil {
|
||||
c.timer.Stop()
|
||||
c.timer = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete:
|
||||
//
|
||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||
// return slowOperation(ctx)
|
||||
// }
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||
return WithDeadline(parent, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
return &valueCtx{parent, key, val}
|
||||
}
|
||||
|
||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||
// delegates all other calls to the embedded Context.
|
||||
type valueCtx struct {
|
||||
Context
|
||||
key, val interface{}
|
||||
}
|
||||
|
||||
func (c *valueCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
||||
}
|
||||
|
||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.5
|
||||
|
||||
package http2
|
||||
|
||||
import "net/http"
|
||||
|
||||
func requestCancel(req *http.Request) <-chan struct{} { return req.Cancel }
|
|
@ -43,7 +43,7 @@ type HeaderField struct {
|
|||
|
||||
// IsPseudo reports whether the header field is an http2 pseudo header.
|
||||
// That is, it reports whether it starts with a colon.
|
||||
// It is not otherwise guaranteed to be a valid psuedo header field,
|
||||
// It is not otherwise guaranteed to be a valid pseudo header field,
|
||||
// though.
|
||||
func (hf HeaderField) IsPseudo() bool {
|
||||
return len(hf.Name) != 0 && hf.Name[0] == ':'
|
||||
|
|
|
@ -170,8 +170,9 @@ var (
|
|||
// RFC 7230 says:
|
||||
// header-field = field-name ":" OWS field-value OWS
|
||||
// field-name = token
|
||||
// token = 1*tchar
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
||||
// "^" / "_" / "
|
||||
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
|
||||
// Further, http2 says:
|
||||
// "Just as in HTTP/1.x, header field names are strings of ASCII
|
||||
// characters that are compared in a case-insensitive
|
||||
|
@ -321,7 +322,7 @@ func mustUint31(v int32) uint32 {
|
|||
}
|
||||
|
||||
// bodyAllowedForStatus reports whether a given response status code
|
||||
// permits a body. See RFC2616, section 4.4.
|
||||
// permits a body. See RFC 2616, section 4.4.
|
||||
func bodyAllowedForStatus(status int) bool {
|
||||
switch {
|
||||
case status >= 100 && status <= 199:
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !go1.5
|
||||
|
||||
package http2
|
||||
|
||||
import "net/http"
|
||||
|
||||
func requestCancel(req *http.Request) <-chan struct{} { return nil }
|
|
@ -1437,6 +1437,8 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
|
|||
if f.Truncated {
|
||||
// Their header list was too long. Send a 431 error.
|
||||
handler = handleHeaderListTooLong
|
||||
} else if err := checkValidHTTP2Request(req); err != nil {
|
||||
handler = new400Handler(err)
|
||||
}
|
||||
|
||||
go sc.runHandler(rw, req, handler)
|
||||
|
@ -1616,9 +1618,13 @@ func (sc *serverConn) newWriterAndRequest(st *stream, f *MetaHeadersFrame) (*res
|
|||
Trailer: trailer,
|
||||
}
|
||||
if bodyOpen {
|
||||
st.reqBuf = sc.getRequestBodyBuf()
|
||||
// Disabled, per golang.org/issue/14960:
|
||||
// st.reqBuf = sc.getRequestBodyBuf()
|
||||
// TODO: remove this 64k of garbage per request (again, but without a data race):
|
||||
buf := make([]byte, initialWindowSize)
|
||||
|
||||
body.pipe = &pipe{
|
||||
b: &fixedBuffer{buf: st.reqBuf},
|
||||
b: &fixedBuffer{buf: buf},
|
||||
}
|
||||
|
||||
if vv, ok := header["Content-Length"]; ok {
|
||||
|
@ -2176,3 +2182,34 @@ func foreachHeaderElement(v string, fn func(string)) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.2
|
||||
var connHeaders = []string{
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Proxy-Connection",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
// checkValidHTTP2Request checks whether req is a valid HTTP/2 request,
|
||||
// per RFC 7540 Section 8.1.2.2.
|
||||
// The returned error is reported to users.
|
||||
func checkValidHTTP2Request(req *http.Request) error {
|
||||
for _, h := range connHeaders {
|
||||
if _, ok := req.Header[h]; ok {
|
||||
return fmt.Errorf("request header %q is not valid in HTTP/2", h)
|
||||
}
|
||||
}
|
||||
te := req.Header["Te"]
|
||||
if len(te) > 0 && (len(te) > 1 || (te[0] != "trailers" && te[0] != "")) {
|
||||
return errors.New(`request header "TE" may only be "trailers" in HTTP/2`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func new400Handler(err error) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -683,7 +683,6 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
}
|
||||
|
||||
readLoopResCh := cs.resc
|
||||
requestCanceledCh := requestCancel(req)
|
||||
bodyWritten := false
|
||||
|
||||
for {
|
||||
|
@ -717,7 +716,7 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
cs.abortRequestBodyWrite(errStopReqBodyWriteAndCancel)
|
||||
}
|
||||
return nil, errTimeout
|
||||
case <-requestCanceledCh:
|
||||
case <-req.Cancel:
|
||||
cc.forgetStreamID(cs.ID)
|
||||
if !hasBody || bodyWritten {
|
||||
cc.writeStreamReset(cs.ID, ErrCodeCancel, nil)
|
||||
|
@ -945,14 +944,11 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
|
|||
// Host is :authority, already sent.
|
||||
// Content-Length is automatic, set below.
|
||||
continue
|
||||
case "connection", "proxy-connection", "transfer-encoding", "upgrade":
|
||||
case "connection", "proxy-connection", "transfer-encoding", "upgrade", "keep-alive":
|
||||
// Per 8.1.2.2 Connection-Specific Header
|
||||
// Fields, don't send connection-specific
|
||||
// fields. We deal with these earlier in
|
||||
// RoundTrip, deciding whether they're
|
||||
// error-worthy, but we don't want to mutate
|
||||
// the user's *Request so at this point, just
|
||||
// skip over them at this point.
|
||||
// fields. We have already checked if any
|
||||
// are error-worthy so just ignore the rest.
|
||||
continue
|
||||
case "user-agent":
|
||||
// Match Go's http1 behavior: at most one
|
||||
|
@ -1263,7 +1259,8 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
|||
}
|
||||
|
||||
streamEnded := f.StreamEnded()
|
||||
if !streamEnded || cs.req.Method == "HEAD" {
|
||||
isHead := cs.req.Method == "HEAD"
|
||||
if !streamEnded || isHead {
|
||||
res.ContentLength = -1
|
||||
if clens := res.Header["Content-Length"]; len(clens) == 1 {
|
||||
if clen64, err := strconv.ParseInt(clens[0], 10, 64); err == nil {
|
||||
|
@ -1278,7 +1275,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
|||
}
|
||||
}
|
||||
|
||||
if streamEnded {
|
||||
if streamEnded || isHead {
|
||||
res.Body = noBody
|
||||
return res, nil
|
||||
}
|
||||
|
@ -1287,7 +1284,7 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra
|
|||
cs.bufPipe = pipe{b: buf}
|
||||
cs.bytesRemain = res.ContentLength
|
||||
res.Body = transportResponseBody{cs}
|
||||
go cs.awaitRequestCancel(requestCancel(cs.req))
|
||||
go cs.awaitRequestCancel(cs.req.Cancel)
|
||||
|
||||
if cs.requestedGzip && res.Header.Get("Content-Encoding") == "gzip" {
|
||||
res.Header.Del("Content-Encoding")
|
||||
|
|
Loading…
Reference in New Issue