initial commit
This commit is contained in:
Vendored
+77
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,163 @@
|
||||
/* global ga */
|
||||
|
||||
(function(ctx) {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function Analytics(options) {
|
||||
/* eslint-disable */
|
||||
|
||||
if (!options.googleAnalyticsTrackingId) {
|
||||
console.log(
|
||||
'Failed to initialize Google Analytics handler, no tracking ID');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Analytics
|
||||
* TODO: Keep this local, there's no need to add it to window.
|
||||
*/
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', options.googleAnalyticsTrackingId, 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the integer to use for a Google Analytics event's value field
|
||||
* from a lib-jitsi-meet analytics event.
|
||||
* @param {Object} event - The lib-jitsi-meet analytics event.
|
||||
* @returns {Object} - The integer to use for the 'value' of a Google
|
||||
* Analytics event.
|
||||
* @private
|
||||
*/
|
||||
Analytics.prototype._extractAction = function(event) {
|
||||
// Page events have a single 'name' field.
|
||||
if (event.type === 'page') {
|
||||
return event.name;
|
||||
}
|
||||
|
||||
// All other events have action, actionSubject, and source fields. All
|
||||
// three fields are required, and the often jitsi-meet and
|
||||
// lib-jitsi-meet use the same value when separate values are not
|
||||
// necessary (i.e. event.action == event.actionSubject).
|
||||
// Here we concatenate these three fields, but avoid adding the same
|
||||
// value twice, because it would only make the GA event's action harder
|
||||
// to read.
|
||||
let action = event.action;
|
||||
|
||||
if (event.actionSubject && event.actionSubject !== event.action) {
|
||||
// Intentionally use string concatenation as analytics needs to
|
||||
// work on IE but this file does not go through babel. For some
|
||||
// reason disabling this globally for the file does not have an
|
||||
// effect.
|
||||
// eslint-disable-next-line prefer-template
|
||||
action = event.actionSubject + '.' + action;
|
||||
}
|
||||
if (event.source && event.source !== event.action
|
||||
&& event.source !== event.action) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
action = event.source + '.' + action;
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the integer to use for a Google Analytics event's value field
|
||||
* from a lib-jitsi-meet analytics event.
|
||||
* @param {Object} event - The lib-jitsi-meet analytics event.
|
||||
* @returns {Object} - The integer to use for the 'value' of a Google
|
||||
* Analytics event, or NaN if the lib-jitsi-meet event doesn't contain a
|
||||
* suitable value.
|
||||
* @private
|
||||
*/
|
||||
Analytics.prototype._extractValue = function(event) {
|
||||
let value = event && event.attributes && event.attributes.value;
|
||||
|
||||
// Try to extract an integer from the "value" attribute.
|
||||
value = Math.round(parseFloat(value));
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the string to use for a Google Analytics event's label field
|
||||
* from a lib-jitsi-meet analytics event.
|
||||
* @param {Object} event - The lib-jitsi-meet analytics event.
|
||||
* @returns {string} - The string to use for the 'label' of a Google
|
||||
* Analytics event.
|
||||
* @private
|
||||
*/
|
||||
Analytics.prototype._extractLabel = function(event) {
|
||||
let label = '';
|
||||
|
||||
// The label field is limited to 500B. We will concatenate all
|
||||
// attributes of the event, except the user agent because it may be
|
||||
// lengthy and is probably included from elsewhere.
|
||||
for (const property in event.attributes) {
|
||||
if (property !== 'permanent_user_agent'
|
||||
&& property !== 'permanent_callstats_name'
|
||||
&& event.attributes.hasOwnProperty(property)) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
label += property + '=' + event.attributes[property] + '&';
|
||||
}
|
||||
}
|
||||
|
||||
if (label.length > 0) {
|
||||
label = label.slice(0, -1);
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the entry point of the API. The function sends an event to
|
||||
* google analytics. The format of the event is described in
|
||||
* AnalyticsAdapter in lib-jitsi-meet.
|
||||
* @param {Object} event - the event in the format specified by
|
||||
* lib-jitsi-meet.
|
||||
*/
|
||||
Analytics.prototype.sendEvent = function(event) {
|
||||
if (!event || !ga) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ignoredEvents
|
||||
= [ 'e2e_rtt', 'rtp.stats', 'rtt.by.region', 'available.device',
|
||||
'stream.switch.delay', 'ice.state.changed', 'ice.duration' ];
|
||||
|
||||
// Temporary removing some of the events that are too noisy.
|
||||
if (ignoredEvents.indexOf(event.action) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gaEvent = {
|
||||
'eventCategory': 'jitsi-meet',
|
||||
'eventAction': this._extractAction(event),
|
||||
'eventLabel': this._extractLabel(event)
|
||||
};
|
||||
const value = this._extractValue(event);
|
||||
|
||||
if (!isNaN(value)) {
|
||||
gaEvent.eventValue = value;
|
||||
}
|
||||
|
||||
ga('send', 'event', gaEvent);
|
||||
};
|
||||
|
||||
if (typeof ctx.JitsiMeetJS === 'undefined') {
|
||||
ctx.JitsiMeetJS = {};
|
||||
}
|
||||
if (typeof ctx.JitsiMeetJS.app === 'undefined') {
|
||||
ctx.JitsiMeetJS.app = {};
|
||||
}
|
||||
if (typeof ctx.JitsiMeetJS.app.analyticsHandlers === 'undefined') {
|
||||
ctx.JitsiMeetJS.app.analyticsHandlers = [];
|
||||
}
|
||||
ctx.JitsiMeetJS.app.analyticsHandlers.push(Analytics);
|
||||
})(window);
|
||||
/* eslint-enable prefer-template */
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/libs/",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);const r=(window.JitsiMeetJS||(window.JitsiMeetJS={}),window.JitsiMeetJS.app||(window.JitsiMeetJS.app={}),window.JitsiMeetJS.app);r.analyticsHandlers=r.analyticsHandlers||[],r.analyticsHandlers.push(class extends class{constructor(e={}){this._enabled=!1,this._whiteListedEvents=e.whiteListedEvents,this._blackListedEvents=[...e.blackListedEvents||[],"e2e_rtt","rtp.stats","rtt.by.region","available.device","stream.switch.delay","ice.state.changed","ice.duration","peer.conn.status.duration"]}_extractName(e){if("page"===e.type)return e.name;const{action:t,actionSubject:n,source:r}=e;let i=t;return n&&n!==t&&(i=`${n}.${t}`),r&&r!==t&&(i=`${r}.${i}`),i}_shouldIgnore(e){if(!e||!this._enabled)return!0;const t=this._extractName(e);return Array.isArray(this._whiteListedEvents)?-1===this._whiteListedEvents.indexOf(t):!!Array.isArray(this._blackListedEvents)&&-1!==this._blackListedEvents.indexOf(t)}}{constructor(e){if(super(e),this._userProperties={},!e.googleAnalyticsTrackingId)throw new Error("Failed to initialize Google Analytics handler, no tracking ID");this._enabled=!0,this._initGoogleAnalytics(e)}_initGoogleAnalytics(e){var t,n,r,i,a,s;t=window,n=document,r="script",i="ga",t.GoogleAnalyticsObject=i,t.ga=t.ga||function(){(t.ga.q=t.ga.q||[]).push(arguments)},t.ga.l=1*new Date,a=n.createElement(r),s=n.getElementsByTagName(r)[0],a.async=1,a.src="//www.google-analytics.com/analytics.js",s.parentNode.insertBefore(a,s),ga("create",e.googleAnalyticsTrackingId,"auto"),ga("send","pageview")}_extractValue(e){let t=e&&e.attributes&&e.attributes.value;return t=Math.round(parseFloat(t)),t}_extractLabel(e){const{attributes:t={}}=e,n=Object.keys(t).map(e=>`${e}=${t[e]}`);return n.push(this._userPropertiesString),n.join("&")}setUserProperties(e={}){if(!this._enabled)return;const t=["user_agent","callstats_name"];this._userPropertiesString=Object.keys(e).filter(e=>-1===t.indexOf(e)).map(t=>`permanent_${t}=${e[t]}`).join("&")}sendEvent(e){if(this._shouldIgnore(e))return;const t={eventCategory:"jitsi-meet",eventAction:this._extractName(e),eventLabel:this._extractLabel(e)},n=this._extractValue(e);isNaN(n)||(t.eventValue=n),ga("send","event",t)}})}]);
|
||||
//# sourceMappingURL=analytics-ga.min.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+225
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"safari": [
|
||||
{"version": "10.2" },
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": false,
|
||||
"videoOut": false,
|
||||
"screenSharing": false
|
||||
},
|
||||
"iframeCapabilities": {
|
||||
"isSupported": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"chrome":
|
||||
[
|
||||
{"version": "51" },
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"opera":
|
||||
[
|
||||
{"version": "22" },
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"firefox": [
|
||||
{"version": "52.4" },
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"edge": [
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"nwjs": [
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"electron": [
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"react-native": [
|
||||
{
|
||||
"capabilities": {
|
||||
"audioIn": true,
|
||||
"audioOut": true,
|
||||
"videoIn": true,
|
||||
"videoOut": true,
|
||||
"screenSharing": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="/libs/",r(r.s=0)}([function(e,t){}]);
|
||||
//# sourceMappingURL=close3.min.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///webpack/bootstrap"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s"],"mappings":"aACE,IAAIA,EAAmB,GAGvB,SAASC,EAAoBC,GAG5B,GAAGF,EAAiBE,GACnB,OAAOF,EAAiBE,GAAUC,QAGnC,IAAIC,EAASJ,EAAiBE,GAAY,CACzCG,EAAGH,EACHI,GAAG,EACHH,QAAS,IAUV,OANAI,EAAQL,GAAUM,KAAKJ,EAAOD,QAASC,EAAQA,EAAOD,QAASF,GAG/DG,EAAOE,GAAI,EAGJF,EAAOD,QAKfF,EAAoBQ,EAAIF,EAGxBN,EAAoBS,EAAIV,EAGxBC,EAAoBU,EAAI,SAASR,EAASS,EAAMC,GAC3CZ,EAAoBa,EAAEX,EAASS,IAClCG,OAAOC,eAAeb,EAASS,EAAM,CAAEK,YAAY,EAAMC,IAAKL,KAKhEZ,EAAoBkB,EAAI,SAAShB,GACX,oBAAXiB,QAA0BA,OAAOC,aAC1CN,OAAOC,eAAeb,EAASiB,OAAOC,YAAa,CAAEC,MAAO,WAE7DP,OAAOC,eAAeb,EAAS,aAAc,CAAEmB,OAAO,KAQvDrB,EAAoBsB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQrB,EAAoBqB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKX,OAAOY,OAAO,MAGvB,GAFA1B,EAAoBkB,EAAEO,GACtBX,OAAOC,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOrB,EAAoBU,EAAEe,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRzB,EAAoB6B,EAAI,SAAS1B,GAChC,IAAIS,EAAST,GAAUA,EAAOqB,WAC7B,WAAwB,OAAOrB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAH,EAAoBU,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRZ,EAAoBa,EAAI,SAASiB,EAAQC,GAAY,OAAOjB,OAAOkB,UAAUC,eAAe1B,KAAKuB,EAAQC,IAGzG/B,EAAoBkC,EAAI,SAIjBlC,EAAoBA,EAAoBmC,EAAI,G","file":"close3.min.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/libs/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n"],"sourceRoot":""}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
@@ -0,0 +1,2 @@
|
||||
!function(n){var e={};function t(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return n[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}t.m=n,t.c=e,t.d=function(n,e,o){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:o})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var r in n)t.d(o,r,function(e){return n[e]}.bind(null,r));return o},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="/libs/",t(t.s=0)}([function(n,e,t){"use strict";t.r(e);function o(n,e=!1,t="hash"){const o="search"===t?n.search:n.hash,r={},i=o&&o.substr(1).split("&")||[];if("hash"===t&&1===i.length){const n=i[0];if(n.startsWith("/")&&1===n.split("&").length)return r}return i.forEach(n=>{const t=n.split("="),o=t[0];if(!o)return;let i;try{if(i=t[1],!e){const n=decodeURIComponent(i).replace(/\\&/,"&");i="undefined"===n?void 0:JSON.parse(n)}}catch(n){return void function(n,e=""){console.error(e,n),window.onerror&&window.onerror(e,null,null,null,n)}(n,"Failed to parse URL parameter value: "+String(i))}r[o]=i}),r}function r(n){if(!n)return n;try{n=decodeURIComponent(n)}catch(n){}return n=(n=n.normalize("NFKC")).toLowerCase(),(n=encodeURIComponent(n)).toLowerCase()}if("function"==typeof createConnectionExternally){let n=o(window.location,!0,"hash")["config.externalConnectUrl"]||config.websocket?void 0:config.externalConnectUrl;const e=o(window.location,!0,"hash")["config.iAmRecorder"];let t;if(n&&(t=function(){const n=window.location.pathname;return r(n.substring(n.lastIndexOf("/")+1)||void 0)}())&&!e){n+="?room="+t;const e=o(window.location,!0,"search").jwt;e&&(n+="&token="+e),createConnectionExternally(n,n=>{window.XMPPAttachInfo={status:"success",data:n},i()},c)}else c()}else c();function i(){window.APP&&"ready"===window.APP.connect.status&&window.APP.connect.handler()}function c(n){n&&console.warn(n),window.XMPPAttachInfo={status:"error"},i()}}]);
|
||||
//# sourceMappingURL=do_external_connect.min.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Requests the given webservice that will create the connection and will return
|
||||
* the necessary details(rid, sid and jid) to attach to this connection and
|
||||
* start using it. This script can be used for optimizing the connection startup
|
||||
* time. The function will send AJAX request to a webservice that should
|
||||
* create the bosh session much faster than the client because the webservice
|
||||
* can be started on the same machine as the XMPP serever.
|
||||
*
|
||||
* NOTE: It's vert important to execute this function as early as you can for
|
||||
* optimal results.
|
||||
*
|
||||
* @param webserviceUrl the url for the web service that is going to create the
|
||||
* connection.
|
||||
* @param successCallback callback function called with the result of the AJAX
|
||||
* request if the request was successfull. The callback will receive one
|
||||
* parameter which will be JS Object with properties - rid, sid and jid. This
|
||||
* result should be passed to JitsiConnection.attach method in order to use that
|
||||
* connection.
|
||||
* @param error_callback callback function called the AJAX request fail. This
|
||||
* callback is going to receive one parameter which is going to be JS error
|
||||
* object with a reason for failure in it.
|
||||
*/
|
||||
function createConnectionExternally( // eslint-disable-line no-unused-vars
|
||||
webserviceUrl,
|
||||
successCallback,
|
||||
error_callback) {
|
||||
if (!window.XMLHttpRequest) {
|
||||
error_callback(new Error('XMLHttpRequest is not supported!'));
|
||||
return;
|
||||
}
|
||||
|
||||
var HTTP_STATUS_OK = 200;
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (xhttp.readyState == xhttp.DONE) {
|
||||
var now = window.connectionTimes['external_connect.done']
|
||||
= window.performance.now();
|
||||
console.log('(TIME) external connect XHR done:\t', now);
|
||||
if (xhttp.status == HTTP_STATUS_OK) {
|
||||
try {
|
||||
var data = JSON.parse(xhttp.responseText);
|
||||
successCallback(data);
|
||||
} catch (e) {
|
||||
error_callback(e);
|
||||
}
|
||||
} else {
|
||||
error_callback(new Error('XMLHttpRequest error. Status: '
|
||||
+ xhttp.status + '. Error message: ' + xhttp.statusText));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open('GET', webserviceUrl, true);
|
||||
|
||||
// Fixes external connect for IE
|
||||
// The timeout property may be set only after calling the open() method
|
||||
// and before calling the send() method.
|
||||
xhttp.timeout = 3000;
|
||||
|
||||
window.connectionTimes = {};
|
||||
var now = window.connectionTimes['external_connect.sending']
|
||||
= window.performance.now();
|
||||
console.log('(TIME) Sending external connect XHR:\t', now);
|
||||
xhttp.send();
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/libs/",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}self.FLAC_SCRIPT_LOCATION="/libs/",importScripts("/libs/libflac4-1.3.2.min.js");const i={0:"FLAC__STREAM_ENCODER_OK",1:"FLAC__STREAM_ENCODER_UNINITIALIZED",2:"FLAC__STREAM_ENCODER_OGG_ERROR",3:"FLAC__STREAM_ENCODER_VERIFY_DECODER_ERROR",4:"FLAC__STREAM_ENCODER_VERIFY_MISMATCH_IN_AUDIO_DATA",5:"FLAC__STREAM_ENCODER_CLIENT_ERROR",6:"FLAC__STREAM_ENCODER_IO_ERROR",7:"FLAC__STREAM_ENCODER_FRAMING_ERROR",8:"FLAC__STREAM_ENCODER_MEMORY_ALLOCATION_ERROR"},o=Object.freeze({UNINTIALIZED:"uninitialized",WORKING:"working",FINISHED:"finished"});class a{constructor(e,t=16,n=4096){if(r(this,"_encoderId",0),r(this,"_flacBuffers",[]),r(this,"_flacLength",0),r(this,"_state",o.UNINTIALIZED),r(this,"_data",null),r(this,"_onMetadataAvailable",()=>{}),!Flac.isReady())throw new Error("libflac is not ready yet!");if(this._sampleRate=e,this._bitDepth=t,this._bufferSize=n,this._encoderId=Flac.init_libflac_encoder(this._sampleRate,1,this._bitDepth,5,0,!0,0),0===this._encoderId)throw new Error("Failed to create libflac encoder.");if(0!==Flac.init_encoder_stream(this._encoderId,this._onEncodedData.bind(this),this._onMetadataAvailable.bind(this)))throw new Error("Failed to initalise libflac encoder.");this._state=o.WORKING}encode(e){if(this._state!==o.WORKING)throw new Error("Encoder is not ready or has finished.");if(!Flac.isReady())throw new Error("Flac not ready");const t=e.length,n=new Int32Array(t),r=new DataView(n.buffer);let a=0;for(let n=0;n<t;n++)r.setInt32(a,32767*e[n],!0),a+=4;if(1!==Flac.FLAC__stream_encoder_process_interleaved(this._encoderId,n,n.length)){const e=Flac.FLAC__stream_encoder_get_state(this._encoderId);console.error("Error during encoding",i[e])}}finish(){if(this._state===o.WORKING){this._state=o.FINISHED;const e=Flac.FLAC__stream_encoder_finish(this._encoderId);console.log("Flac encoding finished: ",e),Flac.FLAC__stream_encoder_delete(this._encoderId),this._data=this._exportFlacBlob()}}getBlob(){return this._state===o.FINISHED?this._data:null}_exportFlacBlob(){const e=function(e,t){const n=new Uint8Array(t);let r=0;const i=e.length;for(let t=0;t<i;t++){const i=e[t];n.set(i,r),r+=i.length}return n}(this._flacBuffers,this._flacLength);return new Blob([e],{type:"audio/flac"})}_onEncodedData(e,t){this._flacBuffers.push(e),this._flacLength+=e.byteLength}}let s=null;self.onmessage=function(e){switch(e.data.command){case"MAIN_THREAD_INIT":{const t=e.data.config.bps,n=e.data.config.sampleRate;Flac.isReady()?(s=new a(n,t),self.postMessage({command:"WORKER_LIBFLAC_READY"})):Flac.onready=function(){setTimeout(()=>{s=new a(n,t),self.postMessage({command:"WORKER_LIBFLAC_READY"})},0)};break}case"MAIN_THREAD_NEW_DATA_ARRIVED":null===s?console.error("flacEncoderWorker received data when the encoder is not ready."):s.encode(e.data.buf);break;case"MAIN_THREAD_FINISH":if(null!==s){s.finish();const e=s.getBlob();self.postMessage({command:"WORKER_BLOB_READY",buf:e}),s=null}}}}]);
|
||||
//# sourceMappingURL=flacEncodeWorker.min.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,608 @@
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = 0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// CONCATENATED MODULE: ./modules/e2ee/crypto-utils.js
|
||||
/**
|
||||
* Derives a set of keys from the master key.
|
||||
* @param {CryptoKey} material - master key to derive from
|
||||
*
|
||||
* See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1
|
||||
*/
|
||||
async function deriveKeys(material) {
|
||||
const info = new ArrayBuffer();
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams
|
||||
const encryptionKey = await crypto.subtle.deriveKey({
|
||||
name: 'HKDF',
|
||||
salt: textEncoder.encode('JFrameEncryptionKey'),
|
||||
hash: 'SHA-256',
|
||||
info
|
||||
}, material, {
|
||||
name: 'AES-CTR',
|
||||
length: 128
|
||||
}, false, [ 'encrypt', 'decrypt' ]);
|
||||
const authenticationKey = await crypto.subtle.deriveKey({
|
||||
name: 'HKDF',
|
||||
salt: textEncoder.encode('JFrameAuthenticationKey'),
|
||||
hash: 'SHA-256',
|
||||
info
|
||||
}, material, {
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
}, false, [ 'sign' ]);
|
||||
const saltKey = await crypto.subtle.deriveBits({
|
||||
name: 'HKDF',
|
||||
salt: textEncoder.encode('JFrameSaltKey'),
|
||||
hash: 'SHA-256',
|
||||
info
|
||||
}, material, 128);
|
||||
|
||||
return {
|
||||
material,
|
||||
encryptionKey,
|
||||
authenticationKey,
|
||||
saltKey
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ratchets a key. See
|
||||
* https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
|
||||
* @param {CryptoKey} material - base key material
|
||||
* @returns {ArrayBuffer} - ratcheted key material
|
||||
*/
|
||||
async function ratchet(material) {
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
|
||||
return crypto.subtle.deriveBits({
|
||||
name: 'HKDF',
|
||||
salt: textEncoder.encode('JFrameRatchetKey'),
|
||||
hash: 'SHA-256',
|
||||
info: new ArrayBuffer()
|
||||
}, material, 256);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a raw key into a WebCrypto key object with default options
|
||||
* suitable for our usage.
|
||||
* @param {ArrayBuffer} keyBytes - raw key
|
||||
* @param {Array} keyUsages - key usages, see importKey documentation
|
||||
* @returns {CryptoKey} - the WebCrypto key.
|
||||
*/
|
||||
async function importKey(keyBytes) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
|
||||
return crypto.subtle.importKey('raw', keyBytes, 'HKDF', false, [ 'deriveBits', 'deriveKey' ]);
|
||||
}
|
||||
|
||||
// CONCATENATED MODULE: ./modules/e2ee/utils.js
|
||||
/**
|
||||
* Polyfill RTCEncoded(Audio|Video)Frame.getMetadata() (not available in M83, available M84+).
|
||||
* The polyfill can not be done on the prototype since its not exposed in workers. Instead,
|
||||
* it is done as another transformation to keep it separate.
|
||||
* TODO: remove when we decode to drop M83 support.
|
||||
*/
|
||||
function polyFillEncodedFrameMetadata(encodedFrame, controller) {
|
||||
if (!encodedFrame.getMetadata) {
|
||||
encodedFrame.getMetadata = function() {
|
||||
return {
|
||||
// TODO: provide a more complete polyfill based on additionalData for video.
|
||||
synchronizationSource: this.synchronizationSource,
|
||||
contributingSources: this.contributingSources
|
||||
};
|
||||
};
|
||||
}
|
||||
controller.enqueue(encodedFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two byteArrays for equality.
|
||||
*/
|
||||
function isArrayEqual(a1, a2) {
|
||||
if (a1.byteLength !== a2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a1.byteLength; i++) {
|
||||
if (a1[i] !== a2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// CONCATENATED MODULE: ./modules/e2ee/Context.js
|
||||
/* eslint-disable no-bitwise */
|
||||
/* global BigInt */
|
||||
|
||||
|
||||
|
||||
|
||||
// We use a ringbuffer of keys so we can change them and still decode packets that were
|
||||
// encrypted with an old key. We use a size of 16 which corresponds to the four bits
|
||||
// in the frame trailer.
|
||||
const keyRingSize = 16;
|
||||
|
||||
// We copy the first bytes of the VP8 payload unencrypted.
|
||||
// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
|
||||
// https://tools.ietf.org/html/rfc6386#section-9.1
|
||||
// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
|
||||
// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
|
||||
// instead of being unable to decode).
|
||||
// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
|
||||
//
|
||||
// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
|
||||
// https://tools.ietf.org/html/rfc6716#section-3.1
|
||||
const unencryptedBytes = {
|
||||
key: 10,
|
||||
delta: 3,
|
||||
undefined: 1 // frame.type is not set on audio
|
||||
};
|
||||
|
||||
// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
|
||||
// This follows the same principles as DTLS-SRTP.
|
||||
const authenticationTagOptions = {
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256'
|
||||
};
|
||||
const digestLength = {
|
||||
key: 10,
|
||||
delta: 10,
|
||||
undefined: 4 // frame.type is not set on audio
|
||||
};
|
||||
|
||||
// Maximum number of forward ratchets to attempt when the authentication
|
||||
// tag on a remote packet does not match the current key.
|
||||
const ratchetWindow = 8;
|
||||
|
||||
/**
|
||||
* Per-participant context holding the cryptographic keys and
|
||||
* encode/decode functions
|
||||
*/
|
||||
class Context_Context {
|
||||
/**
|
||||
* @param {string} id - local muc resourcepart
|
||||
*/
|
||||
constructor(id) {
|
||||
// An array (ring) of keys that we use for sending and receiving.
|
||||
this._cryptoKeyRing = new Array(keyRingSize);
|
||||
|
||||
// A pointer to the currently used key.
|
||||
this._currentKeyIndex = -1;
|
||||
|
||||
// A per-sender counter that is used create the AES CTR.
|
||||
// Must be incremented on every frame that is sent, can be reset on
|
||||
// key changes.
|
||||
this._sendCount = BigInt(0); // eslint-disable-line new-cap
|
||||
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the different subkeys and starts using them for encryption or
|
||||
* decryption.
|
||||
* @param {Uint8Array|false} key bytes. Pass false to disable.
|
||||
* @param {Number} keyIndex
|
||||
*/
|
||||
async setKey(keyBytes, keyIndex) {
|
||||
let newKey;
|
||||
|
||||
if (keyBytes) {
|
||||
const material = await importKey(keyBytes);
|
||||
|
||||
newKey = await deriveKeys(material);
|
||||
} else {
|
||||
newKey = false;
|
||||
}
|
||||
this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
|
||||
this._setKeys(newKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a set of keys and resets the sendCount.
|
||||
* decryption.
|
||||
* @param {Object} keys set of keys.
|
||||
* @private
|
||||
*/
|
||||
_setKeys(keys) {
|
||||
this._cryptoKeyRing[this._currentKeyIndex] = keys;
|
||||
this._sendCount = BigInt(0); // eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that will be injected in a stream and will encrypt the given encoded frames.
|
||||
*
|
||||
* @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
||||
* @param {TransformStreamDefaultController} controller - TransportStreamController.
|
||||
*
|
||||
* The packet format is a variant of
|
||||
* https://tools.ietf.org/html/draft-omara-sframe-00
|
||||
* using a trailer instead of a header. One of the design goals was to not require
|
||||
* changes to the SFU which for video requires not encrypting the keyframe bit of VP8
|
||||
* as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
|
||||
* solve this eventually). This also "hides" that a client is using E2EE a bit.
|
||||
*
|
||||
* Note that this operates on the full frame, i.e. for VP8 the data described in
|
||||
* https://tools.ietf.org/html/rfc6386#section-9.1
|
||||
*
|
||||
* The VP8 payload descriptor described in
|
||||
* https://tools.ietf.org/html/rfc7741#section-4.2
|
||||
* is part of the RTP packet and not part of the encoded frame and is therefore not
|
||||
* controllable by us. This is fine as the SFU keeps having access to it for routing.
|
||||
*/
|
||||
encodeFunction(encodedFrame, controller) {
|
||||
const keyIndex = this._currentKeyIndex;
|
||||
|
||||
if (this._cryptoKeyRing[keyIndex]) {
|
||||
this._sendCount++;
|
||||
|
||||
// Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
||||
const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
||||
|
||||
// Construct frame trailer. Similar to the frame header described in
|
||||
// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
|
||||
// but we put it at the end.
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
||||
// payload | CTR... (length=LEN) |S|LEN |KID |
|
||||
// ---------+---------------------------------+-+-+-+-+-+-+-+-+
|
||||
const counter = new Uint8Array(16);
|
||||
const counterView = new DataView(counter.buffer);
|
||||
|
||||
// The counter is encoded as a variable-length field.
|
||||
counterView.setBigUint64(8, this._sendCount);
|
||||
let counterLength = 8;
|
||||
|
||||
for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
|
||||
if (counterView.getUint8(i) !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const frameTrailer = new Uint8Array(counterLength + 1);
|
||||
|
||||
frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));
|
||||
|
||||
// Since we never send a counter of 0 we send counterLength - 1 on the wire.
|
||||
// This is different from the sframe draft, increases the key space and lets us
|
||||
// ignore the case of a zero-length counter at the receiver.
|
||||
frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);
|
||||
|
||||
// XOR the counter with the saltKey to construct the AES CTR.
|
||||
const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
||||
|
||||
for (let i = 0; i < counter.byteLength; i++) {
|
||||
counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
|
||||
}
|
||||
|
||||
return crypto.subtle.encrypt({
|
||||
name: 'AES-CTR',
|
||||
counter,
|
||||
length: 64
|
||||
}, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
||||
unencryptedBytes[encodedFrame.type]))
|
||||
.then(cipherText => {
|
||||
const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
|
||||
+ digestLength[encodedFrame.type] + frameTrailer.byteLength);
|
||||
const newUint8 = new Uint8Array(newData);
|
||||
|
||||
newUint8.set(frameHeader); // copy first bytes.
|
||||
newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
|
||||
// Leave some space for the authentication tag. This is filled with 0s initially, similar to
|
||||
// STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
|
||||
newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
|
||||
+ digestLength[encodedFrame.type]); // append trailer.
|
||||
|
||||
return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
|
||||
new Uint8Array(newData)).then(authTag => {
|
||||
// Set the truncated authentication tag.
|
||||
newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
|
||||
unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
|
||||
encodedFrame.data = newData;
|
||||
|
||||
return controller.enqueue(encodedFrame);
|
||||
});
|
||||
}, e => {
|
||||
// TODO: surface this to the app.
|
||||
console.error(e);
|
||||
|
||||
// We are not enqueuing the frame here on purpose.
|
||||
});
|
||||
}
|
||||
|
||||
/* NOTE WELL:
|
||||
* This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
|
||||
* This is ok for demo purposes but should not be done once this becomes more relied upon.
|
||||
*/
|
||||
controller.enqueue(encodedFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that will be injected in a stream and will decrypt the given encoded frames.
|
||||
*
|
||||
* @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
|
||||
* @param {TransformStreamDefaultController} controller - TransportStreamController.
|
||||
*/
|
||||
async decodeFunction(encodedFrame, controller) {
|
||||
const data = new Uint8Array(encodedFrame.data);
|
||||
const keyIndex = data[encodedFrame.data.byteLength - 1] & 0xf; // lower four bits.
|
||||
|
||||
if (this._cryptoKeyRing[keyIndex]) {
|
||||
const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
|
||||
const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);
|
||||
|
||||
// Extract the truncated authentication tag.
|
||||
const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
|
||||
+ counterLength + 1);
|
||||
const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
|
||||
+ digestLength[encodedFrame.type]);
|
||||
|
||||
// Set authentication tag bytes to 0.
|
||||
const zeros = new Uint8Array(digestLength[encodedFrame.type]);
|
||||
|
||||
data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));
|
||||
|
||||
// Do truncated hash comparison. If the hash does not match we might have to advance the
|
||||
// ratchet a limited number of times. See (even though the description there is odd)
|
||||
// https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
|
||||
let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
|
||||
let valid = false;
|
||||
let newKeys = null;
|
||||
|
||||
for (let distance = 0; distance < ratchetWindow; distance++) {
|
||||
const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
|
||||
authenticationKey, encodedFrame.data);
|
||||
|
||||
if (isArrayEqual(new Uint8Array(authTag),
|
||||
new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
|
||||
valid = true;
|
||||
if (distance > 0) {
|
||||
this._setKeys(newKeys);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Attempt to ratchet and generate the next set of keys.
|
||||
material = await importKey(await ratchet(material));
|
||||
newKeys = await deriveKeys(material);
|
||||
authenticationKey = newKeys.authenticationKey;
|
||||
}
|
||||
|
||||
// Check whether we found a valid signature.
|
||||
if (!valid) {
|
||||
// TODO: return an error to the app.
|
||||
|
||||
console.error('Authentication tag mismatch');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the counter.
|
||||
const counter = new Uint8Array(16);
|
||||
|
||||
counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
|
||||
encodedFrame.data.byteLength - 1), 16 - counterLength);
|
||||
const counterView = new DataView(counter.buffer);
|
||||
|
||||
// XOR the counter with the saltKey to construct the AES CTR.
|
||||
const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);
|
||||
|
||||
for (let i = 0; i < counter.byteLength; i++) {
|
||||
counterView.setUint8(i,
|
||||
counterView.getUint8(i) ^ saltKey.getUint8(i));
|
||||
}
|
||||
|
||||
return crypto.subtle.decrypt({
|
||||
name: 'AES-CTR',
|
||||
counter,
|
||||
length: 64
|
||||
}, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
|
||||
unencryptedBytes[encodedFrame.type],
|
||||
encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
|
||||
+ digestLength[encodedFrame.type] + counterLength + 1))
|
||||
).then(plainText => {
|
||||
const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
|
||||
const newUint8 = new Uint8Array(newData);
|
||||
|
||||
newUint8.set(frameHeader);
|
||||
newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
|
||||
encodedFrame.data = newData;
|
||||
|
||||
return controller.enqueue(encodedFrame);
|
||||
}, e => {
|
||||
console.error(e);
|
||||
|
||||
// TODO: notify the application about error status.
|
||||
// TODO: For video we need a better strategy since we do not want to based any
|
||||
// non-error frames on a garbage keyframe.
|
||||
if (encodedFrame.type === undefined) { // audio, replace with silence.
|
||||
const newData = new ArrayBuffer(3);
|
||||
const newUint8 = new Uint8Array(newData);
|
||||
|
||||
newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
|
||||
encodedFrame.data = newData;
|
||||
controller.enqueue(encodedFrame);
|
||||
}
|
||||
});
|
||||
} else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
|
||||
// If we are encrypting but don't have a key for the remote drop the frame.
|
||||
// This is a heuristic since we don't know whether a packet is encrypted,
|
||||
// do not have a checksum and do not have signaling for whether a remote participant does
|
||||
// encrypt or not.
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
|
||||
// we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
|
||||
controller.enqueue(encodedFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// CONCATENATED MODULE: ./modules/e2ee/Worker.js
|
||||
/* global TransformStream */
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
// Worker for E2EE/Insertable streams.
|
||||
//
|
||||
|
||||
|
||||
|
||||
|
||||
const contexts = new Map(); // Map participant id => context
|
||||
|
||||
onmessage = async event => {
|
||||
const { operation } = event.data;
|
||||
|
||||
if (operation === 'encode') {
|
||||
const { readableStream, writableStream, participantId } = event.data;
|
||||
|
||||
if (!contexts.has(participantId)) {
|
||||
contexts.set(participantId, new Context_Context(participantId));
|
||||
}
|
||||
const context = contexts.get(participantId);
|
||||
const transformStream = new TransformStream({
|
||||
transform: context.encodeFunction.bind(context)
|
||||
});
|
||||
|
||||
readableStream
|
||||
.pipeThrough(new TransformStream({
|
||||
transform: polyFillEncodedFrameMetadata // M83 polyfill.
|
||||
}))
|
||||
.pipeThrough(transformStream)
|
||||
.pipeTo(writableStream);
|
||||
} else if (operation === 'decode') {
|
||||
const { readableStream, writableStream, participantId } = event.data;
|
||||
|
||||
if (!contexts.has(participantId)) {
|
||||
contexts.set(participantId, new Context_Context(participantId));
|
||||
}
|
||||
const context = contexts.get(participantId);
|
||||
const transformStream = new TransformStream({
|
||||
transform: context.decodeFunction.bind(context)
|
||||
});
|
||||
|
||||
readableStream
|
||||
.pipeThrough(new TransformStream({
|
||||
transform: polyFillEncodedFrameMetadata // M83 polyfill.
|
||||
}))
|
||||
.pipeThrough(transformStream)
|
||||
.pipeTo(writableStream);
|
||||
} else if (operation === 'setKey') {
|
||||
const { participantId, key, keyIndex } = event.data;
|
||||
|
||||
if (!contexts.has(participantId)) {
|
||||
contexts.set(participantId, new Context_Context(participantId));
|
||||
}
|
||||
const context = contexts.get(participantId);
|
||||
|
||||
if (key) {
|
||||
context.setKey(key, keyIndex);
|
||||
} else {
|
||||
context.setKey(false, keyIndex);
|
||||
}
|
||||
} else if (operation === 'cleanup') {
|
||||
const { participantId } = event.data;
|
||||
|
||||
contexts.delete(participantId);
|
||||
} else {
|
||||
console.error('e2ee worker', operation);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***/ })
|
||||
/******/ ]);
|
||||
+26
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
Executable
BIN
Binary file not shown.
Binary file not shown.
+2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
+42
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user