github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/bindings/containers/attach.go (about)

     1  package containers
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/binary"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"reflect"
    14  	"strconv"
    15  	"time"
    16  
    17  	"github.com/hanks177/podman/v4/libpod/define"
    18  	"github.com/hanks177/podman/v4/pkg/bindings"
    19  	"github.com/hanks177/podman/v4/utils"
    20  	"github.com/moby/term"
    21  	"github.com/pkg/errors"
    22  	"github.com/sirupsen/logrus"
    23  	terminal "golang.org/x/term"
    24  )
    25  
    26  // The CloseWriter interface is used to determine whether we can do a  one-sided
    27  // close of a hijacked connection.
    28  type CloseWriter interface {
    29  	CloseWrite() error
    30  }
    31  
    32  // Attach attaches to a running container
    33  func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Writer, stderr io.Writer, attachReady chan bool, options *AttachOptions) error {
    34  	if options == nil {
    35  		options = new(AttachOptions)
    36  	}
    37  	isSet := struct {
    38  		stdin  bool
    39  		stdout bool
    40  		stderr bool
    41  	}{
    42  		stdin:  !(stdin == nil || reflect.ValueOf(stdin).IsNil()),
    43  		stdout: !(stdout == nil || reflect.ValueOf(stdout).IsNil()),
    44  		stderr: !(stderr == nil || reflect.ValueOf(stderr).IsNil()),
    45  	}
    46  	// Ensure golang can determine that interfaces are "really" nil
    47  	if !isSet.stdin {
    48  		stdin = (io.Reader)(nil)
    49  	}
    50  	if !isSet.stdout {
    51  		stdout = (io.Writer)(nil)
    52  	}
    53  	if !isSet.stderr {
    54  		stderr = (io.Writer)(nil)
    55  	}
    56  
    57  	logrus.Infof("Going to attach to container %q", nameOrID)
    58  
    59  	conn, err := bindings.GetClient(ctx)
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	// Do we need to wire in stdin?
    65  	ctnr, err := Inspect(ctx, nameOrID, new(InspectOptions).WithSize(false))
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	params, err := options.ToParams()
    71  	if err != nil {
    72  		return err
    73  	}
    74  	detachKeysInBytes := []byte{}
    75  	if options.Changed("DetachKeys") {
    76  		params.Add("detachKeys", options.GetDetachKeys())
    77  
    78  		detachKeysInBytes, err = term.ToBytes(options.GetDetachKeys())
    79  		if err != nil {
    80  			return errors.Wrapf(err, "invalid detach keys")
    81  		}
    82  	}
    83  	if isSet.stdin {
    84  		params.Add("stdin", "true")
    85  	}
    86  	if isSet.stdout {
    87  		params.Add("stdout", "true")
    88  	}
    89  	if isSet.stderr {
    90  		params.Add("stderr", "true")
    91  	}
    92  
    93  	// Unless all requirements are met, don't use "stdin" is a terminal
    94  	file, ok := stdin.(*os.File)
    95  	outFile, outOk := stdout.(*os.File)
    96  	needTTY := ok && outOk && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty
    97  	if needTTY {
    98  		state, err := setRawTerminal(file)
    99  		if err != nil {
   100  			return err
   101  		}
   102  		defer func() {
   103  			if err := terminal.Restore(int(file.Fd()), state); err != nil {
   104  				logrus.Errorf("Unable to restore terminal: %q", err)
   105  			}
   106  			logrus.SetFormatter(&logrus.TextFormatter{})
   107  		}()
   108  	}
   109  
   110  	headers := make(http.Header)
   111  	headers.Add("Connection", "Upgrade")
   112  	headers.Add("Upgrade", "tcp")
   113  
   114  	var socket net.Conn
   115  	socketSet := false
   116  	dialContext := conn.Client.Transport.(*http.Transport).DialContext
   117  	t := &http.Transport{
   118  		DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
   119  			c, err := dialContext(ctx, network, address)
   120  			if err != nil {
   121  				return nil, err
   122  			}
   123  			if !socketSet {
   124  				socket = c
   125  				socketSet = true
   126  			}
   127  			return c, err
   128  		},
   129  		IdleConnTimeout: time.Duration(0),
   130  	}
   131  	conn.Client.Transport = t
   132  	response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/attach", params, headers, nameOrID)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	if !(response.IsSuccess() || response.IsInformational()) {
   138  		defer response.Body.Close()
   139  		return response.Process(nil)
   140  	}
   141  
   142  	if needTTY {
   143  		winChange := make(chan os.Signal, 1)
   144  		winCtx, winCancel := context.WithCancel(ctx)
   145  		defer winCancel()
   146  		notifyWinChange(winCtx, winChange, file, outFile)
   147  		attachHandleResize(ctx, winCtx, winChange, false, nameOrID, file, outFile)
   148  	}
   149  
   150  	// If we are attaching around a start, we need to "signal"
   151  	// back that we are in fact attached so that started does
   152  	// not execute before we can attach.
   153  	if attachReady != nil {
   154  		attachReady <- true
   155  	}
   156  
   157  	stdoutChan := make(chan error)
   158  	stdinChan := make(chan error, 1) // stdin channel should not block
   159  
   160  	if isSet.stdin {
   161  		go func() {
   162  			logrus.Debugf("Copying STDIN to socket")
   163  
   164  			_, err := utils.CopyDetachable(socket, stdin, detachKeysInBytes)
   165  			if err != nil && err != define.ErrDetach {
   166  				logrus.Errorf("Failed to write input to service: %v", err)
   167  			}
   168  			if err == nil {
   169  				if closeWrite, ok := socket.(CloseWriter); ok {
   170  					if err := closeWrite.CloseWrite(); err != nil {
   171  						logrus.Warnf("Failed to close STDIN for writing: %v", err)
   172  					}
   173  				}
   174  			}
   175  			stdinChan <- err
   176  		}()
   177  	}
   178  
   179  	buffer := make([]byte, 1024)
   180  	if ctnr.Config.Tty {
   181  		go func() {
   182  			logrus.Debugf("Copying STDOUT of container in terminal mode")
   183  
   184  			if !isSet.stdout {
   185  				stdoutChan <- fmt.Errorf("container %q requires stdout to be set", ctnr.ID)
   186  			}
   187  			// If not multiplex'ed, read from server and write to stdout
   188  			_, err := io.Copy(stdout, socket)
   189  
   190  			stdoutChan <- err
   191  		}()
   192  
   193  		for {
   194  			select {
   195  			case err := <-stdoutChan:
   196  				if err != nil {
   197  					return err
   198  				}
   199  
   200  				return nil
   201  			case err := <-stdinChan:
   202  				if err != nil {
   203  					return err
   204  				}
   205  
   206  				return nil
   207  			}
   208  		}
   209  	} else {
   210  		logrus.Debugf("Copying standard streams of container %q in non-terminal mode", ctnr.ID)
   211  		for {
   212  			// Read multiplexed channels and write to appropriate stream
   213  			fd, l, err := DemuxHeader(socket, buffer)
   214  			if err != nil {
   215  				if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
   216  					return nil
   217  				}
   218  				return err
   219  			}
   220  			frame, err := DemuxFrame(socket, buffer, l)
   221  			if err != nil {
   222  				return err
   223  			}
   224  
   225  			switch {
   226  			case fd == 0:
   227  				if isSet.stdout {
   228  					if _, err := stdout.Write(frame[0:l]); err != nil {
   229  						return err
   230  					}
   231  				}
   232  			case fd == 1:
   233  				if isSet.stdout {
   234  					if _, err := stdout.Write(frame[0:l]); err != nil {
   235  						return err
   236  					}
   237  				}
   238  			case fd == 2:
   239  				if isSet.stderr {
   240  					if _, err := stderr.Write(frame[0:l]); err != nil {
   241  						return err
   242  					}
   243  				}
   244  			case fd == 3:
   245  				return fmt.Errorf("from service from stream: %s", frame)
   246  			default:
   247  				return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd)
   248  			}
   249  		}
   250  	}
   251  }
   252  
   253  // DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel
   254  func DemuxHeader(r io.Reader, buffer []byte) (fd, sz int, err error) {
   255  	n, err := io.ReadFull(r, buffer[0:8])
   256  	if err != nil {
   257  		return
   258  	}
   259  	if n < 8 {
   260  		err = io.ErrUnexpectedEOF
   261  		return
   262  	}
   263  
   264  	fd = int(buffer[0])
   265  	if fd < 0 || fd > 3 {
   266  		err = errors.Wrapf(ErrLostSync, fmt.Sprintf(`channel "%d" found, 0-3 supported`, fd))
   267  		return
   268  	}
   269  
   270  	sz = int(binary.BigEndian.Uint32(buffer[4:8]))
   271  	return
   272  }
   273  
   274  // DemuxFrame reads contents for frame from server multiplexed stdin/stdout/stderr/2nd error channel
   275  func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error) {
   276  	if len(buffer) < length {
   277  		buffer = append(buffer, make([]byte, length-len(buffer)+1)...)
   278  	}
   279  
   280  	n, err := io.ReadFull(r, buffer[0:length])
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  	if n < length {
   285  		err = io.ErrUnexpectedEOF
   286  		return
   287  	}
   288  
   289  	return buffer[0:length], nil
   290  }
   291  
   292  // ResizeContainerTTY sets container's TTY height and width in characters
   293  func ResizeContainerTTY(ctx context.Context, nameOrID string, options *ResizeTTYOptions) error {
   294  	if options == nil {
   295  		options = new(ResizeTTYOptions)
   296  	}
   297  	return resizeTTY(ctx, bindings.JoinURL("containers", nameOrID, "resize"), options.Height, options.Width)
   298  }
   299  
   300  // ResizeExecTTY sets session's TTY height and width in characters
   301  func ResizeExecTTY(ctx context.Context, nameOrID string, options *ResizeExecTTYOptions) error {
   302  	if options == nil {
   303  		options = new(ResizeExecTTYOptions)
   304  	}
   305  	return resizeTTY(ctx, bindings.JoinURL("exec", nameOrID, "resize"), options.Height, options.Width)
   306  }
   307  
   308  // resizeTTY set size of TTY of container
   309  func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error {
   310  	conn, err := bindings.GetClient(ctx)
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	params := url.Values{}
   316  	if height != nil {
   317  		params.Set("h", strconv.Itoa(*height))
   318  	}
   319  	if width != nil {
   320  		params.Set("w", strconv.Itoa(*width))
   321  	}
   322  	params.Set("running", "true")
   323  	rsp, err := conn.DoRequest(ctx, nil, http.MethodPost, endpoint, params, nil)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	defer rsp.Body.Close()
   328  
   329  	return rsp.Process(nil)
   330  }
   331  
   332  type rawFormatter struct {
   333  	logrus.TextFormatter
   334  }
   335  
   336  func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   337  	buffer, err := f.TextFormatter.Format(entry)
   338  	if err != nil {
   339  		return buffer, err
   340  	}
   341  	return append(buffer, '\r'), nil
   342  }
   343  
   344  // This is intended to not be run as a goroutine, handling resizing for a container
   345  // or exec session. It will call resize once and then starts a goroutine which calls resize on winChange
   346  func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, isExec bool, id string, file *os.File, outFile *os.File) {
   347  	resize := func() {
   348  		w, h, err := getTermSize(file, outFile)
   349  		if err != nil {
   350  			logrus.Warnf("Failed to obtain TTY size: %v", err)
   351  		}
   352  
   353  		var resizeErr error
   354  		if isExec {
   355  			resizeErr = ResizeExecTTY(ctx, id, new(ResizeExecTTYOptions).WithHeight(h).WithWidth(w))
   356  		} else {
   357  			resizeErr = ResizeContainerTTY(ctx, id, new(ResizeTTYOptions).WithHeight(h).WithWidth(w))
   358  		}
   359  		if resizeErr != nil {
   360  			logrus.Infof("Failed to resize TTY: %v", resizeErr)
   361  		}
   362  	}
   363  
   364  	resize()
   365  
   366  	go func() {
   367  		for {
   368  			select {
   369  			case <-winCtx.Done():
   370  				return
   371  			case <-winChange:
   372  				resize()
   373  			}
   374  		}
   375  	}()
   376  }
   377  
   378  // Configure the given terminal for raw mode
   379  func setRawTerminal(file *os.File) (*terminal.State, error) {
   380  	state, err := makeRawTerm(file)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	logrus.SetFormatter(&rawFormatter{})
   386  
   387  	return state, err
   388  }
   389  
   390  // ExecStartAndAttach starts and attaches to a given exec session.
   391  func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStartAndAttachOptions) error {
   392  	if options == nil {
   393  		options = new(ExecStartAndAttachOptions)
   394  	}
   395  	conn, err := bindings.GetClient(ctx)
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	// TODO: Make this configurable (can't use streams' InputStream as it's
   401  	// buffered)
   402  	terminalFile := os.Stdin
   403  	terminalOutFile := os.Stdout
   404  
   405  	logrus.Debugf("Starting & Attaching to exec session ID %q", sessionID)
   406  
   407  	// We need to inspect the exec session first to determine whether to use
   408  	// -t.
   409  	resp, err := conn.DoRequest(ctx, nil, http.MethodGet, "/exec/%s/json", nil, nil, sessionID)
   410  	if err != nil {
   411  		return err
   412  	}
   413  	defer resp.Body.Close()
   414  
   415  	respStruct := new(define.InspectExecSession)
   416  	if err := resp.Process(respStruct); err != nil {
   417  		return err
   418  	}
   419  	isTerm := true
   420  	if respStruct.ProcessConfig != nil {
   421  		isTerm = respStruct.ProcessConfig.Tty
   422  	}
   423  
   424  	// If we are in TTY mode, we need to set raw mode for the terminal.
   425  	// TODO: Share all of this with Attach() for containers.
   426  	needTTY := terminalFile != nil && terminal.IsTerminal(int(terminalFile.Fd())) && isTerm
   427  
   428  	body := struct {
   429  		Detach bool   `json:"Detach"`
   430  		TTY    bool   `json:"Tty"`
   431  		Height uint16 `json:"h"`
   432  		Width  uint16 `json:"w"`
   433  	}{
   434  		Detach: false,
   435  		TTY:    needTTY,
   436  	}
   437  
   438  	if needTTY {
   439  		state, err := setRawTerminal(terminalFile)
   440  		if err != nil {
   441  			return err
   442  		}
   443  		defer func() {
   444  			if err := terminal.Restore(int(terminalFile.Fd()), state); err != nil {
   445  				logrus.Errorf("Unable to restore terminal: %q", err)
   446  			}
   447  			logrus.SetFormatter(&logrus.TextFormatter{})
   448  		}()
   449  		w, h, err := getTermSize(terminalFile, terminalOutFile)
   450  		if err != nil {
   451  			logrus.Warnf("Failed to obtain TTY size: %v", err)
   452  		}
   453  		body.Width = uint16(w)
   454  		body.Height = uint16(h)
   455  	}
   456  
   457  	bodyJSON, err := json.Marshal(body)
   458  	if err != nil {
   459  		return err
   460  	}
   461  
   462  	var socket net.Conn
   463  	socketSet := false
   464  	dialContext := conn.Client.Transport.(*http.Transport).DialContext
   465  	t := &http.Transport{
   466  		DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
   467  			c, err := dialContext(ctx, network, address)
   468  			if err != nil {
   469  				return nil, err
   470  			}
   471  			if !socketSet {
   472  				socket = c
   473  				socketSet = true
   474  			}
   475  			return c, err
   476  		},
   477  		IdleConnTimeout: time.Duration(0),
   478  	}
   479  	conn.Client.Transport = t
   480  	response, err := conn.DoRequest(ctx, bytes.NewReader(bodyJSON), http.MethodPost, "/exec/%s/start", nil, nil, sessionID)
   481  	if err != nil {
   482  		return err
   483  	}
   484  	defer response.Body.Close()
   485  
   486  	if !(response.IsSuccess() || response.IsInformational()) {
   487  		return response.Process(nil)
   488  	}
   489  
   490  	if needTTY {
   491  		winChange := make(chan os.Signal, 1)
   492  		winCtx, winCancel := context.WithCancel(ctx)
   493  		defer winCancel()
   494  
   495  		notifyWinChange(winCtx, winChange, terminalFile, terminalOutFile)
   496  		attachHandleResize(ctx, winCtx, winChange, true, sessionID, terminalFile, terminalOutFile)
   497  	}
   498  
   499  	if options.GetAttachInput() {
   500  		go func() {
   501  			logrus.Debugf("Copying STDIN to socket")
   502  			_, err := utils.CopyDetachable(socket, options.InputStream, []byte{})
   503  			if err != nil {
   504  				logrus.Errorf("Failed to write input to service: %v", err)
   505  			}
   506  
   507  			if closeWrite, ok := socket.(CloseWriter); ok {
   508  				logrus.Debugf("Closing STDIN")
   509  				if err := closeWrite.CloseWrite(); err != nil {
   510  					logrus.Warnf("Failed to close STDIN for writing: %v", err)
   511  				}
   512  			}
   513  		}()
   514  	}
   515  
   516  	buffer := make([]byte, 1024)
   517  	if isTerm {
   518  		logrus.Debugf("Handling terminal attach to exec")
   519  		if !options.GetAttachOutput() {
   520  			return fmt.Errorf("exec session %s has a terminal and must have STDOUT enabled", sessionID)
   521  		}
   522  		// If not multiplex'ed, read from server and write to stdout
   523  		_, err := utils.CopyDetachable(options.GetOutputStream(), socket, []byte{})
   524  		if err != nil {
   525  			return err
   526  		}
   527  	} else {
   528  		logrus.Debugf("Handling non-terminal attach to exec")
   529  		for {
   530  			// Read multiplexed channels and write to appropriate stream
   531  			fd, l, err := DemuxHeader(socket, buffer)
   532  			if err != nil {
   533  				if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
   534  					return nil
   535  				}
   536  				return err
   537  			}
   538  			frame, err := DemuxFrame(socket, buffer, l)
   539  			if err != nil {
   540  				return err
   541  			}
   542  
   543  			switch {
   544  			case fd == 0:
   545  				if options.GetAttachInput() {
   546  					// Write STDIN to STDOUT (echoing characters
   547  					// typed by another attach session)
   548  					if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil {
   549  						return err
   550  					}
   551  				}
   552  			case fd == 1:
   553  				if options.GetAttachOutput() {
   554  					if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil {
   555  						return err
   556  					}
   557  				}
   558  			case fd == 2:
   559  				if options.GetAttachError() {
   560  					if _, err := options.GetErrorStream().Write(frame[0:l]); err != nil {
   561  						return err
   562  					}
   563  				}
   564  			case fd == 3:
   565  				return fmt.Errorf("from service from stream: %s", frame)
   566  			default:
   567  				return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd)
   568  			}
   569  		}
   570  	}
   571  	return nil
   572  }