[RN] Support JSON Web Token (JWT)

Make 'Add jwt module to react' work on mobile.
This commit is contained in:
Lyubo Marinov 2017-05-04 10:20:41 -05:00
parent ab5c2e9ded
commit e6f6884c36
13 changed files with 390 additions and 377 deletions

View File

@ -1,9 +1,7 @@
/* global config, createConnectionExternally */
import {
getRoomName,
parseURLParams
} from '../react/features/base/config/functions';
import getRoomName from '../react/features/base/config/getRoomName';
import parseURLParams from '../react/features/base/config/parseURLParams';
/**
* Implements external connect using createConnectionExternally function defined

View File

@ -1,9 +1,13 @@
declare var getConfigParamsFromUrl: Function;
// XXX The function parseURLParams is exported by the feature base/config (as
// defined in the terminology of react/). However, this file is (very likely)
// bundled in external_api in addition to app.bundle and, consequently, it is
// best to import as little as possible here (rather than the whole feature
// base/config) in order to minimize the amount of source code bundled into
// multiple bundles.
import parseURLParams from '../../react/features/base/config/parseURLParams';
/**
* JitsiMeetExternalAPI id - unique for a webpage.
*/
export const API_ID
= typeof getConfigParamsFromUrl === 'function'
? getConfigParamsFromUrl().jitsi_meet_external_api_id
: undefined;
= parseURLParams(window.location).jitsi_meet_external_api_id;

View File

