github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/client/hijack.go (about)

     1  package client // import "github.com/docker/docker/client"
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/api/types/versions"
    14  	"github.com/pkg/errors"
    15  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    16  )
    17  
    18  // postHijacked sends a POST request and hijacks the connection.
    19  func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
    20  	bodyEncoded, err := encodeData(body)
    21  	if err != nil {
    22  		return types.HijackedResponse{}, err
    23  	}
    24  	req, err := cli.buildRequest(ctx, http.MethodPost, cli.getAPIPath(ctx, path, query), bodyEncoded, headers)
    25  	if err != nil {
    26  		return types.HijackedResponse{}, err
    27  	}
    28  	conn, mediaType, err := cli.setupHijackConn(req, "tcp")
    29  	if err != nil {
    30  		return types.HijackedResponse{}, err
    31  	}
    32  
    33  	return types.NewHijackedResponse(conn, mediaType), err
    34  }
    35  
    36  // DialHijack returns a hijacked connection with negotiated protocol proto.
    37  func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) {
    38  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	req = cli.addHeaders(req, meta)
    43  
    44  	conn, _, err := cli.setupHijackConn(req, proto)
    45  	return conn, err
    46  }
    47  
    48  func (cli *Client) setupHijackConn(req *http.Request, proto string) (_ net.Conn, _ string, retErr error) {
    49  	ctx := req.Context()
    50  	req.Header.Set("Connection", "Upgrade")
    51  	req.Header.Set("Upgrade", proto)
    52  
    53  	dialer := cli.Dialer()
    54  	conn, err := dialer(ctx)
    55  	if err != nil {
    56  		return nil, "", errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
    57  	}
    58  	defer func() {
    59  		if retErr != nil {
    60  			conn.Close()
    61  		}
    62  	}()
    63  
    64  	// When we set up a TCP connection for hijack, there could be long periods
    65  	// of inactivity (a long running command with no output) that in certain
    66  	// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
    67  	// state. Setting TCP KeepAlive on the socket connection will prohibit
    68  	// ECONNTIMEOUT unless the socket connection truly is broken
    69  	if tcpConn, ok := conn.(*net.TCPConn); ok {
    70  		_ = tcpConn.SetKeepAlive(true)
    71  		_ = tcpConn.SetKeepAlivePeriod(30 * time.Second)
    72  	}
    73  
    74  	hc := &hijackedConn{conn, bufio.NewReader(conn)}
    75  
    76  	// Server hijacks the connection, error 'connection closed' expected
    77  	resp, err := otelhttp.NewTransport(hc).RoundTrip(req)
    78  	if err != nil {
    79  		return nil, "", err
    80  	}
    81  	if resp.StatusCode != http.StatusSwitchingProtocols {
    82  		_ = resp.Body.Close()
    83  		return nil, "", fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode)
    84  	}
    85  
    86  	if hc.r.Buffered() > 0 {
    87  		// If there is buffered content, wrap the connection.  We return an
    88  		// object that implements CloseWrite if the underlying connection
    89  		// implements it.
    90  		if _, ok := hc.Conn.(types.CloseWriter); ok {
    91  			conn = &hijackedConnCloseWriter{hc}
    92  		} else {
    93  			conn = hc
    94  		}
    95  	} else {
    96  		hc.r.Reset(nil)
    97  	}
    98  
    99  	var mediaType string
   100  	if versions.GreaterThanOrEqualTo(cli.ClientVersion(), "1.42") {
   101  		// Prior to 1.42, Content-Type is always set to raw-stream and not relevant
   102  		mediaType = resp.Header.Get("Content-Type")
   103  	}
   104  
   105  	return conn, mediaType, nil
   106  }
   107  
   108  // hijackedConn wraps a net.Conn and is returned by setupHijackConn in the case
   109  // that a) there was already buffered data in the http layer when Hijack() was
   110  // called, and b) the underlying net.Conn does *not* implement CloseWrite().
   111  // hijackedConn does not implement CloseWrite() either.
   112  type hijackedConn struct {
   113  	net.Conn
   114  	r *bufio.Reader
   115  }
   116  
   117  func (c *hijackedConn) RoundTrip(req *http.Request) (*http.Response, error) {
   118  	if err := req.Write(c.Conn); err != nil {
   119  		return nil, err
   120  	}
   121  	return http.ReadResponse(c.r, req)
   122  }
   123  
   124  func (c *hijackedConn) Read(b []byte) (int, error) {
   125  	return c.r.Read(b)
   126  }
   127  
   128  // hijackedConnCloseWriter is a hijackedConn which additionally implements
   129  // CloseWrite().  It is returned by setupHijackConn in the case that a) there
   130  // was already buffered data in the http layer when Hijack() was called, and b)
   131  // the underlying net.Conn *does* implement CloseWrite().
   132  type hijackedConnCloseWriter struct {
   133  	*hijackedConn
   134  }
   135  
   136  var _ types.CloseWriter = &hijackedConnCloseWriter{}
   137  
   138  func (c *hijackedConnCloseWriter) CloseWrite() error {
   139  	conn := c.Conn.(types.CloseWriter)
   140  	return conn.CloseWrite()
   141  }