1
+ /*
2
+ Copyright The containerd 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
+
1
17
/*
2
18
Copyright 2015 The Kubernetes Authors.
3
19
@@ -21,16 +37,18 @@ import (
21
37
"fmt"
22
38
"io"
23
39
"net/http"
24
- "regexp"
25
40
"strings"
26
41
"time"
27
42
28
43
"golang.org/x/net/websocket"
29
- "k8s.io/klog/v2"
30
44
45
+ "k8s.io/apimachinery/pkg/util/httpstream"
31
46
"k8s.io/apimachinery/pkg/util/runtime"
47
+ "k8s.io/klog/v2"
32
48
)
33
49
50
+ const WebSocketProtocolHeader = "Sec-Websocket-Protocol"
51
+
34
52
// The Websocket subprotocol "channel.k8s.io" prepends each binary message with a byte indicating
35
53
// the channel number (zero indexed) the message was sent on. Messages in both directions should
36
54
// prefix their messages with this channel byte. When used for remote execution, the channel numbers
@@ -77,18 +95,47 @@ const (
77
95
ReadWriteChannel
78
96
)
79
97
80
- var (
81
- // connectionUpgradeRegex matches any Connection header value that includes upgrade
82
- connectionUpgradeRegex = regexp .MustCompile ("(^|.*,\\ s*)upgrade($|\\ s*,)" )
83
- )
84
-
85
98
// IsWebSocketRequest returns true if the incoming request contains connection upgrade headers
86
99
// for WebSockets.
87
100
func IsWebSocketRequest (req * http.Request ) bool {
88
101
if ! strings .EqualFold (req .Header .Get ("Upgrade" ), "websocket" ) {
89
102
return false
90
103
}
91
- return connectionUpgradeRegex .MatchString (strings .ToLower (req .Header .Get ("Connection" )))
104
+ return httpstream .IsUpgradeRequest (req )
105
+ }
106
+
107
+ // IsWebSocketRequestWithStreamCloseProtocol returns true if the request contains headers
108
+ // identifying that it is requesting a websocket upgrade with a remotecommand protocol
109
+ // version that supports the "CLOSE" signal; false otherwise.
110
+ func IsWebSocketRequestWithStreamCloseProtocol (req * http.Request ) bool {
111
+ if ! IsWebSocketRequest (req ) {
112
+ return false
113
+ }
114
+ requestedProtocols := strings .TrimSpace (req .Header .Get (WebSocketProtocolHeader ))
115
+ for _ , requestedProtocol := range strings .Split (requestedProtocols , "," ) {
116
+ if protocolSupportsStreamClose (strings .TrimSpace (requestedProtocol )) {
117
+ return true
118
+ }
119
+ }
120
+
121
+ return false
122
+ }
123
+
124
+ // IsWebSocketRequestWithTunnelingProtocol returns true if the request contains headers
125
+ // identifying that it is requesting a websocket upgrade with a tunneling protocol;
126
+ // false otherwise.
127
+ func IsWebSocketRequestWithTunnelingProtocol (req * http.Request ) bool {
128
+ if ! IsWebSocketRequest (req ) {
129
+ return false
130
+ }
131
+ requestedProtocols := strings .TrimSpace (req .Header .Get (WebSocketProtocolHeader ))
132
+ for _ , requestedProtocol := range strings .Split (requestedProtocols , "," ) {
133
+ if protocolSupportsWebsocketTunneling (strings .TrimSpace (requestedProtocol )) {
134
+ return true
135
+ }
136
+ }
137
+
138
+ return false
92
139
}
93
140
94
141
// IgnoreReceives reads from a WebSocket until it is closed, then returns. If timeout is set, the
@@ -172,15 +219,46 @@ func (conn *Conn) SetIdleTimeout(duration time.Duration) {
172
219
conn .timeout = duration
173
220
}
174
221
222
+ // SetWriteDeadline sets a timeout on writing to the websocket connection. The
223
+ // passed "duration" identifies how far into the future the write must complete
224
+ // by before the timeout fires.
225
+ func (conn * Conn ) SetWriteDeadline (duration time.Duration ) {
226
+ conn .ws .SetWriteDeadline (time .Now ().Add (duration )) //nolint:errcheck
227
+ }
228
+
175
229
// Open the connection and create channels for reading and writing. It returns
176
230
// the selected subprotocol, a slice of channels and an error.
177
231
func (conn * Conn ) Open (w http.ResponseWriter , req * http.Request ) (string , []io.ReadWriteCloser , error ) {
232
+ // serveHTTPComplete is channel that is closed/selected when "websocket#ServeHTTP" finishes.
233
+ serveHTTPComplete := make (chan struct {})
234
+ // Ensure panic in spawned goroutine is propagated into the parent goroutine.
235
+ panicChan := make (chan any , 1 )
178
236
go func () {
179
- defer runtime .HandleCrash ()
180
- defer conn .Close ()
237
+ // If websocket server returns, propagate panic if necessary. Otherwise,
238
+ // signal HTTPServe finished by closing "serveHTTPComplete".
239
+ defer func () {
240
+ if p := recover (); p != nil {
241
+ panicChan <- p
242
+ } else {
243
+ close (serveHTTPComplete )
244
+ }
245
+ }()
181
246
websocket.Server {Handshake : conn .handshake , Handler : conn .handle }.ServeHTTP (w , req )
182
247
}()
183
- <- conn .ready
248
+
249
+ // In normal circumstances, "websocket.Server#ServeHTTP" calls "initialize" which closes
250
+ // "conn.ready" and then blocks until serving is complete.
251
+ select {
252
+ case <- conn .ready :
253
+ klog .V (8 ).Infof ("websocket server initialized--serving" )
254
+ case <- serveHTTPComplete :
255
+ // websocket server returned before completing initialization; cleanup and return error.
256
+ conn .closeNonThreadSafe () //nolint:errcheck
257
+ return "" , nil , fmt .Errorf ("websocket server finished before becoming ready" )
258
+ case p := <- panicChan :
259
+ panic (p )
260
+ }
261
+
184
262
rwc := make ([]io.ReadWriteCloser , len (conn .channels ))
185
263
for i := range conn .channels {
186
264
rwc [i ] = conn .channels [i ]
@@ -229,20 +307,50 @@ func (conn *Conn) resetTimeout() {
229
307
}
230
308
}
231
309
232
- // Close is only valid after Open has been called
233
- func ( conn * Conn ) Close () error {
234
- <- conn . ready
310
+ // closeNonThreadSafe cleans up by closing streams and the websocket
311
+ // connection *without* waiting for the "ready" channel.
312
+ func ( conn * Conn ) closeNonThreadSafe () error {
235
313
for _ , s := range conn .channels {
236
314
s .Close ()
237
315
}
238
- conn .ws .Close ()
239
- return nil
316
+ var err error
317
+ if conn .ws != nil {
318
+ err = conn .ws .Close ()
319
+ }
320
+ return err
321
+ }
322
+
323
+ // Close is only valid after Open has been called
324
+ func (conn * Conn ) Close () error {
325
+ <- conn .ready
326
+ return conn .closeNonThreadSafe ()
327
+ }
328
+
329
+ const (
330
+ StreamProtocolV5Name = "v5.channel.k8s.io"
331
+ WebsocketsSPDYTunnelingPrefix = "SPDY/3.1+"
332
+ KubernetesSuffix = ".k8s.io"
333
+ StreamClose = 255
334
+ )
335
+
336
+ // protocolSupportsStreamClose returns true if the passed protocol
337
+ // supports the stream close signal (currently only V5 remotecommand);
338
+ // false otherwise.
339
+ func protocolSupportsStreamClose (protocol string ) bool {
340
+ return protocol == StreamProtocolV5Name
341
+ }
342
+
343
+ // protocolSupportsWebsocketTunneling returns true if the passed protocol
344
+ // is a tunneled Kubernetes spdy protocol; false otherwise.
345
+ func protocolSupportsWebsocketTunneling (protocol string ) bool {
346
+ return strings .HasPrefix (protocol , WebsocketsSPDYTunnelingPrefix ) && strings .HasSuffix (protocol , KubernetesSuffix )
240
347
}
241
348
242
349
// handle implements a websocket handler.
243
350
func (conn * Conn ) handle (ws * websocket.Conn ) {
244
- defer conn .Close ()
245
351
conn .initialize (ws )
352
+ defer conn .Close ()
353
+ supportsStreamClose := protocolSupportsStreamClose (conn .selectedProtocol )
246
354
247
355
for {
248
356
conn .resetTimeout ()
@@ -256,6 +364,21 @@ func (conn *Conn) handle(ws *websocket.Conn) {
256
364
if len (data ) == 0 {
257
365
continue
258
366
}
367
+ if supportsStreamClose && data [0 ] == StreamClose {
368
+ if len (data ) != 2 {
369
+ klog .Errorf ("Single channel byte should follow stream close signal. Got %d bytes" , len (data )- 1 )
370
+ break
371
+ } else {
372
+ channel := data [1 ]
373
+ if int (channel ) >= len (conn .channels ) {
374
+ klog .Errorf ("Close is targeted for a channel %d that is not valid, possible protocol error" , channel )
375
+ break
376
+ }
377
+ klog .V (4 ).Infof ("Received half-close signal from client; close %d stream" , channel )
378
+ conn .channels [channel ].Close () // After first Close, other closes are noop.
379
+ }
380
+ continue
381
+ }
259
382
channel := data [0 ]
260
383
if conn .codec == base64Codec {
261
384
channel = channel - '0'
@@ -266,7 +389,7 @@ func (conn *Conn) handle(ws *websocket.Conn) {
266
389
continue
267
390
}
268
391
if _ , err := conn .channels [channel ].DataFromSocket (data ); err != nil {
269
- klog .Errorf ("Unable to write frame to %d: %v\n %s " , channel , err , string ( data ) )
392
+ klog .Errorf ("Unable to write frame (%d bytes) to %d: %v" , len ( data ), channel , err )
270
393
continue
271
394
}
272
395
}
0 commit comments