@@ -19,6 +19,7 @@ import (
19
19
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
20
20
"github.com/coder/coder/v2/coderd/tracing"
21
21
"github.com/coder/coder/v2/codersdk"
22
+ "github.com/coder/websocket"
22
23
)
23
24
24
25
var Validate * validator.Validate
@@ -280,6 +281,90 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
280
281
return msg
281
282
}
282
283
284
+ type WebSocketEvent [T any ] struct {
285
+ }
286
+
287
+ func OneWayWebSocket [T any ](rw http.ResponseWriter , r * http.Request ) (
288
+ sendEvent func (ctx context.Context , wsEvent T ) error ,
289
+ closed chan struct {},
290
+ err error ,
291
+ ) {
292
+ ctx , cancel := context .WithCancel (r .Context ())
293
+ r = r .WithContext (ctx )
294
+ socket , err := websocket .Accept (rw , r , nil )
295
+ if err != nil {
296
+ cancel ()
297
+ return nil , nil , xerrors .Errorf ("cannot establish connection: %w" , err )
298
+ }
299
+ go Heartbeat (ctx , socket )
300
+
301
+ type SocketError struct {
302
+ Code websocket.StatusCode
303
+ Reason string
304
+ }
305
+ socketErrC := make (chan SocketError , 1 )
306
+ closed = make (chan struct {}, 1 )
307
+ go func () {
308
+ select {
309
+ case err := <- socketErrC :
310
+ _ = socket .Close (err .Code , err .Reason )
311
+ case <- ctx .Done ():
312
+ _ = socket .Close (websocket .StatusNormalClosure , "Connection closed" )
313
+ }
314
+ cancel ()
315
+ close (closed )
316
+ }()
317
+
318
+ // We have some tools in the UI code to help enforce one-way WebSocket
319
+ // connections, but there's still the possibility that the client could send
320
+ // a message when it's not supposed to. If that happens, the client likely
321
+ // forgot to use those tools, and communication probably can't be trusted.
322
+ // Better to just close the socket and force the UI to fix its mess
323
+ go func () {
324
+ msgType , _ , err := socket .Read (ctx )
325
+ if errors .Is (err , context .Canceled ) {
326
+ return
327
+ }
328
+ if err != nil {
329
+ socketErrC <- SocketError {
330
+ Code : websocket .StatusInternalError ,
331
+ Reason : "Unable to process invalid message from client" ,
332
+ }
333
+ return
334
+ }
335
+ switch msgType {
336
+ case websocket .MessageBinary , websocket .MessageText :
337
+ socketErrC <- SocketError {
338
+ Code : websocket .StatusProtocolError ,
339
+ Reason : "Clients cannot send messages for one-way WebSockets" ,
340
+ }
341
+ }
342
+ }()
343
+
344
+ sendEvent = func (context.Context , T ) error {
345
+ // Using multiple selects because we want possible errors to be
346
+ // processed deterministically
347
+ select {
348
+ case _ , open := <- ctx .Done ():
349
+ if ! open {
350
+ return xerrors .New ("connection closed from client" )
351
+ }
352
+ default :
353
+ }
354
+ select {
355
+ case _ , open := <- closed :
356
+ if ! open {
357
+ return xerrors .New ("connection closed internally" )
358
+ }
359
+ default :
360
+ }
361
+
362
+ return nil
363
+ }
364
+
365
+ return sendEvent , closed , nil
366
+ }
367
+
283
368
func ServerSentEventSender (rw http.ResponseWriter , r * http.Request ) (sendEvent func (ctx context.Context , sse codersdk.ServerSentEvent ) error , closed chan struct {}, err error ) {
284
369
h := rw .Header ()
285
370
h .Set ("Content-Type" , "text/event-stream" )
0 commit comments