89bd48481c
This patch removes the need for requestFactories and decorators by implementing http.RoundTripper transports instead. It refactors some challenging-to-read code. NewSession now takes an *http.Client that can already have a custom Transport, it will add its own auth transport by wrapping it. The idea is that callers of http.Client should not bother setting custom headers for every handler but instead it should be transparent to the callers of a same context. This patch is needed for future refactorings of registry, namely refactoring of the v1 client code. Signed-off-by: Tibor Vass <tibor@docker.com>
273 lines
7.3 KiB
Go
273 lines
7.3 KiB
Go
package registry
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/autogen/dockerversion"
|
|
"github.com/docker/docker/pkg/parsers/kernel"
|
|
"github.com/docker/docker/pkg/requestdecorator"
|
|
"github.com/docker/docker/pkg/timeoutconn"
|
|
)
|
|
|
|
var (
|
|
ErrAlreadyExists = errors.New("Image already exists")
|
|
ErrDoesNotExist = errors.New("Image does not exist")
|
|
errLoginRequired = errors.New("Authentication is required.")
|
|
)
|
|
|
|
type TimeoutType uint32
|
|
|
|
const (
|
|
NoTimeout TimeoutType = iota
|
|
ReceiveTimeout
|
|
ConnectTimeout
|
|
)
|
|
|
|
type httpsTransport struct {
|
|
*http.Transport
|
|
}
|
|
|
|
// DRAGONS(tiborvass): If someone wonders why do we set tlsconfig in a roundtrip,
|
|
// it's because it's so as to match the current behavior in master: we generate the
|
|
// certpool on every-goddam-request. It's not great, but it allows people to just put
|
|
// the certs in /etc/docker/certs.d/.../ and let docker "pick it up" immediately. Would
|
|
// prefer an fsnotify implementation, but that was out of scope of my refactoring.
|
|
// TODO: improve things
|
|
func (tr *httpsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
var (
|
|
roots *x509.CertPool
|
|
certs []tls.Certificate
|
|
)
|
|
|
|
if req.URL.Scheme == "https" {
|
|
hasFile := func(files []os.FileInfo, name string) bool {
|
|
for _, f := range files {
|
|
if f.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
|
|
logrus.Debugf("hostDir: %s", hostDir)
|
|
fs, err := ioutil.ReadDir(hostDir)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
for _, f := range fs {
|
|
if strings.HasSuffix(f.Name(), ".crt") {
|
|
if roots == nil {
|
|
roots = x509.NewCertPool()
|
|
}
|
|
logrus.Debugf("crt: %s", hostDir+"/"+f.Name())
|
|
data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
roots.AppendCertsFromPEM(data)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".cert") {
|
|
certName := f.Name()
|
|
keyName := certName[:len(certName)-5] + ".key"
|
|
logrus.Debugf("cert: %s", hostDir+"/"+f.Name())
|
|
if !hasFile(fs, keyName) {
|
|
return nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
|
}
|
|
cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".key") {
|
|
keyName := f.Name()
|
|
certName := keyName[:len(keyName)-4] + ".cert"
|
|
logrus.Debugf("key: %s", hostDir+"/"+f.Name())
|
|
if !hasFile(fs, certName) {
|
|
return nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
|
}
|
|
}
|
|
}
|
|
if tr.Transport.TLSClientConfig == nil {
|
|
tr.Transport.TLSClientConfig = &tls.Config{
|
|
// Avoid fallback to SSL protocols < TLS1.0
|
|
MinVersion: tls.VersionTLS10,
|
|
}
|
|
}
|
|
tr.Transport.TLSClientConfig.RootCAs = roots
|
|
tr.Transport.TLSClientConfig.Certificates = certs
|
|
}
|
|
return tr.Transport.RoundTrip(req)
|
|
}
|
|
|
|
func NewTransport(timeout TimeoutType, secure bool) http.RoundTripper {
|
|
tlsConfig := tls.Config{
|
|
// Avoid fallback to SSL protocols < TLS1.0
|
|
MinVersion: tls.VersionTLS10,
|
|
InsecureSkipVerify: !secure,
|
|
}
|
|
|
|
transport := &http.Transport{
|
|
DisableKeepAlives: true,
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tlsConfig,
|
|
}
|
|
|
|
switch timeout {
|
|
case ConnectTimeout:
|
|
transport.Dial = func(proto string, addr string) (net.Conn, error) {
|
|
// Set the connect timeout to 30 seconds to allow for slower connection
|
|
// times...
|
|
d := net.Dialer{Timeout: 30 * time.Second, DualStack: true}
|
|
|
|
conn, err := d.Dial(proto, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Set the recv timeout to 10 seconds
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
return conn, nil
|
|
}
|
|
case ReceiveTimeout:
|
|
transport.Dial = func(proto string, addr string) (net.Conn, error) {
|
|
d := net.Dialer{DualStack: true}
|
|
|
|
conn, err := d.Dial(proto, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn = timeoutconn.New(conn, 1*time.Minute)
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
if secure {
|
|
// note: httpsTransport also handles http transport
|
|
// but for HTTPS, it sets up the certs
|
|
return &httpsTransport{transport}
|
|
}
|
|
|
|
return transport
|
|
}
|
|
|
|
type DockerHeaders struct {
|
|
http.RoundTripper
|
|
Headers http.Header
|
|
}
|
|
|
|
// cloneRequest returns a clone of the provided *http.Request.
|
|
// The clone is a shallow copy of the struct and its Header map
|
|
func cloneRequest(r *http.Request) *http.Request {
|
|
// shallow copy of the struct
|
|
r2 := new(http.Request)
|
|
*r2 = *r
|
|
// deep copy of the Header
|
|
r2.Header = make(http.Header, len(r.Header))
|
|
for k, s := range r.Header {
|
|
r2.Header[k] = append([]string(nil), s...)
|
|
}
|
|
return r2
|
|
}
|
|
|
|
func (tr *DockerHeaders) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
req = cloneRequest(req)
|
|
httpVersion := make([]requestdecorator.UAVersionInfo, 0, 4)
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("docker", dockerversion.VERSION))
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("go", runtime.Version()))
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("git-commit", dockerversion.GITCOMMIT))
|
|
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("kernel", kernelVersion.String()))
|
|
}
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("os", runtime.GOOS))
|
|
httpVersion = append(httpVersion, requestdecorator.NewUAVersionInfo("arch", runtime.GOARCH))
|
|
|
|
userAgent := requestdecorator.AppendVersions(req.UserAgent(), httpVersion...)
|
|
|
|
req.Header.Set("User-Agent", userAgent)
|
|
|
|
for k, v := range tr.Headers {
|
|
req.Header[k] = v
|
|
}
|
|
return tr.RoundTripper.RoundTrip(req)
|
|
}
|
|
|
|
type debugTransport struct{ http.RoundTripper }
|
|
|
|
func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
dump, err := httputil.DumpRequestOut(req, false)
|
|
if err != nil {
|
|
fmt.Println("could not dump request")
|
|
}
|
|
fmt.Println(string(dump))
|
|
resp, err := tr.RoundTripper.RoundTrip(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dump, err = httputil.DumpResponse(resp, false)
|
|
if err != nil {
|
|
fmt.Println("could not dump response")
|
|
}
|
|
fmt.Println(string(dump))
|
|
return resp, err
|
|
}
|
|
|
|
func HTTPClient(transport http.RoundTripper) *http.Client {
|
|
if transport == nil {
|
|
transport = NewTransport(ConnectTimeout, true)
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: transport,
|
|
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
|
|
}
|
|
}
|
|
|
|
func trustedLocation(req *http.Request) bool {
|
|
var (
|
|
trusteds = []string{"docker.com", "docker.io"}
|
|
hostname = strings.SplitN(req.Host, ":", 2)[0]
|
|
)
|
|
if req.URL.Scheme != "https" {
|
|
return false
|
|
}
|
|
|
|
for _, trusted := range trusteds {
|
|
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
if via != nil && via[0] != nil {
|
|
if trustedLocation(req) && trustedLocation(via[0]) {
|
|
req.Header = via[0].Header
|
|
return nil
|
|
}
|
|
for k, v := range via[0].Header {
|
|
if k != "Authorization" {
|
|
for _, vv := range v {
|
|
req.Header.Add(k, vv)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|