package client import ( "bytes" "crypto/tls" "encoding/base64" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "os" gosignal "os/signal" "regexp" goruntime "runtime" "strconv" "strings" "syscall" "github.com/dotcloud/docker/api" "github.com/dotcloud/docker/dockerversion" "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/pkg/term" "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/utils" ) var ( ErrConnectionRefused = errors.New("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") ) func (cli *DockerCli) dial() (net.Conn, error) { if cli.tlsConfig != nil && cli.proto != "unix" { return tls.Dial(cli.proto, cli.addr, cli.tlsConfig) } return net.Dial(cli.proto, cli.addr) } func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { params := bytes.NewBuffer(nil) if data != nil { if env, ok := data.(engine.Env); ok { if err := env.Encode(params); err != nil { return nil, -1, err } } else { buf, err := json.Marshal(data) if err != nil { return nil, -1, err } if _, err := params.Write(buf); err != nil { return nil, -1, err } } } // fixme: refactor client to support redirect re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) if err != nil { return nil, -1, err } if passAuthInfo { cli.LoadConfigFile() // Resolve the Auth config relevant for this server authConfig := cli.configFile.ResolveAuthConfig(registry.IndexServerAddress()) getHeaders := func(authConfig registry.AuthConfig) (map[string][]string, error) { buf, err := json.Marshal(authConfig) if err != nil { return nil, err } registryAuthHeader := []string{ base64.URLEncoding.EncodeToString(buf), } return map[string][]string{"X-Registry-Auth": registryAuthHeader}, nil } if headers, err := getHeaders(authConfig); err == nil && headers != nil { for k, v := range headers { req.Header[k] = v } } } req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION) req.Host = cli.addr if data != nil { req.Header.Set("Content-Type", "application/json") } else if method == "POST" { req.Header.Set("Content-Type", "plain/text") } dial, err := cli.dial() if err != nil { if strings.Contains(err.Error(), "connection refused") { return nil, -1, ErrConnectionRefused } return nil, -1, err } clientconn := httputil.NewClientConn(dial, nil) resp, err := clientconn.Do(req) if err != nil { clientconn.Close() if strings.Contains(err.Error(), "connection refused") { return nil, -1, ErrConnectionRefused } return nil, -1, err } if resp.StatusCode < 200 || resp.StatusCode >= 400 { body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, -1, err } if len(body) == 0 { return nil, resp.StatusCode, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(resp.StatusCode), req.URL) } return nil, resp.StatusCode, fmt.Errorf("Error: %s", bytes.TrimSpace(body)) } wrapper := utils.NewReadCloserWrapper(resp.Body, func() error { if resp != nil && resp.Body != nil { resp.Body.Close() } return clientconn.Close() }) return wrapper, resp.StatusCode, nil } func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error { if (method == "POST" || method == "PUT") && in == nil { in = bytes.NewReader([]byte{}) } // fixme: refactor client to support redirect re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), in) if err != nil { return err } req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION) req.Host = cli.addr if method == "POST" { req.Header.Set("Content-Type", "plain/text") } if headers != nil { for k, v := range headers { req.Header[k] = v } } dial, err := cli.dial() if err != nil { if strings.Contains(err.Error(), "connection refused") { return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") } return err } clientconn := httputil.NewClientConn(dial, nil) resp, err := clientconn.Do(req) defer clientconn.Close() if err != nil { if strings.Contains(err.Error(), "connection refused") { return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") } return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 400 { body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } if len(body) == 0 { return fmt.Errorf("Error :%s", http.StatusText(resp.StatusCode)) } return fmt.Errorf("Error: %s", bytes.TrimSpace(body)) } if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") { return utils.DisplayJSONMessagesStream(resp.Body, out, cli.terminalFd, cli.isTerminal) } if _, err := io.Copy(out, resp.Body); err != nil { return err } return nil } func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer) error { defer func() { if started != nil { close(started) } }() // fixme: refactor client to support redirect re := regexp.MustCompile("/+") path = re.ReplaceAllString(path, "/") req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), nil) if err != nil { return err } req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION) req.Header.Set("Content-Type", "plain/text") req.Host = cli.addr dial, err := cli.dial() if err != nil { if strings.Contains(err.Error(), "connection refused") { return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?") } return err } clientconn := httputil.NewClientConn(dial, nil) defer clientconn.Close() // Server hijacks the connection, error 'connection closed' expected clientconn.Do(req) rwc, br := clientconn.Hijack() defer rwc.Close() if started != nil { started <- rwc } var receiveStdout chan error var oldState *term.State if in != nil && setRawTerminal && cli.isTerminal && os.Getenv("NORAW") == "" { oldState, err = term.SetRawTerminal(cli.terminalFd) if err != nil { return err } defer term.RestoreTerminal(cli.terminalFd, oldState) } if stdout != nil || stderr != nil { receiveStdout = utils.Go(func() (err error) { defer func() { if in != nil { if setRawTerminal && cli.isTerminal { term.RestoreTerminal(cli.terminalFd, oldState) } // For some reason this Close call blocks on darwin.. // As the client exists right after, simply discard the close // until we find a better solution. if goruntime.GOOS != "darwin" { in.Close() } } }() // When TTY is ON, use regular copy if setRawTerminal { _, err = io.Copy(stdout, br) } else { _, err = utils.StdCopy(stdout, stderr, br) } utils.Debugf("[hijack] End of stdout") return err }) } sendStdin := utils.Go(func() error { if in != nil { io.Copy(rwc, in) utils.Debugf("[hijack] End of stdin") } if tcpc, ok := rwc.(*net.TCPConn); ok { if err := tcpc.CloseWrite(); err != nil { utils.Errorf("Couldn't send EOF: %s\n", err) } } else if unixc, ok := rwc.(*net.UnixConn); ok { if err := unixc.CloseWrite(); err != nil { utils.Errorf("Couldn't send EOF: %s\n", err) } } // Discard errors due to pipe interruption return nil }) if stdout != nil || stderr != nil { if err := <-receiveStdout; err != nil { utils.Errorf("Error receiveStdout: %s", err) return err } } if !cli.isTerminal { if err := <-sendStdin; err != nil { utils.Errorf("Error sendStdin: %s", err) return err } } return nil } func (cli *DockerCli) resizeTty(id string) { height, width := cli.getTtySize() if height == 0 && width == 0 { return } v := url.Values{} v.Set("h", strconv.Itoa(height)) v.Set("w", strconv.Itoa(width)) if _, _, err := readBody(cli.call("POST", "/containers/"+id+"/resize?"+v.Encode(), nil, false)); err != nil { utils.Errorf("Error resize: %s", err) } } func waitForExit(cli *DockerCli, containerId string) (int, error) { stream, _, err := cli.call("POST", "/containers/"+containerId+"/wait", nil, false) if err != nil { return -1, err } var out engine.Env if err := out.Decode(stream); err != nil { return -1, err } return out.GetInt("StatusCode"), nil } // getExitCode perform an inspect on the container. It returns // the running state and the exit code. func getExitCode(cli *DockerCli, containerId string) (bool, int, error) { body, _, err := readBody(cli.call("GET", "/containers/"+containerId+"/json", nil, false)) if err != nil { // If we can't connect, then the daemon probably died. if err != ErrConnectionRefused { return false, -1, err } return false, -1, nil } c := &api.Container{} if err := json.Unmarshal(body, c); err != nil { return false, -1, err } return c.State.Running, c.State.ExitCode, nil } func (cli *DockerCli) monitorTtySize(id string) error { cli.resizeTty(id) sigchan := make(chan os.Signal, 1) gosignal.Notify(sigchan, syscall.SIGWINCH) go func() { for _ = range sigchan { cli.resizeTty(id) } }() return nil } func (cli *DockerCli) getTtySize() (int, int) { if !cli.isTerminal { return 0, 0 } ws, err := term.GetWinsize(cli.terminalFd) if err != nil { utils.Errorf("Error getting size: %s", err) if ws == nil { return 0, 0 } } return int(ws.Height), int(ws.Width) } func readBody(stream io.ReadCloser, statusCode int, err error) ([]byte, int, error) { if stream != nil { defer stream.Close() } if err != nil { return nil, statusCode, err } body, err := ioutil.ReadAll(stream) if err != nil { return nil, -1, err } return body, statusCode, nil }