Added BREACH attack mittigation, #4084

Ylian Saint-Hilaire committed Jun 8, 2022 at 10:47 UTC ea7e98b3b4f61268811a956d9d9e20a398673728
24 files changed +723 -696
MeshCentralServer.njsproj
+1
@@ -613,6 +613,7 @@
613 <Content Include="views\messenger.handlebars" />
614 <Content Include="views\mstsc.handlebars" />
615 <Content Include="views\player.handlebars" />
616 + <Content Include="views\sharing-mobile.handlebars" />
617 <Content Include="views\sharing.handlebars" />
618 <Content Include="views\ssh.handlebars" />
619 <Content Include="views\terms-mobile.handlebars" />
meshcentral-config-schema.json
+1
@@ -120,6 +120,7 @@
120 "amtManager": { "type": "boolean", "default": true, "description": "When enabled, MeshCentral will automatically monitor and manage Intel AMT devices." },
121 "orphanAgentUser": { "type": "string", "default": null, "description": "If an agent attempts to connect to a unknown device group, automatically create a new device group and grant access to the specified user. Example: admin" },
122 "agentIdleTimeout": { "type": "integer", "minimum": 1, "default": 150 ,"description": "How much time in seconds with no traffic from an agent before dropping the agent connection." },
123 + "webPageLengthRandomization": { "type": "boolean", "default": true, "description": "Adds a random length string to generated web pages to mitigate a BREACH attack." },
124 "compression": { "type": "boolean", "default": true, "description": "Enables GZIP compression for web requests." },
125 "wsCompression": { "type": "boolean", "default": false, "description": "Enables server-side, websocket per-message deflate compression." },
126 "agentWsCompression": { "type": "boolean", "default": true, "description": "Enables agent-side, websocket per-message deflate compression. wscompression must also be true for this to work." },
public/translate.bat
+2 -2
@@ -1,7 +1,7 @@
1 @ECHO OFF
2 CD ..\translate
3 %LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js minifyall
4 -%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
5 -%LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall
4 +REM %LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js translateall
5 +REM %LOCALAPPDATA%\..\Roaming\nvm\v12.13.0\node translate.js extractall
6 DEL ..\emails\translations\*-min_*
7 Pause
\ No newline at end of file
views/agentinvite.handlebars
+2
@@ -167,6 +167,8 @@
167 </div>
168 <script>
169 'use strict';
170 +
171 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
172 var uiMode = parseInt(getstore('uiMode', 1));
173 var webPageStackMenu = false;
174 var webPageFullScreen = true;
views/default-mobile.handlebars
+1
@@ -1192,6 +1192,7 @@
1192 <iframe name="fileUploadFrame" style=display:none></iframe>
1193 <script>
1194 'use strict';
1195 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
1196
1197 // Process server-side web state
1198 var webState = '{{{webstate}}}';
views/default.handlebars
+1
@@ -1402,6 +1402,7 @@
1402 </div>
1403 <script type="text/javascript">
1404 'use strict';
1405 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
1406
1407 // Process server-side web state
1408 var webState = '{{{webstate}}}';
views/download.handlebars
+1
@@ -43,6 +43,7 @@
43 </div>
44 </div>
45 <script>
46 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
47 var messageid = parseInt('{{{messageid}}}');
48 var fileurl = '{{{fileurl}}}';
49 var filename = '{{{filename}}}';
views/download2.handlebars
+1
@@ -57,6 +57,7 @@
57 </tr>
58 </table>
59 <script>
60 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
61 var messageid = parseInt('{{{messageid}}}');
62 var fileurl = '{{{fileurl}}}';
63 var filename = '{{{filename}}}';
views/error404.handlebars
+1
@@ -148,6 +148,7 @@
148 </div>
149 <script nonce="{{{cspNonce}}}">
150 'use strict';
151 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
152 var uiMode = parseInt(getstore('uiMode', 1));
153 var webPageStackMenu = false;
154 var webPageFullScreen = true;
views/invite.handlebars
+1
@@ -103,6 +103,7 @@
103 </div>
104 <script>
105 'use strict';
106 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
107 var urlargs = parseUriArgs();
108 if (urlargs.key && (isAlphaNumeric(urlargs.key) == false)) { delete urlargs.key; }
109 var uiMode = parseInt(getstore('uiMode', 1));
views/login-mobile.handlebars
+1
@@ -311,6 +311,7 @@
311 </div>
312 <script>
313 'use strict';
314 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
315 var loginMode = '{{{loginmode}}}';
316 var newAccount = '{{{newAccount}}}';
317 var passhint = '{{{passhint}}}';
views/login.handlebars
+1
@@ -305,6 +305,7 @@
305 </div>
306 <script>
307 'use strict';
308 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
309 var passlogin = '{{{passlogin}}}';
310 var passhint = '{{{passhint}}}';
311 var loginMode = '{{{loginmode}}}';
views/login2.handlebars
+1
@@ -361,6 +361,7 @@
361 </div>
362 <script>
363 'use strict';
364 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
365 var welcomePictureFullScreen = (decodeURIComponent('{{{welcomePictureFullScreen}}}') === 'true');
366 var passlogin = '{{{passlogin}}}';
367 var passhint = '{{{passhint}}}';
views/message.handlebars
+1
@@ -43,6 +43,7 @@
43 </div>
44 </div>
45 <script>
46 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
47 var titleid = parseInt('{{{titleid}}}');
48 var msgid = parseInt('{{{msgid}}}');
49 var domainurl = decodeURIComponent('{{{domainurl}}}');
views/message2.handlebars
+1
@@ -44,6 +44,7 @@
44 </tr>
45 </table>
46 <script>
47 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
48 var titleid = parseInt('{{{titleid}}}');
49 var msgid = parseInt('{{{msgid}}}');
50 var domainurl = decodeURIComponent('{{{domainurl}}}');
views/messenger.handlebars
+695 -694
@@ -60,745 +60,746 @@
60 <canvas width="256" height="256" id="remoteImage" style="position:absolute;right:24px;top:45px;width:200px;height:200px;background-color:gray;border-radius:12px 12px 12px 12px;box-shadow:3px 3px 10px gray;display:none" />
61 <input id="uploadFileInput" type="file" multiple style="display:none">
62 <script type="text/javascript" onunload="onUnLoad()">
63 - var userInputFocus = 0;
64 - var socket = null; // Websocket object
65 - var state = 0; // Connection state. 0 = Disconnected, 1 = Connecting, 2 = Connected.
66 - var args = XparseUriArgs();
67 - if (args.key && (isAlphaNumeric(args.key) == false)) { delete args.key; }
68 - if (args.locale && (isAlphaNumeric(args.locale) == false)) { delete args.locale; }
69 - var pushMessaging = (args.pmt == 1);
70 -
71 - // WebRTC sessions and data, audio and video channels
72 - var random = Math.random(); // Selected random, larger value initiates WebRTC.
73 - var webrtcSessions = { }; // WebRTC objects: 0 for data, 1 for outbound audio/video, 2 for inbound audio/video
74 - var webchannel = null; // WebRTC data channel
75 - var localStream = null;
76 - var remoteStream = null;
77 - var multiWebRtc = true; // if set to true, multiple WebRTC sessions will be setup. If false, everything uses one session.
78 - var userMediaSupport = 0;
79 - var notification = null;
80 - getUserMediaSupport(function (x) { userMediaSupport = x; })
81 - var meshMessengerTitle = '{{{meshMessengerTitle}}}';
82 - var meshMessengerImage = '{{{meshMessengerImage}}}';
83 - var remoteUserName = '{{{username}}}';
84 - var remoteUserId = '{{{userid}}}';
85 - var webrtcconfiguration = '{{{webrtconfig}}}';
86 - if (webrtcconfiguration == '') { webrtcconfiguration = null; } else { try { webrtcconfiguration = JSON.parse(decodeURIComponent(webrtcconfiguration)); } catch (ex) { console.log('Invalid WebRTC config: "' + webrtcconfiguration + '".'); webrtcconfiguration = null; } }
87 - var windowFocus = true;
88 - var chatTextSession = new Date().toString() + '\r\n';
89 - var localOutText = false;
90 - var remoteOutText = false;
91 - var remoteImage = false;
92 - var serverRecording = false;
93 - var notificationSupport = true;
94 -
95 - // File transfer state
96 - var fileUploads = [];
97 - var fileDownloads = {};
98 - var currentFileUpload = null;
99 - var currentFileDownload = null;
100 -
101 - setInterval(resizeVideos, 1000);
102 - function resizeVideos() {
103 - var rheight = 0;
104 - if (remoteStream != null) {
105 - rheight = ((320 * Q('remoteVideoCanvas').videoHeight) / Q('remoteVideoCanvas').videoWidth);
106 - QS('remoteVideo').height = 'calc(' + rheight + 'px + 30px)';
107 - } else {
108 - if (remoteImage == true) { rheight += 200; }
109 - }
110 - if (localStream != null) {
111 - QS('localVideo').height = 'calc(' + ((160 * Q('localVideoCanvas').videoHeight) / Q('localVideoCanvas').videoWidth) + 'px + 30px)';
112 - QS('localVideo').top = (rheight + 50 + ((remoteStream != null)?30:0)) + 'px';
113 - }
114 - }
115 -
116 - // Set the title
117 - var newTitle = '';
118 - if (args.title) { newTitle = decodeURIComponent(args.title); document.title = document.title + ' - ' + decodeURIComponent(args.title); }
119 - else if (meshMessengerTitle == '!') { newTitle = "MeshMessenger"; } else { newTitle = decodeURIComponent(meshMessengerTitle); }
120 - newTitle = newTitle.split('{0}').join(decodeURIComponent(remoteUserName)).split('{1}').join(decodeURIComponent(remoteUserId));
121 - QH('xtitle', EscapeHtml(newTitle).split(' ').join('&nbsp'));
122 -
123 - // Setup web notifications
124 - try { if (Notification) { QV('notifyButton', Notification.permission != 'granted'); } } catch (ex) { notificationSupport = false; }
125 -
126 - // Track window focus
127 - window.addEventListener('focus', function (event) { windowFocus = true; }, false);
128 - window.addEventListener('blur', function (event) { windowFocus = false; }, false);
129 -
130 - // Listen to drag & drop events
131 - document.addEventListener('dragover', haltEvent, false);
132 - document.addEventListener('dragleave', haltEvent, false);
133 - document.addEventListener('drop', fileDrop, false);
134 -
135 - document.onclick = function (e) {
136 - if (notificationSupport) {
137 - if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
138 - if (notification != null) { notification.close(); notification = null; }
63 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
64 + var userInputFocus = 0;
65 + var socket = null; // Websocket object
66 + var state = 0; // Connection state. 0 = Disconnected, 1 = Connecting, 2 = Connected.
67 + var args = XparseUriArgs();
68 + if (args.key && (isAlphaNumeric(args.key) == false)) { delete args.key; }
69 + if (args.locale && (isAlphaNumeric(args.locale) == false)) { delete args.locale; }
70 + var pushMessaging = (args.pmt == 1);
71 +
72 + // WebRTC sessions and data, audio and video channels
73 + var random = Math.random(); // Selected random, larger value initiates WebRTC.
74 + var webrtcSessions = { }; // WebRTC objects: 0 for data, 1 for outbound audio/video, 2 for inbound audio/video
75 + var webchannel = null; // WebRTC data channel
76 + var localStream = null;
77 + var remoteStream = null;
78 + var multiWebRtc = true; // if set to true, multiple WebRTC sessions will be setup. If false, everything uses one session.
79 + var userMediaSupport = 0;
80 + var notification = null;
81 + getUserMediaSupport(function (x) { userMediaSupport = x; })
82 + var meshMessengerTitle = '{{{meshMessengerTitle}}}';
83 + var meshMessengerImage = '{{{meshMessengerImage}}}';
84 + var remoteUserName = '{{{username}}}';
85 + var remoteUserId = '{{{userid}}}';
86 + var webrtcconfiguration = '{{{webrtconfig}}}';
87 + if (webrtcconfiguration == '') { webrtcconfiguration = null; } else { try { webrtcconfiguration = JSON.parse(decodeURIComponent(webrtcconfiguration)); } catch (ex) { console.log('Invalid WebRTC config: "' + webrtcconfiguration + '".'); webrtcconfiguration = null; } }
88 + var windowFocus = true;
89 + var chatTextSession = new Date().toString() + '\r\n';
90 + var localOutText = false;
91 + var remoteOutText = false;
92 + var remoteImage = false;
93 + var serverRecording = false;
94 + var notificationSupport = true;
95 +
96 + // File transfer state
97 + var fileUploads = [];
98 + var fileDownloads = {};
99 + var currentFileUpload = null;
100 + var currentFileDownload = null;
101 +
102 + setInterval(resizeVideos, 1000);
103 + function resizeVideos() {
104 + var rheight = 0;
105 + if (remoteStream != null) {
106 + rheight = ((320 * Q('remoteVideoCanvas').videoHeight) / Q('remoteVideoCanvas').videoWidth);
107 + QS('remoteVideo').height = 'calc(' + rheight + 'px + 30px)';
108 + } else {
109 + if (remoteImage == true) { rheight += 200; }
110 + }
111 + if (localStream != null) {
112 + QS('localVideo').height = 'calc(' + ((160 * Q('localVideoCanvas').videoHeight) / Q('localVideoCanvas').videoWidth) + 'px + 30px)';
113 + QS('localVideo').top = (rheight + 50 + ((remoteStream != null)?30:0)) + 'px';
114 + }
115 + }
116 +
117 + // Set the title
118 + var newTitle = '';
119 + if (args.title) { newTitle = decodeURIComponent(args.title); document.title = document.title + ' - ' + decodeURIComponent(args.title); }
120 + else if (meshMessengerTitle == '!') { newTitle = "MeshMessenger"; } else { newTitle = decodeURIComponent(meshMessengerTitle); }
121 + newTitle = newTitle.split('{0}').join(decodeURIComponent(remoteUserName)).split('{1}').join(decodeURIComponent(remoteUserId));
122 + QH('xtitle', EscapeHtml(newTitle).split(' ').join('&nbsp'));
123 +
124 + // Setup web notifications
125 + try { if (Notification) { QV('notifyButton', Notification.permission != 'granted'); } } catch (ex) { notificationSupport = false; }
126 +
127 + // Track window focus
128 + window.addEventListener('focus', function (event) { windowFocus = true; }, false);
129 + window.addEventListener('blur', function (event) { windowFocus = false; }, false);
130 +
131 + // Listen to drag & drop events
132 + document.addEventListener('dragover', haltEvent, false);
133 + document.addEventListener('dragleave', haltEvent, false);
134 + document.addEventListener('drop', fileDrop, false);
135 +
136 + document.onclick = function (e) {
137 + if (notificationSupport) {
138 + if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
139 + if (notification != null) { notification.close(); notification = null; }
140 + }
141 + }
142 +
143 + // Trap document key up events
144 + document.onkeyup = function ondockeypress(e) {
145 + if (notificationSupport) {
146 + if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
147 + if (notification != null) { notification.close(); notification = null; }
148 + }
149 + if (state == 2) {
150 + if ((e.keyCode == 8) && (userInputFocus == 0)) {
151 + // Backspace
152 + var outtext = Q('xouttext').value;
153 + if (outtext.length > 0) { Q('xouttext').value = outtext.substring(0, outtext.length - 1); }
154 + updateLocalOutText();
155 }
156 }
157 + if (userInputFocus == 0) { haltEvent(e); return false; }
158 + }
159
142 - // Trap document key up events
143 - document.onkeyup = function ondockeypress(e) {
144 - if (notificationSupport) {
145 - if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
146 - if (notification != null) { notification.close(); notification = null; }
147 - }
148 - if (state == 2) {
149 - if ((e.keyCode == 8) && (userInputFocus == 0)) {
150 - // Backspace
151 - var outtext = Q('xouttext').value;
152 - if (outtext.length > 0) { Q('xouttext').value = outtext.substring(0, outtext.length - 1); }
153 - updateLocalOutText();
154 - }
155 - }
156 - if (userInputFocus == 0) { haltEvent(e); return false; }
157 - }
158 -
159 - // Trap document key presses
160 - document.onkeypress = function ondockeypress(e) {
161 - if (notificationSupport) {
162 - if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
163 - if (notification != null) { notification.close(); notification = null; }
164 - }
165 - if ((state == 2) || pushMessaging) {
166 - if (e.keyCode == 13) {
167 - // Return
168 - xsend(e);
169 - } else {
170 - // Any other key
171 - if ((userInputFocus == 0) && (e.key.length == 1)) { Q('xouttext').value = Q('xouttext').value + e.key; updateLocalOutText(); }
172 - }
173 - }
174 - if (userInputFocus == 0) { haltEvent(e); return false; }
160 + // Trap document key presses
161 + document.onkeypress = function ondockeypress(e) {
162 + if (notificationSupport) {
163 + if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
164 + if (notification != null) { notification.close(); notification = null; }
165 }
176 -
177 - function onUserInputFocus(x) { userInputFocus = x; }
178 - function displayClear() { chatTextSession = new Date().toString() + '\r\n'; QH('xmsg', ''); cancelAllFileTransfers(); fileUploads = [], fileDownloads = {}; }
179 -
180 - // Polyfill FileReader if needed
181 - if (!FileReader.prototype.readAsBinaryString) {
182 - FileReader.prototype.readAsBinaryString = function (fileData) {
183 - var binary = '', self = this, reader = new FileReader();
184 - reader.onload = function (e) {
185 - var bytes = new Uint8Array(reader.result);
186 - for (var i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); }
187 - self.onload({ target: { result: binary } });
188 - }
189 - reader.readAsArrayBuffer(fileData);
166 + if ((state == 2) || pushMessaging) {
167 + if (e.keyCode == 13) {
168 + // Return
169 + xsend(e);
170 + } else {
171 + // Any other key
172 + if ((userInputFocus == 0) && (e.key.length == 1)) { Q('xouttext').value = Q('xouttext').value + e.key; updateLocalOutText(); }
173 }
174 }
175 + if (userInputFocus == 0) { haltEvent(e); return false; }
176 + }
177
193 - // Detect if microphone & camera are present
194 - // 0 = nomedia, 1 = miconly, 2 = mic&cam
195 - function getUserMediaSupport(func) {
196 - try {
197 - navigator.mediaDevices.enumerateDevices().then(function (devices) {
198 - try {
199 - var mic = 0, cam = 0;
200 - devices.forEach(function (device) {
201 - if (device.kind === 'audioinput') { mic = 1; }
202 - if (device.kind === 'videoinput') { cam = 1; }
203 - });
204 - if (mic == 0) { func(0); }
205 - func(mic + cam);
206 - } catch (ex) { }
207 - })
208 - } catch (ex) { }
209 - }
210 -
211 - // Display a control message
212 - function displayControl(msg) {
213 - chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Control" + '> ' + msg + '\r\n');
214 - QA('xmsg', '<div style="clear:both"><div style="color:gray;float:left;margin-bottom:2px">' + EscapeHtml(msg) + '</div><div></div></div>');
215 - Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;//Q('xmsg').scrollHeight;
216 - }
217 -
218 - function displayLocalVideo(active) { QV('localVideo', active); adjustVideoWindows(); }
219 - function displayRemoteVideo(active) { QV('remoteVideo', active); adjustVideoWindows(); }
220 - function adjustVideoWindows() {
221 - var lv = (QS('localVideo')['display'] != 'none');
222 - var rv = (QS('remoteVideo')['display'] != 'none');
223 - QV('remoteImage', (remoteImage == true) && (rv == false))
224 - if (rv) { QS('localVideo')['top'] = '320px'; }
225 - else if (remoteImage) { QS('localVideo')['top'] = '320px'; }
226 - else { QS('localVideo')['top'] = '45px'; }
227 - resizeVideos();
228 - }
178 + function onUserInputFocus(x) { userInputFocus = x; }
179 + function displayClear() { chatTextSession = new Date().toString() + '\r\n'; QH('xmsg', ''); cancelAllFileTransfers(); fileUploads = [], fileDownloads = {}; }
180
230 - // Display a message from the remote user
231 - function displayRemote(msg) {
232 - chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Remote" + '> ' + msg + '\r\n');
233 - QA('xmsg', '<div style="clear:both"><div class="remoteBubble">' + EscapeHtml(msg) + '</div><div></div></div>');
234 - Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
235 -
236 - // If web notifications are granted, use it.
237 - if (notificationSupport) {
238 - if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
239 - if (Notification && (windowFocus == false) && (Notification.permission == 'granted')) {
240 - if (notification != null) { notification.close(); notification = null; }
241 - notification = new Notification(Q('xtitle').innerHTML.split('&nbsp;').join(' '), { body: msg });
242 - }
181 + // Polyfill FileReader if needed
182 + if (!FileReader.prototype.readAsBinaryString) {
183 + FileReader.prototype.readAsBinaryString = function (fileData) {
184 + var binary = '', self = this, reader = new FileReader();
185 + reader.onload = function (e) {
186 + var bytes = new Uint8Array(reader.result);
187 + for (var i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); }
188 + self.onload({ target: { result: binary } });
189 }
244 - }
245 -
246 - // Display and send a message from the local user
247 - function xsend(event) {
248 - if (notificationSupport) {
190 + reader.readAsArrayBuffer(fileData);
191 + }
192 + }
193 +
194 + // Detect if microphone & camera are present
195 + // 0 = nomedia, 1 = miconly, 2 = mic&cam
196 + function getUserMediaSupport(func) {
197 + try {
198 + navigator.mediaDevices.enumerateDevices().then(function (devices) {
199 + try {
200 + var mic = 0, cam = 0;
201 + devices.forEach(function (device) {
202 + if (device.kind === 'audioinput') { mic = 1; }
203 + if (device.kind === 'videoinput') { cam = 1; }
204 + });
205 + if (mic == 0) { func(0); }
206 + func(mic + cam);
207 + } catch (ex) { }
208 + })
209 + } catch (ex) { }
210 + }
211 +
212 + // Display a control message
213 + function displayControl(msg) {
214 + chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Control" + '> ' + msg + '\r\n');
215 + QA('xmsg', '<div style="clear:both"><div style="color:gray;float:left;margin-bottom:2px">' + EscapeHtml(msg) + '</div><div></div></div>');
216 + Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;//Q('xmsg').scrollHeight;
217 + }
218 +
219 + function displayLocalVideo(active) { QV('localVideo', active); adjustVideoWindows(); }
220 + function displayRemoteVideo(active) { QV('remoteVideo', active); adjustVideoWindows(); }
221 + function adjustVideoWindows() {
222 + var lv = (QS('localVideo')['display'] != 'none');
223 + var rv = (QS('remoteVideo')['display'] != 'none');
224 + QV('remoteImage', (remoteImage == true) && (rv == false))
225 + if (rv) { QS('localVideo')['top'] = '320px'; }
226 + else if (remoteImage) { QS('localVideo')['top'] = '320px'; }
227 + else { QS('localVideo')['top'] = '45px'; }
228 + resizeVideos();
229 + }
230 +
231 + // Display a message from the remote user
232 + function displayRemote(msg) {
233 + chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Remote" + '> ' + msg + '\r\n');
234 + QA('xmsg', '<div style="clear:both"><div class="remoteBubble">' + EscapeHtml(msg) + '</div><div></div></div>');
235 + Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
236 +
237 + // If web notifications are granted, use it.
238 + if (notificationSupport) {
239 + if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
240 + if (Notification && (windowFocus == false) && (Notification.permission == 'granted')) {
241 if (notification != null) { notification.close(); notification = null; }
250 - if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
251 - }
252 - var outtext = Q('xouttext').value;
253 - if (outtext.length > 0) {
254 - chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Local" + '> ' + outtext + '\r\n');
255 - QA('xmsg', '<div style="clear:both"><div class="localBubble">' + EscapeHtml(outtext) + '</div><div></div></div>');
256 - Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
257 - if ((state == 2) || pushMessaging) { send({ action: 'chat', msg: outtext }); }
258 - Q('xouttext').value = '';
259 - Q('xouttext').focus();
260 - localOutText = false;
242 + notification = new Notification(Q('xtitle').innerHTML.split('&nbsp;').join(' '), { body: msg });
243 }
244 }
245 + }
246
264 - function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
265 -
266 - // Update user controls
267 - function updateControls() {
268 - QE('sendButton', (state == 2) || pushMessaging);
269 - QE('clearButton', (state == 2) || pushMessaging);
270 - QE('xouttext', (state == 2) || pushMessaging);
271 - QV('fileButton', state == 2);
272 - QV('camButton', webchannel && webchannel.ok && !localStream && (userMediaSupport == 2));
273 - QV('micButton', webchannel && webchannel.ok && !localStream && (userMediaSupport > 0));
274 - QV('hangupButton', webchannel && webchannel.ok && localStream);
275 - updateLocalOutText();
247 + // Display and send a message from the local user
248 + function xsend(event) {
249 + if (notificationSupport) {
250 + if (notification != null) { notification.close(); notification = null; }
251 + if (Notification) { QV('notifyButton', Notification.permission != 'granted'); }
252 }
277 -
278 - // This is the WebRTC setup
279 - function startWebRTC(id, startDataChannel) {
280 - if ((webrtcSessions[0] != null) && (multiWebRtc == false)) { return webrtcSessions[0]; };
281 -
282 - // Setup the WebRTC object
283 - var webrtc = null;
284 - if (typeof RTCPeerConnection !== 'undefined') { webrtc = new RTCPeerConnection(webrtcconfiguration); }
285 - else if (typeof webkitRTCPeerConnection !== 'undefined') { webrtc = new webkitRTCPeerConnection(webrtcconfiguration); }
286 - if (webrtc == null) return null; // No WebRTC support.
287 -
288 - webrtc.id = id;
289 - webrtc.onicecandidate = function (e) { try { if (e.candidate != null) { sendws({ action: 'webRtcIce', ice: e.candidate, id: this.id }); } } catch (ex) { } }
290 - webrtc.oniceconnectionstatechange = function () { if (webrtc && webrtc.iceConnectionState == 'failed') { webrtc.close(); if (webrtcSessions[webrtc.id]) { delete webrtcSessions[webrtc.id]; } } }
291 - webrtc.ondatachannel = function (ev) {
292 - //console.log('ondatachannel');
293 - webchannel = ev.channel;
294 - webchannel.onmessage = function (event) { processMessage(event.data, 2); };
295 - webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
296 - webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
297 - }
298 - webrtc.onnegotiationneeded = function (event) {
299 - if (webrtc.holdTimer != null) return;
300 - webrtc.holdTimer = setTimeout(function () { // This time is needed to keep Chrome from being to excited. Wait until we add all tracks before kicking this off.
301 - //console.log('onnegotiationneeded', id);
302 - webrtc.holdTimer = null;
303 - webrtc.createOffer(function (offer) { /*console.log('offer', offer.sdp.length);*/ webrtc.setLocalDescription(offer, function () { sendws({ action: 'webRtcSdp', sdp: offer, id: id }); }, function () { hangUpButtonClick(id); }); }, function () { hangUpButtonClick(id); });
304 - }, 20);
305 - }
306 - webrtc.ontrack = function (event) {
307 - //console.log('ontrack', id);
308 - QV('remoteClickToView', false);
309 - var video = Q('remoteVideoCanvas');
310 - video.srcObject = remoteStream = event.streams[0];
311 - video.onloadedmetadata = function (e) {
312 - var promise = video.play();
313 - if (promise !== undefined) {
314 - promise.then(function() {
315 - // Video start ok
316 - }).catch(function(err) {
317 - // Video start error, display a play button
318 - QV('remoteClickToView', true);
319 - })
320 - }
321 - };
322 - displayRemoteVideo(true);
323 - }
324 - //webrtc.onremovetrack = function (event) { console.log('onremovetrack'); }
325 - //webrtc.onicegatheringstatechange = function (event) { console.log('onicegatheringstatechange', event); }
326 - //webrtc.onsignalingstatechange = function (event) { console.log('onsignalingstatechange', event); }
327 -
328 - // Initiate the WebRTC offer or handle the offer from the peer.
329 - if (startDataChannel == true) {
330 - webchannel = webrtc.createDataChannel('DataChannel', {}); // { ordered: false, maxRetransmits: 2 }
331 - webchannel.onmessage = function (event) { processMessage(event.data, 2); };
332 - webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
333 - webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
334 - }
335 -
336 - webrtcSessions[id] = webrtc;
337 - return webrtc;
338 - }
339 -
340 - function remotePlay() { QV('remoteClickToView', false); Q('remoteVideoCanvas').play(); }
341 -
342 - function webRtcHandleOffer(id, description) {
343 - //console.log('webRtcHandleOffer', description.sdp.length);
344 - var webrtc = webrtcSessions[id];
345 - if (webrtc) {
346 - webrtc.setRemoteDescription(new RTCSessionDescription(description), function () {
347 - if (description.type == 'offer') {
348 - webrtc.createAnswer(function (answer) {
349 - webrtc.setLocalDescription(answer, function (a, b) {
350 - try { sendws({ action: 'webRtcSdp', sdp: answer, id: id }); } catch (ex) { }
351 - }, function () { hangUpButtonClick(id); });
253 + var outtext = Q('xouttext').value;
254 + if (outtext.length > 0) {
255 + chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Local" + '> ' + outtext + '\r\n');
256 + QA('xmsg', '<div style="clear:both"><div class="localBubble">' + EscapeHtml(outtext) + '</div><div></div></div>');
257 + Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
258 + if ((state == 2) || pushMessaging) { send({ action: 'chat', msg: outtext }); }
259 + Q('xouttext').value = '';
260 + Q('xouttext').focus();
261 + localOutText = false;
262 + }
263 + }
264 +
265 + function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
266 +
267 + // Update user controls
268 + function updateControls() {
269 + QE('sendButton', (state == 2) || pushMessaging);
270 + QE('clearButton', (state == 2) || pushMessaging);
271 + QE('xouttext', (state == 2) || pushMessaging);
272 + QV('fileButton', state == 2);
273 + QV('camButton', webchannel && webchannel.ok && !localStream && (userMediaSupport == 2));
274 + QV('micButton', webchannel && webchannel.ok && !localStream && (userMediaSupport > 0));
275 + QV('hangupButton', webchannel && webchannel.ok && localStream);
276 + updateLocalOutText();
277 + }
278 +
279 + // This is the WebRTC setup
280 + function startWebRTC(id, startDataChannel) {
281 + if ((webrtcSessions[0] != null) && (multiWebRtc == false)) { return webrtcSessions[0]; };
282 +
283 + // Setup the WebRTC object
284 + var webrtc = null;
285 + if (typeof RTCPeerConnection !== 'undefined') { webrtc = new RTCPeerConnection(webrtcconfiguration); }
286 + else if (typeof webkitRTCPeerConnection !== 'undefined') { webrtc = new webkitRTCPeerConnection(webrtcconfiguration); }
287 + if (webrtc == null) return null; // No WebRTC support.
288 +
289 + webrtc.id = id;
290 + webrtc.onicecandidate = function (e) { try { if (e.candidate != null) { sendws({ action: 'webRtcIce', ice: e.candidate, id: this.id }); } } catch (ex) { } }
291 + webrtc.oniceconnectionstatechange = function () { if (webrtc && webrtc.iceConnectionState == 'failed') { webrtc.close(); if (webrtcSessions[webrtc.id]) { delete webrtcSessions[webrtc.id]; } } }
292 + webrtc.ondatachannel = function (ev) {
293 + //console.log('ondatachannel');
294 + webchannel = ev.channel;
295 + webchannel.onmessage = function (event) { processMessage(event.data, 2); };
296 + webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
297 + webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
298 + }
299 + webrtc.onnegotiationneeded = function (event) {
300 + if (webrtc.holdTimer != null) return;
301 + webrtc.holdTimer = setTimeout(function () { // This time is needed to keep Chrome from being to excited. Wait until we add all tracks before kicking this off.
302 + //console.log('onnegotiationneeded', id);
303 + webrtc.holdTimer = null;
304 + webrtc.createOffer(function (offer) { /*console.log('offer', offer.sdp.length);*/ webrtc.setLocalDescription(offer, function () { sendws({ action: 'webRtcSdp', sdp: offer, id: id }); }, function () { hangUpButtonClick(id); }); }, function () { hangUpButtonClick(id); });
305 + }, 20);
306 + }
307 + webrtc.ontrack = function (event) {
308 + //console.log('ontrack', id);
309 + QV('remoteClickToView', false);
310 + var video = Q('remoteVideoCanvas');
311 + video.srcObject = remoteStream = event.streams[0];
312 + video.onloadedmetadata = function (e) {
313 + var promise = video.play();
314 + if (promise !== undefined) {
315 + promise.then(function() {
316 + // Video start ok
317 + }).catch(function(err) {
318 + // Video start error, display a play button
319 + QV('remoteClickToView', true);
320 + })
321 + }
322 + };
323 + displayRemoteVideo(true);
324 + }
325 + //webrtc.onremovetrack = function (event) { console.log('onremovetrack'); }
326 + //webrtc.onicegatheringstatechange = function (event) { console.log('onicegatheringstatechange', event); }
327 + //webrtc.onsignalingstatechange = function (event) { console.log('onsignalingstatechange', event); }
328 +
329 + // Initiate the WebRTC offer or handle the offer from the peer.
330 + if (startDataChannel == true) {
331 + webchannel = webrtc.createDataChannel('DataChannel', {}); // { ordered: false, maxRetransmits: 2 }
332 + webchannel.onmessage = function (event) { processMessage(event.data, 2); };
333 + webchannel.onopen = function () { webchannel.ok = true; updateControls(); if (serverRecording == false) { sendws({ action: 'rtcSwitch', v: 0 }); } };
334 + webchannel.onclose = function (event) { if (webchannel && webchannel.ok) { disconnect(); } else { hangUpButtonClick(0); } }
335 + }
336 +
337 + webrtcSessions[id] = webrtc;
338 + return webrtc;
339 + }
340 +
341 + function remotePlay() { QV('remoteClickToView', false); Q('remoteVideoCanvas').play(); }
342 +
343 + function webRtcHandleOffer(id, description) {
344 + //console.log('webRtcHandleOffer', description.sdp.length);
345 + var webrtc = webrtcSessions[id];
346 + if (webrtc) {
347 + webrtc.setRemoteDescription(new RTCSessionDescription(description), function () {
348 + if (description.type == 'offer') {
349 + webrtc.createAnswer(function (answer) {
350 + webrtc.setLocalDescription(answer, function (a, b) {
351 + try { sendws({ action: 'webRtcSdp', sdp: answer, id: id }); } catch (ex) { }
352 }, function () { hangUpButtonClick(id); });
353 - }
354 - }, function () { hangUpButtonClick(id); });
353 + }, function () { hangUpButtonClick(id); });
354 + }
355 + }, function () { hangUpButtonClick(id); });
356 + }
357 + }
358 +
359 + // Indicate to peer that data traffic will no longer be sent over websocket and start holding traffic.
360 + function performWebRtcSwitch() {
361 + if (!serverRecording && webchannel && webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; }
362 + }
363 +
364 + // Disconnect everything
365 + function disconnect() {
366 + serverRecording = false;
367 + QV('recordIcon', false);
368 + if (state > 0) { displayControl("Connection closed."); }
369 + if (state > 1) { setTimeout(start, 500); }
370 + cancelAllFileTransfers();
371 + hangUpButtonClick(0, true); // Data channel
372 + hangUpButtonClick(1, true); // Local audio/video
373 + hangUpButtonClick(2, true); // Remote audio/video
374 + if (socket != null) { socket.close(); socket = null; }
375 + updateControls();
376 + state = 0;
377 + remoteImage = false;
378 + QV('remoteImage', false);
379 + updateLocalOutText();
380 + updateRemoteOutText();
381 + }
382 +
383 + // Send data over the current transport (WebRTC first)
384 + function send(data) {
385 + if ((state != 2) && (pushMessaging == false)) return; // If not in connected state, ignore this.
386 + if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
387 + if (!serverRecording && webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it.
388 + else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that.
389 + }
390 +
391 + // Send data over the websocket transport (WebSocket only)
392 + function sendws(data) {
393 + if (state != 2) return;
394 + //console.log('SEND', data);
395 + if (typeof data == 'object') { data = JSON.stringify(data); }
396 + if (socket != null) { socket.send(data); }
397 + }
398 +
399 + // WebRTC id switcher (0 -> 0, 1 -> 2, 2 -> 1)
400 + function webRtcIdSwitch(id) { if (id == 0) { return 0; } return 3 - id; }
401 +
402 + function drawRemoteImage(imageBase64) {
403 + var canvas = Q('remoteImage'), context = canvas.getContext('2d'), img = new Image();
404 + img.onload = function () { context.drawImage(this, 0, 0, canvas.width, canvas.height); remoteImage = true; adjustVideoWindows(); }
405 + img.src = imageBase64;
406 + }
407 +
408 + // Process incoming messages
409 + function processMessage(data, transport) {
410 + if (typeof data == 'string') {
411 + try { data = JSON.parse(data); } catch (ex) { console.log('Unable to parse', data); return; }
412 + //console.log('RECV', data);
413 +
414 + // Handle control command
415 + if (data.ctrlChannel == '102938') {
416 + switch (data.type) {
417 + case 'image': { drawRemoteImage(data.image); break; }
418 + case 'ping': { send({ ctrlChannel: '102938', type: 'pong' }); break; }
419 + case 'pong': { break; }
420 + }
421 + return;
422 }
356 - }
423
358 - // Indicate to peer that data traffic will no longer be sent over websocket and start holding traffic.
359 - function performWebRtcSwitch() {
360 - if (!serverRecording && webchannel && webchannel.ok) { sendws({ action: 'rtcSwitch', v: 1 }); webchannel.xoutBuffer = []; }
361 - }
362 -
363 - // Disconnect everything
364 - function disconnect() {
365 - serverRecording = false;
366 - QV('recordIcon', false);
367 - if (state > 0) { displayControl("Connection closed."); }
368 - if (state > 1) { setTimeout(start, 500); }
369 - cancelAllFileTransfers();
370 - hangUpButtonClick(0, true); // Data channel
371 - hangUpButtonClick(1, true); // Local audio/video
372 - hangUpButtonClick(2, true); // Remote audio/video
373 - if (socket != null) { socket.close(); socket = null; }
374 - updateControls();
375 - state = 0;
376 - remoteImage = false;
377 - QV('remoteImage', false);
378 - updateLocalOutText();
379 - updateRemoteOutText();
380 - }
381 -
382 - // Send data over the current transport (WebRTC first)
383 - function send(data) {
384 - if ((state != 2) && (pushMessaging == false)) return; // If not in connected state, ignore this.
385 - if (typeof data == 'object') { data = JSON.stringify(data); } // If this is an object, convert it to a string.
386 - if (!serverRecording && webchannel && webchannel.ok) { if (webchannel.xoutBuffer != null) { webchannel.xoutBuffer.push(data); } else { webchannel.send(data); } } // If WebRTC channel is possible, use it or hold until we can use it.
387 - else { if (socket != null) { try { socket.send(data); } catch (ex) { } } } // If a websocket channel is present, use that.
388 - }
389 -
390 - // Send data over the websocket transport (WebSocket only)
391 - function sendws(data) {
392 - if (state != 2) return;
393 - //console.log('SEND', data);
394 - if (typeof data == 'object') { data = JSON.stringify(data); }
395 - if (socket != null) { socket.send(data); }
396 - }
397 -
398 - // WebRTC id switcher (0 -> 0, 1 -> 2, 2 -> 1)
399 - function webRtcIdSwitch(id) { if (id == 0) { return 0; } return 3 - id; }
400 -
401 - function drawRemoteImage(imageBase64) {
402 - var canvas = Q('remoteImage'), context = canvas.getContext('2d'), img = new Image();
403 - img.onload = function () { context.drawImage(this, 0, 0, canvas.width, canvas.height); remoteImage = true; adjustVideoWindows(); }
404 - img.src = imageBase64;
405 - }
406 -
407 - // Process incoming messages
408 - function processMessage(data, transport) {
409 - if (typeof data == 'string') {
410 - try { data = JSON.parse(data); } catch (ex) { console.log('Unable to parse', data); return; }
411 - //console.log('RECV', data);
412 -
413 - // Handle control command
414 - if (data.ctrlChannel == '102938') {
415 - switch (data.type) {
416 - case 'image': { drawRemoteImage(data.image); break; }
417 - case 'ping': { send({ ctrlChannel: '102938', type: 'pong' }); break; }
418 - case 'pong': { break; }
419 - }
420 - return;
424 + // Handle command
425 + switch (data.action) {
426 + case 'chat': { displayRemote(data.msg); updateRemoteOutText(false); break; } // Incoming chat message.
427 + case 'outtext': { updateRemoteOutText(data.value); break; }
428 + case 'ctrl': {
429 + if (data.value == 1) { displayControl("Sent as push notification."); }
430 + else if (data.value == 2) { displayControl("Push notification failed."); }
431 + if (data.msg != null) { displayControl(msg); }
432 + break;
433 }
422 -
423 - // Handle command
424 - switch (data.action) {
425 - case 'chat': { displayRemote(data.msg); updateRemoteOutText(false); break; } // Incoming chat message.
426 - case 'outtext': { updateRemoteOutText(data.value); break; }
427 - case 'ctrl': {
428 - if (data.value == 1) { displayControl("Sent as push notification."); }
429 - else if (data.value == 2) { displayControl("Push notification failed."); }
430 - if (data.msg != null) { displayControl(msg); }
431 - break;
432 - }
433 - case 'random': { if (random > data.random) { startWebRTC(0, true); } break; } // If we have a larger random value, we start WebRTC.
434 - case 'webRtcSdp': { if (!webrtcSessions[webRtcIdSwitch(data.id)]) { startWebRTC(webRtcIdSwitch(data.id), false); } webRtcHandleOffer(webRtcIdSwitch(data.id), data.sdp); break; } // Remote WebRTC offer or answer.
435 - case 'webRtcIce': { var webrtc = webrtcSessions[webRtcIdSwitch(data.id)]; if (webrtc) { try { webrtc.addIceCandidate(new RTCIceCandidate(data.ice)); } catch (ex) { } } break; } // Remote ICE candidate
436 - case 'videoStop': { hangUpButtonClick(webRtcIdSwitch(data.id), true); break; }
437 - case 'rtcSwitch': { // WebRTC switch over commands.
438 - switch (data.v) {
439 - case 0: { performWebRtcSwitch(); break; } // Other side is ready for switch over to WebRTC
440 - case 1: { sendws({ action: 'rtcSwitch', v: 2 }); break; } // Other side no longer sending data on websocket, confirm we got the end marker
441 - case 2: { for (var i in webchannel.xoutBuffer) { webchannel.send(webchannel.xoutBuffer[i]); } delete webchannel.xoutBuffer; break; } // Send any pending data over WebRTC and start using WebRTC with all traffic
442 - default: { console.log('Unknown rtcSwitch value: ' + data.action); break; } //
443 - }
444 - break;
445 - }
446 - case 'file': { startFileDownload(data); break; }
447 - case 'fileUploadCancel': { cancelFileTransfer(data.id); break; }
448 - case 'fileUploadStart': {
449 - if (fileDownloads[data.id]) {
450 - currentFileDownload = fileDownloads[data.id];
451 - currentFileDownload.data = '';
452 - changeFileInfo(data.id, 2, 0);
453 - continueFileDownload(data);
454 - send({ action: 'fileUploadAck', id: data.id });
455 - } break;
434 + case 'random': { if (random > data.random) { startWebRTC(0, true); } break; } // If we have a larger random value, we start WebRTC.
435 + case 'webRtcSdp': { if (!webrtcSessions[webRtcIdSwitch(data.id)]) { startWebRTC(webRtcIdSwitch(data.id), false); } webRtcHandleOffer(webRtcIdSwitch(data.id), data.sdp); break; } // Remote WebRTC offer or answer.
436 + case 'webRtcIce': { var webrtc = webrtcSessions[webRtcIdSwitch(data.id)]; if (webrtc) { try { webrtc.addIceCandidate(new RTCIceCandidate(data.ice)); } catch (ex) { } } break; } // Remote ICE candidate
437 + case 'videoStop': { hangUpButtonClick(webRtcIdSwitch(data.id), true); break; }
438 + case 'rtcSwitch': { // WebRTC switch over commands.
439 + switch (data.v) {
440 + case 0: { performWebRtcSwitch(); break; } // Other side is ready for switch over to WebRTC
441 + case 1: { sendws({ action: 'rtcSwitch', v: 2 }); break; } // Other side no longer sending data on websocket, confirm we got the end marker
442 + case 2: { for (var i in webchannel.xoutBuffer) { webchannel.send(webchannel.xoutBuffer[i]); } delete webchannel.xoutBuffer; break; } // Send any pending data over WebRTC and start using WebRTC with all traffic
443 + default: { console.log('Unknown rtcSwitch value: ' + data.action); break; } //
444 }
457 - case 'fileUploadEnd': {
458 - if (currentFileDownload && (currentFileDownload.id == data.id)) {
459 - changeFileInfo(data.id, 3, 200);
460 - currentFileDownload.done = 1;
461 - currentFileDownload = null;
462 - send({ action: 'fileUploadAck', id: data.id });
463 - }
445 + break;
446 + }
447 + case 'file': { startFileDownload(data); break; }
448 + case 'fileUploadCancel': { cancelFileTransfer(data.id); break; }
449 + case 'fileUploadStart': {
450 + if (fileDownloads[data.id]) {
451 + currentFileDownload = fileDownloads[data.id];
452 + currentFileDownload.data = '';
453 + changeFileInfo(data.id, 2, 0);
454 + continueFileDownload(data);
455 + send({ action: 'fileUploadAck', id: data.id });
456 + } break;
457 + }
458 + case 'fileUploadEnd': {
459 + if (currentFileDownload && (currentFileDownload.id == data.id)) {
460 + changeFileInfo(data.id, 3, 200);
461 + currentFileDownload.done = 1;
462 currentFileDownload = null;
465 - break;
466 - }
467 - case 'fileUploadAck': {
468 - continueFileUpload();
469 - break;
470 - }
471 - case 'fileData': {
472 - if (currentFileDownload && (currentFileDownload.id == data.id)) {
473 - currentFileDownload.data += data.data;
474 - changeFileInfo(data.id, 2, (currentFileDownload.data.length * 200 / currentFileDownload.size));
475 - send({ action: 'fileUploadAck', id: data.id });
476 - }
477 - break;
463 + send({ action: 'fileUploadAck', id: data.id });
464 }
479 - default: { console.log('Unhandled object data', data); break; }
465 + currentFileDownload = null;
466 + break;
467 }
481 - } else {
482 - console.log('Unhandled data', typeof data, data);
483 - }
484 - }
485 -
486 - // File sharing button
487 - function fileButtonClick() {
488 - var chooser = Q('uploadFileInput');
489 - if (chooser.getAttribute('eventset') != 1) {
490 - chooser.setAttribute('eventset', '1');
491 - chooser.addEventListener('change', fileSelect, false);
492 - }
493 - chooser.value = null;
494 - chooser.click();
495 - }
496 -
497 - // User selected one or more files to upload to remote user.
498 - function fileSelect() {
499 - if (state != 2) return;
500 - var x = Q('uploadFileInput');
501 - if (x.files.length > 10) {
502 - displayControl("Limit of 10 file uploads at the same time.");
503 - } else {
504 - for (var i = 0; i < x.files.length; i++) {
505 - if (x.files[i].size > 0) {
506 - var reader = new FileReader();
507 - reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
508 - reader.xfile = x.files[i];
509 - reader.readAsBinaryString(x.files[i]);
510 - }
468 + case 'fileUploadAck': {
469 + continueFileUpload();
470 + break;
471 }
512 - }
513 - }
514 -
515 - // User drag & droped one or more files to upload to remote user.
516 - function fileDrop(e) {
517 - haltEvent(e);
518 - if ((state != 2) || (e.dataTransfer == null)) return;
519 - if (e.dataTransfer.files.length > 10) {
520 - displayControl("Limit of 10 file uploads at the same time.");
521 - } else {
522 - for (var i = 0; i < e.dataTransfer.files.length; i++) {
523 - if (e.dataTransfer.files[i].size > 0) {
524 - var reader = new FileReader();
525 - reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
526 - reader.xfile = e.dataTransfer.files[i];
527 - reader.readAsBinaryString(e.dataTransfer.files[i]);
472 + case 'fileData': {
473 + if (currentFileDownload && (currentFileDownload.id == data.id)) {
474 + currentFileDownload.data += data.data;
475 + changeFileInfo(data.id, 2, (currentFileDownload.data.length * 200 / currentFileDownload.size));
476 + send({ action: 'fileUploadAck', id: data.id });
477 }
478 + break;
479 }
480 + default: { console.log('Unhandled object data', data); break; }
481 }
531 - }
532 -
533 - function startFileUpload(file) {
534 - if (state != 2) return;
535 - file.id = Math.random();
536 - fileUploads.push(file);
537 - chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Upload" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
538 - QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="localBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="cancelFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
539 - Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
540 - send({ action: 'file', size: file.size, id: file.id, type: file.type, name: file.name });
541 - if (currentFileUpload == null) continueFileUpload();
542 - }
543 -
544 - function startFileDownload(file) {
545 - if (state != 2) return;
546 - fileDownloads[file.id] = file;
547 - chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Download" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
548 - QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="remoteBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="saveFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
549 - Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
550 - }
551 -
552 - // Change the file icon and progress
553 - function changeFileInfo(id, icon, progress, progressColor) {
554 - if (icon) {
555 - Q('FILEUP-ICON-' + id).classList.remove('fileicon');
556 - Q('FILEUP-ICON-' + id).classList.remove('fileiconx');
557 - Q('FILEUP-ICON-' + id).classList.remove('fileicontransfer');
558 - Q('FILEUP-ICON-' + id).classList.remove('fileicondone');
559 - Q('FILEUP-ICON-' + id).classList.add(['fileicon', 'fileiconx', 'fileicontransfer', 'fileicondone'][icon]);
482 + } else {
483 + console.log('Unhandled data', typeof data, data);
484 + }
485 + }
486 +
487 + // File sharing button
488 + function fileButtonClick() {
489 + var chooser = Q('uploadFileInput');
490 + if (chooser.getAttribute('eventset') != 1) {
491 + chooser.setAttribute('eventset', '1');
492 + chooser.addEventListener('change', fileSelect, false);
493 + }
494 + chooser.value = null;
495 + chooser.click();
496 + }
497 +
498 + // User selected one or more files to upload to remote user.
499 + function fileSelect() {
500 + if (state != 2) return;
501 + var x = Q('uploadFileInput');
502 + if (x.files.length > 10) {
503 + displayControl("Limit of 10 file uploads at the same time.");
504 + } else {
505 + for (var i = 0; i < x.files.length; i++) {
506 + if (x.files[i].size > 0) {
507 + var reader = new FileReader();
508 + reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
509 + reader.xfile = x.files[i];
510 + reader.readAsBinaryString(x.files[i]);
511 + }
512 }
561 - if (progress) { QS('FILEUP-PROGRESS-' + id)['width'] = progress + 'px'; }
562 - if (progressColor) { QS('FILEUP-PROGRESS-' + id)['background-color'] = progressColor; }
513 }
564 -
565 - // Convert a string into a blob
566 - function data2blob(data) {
567 - var bytes = new Array(data.length);
568 - for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
569 - return new Blob([new Uint8Array(bytes)]);
570 - };
571 -
572 - function saveFileTransfer(id) {
573 - var f = fileDownloads[id];
574 - if (f && f.done == 1) { saveAs(data2blob(f.data), f.name); }
575 - }
576 -
577 - function cancelFileTransfer(id) {
578 - if ((currentFileUpload != null) && (currentFileUpload.id == id)) { currentFileUpload = null; }
579 - if ((currentFileDownload != null) && (currentFileDownload.id == id)) { currentFileDownload = null; }
580 -
581 - var found = false;
582 - if (fileDownloads[id] && (fileDownloads[id].done != 1)) {
583 - delete fileDownloads[id];
584 - found = true;
585 - } else {
586 - for (var i in fileUploads) {
587 - if (fileUploads[i].id == id) {
588 - send({ action: 'fileUploadCancel', id: id });
589 - fileUploads.splice(i, 1);
590 - found = true;
591 - break;
592 - }
514 + }
515 +
516 + // User drag & droped one or more files to upload to remote user.
517 + function fileDrop(e) {
518 + haltEvent(e);
519 + if ((state != 2) || (e.dataTransfer == null)) return;
520 + if (e.dataTransfer.files.length > 10) {
521 + displayControl("Limit of 10 file uploads at the same time.");
522 + } else {
523 + for (var i = 0; i < e.dataTransfer.files.length; i++) {
524 + if (e.dataTransfer.files[i].size > 0) {
525 + var reader = new FileReader();
526 + reader.onload = function (e) { this.xfile.data = e.target.result; startFileUpload(this.xfile); };
527 + reader.xfile = e.dataTransfer.files[i];
528 + reader.readAsBinaryString(e.dataTransfer.files[i]);
529 }
530 }
595 - if (found) { changeFileInfo(id, 1, 200, 'gray'); } // Only cancel a file if it was in the file queue.
531 }
597 -
598 - function cancelAllFileTransfers() {
599 - for (var i in fileDownloads) { cancelFileTransfer(fileDownloads[i].id); }
600 - for (var i in fileUploads) { cancelFileTransfer(fileUploads[i].id); }
601 - }
602 -
603 - function continueFileUpload() {
604 - if (currentFileUpload == null) {
605 - // Select the next file to upload
606 - if (fileUploads.length == 0) { return; } // Nothing to do
607 - currentFileUpload = fileUploads[0];
608 - currentFileUpload.ptr = 0;
609 -
610 - // Indicate that we are sending this file
611 - send({ action: 'fileUploadStart', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
612 - } else {
613 - if (currentFileUpload.size <= currentFileUpload.ptr) {
614 - // If we are done, send the end marker
615 - send({ action: 'fileUploadEnd', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
616 - changeFileInfo(currentFileUpload.id, 3, 200);
617 - fileUploads.splice(0, 1);
618 - currentFileUpload = null;
619 - continueFileUpload(); // Send the next file
620 - } else {
621 - // Send the next block
622 - var nextBlockLen = Math.min(4000, currentFileUpload.data.length - currentFileUpload.ptr);
623 - var data = currentFileUpload.data.substring(currentFileUpload.ptr, currentFileUpload.ptr + nextBlockLen);
624 - send({ action: 'fileData', id: currentFileUpload.id, data: data });
625 - currentFileUpload.ptr += nextBlockLen;
626 - changeFileInfo(currentFileUpload.id, 0, (currentFileUpload.ptr * 200 / currentFileUpload.size));
532 + }
533 +
534 + function startFileUpload(file) {
535 + if (state != 2) return;
536 + file.id = Math.random();
537 + fileUploads.push(file);
538 + chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Upload" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
539 + QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="localBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="cancelFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
540 + Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
541 + send({ action: 'file', size: file.size, id: file.id, type: file.type, name: file.name });
542 + if (currentFileUpload == null) continueFileUpload();
543 + }
544 +
545 + function startFileDownload(file) {
546 + if (state != 2) return;
547 + fileDownloads[file.id] = file;
548 + chatTextSession += ((new Date()).toLocaleTimeString() + ' - ' + "Download" + '> ' + file.name + ' (' + file.size + ' ' + "bytes" + ')\r\n');
549 + QA('xmsg', '<div style="clear:both"></div><div id="FILEUP-' + file.id + '" class="remoteBubble" style="font-size:14px;width:240px;cursor:pointer" onclick="saveFileTransfer(\'' + file.id + '\')"><div id="FILEUP-ICON-' + file.id + '" class="fileicon" style="float:left;width:32px;height:32px"></div><div><div id="FILEUP-NAME-' + file.id + '" style="height:20px;overflow:hidden;white-space:nowrap;" title="' + file.name + '">' + file.name + '</div><div style="width:200px;background-color:lightgray;margin-left:32px;border-radius:3px;margin-top:3px;height:11px"><div id="FILEUP-PROGRESS-' + file.id + '" style="width:0px;background-color:green;border-radius:3px;height:11px">&nbsp;</div></div></div></div>');
550 + Q('xmsgparent').scrollTop = Q('xmsgparent').scrollHeight;
551 + }
552 +
553 + // Change the file icon and progress
554 + function changeFileInfo(id, icon, progress, progressColor) {
555 + if (icon) {
556 + Q('FILEUP-ICON-' + id).classList.remove('fileicon');
557 + Q('FILEUP-ICON-' + id).classList.remove('fileiconx');
558 + Q('FILEUP-ICON-' + id).classList.remove('fileicontransfer');
559 + Q('FILEUP-ICON-' + id).classList.remove('fileicondone');
560 + Q('FILEUP-ICON-' + id).classList.add(['fileicon', 'fileiconx', 'fileicontransfer', 'fileicondone'][icon]);
561 + }
562 + if (progress) { QS('FILEUP-PROGRESS-' + id)['width'] = progress + 'px'; }
563 + if (progressColor) { QS('FILEUP-PROGRESS-' + id)['background-color'] = progressColor; }
564 + }
565 +
566 + // Convert a string into a blob
567 + function data2blob(data) {
568 + var bytes = new Array(data.length);
569 + for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
570 + return new Blob([new Uint8Array(bytes)]);
571 + };
572 +
573 + function saveFileTransfer(id) {
574 + var f = fileDownloads[id];
575 + if (f && f.done == 1) { saveAs(data2blob(f.data), f.name); }
576 + }
577 +
578 + function cancelFileTransfer(id) {
579 + if ((currentFileUpload != null) && (currentFileUpload.id == id)) { currentFileUpload = null; }
580 + if ((currentFileDownload != null) && (currentFileDownload.id == id)) { currentFileDownload = null; }
581 +
582 + var found = false;
583 + if (fileDownloads[id] && (fileDownloads[id].done != 1)) {
584 + delete fileDownloads[id];
585 + found = true;
586 + } else {
587 + for (var i in fileUploads) {
588 + if (fileUploads[i].id == id) {
589 + send({ action: 'fileUploadCancel', id: id });
590 + fileUploads.splice(i, 1);
591 + found = true;
592 + break;
593 }
594 }
595 }
630 -
631 - function continueFileDownload(msg) {
632 - send({ action: 'fileUploadAck', id: msg.id });
633 - }
634 -
635 - // Toggle notification
636 - function enableNotificationsButtonClick() {
637 - if (notificationSupport) {
638 - if (Notification) { Notification.requestPermission().then(function (permission) { QV('notifyButton', permission != 'granted'); }); }
596 + if (found) { changeFileInfo(id, 1, 200, 'gray'); } // Only cancel a file if it was in the file queue.
597 + }
598 +
599 + function cancelAllFileTransfers() {
600 + for (var i in fileDownloads) { cancelFileTransfer(fileDownloads[i].id); }
601 + for (var i in fileUploads) { cancelFileTransfer(fileUploads[i].id); }
602 + }
603 +
604 + function continueFileUpload() {
605 + if (currentFileUpload == null) {
606 + // Select the next file to upload
607 + if (fileUploads.length == 0) { return; } // Nothing to do
608 + currentFileUpload = fileUploads[0];
609 + currentFileUpload.ptr = 0;
610 +
611 + // Indicate that we are sending this file
612 + send({ action: 'fileUploadStart', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
613 + } else {
614 + if (currentFileUpload.size <= currentFileUpload.ptr) {
615 + // If we are done, send the end marker
616 + send({ action: 'fileUploadEnd', size: currentFileUpload.size, id: currentFileUpload.id, type: currentFileUpload.type, name: currentFileUpload.name });
617 + changeFileInfo(currentFileUpload.id, 3, 200);
618 + fileUploads.splice(0, 1);
619 + currentFileUpload = null;
620 + continueFileUpload(); // Send the next file
621 + } else {
622 + // Send the next block
623 + var nextBlockLen = Math.min(4000, currentFileUpload.data.length - currentFileUpload.ptr);
624 + var data = currentFileUpload.data.substring(currentFileUpload.ptr, currentFileUpload.ptr + nextBlockLen);
625 + send({ action: 'fileData', id: currentFileUpload.id, data: data });
626 + currentFileUpload.ptr += nextBlockLen;
627 + changeFileInfo(currentFileUpload.id, 0, (currentFileUpload.ptr * 200 / currentFileUpload.size));
628 }
640 - return false;
629 }
642 -
643 - // Camera button
644 - function camButtonClick() {
645 - if (localStream == null) { startLocalStream({ video: true, audio: true }); }
646 - }
647 -
648 - // Microphone
649 - function micButtonClick() {
650 - if (localStream == null) { startLocalStream({ video: false, audio: true }); }
651 - }
652 -
653 - function hangUpButtonClick(id, fromRemote) {
654 - //console.log('hangUpButtonClick', id);
655 - var localVideo = Q('localVideoCanvas');
656 - var remoteVideo = Q('remoteVideoCanvas');
657 - var webrtc = webrtcSessions[(multiWebRtc == true)? id : 0];
658 -
659 - if ((id == 0) && (webchannel != null)) { try { webchannel.close(); } catch (e) { } webchannel = null; }
660 -
661 - if (webrtc) {
662 - if ((multiWebRtc == true) || (id == 0)) {
663 - webrtc.ontrack = null;
664 - webrtc.onremovetrack = null;
665 - webrtc.onremovestream = null;
666 - webrtc.onnicecandidate = null;
667 - webrtc.oniceconnectionstatechange = null;
668 - webrtc.onsignalingstatechange = null;
669 - webrtc.onicegatheringstatechange = null;
670 - webrtc.onnotificationneeded = null;
671 - }
672 -
673 - if ((id == 1) && localStream) { var tracks = localStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } localStream = null; }
674 - if ((id == 2) && remoteStream) { var tracks = remoteStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } remoteStream = null; }
675 -
676 - if ((multiWebRtc == true) || (id == 0)) {
677 - webrtc.close();
678 - delete webrtcSessions[id];
679 - }
630 + }
631 +
632 + function continueFileDownload(msg) {
633 + send({ action: 'fileUploadAck', id: msg.id });
634 + }
635 +
636 + // Toggle notification
637 + function enableNotificationsButtonClick() {
638 + if (notificationSupport) {
639 + if (Notification) { Notification.requestPermission().then(function (permission) { QV('notifyButton', permission != 'granted'); }); }
640 + }
641 + return false;
642 + }
643 +
644 + // Camera button
645 + function camButtonClick() {
646 + if (localStream == null) { startLocalStream({ video: true, audio: true }); }
647 + }
648 +
649 + // Microphone
650 + function micButtonClick() {
651 + if (localStream == null) { startLocalStream({ video: false, audio: true }); }
652 + }
653 +
654 + function hangUpButtonClick(id, fromRemote) {
655 + //console.log('hangUpButtonClick', id);
656 + var localVideo = Q('localVideoCanvas');
657 + var remoteVideo = Q('remoteVideoCanvas');
658 + var webrtc = webrtcSessions[(multiWebRtc == true)? id : 0];
659 +
660 + if ((id == 0) && (webchannel != null)) { try { webchannel.close(); } catch (e) { } webchannel = null; }
661 +
662 + if (webrtc) {
663 + if ((multiWebRtc == true) || (id == 0)) {
664 + webrtc.ontrack = null;
665 + webrtc.onremovetrack = null;
666 + webrtc.onremovestream = null;
667 + webrtc.onnicecandidate = null;
668 + webrtc.oniceconnectionstatechange = null;
669 + webrtc.onsignalingstatechange = null;
670 + webrtc.onicegatheringstatechange = null;
671 + webrtc.onnotificationneeded = null;
672 }
673
682 - if (id == 1) {
683 - localVideo.removeAttribute('src');
684 - localVideo.removeAttribute('srcObject');
685 - if (localStream != null) { localStream = null; }
686 - displayLocalVideo(false);
687 - } else if (id == 2) {
688 - remoteVideo.removeAttribute('src');
689 - remoteVideo.removeAttribute('srcObject');
690 - displayRemoteVideo(false);
691 - }
674 + if ((id == 1) && localStream) { var tracks = localStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } localStream = null; }
675 + if ((id == 2) && remoteStream) { var tracks = remoteStream.getTracks(); for (var i in tracks) { tracks[i].stop(); } remoteStream = null; }
676
693 - if (fromRemote != true) { send({ action: 'videoStop', id: id }); }
694 - updateControls();
695 - }
696 -
697 - // Setup local audio/video
698 - function startLocalStream(constraints) {
699 - var channel = (multiWebRtc == true) ? 1 : 0;
700 - if (localStream != null) return;
701 - if ((multiWebRtc == true) && (webrtcSessions[1] != null)) return;
702 - if (navigator.mediaDevices.getUserMedia) {
703 - localStream = 1;
704 - updateControls();
705 - navigator.mediaDevices.getUserMedia(constraints)
706 - .then(function (stream) {
707 - localStream = stream;
708 - var tracks = localStream.getTracks();
709 - var webrtc = startWebRTC(channel);
710 - if (constraints.video == true) {
711 - var video = Q('localVideoCanvas');
712 - video.srcObject = stream;
713 - video.onloadedmetadata = function (e) { video.play(); };
714 - displayLocalVideo(true);
715 - }
716 - for (var i in tracks) { webrtc.addTrack(tracks[i], localStream); }
717 - }, function (err) {
718 - displayControl(err.message + '.');
719 - hangUpButtonClick(1);
720 - });
677 + if ((multiWebRtc == true) || (id == 0)) {
678 + webrtc.close();
679 + delete webrtcSessions[id];
680 }
681 }
682
724 - // This is the main start
725 - function start() {
726 - // Get started
683 + if (id == 1) {
684 + localVideo.removeAttribute('src');
685 + localVideo.removeAttribute('srcObject');
686 + if (localStream != null) { localStream = null; }
687 + displayLocalVideo(false);
688 + } else if (id == 2) {
689 + remoteVideo.removeAttribute('src');
690 + remoteVideo.removeAttribute('srcObject');
691 + displayRemoteVideo(false);
692 + }
693 +
694 + if (fromRemote != true) { send({ action: 'videoStop', id: id }); }
695 + updateControls();
696 + }
697 +
698 + // Setup local audio/video
699 + function startLocalStream(constraints) {
700 + var channel = (multiWebRtc == true) ? 1 : 0;
701 + if (localStream != null) return;
702 + if ((multiWebRtc == true) && (webrtcSessions[1] != null)) return;
703 + if (navigator.mediaDevices.getUserMedia) {
704 + localStream = 1;
705 updateControls();
728 - if ((typeof args.id == 'string') && (args.id.length > 0)) {
729 - var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/meshrelay.ashx?id=' + args.id;
730 - if ((args.auth != null) && (args.auth != '')) { url += '&auth=' + args.auth; }
731 - socket = new WebSocket(url);
732 - socket.onopen = function () { state = 1; displayControl("Waiting for other user..."); }
733 - socket.onerror = function (e) { /*console.error(e);*/ }
734 - socket.onclose = function () { disconnect(); }
735 - socket.onmessage = function (msg) {
736 - if ((state < 2) && (typeof msg.data == 'string') && ((msg.data == 'c') || (msg.data == 'cr'))) {
737 - serverRecording = (msg.data == 'cr');
738 - QV('recordIcon', serverRecording);
739 - hangUpButtonClick(0, true);
740 - hangUpButtonClick(1, true);
741 - hangUpButtonClick(2, true);
742 - displayControl("Connected.");
743 - state = 2;
744 - updateControls();
745 - sendws({ action: 'random', random: random }); // Send a random number. Higher number starts the WebRTC session.
746 - updateLocalOutText();
747 - updateRemoteOutText();
748 - return;
749 - }
750 - if (msg.data[0] == '{') { processMessage(msg.data, 1); }
706 + navigator.mediaDevices.getUserMedia(constraints)
707 + .then(function (stream) {
708 + localStream = stream;
709 + var tracks = localStream.getTracks();
710 + var webrtc = startWebRTC(channel);
711 + if (constraints.video == true) {
712 + var video = Q('localVideoCanvas');
713 + video.srcObject = stream;
714 + video.onloadedmetadata = function (e) { video.play(); };
715 + displayLocalVideo(true);
716 }
752 - } else {
753 - displayControl("Error: No connection key specified.");
754 - }
755 - }
756 -
757 - function saveChatSession() {
758 - saveAs(data2blob(chatTextSession), "ChatSession");
759 - }
760 -
761 - start();
762 -
763 - function onUnLoad() {
764 - for (var i = 0; i < 3; i++) { if (webrtcSessions[i]) { try { webrtcSessions[i].close(); delete webrtcSessions[i]; } catch (ex) { } } }
765 - if (webchannel != null) { try { webchannel.close(); } catch (ex) { } webchannel = null; }
766 - if (socket != null) { try { socket.close(); } catch (ex) { } socket = null; }
767 - }
768 -
769 - function isSafeString3(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf(':') == -1)) };
770 -
771 - // Parse URL arguments, only keep safe values
772 - function XparseUriArgs() {
773 - var href = window.document.location.href;
774 - if (href.endsWith('#')) { href = href.substring(0, href.length - 1); }
775 - var name, r = {}, parsedUri = href.split(/[\?&|]/);
776 - parsedUri.splice(0, 1);
777 - for (var j in parsedUri) {
778 - var arg = parsedUri[j], i = arg.indexOf('=');
779 - name = arg.substring(0, i);
780 - r[name] = arg.substring(i + 1);
781 - if (!isSafeString3(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } }
782 - }
783 - return r;
784 - }
785 -
786 - function updateLocalOutText() {
787 - var l = Q('xouttext').value;
788 - if (((state != 2) || (l == '')) && (localOutText == true)) {
789 - localOutText = false;
790 - send({ action: 'outtext', value: false });
791 - } else if ((state == 2) && ((l != '') && (localOutText == false))) {
792 - localOutText = true;
793 - send({ action: 'outtext', value: true });
717 + for (var i in tracks) { webrtc.addTrack(tracks[i], localStream); }
718 + }, function (err) {
719 + displayControl(err.message + '.');
720 + hangUpButtonClick(1);
721 + });
722 + }
723 + }
724 +
725 + // This is the main start
726 + function start() {
727 + // Get started
728 + updateControls();
729 + if ((typeof args.id == 'string') && (args.id.length > 0)) {
730 + var url = window.location.protocol.replace('http', 'ws') + '//' + window.location.host + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')) + '/meshrelay.ashx?id=' + args.id;
731 + if ((args.auth != null) && (args.auth != '')) { url += '&auth=' + args.auth; }
732 + socket = new WebSocket(url);
733 + socket.onopen = function () { state = 1; displayControl("Waiting for other user..."); }
734 + socket.onerror = function (e) { /*console.error(e);*/ }
735 + socket.onclose = function () { disconnect(); }
736 + socket.onmessage = function (msg) {
737 + if ((state < 2) && (typeof msg.data == 'string') && ((msg.data == 'c') || (msg.data == 'cr'))) {
738 + serverRecording = (msg.data == 'cr');
739 + QV('recordIcon', serverRecording);
740 + hangUpButtonClick(0, true);
741 + hangUpButtonClick(1, true);
742 + hangUpButtonClick(2, true);
743 + displayControl("Connected.");
744 + state = 2;
745 + updateControls();
746 + sendws({ action: 'random', random: random }); // Send a random number. Higher number starts the WebRTC session.
747 + updateLocalOutText();
748 + updateRemoteOutText();
749 + return;
750 + }
751 + if (msg.data[0] == '{') { processMessage(msg.data, 1); }
752 }
795 - }
796 -
797 - function updateRemoteOutText(newState) {
798 - //console.log('updateRemoteOutText', newState);
799 - if (state != 2) { newState = false; }
800 - if (remoteOutText != newState) { remoteOutText = newState; QV('typingIndicator', remoteOutText); }
801 - }
753 + } else {
754 + displayControl("Error: No connection key specified.");
755 + }
756 + }
757 +
758 + function saveChatSession() {
759 + saveAs(data2blob(chatTextSession), "ChatSession");
760 + }
761 +
762 + start();
763 +
764 + function onUnLoad() {
765 + for (var i = 0; i < 3; i++) { if (webrtcSessions[i]) { try { webrtcSessions[i].close(); delete webrtcSessions[i]; } catch (ex) { } } }
766 + if (webchannel != null) { try { webchannel.close(); } catch (ex) { } webchannel = null; }
767 + if (socket != null) { try { socket.close(); } catch (ex) { } socket = null; }
768 + }
769 +
770 + function isSafeString3(str) { return ((typeof str == 'string') && (str.indexOf('<') == -1) && (str.indexOf('>') == -1) && (str.indexOf('&') == -1) && (str.indexOf('"') == -1) && (str.indexOf('\'') == -1) && (str.indexOf('+') == -1) && (str.indexOf('(') == -1) && (str.indexOf(')') == -1) && (str.indexOf('#') == -1) && (str.indexOf(':') == -1)) };
771 +
772 + // Parse URL arguments, only keep safe values
773 + function XparseUriArgs() {
774 + var href = window.document.location.href;
775 + if (href.endsWith('#')) { href = href.substring(0, href.length - 1); }
776 + var name, r = {}, parsedUri = href.split(/[\?&|]/);
777 + parsedUri.splice(0, 1);
778 + for (var j in parsedUri) {
779 + var arg = parsedUri[j], i = arg.indexOf('=');
780 + name = arg.substring(0, i);
781 + r[name] = arg.substring(i + 1);
782 + if (!isSafeString3(r[name])) { delete r[name]; } else { var x = parseInt(r[name]); if (x == r[name]) { r[name] = x; } }
783 + }
784 + return r;
785 + }
786 +
787 + function updateLocalOutText() {
788 + var l = Q('xouttext').value;
789 + if (((state != 2) || (l == '')) && (localOutText == true)) {
790 + localOutText = false;
791 + send({ action: 'outtext', value: false });
792 + } else if ((state == 2) && ((l != '') && (localOutText == false))) {
793 + localOutText = true;
794 + send({ action: 'outtext', value: true });
795 + }
796 + }
797 +
798 + function updateRemoteOutText(newState) {
799 + //console.log('updateRemoteOutText', newState);
800 + if (state != 2) { newState = false; }
801 + if (remoteOutText != newState) { remoteOutText = newState; QV('typingIndicator', remoteOutText); }
802 + }
803
804 </script>
805 </body>
views/mstsc.handlebars
+1
@@ -75,6 +75,7 @@
75 }
76 </style>
77 <script language="javascript">
78 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
79 var client = null;
80 var canvas = null;
81 var urlargs = parseUriArgs();
views/player.handlebars
+1
@@ -96,6 +96,7 @@
96 </div>
97 </div>
98 <script>
99 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
100 var recFile = null;
101 var recFilePtr = 0;
102 var recFileStartTime = 0;
views/sharing-mobile.handlebars
+1
@@ -737,6 +737,7 @@
737 <iframe name="fileUploadFrame" style=display:none></iframe>
738 <script>
739 'use strict';
740 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
741 var args = parseUriArgs();
742 var urlargs = args;
743 var sessionTime = parseInt('{{{sessiontime}}}');
views/sharing.handlebars
+1
@@ -283,6 +283,7 @@
283 </div>
284 </div>
285 <script>
286 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
287 var sessionActivity = null;
288 var desktop = null;
289 var agentPresent = true;
views/ssh.handlebars
+1
@@ -68,6 +68,7 @@
68 </div>
69 </div>
70 <script>
71 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
72 var term = null;
73 var termfit = null;
74 var resizeTimer = null;
views/terms.handlebars
+1
@@ -161,6 +161,7 @@
161 </div>
162 <script>
163 'use strict';
164 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
165 var uiMode = parseInt(getstore('uiMode', 1));
166 var webPageStackMenu = false;
167 var webPageFullScreen = true;
views/xterm.handlebars
+1
@@ -80,6 +80,7 @@
80 </div>
81 </div>
82 <script>
83 + var random = '{{{randomlength}}}' // Random length string for BREACH mitigation
84 var term = null;
85 var termfit = null;
86 var tunnel = null;
webserver.js
+4
@@ -7696,6 +7696,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7696 xargs.domainurl = domain.url;
7697 xargs.autocomplete = (domain.autocomplete === false)?'x':'autocomplete'; // This option allows autocomplete to be turned off on the login page.
7698 if (typeof domain.hide == 'number') { xargs.hide = domain.hide; }
7699 +
7700 + // To mitigate any possible BREACH attack, we generate a random length string here.
7701 + xargs.randomlength = (args.webpagelengthrandomization !== false) ? parent.crypto.randomBytes(parent.crypto.randomInt(0, 256)).toString('base64') : '';
7702 +
7703 return xargs;
7704 }
7705