From daca0d93f621bcb1daae149c178af0631bcd120a Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Tue, 28 Mar 2017 04:58:19 -0400 Subject: Move WebSocket API to it's own package and add websocket v4 endpoint (#5881) --- api/api.go | 1 - api/apitestlib.go | 5 ++++ api/general.go | 13 --------- api/status.go | 22 -------------- api/user.go | 25 ---------------- api/webrtc.go | 16 ---------- api/websocket.go | 16 +--------- api/websocket_handler.go | 61 -------------------------------------- api4/api.go | 2 +- api4/apitestlib.go | 9 ++++++ api4/websocket.go | 46 +++++++++++++++++++++++++++++ api4/websocket_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/platform/server.go | 3 ++ cmd/platform/test.go | 5 ++++ i18n/en.json | 16 ++++++++++ model/websocket_client.go | 31 ++++++++++++++++++-- utils/api.go | 24 +++++++++++++++ wsapi/api.go | 21 +++++++++++++ wsapi/status.go | 38 ++++++++++++++++++++++++ wsapi/system.go | 27 +++++++++++++++++ wsapi/user.go | 40 +++++++++++++++++++++++++ wsapi/webrtc.go | 31 ++++++++++++++++++++ wsapi/websocket_handler.go | 61 ++++++++++++++++++++++++++++++++++++++ 23 files changed, 430 insertions(+), 156 deletions(-) delete mode 100644 api/websocket_handler.go create mode 100644 api4/websocket.go create mode 100644 api4/websocket_test.go create mode 100644 utils/api.go create mode 100644 wsapi/api.go create mode 100644 wsapi/status.go create mode 100644 wsapi/system.go create mode 100644 wsapi/user.go create mode 100644 wsapi/webrtc.go create mode 100644 wsapi/websocket_handler.go diff --git a/api/api.go b/api/api.go index 8f7e6c37e..8ec078dd2 100644 --- a/api/api.go +++ b/api/api.go @@ -61,7 +61,6 @@ var BaseRoutes *Routes func InitRouter() { app.Srv.Router = mux.NewRouter() app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) - app.Srv.WebSocketRouter = app.NewWebSocketRouter() } func InitApi() { diff --git a/api/apitestlib.go b/api/apitestlib.go index 89a65518a..f1d06ec0f 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/wsapi" l4g "github.com/alecthomas/log4go" ) @@ -42,10 +43,12 @@ func SetupEnterprise() *TestHelper { app.NewServer() app.InitStores() InitRouter() + wsapi.InitRouter() app.StartServer() utils.InitHTML() api4.InitApi(false) InitApi() + wsapi.InitApi() utils.EnableDebugLogForTest() app.Srv.Store.MarkSystemRanUnitTests() @@ -70,8 +73,10 @@ func Setup() *TestHelper { app.NewServer() app.InitStores() InitRouter() + wsapi.InitRouter() app.StartServer() InitApi() + wsapi.InitApi() utils.EnableDebugLogForTest() app.Srv.Store.MarkSystemRanUnitTests() diff --git a/api/general.go b/api/general.go index 5c8e45082..e273268a4 100644 --- a/api/general.go +++ b/api/general.go @@ -10,7 +10,6 @@ import ( l4g "github.com/alecthomas/log4go" - "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -21,8 +20,6 @@ func InitGeneral() { BaseRoutes.General.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") BaseRoutes.General.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") BaseRoutes.General.Handle("/ping", ApiAppHandler(ping)).Methods("GET") - - app.Srv.WebSocketRouter.Handle("ping", ApiWebSocketHandler(webSocketPing)) } func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { @@ -72,13 +69,3 @@ func ping(c *Context, w http.ResponseWriter, r *http.Request) { m["server_time"] = fmt.Sprintf("%v", model.GetMillis()) w.Write([]byte(model.MapToJson(m))) } - -func webSocketPing(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - data := map[string]interface{}{} - data["text"] = "pong" - data["version"] = model.CurrentVersion - data["server_time"] = model.GetMillis() - data["node_id"] = "" - - return data, nil -} diff --git a/api/status.go b/api/status.go index 69f391f47..df4be4603 100644 --- a/api/status.go +++ b/api/status.go @@ -18,8 +18,6 @@ func InitStatus() { BaseRoutes.Users.Handle("/status", ApiUserRequired(getStatusesHttp)).Methods("GET") BaseRoutes.Users.Handle("/status/ids", ApiUserRequired(getStatusesByIdsHttp)).Methods("POST") - app.Srv.WebSocketRouter.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket)) - app.Srv.WebSocketRouter.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket)) } func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -27,11 +25,6 @@ func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.StringInterfaceToJson(statusMap))) } -func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - statusMap := app.GetAllStatuses() - return model.StatusMapToInterfaceMap(statusMap), nil -} - func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) { userIds := model.ArrayFromJson(r.Body) @@ -48,18 +41,3 @@ func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.StringInterfaceToJson(statusMap))) } - -func getStatusesByIdsWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - var userIds []string - if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 { - l4g.Error(model.StringInterfaceToJson(req.Data)) - return nil, NewInvalidWebSocketParamError(req.Action, "user_ids") - } - - statusMap, err := app.GetStatusesByIds(userIds) - if err != nil { - return nil, err - } - - return statusMap, nil -} diff --git a/api/user.go b/api/user.go index 1a9380368..f5bed17ca 100644 --- a/api/user.go +++ b/api/user.go @@ -71,8 +71,6 @@ func InitUser() { BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(loginWithSaml)).Methods("GET") BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(completeSaml)).Methods("POST") - - app.Srv.WebSocketRouter.Handle("user_typing", ApiWebSocketHandler(userTyping)) } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1442,29 +1440,6 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { } } -func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - var ok bool - var channelId string - if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 { - return nil, NewInvalidWebSocketParamError(req.Action, "channel_id") - } - - var parentId string - if parentId, ok = req.Data["parent_id"].(string); !ok { - parentId = "" - } - - omitUsers := make(map[string]bool, 1) - omitUsers[req.Session.UserId] = true - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers) - event.Add("parent_id", parentId) - event.Add("user_id", req.Session.UserId) - go app.Publish(event) - - return nil, nil -} - func sanitizeProfile(c *Context, user *model.User) *model.User { options := utils.Cfg.GetSanitizeOptions() diff --git a/api/webrtc.go b/api/webrtc.go index 8b00e724d..7ba9d3762 100644 --- a/api/webrtc.go +++ b/api/webrtc.go @@ -23,8 +23,6 @@ func InitWebrtc() { l4g.Debug(utils.T("api.webrtc.init.debug")) BaseRoutes.Webrtc.Handle("/token", ApiUserRequired(webrtcToken)).Methods("POST") - - app.Srv.WebSocketRouter.Handle("webrtc", ApiWebSocketHandler(webrtcMessage)) } func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { @@ -52,20 +50,6 @@ func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { } } -func webrtcMessage(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { - var ok bool - var toUserId string - if toUserId, ok = req.Data["to_user_id"].(string); !ok || len(toUserId) != 26 { - return nil, NewInvalidWebSocketParamError(req.Action, "to_user_id") - } - - event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil) - event.Data = req.Data - go app.Publish(event) - - return nil, nil -} - func getWebrtcToken(sessionId string) (string, *model.AppError) { if !*utils.Cfg.WebrtcSettings.Enable { return "", model.NewLocAppError("WebRTC.getWebrtcToken", "api.webrtc.disabled.app_error", nil, "") diff --git a/api/websocket.go b/api/websocket.go index 2de9abb0a..192513bc0 100644 --- a/api/websocket.go +++ b/api/websocket.go @@ -5,7 +5,6 @@ package api import ( "net/http" - "strings" l4g "github.com/alecthomas/log4go" "github.com/gorilla/websocket" @@ -17,23 +16,10 @@ import ( func InitWebSocket() { l4g.Debug(utils.T("api.web_socket.init.debug")) BaseRoutes.Users.Handle("/websocket", ApiAppHandlerTrustRequester(connect)).Methods("GET") - app.HubStart() -} - -type OriginCheckerProc func(*http.Request) bool - -func OriginChecker(r *http.Request) bool { - origin := r.Header.Get("Origin") - return *utils.Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(origin, *utils.Cfg.ServiceSettings.AllowCorsFrom) } func connect(c *Context, w http.ResponseWriter, r *http.Request) { - - var originChecker OriginCheckerProc = nil - - if len(*utils.Cfg.ServiceSettings.AllowCorsFrom) > 0 { - originChecker = OriginChecker - } + originChecker := utils.GetOriginChecker(r) upgrader := websocket.Upgrader{ ReadBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB, diff --git a/api/websocket_handler.go b/api/websocket_handler.go deleted file mode 100644 index 25cdf6458..000000000 --- a/api/websocket_handler.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package api - -import ( - l4g "github.com/alecthomas/log4go" - - "github.com/mattermost/platform/app" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" -) - -func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) webSocketHandler { - return webSocketHandler{wh} -} - -type webSocketHandler struct { - handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError) -} - -func (wh webSocketHandler) ServeWebSocket(conn *app.WebConn, r *model.WebSocketRequest) { - l4g.Debug("/api/v3/users/websocket:%s", r.Action) - - session, sessionErr := app.GetSession(conn.SessionToken) - if sessionErr != nil { - l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, conn.UserId, sessionErr.SystemMessage(utils.T), sessionErr.Error()) - sessionErr.DetailedError = "" - errResp := model.NewWebSocketError(r.Seq, sessionErr) - errResp.DoPreComputeJson() - - conn.Send <- errResp - return - } - - r.Session = *session - r.T = conn.T - r.Locale = conn.Locale - - var data map[string]interface{} - var err *model.AppError - - if data, err = wh.handlerFunc(r); err != nil { - l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError) - err.DetailedError = "" - errResp := model.NewWebSocketError(r.Seq, err) - errResp.DoPreComputeJson() - - conn.Send <- errResp - return - } - - resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data) - resp.DoPreComputeJson() - - conn.Send <- resp -} - -func NewInvalidWebSocketParamError(action string, name string) *model.AppError { - return model.NewLocAppError("/api/v3/users/websocket:"+action, "api.websocket_handler.invalid_param.app_error", map[string]interface{}{"Name": name}, "") -} diff --git a/api4/api.go b/api4/api.go index c967537ee..dffed60e4 100644 --- a/api4/api.go +++ b/api4/api.go @@ -92,7 +92,6 @@ var BaseRoutes *Routes func InitRouter() { app.Srv.Router = mux.NewRouter() app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404) - app.Srv.WebSocketRouter = app.NewWebSocketRouter() } func InitApi(full bool) { @@ -174,6 +173,7 @@ func InitApi(full bool) { InitBrand() InitCommand() InitStatus() + InitWebSocket() app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index c6c1dfa94..87a3976f5 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -20,6 +20,7 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/wsapi" s3 "github.com/minio/minio-go" ) @@ -55,9 +56,11 @@ func SetupEnterprise() *TestHelper { app.NewServer() app.InitStores() InitRouter() + wsapi.InitRouter() app.StartServer() utils.InitHTML() InitApi(true) + wsapi.InitApi() utils.EnableDebugLogForTest() app.Srv.Store.MarkSystemRanUnitTests() @@ -85,8 +88,10 @@ func Setup() *TestHelper { app.NewServer() app.InitStores() InitRouter() + wsapi.InitRouter() app.StartServer() InitApi(true) + wsapi.InitApi() utils.EnableDebugLogForTest() app.Srv.Store.MarkSystemRanUnitTests() @@ -167,6 +172,10 @@ func (me *TestHelper) CreateClient() *model.Client4 { return model.NewAPIv4Client("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress) } +func (me *TestHelper) CreateWebSocketClient() (*model.WebSocketClient, *model.AppError) { + return model.NewWebSocketClient4("ws://localhost"+utils.Cfg.ServiceSettings.ListenAddress, me.Client.AuthToken) +} + func (me *TestHelper) CreateUser() *model.User { return me.CreateUserWithClient(me.Client) } diff --git a/api4/websocket.go b/api4/websocket.go new file mode 100644 index 000000000..c70327222 --- /dev/null +++ b/api4/websocket.go @@ -0,0 +1,46 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "net/http" + + l4g "github.com/alecthomas/log4go" + "github.com/gorilla/websocket" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitWebSocket() { + l4g.Debug(utils.T("api.web_socket.init.debug")) + + BaseRoutes.ApiRoot.Handle("/websocket", ApiHandlerTrustRequester(connectWebSocket)).Methods("GET") +} + +func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) { + originChecker := utils.GetOriginChecker(r) + + upgrader := websocket.Upgrader{ + ReadBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB, + WriteBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB, + CheckOrigin: originChecker, + } + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + l4g.Error(utils.T("api.web_socket.connect.error"), err) + c.Err = model.NewLocAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, "") + return + } + + wc := app.NewWebConn(ws, c.Session, c.T, "") + + if len(c.Session.UserId) > 0 { + app.HubRegister(wc) + } + + go wc.WritePump() + wc.ReadPump() +} diff --git a/api4/websocket_test.go b/api4/websocket_test.go new file mode 100644 index 000000000..6018bf7da --- /dev/null +++ b/api4/websocket_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "testing" + "time" + + "github.com/mattermost/platform/model" +) + +func TestWebSocket(t *testing.T) { + th := Setup().InitBasic() + defer TearDown() + WebSocketClient, err := th.CreateWebSocketClient() + if err != nil { + t.Fatal(err) + } + defer WebSocketClient.Close() + + time.Sleep(300 * time.Millisecond) + + // Test closing and reconnecting + WebSocketClient.Close() + if err := WebSocketClient.Connect(); err != nil { + t.Fatal(err) + } + + WebSocketClient.Listen() + + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Status != model.STATUS_OK { + t.Fatal("should have responded OK to authentication challenge") + } + + WebSocketClient.SendMessage("ping", nil) + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Data["text"].(string) != "pong" { + t.Fatal("wrong response") + } + + WebSocketClient.SendMessage("", nil) + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Error.Id != "api.web_socket_router.no_action.app_error" { + t.Fatal("should have been no action response") + } + + WebSocketClient.SendMessage("junk", nil) + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Error.Id != "api.web_socket_router.bad_action.app_error" { + t.Fatal("should have been bad action response") + } + + req := &model.WebSocketRequest{} + req.Seq = 0 + req.Action = "ping" + WebSocketClient.Conn.WriteJSON(req) + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Error.Id != "api.web_socket_router.bad_seq.app_error" { + t.Fatal("should have been bad action response") + } + + WebSocketClient.UserTyping("", "") + time.Sleep(300 * time.Millisecond) + if resp := <-WebSocketClient.ResponseChannel; resp.Error.Id != "api.websocket_handler.invalid_param.app_error" { + t.Fatal("should have been invalid param response") + } else { + if resp.Error.DetailedError != "" { + t.Fatal("detailed error not cleared") + } + } +} diff --git a/cmd/platform/server.go b/cmd/platform/server.go index be14f7f1f..317129354 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -18,6 +18,7 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/mattermost/platform/web" + "github.com/mattermost/platform/wsapi" "github.com/spf13/cobra" ) @@ -62,8 +63,10 @@ func runServer(configFileLocation string) { app.NewServer() app.InitStores() api.InitRouter() + wsapi.InitRouter() api4.InitApi(false) api.InitApi() + wsapi.InitApi() web.InitWeb() if model.BuildEnterpriseReady == "true" { diff --git a/cmd/platform/test.go b/cmd/platform/test.go index cf67d6702..cd548568f 100644 --- a/cmd/platform/test.go +++ b/cmd/platform/test.go @@ -13,6 +13,7 @@ import ( "github.com/mattermost/platform/api4" "github.com/mattermost/platform/app" "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/wsapi" "github.com/spf13/cobra" "os/signal" "syscall" @@ -47,8 +48,10 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error { initDBCommandContextCobra(cmd) utils.InitTranslations(utils.Cfg.LocalizationSettings) api.InitRouter() + wsapi.InitRouter() api4.InitApi(false) api.InitApi() + wsapi.InitApi() setupClientTests() app.StartServer() runWebClientTests() @@ -61,8 +64,10 @@ func serverForWebClientTestsCmdF(cmd *cobra.Command, args []string) error { initDBCommandContextCobra(cmd) utils.InitTranslations(utils.Cfg.LocalizationSettings) api.InitRouter() + wsapi.InitRouter() api4.InitApi(false) api.InitApi() + wsapi.InitApi() setupClientTests() app.StartServer() diff --git a/i18n/en.json b/i18n/en.json index 0589d22e1..36ac0835f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -151,6 +151,22 @@ "id": "api.api.render.error", "translation": "Error rendering template %v err=%v" }, + { + "id": "wsapi.user.init.debug", + "translation": "Initializing user WebSocket API routes" + }, + { + "id": "wsapi.system.init.debug", + "translation": "Initializing system WebSocket API routes" + }, + { + "id": "wsapi.status.init.debug", + "translation": "Initializing status WebSocket API routes" + }, + { + "id": "wsapi.webrtc.init.debug", + "translation": "Initializing webrtc WebSocket API routes" + }, { "id": "api.auth.unable_to_get_user.app_error", "translation": "Unable to get user to check permissions." diff --git a/model/websocket_client.go b/model/websocket_client.go index 083fe110a..2da83be56 100644 --- a/model/websocket_client.go +++ b/model/websocket_client.go @@ -15,6 +15,7 @@ const ( type WebSocketClient struct { Url string // The location of the server like "ws://localhost:8065" ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3" + ConnectUrl string // The websocket URL to connect to like "ws://localhost:8065/api/v3/path/to/websocket" Conn *websocket.Conn // The WebSocket connection AuthToken string // The token used to open the WebSocket Sequence int64 // The ever-incrementing sequence attached to each WebSocket action @@ -34,6 +35,32 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { client := &WebSocketClient{ url, url + API_URL_SUFFIX_V3, + url + API_URL_SUFFIX_V3 + "/users/websocket", + conn, + authToken, + 1, + make(chan *WebSocketEvent, 100), + make(chan *WebSocketResponse, 100), + nil, + } + + client.SendMessage(WEBSOCKET_AUTHENTICATION_CHALLENGE, map[string]interface{}{"token": authToken}) + + return client, nil +} + +// NewWebSocketClient4 constructs a new WebSocket client with convienence +// methods for talking to the server. Uses the v4 endpoint. +func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { + conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) + if err != nil { + return nil, NewLocAppError("NewWebSocketClient4", "model.websocket_client.connect_fail.app_error", nil, err.Error()) + } + + client := &WebSocketClient{ + url, + url + API_URL_SUFFIX, + url + API_URL_SUFFIX + "/websocket", conn, authToken, 1, @@ -49,9 +76,9 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { func (wsc *WebSocketClient) Connect() *AppError { var err error - wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ApiUrl+"/users/websocket", nil) + wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ConnectUrl, nil) if err != nil { - return NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error()) + return NewLocAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error()) } wsc.EventChannel = make(chan *WebSocketEvent, 100) diff --git a/utils/api.go b/utils/api.go new file mode 100644 index 000000000..388271bd2 --- /dev/null +++ b/utils/api.go @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "net/http" + "strings" +) + +type OriginCheckerProc func(*http.Request) bool + +func OriginChecker(r *http.Request) bool { + origin := r.Header.Get("Origin") + return *Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(origin, *Cfg.ServiceSettings.AllowCorsFrom) +} + +func GetOriginChecker(r *http.Request) OriginCheckerProc { + if len(*Cfg.ServiceSettings.AllowCorsFrom) > 0 { + return OriginChecker + } + + return nil +} diff --git a/wsapi/api.go b/wsapi/api.go new file mode 100644 index 000000000..2d4c99674 --- /dev/null +++ b/wsapi/api.go @@ -0,0 +1,21 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + "github.com/mattermost/platform/app" +) + +func InitRouter() { + app.Srv.WebSocketRouter = app.NewWebSocketRouter() +} + +func InitApi() { + InitUser() + InitSystem() + InitStatus() + InitWebrtc() + + app.HubStart() +} diff --git a/wsapi/status.go b/wsapi/status.go new file mode 100644 index 000000000..a9ff8831d --- /dev/null +++ b/wsapi/status.go @@ -0,0 +1,38 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitStatus() { + l4g.Debug(utils.T("wsapi.status.init.debug")) + + app.Srv.WebSocketRouter.Handle("get_statuses", ApiWebSocketHandler(getStatuses)) + app.Srv.WebSocketRouter.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIds)) +} + +func getStatuses(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + statusMap := app.GetAllStatuses() + return model.StatusMapToInterfaceMap(statusMap), nil +} + +func getStatusesByIds(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + var userIds []string + if userIds = model.ArrayFromInterface(req.Data["user_ids"]); len(userIds) == 0 { + l4g.Error(model.StringInterfaceToJson(req.Data)) + return nil, NewInvalidWebSocketParamError(req.Action, "user_ids") + } + + statusMap, err := app.GetStatusesByIds(userIds) + if err != nil { + return nil, err + } + + return statusMap, nil +} diff --git a/wsapi/system.go b/wsapi/system.go new file mode 100644 index 000000000..644d0196f --- /dev/null +++ b/wsapi/system.go @@ -0,0 +1,27 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitSystem() { + l4g.Debug(utils.T("wsapi.system.init.debug")) + + app.Srv.WebSocketRouter.Handle("ping", ApiWebSocketHandler(ping)) +} + +func ping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + data := map[string]interface{}{} + data["text"] = "pong" + data["version"] = model.CurrentVersion + data["server_time"] = model.GetMillis() + data["node_id"] = "" + + return data, nil +} diff --git a/wsapi/user.go b/wsapi/user.go new file mode 100644 index 000000000..a89bf1118 --- /dev/null +++ b/wsapi/user.go @@ -0,0 +1,40 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitUser() { + l4g.Debug(utils.T("wsapi.user.init.debug")) + + app.Srv.WebSocketRouter.Handle("user_typing", ApiWebSocketHandler(userTyping)) +} + +func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + var ok bool + var channelId string + if channelId, ok = req.Data["channel_id"].(string); !ok || len(channelId) != 26 { + return nil, NewInvalidWebSocketParamError(req.Action, "channel_id") + } + + var parentId string + if parentId, ok = req.Data["parent_id"].(string); !ok { + parentId = "" + } + + omitUsers := make(map[string]bool, 1) + omitUsers[req.Session.UserId] = true + + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers) + event.Add("parent_id", parentId) + event.Add("user_id", req.Session.UserId) + go app.Publish(event) + + return nil, nil +} diff --git a/wsapi/webrtc.go b/wsapi/webrtc.go new file mode 100644 index 000000000..fd8eede30 --- /dev/null +++ b/wsapi/webrtc.go @@ -0,0 +1,31 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func InitWebrtc() { + l4g.Debug(utils.T("wsapi.webtrc.init.debug")) + + app.Srv.WebSocketRouter.Handle("webrtc", ApiWebSocketHandler(webrtcMessage)) +} + +func webrtcMessage(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + var ok bool + var toUserId string + if toUserId, ok = req.Data["to_user_id"].(string); !ok || len(toUserId) != 26 { + return nil, NewInvalidWebSocketParamError(req.Action, "to_user_id") + } + + event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil) + event.Data = req.Data + go app.Publish(event) + + return nil, nil +} diff --git a/wsapi/websocket_handler.go b/wsapi/websocket_handler.go new file mode 100644 index 000000000..193539242 --- /dev/null +++ b/wsapi/websocket_handler.go @@ -0,0 +1,61 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package wsapi + +import ( + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) webSocketHandler { + return webSocketHandler{wh} +} + +type webSocketHandler struct { + handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError) +} + +func (wh webSocketHandler) ServeWebSocket(conn *app.WebConn, r *model.WebSocketRequest) { + l4g.Debug("/api/v3/users/websocket:%s", r.Action) + + session, sessionErr := app.GetSession(conn.SessionToken) + if sessionErr != nil { + l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, conn.UserId, sessionErr.SystemMessage(utils.T), sessionErr.Error()) + sessionErr.DetailedError = "" + errResp := model.NewWebSocketError(r.Seq, sessionErr) + errResp.DoPreComputeJson() + + conn.Send <- errResp + return + } + + r.Session = *session + r.T = conn.T + r.Locale = conn.Locale + + var data map[string]interface{} + var err *model.AppError + + if data, err = wh.handlerFunc(r); err != nil { + l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, r.Session.UserId, err.SystemMessage(utils.T), err.DetailedError) + err.DetailedError = "" + errResp := model.NewWebSocketError(r.Seq, err) + errResp.DoPreComputeJson() + + conn.Send <- errResp + return + } + + resp := model.NewWebSocketResponse(model.STATUS_OK, r.Seq, data) + resp.DoPreComputeJson() + + conn.Send <- resp +} + +func NewInvalidWebSocketParamError(action string, name string) *model.AppError { + return model.NewLocAppError("/api/v3/users/websocket:"+action, "api.websocket_handler.invalid_param.app_error", map[string]interface{}{"Name": name}, "") +} -- cgit v1.2.3-1-g7c22