9a072adff3
This package doesn't really seem to do anything of real interest. Removing it and replacing with a few helper functions. Most of this was maintaining a fork of ctxhttp to support a mock that was unnecessary. We could probably do with a further refactor of the client interface. There is a lot of confusion of between transport, http layer and application layer that makes for some awkward code. This change improves the situation to the point where no breaking changes are introduced. Signed-off-by: Stephen J Day <stephen.day@docker.com>
179 lines
5.2 KiB
Go
179 lines
5.2 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/pkg/tlsconfig"
|
|
"github.com/docker/go-connections/sockets"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// tlsClientCon holds tls information and a dialed connection.
|
|
type tlsClientCon struct {
|
|
*tls.Conn
|
|
rawConn net.Conn
|
|
}
|
|
|
|
func (c *tlsClientCon) CloseWrite() error {
|
|
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
|
|
// on its underlying connection.
|
|
if conn, ok := c.rawConn.(types.CloseWriter); ok {
|
|
return conn.CloseWrite()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// postHijacked sends a POST request and hijacks the connection.
|
|
func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
|
|
bodyEncoded, err := encodeData(body)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
req.Host = cli.addr
|
|
|
|
req.Header.Set("Connection", "Upgrade")
|
|
req.Header.Set("Upgrade", "tcp")
|
|
|
|
tlsConfig, err := resolveTLSConfig(cli.client.Transport)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
conn, err := dial(cli.proto, cli.addr, tlsConfig)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "connection refused") {
|
|
return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
|
|
}
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
// When we set up a TCP connection for hijack, there could be long periods
|
|
// of inactivity (a long running command with no output) that in certain
|
|
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
tcpConn.SetKeepAlive(true)
|
|
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
}
|
|
|
|
clientconn := httputil.NewClientConn(conn, nil)
|
|
defer clientconn.Close()
|
|
|
|
// Server hijacks the connection, error 'connection closed' expected
|
|
_, err = clientconn.Do(req)
|
|
|
|
rwc, br := clientconn.Hijack()
|
|
|
|
return types.HijackedResponse{Conn: rwc, Reader: br}, err
|
|
}
|
|
|
|
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
|
|
return tlsDialWithDialer(new(net.Dialer), network, addr, config)
|
|
}
|
|
|
|
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
|
|
// order to return our custom tlsClientCon struct which holds both the tls.Conn
|
|
// object _and_ its underlying raw connection. The rationale for this is that
|
|
// we need to be able to close the write end of the connection when attaching,
|
|
// which tls.Conn does not provide.
|
|
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
|
|
// We want the Timeout and Deadline values from dialer to cover the
|
|
// whole process: TCP connection and TLS handshake. This means that we
|
|
// also need to start our own timers now.
|
|
timeout := dialer.Timeout
|
|
|
|
if !dialer.Deadline.IsZero() {
|
|
deadlineTimeout := dialer.Deadline.Sub(time.Now())
|
|
if timeout == 0 || deadlineTimeout < timeout {
|
|
timeout = deadlineTimeout
|
|
}
|
|
}
|
|
|
|
var errChannel chan error
|
|
|
|
if timeout != 0 {
|
|
errChannel = make(chan error, 2)
|
|
time.AfterFunc(timeout, func() {
|
|
errChannel <- errors.New("")
|
|
})
|
|
}
|
|
|
|
proxyDialer, err := sockets.DialerFromEnvironment(dialer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rawConn, err := proxyDialer.Dial(network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// When we set up a TCP connection for hijack, there could be long periods
|
|
// of inactivity (a long running command with no output) that in certain
|
|
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
|
|
tcpConn.SetKeepAlive(true)
|
|
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
}
|
|
|
|
colonPos := strings.LastIndex(addr, ":")
|
|
if colonPos == -1 {
|
|
colonPos = len(addr)
|
|
}
|
|
hostname := addr[:colonPos]
|
|
|
|
// If no ServerName is set, infer the ServerName
|
|
// from the hostname we're connecting to.
|
|
if config.ServerName == "" {
|
|
// Make a copy to avoid polluting argument or default.
|
|
config = tlsconfig.Clone(config)
|
|
config.ServerName = hostname
|
|
}
|
|
|
|
conn := tls.Client(rawConn, config)
|
|
|
|
if timeout == 0 {
|
|
err = conn.Handshake()
|
|
} else {
|
|
go func() {
|
|
errChannel <- conn.Handshake()
|
|
}()
|
|
|
|
err = <-errChannel
|
|
}
|
|
|
|
if err != nil {
|
|
rawConn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// This is Docker difference with standard's crypto/tls package: returned a
|
|
// wrapper which holds both the TLS and raw connections.
|
|
return &tlsClientCon{conn, rawConn}, nil
|
|
}
|
|
|
|
func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
|
|
if tlsConfig != nil && proto != "unix" && proto != "npipe" {
|
|
// Notice this isn't Go standard's tls.Dial function
|
|
return tlsDial(proto, addr, tlsConfig)
|
|
}
|
|
if proto == "npipe" {
|
|
return sockets.DialPipe(addr, 32*time.Second)
|
|
}
|
|
return net.Dial(proto, addr)
|
|
}
|