Files
snapd/httputil/client.go
Pawel Stolowski c8122fc15a store: set ResponseHeaderTimeout on the default transport
It appears that ResponseHeaderTimeout needs to be set explicitly, no headers response from the server doesn't seem to be covered by any other timeout.

This should fix the download getting stuck issue.

For more context, see the stacktrace obtained from a system where download got stuck: https://pastebin.canonical.com/p/nZkRMTBbv3/
(the affected goroutine seems to be sitting inside for-select loop of /usr/lib/go-1.10/src/net/http/transport.go:2033)

(to play with it comment out ResponseHeaderTimeout: ... from transport.go and modify test timeout from 5 * time.Second to a huge value and it should be stuck)

* Set ResponseHeaderTimeout on the default transport.

* Use 250ms for mocked ResponseHeaderTimeout.

* Bump ResponseHeaderTimeout to 15s.

* Bump test timeout to avoid potential issues with LP builds.
2021-07-13 11:21:01 +02:00

197 lines
5.0 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018-2020 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package httputil
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"path/filepath"
"time"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
)
// CertData contains the raw data of a certificate and the origin of
// the cert, this is usually a file path on disk and is just used
// for error reporting.
type CertData struct {
Raw []byte
Origin string
}
// ExtraSSLCerts is an interface that provides a way to add extra
// SSL certificates to the httputil.Client
type ExtraSSLCerts interface {
Certs() ([]*CertData, error)
}
// ExtraSSLCertsFromDir implements ExtraSSLCerts and provides all the
// pem encoded certs from the given directory.
type ExtraSSLCertsFromDir struct {
Dir string
}
// Certs returns a slice CertData or an error.
func (e *ExtraSSLCertsFromDir) Certs() ([]*CertData, error) {
extraCertFiles, err := filepath.Glob(filepath.Join(e.Dir, "*.pem"))
if err != nil {
return nil, err
}
extraCerts := make([]*CertData, 0, len(extraCertFiles))
for _, p := range extraCertFiles {
cert, err := ioutil.ReadFile(p)
if err != nil {
return nil, fmt.Errorf("cannot read certificate: %v", err)
}
extraCerts = append(extraCerts, &CertData{
Raw: cert,
Origin: p,
})
}
return extraCerts, nil
}
// dialTLS holds a tls.Config that is used by the dialTLS.dialTLS()
// function.
type dialTLS struct {
conf *tls.Config
extraSSLCerts ExtraSSLCerts
}
// dialTLS will use it's tls.Config and use that to do a tls connection.
func (d *dialTLS) dialTLS(network, addr string) (net.Conn, error) {
if d.conf == nil {
// c.f. go source: crypto/tls/common.go
var emptyConfig tls.Config
d.conf = &emptyConfig
}
// ensure we never use anything lower than TLS v1.2, see
// https://github.com/snapcore/snapd/pull/8100/files#r384046667
if d.conf.MinVersion < tls.VersionTLS12 {
d.conf.MinVersion = tls.VersionTLS12
}
// add extraSSLCerts if needed
if err := d.addLocalSSLCertificates(); err != nil {
logger.Noticef("cannot add local ssl certificates: %v", err)
}
return tls.Dial(network, addr, d.conf)
}
// addLocalSSLCertificates() is an internal helper that is called by
// dialTLS to add an extra certificates.
func (d *dialTLS) addLocalSSLCertificates() (err error) {
if d.extraSSLCerts == nil {
// nothing to add
return nil
}
var allCAs *x509.CertPool
// start with all our current certs
if d.conf.RootCAs != nil {
allCAs = d.conf.RootCAs
} else {
allCAs, err = x509.SystemCertPool()
if err != nil {
return fmt.Errorf("cannot read system certificates: %v", err)
}
}
if allCAs == nil {
return fmt.Errorf("cannot use empty certificate pool")
}
// and now collect any new ones
extraCerts, err := d.extraSSLCerts.Certs()
if err != nil {
return err
}
for _, cert := range extraCerts {
if ok := allCAs.AppendCertsFromPEM(cert.Raw); !ok {
logger.Noticef("cannot load ssl certificate: %v", cert.Origin)
}
}
// and add them
d.conf.RootCAs = allCAs
return nil
}
type ClientOptions struct {
Timeout time.Duration
TLSConfig *tls.Config
MayLogBody bool
Proxy func(*http.Request) (*url.URL, error)
ProxyConnectHeader http.Header
ExtraSSLCerts ExtraSSLCerts
}
// NewHTTPCLient returns a new http.Client with a LoggedTransport, a
// Timeout and preservation of range requests across redirects
func NewHTTPClient(opts *ClientOptions) *http.Client {
if opts == nil {
opts = &ClientOptions{}
}
transport := newDefaultTransport()
if opts.Proxy != nil {
transport.Proxy = opts.Proxy
}
transport.ProxyConnectHeader = opts.ProxyConnectHeader
// Remember the original ClientOptions.TLSConfig when making
// tls connection.
// Note that we only set TLSClientConfig here because it's extracted
// by the cmd/snap-repair/runner_test.go
transport.TLSClientConfig = opts.TLSConfig
dialTLS := &dialTLS{
conf: opts.TLSConfig,
extraSSLCerts: opts.ExtraSSLCerts,
}
transport.DialTLS = dialTLS.dialTLS
return &http.Client{
Transport: &LoggedTransport{
Transport: transport,
Key: "SNAPD_DEBUG_HTTP",
body: opts.MayLogBody,
},
Timeout: opts.Timeout,
CheckRedirect: checkRedirect,
}
}
func MockResponseHeaderTimeout(d time.Duration) (restore func()) {
osutil.MustBeTestBinary("cannot mock ResponseHeaderTimeout outside of tests")
old := responseHeaderTimeout
responseHeaderTimeout = d
return func() {
responseHeaderTimeout = old
}
}