Merge pull request #693 from jitsi/jwt_support

JWT client support
This commit is contained in:
Дамян Минков 2016-06-14 14:38:36 -05:00 committed by GitHub
commit 334f7bf95a
28 changed files with 681 additions and 137 deletions

View File

@ -3,7 +3,7 @@ BROWSERIFY = ./node_modules/.bin/browserify
UGLIFYJS = ./node_modules/.bin/uglifyjs UGLIFYJS = ./node_modules/.bin/uglifyjs
EXORCIST = ./node_modules/.bin/exorcist EXORCIST = ./node_modules/.bin/exorcist
CLEANCSS = ./node_modules/.bin/cleancss CLEANCSS = ./node_modules/.bin/cleancss
CSS_FILES = font.css toastr.css main.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css CSS_FILES = font.css toastr.css main.css overlay.css videolayout_default.css font-awesome.css jquery-impromptu.css modaldialog.css notice.css popup_menu.css login_menu.css popover.css jitsi_popover.css contact_list.css chat.css welcome_page.css settingsmenu.css feedback.css jquery.contextMenu.css
DEPLOY_DIR = libs DEPLOY_DIR = libs
BROWSERIFY_FLAGS = -d BROWSERIFY_FLAGS = -d
OUTPUT_DIR = . OUTPUT_DIR = .

22
app.js
View File

