From 1fc12dd8ba2238eba7d154eee55e1381e7415372 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Tue, 20 Oct 2015 14:49:42 -0700 Subject: Multi-session login --- api/context.go | 124 ++++++++++++--------- api/post.go | 2 +- api/user.go | 6 +- model/client.go | 25 +++-- .../admin_console/admin_sidebar_header.jsx | 2 +- web/react/components/admin_console/user_item.jsx | 2 +- web/react/components/file_attachment.jsx | 4 +- web/react/components/file_preview.jsx | 2 +- web/react/components/member_list_item.jsx | 2 +- web/react/components/member_list_team_item.jsx | 2 +- web/react/components/mention.jsx | 2 +- web/react/components/more_direct_channels.jsx | 2 +- web/react/components/post.jsx | 2 +- web/react/components/post_list.jsx | 2 +- web/react/components/rhs_comment.jsx | 2 +- web/react/components/rhs_root_post.jsx | 2 +- web/react/components/search_results_item.jsx | 2 +- web/react/components/sidebar_header.jsx | 2 +- web/react/components/user_profile.jsx | 2 +- .../user_settings/user_settings_general.jsx | 2 +- web/react/components/view_image.jsx | 4 +- web/react/stores/socket_store.jsx | 8 +- web/react/utils/client.jsx | 16 +-- web/react/utils/utils.jsx | 10 +- web/templates/head.html | 11 +- web/web.go | 11 +- 26 files changed, 143 insertions(+), 108 deletions(-) diff --git a/api/context.go b/api/context.go index e5ef8b312..28d6951da 100644 --- a/api/context.go +++ b/api/context.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/url" + "strconv" "strings" l4g "code.google.com/p/log4go" @@ -19,23 +20,24 @@ import ( var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + SessionTokenIndex int64 } type Page struct { - TemplateName string - Props map[string]string - ClientCfg map[string]string - User *model.User - Team *model.Team - SessionTokenHash string + TemplateName string + Props map[string]string + ClientCfg map[string]string + User *model.User + Team *model.Team + SessionTokenIndex int64 } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -99,29 +101,37 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - multiToken := cookie.Value - - fmt.Println(">>>>>>>> multiToken: " + multiToken) - - if len(multiToken) > 0 { - tokens := strings.Split(multiToken, " ") - - // If there is only 1 token in the cookie then just use it like normal - if len(tokens) == 1 { - token = multiToken + tokens := GetMultiSessionCookie(r) + if len(tokens) > 0 { + // If there is only 1 token in the cookie then just use it like normal + if len(tokens) == 1 { + token = tokens[0] + } else { + // If it is a multi-session token then find the correct session + sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX) + sessionTokenIndex := int64(-1) + if len(sessionTokenIndexStr) > 0 { + if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { + sessionTokenIndex = index + } } else { - // If it is a multi-session token then find the correct session - sessionTokenHash := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_HASH) - fmt.Println(">>>>>>>> sessionHash: " + sessionTokenHash + " url=" + r.URL.Path) - for _, t := range tokens { - if sessionTokenHash == model.HashPassword(t) { - token = token - break + sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX) + if len(sessionTokenIndexStr) > 0 { + if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { + sessionTokenIndex = index } } } + + if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) { + token = tokens[sessionTokenIndex] + c.SessionTokenIndex = sessionTokenIndex + } else { + c.SessionTokenIndex = -1 + } } + } else { + c.SessionTokenIndex = -1 } } @@ -200,7 +210,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(c.Err.ToJson())) } else { if c.Err.StatusCode == http.StatusUnauthorized { - fmt.Println("!!!!!!!!!!!!!!!! url=" + r.URL.Path) http.Redirect(w, r, c.GetTeamURL()+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) } else { RenderWebError(c.Err, w, r) @@ -332,20 +341,30 @@ func (c *Context) IsTeamAdmin() bool { func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - multiToken := "" - if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - multiToken = oldMultiCookie.Value - } + // multiToken := "" + // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + // multiToken = oldMultiCookie.Value + // } - multiCookie := &http.Cookie{ + // multiCookie := &http.Cookie{ + // Name: model.SESSION_COOKIE_TOKEN, + // Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), + // Path: "/", + // MaxAge: model.SESSION_TIME_WEB_IN_SECS, + // HttpOnly: true, + // } + + //http.SetCookie(w, multiCookie) + + cookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, - Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), + Value: "", Path: "/", - MaxAge: model.SESSION_TIME_WEB_IN_SECS, + MaxAge: -1, HttpOnly: true, } - http.SetCookie(w, multiCookie) + http.SetCookie(w, cookie) } func (c *Context) SetInvalidParam(where string, name string) { @@ -508,24 +527,27 @@ func GetSession(token string) *model.Session { return session } -func FindMultiSessionForTeamId(r *http.Request, teamId string) *model.Session { - +func GetMultiSessionCookie(r *http.Request) []string { if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { multiToken := multiCookie.Value if len(multiToken) > 0 { - tokens := strings.Split(multiToken, " ") + return strings.Split(multiToken, " ") + } + } - for _, token := range tokens { - s := GetSession(token) - if s != nil && !s.IsExpired() && s.TeamId == teamId { - return s - } - } + return []string{} +} + +func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) { + for index, token := range GetMultiSessionCookie(r) { + s := GetSession(token) + if s != nil && !s.IsExpired() && s.TeamId == teamId { + return int64(index), s } } - return nil + return -1, nil } func AddSessionToCache(session *model.Session) { diff --git a/api/post.go b/api/post.go index 73a63cb72..ded71f727 100644 --- a/api/post.go +++ b/api/post.go @@ -281,7 +281,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team // copy the context and create a mock session for posting the message mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false} - newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil { diff --git a/api/user.go b/api/user.go index 1216dd30d..3770baa76 100644 --- a/api/user.go +++ b/api/user.go @@ -434,8 +434,6 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, multiToken = originalMultiSessionCookie.Value } - fmt.Println("original: " + multiToken) - // Attempt to clean all the old tokens or duplicate tokens if len(multiToken) > 0 { tokens := strings.Split(multiToken, " ") @@ -454,9 +452,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, } } - multiToken = strings.TrimSpace(session.Token + " " + multiToken) - - fmt.Println("new: " + multiToken) + multiToken = strings.TrimSpace(multiToken + " " + session.Token) multiSessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, diff --git a/model/client.go b/model/client.go index 79480d667..48a560838 100644 --- a/model/client.go +++ b/model/client.go @@ -16,18 +16,19 @@ import ( ) const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - HEADER_MM_SESSION_TOKEN_HASH = "X-MM-TokenHash" - API_URL_SUFFIX = "/api/v1" + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" + SESSION_TOKEN_INDEX = "session_token_index" + API_URL_SUFFIX = "/api/v1" ) type Result struct { diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx index c80811bcd..e66beaf35 100644 --- a/web/react/components/admin_console/admin_sidebar_header.jsx +++ b/web/react/components/admin_console/admin_sidebar_header.jsx @@ -36,7 +36,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( ); } diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 395e22e6c..f7e92672d 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -215,7 +215,7 @@ export default class UserItem extends React.Component {
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index c6dff6550..307c543a2 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -39,7 +39,7 @@ export default class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { + $('').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -62,7 +62,7 @@ export default class FileAttachment extends React.Component { var re2 = new RegExp('\\(', 'g'); var re3 = new RegExp('\\)', 'g'); var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); } }; }(fileInfo.path, filename)); diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx index a40ed1dcf..df5deb8bc 100644 --- a/web/react/components/file_preview.jsx +++ b/web/react/components/file_preview.jsx @@ -34,7 +34,7 @@ export default class FilePreview extends React.Component { if (filename.indexOf('/api/v1/files/get') !== -1) { filename = filename.split('/api/v1/files/get')[1]; } - filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename; + filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex(); if (type === 'image') { previews.push( diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index 5c3695ad4..8ed94680e 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -105,7 +105,7 @@ export default class MemberListItem extends React.Component {
diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 3af1d3800..14db05cdb 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -169,7 +169,7 @@ export default class MemberListTeamItem extends React.Component {
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx index aeed724a8..09035523a 100644 --- a/web/react/components/mention.jsx +++ b/web/react/components/mention.jsx @@ -25,7 +25,7 @@ export default class Mention extends React.Component { ); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 105199035..21f9a53a0 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -179,7 +179,7 @@ export default class MoreDirectChannels extends React.Component { className='profile-img pull-left' width='38' height='38' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} />
{user.username} diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 3b3b0383c..bc3144dbc 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -158,7 +158,7 @@ export default class Post extends React.Component { var profilePic = null; if (!this.props.hideProfilePic) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 4402745e1..29cd22c44 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -323,7 +323,7 @@ export default class PostList extends React.Component {
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index d3a4cfaeb..cfff04fa2 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -199,7 +199,7 @@ export default class RhsComment extends React.Component {
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 979c56036..deef389e2 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -134,7 +134,7 @@ export default class RhsRootPost extends React.Component { botIndicator =
  • {'BOT'}
  • ; } - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 75d2e7a45..a7d4bb229 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -77,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
    diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 6b29da622..f5d2ed3b4 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -32,7 +32,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( ); } diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 4a759bb21..38d15b7f8 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -67,7 +67,7 @@ export default class UserProfile extends React.Component { dataContent.push( - + ); } else { diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 77951f214..33cdc79fb 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -38,6 +38,10 @@ class SocketStoreClass extends EventEmitter { return; } + if (!global.window.mm_session_token_index) { + return; + } + this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -45,7 +49,9 @@ class SocketStoreClass extends EventEmitter { if (window.location.protocol === 'https:') { protocol = 'wss://'; } - var connUrl = protocol + location.host + '/api/v1/websocket'; + + var connUrl = protocol + location.host + '/api/v1/websocket?' + Utils.getSessionIndex(); + if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index fab0640fb..ee1f9ad27 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -48,14 +48,14 @@ function handleError(methodName, xhr, status, err) { track('api', 'api_weberror', methodName, 'message', msg); - // if (xhr.status === 401) { - // if (window.location.href.indexOf('/channels') === 0) { - // window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); - // } else { - // var teamURL = window.location.href.split('/channels')[0]; - // window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); - // } - // } + if (xhr.status === 401) { + if (window.location.href.indexOf('/channels') === 0) { + window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + } else { + var teamURL = window.location.href.split('/channels')[0]; + window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + } + } return e; } diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 38ac68d58..f17a55142 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -872,7 +872,7 @@ export function getFileUrl(filename) { if (url.indexOf('/api/v1/files/get') !== -1) { url = filename.split('/api/v1/files/get')[1]; } - url = getWindowLocationOrigin() + '/api/v1/files/get' + url; + url = getWindowLocationOrigin() + '/api/v1/files/get' + url + '?' + getSessionIndex(); return url; } @@ -883,6 +883,14 @@ export function getFileName(path) { return split[split.length - 1]; } +export function getSessionIndex() { + if (global.window.mm_session_token_index >= 0) { + return 'session_token_index=' + global.window.mm_session_token_index; + } + + return ''; +} + // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 diff --git a/web/templates/head.html b/web/templates/head.html index 731bcd691..041831ed7 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -43,10 +43,13 @@ window.mm_config = {{ .ClientCfg }}; window.mm_team = {{ .Team }}; window.mm_user = {{ .User }}; - window.mm_session_token_hash = {{ .SessionTokenHash }}; - $.ajaxSetup({ - headers: { 'X-MM-TokenHash': mm_session_token_hash } - }); + + if ({{.SessionTokenIndex}} >= 0) { + window.mm_session_token_index = {{.SessionTokenIndex}}; + $.ajaxSetup({ + headers: { 'X-MM-TokenIndex': mm_session_token_index } + }); + }