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