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:
Roland Bracewell Shoemaker 2016-05-05 11:16:58 -07:00 committed by Jacob Hoffman-Andrews
parent 2c39c684cb
commit 35b6e83e81
23 changed files with 1225 additions and 403 deletions

14
Godeps/Godeps.json generated
View File

@ -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",

View File

@ -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
}

View File

@ -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"

248
cdr/resolver.go Normal file
View File

@ -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
}

160
cdr/resolver_test.go Normal file
View File

@ -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))
}

View File

@ -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

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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.

View File

@ -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) {

View File

@ -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

View File

@ -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)
}

146
vendor/golang.org/x/net/context/ctxhttp/ctxhttp.go generated vendored Normal file
View File

@ -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 = &notifyingReader{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
}

72
vendor/golang.org/x/net/context/go17.go generated vendored Normal file
View File

@ -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)
}

300
vendor/golang.org/x/net/context/pre_go17.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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 }

View File

@ -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] == ':'

View File

@ -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:

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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")