From 74ffb6f98f7ee8b4e61743919ab20460c57ad4da Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Sun, 16 Apr 2017 21:14:31 -0400 Subject: Implement GET /webrtc/token endpoint for APIv4 (#6046) --- api/webrtc.go | 72 +++---------------------------------------- api4/api.go | 1 + api4/webrtc.go | 29 ++++++++++++++++++ api4/webrtc_test.go | 29 ++++++++++++++++++ app/webrtc.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ model/client4.go | 13 ++++++++ model/webrtc.go | 32 +++++++++++++++++++ model/webrtc_test.go | 19 ++++++++++++ 8 files changed, 214 insertions(+), 68 deletions(-) create mode 100644 api4/webrtc.go create mode 100644 api4/webrtc_test.go create mode 100644 app/webrtc.go create mode 100644 model/webrtc_test.go diff --git a/api/webrtc.go b/api/webrtc.go index ad1da31e4..58e35f2fa 100644 --- a/api/webrtc.go +++ b/api/webrtc.go @@ -4,18 +4,10 @@ package api import ( - "crypto/hmac" - "crypto/sha1" - "crypto/tls" - "encoding/base64" "net/http" - "strconv" - "strings" - "time" l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/app" - "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -26,68 +18,12 @@ func InitWebrtc() { } func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { - if token, err := getWebrtcToken(c.Session.Id); err != nil { + result, err := app.GetWebrtcInfoForSession(c.Session.Id) + + if err != nil { c.Err = err return - } else { - result := make(map[string]string) - result["token"] = token - result["gateway_url"] = *utils.Cfg.WebrtcSettings.GatewayWebsocketUrl - - if len(*utils.Cfg.WebrtcSettings.StunURI) > 0 { - result["stun_uri"] = *utils.Cfg.WebrtcSettings.StunURI - } - - if len(*utils.Cfg.WebrtcSettings.TurnURI) > 0 { - timestamp := strconv.FormatInt(utils.EndOfDay(time.Now().AddDate(0, 0, 1)).Unix(), 10) - username := timestamp + ":" + *utils.Cfg.WebrtcSettings.TurnUsername - - result["turn_uri"] = *utils.Cfg.WebrtcSettings.TurnURI - result["turn_password"] = generateTurnPassword(username, *utils.Cfg.WebrtcSettings.TurnSharedKey) - result["turn_username"] = username - } - w.Write([]byte(model.MapToJson(result))) } -} - -func getWebrtcToken(sessionId string) (string, *model.AppError) { - if !*utils.Cfg.WebrtcSettings.Enable { - return "", model.NewLocAppError("WebRTC.getWebrtcToken", "api.webrtc.disabled.app_error", nil, "") - } - - token := base64.StdEncoding.EncodeToString([]byte(sessionId)) - - data := make(map[string]string) - data["janus"] = "add_token" - data["token"] = token - data["transaction"] = model.NewId() - data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret - - rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) - rq.Header.Set("Content-Type", "application/json") - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, - } - httpClient := &http.Client{Transport: tr} - if rp, err := httpClient.Do(rq); err != nil { - return "", model.NewLocAppError("WebRTC.Token", "model.client.connecting.app_error", nil, err.Error()) - } else if rp.StatusCode >= 300 { - defer app.CloseBody(rp) - return "", model.AppErrorFromJson(rp.Body) - } else { - janusResponse := model.GatewayResponseFromJson(rp.Body) - if janusResponse.Status != "success" { - return "", model.NewLocAppError("getWebrtcToken", "api.webrtc.register_token.app_error", nil, "") - } - } - - return token, nil -} -func generateTurnPassword(username string, secret string) string { - key := []byte(secret) - h := hmac.New(sha1.New, key) - h.Write([]byte(username)) - return base64.StdEncoding.EncodeToString(h.Sum(nil)) + w.Write([]byte(result.ToJson())) } diff --git a/api4/api.go b/api4/api.go index 45581525e..a91fb80b5 100644 --- a/api4/api.go +++ b/api4/api.go @@ -178,6 +178,7 @@ func InitApi(full bool) { InitWebSocket() InitEmoji() InitReaction() + InitWebrtc() app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/webrtc.go b/api4/webrtc.go new file mode 100644 index 000000000..81a1599b2 --- /dev/null +++ b/api4/webrtc.go @@ -0,0 +1,29 @@ +// 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/mattermost/platform/app" + "github.com/mattermost/platform/utils" +) + +func InitWebrtc() { + l4g.Debug(utils.T("api.webrtc.init.debug")) + + BaseRoutes.Webrtc.Handle("/token", ApiSessionRequired(webrtcToken)).Methods("GET") +} + +func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) { + result, err := app.GetWebrtcInfoForSession(c.Session.Id) + + if err != nil { + c.Err = err + return + } + + w.Write([]byte(result.ToJson())) +} diff --git a/api4/webrtc_test.go b/api4/webrtc_test.go new file mode 100644 index 000000000..806118f76 --- /dev/null +++ b/api4/webrtc_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "testing" + + "github.com/mattermost/platform/utils" +) + +func TestGetWebrtcToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + enableWebrtc := *utils.Cfg.WebrtcSettings.Enable + defer func() { + *utils.Cfg.WebrtcSettings.Enable = enableWebrtc + }() + *utils.Cfg.WebrtcSettings.Enable = false + + _, resp := Client.GetWebrtcToken() + CheckNotImplementedStatus(t, resp) + + Client.Logout() + _, resp = Client.GetWebrtcToken() + CheckUnauthorizedStatus(t, resp) +} diff --git a/app/webrtc.go b/app/webrtc.go new file mode 100644 index 000000000..6692fff60 --- /dev/null +++ b/app/webrtc.go @@ -0,0 +1,87 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package app + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/tls" + "encoding/base64" + "net/http" + "strconv" + "strings" + "time" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +func GetWebrtcInfoForSession(sessionId string) (*model.WebrtcInfoResponse, *model.AppError) { + token, err := GetWebrtcToken(sessionId) + if err != nil { + return nil, err + } + + result := &model.WebrtcInfoResponse{ + Token: token, + GatewayUrl: *utils.Cfg.WebrtcSettings.GatewayWebsocketUrl, + } + + if len(*utils.Cfg.WebrtcSettings.StunURI) > 0 { + result.StunUri = *utils.Cfg.WebrtcSettings.StunURI + } + + if len(*utils.Cfg.WebrtcSettings.TurnURI) > 0 { + timestamp := strconv.FormatInt(utils.EndOfDay(time.Now().AddDate(0, 0, 1)).Unix(), 10) + username := timestamp + ":" + *utils.Cfg.WebrtcSettings.TurnUsername + + result.TurnUri = *utils.Cfg.WebrtcSettings.TurnURI + result.TurnPassword = GenerateTurnPassword(username, *utils.Cfg.WebrtcSettings.TurnSharedKey) + result.TurnUsername = username + } + + return result, nil +} + +func GetWebrtcToken(sessionId string) (string, *model.AppError) { + if !*utils.Cfg.WebrtcSettings.Enable { + return "", model.NewAppError("WebRTC.getWebrtcToken", "api.webrtc.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + token := base64.StdEncoding.EncodeToString([]byte(sessionId)) + + data := make(map[string]string) + data["janus"] = "add_token" + data["token"] = token + data["transaction"] = model.NewId() + data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret + + rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) + rq.Header.Set("Content-Type", "application/json") + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections}, + } + httpClient := &http.Client{Transport: tr} + if rp, err := httpClient.Do(rq); err != nil { + return "", model.NewAppError("WebRTC.Token", "model.client.connecting.app_error", nil, err.Error(), http.StatusInternalServerError) + } else if rp.StatusCode >= 300 { + defer CloseBody(rp) + return "", model.AppErrorFromJson(rp.Body) + } else { + janusResponse := model.GatewayResponseFromJson(rp.Body) + if janusResponse.Status != "success" { + return "", model.NewAppError("getWebrtcToken", "api.webrtc.register_token.app_error", nil, "", http.StatusInternalServerError) + } + } + + return token, nil +} + +func GenerateTurnPassword(username string, secret string) string { + key := []byte(secret) + h := hmac.New(sha1.New, key) + h.Write([]byte(username)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/model/client4.go b/model/client4.go index 943525420..6281b3df4 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2286,6 +2286,19 @@ func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, } } +// Webrtc Section + +// GetWebrtcToken returns a valid token, stun server and turn server with credentials to +// use with the Mattermost WebRTC service. +func (c *Client4) GetWebrtcToken() (*WebrtcInfoResponse, *Response) { + if r, err := c.DoApiGet("/webrtc/token", ""); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return WebrtcInfoResponseFromJson(r.Body), BuildResponse(r) + } +} + // Emoji Section // CreateEmoji will save an emoji to the server if the current user has permission diff --git a/model/webrtc.go b/model/webrtc.go index e746d62a8..fa15a4b71 100644 --- a/model/webrtc.go +++ b/model/webrtc.go @@ -1,3 +1,6 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + package model import ( @@ -5,6 +8,15 @@ import ( "io" ) +type WebrtcInfoResponse struct { + Token string `json:"token"` + GatewayUrl string `json:"gateway_url"` + StunUri string `json:"stun_uri,omitempty"` + TurnUri string `json:"turn_uri,omitempty"` + TurnPassword string `json:"turn_password,omitempty"` + TurnUsername string `json:"turn_username,omitempty"` +} + type GatewayResponse struct { Status string `json:"janus"` } @@ -19,3 +31,23 @@ func GatewayResponseFromJson(data io.Reader) *GatewayResponse { return nil } } + +func (o *WebrtcInfoResponse) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } else { + return string(b) + } +} + +func WebrtcInfoResponseFromJson(data io.Reader) *WebrtcInfoResponse { + decoder := json.NewDecoder(data) + var o WebrtcInfoResponse + err := decoder.Decode(&o) + if err == nil { + return &o + } else { + return nil + } +} diff --git a/model/webrtc_test.go b/model/webrtc_test.go new file mode 100644 index 000000000..2418bd53a --- /dev/null +++ b/model/webrtc_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestWebrtcJson(t *testing.T) { + o := WebrtcInfoResponse{Token: NewId(), GatewayUrl: NewId()} + json := o.ToJson() + ro := WebrtcInfoResponseFromJson(strings.NewReader(json)) + + if o.Token != ro.Token { + t.Fatal("Tokens do not match") + } +} -- cgit v1.2.3-1-g7c22