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 }