@@ -16,6 +16,9 @@ import (
16
16
"github.com/go-playground/validator/v10"
17
17
"golang.org/x/xerrors"
18
18
19
+ "github.com/coder/websocket"
20
+ "github.com/coder/websocket/wsjson"
21
+
19
22
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
20
23
"github.com/coder/coder/v2/coderd/tracing"
21
24
"github.com/coder/coder/v2/codersdk"
@@ -282,7 +285,25 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
282
285
return msg
283
286
}
284
287
285
- func ServerSentEventSender (rw http.ResponseWriter , r * http.Request ) (sendEvent func (ctx context.Context , sse codersdk.ServerSentEvent ) error , closed chan struct {}, err error ) {
288
+ type InitializeConnectionCallback func (rw http.ResponseWriter , r * http.Request ) (
289
+ sendEvent func (sse codersdk.ServerSentEvent ) error ,
290
+ done <- chan struct {},
291
+ err error ,
292
+ )
293
+
294
+ // ServerSentEventSender establishes a Server-Sent Event connection and allows
295
+ // the consumer to send messages to the client.
296
+ //
297
+ // The function returned allows you to send a single message to the client,
298
+ // while the channel lets you listen for when the connection closes.
299
+ //
300
+ // As much as possible, this function should be avoided in favor of using the
301
+ // OneWayWebSocket function. See OneWayWebSocket for more context.
302
+ func ServerSentEventSender (rw http.ResponseWriter , r * http.Request ) (
303
+ func (sse codersdk.ServerSentEvent ) error ,
304
+ <- chan struct {},
305
+ error ,
306
+ ) {
286
307
h := rw .Header ()
287
308
h .Set ("Content-Type" , "text/event-stream" )
288
309
h .Set ("Cache-Control" , "no-cache" )
@@ -294,7 +315,8 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (sendEvent f
294
315
panic ("http.ResponseWriter is not http.Flusher" )
295
316
}
296
317
297
- closed = make (chan struct {})
318
+ ctx := r .Context ()
319
+ closed := make (chan struct {})
298
320
type sseEvent struct {
299
321
payload []byte
300
322
errC chan error
@@ -333,21 +355,21 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (sendEvent f
333
355
}
334
356
}()
335
357
336
- sendEvent = func (ctx context. Context , sse codersdk.ServerSentEvent ) error {
358
+ sendEvent : = func (newEvent codersdk.ServerSentEvent ) error {
337
359
buf := & bytes.Buffer {}
338
360
enc := json .NewEncoder (buf )
339
361
340
- _ , err := buf .WriteString (fmt .Sprintf ("event: %s\n " , sse .Type ))
362
+ _ , err := buf .WriteString (fmt .Sprintf ("event: %s\n " , newEvent .Type ))
341
363
if err != nil {
342
364
return err
343
365
}
344
366
345
- if sse .Data != nil {
367
+ if newEvent .Data != nil {
346
368
_ , err = buf .WriteString ("data: " )
347
369
if err != nil {
348
370
return err
349
371
}
350
- err = enc .Encode (sse .Data )
372
+ err = enc .Encode (newEvent .Data )
351
373
if err != nil {
352
374
return err
353
375
}
@@ -387,3 +409,94 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (sendEvent f
387
409
388
410
return sendEvent , closed , nil
389
411
}
412
+
413
+ // OneWayWebSocket establishes a new WebSocket connection that enforces one-way
414
+ // communication from the server to the client.
415
+ //
416
+ // The function returned allows you to send a single message to the client,
417
+ // while the channel lets you listen for when the connection closes.
418
+ //
419
+ // We must use an approach like this instead of Server-Sent Events for the
420
+ // browser, because on HTTP/1.1 connections, browsers are locked to no more than
421
+ // six HTTP connections for a domain total, across all tabs. If a user were to
422
+ // open a workspace in multiple tabs, the entire UI can start to lock up.
423
+ // WebSockets have no such limitation, no matter what HTTP protocol was used to
424
+ // establish the connection.
425
+ func OneWayWebSocket (rw http.ResponseWriter , r * http.Request ) (
426
+ func (event codersdk.ServerSentEvent ) error ,
427
+ <- chan struct {},
428
+ error ,
429
+ ) {
430
+ ctx , cancel := context .WithCancel (r .Context ())
431
+ r = r .WithContext (ctx )
432
+ socket , err := websocket .Accept (rw , r , nil )
433
+ if err != nil {
434
+ cancel ()
435
+ return nil , nil , xerrors .Errorf ("cannot establish connection: %w" , err )
436
+ }
437
+ go Heartbeat (ctx , socket )
438
+
439
+ type SocketError struct {
440
+ Code websocket.StatusCode
441
+ Reason string
442
+ }
443
+ eventC := make (chan codersdk.ServerSentEvent )
444
+ socketErrC := make (chan SocketError , 1 )
445
+ closed := make (chan struct {})
446
+ go func () {
447
+ defer cancel ()
448
+ defer close (closed )
449
+
450
+ for {
451
+ select {
452
+ case event := <- eventC :
453
+ writeCtx , cancel := context .WithTimeout (ctx , 10 * time .Second )
454
+ err := wsjson .Write (writeCtx , socket , event )
455
+ cancel ()
456
+ if err == nil {
457
+ continue
458
+ }
459
+ _ = socket .Close (websocket .StatusInternalError , "Unable to send newest message" )
460
+ case err := <- socketErrC :
461
+ _ = socket .Close (err .Code , err .Reason )
462
+ case <- ctx .Done ():
463
+ _ = socket .Close (websocket .StatusNormalClosure , "Connection closed" )
464
+ }
465
+ return
466
+ }
467
+ }()
468
+
469
+ // We have some tools in the UI code to help enforce one-way WebSocket
470
+ // connections, but there's still the possibility that the client could send
471
+ // a message when it's not supposed to. If that happens, the client likely
472
+ // forgot to use those tools, and communication probably can't be trusted.
473
+ // Better to just close the socket and force the UI to fix its mess
474
+ go func () {
475
+ _ , _ , err := socket .Read (ctx )
476
+ if errors .Is (err , context .Canceled ) {
477
+ return
478
+ }
479
+ if err != nil {
480
+ socketErrC <- SocketError {
481
+ Code : websocket .StatusInternalError ,
482
+ Reason : "Unable to process invalid message from client" ,
483
+ }
484
+ return
485
+ }
486
+ socketErrC <- SocketError {
487
+ Code : websocket .StatusProtocolError ,
488
+ Reason : "Clients cannot send messages for one-way WebSockets" ,
489
+ }
490
+ }()
491
+
492
+ sendEvent := func (event codersdk.ServerSentEvent ) error {
493
+ select {
494
+ case eventC <- event :
495
+ case <- ctx .Done ():
496
+ return ctx .Err ()
497
+ }
498
+ return nil
499
+ }
500
+
501
+ return sendEvent , closed , nil
502
+ }
0 commit comments