Skip to content

Commit

Permalink
Add support for JaaS
Browse files Browse the repository at this point in the history
  • Loading branch information
cristifg committed Mar 22, 2021
1 parent e5e3252 commit b903bb9
Show file tree
Hide file tree
Showing 36 changed files with 1,621 additions and 307 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/mattermost/mattermost-plugin-jitsi
go 1.12

require (
github.com/cristalhq/jwt/v2 v2.0.0
github.com/cristalhq/jwt/v3 v3.0.12
github.com/google/uuid v1.1.1
github.com/mattermost/mattermost-plugin-api v0.0.12
github.com/mattermost/mattermost-server/v5 v5.25.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37g
github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cristalhq/jwt/v2 v2.0.0 h1:CxleHxkZQQ5J0siUQ2gwZrhAysmh8Ddh/R06AzCiYao=
github.com/cristalhq/jwt/v2 v2.0.0/go.mod h1:nQT19GqJbrWubmI+ULE8PYsR1GCbwI5hAg1nG+9AbTg=
github.com/cristalhq/jwt/v3 v3.0.12 h1:hblvQJcmcBVt0TJgaaWHwRMa/nLVG1jUnCQGlv67rxs=
github.com/cristalhq/jwt/v3 v3.0.12/go.mod h1:XOnIXst8ozq/esy5N1XOlSyQqBd+84fxJ99FK+1jgL8=
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
Expand Down
73 changes: 3 additions & 70 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"support_url": "https://github.com/mattermost/mattermost-plugin-jitsi/issues",
"release_notes_url": "https://github.com/mattermost/mattermost-plugin-jitsi/releases/tag/v2.0.0",
"icon_path": "assets/icon.svg",
"version": "2.0.0",
"version": "2.1.0",
"min_server_version": "5.2.0",
"server": {
"executables": {
Expand All @@ -21,75 +21,8 @@
"settings_schema": {
"settings": [
{
"key": "JitsiURL",
"display_name": "Jitsi Server URL:",
"type": "text",
"help_text": "The URL for your Jitsi server, for example https://jitsi.example.com. Defaults to https://meet.jit.si, which is the public server provided by Jitsi.",
"placeholder": "https://meet.jit.si",
"default": "https://meet.jit.si"
},
{
"key": "JitsiEmbedded",
"display_name": "Embed Jitsi video inside Mattermost:",
"type": "bool",
"help_text": "(Experimental) When true, Jitsi video is embedded as a floating window inside Mattermost by default. Users can override this setting with '/jitsi settings'."
},
{
"key": "JitsiNamingScheme",
"display_name": "Jitsi Meeting Names:",
"type": "radio",
"help_text": "Select how meeting names are generated by default. Users can override this setting with '/jitsi settings'.",
"default": "words",
"options": [
{
"display_name": "Random English words in title case (e.g. PlayfulDragonsObserveCuriously)",
"value": "words"
},
{
"display_name": "UUID (universally unique identifier)",
"value": "uuid"
},
{
"display_name": "Mattermost context specific names. Combination of team name, channel name, and random text in Public and Private channels; personal meeting name in Direct and Group Message channels.",
"value": "mattermost"
},
{
"display_name": "Allow user to select meeting name",
"value": "ask"
}
]
},
{
"key": "JitsiJWT",
"display_name": "Use JWT Authentication for Jitsi:",
"type": "bool",
"help_text": "(Optional) If your Jitsi server uses JSON Web Tokens (JWT) for authentication, set this value to true."
},
{
"key": "JitsiAppID",
"display_name": "App ID for JWT Authentication:",
"type": "text",
"help_text": "(Optional) The app ID used for authentication by the Jitsi server and JWT token generator."
},
{
"key": "JitsiAppSecret",
"display_name": "App Secret for JWT Authentication:",
"type": "text",
"help_text": "(Optional) The app secret used for authentication by the Jitsi server and JWT token generator."
},
{
"key": "JitsiLinkValidTime",
"display_name": "Meeting Link Expiry Time (minutes):",
"type": "number",
"help_text": "(Optional) The number of minutes from when the meeting link is created to when it becomes invalid. Minimum is 1 minute. Only applies if using JWT authentication for your Jitsi server.",
"default": 30
},
{
"key": "JitsiCompatibilityMode",
"display_name": "Enable Compatibility Mode:",
"type": "bool",
"help_text": "(Insecure) If your Jitsi server is not compatible with this plugin, include the JavaScript API hosted on your Jitsi server directly in Mattermost instead of the default API version provided by the plugin. **WARNING:** Enabling this setting can compromise the security of your Mattermost system, if your Jitsi server is not fully trusted and allows direct modification of program files. Use with caution.",
"default": false
"key": "JitsiSettings",
"type": "custom"
}
]
}
Expand Down
189 changes: 188 additions & 1 deletion server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type StartMeetingFromAction struct {
} `json:"context"`
}

type JaaSSettingsFromAction struct {
Jwt string `json:"jaasJwt"`
Path string `json:"jaasPath"`
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
switch path := r.URL.Path; path {
case "/api/v1/meetings/enrich":
Expand All @@ -46,8 +51,151 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
p.handleConfig(w, r)
case "/jitsi_meet_external_api.js":
p.handleExternalAPIjs(w, r)
case "/jaas-main.js":
p.handleJaaSBundle(w, r)
case "/api/v1/meetings/jaas/settings":
p.handleJaaSSettings(w, r)
default:

if p.getConfiguration().UseJaaS {
if p.isJaaSMeeting(path) {
p.handleOpenJaaSMeeting(w, r)
return
}
}

http.NotFound(w, r)
}
}

func (p *Plugin) handleJaaSSettings(w http.ResponseWriter, r *http.Request) {
if !p.getConfiguration().UseJaaS {
mlog.Error("error JaaS requested while disabled")
http.NotFound(w, r)
return
}

if err := p.getConfiguration().IsValid(); err != nil {
mlog.Error("Invalid plugin configuration", mlog.Err(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

var jaasSettingsFromAction JaaSSettingsFromAction

bodyData, err := ioutil.ReadAll(r.Body)
if err != nil {
mlog.Debug("Unable to read request body", mlog.Err(err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

err1 := json.NewDecoder(bytes.NewReader(bodyData)).Decode(&jaasSettingsFromAction)
if err1 != nil {
mlog.Debug("Unable to decode the request content as start meeting request or start meeting action")
http.Error(w, "Unable to decode your request", http.StatusBadRequest)
return
}
// TODO remove reusage of variables
var user *model.User = nil
userID := r.Header.Get("Mattermost-User-Id")
if userID != "" {
// Handle moderator
userRet, appErr := p.API.GetUser(userID)
if appErr != nil {
mlog.Debug("Unable to the user", mlog.Err(appErr))
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
user = userRet
}

jaasSettings, err2 := p.getJaaSSettings(jaasSettingsFromAction.Jwt, jaasSettingsFromAction.Path, user)
if err2 != nil {
mlog.Error("Error getting JaaSSettings", mlog.Err(err2))
http.Error(w, "Invalid JaaS settings", http.StatusBadRequest)
return
}

settingsJSON, err := json.Marshal(jaasSettings)
if err != nil {
mlog.Error("Error marshaling the JaaSSettings to json", mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(settingsJSON)
if err != nil {
mlog.Warn("Unable to write response body", mlog.String("handler", "handleJaaSSettings"), mlog.Err(err))
}
}

func (p *Plugin) isJaaSMeeting(path string) bool {
return p.jaasURLCheckRegExp.MatchString(path)
}

func (p *Plugin) handleJaaSBundle(w http.ResponseWriter, r *http.Request) {
if !p.getConfiguration().UseJaaS {
http.Error(w, "Not found", http.StatusFound)
return
}

bundlePath, err := p.API.GetBundlePath()
if err != nil {
mlog.Error("Filed to get the bundle path")
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

jaasMainPath := filepath.Join(bundlePath, "webapp", "dist", "jaas", "jaas-main.js")
jaasMainFile, err := os.Open(jaasMainPath)
if err != nil {
mlog.Error("Error opening file", mlog.String("path", jaasMainPath), mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

code, err := ioutil.ReadAll(jaasMainFile)
if err != nil {
mlog.Error("Error reading file content", mlog.String("path", jaasMainPath), mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/javascript")
_, err = w.Write(code)
if err != nil {
mlog.Warn("Unable to write response body", mlog.String("handler", "jaas-main.js"), mlog.Err(err))
}
}

func (p *Plugin) handleOpenJaaSMeeting(w http.ResponseWriter, r *http.Request) {
bundlePath, err := p.API.GetBundlePath()
if err != nil {
mlog.Error("Filed to get the bundle path")
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

jaasPath := filepath.Join(bundlePath, "webapp", "dist", "jaas", "index.html")
jaasFile, err := os.Open(jaasPath)
if err != nil {
mlog.Error("Error opening file", mlog.String("path", jaasPath), mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
code, err := ioutil.ReadAll(jaasFile)
if err != nil {
mlog.Error("Error reading file content", mlog.String("path", jaasPath), mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "text/html; charset=UTF-8")
_, err = w.Write(code)
if err != nil {
mlog.Warn("Unable to write response body", mlog.String("handler", "handleJaaSMeeting"), mlog.Err(err))
}
}

Expand All @@ -72,6 +220,7 @@ func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
Expand All @@ -80,6 +229,11 @@ func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {
}

func (p *Plugin) handleExternalAPIjs(w http.ResponseWriter, r *http.Request) {
if p.getConfiguration().UseJaaS {
p.proxyExternalAPIjsJaaS(w, r)
return
}

if p.getConfiguration().JitsiCompatibilityMode {
p.proxyExternalAPIjs(w, r)
return
Expand Down Expand Up @@ -111,6 +265,37 @@ func (p *Plugin) handleExternalAPIjs(w http.ResponseWriter, r *http.Request) {
}
}

func (p *Plugin) proxyExternalAPIjsJaaS(w http.ResponseWriter, r *http.Request) {
externalAPICacheMutex.Lock()
defer externalAPICacheMutex.Unlock()

if externalAPICache != nil && externalAPILastUpdate > (model.GetMillis()-externalAPICacheTTL) {
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write(externalAPICache)
return
}
resp, err := http.Get(p.getConfiguration().Get8x8vcURL() + "/libs/external_api.min.js")
if err != nil {
mlog.Error("Error getting the external_api.min.js file from your 8x8", mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
mlog.Error("Error getting reading the content", mlog.String("url", p.getConfiguration().Get8x8vcURL()+"/external_api.min.js"), mlog.Err(err))
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
externalAPICache = body
externalAPILastUpdate = model.GetMillis()
w.Header().Set("Content-Type", "application/javascript")
_, err = w.Write(body)
if err != nil {
mlog.Warn("Unable to write response body", mlog.String("handler", "proxyExternalAPIjs"), mlog.Err(err))
}
}

func (p *Plugin) proxyExternalAPIjs(w http.ResponseWriter, r *http.Request) {
externalAPICacheMutex.Lock()
defer externalAPICacheMutex.Unlock()
Expand Down Expand Up @@ -243,6 +428,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
Expand Down Expand Up @@ -277,7 +463,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request)
http.Error(w, err.Error(), err.StatusCode)
}

JWTMeeting := p.getConfiguration().JitsiJWT
JWTMeeting := p.getConfiguration().JitsiJWT || p.getConfiguration().UseJaaS

if !JWTMeeting {
http.Error(w, "Not authorized", http.StatusUnauthorized)
Expand All @@ -297,6 +483,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err2 = w.Write(b)
if err2 != nil {
Expand Down
1 change: 1 addition & 0 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ func (p *Plugin) settingsError(userID string, channelID string, errorText string
func (p *Plugin) executeSettingsCommand(c *plugin.Context, args *model.CommandArgs, parameters []string) (*model.CommandResponse, *model.AppError) {
l := p.b.GetUserLocalizer(args.UserId)
text := ""
// TODO maybe handle JaaS

userConfig, err := p.getUserConfig(args.UserId)
if err != nil {
Expand Down
16 changes: 11 additions & 5 deletions server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import (
func TestCommandHelp(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
JitsiSettings: jitsisettings{
JitsiURL: "http://test",
},
},
botID: "test-bot-id",
}
Expand Down Expand Up @@ -60,9 +62,11 @@ func TestCommandHelp(t *testing.T) {
func TestCommandSettings(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
JitsiEmbedded: false,
JitsiNamingScheme: "mattermost",
JitsiSettings: jitsisettings{
JitsiURL: "http://test",
JitsiEmbedded: false,
JitsiNamingScheme: "mattermost",
},
},
botID: "test-bot-id",
}
Expand Down Expand Up @@ -146,7 +150,9 @@ func TestCommandSettings(t *testing.T) {
func TestCommandStartMeeting(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
JitsiSettings: jitsisettings{
JitsiURL: "http://test",
},
},
}

Expand Down
Loading

0 comments on commit b903bb9

Please sign in to comment.