@ -23,6 +23,7 @@ import conference from './conference';
import API from './modules/API/API'; import API from './modules/API/API';
import UIEvents from './service/UI/UIEvents'; import UIEvents from './service/UI/UIEvents';
import getTokenData from "./modules/TokenData/TokenData";
/** /**
* Tries to push history state with the following parameters: * Tries to push history state with the following parameters:
@ -84,10 +85,24 @@ const APP = {
require("./modules/keyboardshortcut/keyboardshortcut"); require("./modules/keyboardshortcut/keyboardshortcut");
this.translation = require("./modules/translation/translation"); this.translation = require("./modules/translation/translation");
this.configFetch = require("./modules/config/HttpConfigFetch"); this.configFetch = require("./modules/config/HttpConfigFetch");
this.tokenData = getTokenData();
} }
}; };
/**
* If JWT token data it will be used for local user settings
*/
function setTokenData() {
let localUser = APP.tokenData.caller;
if(localUser) {
APP.settings.setEmail((localUser.getEmail() || "").trim());
APP.settings.setAvatarUrl((localUser.getAvatarUrl() || "").trim());
APP.settings.setDisplayName((localUser.getName() || "").trim());
}
}
function init() { function init() {
setTokenData();
var isUIReady = APP.UI.start(); var isUIReady = APP.UI.start();
if (isUIReady) { if (isUIReady) {
APP.conference.init({roomName: buildRoomName()}).then(function () { APP.conference.init({roomName: buildRoomName()}).then(function () {
@ -100,6 +115,11 @@ function init() {
APP.keyboardshortcut.init(); APP.keyboardshortcut.init();
}).catch(function (err) { }).catch(function (err) {
APP.UI.hideRingOverLay();
APP.API.sendPostisMessage({
method: 'video-conference-left',
params: {roomName: APP.conference.roomName}
});
console.error(err); console.error(err);
}); });
} }
@ -151,7 +171,7 @@ $(document).ready(function () {
APP.translation.init(settings.getLanguage()); APP.translation.init(settings.getLanguage());
APP.API.init(); APP.API.init(APP.tokenData.externalAPISettings);
obtainConfigAndInit(); obtainConfigAndInit();
}); });

View File

@ -26,6 +26,18 @@ let currentAudioInputDevices, currentVideoInputDevices;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo"; import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
/**
* Known custom conference commands.
*/
const commands = {
CONNECTION_QUALITY: "stats",
EMAIL: "email",
AVATAR_URL: "avatar-url",
ETHERPAD: "etherpad",
SHARED_VIDEO: "shared-video",
CUSTOM_ROLE: "custom-role"
};
/** /**
* Open Connection. When authentication failed it shows auth dialog. * Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use * @param roomName the room name to use
@ -44,17 +56,13 @@ function connect(roomName) {
} }
/** /**
* Share email with other users. * Share data to other users.
* @param emailCommand the email command * @param command the command
* @param {string} email new email * @param {string} value new value
*/ */
function sendEmail (emailCommand, email) { function sendData (command, value) {
room.sendCommand(emailCommand, { room.removeCommand(command);
value: email, room.sendCommand(command, {value: value});
attributes: {
id: room.myUserId()
}
});
} }
/** /**
@ -144,7 +152,12 @@ function maybeRedirectToWelcomePage() {
* @returns Promise. * @returns Promise.
*/ */
function disconnectAndShowFeedback(requestFeedback) { function disconnectAndShowFeedback(requestFeedback) {
APP.UI.hideRingOverLay();
connection.disconnect(); connection.disconnect();
APP.API.sendPostisMessage({
method: 'video-conference-left',
params: {roomName: APP.conference.roomName}
});
if (requestFeedback) { if (requestFeedback) {
return APP.UI.requestFeedback(); return APP.UI.requestFeedback();
} else { } else {
@ -209,6 +222,55 @@ function setCurrentMediaDevices(devices) {
d => d.kind === 'videoinput'); d => d.kind === 'videoinput');
} }
/**
* Changes the email for the local user
* @param email {string} the new email
*/
function changeLocalEmail(email = '') {
email = email.trim();
if (email === APP.settings.getEmail()) {
return;
}
APP.settings.setEmail(email);
APP.UI.setUserEmail(room.myUserId(), email);
sendData(commands.EMAIL, email);
}
/**
* Changes the local avatar url for the local user
* @param avatarUrl {string} the new avatar url
*/
function changeLocalAvatarUrl(avatarUrl = '') {
avatarUrl = avatarUrl.trim();
if (avatarUrl === APP.settings.getAvatarUrl()) {
return;
}
APP.settings.setAvatarUrl(avatarUrl);
APP.UI.setUserAvatarUrl(room.myUserId(), avatarUrl);
sendData(commands.AVATAR_URL, avatarUrl);
}
/**
* Changes the display name for the local user
* @param nickname {string} the new display name
*/
function changeLocalDisplayName(nickname = '') {
nickname = nickname.trim();
if (nickname === APP.settings.getDisplayName()) {
return;
}
APP.settings.setDisplayName(nickname);
room.setDisplayName(nickname);
APP.UI.changeDisplayName(APP.conference.localId, nickname);
}
class ConferenceConnector { class ConferenceConnector {
constructor(resolve, reject) { constructor(resolve, reject) {
this._resolve = resolve; this._resolve = resolve;
@ -227,6 +289,7 @@ class ConferenceConnector {
} }
_onConferenceFailed(err, ...params) { _onConferenceFailed(err, ...params) {
console.error('CONFERENCE FAILED:', err, ...params); console.error('CONFERENCE FAILED:', err, ...params);
APP.UI.hideRingOverLay();
switch (err) { switch (err) {
// room is locked by the password // room is locked by the password
case ConferenceErrors.PASSWORD_REQUIRED: case ConferenceErrors.PASSWORD_REQUIRED:
@ -412,6 +475,9 @@ export default {
this._createRoom(tracks); this._createRoom(tracks);
this.isDesktopSharingEnabled = this.isDesktopSharingEnabled =
JitsiMeetJS.isDesktopSharingEnabled(); JitsiMeetJS.isDesktopSharingEnabled();
if(this.isDesktopSharingEnabled)
APP.API.addPostisMessageListener('toggle-share-screen',
() => this.toggleScreenSharing());
// if user didn't give access to mic or camera or doesn't have // if user didn't give access to mic or camera or doesn't have
// them at all, we disable corresponding toolbar buttons // them at all, we disable corresponding toolbar buttons
@ -851,13 +917,7 @@ export default {
/** /**
* Known custom conference commands. * Known custom conference commands.
*/ */
defaults: { defaults: commands,
CONNECTION_QUALITY: "stats",
EMAIL: "email",
ETHERPAD: "etherpad",
SHARED_VIDEO: "shared-video",
CUSTOM_ROLE: "custom-role"
},
/** /**
* Receives notifications from other participants about commands aka * Receives notifications from other participants about commands aka
* custom events (sent by sendCommand or sendCommandOnce methods). * custom events (sent by sendCommand or sendCommandOnce methods).
@ -910,7 +970,11 @@ export default {
this._room = room; // FIXME do not use this this._room = room; // FIXME do not use this
let email = APP.settings.getEmail(); let email = APP.settings.getEmail();
email && sendEmail(this.commands.defaults.EMAIL, email); email && sendData(this.commands.defaults.EMAIL, email);
let avatarUrl = APP.settings.getAvatarUrl();
avatarUrl && sendData(this.commands.defaults.AVATAR_URL,
avatarUrl);
let nick = APP.settings.getDisplayName(); let nick = APP.settings.getDisplayName();
if (config.useNicks && !nick) { if (config.useNicks && !nick) {
@ -1093,6 +1157,10 @@ export default {
// add local streams when joined to the conference // add local streams when joined to the conference
room.on(ConferenceEvents.CONFERENCE_JOINED, () => { room.on(ConferenceEvents.CONFERENCE_JOINED, () => {
APP.UI.mucJoined(); APP.UI.mucJoined();
APP.API.sendPostisMessage({
method: 'video-conference-joined',
params: {roomName: APP.conference.roomName}
});
}); });
room.on( room.on(
@ -1297,33 +1365,20 @@ export default {
APP.UI.initEtherpad(value); APP.UI.initEtherpad(value);
}); });
APP.UI.addListener(UIEvents.EMAIL_CHANGED, (email = '') => { APP.UI.addListener(UIEvents.EMAIL_CHANGED, changeLocalEmail);
email = email.trim(); room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
APP.UI.setUserEmail(from, data.value);
if (email === APP.settings.getEmail()) {
return;
}
APP.settings.setEmail(email);
APP.UI.setUserAvatar(room.myUserId(), email);
sendEmail(this.commands.defaults.EMAIL, email);
});
room.addCommandListener(this.commands.defaults.EMAIL, (data) => {
APP.UI.setUserAvatar(data.attributes.id, data.value);
}); });
APP.UI.addListener(UIEvents.NICKNAME_CHANGED, (nickname = '') => { APP.UI.addListener(UIEvents.AVATAR_URL_CHANGED, changeLocalAvatarUrl);
nickname = nickname.trim();
if (nickname === APP.settings.getDisplayName()) { room.addCommandListener(this.commands.defaults.AVATAR_URL,
return; (data, from) => {
} APP.UI.setUserAvatarUrl(from, data.value);
APP.settings.setDisplayName(nickname);
room.setDisplayName(nickname);
APP.UI.changeDisplayName(APP.conference.localId, nickname);
}); });
APP.UI.addListener(UIEvents.NICKNAME_CHANGED, changeLocalDisplayName);
APP.UI.addListener(UIEvents.START_MUTED_CHANGED, APP.UI.addListener(UIEvents.START_MUTED_CHANGED,
(startAudioMuted, startVideoMuted) => { (startAudioMuted, startVideoMuted) => {
room.setStartMutedPolicy({ room.setStartMutedPolicy({

View File

@ -16,12 +16,13 @@
* Executes createConnectionExternally function. * Executes createConnectionExternally function.
*/ */
(function () { (function () {
var params = getConfigParamsFromUrl(); var hashParams = getConfigParamsFromUrl("hash", true);
var searchParams = getConfigParamsFromUrl("search", true);
//Url params have higher proirity than config params //Url params have higher proirity than config params
var url = config.externalConnectUrl; var url = config.externalConnectUrl;
if(params.hasOwnProperty('config.externalConnectUrl')) if(hashParams.hasOwnProperty('config.externalConnectUrl'))
url = params["config.externalConnectUrl"]; url = hashParams["config.externalConnectUrl"];
/** /**
* Check if connect from connection.js was executed and executes the handler * Check if connect from connection.js was executed and executes the handler
@ -57,7 +58,8 @@
url += "?room=" + room_name; url += "?room=" + room_name;
var token = params["config.token"] || config.token; var token = hashParams["config.token"] || config.token ||
searchParams.jwt;
if(token) if(token)
url += "&token=" + token; url += "&token=" + token;

View File

@ -33,6 +33,10 @@ html, body{
display:none; display:none;
} }
#header_container {
z-index: 1014;
}
.toolbar_span { .toolbar_span {
display: inline-block; display: inline-block;
position: relative; position: relative;

51
css/overlay.css Normal file
View File

@ -0,0 +1,51 @@
.overlay {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1013;
background: #000000; /* Old browsers */
opacity: 0.75;
display: block;
}
.overlay_container {
width: 100%;
height: 100%;
position: fixed;
z-index: 1013;
}
.overlay_content {
color: #fff;
font-weight: normal;
font-size: 20px;
text-align: center;
width: 400px;
height: 250px;
top: 50%;
left: 50%;
position:absolute;
margin-top: -125px;
margin-left: -200px;
}
.overlay_avatar {
width: 200px;
height: 200px;
position: relative;
border-radius: 100px;
z-index: 1013;
float: left;
margin-left: 100px;
}
.overlay_text {
position: relative;
width: 400px;
z-index: 1013;
margin-top: 20px;
float: left;
}

View File

@ -2,7 +2,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1010; z-index: 1015;
display: none; display: none;
max-width: 300px; max-width: 300px;
min-width: 100px; min-width: 100px;
@ -121,4 +121,4 @@
border-right-width: 0; border-right-width: 0;
border-left-color: #ffffff; border-left-color: #ffffff;
bottom: -10px; bottom: -10px;
} }

View File

@ -84,7 +84,7 @@ button.toast-close-button {
} }
#toast-container { #toast-container {
position: fixed; position: fixed;
z-index: 999999; z-index: 1012;
/*overrides*/ /*overrides*/
} }

View File

@ -468,7 +468,7 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
top:50%; top:50%;
z-index: 10000; z-index: 1011;
font-weight: 600; font-weight: 600;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
@ -488,7 +488,7 @@
background: rgba(0,0,0,.5); background: rgba(0,0,0,.5);
padding: 10px; padding: 10px;
color: rgba(255,255,255,.5); color: rgba(255,255,255,.5);
z-index: 10000; z-index: 1011;
} }
.centeredVideoLabel { .centeredVideoLabel {
@ -506,7 +506,7 @@
margin-left: auto; margin-left: auto;
background: rgba(0,0,0,.5); background: rgba(0,0,0,.5);
color: #FFF; color: #FFF;
z-index: 10000; z-index: 1011;
border-radius: 2px; border-radius: 2px;
-webkit-transition: all 2s 2s linear; -webkit-transition: all 2s 2s linear;
transition: all 2s 2s linear; transition: all 2s 2s linear;
@ -522,4 +522,4 @@
} }
.hidden { .hidden {
} }

View File

@ -67,8 +67,8 @@ case "$1" in
sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG
# Install luajwt # Install luajwt
if ! luarocks install luajwt; then if ! luarocks install jwt; then
echo "Failed to install luajwt - try installing it manually" echo "Failed to install jwt - try installing it manually"
fi fi
if [ -x "/etc/init.d/prosody" ]; then if [ -x "/etc/init.d/prosody" ]; then
@ -85,7 +85,7 @@ case "$1" in
else else
echo "Prosody config not found at $PROSODY_HOST_CONFIG - unable to auto-configure token authentication" echo "Prosody config not found at $PROSODY_HOST_CONFIG - unable to auto-configure token authentication"
fi fi
;; ;;
abort-upgrade|abort-remove|abort-deconfigure) abort-upgrade|abort-remove|abort-deconfigure)

View File

@ -241,6 +241,7 @@
<div class="arrow-up"></div> <div class="arrow-up"></div>
<input type="text" id="setDisplayName" data-i18n="[placeholder]settings.name" placeholder="Name"> <input type="text" id="setDisplayName" data-i18n="[placeholder]settings.name" placeholder="Name">
<input type="text" id="setEmail" placeholder="E-Mail"> <input type="text" id="setEmail" placeholder="E-Mail">
<input type="text" id="setAvatarUrl" placeholder="Avatar URL" data-i18n="[placeholder]settings.avatar">
<select id="languages_selectbox"></select> <select id="languages_selectbox"></select>
<div id = "startMutedOptions"> <div id = "startMutedOptions">
<label class = "startMutedLabel"> <label class = "startMutedLabel">

View File

@ -10,6 +10,7 @@
"speaker": "Speaker", "speaker": "Speaker",
"defaultNickname": "ex. Jane Pink", "defaultNickname": "ex. Jane Pink",
"defaultLink": "e.g. __url__", "defaultLink": "e.g. __url__",
"calling": "Calling __name__ ...",
"welcomepage":{ "welcomepage":{
"go": "GO", "go": "GO",
"roomname": "Enter room name", "roomname": "Enter room name",
@ -94,7 +95,8 @@
"selectAudioOutput": "Select audio output", "selectAudioOutput": "Select audio output",
"followMe": "Enable follow me", "followMe": "Enable follow me",
"noDevice": "None", "noDevice": "None",
"noPermission": "Permission to use device is not granted" "noPermission": "Permission to use device is not granted",
"avatarUrl": "Avatar URL"
}, },
"videothumbnail": "videothumbnail":
{ {

View File

@ -5,6 +5,8 @@
* applications that embed Jitsi Meet * applications that embed Jitsi Meet
*/ */
import postis from 'postis';
/** /**
* List of the available commands. * List of the available commands.
* @type {{ * @type {{
@ -16,7 +18,28 @@
* toggleContactList: toggleContactList * toggleContactList: toggleContactList
* }} * }}
*/ */
var commands = {}; let commands = {};
/**
* Object that will execute sendMessage
*/
let target = window.opener ? window.opener : window.parent;
/**
* Array of functions that are going to receive the objects passed to this
* window
*/
let messageListeners = [];
/**
* Current status (enabled/disabled) of Postis.
*/
let enablePostis = false;
/**
* Current status (enabled/disabled) of Post Message API.
*/
let enablePostMessage = false;
function initCommands() { function initCommands() {
commands = { commands = {
@ -91,14 +114,6 @@ function processEvent(event) {
} }
} }
/**
* Sends message to the external application.
* @param object
*/
function sendMessage(object) {
window.parent.postMessage(JSON.stringify(object), "*");
}
/** /**
* Processes a message event from the external application * Processes a message event from the external application
* @param event the message event * @param event the message event
@ -107,10 +122,11 @@ function processMessage(event) {
var message; var message;
try { try {
message = JSON.parse(event.data); message = JSON.parse(event.data);
} catch (e) {} } catch (e) {
console.error("Cannot parse data", event.data);
if(!message.type)
return; return;
}
switch (message.type) { switch (message.type) {
case "command": case "command":
processCommand(message); processCommand(message);
@ -119,18 +135,26 @@ function processMessage(event) {
processEvent(message); processEvent(message);
break; break;
default: default:
console.error("Unknown type of the message"); console.warn("Unknown message type");
return;
} }
} }
/**
* Sends message to the external application.
* @param object {object} the object that will be sent as JSON string
*/
function sendMessage(object) {
if(enablePostMessage)
target.postMessage(JSON.stringify(object), "*");
}
/** /**
* Check whether the API should be enabled or not. * Check whether the API should be enabled or not.
* @returns {boolean} * @returns {boolean}
*/ */
function isEnabled () { function isEnabled () {
let hash = location.hash; let hash = location.hash;
return hash && hash.indexOf("external=true") > -1 && window.postMessage; return !!(hash && hash.indexOf("external=true") > -1 && window.postMessage);
} }
/** /**
@ -149,7 +173,7 @@ function isEventEnabled (name) {
* @param object data associated with the event * @param object data associated with the event
*/ */
function triggerEvent (name, object) { function triggerEvent (name, object) {
if (isEnabled() && isEventEnabled(name)) { if (isEventEnabled(name) && enablePostMessage) {
sendMessage({ sendMessage({
type: "event", type: "event",
action: "result", action: "result",
@ -165,18 +189,34 @@ export default {
* receive information from external applications that embed Jitsi Meet. * receive information from external applications that embed Jitsi Meet.
* It also sends a message to the external application that APIConnector * It also sends a message to the external application that APIConnector
* is initialized. * is initialized.
* @param options {object}
* @param enablePostis {boolean} if true the postis npm
* package for comminication with the parent window will be enabled.
* @param enablePostMessage {boolean} if true the postMessageAPI for
* comminication with the parent window will be enabled.
*/ */
init: function () { init: function (options = {}) {
if (!isEnabled()) { options.enablePostMessage = options.enablePostMessage || isEnabled();
if (!options.enablePostis &&
!options.enablePostMessage) {
return; return;
} }
initCommands(); enablePostis = options.enablePostis;
if (window.addEventListener) { enablePostMessage = options.enablePostMessage;
window.addEventListener('message', processMessage, false);
} else { if(enablePostMessage) {
window.attachEvent('onmessage', processMessage); initCommands();
if (window.addEventListener) {
window.addEventListener('message', processMessage, false);
} else {
window.attachEvent('onmessage', processMessage);
}
sendMessage({type: "system", loaded: true});
}
if(enablePostis) {
this.postis = postis({window: target});
} }
sendMessage({type: "system", loaded: true});
}, },
/** /**
@ -187,6 +227,27 @@ export default {
triggerEvent("outgoingMessage", {"message": body}); triggerEvent("outgoingMessage", {"message": body});
}, },
/**
* Sends message to the external application.
* @param options {object}
* @param method {string}
* @param params {object} the object that will be sent as JSON string
*/
sendPostisMessage(options) {
if(enablePostis)
this.postis.send(options);
},
/**
* Adds listener for Postis messages.
* @param method {string} postis mehtod
* @param listener {function}
*/
addPostisMessageListener (method, listener) {
if(enablePostis)
this.postis.listen(method, listener);
},
/** /**
* Notify external application (if API is enabled) that * Notify external application (if API is enabled) that
* message was received. * message was received.
@ -238,14 +299,14 @@ export default {
* Removes the listeners. * Removes the listeners.
*/ */
dispose: function () { dispose: function () {
if (!isEnabled()) { if (enablePostMessage) {
return; if (window.removeEventListener) {
} window.removeEventListener("message", processMessage, false);
} else {
if (window.removeEventListener) { window.detachEvent('onmessage', processMessage);
window.removeEventListener("message", processMessage, false); }
} else {
window.detachEvent('onmessage', processMessage);
} }
if(enablePostis)
this.postis.destroy();
} }
}; };

View File

@ -0,0 +1,120 @@
/* global getConfigParamsFromUrl, config */
/**
* Parses and handles JWT tokens. Sets config.token.
*/
import * as jws from "jws";
/**
* Get the JWT token from the URL.
*/
let params = getConfigParamsFromUrl("search", true);
let jwt = params.jwt;
/**
* Implements a user of conference.
*/
class User {
/**
* @param name {string} the name of the user.
* @param email {string} the email of the user.
* @param avatarUrl {string} the URL for the avatar of the user.
*/
constructor(name, email, avatarUrl) {
this._name = name;
this._email = email;
this._avatarUrl = avatarUrl;
}
/**
* GETERS START.
*/
/**
* Returns the name property
*/
getName() {
return this._name;
}
/**
* Returns the email property
*/
getEmail() {
return this._email;
}
/**
* Returns the URL of the avatar
*/
getAvatarUrl() {
return this._avatarUrl;
}
/**
* GETERS END.
*/
}
/**
* Represent the data parsed from the JWT token
*/
class TokenData{
/**
* @param {string} the JWT token
*/
constructor(jwt) {
if(!jwt)
return;
this.jwt = jwt;
//External API settings
this.externalAPISettings = {
enablePostis: true
};
this._decode();
// Use JWT param as token if there is not other token set and if the
// iss field is not anonymous. If you want to pass data with JWT token
// but you don't want to pass the JWT token for verification the iss
// field should be set to "anonymous"
if(!config.token && this.payload && this.payload.iss !== "anonymous")
config.token = jwt;
}
/**
* Decodes the JWT token and sets the decoded data to properties.
*/
_decode() {
this.decodedJWT = jws.decode(jwt);
if(!this.decodedJWT || !this.decodedJWT.payload)
return;
this.payload = this.decodedJWT.payload;
if(!this.payload.context)
return;
let callerData = this.payload.context.user;
let calleeData = this.payload.context.callee;
if(callerData)
this.caller = new User(callerData.name, callerData.email,
callerData.avatarUrl);
if(calleeData)
this.callee = new User(calleeData.name, calleeData.email,
calleeData.avatarUrl);
}
}
/**
* Stores the TokenData instance.
*/
let data = null;
/**
* Returns the data variable. Creates new TokenData instance if <tt>data</tt>
* variable is null.
*/
export default function getTokenData() {
if(!data)
data = new TokenData(jwt);
return data;
}

View File

@ -21,6 +21,7 @@ import FilmStrip from "./videolayout/FilmStrip";
import SettingsMenu from "./side_pannels/settings/SettingsMenu"; import SettingsMenu from "./side_pannels/settings/SettingsMenu";
import Settings from "./../settings/Settings"; import Settings from "./../settings/Settings";
import { reload } from '../util/helpers'; import { reload } from '../util/helpers';
import RingOverlay from "./ring_overlay/RingOverlay";
var EventEmitter = require("events"); var EventEmitter = require("events");
UI.messageHandler = require("./util/MessageHandler"); UI.messageHandler = require("./util/MessageHandler");
@ -277,7 +278,7 @@ UI.initConference = function () {
} }
// Make sure we configure our avatar id, before creating avatar for us // Make sure we configure our avatar id, before creating avatar for us
UI.setUserAvatar(id, Settings.getEmail()); UI.setUserEmail(id, Settings.getEmail());
Toolbar.checkAutoEnableDesktopSharing(); Toolbar.checkAutoEnableDesktopSharing();
@ -388,6 +389,7 @@ UI.start = function () {
registerListeners(); registerListeners();
ToolbarToggler.init();
BottomToolbar.init(); BottomToolbar.init();
FilmStrip.init(eventEmitter); FilmStrip.init(eventEmitter);
@ -470,6 +472,10 @@ UI.start = function () {
SettingsMenu.init(eventEmitter); SettingsMenu.init(eventEmitter);
} }
if(APP.tokenData.callee) {
UI.showRingOverLay();
}
// Return true to indicate that the UI has been fully started and // Return true to indicate that the UI has been fully started and
// conference ready. // conference ready.
return true; return true;
@ -551,6 +557,7 @@ UI.getSharedDocumentManager = function () {
* @param {string} displayName user nickname * @param {string} displayName user nickname
*/ */
UI.addUser = function (id, displayName) { UI.addUser = function (id, displayName) {
UI.hideRingOverLay();
ContactList.addContact(id); ContactList.addContact(id);
messageHandler.notify( messageHandler.notify(
@ -565,7 +572,7 @@ UI.addUser = function (id, displayName) {
VideoLayout.addParticipantContainer(id); VideoLayout.addParticipantContainer(id);
// Configure avatar // Configure avatar
UI.setUserAvatar(id); UI.setUserEmail(id);
// set initial display name // set initial display name
if(displayName) if(displayName)
@ -800,21 +807,41 @@ UI.dockToolbar = function (isDock) {
}; };
/** /**
* Update user avatar. * Updates the avatar for participant.
* @param {string} id user id * @param {string} id user id
* @param {stirng} email user email * @param {stirng} avatarUrl the URL for the avatar
*/ */
UI.setUserAvatar = function (id, email) { function changeAvatar(id, avatarUrl) {
// update avatar
Avatar.setUserAvatar(id, email);
var avatarUrl = Avatar.getAvatarUrl(id);
VideoLayout.changeUserAvatar(id, avatarUrl); VideoLayout.changeUserAvatar(id, avatarUrl);
ContactList.changeUserAvatar(id, avatarUrl); ContactList.changeUserAvatar(id, avatarUrl);
if (APP.conference.isLocalId(id)) { if (APP.conference.isLocalId(id)) {
SettingsMenu.changeAvatar(avatarUrl); SettingsMenu.changeAvatar(avatarUrl);
} }
}
/**
* Update user email.
* @param {string} id user id
* @param {stirng} email user email
*/
UI.setUserEmail = function (id, email) {
// update avatar
Avatar.setUserEmail(id, email);
changeAvatar(id, Avatar.getAvatarUrl(id));
};
/**
* Update user avatar URL.
* @param {string} id user id
* @param {stirng} url user avatar url
*/
UI.setUserAvatarUrl = function (id, url) {
// update avatar
Avatar.setUserAvatarUrl(id, url);
changeAvatar(id, Avatar.getAvatarUrl(id));
}; };
/** /**
@ -1216,7 +1243,7 @@ UI.showDeviceErrorDialog = function (micError, cameraError) {
: ``; : ``;
let doNotShowWarningAgainSection = showDoNotShowWarning let doNotShowWarningAgainSection = showDoNotShowWarning
? `<label> ? `<label>
<input type='checkbox' id='doNotShowWarningAgain'> <input type='checkbox' id='doNotShowWarningAgain'>
<span data-i18n='dialog.doNotShowWarningAgain'></span> <span data-i18n='dialog.doNotShowWarningAgain'></span>
</label>` </label>`
: ``; : ``;
@ -1343,4 +1370,19 @@ UI.enableMicrophoneButton = function () {
Toolbar.markAudioIconAsDisabled(false); Toolbar.markAudioIconAsDisabled(false);
}; };
let bottomToolbarEnabled = null;
UI.showRingOverLay = function () {
RingOverlay.show(APP.tokenData.callee);
ToolbarToggler.setAlwaysVisibleToolbar(true);
FilmStrip.toggleFilmStrip(false);
};
UI.hideRingOverLay = function () {
if(!RingOverlay.hide())
return;
ToolbarToggler.resetAlwaysVisibleToolbar();
FilmStrip.toggleFilmStrip(true);
};
module.exports = UI; module.exports = UI;

View File

@ -3,6 +3,19 @@
let users = {}; let users = {};
export default { export default {
/**
* Sets prop in users object.
* @param id {string} user id
* @param prop {string} name of the prop
* @param val {string} value to be set
*/
_setUserProp: function (id, prop, val) {
if(!val || (users[id] && users[id][prop] === val))
return;
if(!users[id])
users[id] = {};
users[id][prop] = val;
},
/** /**
* Sets the user's avatar in the settings menu(if local user), contact list * Sets the user's avatar in the settings menu(if local user), contact list
@ -10,13 +23,18 @@ export default {
* @param id id of the user * @param id id of the user
* @param email email or nickname to be used as a hash * @param email email or nickname to be used as a hash
*/ */
setUserAvatar: function (id, email) { setUserEmail: function (id, email) {
if (email) { this._setUserProp(id, "email", email);
if (users[id] === email) { },
return;
} /**
users[id] = email; * Sets the user's avatar in the settings menu(if local user), contact list
} * and thumbnail
* @param id id of the user
* @param url the url for the avatar
*/
setUserAvatarUrl: function (id, url) {
this._setUserProp(id, "url", url);
}, },
/** /**
@ -34,7 +52,15 @@ export default {
return null; return null;
} }
let avatarId = users[userId]; let avatarId = null;
const user = users[userId];
if(user) {
if(user.url)
return users[userId].url;
avatarId = users[userId].email;
}
// If the ID looks like an email, we'll use gravatar. // If the ID looks like an email, we'll use gravatar.
// Otherwise, it's a random avatar, and we'll use the configured // Otherwise, it's a random avatar, and we'll use the configured

View File

@ -0,0 +1,82 @@
/* global $ */
/**
* Shows ring overlay
*/
class RingOverlay {
/**
* @param callee instance of User class from TokenData.js
*/
constructor(callee) {
this.callee = callee;
this._buildHtml();
this.audio = $("#ring_overlay_ringing");
this.audio[0].play();
this._setAudioTimeout();
}
/**
* Builds and appends the ring overlay to the html document
*/
_buildHtml() {
$("body").append("<div class='overlay_container' >" +
"<div class='overlay' /><div class='overlay_content'>" +
"<img class='overlay_avatar' src='" +
this.callee.getAvatarUrl() + "' />" +
"<span data-i18n='calling' data-i18n-options='" +
JSON.stringify({name: this.callee.getName()}) +
"' class='overlay_text'>Calling " +
this.callee.getName() + "...</span></div>" +
"<audio id='ring_overlay_ringing' src='/sounds/ring.ogg' /></div>");
}
/**
* Sets the interval that is going to play the ringing sound.
*/
_setAudioTimeout() {
this.interval = setInterval( () => {
this.audio[0].play();
}, 5000);
}
/**
* Destroys and clears all the objects (html elements and audio interval)
* related to the ring overlay.
*/
destroy() {
if(this.interval)
clearInterval(this.interval);
$(".overlay_container").remove();
}
}
/**
* Store the current ring overlay instance.
* Note: We want to have only 1 instance at a time.
*/
let overlay = null;
export default {
/**
* Shows the ring overlay for the passed callee.
* @param callee {class User} the callee. Instance of User class from
* TokenData.js
*/
show(callee) {
if(overlay) {
this.hide();
}
overlay = new RingOverlay(callee);
},
/**
* Hides the ring overlay. Destroys all the elements related to the ring
* overlay.
*/
hide() {
if(!overlay)
return false;
overlay.destroy();
overlay = null;
return true;
}
};

View File

@ -81,6 +81,12 @@ export default {
function updateEmail () { function updateEmail () {
emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val()); emitter.emit(UIEvents.EMAIL_CHANGED, $('#setEmail').val());
} }
// AVATAR URL CHANGED
function updateAvatarUrl () {
emitter.emit(UIEvents.AVATAR_URL_CHANGED, $('#setAvatarUrl').val());
}
$('#setEmail') $('#setEmail')
.val(Settings.getEmail()) .val(Settings.getEmail())
.keyup(function (event) { .keyup(function (event) {
@ -89,6 +95,14 @@ export default {
} }
}).focusout(updateEmail); }).focusout(updateEmail);
$('#setAvatarUrl')
.val(Settings.getAvatarUrl())
.keyup(function (event) {
if (event.keyCode === 13) { // enter
updateAvatarUrl();
}
}).focusout(updateAvatarUrl);
// START MUTED // START MUTED
$("#startMutedOptions").change(function () { $("#startMutedOptions").change(function () {

View File

@ -7,6 +7,10 @@ import FilmStrip from '../videolayout/FilmStrip.js';
let toolbarTimeoutObject; let toolbarTimeoutObject;
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT; let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
/**
* If true the toolbar will be always displayed
*/
let alwaysVisibleToolbar = false;
function showDesktopSharingButton() { function showDesktopSharingButton() {
if (APP.conference.isDesktopSharingEnabled && if (APP.conference.isDesktopSharingEnabled &&
@ -21,7 +25,7 @@ function showDesktopSharingButton() {
* Hides the toolbar. * Hides the toolbar.
*/ */
function hideToolbar() { function hideToolbar() {
if (config.alwaysVisibleToolbar) { if (alwaysVisibleToolbar) {
return; return;
} }
@ -40,6 +44,25 @@ function hideToolbar() {
} }
const ToolbarToggler = { const ToolbarToggler = {
/**
* Initializes the ToolbarToggler
*/
init() {
alwaysVisibleToolbar = (config.alwaysVisibleToolbar === true);
},
/**
* Sets the value of alwaysVisibleToolbar variable.
* @param value {boolean} the new value of alwaysVisibleToolbar variable
*/
setAlwaysVisibleToolbar(value) {
alwaysVisibleToolbar = value;
},
/**
* Resets the value of alwaysVisibleToolbar variable to the default one.
*/
resetAlwaysVisibleToolbar() {
alwaysVisibleToolbar = (config.alwaysVisibleToolbar === true);
},
/** /**
* Shows the main toolbar. * Shows the main toolbar.
*/ */

View File

@ -9,6 +9,7 @@ let cameraDeviceId = '';
let micDeviceId = ''; let micDeviceId = '';
let welcomePageDisabled = false; let welcomePageDisabled = false;
let localFlipX = null; let localFlipX = null;
let avatarUrl = '';
function supportsLocalStorage() { function supportsLocalStorage() {
try { try {
@ -34,6 +35,7 @@ if (supportsLocalStorage()) {
} }
email = UIUtil.unescapeHtml(window.localStorage.email || ''); email = UIUtil.unescapeHtml(window.localStorage.email || '');
avatarUrl = UIUtil.unescapeHtml(window.localStorage.avatarUrl || '');
localFlipX = JSON.parse(window.localStorage.localFlipX || true); localFlipX = JSON.parse(window.localStorage.localFlipX || true);
displayName = UIUtil.unescapeHtml(window.localStorage.displayname || ''); displayName = UIUtil.unescapeHtml(window.localStorage.displayname || '');
language = window.localStorage.language; language = window.localStorage.language;
@ -98,6 +100,24 @@ export default {
return email; return email;
}, },
/**
* Sets new avatarUrl for local user and saves it to the local storage.
* @param {string} newAvatarUrl new avatarUrl for the local user
*/
setAvatarUrl: function (newAvatarUrl) {
avatarUrl = newAvatarUrl;
window.localStorage.avatarUrl = UIUtil.escapeHtml(newAvatarUrl);
},
/**
* Returns avatarUrl address of the local user.
* @returns {string} avatarUrl
*/
getAvatarUrl: function () {
return avatarUrl;
},
getLanguage () { getLanguage () {
return language; return language;
}, },

View File

@ -30,7 +30,9 @@
"retry": "0.6.1", "retry": "0.6.1",
"strophe": "^1.2.2", "strophe": "^1.2.2",
"strophejs-plugins": "^0.0.6", "strophejs-plugins": "^0.0.6",
"toastr": "^2.0.3" "toastr": "^2.0.3",
"postis": "^2.2.0",
"jws": "*"
}, },
"devDependencies": { "devDependencies": {
"browserify": "11.1.x", "browserify": "11.1.x",

View File

@ -15,6 +15,7 @@ local host = module.host;
local appId = module:get_option_string("app_id"); local appId = module:get_option_string("app_id");
local appSecret = module:get_option_string("app_secret"); local appSecret = module:get_option_string("app_secret");
local allowEmptyToken = module:get_option_boolean("allow_empty_token"); local allowEmptyToken = module:get_option_boolean("allow_empty_token");
local disableRoomNameConstraints = module:get_option_boolean("disable_room_name_constraints");
if allowEmptyToken == true then if allowEmptyToken == true then
module:log("warn", "WARNING - empty tokens allowed"); module:log("warn", "WARNING - empty tokens allowed");
@ -79,7 +80,7 @@ function provider.get_sasl_handler(session)
-- here we check if 'room' claim exists -- here we check if 'room' claim exists
local room, roomErr = token_util.get_room_name(token, appSecret); local room, roomErr = token_util.get_room_name(token, appSecret);
if room == nil then if room == nil and disableRoomNameConstraints ~= true then
if roomErr == nil then if roomErr == nil then
roomErr = "'room' claim is missing"; roomErr = "'room' claim is missing";
end end
@ -88,7 +89,7 @@ function provider.get_sasl_handler(session)
-- now verify the whole token -- now verify the whole token
local result, msg local result, msg
= token_util.verify_token(token, appId, appSecret, room); = token_util.verify_token(token, appId, appSecret, room, disableRoomNameConstraints);
if result == true then if result == true then
-- Binds room name to the session which is later checked on MUC join -- Binds room name to the session which is later checked on MUC join
session.jitsi_meet_room = room; session.jitsi_meet_room = room;
@ -121,4 +122,3 @@ local function anonymous(self, message)
end end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);

View File

@ -24,6 +24,7 @@ end
local appId = parentCtx:get_option_string("app_id"); local appId = parentCtx:get_option_string("app_id");
local appSecret = parentCtx:get_option_string("app_secret"); local appSecret = parentCtx:get_option_string("app_secret");
local allowEmptyToken = parentCtx:get_option_boolean("allow_empty_token"); local allowEmptyToken = parentCtx:get_option_boolean("allow_empty_token");
local disableRoomNameConstraints = parentCtx:get_option_boolean("disable_room_name_constraints")
log("debug", log("debug",
"%s - starting MUC token verifier app_id: %s app_secret: %s allow empty: %s", "%s - starting MUC token verifier app_id: %s app_secret: %s allow empty: %s",
@ -35,13 +36,6 @@ local function verify_user(session, stanza)
tostring(session.auth_token), tostring(session.auth_token),
tostring(session.jitsi_meet_room)); tostring(session.jitsi_meet_room));
if allowEmptyToken and session.auth_token == nil then
module:log(
"debug",
"Skipped room token verification - empty tokens are allowed");
return nil;
end
-- token not required for admin users -- token not required for admin users
local user_jid = stanza.attr.from; local user_jid = stanza.attr.from;
if is_admin(user_jid) then if is_admin(user_jid) then
@ -49,6 +43,13 @@ local function verify_user(session, stanza)
return nil; return nil;
end end
if allowEmptyToken and session.auth_token == nil then
module:log(
"debug",
"Skipped room token verification - empty tokens are allowed");
return nil;
end
local room = string.match(stanza.attr.to, "^(%w+)@"); local room = string.match(stanza.attr.to, "^(%w+)@");
log("debug", "Will verify token for user: %s, room: %s ", user_jid, room); log("debug", "Will verify token for user: %s, room: %s ", user_jid, room);
if room == nil then if room == nil then
@ -59,7 +60,7 @@ local function verify_user(session, stanza)
local token = session.auth_token; local token = session.auth_token;
local auth_room = session.jitsi_meet_room; local auth_room = session.jitsi_meet_room;
if room ~= auth_room then if room ~= auth_room and disableRoomNameConstraints ~= true then
log("error", "Token %s not allowed to join: %s", log("error", "Token %s not allowed to join: %s",
tostring(token), tostring(auth_room)); tostring(token), tostring(auth_room));
session.send( session.send(
@ -81,4 +82,3 @@ module:hook("muc-occupant-pre-join", function(event)
log("debug", "pre join: %s %s", tostring(room), tostring(stanza)); log("debug", "pre join: %s %s", tostring(room), tostring(stanza));
return verify_user(origin, stanza); return verify_user(origin, stanza);
end); end);

View File

@ -1,7 +1,7 @@
-- Token authentication -- Token authentication
-- Copyright (C) 2015 Atlassian -- Copyright (C) 2015 Atlassian
local jwt = require "luajwt"; local jwt = require "jwt";
local _M = {}; local _M = {};
@ -14,7 +14,7 @@ local function _get_room_name(token, appSecret)
end end
end end
local function _verify_token(token, appId, appSecret, roomName) local function _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints)
local claims, err = jwt.decode(token, appSecret, true); local claims, err = jwt.decode(token, appSecret, true);
if claims == nil then if claims == nil then
@ -30,22 +30,22 @@ local function _verify_token(token, appId, appSecret, roomName)
end end
local roomClaim = claims["room"]; local roomClaim = claims["room"];
if roomClaim == nil then if roomClaim == nil and disableRoomNameConstraints ~= true then
return nil, "'room' claim is missing"; return nil, "'room' claim is missing";
end end
if roomName ~= nil and roomName ~= roomClaim then if roomName ~= nil and roomName ~= roomClaim and disableRoomNameConstraints ~= true then
return nil, "Invalid room name('room' claim)"; return nil, "Invalid room name('room' claim)";
end end
return true; return true;
end end
function _M.verify_token(token, appId, appSecret, roomName) function _M.verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints)
return _verify_token(token, appId, appSecret, roomName); return _verify_token(token, appId, appSecret, roomName, disableRoomNameConstraints);
end end
function _M.get_room_name(token, appSecret) function _M.get_room_name(token, appSecret)
return _get_room_name(token, appSecret); return _get_room_name(token, appSecret);
end end
return _M; return _M;

View File

@ -14,6 +14,10 @@ export default {
* Notifies that local user changed email. * Notifies that local user changed email.
*/ */
EMAIL_CHANGED: "UI.email_changed", EMAIL_CHANGED: "UI.email_changed",
/**
* Notifies that local user changed avatar url.
*/
AVATAR_URL_CHANGED: "UI.avatar_url_changed",
/** /**
* Notifies that "start muted" settings changed. * Notifies that "start muted" settings changed.
*/ */

BIN
sounds/ring.ogg Normal file

Binary file not shown.

BIN
sounds/ring.wav Normal file

Binary file not shown.

View File

@ -36,17 +36,32 @@ function getRoomName () {
} }
/** /**
* Parses the hash parameters from the URL and returns them as a JS object. * Parses the parameters from the URL and returns them as a JS object.
* @param source {string} values - "hash"/"search" if "search" the parameters
* will parsed from location.search otherwise from location.hash
* @param dontParse if false or undefined some transformations
* (for parsing the value as JSON) are going to be executed
*/ */
function getConfigParamsFromUrl() { function getConfigParamsFromUrl(source, dontParse) {
if (!location.hash) var paramStr = (source === "search")? location.search : location.hash;
if (!paramStr)
return {}; return {};
var hash = location.hash.substr(1); paramStr = paramStr.substr(1);
var result = {}; var result = {};
hash.split("&").forEach(function (part) { paramStr.split("&").forEach(function (part) {
var item = part.split("="); var item = part.split("=");
result[item[0]] = JSON.parse( var value;
decodeURIComponent(item[1]).replace(/\\&/, "&")); try {
value = (dontParse)? item[1] : JSON.parse(
decodeURIComponent(item[1]).replace(/\\&/, "&"));
} catch (e) {
console.warn("Failed to parse URL argument", e);
if(window.onerror)
window.onerror("Failed to parse URL argument", null, null,
null, e);
return;
}
result[item[0]] = value;
}); });
return result; return result;
} }