1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Functions related to the 'client screen' for Chromoting.
12 /** @suppress {duplicate} */
13 var remoting
= remoting
|| {};
16 * @type {remoting.ClientSession} The client session object, set once the
17 * access code has been successfully verified.
19 remoting
.clientSession
= null;
22 * @type {string} The normalized access code.
24 remoting
.accessCode
= '';
27 * @type {string} The host's JID, returned by the server.
29 remoting
.hostJid
= '';
32 * @type {string} For Me2Me connections, the id of the current host.
37 * @type {boolean} For Me2Me connections. Set to true if connection
38 * must be retried on failure.
40 remoting
.retryIfOffline
= false;
43 * @type {string} The host's public key, returned by the server.
45 remoting
.hostPublicKey
= '';
48 * @type {XMLHttpRequest} The XHR object corresponding to the current
49 * support-hosts request, if there is one outstanding.
52 remoting
.supportHostsXhr_
= null;
57 remoting
.ConnectionType
= {
63 * @type {remoting.ConnectionType?}
65 remoting
.currentConnectionType
= null;
68 * Entry point for the 'connect' functionality. This function defers to the
69 * WCS loader to call it back with an access token.
71 remoting
.connectIt2Me = function() {
72 remoting
.currentConnectionType
= remoting
.ConnectionType
.It2Me
;
73 remoting
.WcsLoader
.load(connectIt2MeWithAccessToken_
,
74 remoting
.showErrorMessage
);
78 * Cancel an incomplete connect operation.
80 * @return {void} Nothing.
82 remoting
.cancelConnect = function() {
83 if (remoting
.supportHostsXhr_
) {
84 remoting
.supportHostsXhr_
.abort();
85 remoting
.supportHostsXhr_
= null;
87 if (remoting
.clientSession
) {
88 remoting
.clientSession
.removePlugin();
89 remoting
.clientSession
= null;
91 if (remoting
.currentConnectionType
== remoting
.ConnectionType
.Me2Me
) {
92 remoting
.initDaemonUi();
94 remoting
.setMode(remoting
.AppMode
.HOME
);
95 document
.getElementById('access-code-entry').value
= '';
100 * Toggle the scale-to-fit feature for the current client session.
102 * @return {void} Nothing.
104 remoting
.toggleScaleToFit = function() {
105 remoting
.clientSession
.setScaleToFit(!remoting
.clientSession
.getScaleToFit());
109 * Update the remoting client layout in response to a resize event.
111 * @return {void} Nothing.
113 remoting
.onResize = function() {
114 if (remoting
.clientSession
)
115 remoting
.clientSession
.onResize();
119 * Handle changes in the visibility of the window, for example by pausing video.
121 * @return {void} Nothing.
123 remoting
.onVisibilityChanged = function() {
124 if (remoting
.clientSession
)
125 remoting
.clientSession
.pauseVideo(document
.webkitHidden
);
129 * Disconnect the remoting client.
131 * @return {void} Nothing.
133 remoting
.disconnect = function() {
134 if (remoting
.clientSession
) {
135 remoting
.clientSession
.disconnect();
136 remoting
.clientSession
= null;
137 console
.log('Disconnected.');
138 if (remoting
.currentConnectionType
== remoting
.ConnectionType
.It2Me
) {
139 remoting
.setMode(remoting
.AppMode
.CLIENT_SESSION_FINISHED_IT2ME
);
141 remoting
.setMode(remoting
.AppMode
.CLIENT_SESSION_FINISHED_ME2ME
);
147 * Sends a Ctrl-Alt-Del sequence to the remoting client.
149 * @return {void} Nothing.
151 remoting
.sendCtrlAltDel = function() {
152 if (remoting
.clientSession
) {
153 console
.log('Sending Ctrl-Alt-Del.');
154 remoting
.clientSession
.sendCtrlAltDel();
159 * Sends a Print Screen keypress to the remoting client.
161 * @return {void} Nothing.
163 remoting
.sendPrintScreen = function() {
164 if (remoting
.clientSession
) {
165 console
.log('Sending Print Screen.');
166 remoting
.clientSession
.sendPrintScreen();
171 * If WCS was successfully loaded, proceed with the connection, otherwise
174 * @param {string?} token The OAuth2 access token, or null if an error occurred.
175 * @return {void} Nothing.
177 function connectIt2MeWithAccessToken_(token
) {
179 var accessCode
= document
.getElementById('access-code-entry').value
;
180 remoting
.accessCode
= normalizeAccessCode_(accessCode
);
181 // At present, only 12-digit access codes are supported, of which the first
182 // 7 characters are the supportId.
183 var kSupportIdLen
= 7;
184 var kHostSecretLen
= 5;
185 var kAccessCodeLen
= kSupportIdLen
+ kHostSecretLen
;
186 if (remoting
.accessCode
.length
!= kAccessCodeLen
) {
187 console
.error('Bad access code length');
188 showConnectError_(remoting
.Error
.INVALID_ACCESS_CODE
);
190 var supportId
= remoting
.accessCode
.substring(0, kSupportIdLen
);
191 remoting
.setMode(remoting
.AppMode
.CLIENT_CONNECTING
);
192 resolveSupportId(supportId
, token
);
195 showConnectError_(remoting
.Error
.AUTHENTICATION_FAILED
);
200 * Callback function called when the state of the client plugin changes. The
201 * current state is available via the |state| member variable.
203 * @param {number} oldState The previous state of the plugin.
204 * @param {number} newState The current state of the plugin.
206 // TODO(jamiewalch): Make this pass both the current and old states to avoid
208 function onClientStateChange_(oldState
, newState
) {
209 if (!remoting
.clientSession
) {
210 // If the connection has been cancelled, then we no longer have a reference
211 // to the session object and should ignore any state changes.
215 // Clear the PIN on successful connection, or on error if we're not going to
216 // automatically retry.
217 var clearPin
= false;
219 if (newState
== remoting
.ClientSession
.State
.CREATED
) {
220 console
.log('Created plugin');
222 } else if (newState
== remoting
.ClientSession
.State
.BAD_PLUGIN_VERSION
) {
223 showConnectError_(remoting
.Error
.BAD_PLUGIN_VERSION
);
225 } else if (newState
== remoting
.ClientSession
.State
.CONNECTING
) {
226 console
.log('Connecting as ' + remoting
.oauth2
.getCachedEmail());
228 } else if (newState
== remoting
.ClientSession
.State
.INITIALIZING
) {
229 console
.log('Initializing connection');
231 } else if (newState
== remoting
.ClientSession
.State
.CONNECTED
) {
232 if (remoting
.clientSession
) {
234 setConnectionInterruptedButtonsText_();
235 remoting
.setMode(remoting
.AppMode
.IN_SESSION
);
236 remoting
.toolbar
.center();
237 remoting
.toolbar
.preview();
238 remoting
.clipboard
.startSession();
242 } else if (newState
== remoting
.ClientSession
.State
.CLOSED
) {
243 if (oldState
== remoting
.ClientSession
.State
.CONNECTED
) {
244 remoting
.clientSession
.removePlugin();
245 remoting
.clientSession
= null;
246 console
.log('Connection closed by host');
247 if (remoting
.currentConnectionType
== remoting
.ConnectionType
.It2Me
) {
248 remoting
.setMode(remoting
.AppMode
.CLIENT_SESSION_FINISHED_IT2ME
);
250 remoting
.setMode(remoting
.AppMode
.CLIENT_SESSION_FINISHED_ME2ME
);
253 // The transition from CONNECTING to CLOSED state may happen
254 // only with older client plugins. Current version should go the
255 // FAILED state when connection fails.
256 showConnectError_(remoting
.Error
.INVALID_ACCESS_CODE
);
259 } else if (newState
== remoting
.ClientSession
.State
.FAILED
) {
260 console
.error('Client plugin reported connection failed: ' +
261 remoting
.clientSession
.error
);
263 if (remoting
.clientSession
.error
==
264 remoting
.ClientSession
.ConnectionError
.HOST_IS_OFFLINE
) {
266 retryConnectOrReportOffline_();
267 } else if (remoting
.clientSession
.error
==
268 remoting
.ClientSession
.ConnectionError
.SESSION_REJECTED
) {
269 showConnectError_(remoting
.Error
.INVALID_ACCESS_CODE
);
270 } else if (remoting
.clientSession
.error
==
271 remoting
.ClientSession
.ConnectionError
.INCOMPATIBLE_PROTOCOL
) {
272 showConnectError_(remoting
.Error
.INCOMPATIBLE_PROTOCOL
);
273 } else if (remoting
.clientSession
.error
==
274 remoting
.ClientSession
.ConnectionError
.NETWORK_FAILURE
) {
275 showConnectError_(remoting
.Error
.NETWORK_FAILURE
);
276 } else if (remoting
.clientSession
.error
==
277 remoting
.ClientSession
.ConnectionError
.HOST_OVERLOAD
) {
278 showConnectError_(remoting
.Error
.HOST_OVERLOAD
);
280 showConnectError_(remoting
.Error
.UNEXPECTED
);
284 document
.getElementById('pin-entry').value
= '';
288 console
.error('Unexpected client plugin state: ' + newState
);
289 // This should only happen if the web-app and client plugin get out of
290 // sync, and even then the version check should allow compatibility.
291 showConnectError_(remoting
.Error
.MISSING_PLUGIN
);
296 * If we have a hostId to retry, try refreshing it and connecting again. If not,
297 * then show the 'host offline' error message.
299 * @return {void} Nothing.
301 function retryConnectOrReportOffline_() {
302 if (remoting
.clientSession
) {
303 remoting
.clientSession
.removePlugin();
304 remoting
.clientSession
= null;
306 if (remoting
.hostId
&& remoting
.retryIfOffline
) {
307 console
.warn('Connection failed. Retrying.');
308 /** @param {boolean} success True if the refresh was successful. */
309 var onDone = function(success
) {
311 remoting
.retryIfOffline
= false;
312 remoting
.connectMe2MeWithPin();
314 showConnectError_(remoting
.Error
.HOST_IS_OFFLINE
);
317 remoting
.hostList
.refresh(onDone
);
319 console
.error('Connection failed. Not retrying.');
320 showConnectError_(remoting
.Error
.HOST_IS_OFFLINE
);
325 * Create the client session object and initiate the connection.
327 * @return {void} Nothing.
329 function startSession_() {
330 console
.log('Starting session...');
331 var accessCode
= document
.getElementById('access-code-entry');
332 accessCode
.value
= ''; // The code has been validated and won't work again.
333 remoting
.clientSession
=
334 new remoting
.ClientSession(
335 remoting
.hostJid
, remoting
.hostPublicKey
,
336 remoting
.accessCode
, 'spake2_plain', '',
337 /** @type {string} */ (remoting
.oauth2
.getCachedEmail()),
338 remoting
.ClientSession
.Mode
.IT2ME
,
339 onClientStateChange_
);
340 /** @param {string?} token The auth token. */
341 var createPluginAndConnect = function(token
) {
343 remoting
.clientSession
.createPluginAndConnect(
344 document
.getElementById('session-mode'),
347 showConnectError_(remoting
.Error
.AUTHENTICATION_FAILED
);
350 remoting
.oauth2
.callWithToken(createPluginAndConnect
,
351 remoting
.showErrorMessage
);
355 * Show a client-side error message.
357 * @param {remoting.Error} errorTag The error to be localized and
359 * @return {void} Nothing.
361 function showConnectError_(errorTag
) {
362 console
.error('Connection failed: ' + errorTag
);
363 var errorDiv
= document
.getElementById('connect-error-message');
364 l10n
.localizeElementFromTag(errorDiv
, /** @type {string} */ (errorTag
));
365 remoting
.accessCode
= '';
366 if (remoting
.clientSession
) {
367 remoting
.clientSession
.disconnect();
368 remoting
.clientSession
= null;
370 if (remoting
.currentConnectionType
== remoting
.ConnectionType
.It2Me
) {
371 remoting
.setMode(remoting
.AppMode
.CLIENT_CONNECT_FAILED_IT2ME
);
373 remoting
.setMode(remoting
.AppMode
.CLIENT_CONNECT_FAILED_ME2ME
);
378 * Set the text on the buttons shown under the error message so that they are
379 * easy to understand in the case where a successful connection failed, as
380 * opposed to the case where a connection never succeeded.
382 function setConnectionInterruptedButtonsText_() {
383 var button1
= document
.getElementById('client-reconnect-button');
384 l10n
.localizeElementFromTag(button1
, /*i18n-content*/'RECONNECT');
385 button1
.removeAttribute('autofocus');
386 var button2
= document
.getElementById('client-finished-me2me-button');
387 l10n
.localizeElementFromTag(button2
, /*i18n-content*/'OK');
388 button2
.setAttribute('autofocus', 'autofocus');
392 * Parse the response from the server to a request to resolve a support id.
394 * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
395 * @return {void} Nothing.
397 function parseServerResponse_(xhr
) {
398 remoting
.supportHostsXhr_
= null;
399 console
.log('parseServerResponse: xhr =', xhr
);
400 if (xhr
.status
== 200) {
401 var host
= /** @type {{data: {jabberId: string, publicKey: string}}} */
402 jsonParseSafe(xhr
.responseText
);
403 if (host
&& host
.data
&& host
.data
.jabberId
&& host
.data
.publicKey
) {
404 remoting
.hostJid
= host
.data
.jabberId
;
405 remoting
.hostPublicKey
= host
.data
.publicKey
;
406 var split
= remoting
.hostJid
.split('/');
407 document
.getElementById('connected-to').innerText
= split
[0];
411 console
.error('Invalid "support-hosts" response from server.');
414 var errorMsg
= remoting
.Error
.UNEXPECTED
;
415 if (xhr
.status
== 404) {
416 errorMsg
= remoting
.Error
.INVALID_ACCESS_CODE
;
417 } else if (xhr
.status
== 0) {
418 errorMsg
= remoting
.Error
.NO_RESPONSE
;
419 } else if (xhr
.status
== 503) {
420 errorMsg
= remoting
.Error
.SERVICE_UNAVAILABLE
;
422 console
.error('The server responded: ' + xhr
.responseText
);
424 showConnectError_(errorMsg
);
428 * Normalize the access code entered by the user.
430 * @param {string} accessCode The access code, as entered by the user.
431 * @return {string} The normalized form of the code (whitespace removed).
433 function normalizeAccessCode_(accessCode
) {
435 // TODO(sergeyu): Do we need to do any other normalization here?
436 return accessCode
.replace(/\s/g, '');
440 * Initiate a request to the server to resolve a support ID.
442 * @param {string} supportId The canonicalized support ID.
443 * @param {string} token The OAuth access token.
445 function resolveSupportId(supportId
, token
) {
447 'Authorization': 'OAuth ' + token
450 remoting
.supportHostsXhr_
= remoting
.xhr
.get(
451 'https://www.googleapis.com/chromoting/v1/support-hosts/' +
452 encodeURIComponent(supportId
),
453 parseServerResponse_
,
459 * Timer callback to update the statistics panel.
461 function updateStatistics_() {
462 if (!remoting
.clientSession
||
463 remoting
.clientSession
.state
!= remoting
.ClientSession
.State
.CONNECTED
) {
466 var perfstats
= remoting
.clientSession
.getPerfStats();
467 remoting
.stats
.update(perfstats
);
468 remoting
.clientSession
.logStatistics(perfstats
);
469 // Update the stats once per second.
470 window
.setTimeout(updateStatistics_
, 1000);
474 * Shows PIN entry screen.
476 * @param {string} hostId The unique id of the host.
477 * @param {boolean} retryIfOffline If true and the host can't be contacted,
478 * refresh the host list and try again. This allows bookmarked hosts to
479 * work even if they reregister with Talk and get a different Jid.
480 * @return {void} Nothing.
482 remoting
.connectMe2Me = function(hostId
, retryIfOffline
) {
483 remoting
.currentConnectionType
= remoting
.ConnectionType
.Me2Me
;
484 remoting
.hostId
= hostId
;
485 remoting
.retryIfOffline
= retryIfOffline
;
487 var host
= remoting
.hostList
.getHostForId(remoting
.hostId
);
488 // If we're re-loading a tab for a host that has since been unregistered
489 // then the hostId may no longer resolve.
491 showConnectError_(remoting
.Error
.HOST_IS_OFFLINE
);
494 var message
= document
.getElementById('pin-message');
495 l10n
.localizeElement(message
, host
.hostName
);
496 remoting
.setMode(remoting
.AppMode
.CLIENT_PIN_PROMPT
);
500 * Start a connection to the specified host, using the cached details
501 * and the PIN entered by the user.
503 * @return {void} Nothing.
505 remoting
.connectMe2MeWithPin = function() {
506 console
.log('Connecting to host...');
507 remoting
.setMode(remoting
.AppMode
.CLIENT_CONNECTING
);
509 var host
= remoting
.hostList
.getHostForId(remoting
.hostId
);
510 // If the user clicked on a cached host that has since been removed then we
511 // won't find the hostId. If the user clicked on the entry for the local host
512 // immediately after having enabled it then we won't know it's JID or public
513 // key until the host heartbeats and we pull a fresh host list.
514 if (!host
|| !host
.jabberId
|| !host
.publicKey
) {
515 retryConnectOrReportOffline_();
518 remoting
.hostJid
= host
.jabberId
;
519 remoting
.hostPublicKey
= host
.publicKey
;
520 document
.getElementById('connected-to').innerText
= host
.hostName
;
521 document
.title
= chrome
.i18n
.getMessage('PRODUCT_NAME') + ': ' +
524 remoting
.WcsLoader
.load(connectMe2MeWithAccessToken_
,
525 remoting
.showErrorMessage
);
529 * Continue making the connection to a host, once WCS has initialized.
531 * @param {string?} token The OAuth2 access token, or null if an error occurred.
532 * @return {void} Nothing.
534 function connectMe2MeWithAccessToken_(token
) {
536 /** @type {string} */
537 var pin
= document
.getElementById('pin-entry').value
;
539 remoting
.clientSession
=
540 new remoting
.ClientSession(
541 remoting
.hostJid
, remoting
.hostPublicKey
,
542 pin
, 'spake2_hmac,spake2_plain', remoting
.hostId
,
543 /** @type {string} */ (remoting
.oauth2
.getCachedEmail()),
544 remoting
.ClientSession
.Mode
.ME2ME
, onClientStateChange_
);
545 // Don't log errors for cached JIDs.
546 remoting
.clientSession
.logErrors(!remoting
.retryIfOffline
);
547 remoting
.clientSession
.createPluginAndConnect(
548 document
.getElementById('session-mode'),
551 showConnectError_(remoting
.Error
.AUTHENTICATION_FAILED
);