1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Merge pull request #9537 from icecrime/9476-tty_stdin_redirect

Forbid client piping to tty enabled container
This commit is contained in:
Alexander Morozov 2014-12-09 15:17:44 -08:00
commit fa7e01794f
10 changed files with 253 additions and 87 deletions

View file

@ -3,6 +3,7 @@ package client
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@ -104,6 +105,16 @@ func (cli *DockerCli) LoadConfigFile() (err error) {
return err
}
func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
// In order to attach to a container tty, input stream for the client must
// be a tty itself: redirecting or piping the client standard input is
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
if ttyMode && attachStdin && !cli.isTerminalIn {
return errors.New("cannot enable tty mode on non tty input")
}
return nil
}
func NewDockerCli(in io.ReadCloser, out, err io.Writer, key libtrust.PrivateKey, proto, addr string, tlsConfig *tls.Config) *DockerCli {
var (
inFd uintptr

View file

@ -1974,6 +1974,10 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
tty = config.GetBool("Tty")
)
if err := cli.CheckTtyInput(!*noStdin, tty); err != nil {
return err
}
if tty && cli.isTerminalOut {
if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
log.Debugf("Error monitoring TTY size: %s", err)
@ -2288,7 +2292,11 @@ func (cli *DockerCli) CmdRun(args ...string) error {
return nil
}
if *flDetach {
if !*flDetach {
if err := cli.CheckTtyInput(config.AttachStdin, config.Tty); err != nil {
return err
}
} else {
if fl := cmd.Lookup("attach"); fl != nil {
flAttach = fl.Value.(*opts.ListOpts)
if flAttach.Len() != 0 {
@ -2600,7 +2608,11 @@ func (cli *DockerCli) CmdExec(args ...string) error {
return nil
}
if execConfig.Detach {
if !execConfig.Detach {
if err := cli.CheckTtyInput(execConfig.AttachStdin, execConfig.Tty); err != nil {
return err
}
} else {
if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil {
return err
}

View file

@ -20,6 +20,9 @@ container, or `CTRL-\` to get a stacktrace of the Docker client when it quits.
When you detach from a container the exit code will be returned to
the client.
It is forbidden to redirect the standard input of a docker attach command while
attaching to a tty-enabled container (i.e.: launched with -t`).
# OPTIONS
**--no-stdin**=*true*|*false*
Do not attach STDIN. The default is *false*.

View file

@ -31,5 +31,8 @@ container is unpaused, and then run
**-t**, **--tty**=*true*|*false*
Allocate a pseudo-TTY. The default is *false*.
The **-t** option is incompatible with a redirection of the docker client
standard input.
# HISTORY
November 2014, updated by Sven Dowideit <SvenDowideit@home.org.au>

View file

@ -267,6 +267,9 @@ outside of a container on the host.
input of any container. This can be used, for example, to run a throwaway
interactive shell. The default is value is false.
The **-t** option is incompatible with a redirection of the docker client
standard input.
**-u**, **--user**=""
Username or UID

View file

@ -94,9 +94,10 @@ specify to which of the three standard streams (`STDIN`, `STDOUT`,
$ sudo docker run -a stdin -a stdout -i -t ubuntu /bin/bash
For interactive processes (like a shell) you will typically want a tty
as well as persistent standard input (`STDIN`), so you'll use `-i -t`
together in most interactive cases.
For interactive processes (like a shell), you must use `-i -t` together in
order to allocate a tty for the container process. Specifying `-t` is however
forbidden when the client standard output is redirected or pipe, such as in:
`echo test | docker run -i busybox cat`.
## Container identification

View file

@ -87,3 +87,50 @@ func TestAttachMultipleAndRestart(t *testing.T) {
logDone("attach - multiple attach")
}
func TestAttachTtyWithoutStdin(t *testing.T) {
defer deleteAllContainers()
cmd := exec.Command(dockerBinary, "run", "-d", "-ti", "busybox")
out, _, err := runCommandWithOutput(cmd)
if err != nil {
t.Fatalf("failed to start container: %v (%v)", out, err)
}
id := strings.TrimSpace(out)
if err := waitRun(id); err != nil {
t.Fatal(err)
}
defer func() {
cmd := exec.Command(dockerBinary, "kill", id)
if out, _, err := runCommandWithOutput(cmd); err != nil {
t.Fatalf("failed to kill container: %v (%v)", out, err)
}
}()
done := make(chan struct{})
go func() {
defer close(done)
cmd := exec.Command(dockerBinary, "attach", id)
if _, err := cmd.StdinPipe(); err != nil {
t.Fatal(err)
}
expected := "cannot enable tty mode"
if out, _, err := runCommandWithOutput(cmd); err == nil {
t.Fatal("attach should have failed")
} else if !strings.Contains(out, expected) {
t.Fatal("attach failed with error %q: expected %q", out, expected)
}
}()
select {
case <-done:
case <-time.After(attachWait):
t.Fatal("attach is running but should have failed")
}
logDone("attach - forbid piped stdin to tty enabled container")
}

View file

@ -263,3 +263,91 @@ func TestExecPausedContainer(t *testing.T) {
logDone("exec - exec should not exec a pause container")
}
// regression test for #9476
func TestExecTtyCloseStdin(t *testing.T) {
defer deleteAllContainers()
cmd := exec.Command(dockerBinary, "run", "-d", "-it", "--name", "exec_tty_stdin", "busybox")
if out, _, err := runCommandWithOutput(cmd); err != nil {
t.Fatal(out, err)
}
cmd = exec.Command(dockerBinary, "exec", "-i", "exec_tty_stdin", "cat")
stdinRw, err := cmd.StdinPipe()
if err != nil {
t.Fatal(err)
}
stdinRw.Write([]byte("test"))
stdinRw.Close()
if out, _, err := runCommandWithOutput(cmd); err != nil {
t.Fatal(out, err)
}
cmd = exec.Command(dockerBinary, "top", "exec_tty_stdin")
out, _, err := runCommandWithOutput(cmd)
if err != nil {
t.Fatal(out, err)
}
outArr := strings.Split(out, "\n")
if len(outArr) > 3 || strings.Contains(out, "nsenter-exec") {
// This is the really bad part
if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "rm", "-f", "exec_tty_stdin")); err != nil {
t.Fatal(out, err)
}
t.Fatalf("exec process left running\n\t %s", out)
}
logDone("exec - stdin is closed properly with tty enabled")
}
func TestExecTtyWithoutStdin(t *testing.T) {
defer deleteAllContainers()
cmd := exec.Command(dockerBinary, "run", "-d", "-ti", "busybox")
out, _, err := runCommandWithOutput(cmd)
if err != nil {
t.Fatalf("failed to start container: %v (%v)", out, err)
}
id := strings.TrimSpace(out)
if err := waitRun(id); err != nil {
t.Fatal(err)
}
defer func() {
cmd := exec.Command(dockerBinary, "kill", id)
if out, _, err := runCommandWithOutput(cmd); err != nil {
t.Fatalf("failed to kill container: %v (%v)", out, err)
}
}()
done := make(chan struct{})
go func() {
defer close(done)
cmd := exec.Command(dockerBinary, "exec", "-ti", id, "true")
if _, err := cmd.StdinPipe(); err != nil {
t.Fatal(err)
}
expected := "cannot enable tty mode"
if out, _, err := runCommandWithOutput(cmd); err == nil {
t.Fatal("exec should have failed")
} else if !strings.Contains(out, expected) {
t.Fatal("exec failed with error %q: expected %q", out, expected)
}
}()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("exec is running but should have failed")
}
logDone("exec - forbid piped stdin to tty enabled container")
}

View file

@ -2742,3 +2742,32 @@ func TestRunPortFromDockerRangeInUse(t *testing.T) {
logDone("run - find another port if port from autorange already bound")
}
func TestRunTtyWithPipe(t *testing.T) {
defer deleteAllContainers()
done := make(chan struct{})
go func() {
defer close(done)
cmd := exec.Command(dockerBinary, "run", "-ti", "busybox", "true")
if _, err := cmd.StdinPipe(); err != nil {
t.Fatal(err)
}
expected := "cannot enable tty mode"
if out, _, err := runCommandWithOutput(cmd); err == nil {
t.Fatal("run should have failed")
} else if !strings.Contains(out, expected) {
t.Fatal("run failed with error %q: expected %q", out, expected)
}
}()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("container is running but should have failed")
}
logDone("run - forbid piped stdin with tty")
}

View file

@ -15,6 +15,7 @@ import (
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/utils"
"github.com/docker/libtrust"
"github.com/kr/pty"
)
func closeWrap(args ...io.Closer) error {
@ -162,72 +163,20 @@ func TestRunDisconnect(t *testing.T) {
})
}
// Expected behaviour: the process stay alive when the client disconnects
// but the client detaches.
func TestRunDisconnectTty(t *testing.T) {
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
key, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
defer cleanup(globalEngine, t)
c1 := make(chan struct{})
go func() {
defer close(c1)
// We're simulating a disconnect so the return value doesn't matter. What matters is the
// fact that CmdRun returns.
if err := cli.CmdRun("-i", "-t", unitTestImageID, "/bin/cat"); err != nil {
log.Debugf("Error CmdRun: %s", err)
}
}()
container := waitContainerStart(t, 10*time.Second)
state := setRaw(t, container)
defer unsetRaw(t, container, state)
// Client disconnect after run -i should keep stdin out in TTY mode
setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
t.Fatal(err)
}
})
// Close pipes (simulate disconnect)
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
t.Fatal(err)
}
// wait for CmdRun to return
setTimeout(t, "Waiting for CmdRun timed out", 5*time.Second, func() {
<-c1
})
// In tty mode, we expect the process to stay alive even after client's stdin closes.
// Give some time to monitor to do his thing
container.WaitStop(500 * time.Millisecond)
if !container.IsRunning() {
t.Fatalf("/bin/cat should still be running after closing stdin (tty mode)")
}
}
// TestRunDetach checks attaching and detaching with the escape sequence.
func TestRunDetach(t *testing.T) {
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
cpty, tty, err := pty.Open()
if err != nil {
t.Fatal(err)
}
key, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
defer cleanup(globalEngine, t)
ch := make(chan struct{})
@ -242,22 +191,22 @@ func TestRunDetach(t *testing.T) {
defer unsetRaw(t, container, state)
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
t.Fatal(err)
}
})
setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
stdinPipe.Write([]byte{16})
cpty.Write([]byte{16})
time.Sleep(100 * time.Millisecond)
stdinPipe.Write([]byte{17})
cpty.Write([]byte{17})
})
// wait for CmdRun to return
setTimeout(t, "Waiting for CmdRun timed out", 15*time.Second, func() {
<-ch
})
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
closeWrap(cpty, stdout, stdoutPipe)
time.Sleep(500 * time.Millisecond)
if !container.IsRunning() {
@ -271,14 +220,18 @@ func TestRunDetach(t *testing.T) {
// TestAttachDetach checks that attach in tty mode can be detached using the long container ID
func TestAttachDetach(t *testing.T) {
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
cpty, tty, err := pty.Open()
if err != nil {
t.Fatal(err)
}
key, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
defer cleanup(globalEngine, t)
ch := make(chan struct{})
@ -309,9 +262,13 @@ func TestAttachDetach(t *testing.T) {
state := setRaw(t, container)
defer unsetRaw(t, container, state)
stdin, stdinPipe = io.Pipe()
stdout, stdoutPipe = io.Pipe()
cli = client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cpty, tty, err = pty.Open()
if err != nil {
t.Fatal(err)
}
cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
ch = make(chan struct{})
go func() {
@ -324,7 +281,7 @@ func TestAttachDetach(t *testing.T) {
}()
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
if err != io.ErrClosedPipe {
t.Fatal(err)
}
@ -332,9 +289,9 @@ func TestAttachDetach(t *testing.T) {
})
setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
stdinPipe.Write([]byte{16})
cpty.Write([]byte{16})
time.Sleep(100 * time.Millisecond)
stdinPipe.Write([]byte{17})
cpty.Write([]byte{17})
})
// wait for CmdRun to return
@ -342,7 +299,7 @@ func TestAttachDetach(t *testing.T) {
<-ch
})
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
closeWrap(cpty, stdout, stdoutPipe)
time.Sleep(500 * time.Millisecond)
if !container.IsRunning() {
@ -356,14 +313,18 @@ func TestAttachDetach(t *testing.T) {
// TestAttachDetachTruncatedID checks that attach in tty mode can be detached
func TestAttachDetachTruncatedID(t *testing.T) {
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
cpty, tty, err := pty.Open()
if err != nil {
t.Fatal(err)
}
key, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
defer cleanup(globalEngine, t)
// Discard the CmdRun output
@ -379,9 +340,13 @@ func TestAttachDetachTruncatedID(t *testing.T) {
state := setRaw(t, container)
defer unsetRaw(t, container, state)
stdin, stdinPipe = io.Pipe()
stdout, stdoutPipe = io.Pipe()
cli = client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cpty, tty, err = pty.Open()
if err != nil {
t.Fatal(err)
}
cli = client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
ch := make(chan struct{})
go func() {
@ -394,7 +359,7 @@ func TestAttachDetachTruncatedID(t *testing.T) {
}()
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
if err != io.ErrClosedPipe {
t.Fatal(err)
}
@ -402,16 +367,16 @@ func TestAttachDetachTruncatedID(t *testing.T) {
})
setTimeout(t, "Escape sequence timeout", 5*time.Second, func() {
stdinPipe.Write([]byte{16})
cpty.Write([]byte{16})
time.Sleep(100 * time.Millisecond)
stdinPipe.Write([]byte{17})
cpty.Write([]byte{17})
})
// wait for CmdRun to return
setTimeout(t, "Waiting for CmdAttach timed out", 15*time.Second, func() {
<-ch
})
closeWrap(stdin, stdinPipe, stdout, stdoutPipe)
closeWrap(cpty, stdout, stdoutPipe)
time.Sleep(500 * time.Millisecond)
if !container.IsRunning() {
@ -425,14 +390,18 @@ func TestAttachDetachTruncatedID(t *testing.T) {
// Expected behaviour, the process stays alive when the client disconnects
func TestAttachDisconnect(t *testing.T) {
stdin, stdinPipe := io.Pipe()
stdout, stdoutPipe := io.Pipe()
cpty, tty, err := pty.Open()
if err != nil {
t.Fatal(err)
}
key, err := libtrust.GenerateECP256PrivateKey()
if err != nil {
t.Fatal(err)
}
cli := client.NewDockerCli(stdin, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
cli := client.NewDockerCli(tty, stdoutPipe, ioutil.Discard, key, testDaemonProto, testDaemonAddr, nil)
defer cleanup(globalEngine, t)
go func() {
@ -470,12 +439,12 @@ func TestAttachDisconnect(t *testing.T) {
}()
setTimeout(t, "First read/write assertion timed out", 2*time.Second, func() {
if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 150); err != nil {
if err := assertPipe("hello\n", "hello", stdout, cpty, 150); err != nil {
t.Fatal(err)
}
})
// Close pipes (client disconnects)
if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil {
if err := closeWrap(cpty, stdout, stdoutPipe); err != nil {
t.Fatal(err)
}