@ -1,46 +0,0 @@
const logger = require("jitsi-meet-logger").getLogger(__filename);
var JSSHA = require('jssha');
module.exports = {
/**
* Looks for a list of possible BOSH addresses in 'config.boshList' and
* sets the value of 'config.bosh' based on that list and 'roomName'.
* @param config the configuration object.
* @param roomName the name of the room/conference.
*/
chooseAddress: function(config, roomName) {
if (!roomName || !config.boshList || !Array.isArray(config.boshList) ||
!config.boshList.length) {
return;
}
// This implements the actual choice of an entry in the list based on
// roomName. Please consider the implications for existing deployments
// before introducing changes.
var hash = (new JSSHA(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
var n = parseInt("0x"+hash.substr(-6));
var idx = n % config.boshList.length;
var attemptFirstAddress;
config.bosh = config.boshList[idx];
logger.log('Setting config.bosh to ' + config.bosh +
' (idx=' + idx + ')');
if (config.boshAttemptFirstList &&
Array.isArray(config.boshAttemptFirstList) &&
config.boshAttemptFirstList.length > 0) {
idx = n % config.boshAttemptFirstList.length;
attemptFirstAddress = config.boshAttemptFirstList[idx];
if (attemptFirstAddress != config.bosh) {
config.boshAttemptFirst = attemptFirstAddress;
logger.log('Setting config.boshAttemptFirst=' +
attemptFirstAddress + ' (idx=' + idx + ')');
} else {
logger.log('Not setting boshAttemptFirst, address matches.');
}
}
}
};

View File

@ -1,52 +0,0 @@
/* global $, config, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename);
var configUtil = require('./Util');
var HttpConfig = {
/**
* Sends HTTP POST request to specified <tt>endpoint</tt>. In request
* the name of the room is included in JSON format:
* {
* "rooomName": "someroom12345"
* }
* @param endpoint the name of HTTP endpoint to which HTTP POST request will
* be sent.
* @param roomName the name of the conference room for which config will be
* requested.
* @param complete
*/
obtainConfig: function (endpoint, roomName, complete) {
logger.info(
"Send config request to " + endpoint + " for room: " + roomName);
$.ajax(
endpoint,
{
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({"roomName": roomName}),
dataType: 'json',
error: function(jqXHR, textStatus, errorThrown) {
logger.error("Get config error: ", jqXHR, errorThrown);
var error = "Get config response status: " + textStatus;
complete(false, error);
},
success: function(data) {
try {
configUtil.overrideConfigJSON(
config, interfaceConfig, data);
complete(true);
return;
} catch (exception) {
logger.error("Parse config error: ", exception);
complete(false, exception);
}
}
}
);
}
};
module.exports = HttpConfig;

View File

@ -1,93 +0,0 @@
/* global config, interfaceConfig, loggingConfig */
import { parseURLParams } from '../../react/features/base/config';
import configUtils from './Util';
const logger = require("jitsi-meet-logger").getLogger(__filename);
/**
* URL params with this prefix should be merged to config.
*/
const CONFIG_PREFIX = 'config.';
/**
* URL params with this prefix should be merged to interface config.
*/
const INTERFACE_CONFIG_PREFIX = 'interfaceConfig.';
/**
* Config keys to be ignored.
*
* @type Set
*/
const KEYS_TO_IGNORE = new Set([
'analyticsScriptUrls',
'callStatsCustomScriptUrl'
]);
/**
* URL params with this prefix should be merged to logging config.
*/
const LOGGING_CONFIG_PREFIX = 'loggingConfig.';
/**
* Convert 'URL_PARAMS' to JSON object
* We have:
* {
* "config.disableAudioLevels": false,
* "config.channelLastN": -1,
* "interfaceConfig.APP_NAME": "Jitsi Meet"
* }
* We want to have:
* {
* "config": {
* "disableAudioLevels": false,
* "channelLastN": -1
* },
* interfaceConfig: {
* "APP_NAME": "Jitsi Meet"
* }
* }
*/
export function setConfigParametersFromUrl() {
// Parsing config params from URL hash.
const params = parseURLParams(window.location);
const configJSON = {
config: {},
interfaceConfig: {},
loggingConfig: {}
};
for (const key in params) {
if (typeof key === 'string') {
let confObj = null;
let confKey;
if (key.indexOf(CONFIG_PREFIX) === 0) {
confObj = configJSON.config;
confKey = key.substr(CONFIG_PREFIX.length);
} else if (key.indexOf(INTERFACE_CONFIG_PREFIX) === 0) {
confObj = configJSON.interfaceConfig;
confKey
= key.substr(INTERFACE_CONFIG_PREFIX.length);
} else if (key.indexOf(LOGGING_CONFIG_PREFIX) === 0) {
confObj = configJSON.loggingConfig;
confKey = key.substr(LOGGING_CONFIG_PREFIX.length);
}
// prevent passing some parameters which can inject scripts
if (confObj && !KEYS_TO_IGNORE.has(confKey)) {
confObj[confKey] = params[key];
}
} else {
logger.warn('Invalid config key: ', key);
}
}
configUtils.overrideConfigJSON(
config, interfaceConfig, loggingConfig,
configJSON);
}

View File

@ -1,54 +0,0 @@
const logger = require("jitsi-meet-logger").getLogger(__filename);
var ConfigUtil = {
/**
* Method overrides JSON properties in <tt>config</tt> and
* <tt>interfaceConfig</tt> Objects with the values from <tt>newConfig</tt>
* @param config the config object for which we'll be overriding properties
* @param interfaceConfig the interfaceConfig object for which we'll be
* overriding properties.
* @param loggingConfig the logging config object for which we'll be
* overriding properties.
* @param newConfig object containing configuration properties. Destination
* object is selected based on root property name:
* {
* config: {
* // config.js properties to be
* },
* interfaceConfig: {
* // interface_config.js properties here
* },
* loggingConfig: {
* // logging_config.js properties here
* }
* }
*/
overrideConfigJSON: function (config,
interfaceConfig, loggingConfig, newConfig) {
var configRoot, key, value, confObj;
for (configRoot in newConfig) {
confObj = null;
if (configRoot == "config") {
confObj = config;
} else if (configRoot == "interfaceConfig") {
confObj = interfaceConfig;
} else if (configRoot == "loggingConfig") {
confObj = loggingConfig;
} else {
continue;
}
for (key in newConfig[configRoot]) {
value = newConfig[configRoot][key];
if (confObj[key] && typeof confObj[key] !== typeof value) {
logger.log("Overriding a " + configRoot +
" property with a property of different type.");
}
logger.info("Overriding " + key + " with: " + value);
confObj[key] = value;
}
}
}
};
module.exports = ConfigUtil;

View File

@ -18,7 +18,8 @@ import {
* @returns {Function}
*/
export function appInit() {
return (dispatch, getState) => init(getState());
return (dispatch: Dispatch<*>, getState: Function) =>
init(getState());
}
/**
@ -31,7 +32,7 @@ export function appInit() {
* @returns {Function}
*/
export function appNavigate(uri) {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const oldDomain = getDomain(state);
const defaultURL = state['features/app'].app._getDefaultURL();
@ -43,14 +44,12 @@ export function appNavigate(uri) {
// If the specified URI does not identify a domain, use the app's
// default.
if (typeof domain === 'undefined') {
domain
= _parseURIString(defaultURL)
.domain;
domain = _parseURIString(defaultURL).domain;
}
if (room) {
const splitUrl = uri.split(domain);
const urlWithoutDomain = splitUrl[splitUrl.length - 1];
const splitURL = uri.split(domain);
const urlWithoutDomain = splitURL[splitURL.length - 1];
urlObject = new URL(urlWithoutDomain, `https://${domain}`);
}

View File

@ -1,72 +1,247 @@
/* @flow */
declare var config: Object;
import JSSHA from 'jssha';
import parseURLParams from './parseURLParams';
declare var $: Object;
/**
* Builds and returns the room name.
* The config keys to ignore because, for example, their values identify scripts
* and it is not desireable to inject these through URL params.
*
* @returns {string}
* @private
* @type Array
*/
export function getRoomName(): ?string {
const { getroomnode } = config;
const path = window.location.pathname;
let roomName;
const _KEYS_TO_IGNORE = [
'analyticsScriptUrls',
'callStatsCustomScriptUrl'
];
// Determine the room node from the URL.
if (getroomnode && typeof getroomnode === 'function') {
roomName = getroomnode.call(config, path);
} else {
// Fall back to the default strategy of making assumptions about how the
// URL maps to the room (name). It currently assumes a deployment in
// which the last non-directory component of the path (name) is the
// room.
roomName
= path.substring(path.lastIndexOf('/') + 1).toLowerCase()
|| undefined;
const logger = require('jitsi-meet-logger').getLogger(__filename);
// XXX The functions getRoomName and parseURLParams are split out of
// functions.js because they are bundled in both app.bundle and
// do_external_connect, webpack 1 does not support tree shaking, and we don't
// want all functions to be bundled in do_external_connect.
export { default as getRoomName } from './getRoomName';
export { parseURLParams };
/* eslint-disable no-shadow */
/**
* Looks for a list of possible BOSH addresses in {@code config.boshList} and
* sets the value of {@code config.bosh} based on that list and
* {@code roomName}.
*
* @param {Object} config - The configuration object.
* @param {string} roomName - The name of the room/conference.
* @returns {void}
*/
export function chooseBOSHAddress(config: Object, roomName: string) {
if (!roomName) {
return;
}
return roomName;
const { boshList } = config;
if (!boshList || !Array.isArray(boshList) || !boshList.length) {
return;
}
// This implements the actual choice of an entry in the list based on
// roomName. Please consider the implications for existing deployments
// before introducing changes.
const hash = (new JSSHA(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
const n = parseInt(hash.substr(-6), 16);
let idx = n % boshList.length;
config.bosh = boshList[idx];
logger.log(`Setting config.bosh to ${config.bosh} (idx=${idx})`);
const { boshAttemptFirstList } = config;
if (boshAttemptFirstList
&& Array.isArray(boshAttemptFirstList)
&& boshAttemptFirstList.length > 0) {
idx = n % boshAttemptFirstList.length;
const attemptFirstAddress = boshAttemptFirstList[idx];
if (attemptFirstAddress === config.bosh) {
logger.log('Not setting config.boshAttemptFirst, address matches.');
} else {
config.boshAttemptFirst = attemptFirstAddress;
logger.log(
`Setting config.boshAttemptFirst=${attemptFirstAddress} (idx=${
idx})`);
}
}
}
/* eslint-enable no-shadow */
/**
* Parses the parameters from the URL and returns them as a JS object.
* Sends HTTP POST request to specified <tt>endpoint</tt>. In request the name
* of the room is included in JSON format:
* {
* "rooomName": "someroom12345"
* }.
*
* @param {string} url - URL to parse.
* @param {boolean} dontParse - If false or undefined some transformations
* (for parsing the value as JSON) are going to be executed.
* @param {string} source - Values - "hash"/"search" if "search" the parameters
* will parsed from location.search otherwise from location.hash.
* @returns {Object}
* @param {string} endpoint - The name of HTTP endpoint to which to send
* the HTTP POST request.
* @param {string} roomName - The name of the conference room for which config
* is requested.
* @param {Function} complete - The callback to invoke upon success or failure.
* @returns {void}
*/
export function parseURLParams(
url: URL,
dontParse: boolean = false,
source: string = 'hash'): Object {
const paramStr = source === 'search' ? url.search : url.hash;
const params = {};
export function obtainConfig(
endpoint: string,
roomName: string,
complete: Function) {
logger.info(`Send config request to ${endpoint} for room: ${roomName}`);
$.ajax(
endpoint,
{
contentType: 'application/json',
data: JSON.stringify({ roomName }),
dataType: 'json',
method: 'POST',
// eslint-disable-next-line newline-per-chained-call
paramStr && paramStr.substr(1).split('&').forEach(part => {
const param = part.split('=');
let value;
error(jqXHR, textStatus, errorThrown) {
logger.error('Get config error: ', jqXHR, errorThrown);
complete(false, `Get config response status: ${textStatus}`);
},
success(data) {
const { config, interfaceConfig, loggingConfig } = window;
try {
value = param[1];
if (!dontParse) {
value
= JSON.parse(
decodeURIComponent(param[1]).replace(/\\&/, '&'));
try {
overrideConfigJSON(
config, interfaceConfig, loggingConfig,
data);
complete(true);
} catch (e) {
logger.error('Parse config error: ', e);
complete(false, e);
}
}
} catch (e) {
const msg = `Failed to parse URL parameter value: ${String(value)}`;
console.warn(msg, e);
window.onerror && window.onerror(msg, null, null, null, e);
return;
}
params[param[0]] = value;
});
return params;
);
}
/* eslint-disable max-params, no-shadow */
/**
* Overrides JSON properties in {@code config} and
* {@code interfaceConfig} Objects with the values from {@code newConfig}.
*
* @param {Object} config - The config Object in which we'll be overriding
* properties.
* @param {Object} interfaceConfig - The interfaceConfig Object in which we'll
* be overriding properties.
* @param {Object} loggingConfig - The loggingConfig Object in which we'll be
* overriding properties.
* @param {Object} json - Object containing configuration properties.
* Destination object is selected based on root property name:
* {
* config: {
* // config.js properties here
* },
* interfaceConfig: {
* // interface_config.js properties here
* },
* loggingConfig: {
* // logging_config.js properties here
* }
* }.
* @returns {void}
*/
export function overrideConfigJSON(
config: Object, interfaceConfig: Object, loggingConfig: Object,
json: Object) {
for (const configName of Object.keys(json)) {
let configObj;
if (configName === 'config') {
configObj = config;
} else if (configName === 'interfaceConfig') {
configObj = interfaceConfig;
} else if (configName === 'loggingConfig') {
configObj = loggingConfig;
}
if (configObj) {
const configJSON = json[configName];
for (const key of Object.keys(configJSON)) {
const oldValue = configObj[key];
const newValue = configJSON[key];
if (oldValue && typeof oldValue !== typeof newValue) {
logger.log(
`Overriding a ${configName
} property with a property of different type.`);
}
logger.info(`Overriding ${key} with: ${newValue}`);
configObj[key] = newValue;
}
}
}
}
/* eslint-enable max-params, no-shadow */
/**
* Converts 'URL_PARAMS' to JSON object.
* We have:
* {
* "config.disableAudioLevels": false,
* "config.channelLastN": -1,
* "interfaceConfig.APP_NAME": "Jitsi Meet"
* }.
* We want to have:
* {
* "config": {
* "disableAudioLevels": false,
* "channelLastN": -1
* },
* interfaceConfig: {
* "APP_NAME": "Jitsi Meet"
* }
* }.
*
* @returns {void}
*/
export function setConfigFromURLParams() {
const params = parseURLParams(window.location);
const { config, interfaceConfig, loggingConfig } = window;
const json = {};
// TODO We're still in the middle ground between old Web with config,
// interfaceConfig, and loggingConfig used via global variables and new Web
// and mobile reading the respective values from the redux store. On React
// Native there's no interfaceConfig at all yet and loggingConfig is not
// loaded but there's a default value in the redux store.
config && (json.config = {});
interfaceConfig && (json.interfaceConfig = {});
loggingConfig && (json.loggingConfig = {});
for (const param of Object.keys(params)) {
const objEnd = param.indexOf('.');
if (objEnd !== -1) {
const obj = param.substring(0, objEnd);
if (json.hasOwnProperty(obj)) {
const key = param.substring(objEnd + 1);
// Prevent passing some parameters which can inject scripts.
if (key && _KEYS_TO_IGNORE.indexOf(key) === -1) {
json[obj][key] = params[param];
}
}
}
}
overrideConfigJSON(config, interfaceConfig, loggingConfig, json);
}

View File

@ -0,0 +1,29 @@
/* @flow */
declare var config: Object;
/**
* Builds and returns the room name.
*
* @returns {string}
*/
export default function getRoomName(): ?string {
const { getroomnode } = config;
const path = window.location.pathname;
let roomName;
// Determine the room node from the URL.
if (getroomnode && typeof getroomnode === 'function') {
roomName = getroomnode.call(config, path);
} else {
// Fall back to the default strategy of making assumptions about how the
// URL maps to the room (name). It currently assumes a deployment in
// which the last non-directory component of the path (name) is the
// room.
roomName
= path.substring(path.lastIndexOf('/') + 1).toLowerCase()
|| undefined;
}
return roomName;
}

View File

@ -0,0 +1,49 @@
/* @flow */
/**
* Parses the parameters from the URL and returns them as a JS object.
*
* @param {string} url - URL to parse.
* @param {boolean} dontParse - If false or undefined some transformations
* (for parsing the value as JSON) are going to be executed.
* @param {string} source - Values - "hash"/"search" if "search" the parameters
* will parsed from location.search otherwise from location.hash.
* @returns {Object}
*/
export default function parseURLParams(
url: URL,
dontParse: boolean = false,
source: string = 'hash'): Object {
const paramStr = source === 'search' ? url.search : url.hash;
const params = {};
// eslint-disable-next-line newline-per-chained-call
paramStr && paramStr.substr(1).split('&').forEach(part => {
const param = part.split('=');
const key = param[0];
if (!key) {
return;
}
let value;
try {
value = param[1];
if (!dontParse) {
value
= JSON.parse(decodeURIComponent(value).replace(/\\&/, '&'));
}
} catch (e) {
const msg = `Failed to parse URL parameter value: ${String(value)}`;
console.warn(msg, e);
window.onerror && window.onerror(msg, null, null, null, e);
return;
}
params[key] = value;
});
return params;
}

View File

@ -21,11 +21,12 @@ export function connect() {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const { options } = state['features/base/connection'];
const { issuer, jwt } = state['features/jwt'];
const { room } = state['features/base/conference'];
const connection
= new JitsiMeetJS.JitsiConnection(
options.appId,
options.token,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
{
...options,
bosh:

View File

@ -1,8 +1,7 @@
import { loadScript } from '../../base/util';
/* @flow */
import {
setConfigParametersFromUrl
} from '../../../../modules/config/URLProcessor';
import { setConfigFromURLParams } from '../../base/config';
import { loadScript } from '../../base/util';
import JitsiMeetJS from './_';
@ -10,6 +9,28 @@ declare var APP: Object;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
/**
* Creates a JitsiLocalTrack model from the given device id.
*
* @param {string} type - The media type of track being created. Expected values
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @returns {Promise<JitsiLocalTrack>}
*/
export function createLocalTrack(type: string, deviceId: string) {
return (
JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId,
devices: [ type ],
// eslint-disable-next-line camelcase
firefox_fake_device:
window.config && window.config.firefox_fake_device,
micDeviceId: deviceId
})
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
}
/**
* Determines whether a specific JitsiConnectionErrors instance indicates a
* fatal JitsiConnection error.
@ -38,56 +59,43 @@ export function isFatalJitsiConnectionError(error: string) {
* @returns {Promise<Object>}
*/
export function loadConfig(host: string, path: string = '/config.js') {
// Returns config.js file from global scope. We can't use the version that's
// being used for the React Native app because the old/current Web app uses
// config from the global scope.
if (typeof APP !== 'undefined') {
// FIXME The following call to setConfigParametersFromUrl is bad design
// but URLProcessor still deals with the global variables config,
// interfaceConfig, and loggingConfig and loadConfig. As the latter will
// surely change in the future, so will the former then.
setConfigParametersFromUrl();
let promise;
return Promise.resolve(window.config);
if (typeof APP === 'undefined') {
promise
= loadScript(new URL(path, host).toString())
.then(() => {
const { config } = window;
// We don't want to pollute global scope.
window.config = undefined;
if (typeof config !== 'object') {
throw new Error('window.config is not an object');
}
return config;
})
.catch(err => {
console.error(`Failed to load ${path} from ${host}`, err);
throw err;
});
} else {
// Return config.js file from global scope. We can't use the version
// that's being used for the React Native app because the old/current
// Web app uses config from the global scope.
promise = Promise.resolve(window.config);
}
return loadScript(new URL(path, host).toString())
.then(() => {
const config = window.config;
// FIXME It's neither here nor there at the time of this writing where
// config, interfaceConfig, and loggingConfig should be overwritten by URL
// params.
promise = promise.then(value => {
setConfigFromURLParams();
// We don't want to pollute global scope.
window.config = undefined;
return value;
});
if (typeof config !== 'object') {
throw new Error('window.config is not an object');
}
return config;
})
.catch(err => {
console.error(`Failed to load ${path} from ${host}`, err);
throw err;
});
}
/**
* Creates a JitsiLocalTrack model from the given device id.
*
* @param {string} type - The media type of track being created. Expected values
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @returns {Promise<JitsiLocalTrack>}
*/
export function createLocalTrack(type, deviceId) {
return JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId,
devices: [ type ],
// eslint-disable-next-line camelcase
firefox_fake_device:
window.config && window.config.firefox_fake_device,
micDeviceId: deviceId
})
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack);
return promise;
}

View File

@ -1,9 +1,8 @@
/* global APP, config */
import BoshAddressChoice from '../../../modules/config/BoshAddressChoice';
import HttpConfigFetch from '../../../modules/config/HttpConfigFetch';
import ConferenceUrl from '../../../modules/URL/ConferenceUrl';
import { chooseBOSHAddress, obtainConfig } from '../base/config';
import { RouteRegistry } from '../base/react';
import { Conference } from './components';
@ -47,15 +46,11 @@ function _initConference() {
* @returns {Promise}
*/
function _obtainConfig(location, room) {
return new Promise((resolve, reject) => {
HttpConfigFetch.obtainConfig(location, room, (success, error) => {
if (success) {
resolve();
} else {
reject(error);
}
});
});
return new Promise((resolve, reject) =>
obtainConfig(location, room, (success, error) => {
success ? resolve() : reject(error);
})
);
}
/**
@ -87,7 +82,7 @@ function _obtainConfigAndInit() {
null, 'dialog.connectError', err);
});
} else {
BoshAddressChoice.chooseAddress(config, room);
chooseBOSHAddress(config, room);
_initConference();
}
}