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(' '));
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(' '));
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(' ').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(' ').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"> </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"> </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"> </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"> </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