26231b29e7
When user execute docker exec command, docker daemon maybe have err return because of ExecExists check, and then the hijack stream will not be close, it can lead to docker exec command block. Signed-off-by: yangshukui <yangshukui@huawei.com>
191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"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
|
|
}
|
|
|
|
apiPath := cli.getAPIPath(path, query)
|
|
req, err := http.NewRequest("POST", apiPath, bodyEncoded)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
req = cli.addHeaders(req, headers)
|
|
|
|
req.Host = cli.addr
|
|
req.Header.Set("Connection", "Upgrade")
|
|
req.Header.Set("Upgrade", "tcp")
|
|
|
|
conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
|
|
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
|
|
resp, err := clientconn.Do(req)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
switch resp.StatusCode {
|
|
case http.StatusOK, http.StatusSwitchingProtocols:
|
|
rwc, br := clientconn.Hijack()
|
|
return types.HijackedResponse{Conn: rwc, Reader: br}, err
|
|
}
|
|
|
|
errbody, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
return types.HijackedResponse{}, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(errbody))
|
|
}
|
|
|
|
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)
|
|
}
|