37983921c9
connection does. If this isn't done, then a container listening on stdin won't receive an EOF when the client closes the stream at their end. Signed-off-by: Jim Minter <jminter@redhat.com>
230 lines
6.8 KiB
Go
230 lines
6.8 KiB
Go
package client // import "github.com/docker/docker/client"
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/go-connections/sockets"
|
|
"github.com/pkg/errors"
|
|
"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
|
|
}
|
|
|
|
apiPath := cli.getAPIPath(path, query)
|
|
req, err := http.NewRequest("POST", apiPath, bodyEncoded)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
req = cli.addHeaders(req, headers)
|
|
|
|
conn, err := cli.setupHijackConn(req, "tcp")
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
return types.HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn)}, 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 := time.Until(dialer.Deadline)
|
|
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 = tlsConfigClone(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)
|
|
}
|
|
|
|
func (cli *Client) setupHijackConn(req *http.Request, proto string) (net.Conn, error) {
|
|
req.Host = cli.addr
|
|
req.Header.Set("Connection", "Upgrade")
|
|
req.Header.Set("Upgrade", proto)
|
|
|
|
conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
|
|
}
|
|
|
|
// 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
|
|
resp, err := clientconn.Do(req)
|
|
if err != httputil.ErrPersistEOF {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
c, br := clientconn.Hijack()
|
|
if br.Buffered() > 0 {
|
|
// If there is buffered content, wrap the connection. We return an
|
|
// object that implements CloseWrite iff the underlying connection
|
|
// implements it.
|
|
if _, ok := c.(types.CloseWriter); ok {
|
|
c = &hijackedConnCloseWriter{c, br}
|
|
} else {
|
|
c = &hijackedConn{c, br}
|
|
}
|
|
} else {
|
|
br.Reset(nil)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// hijackedConn wraps a net.Conn and is returned by setupHijackConn in the case
|
|
// that a) there was already buffered data in the http layer when Hijack() was
|
|
// called, and b) the underlying net.Conn does *not* implement CloseWrite().
|
|
// hijackedConn does not implement CloseWrite() either.
|
|
type hijackedConn struct {
|
|
net.Conn
|
|
r *bufio.Reader
|
|
}
|
|
|
|
func (c *hijackedConn) Read(b []byte) (int, error) {
|
|
return c.r.Read(b)
|
|
}
|
|
|
|
// hijackedConnCloseWriter is a hijackedConn which additionally implements
|
|
// CloseWrite(). It is returned by setupHijackConn in the case that a) there
|
|
// was already buffered data in the http layer when Hijack() was called, and b)
|
|
// the underlying net.Conn *does* implement CloseWrite().
|
|
type hijackedConnCloseWriter hijackedConn
|
|
|
|
var _ types.CloseWriter = &hijackedConnCloseWriter{}
|
|
|
|
func (c *hijackedConnCloseWriter) CloseWrite() error {
|
|
conn := c.Conn.(types.CloseWriter)
|
|
return conn.CloseWrite()
|
|
}
|