Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JaaS #185

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -100,8 +100,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
71 changes: 2 additions & 69 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
188 changes: 187 additions & 1 deletion server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 @@ -47,8 +52,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) {
larkox marked this conversation as resolved.
Show resolved Hide resolved
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
}

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 {
larkox marked this conversation as resolved.
Show resolved Hide resolved
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()
larkox marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -73,6 +221,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 @@ -82,6 +231,10 @@ func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {

func (p *Plugin) handleExternalAPIjs(w http.ResponseWriter, r *http.Request) {
if p.getConfiguration().JitsiCompatibilityMode {
if p.getConfiguration().UseJaaS {
p.proxyExternalAPIjsJaaS(w, r)
return
}
p.proxyExternalAPIjs(w, r)
return
}
Expand Down Expand Up @@ -112,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 @@ -244,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 @@ -278,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 @@ -298,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
16 changes: 11 additions & 5 deletions server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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 @@ -61,9 +63,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 @@ -147,7 +151,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