diff --git a/api/client/attach.go b/api/client/attach.go index 9ebd02d760..f0979e2bf8 100644 --- a/api/client/attach.go +++ b/api/client/attach.go @@ -1,10 +1,8 @@ package client import ( - "encoding/json" "fmt" "io" - "net/url" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" @@ -25,18 +23,11 @@ func (cli *DockerCli) CmdAttach(args ...string) error { cmd.ParseFlags(args, true) - serverResp, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil, nil) + c, err := cli.client.ContainerInspect(cmd.Arg(0)) if err != nil { return err } - defer serverResp.body.Close() - - var c types.ContainerJSON - if err := json.NewDecoder(serverResp.body).Decode(&c); err != nil { - return err - } - if !c.State.Running { return fmt.Errorf("You cannot attach to a stopped container, start it first") } @@ -55,28 +46,35 @@ func (cli *DockerCli) CmdAttach(args ...string) error { } } - var in io.ReadCloser + options := types.ContainerAttachOptions{ + ContainerID: cmd.Arg(0), + Stream: true, + Stdin: !*noStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + } - v := url.Values{} - v.Set("stream", "1") - if !*noStdin && c.Config.OpenStdin { - v.Set("stdin", "1") + var in io.ReadCloser + if options.Stdin { in = cli.in } - v.Set("stdout", "1") - v.Set("stderr", "1") - if *proxy && !c.Config.Tty { - sigc := cli.forwardAllSignals(cmd.Arg(0)) + sigc := cli.forwardAllSignals(options.ContainerID) defer signal.StopCatch(sigc) } - if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), c.Config.Tty, in, cli.out, cli.err, nil, nil); err != nil { + resp, err := cli.client.ContainerAttach(options) + if err != nil { + return err + } + defer resp.Close() + + if err := cli.holdHijackedConnection(c.Config.Tty, in, cli.out, cli.err, resp); err != nil { return err } - _, status, err := getExitCode(cli, cmd.Arg(0)) + _, status, err := getExitCode(cli, options.ContainerID) if err != nil { return err } diff --git a/api/client/client.go b/api/client/client.go index 8f9efa8374..acd5bd35ea 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -15,6 +15,7 @@ import ( // apiClient is an interface that clients that talk with a docker server must implement. type apiClient interface { + ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) ContainerCommit(options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) ContainerCreate(config *runconfig.ContainerConfigWrapper, containerName string) (types.ContainerCreateResponse, error) ContainerDiff(containerID string) ([]types.ContainerChange, error) diff --git a/api/client/hijack.go b/api/client/hijack.go index bf152c6a9e..1c495afe9e 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -15,11 +15,79 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api" + "github.com/docker/docker/api/types" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/term" ) +func (cli *DockerCli) holdHijackedConnection(setRawTerminal bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp *types.HijackedResponse) error { + var ( + err error + oldState *term.State + ) + if inputStream != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { + oldState, err = term.SetRawTerminal(cli.inFd) + if err != nil { + return err + } + defer term.RestoreTerminal(cli.inFd, oldState) + } + + receiveStdout := make(chan error, 1) + if outputStream != nil || errorStream != nil { + go func() { + defer func() { + if inputStream != nil { + if setRawTerminal && cli.isTerminalIn { + term.RestoreTerminal(cli.inFd, oldState) + } + inputStream.Close() + } + }() + + // When TTY is ON, use regular copy + if setRawTerminal && outputStream != nil { + _, err = io.Copy(outputStream, resp.Reader) + } else { + _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) + } + logrus.Debugf("[hijack] End of stdout") + receiveStdout <- err + }() + } + + stdinDone := make(chan struct{}) + go func() { + if inputStream != nil { + io.Copy(resp.Conn, inputStream) + logrus.Debugf("[hijack] End of stdin") + } + + if err := resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + close(stdinDone) + }() + + select { + case err := <-receiveStdout: + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + case <-stdinDone: + if outputStream != nil || errorStream != nil { + if err := <-receiveStdout; err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + } + } + + return nil +} + type tlsClientCon struct { *tls.Conn rawConn net.Conn @@ -190,7 +258,6 @@ func (cli *DockerCli) hijackWithContentType(method, path, contentType string, se } var oldState *term.State - if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" { oldState, err = term.SetRawTerminal(cli.inFd) if err != nil { diff --git a/api/client/lib/client.go b/api/client/lib/client.go index dbb088a9ca..31bd3efc02 100644 --- a/api/client/lib/client.go +++ b/api/client/lib/client.go @@ -24,6 +24,8 @@ type Client struct { BasePath string // scheme holds the scheme of the client i.e. https. Scheme string + // tlsConfig holds the tls configuration to use in hijacked requests. + tlsConfig *tls.Config // httpClient holds the client transport instance. Exported to keep the old code running. HTTPClient *http.Client // version of the server to talk to. @@ -78,9 +80,11 @@ func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsc sockets.ConfigureTCPTransport(transport, proto, addr) return &Client{ + Proto: proto, Addr: addr, BasePath: basePath, Scheme: scheme, + tlsConfig: tlsConfig, HTTPClient: &http.Client{Transport: transport}, version: version, customHTTPHeaders: httpHeaders, diff --git a/api/client/lib/container_attach.go b/api/client/lib/container_attach.go new file mode 100644 index 0000000000..4ed821fa1d --- /dev/null +++ b/api/client/lib/container_attach.go @@ -0,0 +1,30 @@ +package lib + +import ( + "net/url" + + "github.com/docker/docker/api/types" +) + +// ContainerAttach attaches a connection to a container in the server. +// It returns a types.HijackedConnection with the hijacked connection +// and the a reader to get output. It's up to the called to close +// the hijacked connection by calling types.HijackedResponse.Close. +func (cli *Client) ContainerAttach(options types.ContainerAttachOptions) (*types.HijackedResponse, error) { + query := url.Values{} + if options.Stream { + query.Set("stream", "1") + } + if options.Stdin { + query.Set("stdin", "1") + } + if options.Stdout { + query.Set("stdout", "1") + } + if options.Stderr { + query.Set("stderr", "1") + } + + headers := map[string][]string{"Content-Type": {"text/plain"}} + return cli.postHijacked("/containers/"+options.ContainerID+"/attach", query, nil, headers) +} diff --git a/api/client/lib/hijack.go b/api/client/lib/hijack.go new file mode 100644 index 0000000000..4c9475bffa --- /dev/null +++ b/api/client/lib/hijack.go @@ -0,0 +1,165 @@ +package lib + +import ( + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/docker/docker/api/types" +) + +// 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(path string, query url.Values, body io.Reader, headers map[string][]string) (*types.HijackedResponse, error) { + bodyEncoded, err := encodeData(body) + if err != nil { + return nil, err + } + + req, err := cli.newRequest("POST", path, query, bodyEncoded, headers) + if err != nil { + return nil, err + } + req.Host = cli.Addr + + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "tcp") + + conn, err := dial(cli.Proto, cli.Addr, cli.tlsConfig) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return nil, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") + } + 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 := 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 + clientconn.Do(req) + + rwc, br := clientconn.Hijack() + + return &types.HijackedResponse{rwc, br}, nil +} + +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("") + }) + } + + rawConn, err := dialer.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. + c := *config + c.ServerName = hostname + config = &c + } + + 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" { + // Notice this isn't Go standard's tls.Dial function + return tlsDial(proto, addr, tlsConfig) + } + return net.Dial(proto, addr) +} diff --git a/api/client/lib/request.go b/api/client/lib/request.go index 9f98ddadba..9fcab7e425 100644 --- a/api/client/lib/request.go +++ b/api/client/lib/request.go @@ -71,38 +71,21 @@ func (cli *Client) sendRequest(method, path string, query url.Values, body inter return cli.sendClientRequest(method, path, query, params, headers) } -func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*serverResponse, error) { +func (cli *Client) sendClientRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*serverResponse, error) { serverResp := &serverResponse{ body: nil, statusCode: -1, } expectedPayload := (method == "POST" || method == "PUT") - if expectedPayload && in == nil { - in = bytes.NewReader([]byte{}) - } - - apiPath := cli.getAPIPath(path, query) - req, err := http.NewRequest(method, apiPath, in) - if err != nil { - return serverResp, err - } - - // Add CLI Config's HTTP Headers BEFORE we set the Docker headers - // then the user can't change OUR headers - for k, v := range cli.customHTTPHeaders { - req.Header.Set(k, v) + if expectedPayload && body == nil { + body = bytes.NewReader([]byte{}) } + req, err := cli.newRequest(method, path, query, body, headers) req.URL.Host = cli.Addr req.URL.Scheme = cli.Scheme - if headers != nil { - for k, v := range headers { - req.Header[k] = v - } - } - if expectedPayload && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "text/plain") } @@ -143,6 +126,28 @@ func (cli *Client) sendClientRequest(method, path string, query url.Values, in i return serverResp, nil } +func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) { + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, body) + if err != nil { + return nil, err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.customHTTPHeaders { + req.Header.Set(k, v) + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + return req, nil +} + func encodeData(data interface{}) (*bytes.Buffer, error) { params := bytes.NewBuffer(nil) if data != nil { diff --git a/api/types/client.go b/api/types/client.go index b528999f25..dbe299cbdb 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -1,14 +1,25 @@ package types import ( + "bufio" "io" + "net" "github.com/docker/docker/cliconfig" "github.com/docker/docker/pkg/parsers/filters" "github.com/docker/docker/pkg/ulimit" ) -// ContainerCommitOptions hods parameters to commit changes into a container. +// ContainerAttachOptions holds parameters to attach to a container. +type ContainerAttachOptions struct { + ContainerID string + Stream bool + Stdin bool + Stdout bool + Stderr bool +} + +// ContainerCommitOptions holds parameters to commit changes into a container. type ContainerCommitOptions struct { ContainerID string RepositoryName string @@ -67,6 +78,31 @@ type EventsOptions struct { Filters filters.Args } +// HijackedResponse holds connection information for a hijacked request. +type HijackedResponse struct { + Conn net.Conn + Reader *bufio.Reader +} + +// Close closes the hijacked connection and reader. +func (h *HijackedResponse) Close() { + h.Conn.Close() +} + +// CloseWriter is an interface that implement structs +// that close input streams to prevent from writing. +type CloseWriter interface { + CloseWrite() error +} + +// CloseWrite closes a readWriter for writing. +func (h *HijackedResponse) CloseWrite() error { + if conn, ok := h.Conn.(CloseWriter); ok { + return conn.CloseWrite() + } + return nil +} + // ImageBuildOptions holds the information // necessary to build images. type ImageBuildOptions struct {