@@ -2,14 +2,20 @@ package router
22
33import (
44 "context"
5+ "emperror.dev/errors"
56 "encoding/json"
7+
8+ "net/http"
69 "time"
710
811 "github.com/gin-gonic/gin"
912 ws "github.com/gorilla/websocket"
1013
1114 "github.com/pyrohost/elytra/src/router/middleware"
1215 "github.com/pyrohost/elytra/src/router/websocket"
16+
17+ "github.com/pyrohost/elytra/src/server"
18+ "golang.org/x/time/rate"
1319)
1420
1521var expectedCloseCodes = []int {
@@ -25,6 +31,27 @@ func getServerWebsocket(c *gin.Context) {
2531 manager := middleware .ExtractManager (c )
2632 s , _ := manager .Get (c .Param ("server" ))
2733
34+ // Limit the total number of websockets that can be opened at any one time for
35+ // a server instance. This applies across all users connected to the server, and
36+ // is not applied on a per-user basis.
37+ //
38+ // todo: it would be great to make this per-user instead, but we need to modify
39+ // how we even request this endpoint in order for that to be possible. Some type
40+ // of signed identifier in the URL that is verified on this end and set by the
41+ // panel using a shared secret is likely the easiest option. The benefit of that
42+ // is that we can both scope things to the user before authentication, and also
43+ // verify that the JWT provided by the panel is assigned to the same user.
44+ if s .Websockets ().Len () >= 30 {
45+ c .AbortWithStatusJSON (http .StatusBadRequest , gin.H {
46+ "error" : "Too many open websocket connections." ,
47+ })
48+
49+ return
50+ }
51+
52+ c .Header ("Content-Security-Policy" , "default-src 'self'" )
53+ c .Header ("X-Frame-Options" , "DENY" )
54+
2855 // Create a context that can be canceled when the user disconnects from this
2956 // socket that will also cancel listeners running in separate threads. If the
3057 // connection itself is terminated listeners using this context will also be
@@ -37,53 +64,101 @@ func getServerWebsocket(c *gin.Context) {
3764 middleware .CaptureAndAbort (c , err )
3865 return
3966 }
40- defer handler .Connection .Close ()
4167
4268 // Track this open connection on the server so that we can close them all programmatically
4369 // if the server is deleted.
4470 s .Websockets ().Push (handler .Uuid (), & cancel )
4571 handler .Logger ().Debug ("opening connection to server websocket" )
72+ defer s .Websockets ().Remove (handler .Uuid ())
4673
47- defer func () {
48- s .Websockets ().Remove (handler .Uuid ())
49- handler .Logger ().Debug ("closing connection to server websocket" )
74+ go func () {
75+ select {
76+ // When the main context is canceled (through disconnect, server deletion, or server
77+ // suspension) close the connection itself.
78+ case <- ctx .Done ():
79+ handler .Logger ().Debug ("closing connection to server websocket" )
80+ if err := handler .Connection .Close (); err != nil {
81+ handler .Logger ().WithError (err ).Error ("failed to close websocket connection" )
82+ }
83+ break
84+ }
5085 }()
5186
52- // If the server is deleted we need to send a close message to the connected client
53- // so that they disconnect since there will be no more events sent along. Listen for
54- // the request context being closed to break this loop, otherwise this routine will
55- // be left hanging in the background.
5687 go func () {
5788 select {
5889 case <- ctx .Done ():
59- break
90+ return
91+ // If the server is deleted we need to send a close message to the connected client
92+ // so that they disconnect since there will be no more events sent along. Listen for
93+ // the request context being closed to break this loop, otherwise this routine will
94+ // be left hanging in the background.
6095 case <- s .Context ().Done ():
61- _ = handler . Connection . WriteControl ( ws . CloseMessage , ws . FormatCloseMessage ( ws . CloseGoingAway , "server deleted" ), time . Now (). Add ( time . Second * 5 ) )
96+ cancel ( )
6297 break
6398 }
6499 }()
65100
66- for {
67- j := websocket.Message {}
101+ // Due to how websockets are handled we need to connect to the socket
102+ // and _then_ abort it if the server is suspended. You cannot capture
103+ // the HTTP response in the websocket client, thus we connect and then
104+ // immediately close with failure.
105+ if s .IsSuspended () {
106+ _ = handler .Connection .WriteMessage (ws .CloseMessage , ws .FormatCloseMessage (4409 , "server is suspended" ))
107+
108+ return
109+ }
68110
69- _ , p , err := handler .Connection .ReadMessage ()
111+ // There is a separate rate limiter that applies to individual message types
112+ // within the actual websocket logic handler. _This_ rate limiter just exists
113+ // to avoid enormous floods of data through the socket since we need to parse
114+ // JSON each time. This rate limit realistically should never be hit since this
115+ // would require sending 50+ messages a second over the websocket (no more than
116+ // 10 per 200ms).
117+ var throttled bool
118+ rl := rate .NewLimiter (rate .Every (time .Millisecond * 200 ), 10 )
119+
120+ for {
121+ t , p , err := handler .Connection .ReadMessage ()
70122 if err != nil {
71123 if ws .IsUnexpectedCloseError (err , expectedCloseCodes ... ) {
72124 handler .Logger ().WithField ("error" , err ).Warn ("error handling websocket message for server" )
73125 }
74126 break
75127 }
76128
129+ if ! rl .Allow () {
130+ if ! throttled {
131+ throttled = true
132+ _ = handler .Connection .WriteJSON (websocket.Message {Event : websocket .ThrottledEvent , Args : []string {"global" }})
133+ }
134+ continue
135+ }
136+
137+ throttled = false
138+
139+ // If the message isn't a format we expect, or the length of the message is far larger
140+ // than we'd ever expect, drop it. The websocket upgrader logic does enforce a maximum
141+ // _compressed_ message size of 4Kb but that could decompress to a much larger amount
142+ // of data.
143+ if t != ws .TextMessage || len (p ) > 32_768 {
144+ continue
145+ }
146+
77147 // Discard and JSON parse errors into the void and don't continue processing this
78148 // specific socket request. If we did a break here the client would get disconnected
79149 // from the socket, which is NOT what we want to do.
150+ var j websocket.Message
80151 if err := json .Unmarshal (p , & j ); err != nil {
81152 continue
82153 }
83154
84155 go func (msg websocket.Message ) {
85156 if err := handler .HandleInbound (ctx , msg ); err != nil {
86- _ = handler .SendErrorJson (msg , err )
157+ if errors .Is (err , server .ErrSuspended ) {
158+ cancel ()
159+ } else {
160+ _ = handler .SendErrorJson (msg , err )
161+ }
87162 }
88163 }(j )
89164 }
0 commit comments