github.com/psyb0t/mattermost-server@v4.6.1-0.20180125161845-5503a1351abf+incompatible/app/web_conn.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package app 5 6 import ( 7 "fmt" 8 "sync/atomic" 9 "time" 10 11 "github.com/mattermost/mattermost-server/model" 12 "github.com/mattermost/mattermost-server/utils" 13 14 l4g "github.com/alecthomas/log4go" 15 "github.com/gorilla/websocket" 16 goi18n "github.com/nicksnyder/go-i18n/i18n" 17 ) 18 19 const ( 20 SEND_QUEUE_SIZE = 256 21 SEND_SLOW_WARN = (SEND_QUEUE_SIZE * 50) / 100 22 SEND_DEADLOCK_WARN = (SEND_QUEUE_SIZE * 95) / 100 23 WRITE_WAIT = 30 * time.Second 24 PONG_WAIT = 100 * time.Second 25 PING_PERIOD = (PONG_WAIT * 6) / 10 26 AUTH_TIMEOUT = 5 * time.Second 27 WEBCONN_MEMBER_CACHE_TIME = 1000 * 60 * 30 // 30 minutes 28 ) 29 30 type WebConn struct { 31 sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically 32 App *App 33 WebSocket *websocket.Conn 34 Send chan model.WebSocketMessage 35 sessionToken atomic.Value 36 session atomic.Value 37 UserId string 38 T goi18n.TranslateFunc 39 Locale string 40 AllChannelMembers map[string]string 41 LastAllChannelMembersTime int64 42 Sequence int64 43 endWritePump chan struct{} 44 pumpFinished chan struct{} 45 } 46 47 func (a *App) NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn { 48 if len(session.UserId) > 0 { 49 a.Go(func() { 50 a.SetStatusOnline(session.UserId, session.Id, false) 51 a.UpdateLastActivityAtIfNeeded(session) 52 }) 53 } 54 55 wc := &WebConn{ 56 App: a, 57 Send: make(chan model.WebSocketMessage, SEND_QUEUE_SIZE), 58 WebSocket: ws, 59 UserId: session.UserId, 60 T: t, 61 Locale: locale, 62 endWritePump: make(chan struct{}, 2), 63 pumpFinished: make(chan struct{}, 1), 64 } 65 66 wc.SetSession(&session) 67 wc.SetSessionToken(session.Token) 68 wc.SetSessionExpiresAt(session.ExpiresAt) 69 70 return wc 71 } 72 73 func (wc *WebConn) Close() { 74 wc.WebSocket.Close() 75 wc.endWritePump <- struct{}{} 76 <-wc.pumpFinished 77 } 78 79 func (c *WebConn) GetSessionExpiresAt() int64 { 80 return atomic.LoadInt64(&c.sessionExpiresAt) 81 } 82 83 func (c *WebConn) SetSessionExpiresAt(v int64) { 84 atomic.StoreInt64(&c.sessionExpiresAt, v) 85 } 86 87 func (c *WebConn) GetSessionToken() string { 88 return c.sessionToken.Load().(string) 89 } 90 91 func (c *WebConn) SetSessionToken(v string) { 92 c.sessionToken.Store(v) 93 } 94 95 func (c *WebConn) GetSession() *model.Session { 96 return c.session.Load().(*model.Session) 97 } 98 99 func (c *WebConn) SetSession(v *model.Session) { 100 if v != nil { 101 v = v.DeepCopy() 102 } 103 104 c.session.Store(v) 105 } 106 107 func (c *WebConn) Pump() { 108 ch := make(chan struct{}, 1) 109 go func() { 110 c.writePump() 111 ch <- struct{}{} 112 }() 113 c.readPump() 114 c.endWritePump <- struct{}{} 115 <-ch 116 c.App.HubUnregister(c) 117 c.pumpFinished <- struct{}{} 118 } 119 120 func (c *WebConn) readPump() { 121 defer func() { 122 c.WebSocket.Close() 123 }() 124 c.WebSocket.SetReadLimit(model.SOCKET_MAX_MESSAGE_SIZE_KB) 125 c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) 126 c.WebSocket.SetPongHandler(func(string) error { 127 c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT)) 128 if c.IsAuthenticated() { 129 c.App.Go(func() { 130 c.App.SetStatusAwayIfNeeded(c.UserId, false) 131 }) 132 } 133 return nil 134 }) 135 136 for { 137 var req model.WebSocketRequest 138 if err := c.WebSocket.ReadJSON(&req); err != nil { 139 // browsers will appear as CloseNoStatusReceived 140 if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { 141 l4g.Debug(fmt.Sprintf("websocket.read: client side closed socket userId=%v", c.UserId)) 142 } else { 143 l4g.Debug(fmt.Sprintf("websocket.read: closing websocket for userId=%v error=%v", c.UserId, err.Error())) 144 } 145 146 return 147 } else { 148 c.App.Srv.WebSocketRouter.ServeWebSocket(c, &req) 149 } 150 } 151 } 152 153 func (c *WebConn) writePump() { 154 ticker := time.NewTicker(PING_PERIOD) 155 authTicker := time.NewTicker(AUTH_TIMEOUT) 156 157 defer func() { 158 ticker.Stop() 159 authTicker.Stop() 160 c.WebSocket.Close() 161 }() 162 163 for { 164 select { 165 case msg, ok := <-c.Send: 166 if !ok { 167 c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) 168 c.WebSocket.WriteMessage(websocket.CloseMessage, []byte{}) 169 return 170 } 171 172 evt, evtOk := msg.(*model.WebSocketEvent) 173 174 skipSend := false 175 if len(c.Send) >= SEND_SLOW_WARN { 176 // When the pump starts to get slow we'll drop non-critical messages 177 if msg.EventType() == model.WEBSOCKET_EVENT_TYPING || 178 msg.EventType() == model.WEBSOCKET_EVENT_STATUS_CHANGE || 179 msg.EventType() == model.WEBSOCKET_EVENT_CHANNEL_VIEWED { 180 l4g.Info(fmt.Sprintf("websocket.slow: dropping message userId=%v type=%v channelId=%v", c.UserId, msg.EventType(), evt.Broadcast.ChannelId)) 181 skipSend = true 182 } 183 } 184 185 if !skipSend { 186 var msgBytes []byte 187 if evtOk { 188 cpyEvt := &model.WebSocketEvent{} 189 *cpyEvt = *evt 190 cpyEvt.Sequence = c.Sequence 191 msgBytes = []byte(cpyEvt.ToJson()) 192 c.Sequence++ 193 } else { 194 msgBytes = []byte(msg.ToJson()) 195 } 196 197 if len(c.Send) >= SEND_DEADLOCK_WARN { 198 if evtOk { 199 l4g.Error(fmt.Sprintf("websocket.full: message userId=%v type=%v channelId=%v size=%v", c.UserId, msg.EventType(), evt.Broadcast.ChannelId, len(msg.ToJson()))) 200 } else { 201 l4g.Error(fmt.Sprintf("websocket.full: message userId=%v type=%v size=%v", c.UserId, msg.EventType(), len(msg.ToJson()))) 202 } 203 } 204 205 c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) 206 if err := c.WebSocket.WriteMessage(websocket.TextMessage, msgBytes); err != nil { 207 // browsers will appear as CloseNoStatusReceived 208 if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { 209 l4g.Debug(fmt.Sprintf("websocket.send: client side closed socket userId=%v", c.UserId)) 210 } else { 211 l4g.Debug(fmt.Sprintf("websocket.send: closing websocket for userId=%v, error=%v", c.UserId, err.Error())) 212 } 213 214 return 215 } 216 217 if c.App.Metrics != nil { 218 c.App.Go(func() { 219 c.App.Metrics.IncrementWebSocketBroadcast(msg.EventType()) 220 }) 221 } 222 223 } 224 case <-ticker.C: 225 c.WebSocket.SetWriteDeadline(time.Now().Add(WRITE_WAIT)) 226 if err := c.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil { 227 // browsers will appear as CloseNoStatusReceived 228 if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { 229 l4g.Debug(fmt.Sprintf("websocket.ticker: client side closed socket userId=%v", c.UserId)) 230 } else { 231 l4g.Debug(fmt.Sprintf("websocket.ticker: closing websocket for userId=%v error=%v", c.UserId, err.Error())) 232 } 233 234 return 235 } 236 case <-c.endWritePump: 237 return 238 case <-authTicker.C: 239 if c.GetSessionToken() == "" { 240 l4g.Debug(fmt.Sprintf("websocket.authTicker: did not authenticate ip=%v", c.WebSocket.RemoteAddr())) 241 return 242 } 243 authTicker.Stop() 244 } 245 } 246 } 247 248 func (webCon *WebConn) InvalidateCache() { 249 webCon.AllChannelMembers = nil 250 webCon.LastAllChannelMembersTime = 0 251 webCon.SetSession(nil) 252 webCon.SetSessionExpiresAt(0) 253 } 254 255 func (webCon *WebConn) IsAuthenticated() bool { 256 // Check the expiry to see if we need to check for a new session 257 if webCon.GetSessionExpiresAt() < model.GetMillis() { 258 if webCon.GetSessionToken() == "" { 259 return false 260 } 261 262 session, err := webCon.App.GetSession(webCon.GetSessionToken()) 263 if err != nil { 264 l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error()) 265 webCon.SetSessionToken("") 266 webCon.SetSession(nil) 267 webCon.SetSessionExpiresAt(0) 268 return false 269 } 270 271 webCon.SetSession(session) 272 webCon.SetSessionExpiresAt(session.ExpiresAt) 273 } 274 275 return true 276 } 277 278 func (webCon *WebConn) SendHello() { 279 msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil) 280 msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, webCon.App.ClientConfigHash(), utils.IsLicensed())) 281 webCon.Send <- msg 282 } 283 284 func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool { 285 // IMPORTANT: Do not send event if WebConn does not have a session 286 if !webCon.IsAuthenticated() { 287 return false 288 } 289 290 // If the event is destined to a specific user 291 if len(msg.Broadcast.UserId) > 0 { 292 if webCon.UserId == msg.Broadcast.UserId { 293 return true 294 } else { 295 return false 296 } 297 } 298 299 // if the user is omitted don't send the message 300 if len(msg.Broadcast.OmitUsers) > 0 { 301 if _, ok := msg.Broadcast.OmitUsers[webCon.UserId]; ok { 302 return false 303 } 304 } 305 306 // Only report events to users who are in the channel for the event 307 if len(msg.Broadcast.ChannelId) > 0 { 308 if model.GetMillis()-webCon.LastAllChannelMembersTime > WEBCONN_MEMBER_CACHE_TIME { 309 webCon.AllChannelMembers = nil 310 webCon.LastAllChannelMembersTime = 0 311 } 312 313 if webCon.AllChannelMembers == nil { 314 if result := <-webCon.App.Srv.Store.Channel().GetAllChannelMembersForUser(webCon.UserId, true); result.Err != nil { 315 l4g.Error("webhub.shouldSendEvent: " + result.Err.Error()) 316 return false 317 } else { 318 webCon.AllChannelMembers = result.Data.(map[string]string) 319 webCon.LastAllChannelMembersTime = model.GetMillis() 320 } 321 } 322 323 if _, ok := webCon.AllChannelMembers[msg.Broadcast.ChannelId]; ok { 324 return true 325 } else { 326 return false 327 } 328 } 329 330 // Only report events to users who are in the team for the event 331 if len(msg.Broadcast.TeamId) > 0 { 332 return webCon.IsMemberOfTeam(msg.Broadcast.TeamId) 333 334 } 335 336 return true 337 } 338 339 func (webCon *WebConn) IsMemberOfTeam(teamId string) bool { 340 341 currentSession := webCon.GetSession() 342 343 if currentSession == nil || len(currentSession.Token) == 0 { 344 session, err := webCon.App.GetSession(webCon.GetSessionToken()) 345 if err != nil { 346 l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error()) 347 return false 348 } else { 349 webCon.SetSession(session) 350 currentSession = session 351 } 352 } 353 354 member := currentSession.GetTeamByTeamId(teamId) 355 356 if member != nil { 357 return true 358 } else { 359 return false 360 } 361 }