We invert the skipSend condition and outdent the remaining block to make the code a bit more idiomatic. While here, we also change the dropping message level from info to warn because that's what it should be.
422 строки
12 KiB
Go
422 строки
12 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
|
|
"github.com/gorilla/websocket"
|
|
goi18n "github.com/mattermost/go-i18n/i18n"
|
|
)
|
|
|
|
const (
|
|
sendQueueSize = 256
|
|
sendSlowWarn = (sendQueueSize * 50) / 100
|
|
sendFullWarn = (sendQueueSize * 95) / 100
|
|
writeWaitTime = 30 * time.Second
|
|
pongWaitTime = 100 * time.Second
|
|
pingInterval = (pongWaitTime * 6) / 10
|
|
authCheckInterval = 5 * time.Second
|
|
webConnMemberCacheTime = 1000 * 60 * 30 // 30 minutes
|
|
)
|
|
|
|
// WebConn represents a single websocket connection to a user.
|
|
// It contains all the necesarry state to manage sending/receiving data to/from
|
|
// a websocket.
|
|
type WebConn struct {
|
|
sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically
|
|
App *App
|
|
WebSocket *websocket.Conn
|
|
T goi18n.TranslateFunc
|
|
Locale string
|
|
Sequence int64
|
|
UserId string
|
|
|
|
allChannelMembers map[string]string
|
|
lastAllChannelMembersTime int64
|
|
lastUserActivityAt int64
|
|
send chan model.WebSocketMessage
|
|
sessionToken atomic.Value
|
|
session atomic.Value
|
|
endWritePump chan struct{}
|
|
pumpFinished chan struct{}
|
|
}
|
|
|
|
// NewWebConn returns a new WebConn instance.
|
|
func (a *App) NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn {
|
|
if session.UserId != "" {
|
|
a.Srv().Go(func() {
|
|
a.SetStatusOnline(session.UserId, false)
|
|
a.UpdateLastActivityAtIfNeeded(session)
|
|
})
|
|
}
|
|
|
|
wc := &WebConn{
|
|
App: a,
|
|
send: make(chan model.WebSocketMessage, sendQueueSize),
|
|
WebSocket: ws,
|
|
lastUserActivityAt: model.GetMillis(),
|
|
UserId: session.UserId,
|
|
T: t,
|
|
Locale: locale,
|
|
endWritePump: make(chan struct{}),
|
|
pumpFinished: make(chan struct{}),
|
|
}
|
|
|
|
wc.SetSession(&session)
|
|
wc.SetSessionToken(session.Token)
|
|
wc.SetSessionExpiresAt(session.ExpiresAt)
|
|
|
|
return wc
|
|
}
|
|
|
|
// Close closes the WebConn.
|
|
func (wc *WebConn) Close() {
|
|
wc.WebSocket.Close()
|
|
<-wc.pumpFinished
|
|
}
|
|
|
|
// GetSessionExpiresAt returns the time at which the session expires.
|
|
func (wc *WebConn) GetSessionExpiresAt() int64 {
|
|
return atomic.LoadInt64(&wc.sessionExpiresAt)
|
|
}
|
|
|
|
// SetSessionExpiresAt sets the time at which the session expires.
|
|
func (wc *WebConn) SetSessionExpiresAt(v int64) {
|
|
atomic.StoreInt64(&wc.sessionExpiresAt, v)
|
|
}
|
|
|
|
// GetSessionToken returns the session token of the connection.
|
|
func (wc *WebConn) GetSessionToken() string {
|
|
return wc.sessionToken.Load().(string)
|
|
}
|
|
|
|
// SetSessionToken sets the session token of the connection.
|
|
func (wc *WebConn) SetSessionToken(v string) {
|
|
wc.sessionToken.Store(v)
|
|
}
|
|
|
|
// GetSession returns the session of the connection.
|
|
func (wc *WebConn) GetSession() *model.Session {
|
|
return wc.session.Load().(*model.Session)
|
|
}
|
|
|
|
// SetSession sets the session of the connection.
|
|
func (wc *WebConn) SetSession(v *model.Session) {
|
|
if v != nil {
|
|
v = v.DeepCopy()
|
|
}
|
|
|
|
wc.session.Store(v)
|
|
}
|
|
|
|
// Pump starts the WebConn instance. After this, the websocket
|
|
// is ready to send/receive messages.
|
|
func (wc *WebConn) Pump() {
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
wc.writePump()
|
|
}()
|
|
wc.readPump()
|
|
close(wc.endWritePump)
|
|
wg.Wait()
|
|
wc.App.HubUnregister(wc)
|
|
close(wc.pumpFinished)
|
|
}
|
|
|
|
func (wc *WebConn) readPump() {
|
|
defer func() {
|
|
wc.WebSocket.Close()
|
|
}()
|
|
wc.WebSocket.SetReadLimit(model.SOCKET_MAX_MESSAGE_SIZE_KB)
|
|
wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime))
|
|
wc.WebSocket.SetPongHandler(func(string) error {
|
|
wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime))
|
|
if wc.IsAuthenticated() {
|
|
wc.App.Srv().Go(func() {
|
|
wc.App.SetStatusAwayIfNeeded(wc.UserId, false)
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
|
|
for {
|
|
var req model.WebSocketRequest
|
|
if err := wc.WebSocket.ReadJSON(&req); err != nil {
|
|
wc.logSocketErr("websocket.read", err)
|
|
return
|
|
}
|
|
wc.App.Srv().WebSocketRouter.ServeWebSocket(wc, &req)
|
|
}
|
|
}
|
|
|
|
func (wc *WebConn) writePump() {
|
|
ticker := time.NewTicker(pingInterval)
|
|
authTicker := time.NewTicker(authCheckInterval)
|
|
|
|
defer func() {
|
|
ticker.Stop()
|
|
authTicker.Stop()
|
|
wc.WebSocket.Close()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case msg, ok := <-wc.send:
|
|
if !ok {
|
|
wc.WebSocket.SetWriteDeadline(time.Now().Add(writeWaitTime))
|
|
wc.WebSocket.WriteMessage(websocket.CloseMessage, []byte{})
|
|
return
|
|
}
|
|
|
|
evt, evtOk := msg.(*model.WebSocketEvent)
|
|
|
|
skipSend := false
|
|
if len(wc.send) >= sendSlowWarn {
|
|
// When the pump starts to get slow we'll drop non-critical messages
|
|
switch msg.EventType() {
|
|
case model.WEBSOCKET_EVENT_TYPING,
|
|
model.WEBSOCKET_EVENT_STATUS_CHANGE,
|
|
model.WEBSOCKET_EVENT_CHANNEL_VIEWED:
|
|
mlog.Warn(
|
|
"websocket.slow: dropping message",
|
|
mlog.String("user_id", wc.UserId),
|
|
mlog.String("type", msg.EventType()),
|
|
mlog.String("channel_id", evt.GetBroadcast().ChannelId),
|
|
)
|
|
skipSend = true
|
|
}
|
|
}
|
|
|
|
if skipSend {
|
|
continue
|
|
}
|
|
|
|
var msgBytes []byte
|
|
if evtOk {
|
|
cpyEvt := evt.SetSequence(wc.Sequence)
|
|
msgBytes = []byte(cpyEvt.ToJson())
|
|
wc.Sequence++
|
|
} else {
|
|
msgBytes = []byte(msg.ToJson())
|
|
}
|
|
|
|
if len(wc.send) >= sendFullWarn {
|
|
logData := []mlog.Field{
|
|
mlog.String("user_id", wc.UserId),
|
|
mlog.String("type", msg.EventType()),
|
|
mlog.Int("size", len(msgBytes)),
|
|
}
|
|
if evtOk {
|
|
logData = append(logData, mlog.String("channel_id", evt.GetBroadcast().ChannelId))
|
|
}
|
|
|
|
mlog.Warn("websocket.full", logData...)
|
|
}
|
|
|
|
wc.WebSocket.SetWriteDeadline(time.Now().Add(writeWaitTime))
|
|
if err := wc.WebSocket.WriteMessage(websocket.TextMessage, msgBytes); err != nil {
|
|
wc.logSocketErr("websocket.send", err)
|
|
return
|
|
}
|
|
|
|
if wc.App.Metrics() != nil {
|
|
wc.App.Metrics().IncrementWebSocketBroadcast(msg.EventType())
|
|
}
|
|
case <-ticker.C:
|
|
wc.WebSocket.SetWriteDeadline(time.Now().Add(writeWaitTime))
|
|
if err := wc.WebSocket.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
|
wc.logSocketErr("websocket.ticker", err)
|
|
return
|
|
}
|
|
|
|
case <-wc.endWritePump:
|
|
return
|
|
|
|
case <-authTicker.C:
|
|
if wc.GetSessionToken() == "" {
|
|
mlog.Debug("websocket.authTicker: did not authenticate", mlog.Any("ip_address", wc.WebSocket.RemoteAddr()))
|
|
return
|
|
}
|
|
authTicker.Stop()
|
|
}
|
|
}
|
|
}
|
|
|
|
// InvalidateCache resets all internal data of the WebConn.
|
|
func (wc *WebConn) InvalidateCache() {
|
|
wc.allChannelMembers = nil
|
|
wc.lastAllChannelMembersTime = 0
|
|
wc.SetSession(nil)
|
|
wc.SetSessionExpiresAt(0)
|
|
}
|
|
|
|
// IsAuthenticated returns whether the given WebConn is authenticated or not.
|
|
func (wc *WebConn) IsAuthenticated() bool {
|
|
// Check the expiry to see if we need to check for a new session
|
|
if wc.GetSessionExpiresAt() < model.GetMillis() {
|
|
if wc.GetSessionToken() == "" {
|
|
return false
|
|
}
|
|
|
|
session, err := wc.App.GetSession(wc.GetSessionToken())
|
|
if err != nil {
|
|
mlog.Error("Invalid session.", mlog.Err(err))
|
|
wc.SetSessionToken("")
|
|
wc.SetSession(nil)
|
|
wc.SetSessionExpiresAt(0)
|
|
return false
|
|
}
|
|
|
|
wc.SetSession(session)
|
|
wc.SetSessionExpiresAt(session.ExpiresAt)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (wc *WebConn) createHelloMessage() *model.WebSocketEvent {
|
|
msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", wc.UserId, nil)
|
|
msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, wc.App.ClientConfigHash(), wc.App.License() != nil))
|
|
return msg
|
|
}
|
|
|
|
func (wc *WebConn) shouldSendEventToGuest(msg *model.WebSocketEvent) bool {
|
|
var userId string
|
|
var canSee bool
|
|
|
|
switch msg.EventType() {
|
|
case model.WEBSOCKET_EVENT_USER_UPDATED:
|
|
user, ok := msg.GetData()["user"].(*model.User)
|
|
if !ok {
|
|
mlog.Error("webhub.shouldSendEvent: user not found in message", mlog.Any("user", msg.GetData()["user"]))
|
|
return false
|
|
}
|
|
userId = user.Id
|
|
case model.WEBSOCKET_EVENT_NEW_USER:
|
|
userId = msg.GetData()["user_id"].(string)
|
|
default:
|
|
return true
|
|
}
|
|
|
|
canSee, err := wc.App.UserCanSeeOtherUser(wc.UserId, userId)
|
|
if err != nil {
|
|
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
|
|
return false
|
|
}
|
|
|
|
return canSee
|
|
}
|
|
|
|
// shouldSendEvent returns whether the message should be sent or not.
|
|
func (wc *WebConn) shouldSendEvent(msg *model.WebSocketEvent) bool {
|
|
// IMPORTANT: Do not send event if WebConn does not have a session
|
|
if !wc.IsAuthenticated() {
|
|
return false
|
|
}
|
|
|
|
// If the event contains sanitized data, only send to users that don't have permission to
|
|
// see sensitive data. Prevents admin clients from receiving events with bad data
|
|
var hasReadPrivateDataPermission *bool
|
|
if msg.GetBroadcast().ContainsSanitizedData {
|
|
hasReadPrivateDataPermission = model.NewBool(wc.App.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PERMISSION_MANAGE_SYSTEM.Id))
|
|
|
|
if *hasReadPrivateDataPermission {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If the event contains sensitive data, only send to users with permission to see it
|
|
if msg.GetBroadcast().ContainsSensitiveData {
|
|
if hasReadPrivateDataPermission == nil {
|
|
hasReadPrivateDataPermission = model.NewBool(wc.App.RolesGrantPermission(wc.GetSession().GetUserRoles(), model.PERMISSION_MANAGE_SYSTEM.Id))
|
|
}
|
|
|
|
if !*hasReadPrivateDataPermission {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If the event is destined to a specific user
|
|
if msg.GetBroadcast().UserId != "" {
|
|
return wc.UserId == msg.GetBroadcast().UserId
|
|
}
|
|
|
|
// if the user is omitted don't send the message
|
|
if len(msg.GetBroadcast().OmitUsers) > 0 {
|
|
if _, ok := msg.GetBroadcast().OmitUsers[wc.UserId]; ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Only report events to users who are in the channel for the event
|
|
if msg.GetBroadcast().ChannelId != "" {
|
|
if model.GetMillis()-wc.lastAllChannelMembersTime > webConnMemberCacheTime {
|
|
wc.allChannelMembers = nil
|
|
wc.lastAllChannelMembersTime = 0
|
|
}
|
|
|
|
if wc.allChannelMembers == nil {
|
|
result, err := wc.App.Srv().Store.Channel().GetAllChannelMembersForUser(wc.UserId, true, false)
|
|
if err != nil {
|
|
mlog.Error("webhub.shouldSendEvent.", mlog.Err(err))
|
|
return false
|
|
}
|
|
wc.allChannelMembers = result
|
|
wc.lastAllChannelMembersTime = model.GetMillis()
|
|
}
|
|
|
|
if _, ok := wc.allChannelMembers[msg.GetBroadcast().ChannelId]; ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Only report events to users who are in the team for the event
|
|
if msg.GetBroadcast().TeamId != "" {
|
|
return wc.isMemberOfTeam(msg.GetBroadcast().TeamId)
|
|
}
|
|
|
|
if wc.GetSession().Props[model.SESSION_PROP_IS_GUEST] == "true" {
|
|
return wc.shouldSendEventToGuest(msg)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// IsMemberOfTeam returns whether the user of the WebConn
|
|
// is a member of the given teamId or not.
|
|
func (wc *WebConn) isMemberOfTeam(teamId string) bool {
|
|
currentSession := wc.GetSession()
|
|
|
|
if currentSession == nil || currentSession.Token == "" {
|
|
session, err := wc.App.GetSession(wc.GetSessionToken())
|
|
if err != nil {
|
|
mlog.Error("Invalid session.", mlog.Err(err))
|
|
return false
|
|
}
|
|
wc.SetSession(session)
|
|
currentSession = session
|
|
}
|
|
|
|
return currentSession.GetTeamByTeamId(teamId) != nil
|
|
}
|
|
|
|
func (wc *WebConn) logSocketErr(source string, err error) {
|
|
// browsers will appear as CloseNoStatusReceived
|
|
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
|
|
mlog.Debug(source+": client side closed socket", mlog.String("user_id", wc.UserId))
|
|
} else {
|
|
mlog.Debug(source+": closing websocket", mlog.String("user_id", wc.UserId), mlog.Err(err))
|
|
}
|
|
}
|