From 30a2ff8cfa264e62f022f53643af53550bf49d20 Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Mon, 13 Jul 2020 07:30:18 -0400 Subject: [PATCH 01/56] Add contributing information to README --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index ec21f61a..3a9664b5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,16 @@ Go to **System Console > Plugins > Jitsi** and set the following values: You're all set! To test it, go to any Mattermost channel and click the video icon in the channel header to start a new Jitsi meeting. +## Localization + +Mattermost Jitsi Plugin supports localization of user specify messages. You can change language of plugin by setting it in **System Console > General > Localization > Default Server Language**. Language of messages that only a user can see (e.g.: help messages, error messages) use the language set in **Account Settings > Display > Language**. + +The currently supported languages are: +- English +- France +- German +- Spanish + ### Manual Builds You can use Docker to compile the binaries yourself. Run `./docker-make` shell script which builds a Docker image with necessary build dependencies and runs `make all` afterwards. @@ -68,3 +78,9 @@ Inside the `/server` directory, you will find the Go files that make up the serv ### Web App Inside the `/webapp` directory, you will find the JS and React files that make up the client-side of the plugin. Within there, modify files and components as necessary. Test your syntax by running `npm run build`. + +## Contributing + +We welcome contributions for bug reports, issues, feature requests, feature implementations and pull requests. Feel free to [**file a new issue**](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new/choose) or join the [**Plugin: Jitsi channel**](https://community.mattermost.com/core/channels/plugin-jitsi) on the Mattermost community server. + +For a complete guide on contributing to the plugin, see the [Contribution Guideline](CONTRIBUTING.md). From 74a6f0c85905efcc82337c83ca3e182d3db8f41f Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Mon, 13 Jul 2020 07:30:53 -0400 Subject: [PATCH 02/56] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a9664b5..763e1a32 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,18 @@ You're all set! To test it, go to any Mattermost channel and click the video ico ## Localization -Mattermost Jitsi Plugin supports localization of user specify messages. You can change language of plugin by setting it in **System Console > General > Localization > Default Server Language**. Language of messages that only a user can see (e.g.: help messages, error messages) use the language set in **Account Settings > Display > Language**. +### Localization -The currently supported languages are: +Mattermost Jitsi Plugin supports localization in multiple languages: - English - France - German - Spanish +The plugin automatically displays languages based on the following: +- For system messages, the locale set in **System Console > General > Localization > Default Server Language** is used. +- For user messages, such as help text and error messages, the locale set set in **Account Settings > Display > Language** is used. + ### Manual Builds You can use Docker to compile the binaries yourself. Run `./docker-make` shell script which builds a Docker image with necessary build dependencies and runs `make all` afterwards. @@ -83,4 +87,4 @@ Inside the `/webapp` directory, you will find the JS and React files that make u We welcome contributions for bug reports, issues, feature requests, feature implementations and pull requests. Feel free to [**file a new issue**](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new/choose) or join the [**Plugin: Jitsi channel**](https://community.mattermost.com/core/channels/plugin-jitsi) on the Mattermost community server. -For a complete guide on contributing to the plugin, see the [Contribution Guideline](CONTRIBUTING.md). +For a complete guide on contributing to the plugin, see the [Contribution Guidelines](CONTRIBUTING.md). From 18b443c7713eee336b1f30ce9651ed6873939d6d Mon Sep 17 00:00:00 2001 From: Jason Blais <13119842+jasonblais@users.noreply.github.com> Date: Mon, 13 Jul 2020 07:31:12 -0400 Subject: [PATCH 03/56] Create CONTRIBUTING.md --- CONTRIBUTING.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5c429f6b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,88 @@ +# Contributing to Mattermost Jitsi Plugin + +Thank you for your interest in contributing! Join the [**Plugin: Jitsi**](https://community-daily.mattermost.com/core/channels/plugin-jitsi) channel on the Mattermost community server for discussion about this plugin. + +## Reporting issues + +If you think you found a bug, [please use the GitHub issue tracker](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) to open an issue. To help us troubleshoot the issue, please provide the required information in the issue template. + +## Translating strings + +Mattermost Jitsi Plugin supports localization to various languages. We as maintainers rely on contributors to help with the translations. + +The plugin uses [go-i18n](https://github.com/nicksnyder/go-i18n) as library and tool to manage translation. The CLI tool `goi18n` is required to manage translation. You can install it by running `env GO111MODULE=off go get -u github.com/nicksnyder/go-i18n/v2/goi18n`. + +The localization process is defined below: +- During development, new translation strings may be added or existing ones updated. +- When a new version is planned to release soon, a repository maintainer opens an issue informing about the new release and mentions all translation maintainers in the issue. +- Translation maintainers submit PRs with new translations, which may get reviewed by other translators. +- After all translation PRs are merged, the new version is released. If a translation PR is not submitted within a week, the release is cut without it. + +### Translation maintainers + +- French: [@Extazx2](https://github.com/Extazx2) +- German: [@hanzei](https://github.com/hanzei) +- Spanish: [@jespino](https://github.com/jespino) + +### Translate to a new language + +Note: We use the German locale (`de`) in this example. When translating to a new language, replace `de` in the following commands with the locale you want to translate. [See available locales](https://github.com/mattermost/mattermost-server/tree/master/i18n). + +1. Create a translation file: + + `touch asserts/i18n/translate.de.json` + +2. Merge all current messages into your translation file: + + `make i18n-extract-server` + +3. Translate all messages in `asserts/i18n/translate.de.json`. + +4. Merge the translated messages into the active message files: + + `make i18n-merge-server` + +5. Add your language to the list of [supported languages](https://github.com/mattermost/mattermost-plugin-jitsi#localization) in `README.md` and add yourself to the list of [translation maintainers](#translation-maintainers) in `CONTRIBUTING.md`. + +6. [Submit a PR](https://github.com/mattermost/mattermost-plugin-jitsi/compare) with these files. + +Once you've submitted a PR, your changes will be reviewed. Big thank you for your contribution! + +### Translate existing languages + +1. Ensure all translation messages are correctly extracted: + + `make i18n-extract-server` + +2. Translate all messages in `asserts/i18n/translate.*.json` for the languages you are comfortable with. + +3. Merge the translated messages into the active message files: + + `make i18n-merge-server` + +4. Commit **only the language files you edited** and [submit a PR](https://github.com/mattermost/mattermost-plugin-jitsi/compare). + +## Submitting bug fixes or features + +We have [open help wanted issues](https://github.com/mattermost/mattermost-plugin-jitsi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22Help+Wanted%22) we are looking for help. + +If you are contributing a feature, [please open a feature request](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) first. This enables the feature to be discussed and fully specified before you start working on it. Small code changes can be submitted without opening an issue. + +Note that this project uses [Go modules](https://github.com/golang/go/wiki/Modules). Be sure to locate the project outside of `$GOPATH`, or allow the use of Go modules within your `$GOPATH` with an `export GO111MODULE=on`. + +## Development + +This plugin contains both a server and webapp portion. + +* Use `make dist` to build distributions of the plugin that you can upload to a Mattermost server. +* Use `make test` to run tests of the plugin. +* Use `make check-style` to check the style. +* Use `make deploy` to deploy the plugin to your Mattermost server. Before running make deploy you need to set a few environment variables: + +```sh +export MM_SERVICESETTINGS_SITEURL=http://localhost:8065 +export MM_ADMIN_USERNAME=admin +export MM_ADMIN_PASSWORD=password +``` + +* Use `make help` to know all useful targets for devleopment From 0a419710d1a199ef2b4ea78c301d5518d3380edc Mon Sep 17 00:00:00 2001 From: Justine Geffen Date: Wed, 13 Jan 2021 22:47:53 +0200 Subject: [PATCH 04/56] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c429f6b..495949b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Mattermost Jitsi Plugin -Thank you for your interest in contributing! Join the [**Plugin: Jitsi**](https://community-daily.mattermost.com/core/channels/plugin-jitsi) channel on the Mattermost community server for discussion about this plugin. +Thank you for your interest in contributing! Join the [Plugin: Jitsi](https://community-daily.mattermost.com/core/channels/plugin-jitsi) channel on the Mattermost community server for discussion about this plugin. ## Reporting issues From e9e710d7814ba333a8e777f982aba3a15c388db4 Mon Sep 17 00:00:00 2001 From: Justine Geffen Date: Wed, 13 Jan 2021 22:48:23 +0200 Subject: [PATCH 05/56] Apply suggestions from code review --- CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 495949b3..f67d66e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,11 +4,11 @@ Thank you for your interest in contributing! Join the [Plugin: Jitsi](https://co ## Reporting issues -If you think you found a bug, [please use the GitHub issue tracker](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) to open an issue. To help us troubleshoot the issue, please provide the required information in the issue template. +If you think you've found a bug, [please use the GitHub issue tracker](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) to open an issue. To help us troubleshoot the issue, please provide the required information in the issue template. ## Translating strings -Mattermost Jitsi Plugin supports localization to various languages. We as maintainers rely on contributors to help with the translations. +The Mattermost Jitsi plugin supports localization to various languages. We as maintainers rely on contributors to help with the translations. The plugin uses [go-i18n](https://github.com/nicksnyder/go-i18n) as library and tool to manage translation. The CLI tool `goi18n` is required to manage translation. You can install it by running `env GO111MODULE=off go get -u github.com/nicksnyder/go-i18n/v2/goi18n`. diff --git a/README.md b/README.md index 763e1a32..3a2849b1 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ You're all set! To test it, go to any Mattermost channel and click the video ico Mattermost Jitsi Plugin supports localization in multiple languages: - English -- France +- French - German - Spanish @@ -63,7 +63,7 @@ The plugin automatically displays languages based on the following: - For system messages, the locale set in **System Console > General > Localization > Default Server Language** is used. - For user messages, such as help text and error messages, the locale set set in **Account Settings > Display > Language** is used. -### Manual Builds +### Manual builds You can use Docker to compile the binaries yourself. Run `./docker-make` shell script which builds a Docker image with necessary build dependencies and runs `make all` afterwards. @@ -85,6 +85,6 @@ Inside the `/webapp` directory, you will find the JS and React files that make u ## Contributing -We welcome contributions for bug reports, issues, feature requests, feature implementations and pull requests. Feel free to [**file a new issue**](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new/choose) or join the [**Plugin: Jitsi channel**](https://community.mattermost.com/core/channels/plugin-jitsi) on the Mattermost community server. +We welcome contributions for bug reports, issues, feature requests, feature implementations, and pull requests. Feel free to [file a new issue](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new/choose) or join the [Plugin: Jitsi channel](https://community.mattermost.com/core/channels/plugin-jitsi) on the Mattermost community server. For a complete guide on contributing to the plugin, see the [Contribution Guidelines](CONTRIBUTING.md). From 79dc804e234a7c410c3657f5fb63f1ff56fda9e3 Mon Sep 17 00:00:00 2001 From: Justine Geffen Date: Wed, 13 Jan 2021 22:52:58 +0200 Subject: [PATCH 06/56] Update CONTRIBUTING.md --- CONTRIBUTING.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f67d66e0..f00e2996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,10 +13,11 @@ The Mattermost Jitsi plugin supports localization to various languages. We as ma The plugin uses [go-i18n](https://github.com/nicksnyder/go-i18n) as library and tool to manage translation. The CLI tool `goi18n` is required to manage translation. You can install it by running `env GO111MODULE=off go get -u github.com/nicksnyder/go-i18n/v2/goi18n`. The localization process is defined below: -- During development, new translation strings may be added or existing ones updated. -- When a new version is planned to release soon, a repository maintainer opens an issue informing about the new release and mentions all translation maintainers in the issue. -- Translation maintainers submit PRs with new translations, which may get reviewed by other translators. -- After all translation PRs are merged, the new version is released. If a translation PR is not submitted within a week, the release is cut without it. + +1. During development, new translation strings may be added or existing ones updated. +2. When a new version is planned to release soon, a repository maintainer opens an issue informing about the new release and mentions all translation maintainers in the issue. +3. Translation maintainers submit PRs with new translations, which may get reviewed by other translators. +4. After all translation PRs are merged, the new version is released. If a translation PR is not submitted within a week, the release is cut without it. ### Translation maintainers @@ -28,7 +29,7 @@ The localization process is defined below: Note: We use the German locale (`de`) in this example. When translating to a new language, replace `de` in the following commands with the locale you want to translate. [See available locales](https://github.com/mattermost/mattermost-server/tree/master/i18n). -1. Create a translation file: +1. Open your teminal window and create a translation file: `touch asserts/i18n/translate.de.json` @@ -64,25 +65,14 @@ Once you've submitted a PR, your changes will be reviewed. Big thank you for you ## Submitting bug fixes or features -We have [open help wanted issues](https://github.com/mattermost/mattermost-plugin-jitsi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22Help+Wanted%22) we are looking for help. - -If you are contributing a feature, [please open a feature request](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) first. This enables the feature to be discussed and fully specified before you start working on it. Small code changes can be submitted without opening an issue. +If you're contributing a feature, [please open a feature request](https://github.com/mattermost/mattermost-plugin-jitsi/issues/new?template=issue.md) first. This enables the feature to be discussed and fully specified before you start working on it. Small code changes can be submitted without opening an issue. Note that this project uses [Go modules](https://github.com/golang/go/wiki/Modules). Be sure to locate the project outside of `$GOPATH`, or allow the use of Go modules within your `$GOPATH` with an `export GO111MODULE=on`. -## Development +### Help wanted -This plugin contains both a server and webapp portion. +You can view [open help wanted issues](https://github.com/mattermost/mattermost-plugin-jitsi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22Help+Wanted%22) to get started with contributing to the plugin. -* Use `make dist` to build distributions of the plugin that you can upload to a Mattermost server. -* Use `make test` to run tests of the plugin. -* Use `make check-style` to check the style. -* Use `make deploy` to deploy the plugin to your Mattermost server. Before running make deploy you need to set a few environment variables: - -```sh -export MM_SERVICESETTINGS_SITEURL=http://localhost:8065 -export MM_ADMIN_USERNAME=admin -export MM_ADMIN_PASSWORD=password -``` +## Development -* Use `make help` to know all useful targets for devleopment +This plugin contains both a server and web app portion. Read our documentation about the [Developer Workflow](https://developers.mattermost.com/extend/plugins/developer-workflow/) and [Developer Setup](https://developers.mattermost.com/extend/plugins/developer-setup/) for more information about developing and extending plugins. From a43d1ec1187a33fdeb297fba5136783f01bf65e5 Mon Sep 17 00:00:00 2001 From: Cristian Florin Ghita Date: Wed, 17 Mar 2021 14:52:36 +0200 Subject: [PATCH 07/56] Add support for JaaS --- go.mod | 2 +- go.sum | 4 +- plugin.json | 73 +--- server/api.go | 189 +++++++++- server/command.go | 1 + server/command_test.go | 16 +- server/configuration.go | 84 ++++- server/manifest.go | 84 +---- server/plugin.go | 331 ++++++++++++++++-- server/plugin_test.go | 6 +- webapp/.eslintrc.json | 2 +- webapp/i18n/de.json | 1 + webapp/i18n/en.json | 1 + webapp/i18n/es.json | 1 + webapp/i18n/fr.json | 1 + webapp/i18n/ru.json | 1 + webapp/package.json | 7 +- .../admin_settings/jaas_section.tsx | 118 +++++++ .../admin_settings/jitsi_section.tsx | 271 ++++++++++++++ .../admin_settings/jitsi_settings.tsx | 271 ++++++++++++++ .../src/components/conference/conference.tsx | 3 +- .../post_type_jitsi/post_type_jitsi.test.tsx | 2 +- .../post_type_jitsi/post_type_jitsi.tsx | 6 +- webapp/src/index.tsx | 2 + webapp/src/jaas/action_types/index.ts | 4 + webapp/src/jaas/actions/index.ts | 49 +++ .../src/jaas/components/conference/index.ts | 4 + .../components/conference/jaas_conference.tsx | 110 ++++++ webapp/src/jaas/index.html | 11 + webapp/src/jaas/index.tsx | 13 + webapp/src/jaas/reducers/index.ts | 38 ++ webapp/src/jaas/reducers/store.ts | 13 + webapp/src/jaas/util/index.ts | 23 ++ webapp/src/manifest.ts | 84 +---- webapp/tsconfig.json | 3 +- webapp/webpack.config.js | 99 ++++-- 36 files changed, 1621 insertions(+), 307 deletions(-) create mode 100644 webapp/src/components/admin_settings/jaas_section.tsx create mode 100644 webapp/src/components/admin_settings/jitsi_section.tsx create mode 100644 webapp/src/components/admin_settings/jitsi_settings.tsx create mode 100644 webapp/src/jaas/action_types/index.ts create mode 100644 webapp/src/jaas/actions/index.ts create mode 100644 webapp/src/jaas/components/conference/index.ts create mode 100644 webapp/src/jaas/components/conference/jaas_conference.tsx create mode 100644 webapp/src/jaas/index.html create mode 100644 webapp/src/jaas/index.tsx create mode 100644 webapp/src/jaas/reducers/index.ts create mode 100644 webapp/src/jaas/reducers/store.ts create mode 100644 webapp/src/jaas/util/index.ts diff --git a/go.mod b/go.mod index 1e5f86c1..e4a9d9f9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c998383d..968cdbde 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/plugin.json b/plugin.json index 64e90e7f..efdabbc1 100644 --- a/plugin.json +++ b/plugin.json @@ -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": { @@ -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" } ] } diff --git a/server/api.go b/server/api.go index 7fd432c9..5f3de9e7 100644 --- a/server/api.go +++ b/server/api.go @@ -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": @@ -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)) } } @@ -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 { @@ -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 @@ -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() @@ -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 { @@ -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) @@ -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 { diff --git a/server/command.go b/server/command.go index 51beb2aa..61714fa4 100644 --- a/server/command.go +++ b/server/command.go @@ -209,6 +209,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 { diff --git a/server/command_test.go b/server/command_test.go index 9e48a02e..41a8d411 100644 --- a/server/command_test.go +++ b/server/command_test.go @@ -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", } @@ -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", } @@ -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", + }, }, } diff --git a/server/configuration.go b/server/configuration.go index 4e9de91d..aa3f095b 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -25,6 +25,10 @@ import ( // If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep // copy appropriate for your types. type configuration struct { + JitsiSettings jitsisettings +} + +type jitsisettings struct { JitsiURL string JitsiJWT bool JitsiEmbedded bool @@ -33,13 +37,25 @@ type configuration struct { JitsiLinkValidTime int JitsiNamingScheme string JitsiCompatibilityMode bool + UseJaaS bool + JaaSAppID string + JaaSApiKey string + JaaSPrivateKey string } const publicJitsiServerURL = "https://meet.jit.si" +const public8x8vcURL = "https://8x8.vc" // GetJitsiURL return the currently configured JitsiURL or the URL from the // public servers provided by Jitsi. func (c *configuration) GetJitsiURL() string { + if len(c.JitsiSettings.JitsiURL) > 0 { + return c.JitsiSettings.JitsiURL + } + return publicJitsiServerURL +} + +func (c *jitsisettings) GetJitsiURL() string { if len(c.JitsiURL) > 0 { return c.JitsiURL } @@ -50,6 +66,14 @@ func (c *configuration) GetDefaultJitsiURL() string { return publicJitsiServerURL } +func (c *configuration) Get8x8vcURL() string { + return public8x8vcURL +} + +func (c *jitsisettings) Get8x8vcURL() string { + return public8x8vcURL +} + // Clone shallow copies the configuration. Your implementation may require a deep copy if // your configuration has reference types. func (c *configuration) Clone() *configuration { @@ -59,6 +83,43 @@ func (c *configuration) Clone() *configuration { // IsValid checks if all needed fields are set. func (c *configuration) IsValid() error { + if len(c.JitsiSettings.JitsiURL) > 0 { + _, err := url.Parse(c.JitsiSettings.JitsiURL) + if err != nil { + return fmt.Errorf("error invalid jitsiURL") + } + } + + if c.JitsiSettings.JitsiJWT { + if len(c.JitsiSettings.JitsiAppID) == 0 { + return fmt.Errorf("error no Jitsi app ID was provided to use with JWT") + } + if len(c.JitsiSettings.JitsiAppSecret) == 0 { + return fmt.Errorf("error no Jitsi app secret provided to use with JWT") + } + if c.JitsiSettings.JitsiLinkValidTime < 1 { + c.JitsiSettings.JitsiLinkValidTime = 30 + } + } + + if c.JitsiSettings.UseJaaS { + if len(c.JitsiSettings.JaaSApiKey) == 0 { + return fmt.Errorf("error no JaaS Api Key was provided for JaaS") + } + + if len(c.JitsiSettings.JaaSAppID) == 0 { + return fmt.Errorf("error no JaaS AppID was provided for JaaS") + } + + if len(c.JitsiSettings.JaaSPrivateKey) == 0 { + return fmt.Errorf("error no JaaS Private KEy was provided for JaaS") + } + } + + return nil +} + +func (c *jitsisettings) IsValid() error { if len(c.JitsiURL) > 0 { _, err := url.Parse(c.JitsiURL) if err != nil { @@ -78,21 +139,36 @@ func (c *configuration) IsValid() error { } } + if c.UseJaaS { + if len(c.JaaSApiKey) == 0 { + return fmt.Errorf("error no JaaS Api Key was provided for JaaS") + } + + if len(c.JaaSAppID) == 0 { + return fmt.Errorf("error no JaaS AppID was provided for JaaS") + } + + if len(c.JaaSPrivateKey) == 0 { + return fmt.Errorf("error no JaaS Private KEy was provided for JaaS") + } + } + return nil } // getConfiguration retrieves the active configuration under lock, making it safe to use // concurrently. The active configuration may change underneath the client of this method, but // the struct returned by this API call is considered immutable. -func (p *Plugin) getConfiguration() *configuration { +func (p *Plugin) getConfiguration() *jitsisettings { p.configurationLock.RLock() defer p.configurationLock.RUnlock() if p.configuration == nil { - return &configuration{} + newConfiguration := configuration{} + return &newConfiguration.JitsiSettings } - return p.configuration + return &p.configuration.JitsiSettings } // setConfiguration replaces the active configuration under lock. @@ -126,7 +202,6 @@ func (p *Plugin) setConfiguration(configuration *configuration) { // OnConfigurationChange is invoked when configuration changes may have been made. func (p *Plugin) OnConfigurationChange() error { var configuration = new(configuration) - // Load the public configuration fields from the Mattermost server configuration. if err := p.API.LoadPluginConfiguration(configuration); err != nil { return errors.Wrap(err, "failed to load plugin configuration") @@ -142,7 +217,6 @@ func (p *Plugin) OnConfigurationChange() error { p.tracker = telemetry.NewTracker(p.telemetryClient, p.API.GetDiagnosticId(), p.API.GetServerVersion(), manifest.Id, manifest.Version, "jitsi", enableDiagnostics) p.setConfiguration(configuration) - return nil } diff --git a/server/manifest.go b/server/manifest.go index 7429b78f..834ad3a3 100644 --- a/server/manifest.go +++ b/server/manifest.go @@ -19,7 +19,7 @@ const manifestStr = ` "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": { @@ -37,86 +37,12 @@ const manifestStr = ` "footer": "", "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'.", - "placeholder": "", - "default": null - }, - { - "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'.", - "placeholder": "", - "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.", - "placeholder": "", - "default": null - }, - { - "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.", - "placeholder": "", - "default": null - }, - { - "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": "JitsiSettings", + "display_name": "", + "type": "custom", + "help_text": "", "placeholder": "", "default": null - }, - { - "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.", - "placeholder": "", - "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.", - "placeholder": "", - "default": false } ] } diff --git a/server/plugin.go b/server/plugin.go index a0ece18a..be625ac3 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -1,7 +1,10 @@ package main import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "net/url" "path/filepath" @@ -10,8 +13,9 @@ import ( "sync" "time" - "github.com/cristalhq/jwt/v2" + "github.com/cristalhq/jwt/v3" "github.com/mattermost/mattermost-plugin-api/experimental/telemetry" + "github.com/google/uuid" "github.com/mattermost/mattermost-plugin-api/i18n" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" @@ -25,6 +29,9 @@ const jitsiNameSchemeUUID = "uuid" const jitsiNameSchemeMattermost = "mattermost" const configChangeEvent = "config_update" +const jaasAudienceClaim = "jitsi" +const jaasIssuerClaim = "chat" + type UserConfig struct { Embedded bool `json:"embedded"` NamingScheme string `json:"naming_scheme"` @@ -46,9 +53,17 @@ type Plugin struct { b *i18n.Bundle botID string + + jaasURLCheckRegExp *regexp.Regexp } func (p *Plugin) OnActivate() error { + var onceRegEx sync.Once + initRegExp := func() { + p.jaasURLCheckRegExp = regexp.MustCompile(`(vpaas-magic-cookie-[a-f0-9]{32}\/.+)`) + } + onceRegEx.Do(initRegExp) + config := p.getConfiguration() if err := config.IsValid(); err != nil { return err @@ -116,6 +131,62 @@ type Claims struct { Room string `json:"room,omitempty"` } +// JaaSUser ... +type JaaSUser struct { + Avatar string `json:"avatar"` + Name string `json:"name"` + Email string `json:"email"` + ID string `json:"id,omitempty"` + IsModerator string `json:"moderator,omitempty"` +} + +// JaaSFeatures ... +type JaaSFeatures struct { + LiveStreaming string `json:"livestreaming"` + Recording string `json:"recording"` + OutboundCall string `json:"outbound-call"` + Transcription string `json:"transcription"` +} + +// JaaSContext ... +type JaaSContext struct { + User JaaSUser `json:"user"` + Features JaaSFeatures `json:"features"` +} + +// JaaSClaims ... +type JaaSClaims struct { + Audience string `json:"aud,omitempty"` + Subject string `json:"sub,omitempty"` + Issuer string `json:"iss,omitempty"` + Room string `json:"room,omitempty"` + Exp int64 `json:"exp,omitempty"` + Nbf int64 `json:"nbf,omitempty"` + Context JaaSContext `json:"context"` +} + +// JaaSSettings ... +type JaaSSettings struct { + Jwt string `json:"jaasJwt"` + Room string `json:"jaasRoom"` +} + +func verifyJwtJaaS(jaasToken string) (*JaaSClaims, error) { + newToken, err := jwt.ParseString(jaasToken) + if err != nil { + mlog.Error("Error parsing jaas jwt", mlog.Err(err)) + return nil, err + } + + var claims JaaSClaims + if err = json.Unmarshal(newToken.RawClaims(), &claims); err != nil { + mlog.Error("Error unmarshalling claims for jaas jwt", mlog.Err(err)) + return nil, err + } + + return &claims, nil +} + func verifyJwt(secret string, jwtToken string) (*Claims, error) { verifier, err := jwt.NewVerifierHS(jwt.HS256, []byte(secret)) if err != nil { @@ -151,6 +222,49 @@ func signClaims(secret string, claims *Claims) (string, error) { return string(token.Raw()), nil } +func signClaimsJaaS(apiKeyJaaS string, privateKeyJaaS string, claimsJaaS *JaaSClaims) (string, error) { + var err error + privateKeyJaaSBytes := []byte(privateKeyJaaS) + privPem, _ := pem.Decode(privateKeyJaaSBytes) + + if privPem == nil { + mlog.Error("Error decoding specified JaaS private key") + return "", errors.New("internal server error") + } + + var privPemBytes []byte = privPem.Bytes + var parsedKey interface{} + parsedKey, err = x509.ParsePKCS8PrivateKey(privPemBytes) + + if err != nil { + mlog.Error("Error parsing JaaS private key", mlog.Err(err)) + return "", err + } + success := false + var privateKey *rsa.PrivateKey + privateKey, success = parsedKey.(*rsa.PrivateKey) + + if !success { + mlog.Error("Error converting JaaS private Key", mlog.Err(err)) + return "", err + } + + signer, err := jwt.NewSignerRS(jwt.RS256, privateKey) + + if err != nil { + return "", err + } + + builder := jwt.NewBuilder(signer, jwt.WithKeyID(apiKeyJaaS)) + token, err := builder.Build(claimsJaaS) + + if err != nil { + return "", err + } + + return string(token.Raw()), nil +} + func (p *Plugin) trackMeeting(args *model.CommandArgs) { // disables tracking if the user is not using the default jitsi url isNotDefaultJitsiURL := p.isNotDefaultJitsiURL() @@ -173,9 +287,41 @@ func (p *Plugin) deleteEphemeralPost(userID, postID string) { } func (p *Plugin) updateJwtUserInfo(jwtToken string, user *model.User) (string, error) { - secret := p.getConfiguration().JitsiAppSecret sanitizedUser := user.DeepCopy() + if p.getConfiguration().UseJaaS { + claims, err := verifyJwtJaaS(jwtToken) + if err != nil { + return "", err + } + + config := p.API.GetConfig() + if config.PrivacySettings.ShowFullName == nil || !*config.PrivacySettings.ShowFullName { + sanitizedUser.FirstName = "" + sanitizedUser.LastName = "" + } + if config.PrivacySettings.ShowEmailAddress == nil || !*config.PrivacySettings.ShowEmailAddress { + sanitizedUser.Email = "" + } + + newContext := JaaSContext{ + User: JaaSUser{ + Avatar: fmt.Sprintf("%s/api/v4/users/%s/image?_=%d", *config.ServiceSettings.SiteURL, sanitizedUser.Id, sanitizedUser.LastPictureUpdate), + Name: sanitizedUser.GetDisplayName(model.SHOW_NICKNAME_FULLNAME), + Email: sanitizedUser.Email, + ID: sanitizedUser.Id, + IsModerator: `true`, + }, + Features: claims.Context.Features, + } + + claims.Context = newContext + + return signClaimsJaaS(p.getConfiguration().JaaSApiKey, p.getConfiguration().JaaSPrivateKey, claims) + } + + secret := p.getConfiguration().JitsiAppSecret + claims, err := verifyJwt(secret, jwtToken) if err != nil { return "", err @@ -204,6 +350,100 @@ func (p *Plugin) updateJwtUserInfo(jwtToken string, user *model.User) (string, e return signClaims(secret, claims) } +func (p *Plugin) generateJaaSJwtForUser(user *model.User) (string, error) { + // User did not specify a jwt, generate a new jwt for user + const ExpTimeDelaySec = 7200 + const _true = "true" + + claims := JaaSClaims{} + claims.Issuer = jaasIssuerClaim + claims.Audience = jaasAudienceClaim + claims.Subject = p.getConfiguration().JaaSAppID + claims.Room = "*" + claims.Exp = time.Now().Unix() + ExpTimeDelaySec + claims.Nbf = time.Now().Unix() + claims.Context.Features.LiveStreaming = _true + claims.Context.Features.Recording = _true + claims.Context.Features.OutboundCall = _true + claims.Context.Features.Transcription = _true + claims.Context.User.Avatar = "" + claims.Context.User.Email = user.Email + claims.Context.User.ID = user.Id + claims.Context.User.IsModerator = _true + claims.Context.User.Name = user.GetFullName() + + var err2 error + var jwtToken string + jwtToken, err2 = signClaimsJaaS(p.getConfiguration().JaaSApiKey, p.getConfiguration().JaaSPrivateKey, &claims) + + // Maybe let the user join as guest...? + return jwtToken, err2 +} + +func (p *Plugin) generateJaaSJwtForGuest(userid string) (string, error) { + const ExpTimeDelaySec = 7200 + const _false = "false" + + guestClaims := JaaSClaims{} + guestClaims.Issuer = jaasIssuerClaim + guestClaims.Audience = jaasAudienceClaim + guestClaims.Subject = p.getConfiguration().JaaSAppID + guestClaims.Room = "*" + guestClaims.Exp = time.Now().Unix() + ExpTimeDelaySec + guestClaims.Nbf = time.Now().Unix() + guestClaims.Context.Features.LiveStreaming = _false + guestClaims.Context.Features.Recording = _false + guestClaims.Context.Features.OutboundCall = _false + guestClaims.Context.Features.Transcription = _false + guestClaims.Context.User.Avatar = "" + guestClaims.Context.User.Email = "" + guestClaims.Context.User.ID = userid + guestClaims.Context.User.IsModerator = _false + guestClaims.Context.User.Name = "" + + jwtToken, err := signClaimsJaaS(p.getConfiguration().JaaSApiKey, p.getConfiguration().JaaSPrivateKey, &guestClaims) + if err != nil { + mlog.Error("Error generating JaaS token for guest", mlog.Err(err)) + return "", errors.New("failed creating new JaaS token for guest") + } + + return jwtToken, err +} + +func (p *Plugin) getJaaSSettings(jwtToken string, path string, user *model.User) (*JaaSSettings, error) { + if user != nil { + claims, err := verifyJwtJaaS(jwtToken) + if err != nil { + jwtToken, err = p.generateJaaSJwtForUser(user) + if err != nil { + mlog.Error("failed to generate new token for user!") + return nil, err + } + + // jwtToken = jwtGen + } else if claims.Context.User.ID != user.Id { + return nil, errors.New("not authorized") + } + } else { + var err error + jwtToken, err = p.generateJaaSJwtForGuest(uuid.New().String() + "-guest") + + if err != nil { + mlog.Error("Error generating JaaS token for guest", mlog.Err(err)) + return nil, errors.New("failed creating new JaaS token for guest") + } + } + + // Get room + roomPath := strings.ReplaceAll(path, "/plugins/jitsi/api/v1/meetings/", "") + + var settings JaaSSettings + settings.Jwt = jwtToken + settings.Room = roomPath + + return &settings, nil +} + func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingID string, meetingTopic string, personal bool, rootID string) (string, error) { l := p.b.GetServerLocalizer() if meetingID == "" { @@ -213,10 +453,16 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI } meetingID += randomString(LETTERS, 20) } + meetingPersonal := false + defaultSelectedMeetingTopic := "Jitsi Meeting" + if p.getConfiguration().UseJaaS { + defaultSelectedMeetingTopic = "JaaS Meeting" + } + defaultMeetingTopic := p.b.LocalizeDefaultMessage(l, &i18n.Message{ ID: "jitsi.start_meeting.default_meeting_topic", - Other: "Jitsi Meeting", + Other: defaultSelectedMeetingTopic, }) if len(meetingTopic) < 1 { @@ -259,44 +505,41 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI meetingID = generateEnglishTitleName() } } + + meetingIDLabel := meetingID + + if p.getConfiguration().UseJaaS { + appID := p.getConfiguration().JaaSAppID + meetingID = appID + "/" + meetingID + } + jitsiURL := strings.TrimSpace(p.getConfiguration().GetJitsiURL()) + + if p.getConfiguration().UseJaaS { + jitsiURL = strings.TrimSpace(*p.API.GetConfig().ServiceSettings.SiteURL + "/plugins/jitsi/api/v1/meetings") + } + jitsiURL = strings.TrimRight(jitsiURL, "/") meetingURL := jitsiURL + "/" + meetingID meetingLink := meetingURL var meetingLinkValidUntil = time.Time{} - JWTMeeting := p.getConfiguration().JitsiJWT + JWTMeeting := p.getConfiguration().JitsiJWT || p.getConfiguration().UseJaaS var jwtToken string - if JWTMeeting { - // Error check is done in configuration.IsValid() - jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) - - meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) + meetingUntil := "" - claims := Claims{} - claims.Issuer = p.getConfiguration().JitsiAppID - claims.Audience = []string{p.getConfiguration().JitsiAppID} - claims.ExpiresAt = jwt.NewNumericDate(meetingLinkValidUntil) - claims.Subject = jURL.Hostname() - claims.Room = meetingID + if p.getConfiguration().UseJaaS { + meetingLinkValidUntil = time.Now().Add(time.Duration(120) * time.Minute) var err2 error - jwtToken, err2 = signClaims(p.getConfiguration().JitsiAppSecret, &claims) + jwtToken, err2 = p.generateJaaSJwtForUser(user) if err2 != nil { return "", err2 } meetingURL = meetingURL + "?jwt=" + jwtToken - } - if meetingTopic == "" { - meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+defaultMeetingTopic+"\"") - } else { - meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+meetingTopic+"\"") - } - meetingUntil := "" - if JWTMeeting { meetingUntil = p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ DefaultMessage: &i18n.Message{ ID: "jitsi.start_meeting.meeting_link_valid_until", @@ -304,6 +547,44 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI }, TemplateData: map[string]string{"Datetime": meetingLinkValidUntil.Format("Mon Jan 2 15:04:05 -0700 MST 2006")}, }) + } else { + if JWTMeeting { + // Error check is done in configuration.IsValid() + jURL, _ := url.Parse(p.getConfiguration().GetJitsiURL()) + + meetingLinkValidUntil = time.Now().Add(time.Duration(p.getConfiguration().JitsiLinkValidTime) * time.Minute) + + claims := Claims{} + claims.Issuer = p.getConfiguration().JitsiAppID + claims.Audience = []string{p.getConfiguration().JitsiAppID} + claims.ExpiresAt = jwt.NewNumericDate(meetingLinkValidUntil) + claims.Subject = jURL.Hostname() + claims.Room = meetingID + + var err2 error + jwtToken, err2 = signClaims(p.getConfiguration().JitsiAppSecret, &claims) + if err2 != nil { + return "", err2 + } + + meetingURL = meetingURL + "?jwt=" + jwtToken + } + + if JWTMeeting { + meetingUntil = p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: "jitsi.start_meeting.meeting_link_valid_until", + Other: "Meeting link valid until: {{.Datetime}}", + }, + TemplateData: map[string]string{"Datetime": meetingLinkValidUntil.Format("Mon Jan 2 15:04:05 -0700 MST 2006")}, + }) + } + } + + if meetingTopic == "" { + meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+defaultMeetingTopic+"\"") + } else { + meetingURL = meetingURL + "#config.callDisplayName=" + url.PathEscape("\""+meetingTopic+"\"") } meetingTypeString := p.b.LocalizeWithConfig(l, &i18n.LocalizeConfig{ @@ -369,6 +650,8 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI "meeting_personal": meetingPersonal, "meeting_topic": meetingTopic, "default_meeting_topic": defaultMeetingTopic, + "jaas_meeting": p.getConfiguration().UseJaaS, + "meeting_id_label": meetingIDLabel, }, RootId: rootID, } diff --git a/server/plugin_test.go b/server/plugin_test.go index 7e8019de..04f47d3c 100644 --- a/server/plugin_test.go +++ b/server/plugin_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/cristalhq/jwt/v2" + "github.com/cristalhq/jwt/v3" "github.com/mattermost/mattermost-plugin-api/i18n" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin/plugintest" @@ -51,7 +51,9 @@ func TestSignClaims(t *testing.T) { func TestStartMeeting(t *testing.T) { p := Plugin{ configuration: &configuration{ - JitsiURL: "http://test", + JitsiSettings: jitsisettings{ + JitsiURL: "http://test", + }, }, } apiMock := plugintest.API{} diff --git a/webapp/.eslintrc.json b/webapp/.eslintrc.json index 2b146c9d..f21c0a84 100644 --- a/webapp/.eslintrc.json +++ b/webapp/.eslintrc.json @@ -85,7 +85,7 @@ "no-compare-neg-zero": 2, "no-cond-assign": [2, "except-parens"], "no-confusing-arrow": 2, - "no-console": 2, + "no-console": 0, "no-const-assign": 2, "no-constant-condition": 2, "no-debugger": 2, diff --git a/webapp/i18n/de.json b/webapp/i18n/de.json index aa77a834..44b06c37 100644 --- a/webapp/i18n/de.json +++ b/webapp/i18n/de.json @@ -2,6 +2,7 @@ "jitsi.close": "Schließen", "jitsi.creator-has-started-a-meeting": "{creator} hat ein Meeting gestartet", "jitsi.default-title": "Jitsi Meeting", + "jaas.default-title": "JaaS Meeting", "jitsi.join-meeting": "Tritt Meeting bei", "jitsi.link-valid-until": "Meeting-Link gültig bis: ", "jitsi.maximize": "Maximieren", diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 7b1ca538..9f298c07 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -2,6 +2,7 @@ "jitsi.close": "Close", "jitsi.creator-has-started-a-meeting": "{creator} has started a meeting", "jitsi.default-title": "Jitsi Meeting", + "jaas.default-title": "JaaS Meeting", "jitsi.join-meeting": "JOIN MEETING", "jitsi.link-valid-until": "Meeting link valid until: ", "jitsi.maximize": "Maximize", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index 3d25637a..420ebe82 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -2,6 +2,7 @@ "jitsi.close": "Cerrar", "jitsi.creator-has-started-a-meeting": "{creator} ha iniciado una reunión", "jitsi.default-title": "Reunión Jitsi", + "jaas.default-title": "Reunión JaaS", "jitsi.join-meeting": "UNIRSE A LA REUNIÓN", "jitsi.link-valid-until": "El enlace a la reunión es válido hasta: ", "jitsi.maximize": "Maximizar", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index 97af039f..443a97c7 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -2,6 +2,7 @@ "jitsi.close": "Fermer", "jitsi.creator-has-started-a-meeting": "{creator} a démarré une réunion", "jitsi.default-title": "Réunion Jitsi", + "jaas.default-title": "Réunion JaaS", "jitsi.join-meeting": "REJOINDRE LA RÉUNION", "jitsi.link-valid-until": "Le lien de la réunion est valide jusqu'au : ", "jitsi.maximize": "Maximiser", diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json index ab2e392f..292a2b43 100644 --- a/webapp/i18n/ru.json +++ b/webapp/i18n/ru.json @@ -2,6 +2,7 @@ "jitsi.close": "Закрыть", "jitsi.creator-has-started-a-meeting": "{creator} начал совещание", "jitsi.default-title": "Совещание Jitsi", + "jaas.default-title": "Совещание JaaS", "jitsi.join-meeting": "ПРИСОЕДИНИТЬСЯ", "jitsi.link-valid-until": "Ссылка на совещание действительна до: ", "jitsi.maximize": "Развернуть", diff --git a/webapp/package.json b/webapp/package.json index 226b062d..b6f3344d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -20,6 +20,7 @@ "@types/enzyme": "3.10.5", "@types/jest": "25.2.3", "@types/node": "14.0.5", + "@types/prop-types": "15.7.3", "@types/react": "16.9.35", "@types/react-dom": "16.9.8", "@types/react-redux": "7.1.9", @@ -35,6 +36,7 @@ "eslint-plugin-import": "2.20.2", "eslint-plugin-react": "7.20.0", "file-loader": "6.0.0", + "html-webpack-plugin": "4.5.2", "identity-obj-proxy": "3.0.0", "jest": "26.0.1", "jest-canvas-mock": "2.2.0", @@ -48,12 +50,15 @@ "webpack-cli": "3.3.11" }, "dependencies": { + "@types/html-webpack-plugin": "3.2.4", "core-js": "3.6.5", "mattermost-redux": "5.23.0", + "prop-types": "15.7.2", "react": "16.13.1", "react-intl": "4.6.3", "react-redux": "7.2.0", - "redux": "4.0.5" + "redux": "4.0.5", + "redux-thunk": "2.3.0" }, "jest": { "preset": "ts-jest", diff --git a/webapp/src/components/admin_settings/jaas_section.tsx b/webapp/src/components/admin_settings/jaas_section.tsx new file mode 100644 index 00000000..b448d61d --- /dev/null +++ b/webapp/src/components/admin_settings/jaas_section.tsx @@ -0,0 +1,118 @@ +import * as React from 'react'; + +type Props = { + disabled: boolean, + onAppIDChange: any, + onApiKeyChange: any, + onPrivateKeyChange: any, + onEmbeddedChange: any, + apiKey: string, + appID: string, + privateKey: string + embedded: boolean +}; + +export default class JaaSSection extends React.Component { + constructor(props: any) { + super(props); + console.log('JaaSSection'); + } + + render() { + // TODO replace with common components + return ( +
+
+ +
+ + +
+ + {'(Experimental) When true, JaaS video is embedded as a floating window inside Mattermost by default.'} + +
+
+
+
+ +
+ +
+ + {'Specify your JaaS AppID. You can get the AppID from https://jaas.8x8.vc/#/apikeys .'} + +
+
+
+
+ +
+ +
+ + {'Specify your JaaS Api Key. You can get the Api Key from https://jaas.8x8.vc/#/apikeys .'} + +
+
+
+
+ +
+