mirror of
https://github.com/containers/podman.git
synced 2025-10-13 17:26:13 +08:00
pkg/bindings: use HTTP 101 upgrade request for attach
For exec and attach use an upgrade request which the server responds with HTTP 101 status. Since go 1.12 the Body can be casted to an io.Writer and then use that to write to the server. This does however not allow us to skip the ugly hack of overwriting the default dialContext() because the ReadWriterCloser on the body does not allow us to call CloseWrite() which is critical to correctly close the stdin side. So we still have to extract the underlying net.Conn for that. Using the cast response.Body is important because the underlying http lib reads from the socket to parse the header and response code of course and it is possible that it read more content than needed that is kept in its buffer but because we then only directly read from the connection it was possible we miss the first few bytes of the message. This should fix the issue and hopefully also some long standing podman-remote missing output flakes in CI. Fixes: #26951 Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
@ -111,37 +111,11 @@ func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Wri
|
||||
}()
|
||||
}
|
||||
|
||||
headers := make(http.Header)
|
||||
headers.Add("Connection", "Upgrade")
|
||||
headers.Add("Upgrade", "tcp")
|
||||
|
||||
var socket net.Conn
|
||||
socketSet := false
|
||||
dialContext := conn.Client.Transport.(*http.Transport).DialContext
|
||||
t := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
c, err := dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !socketSet {
|
||||
socket = c
|
||||
socketSet = true
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
IdleConnTimeout: time.Duration(0),
|
||||
}
|
||||
conn.Client.Transport = t
|
||||
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/attach", params, headers, nameOrID)
|
||||
cw, socket, err := newUpgradeRequest(ctx, conn, nil, fmt.Sprintf("/containers/%s/attach", nameOrID), params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response.IsSuccess() && !response.IsInformational() {
|
||||
defer response.Body.Close()
|
||||
return response.Process(nil)
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
if needTTY {
|
||||
winChange := make(chan os.Signal, 1)
|
||||
@ -173,11 +147,7 @@ func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Wri
|
||||
logrus.Errorf("Failed to write input to service: %v", err)
|
||||
}
|
||||
if err == nil {
|
||||
if closeWrite, ok := socket.(CloseWriter); ok {
|
||||
if err := closeWrite.CloseWrite(); err != nil {
|
||||
logrus.Warnf("Failed to close STDIN for writing: %v", err)
|
||||
}
|
||||
}
|
||||
cw.CloseWrite()
|
||||
}
|
||||
stdinChan <- err
|
||||
}()
|
||||
@ -464,33 +434,11 @@ func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStar
|
||||
return err
|
||||
}
|
||||
|
||||
var socket net.Conn
|
||||
socketSet := false
|
||||
dialContext := conn.Client.Transport.(*http.Transport).DialContext
|
||||
t := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
c, err := dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !socketSet {
|
||||
socket = c
|
||||
socketSet = true
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
IdleConnTimeout: time.Duration(0),
|
||||
}
|
||||
conn.Client.Transport = t
|
||||
response, err := conn.DoRequest(ctx, bytes.NewReader(bodyJSON), http.MethodPost, "/exec/%s/start", nil, nil, sessionID)
|
||||
cw, socket, err := newUpgradeRequest(ctx, conn, bytes.NewReader(bodyJSON), fmt.Sprintf("/exec/%s/start", sessionID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if !response.IsSuccess() && !response.IsInformational() {
|
||||
return response.Process(nil)
|
||||
}
|
||||
defer socket.Close()
|
||||
|
||||
if needTTY {
|
||||
winChange := make(chan os.Signal, 1)
|
||||
@ -513,12 +461,7 @@ func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStar
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if closeWrite, ok := socket.(CloseWriter); ok {
|
||||
logrus.Debugf("Closing STDIN")
|
||||
if err := closeWrite.CloseWrite(); err != nil {
|
||||
logrus.Warnf("Failed to close STDIN for writing: %v", err)
|
||||
}
|
||||
}
|
||||
cw.CloseWrite()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -580,3 +523,66 @@ func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStar
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type closeWrite struct {
|
||||
// sock is the underlying socket of the connection.
|
||||
// Do not use that field directly.
|
||||
sock net.Conn
|
||||
}
|
||||
|
||||
func (cw *closeWrite) CloseWrite() {
|
||||
if closeWrite, ok := cw.sock.(CloseWriter); ok {
|
||||
logrus.Debugf("Closing STDIN")
|
||||
if err := closeWrite.CloseWrite(); err != nil {
|
||||
logrus.Warnf("Failed to close STDIN for writing: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newUpgradeRequest performs a new http Upgrade request, it return the closeWrite which should be used
|
||||
// to close the STDIN side used and the ReadWriter which MUST be uses to write/read from the connection
|
||||
// and which must closed when finished. Do not access the new.Conn in closeWrite directly.
|
||||
func newUpgradeRequest(ctx context.Context, conn *bindings.Connection, body io.Reader, path string, params url.Values) (*closeWrite, io.ReadWriteCloser, error) {
|
||||
headers := http.Header{
|
||||
"Connection": []string{"Upgrade"},
|
||||
"Upgrade": []string{"tcp"},
|
||||
}
|
||||
|
||||
var socket net.Conn
|
||||
socketSet := false
|
||||
dialContext := conn.Client.Transport.(*http.Transport).DialContext
|
||||
t := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
c, err := dialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !socketSet {
|
||||
socket = c
|
||||
socketSet = true
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
IdleConnTimeout: time.Duration(0),
|
||||
}
|
||||
conn.Client.Transport = t
|
||||
response, err := conn.DoRequest(ctx, body, http.MethodPost, path, params, headers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusSwitchingProtocols {
|
||||
defer response.Body.Close()
|
||||
if err := response.Process(nil); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("incorrect server response code %d, expected %d", response.StatusCode, http.StatusSwitchingProtocols)
|
||||
}
|
||||
rw, ok := response.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
response.Body.Close()
|
||||
return nil, nil, errors.New("internal error: cannot cast to http response Body to io.ReadWriteCloser")
|
||||
}
|
||||
|
||||
return &closeWrite{sock: socket}, rw, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user