k8s.io/apiserver@v0.31.1/pkg/util/proxy/websocket.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package proxy
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"time"
    26  
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
    29  	constants "k8s.io/apimachinery/pkg/util/remotecommand"
    30  	"k8s.io/apimachinery/pkg/util/runtime"
    31  	"k8s.io/client-go/tools/remotecommand"
    32  )
    33  
    34  const (
    35  	// idleTimeout is the read/write deadline set for websocket server connection. Reading
    36  	// or writing the connection will return an i/o timeout if this deadline is exceeded.
    37  	// Currently, we use the same value as the kubelet websocket server.
    38  	defaultIdleConnectionTimeout = 4 * time.Hour
    39  
    40  	// Deadline for writing errors to the websocket connection before io/timeout.
    41  	writeErrorDeadline = 10 * time.Second
    42  )
    43  
    44  // Options contains details about which streams are required for
    45  // remote command execution.
    46  type Options struct {
    47  	Stdin  bool
    48  	Stdout bool
    49  	Stderr bool
    50  	Tty    bool
    51  }
    52  
    53  // conns contains the connection and streams used when
    54  // forwarding an attach or execute session into a container.
    55  type conns struct {
    56  	conn         io.Closer
    57  	stdinStream  io.ReadCloser
    58  	stdoutStream io.WriteCloser
    59  	stderrStream io.WriteCloser
    60  	writeStatus  func(status *apierrors.StatusError) error
    61  	resizeStream io.ReadCloser
    62  	resizeChan   chan remotecommand.TerminalSize
    63  	tty          bool
    64  }
    65  
    66  // Create WebSocket server streams to respond to a WebSocket client. Creates the streams passed
    67  // in the stream options.
    68  func webSocketServerStreams(req *http.Request, w http.ResponseWriter, opts Options) (*conns, error) {
    69  	ctx, err := createWebSocketStreams(req, w, opts)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	if ctx.resizeStream != nil {
    75  		ctx.resizeChan = make(chan remotecommand.TerminalSize)
    76  		go func() {
    77  			// Resize channel closes in panic case, and panic does not take down caller.
    78  			defer func() {
    79  				if p := recover(); p != nil {
    80  					// Standard panic logging.
    81  					for _, fn := range runtime.PanicHandlers {
    82  						fn(req.Context(), p)
    83  					}
    84  				}
    85  			}()
    86  			handleResizeEvents(req.Context(), ctx.resizeStream, ctx.resizeChan)
    87  		}()
    88  	}
    89  
    90  	return ctx, nil
    91  }
    92  
    93  // Read terminal resize events off of passed stream and queue into passed channel.
    94  func handleResizeEvents(ctx context.Context, stream io.Reader, channel chan<- remotecommand.TerminalSize) {
    95  	defer close(channel)
    96  
    97  	decoder := json.NewDecoder(stream)
    98  	for {
    99  		size := remotecommand.TerminalSize{}
   100  		if err := decoder.Decode(&size); err != nil {
   101  			break
   102  		}
   103  
   104  		select {
   105  		case channel <- size:
   106  		case <-ctx.Done():
   107  			// To avoid leaking this routine, exit if the http request finishes. This path
   108  			// would generally be hit if starting the process fails and nothing is started to
   109  			// ingest these resize events.
   110  			return
   111  		}
   112  	}
   113  }
   114  
   115  // createChannels returns the standard channel types for a shell connection (STDIN 0, STDOUT 1, STDERR 2)
   116  // along with the approximate duplex value. It also creates the error (3) and resize (4) channels.
   117  func createChannels(opts Options) []wsstream.ChannelType {
   118  	// open the requested channels, and always open the error channel
   119  	channels := make([]wsstream.ChannelType, 5)
   120  	channels[constants.StreamStdIn] = readChannel(opts.Stdin)
   121  	channels[constants.StreamStdOut] = writeChannel(opts.Stdout)
   122  	channels[constants.StreamStdErr] = writeChannel(opts.Stderr)
   123  	channels[constants.StreamErr] = wsstream.WriteChannel
   124  	channels[constants.StreamResize] = wsstream.ReadChannel
   125  	return channels
   126  }
   127  
   128  // readChannel returns wsstream.ReadChannel if real is true, or wsstream.IgnoreChannel.
   129  func readChannel(real bool) wsstream.ChannelType {
   130  	if real {
   131  		return wsstream.ReadChannel
   132  	}
   133  	return wsstream.IgnoreChannel
   134  }
   135  
   136  // writeChannel returns wsstream.WriteChannel if real is true, or wsstream.IgnoreChannel.
   137  func writeChannel(real bool) wsstream.ChannelType {
   138  	if real {
   139  		return wsstream.WriteChannel
   140  	}
   141  	return wsstream.IgnoreChannel
   142  }
   143  
   144  // createWebSocketStreams returns a "conns" struct containing the websocket connection and
   145  // streams needed to perform an exec or an attach.
   146  func createWebSocketStreams(req *http.Request, w http.ResponseWriter, opts Options) (*conns, error) {
   147  	channels := createChannels(opts)
   148  	conn := wsstream.NewConn(map[string]wsstream.ChannelProtocolConfig{
   149  		// WebSocket server only supports remote command version 5.
   150  		constants.StreamProtocolV5Name: {
   151  			Binary:   true,
   152  			Channels: channels,
   153  		},
   154  	})
   155  	conn.SetIdleTimeout(defaultIdleConnectionTimeout)
   156  	// Opening the connection responds to WebSocket client, negotiating
   157  	// the WebSocket upgrade connection and the subprotocol.
   158  	_, streams, err := conn.Open(w, req)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// Send an empty message to the lowest writable channel to notify the client the connection is established
   164  	switch {
   165  	case opts.Stdout:
   166  		_, err = streams[constants.StreamStdOut].Write([]byte{})
   167  	case opts.Stderr:
   168  		_, err = streams[constants.StreamStdErr].Write([]byte{})
   169  	default:
   170  		_, err = streams[constants.StreamErr].Write([]byte{})
   171  	}
   172  	if err != nil {
   173  		conn.Close()
   174  		return nil, fmt.Errorf("write error during websocket server creation: %v", err)
   175  	}
   176  
   177  	ctx := &conns{
   178  		conn:         conn,
   179  		stdinStream:  streams[constants.StreamStdIn],
   180  		stdoutStream: streams[constants.StreamStdOut],
   181  		stderrStream: streams[constants.StreamStdErr],
   182  		tty:          opts.Tty,
   183  		resizeStream: streams[constants.StreamResize],
   184  	}
   185  
   186  	// writeStatus returns a WriteStatusFunc that marshals a given api Status
   187  	// as json in the error channel.
   188  	ctx.writeStatus = func(status *apierrors.StatusError) error {
   189  		bs, err := json.Marshal(status.Status())
   190  		if err != nil {
   191  			return err
   192  		}
   193  		// Write status error to error stream with deadline.
   194  		conn.SetWriteDeadline(writeErrorDeadline)
   195  		_, err = streams[constants.StreamErr].Write(bs)
   196  		return err
   197  	}
   198  
   199  	return ctx, nil
   200  }