Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(39)

Side by Side Diff: remoting/webapp/session_connector.js

Issue 552403004: Interfaceify ClientPlugin in preparation for mocking it. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Renamed interfaces. Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview 6 * @fileoverview
7 * Connect set-up state machine for Me2Me and IT2Me 7 * Interface abstracting the SessionConnector functionality.
Sergey Ulanov 2014/09/20 00:35:25 Now that the implementation is called SessionConne
Jamie 2014/09/20 00:54:22 Done.
8 */ 8 */
9 9
10 'use strict'; 10 'use strict';
11 11
12 /** @suppress {duplicate} */ 12 /** @suppress {duplicate} */
13 var remoting = remoting || {}; 13 var remoting = remoting || {};
14 14
15 /** 15 /**
16 * @param {HTMLElement} clientContainer Container element for the client view. 16 * @interface
17 * @param {function(remoting.ClientSession):void} onConnected Callback on
18 * success.
19 * @param {function(remoting.Error):void} onError Callback on error.
20 * @param {function(string, string):boolean} onExtensionMessage The handler for
21 * protocol extension messages. Returns true if a message is recognized;
22 * false otherwise.
23 * @constructor
24 */ 17 */
25 remoting.SessionConnector = function(clientContainer, onConnected, onError, 18 remoting.SessionConnector = function() {};
26 onExtensionMessage) {
27 /**
28 * @type {HTMLElement}
29 * @private
30 */
31 this.clientContainer_ = clientContainer;
32
33 /**
34 * @type {function(remoting.ClientSession):void}
35 * @private
36 */
37 this.onConnected_ = onConnected;
38
39 /**
40 * @type {function(remoting.Error):void}
41 * @private
42 */
43 this.onError_ = onError;
44
45 /**
46 * @type {function(string, string):boolean}
47 * @private
48 */
49 this.onExtensionMessage_ = onExtensionMessage;
50
51 /**
52 * @type {string}
53 * @private
54 */
55 this.clientJid_ = '';
56
57 /**
58 * @type {remoting.ClientSession.Mode}
59 * @private
60 */
61 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
62
63 /**
64 * @type {remoting.SignalStrategy}
65 * @private
66 */
67 this.signalStrategy_ = null;
68
69 /**
70 * @type {remoting.SmartReconnector}
71 * @private
72 */
73 this.reconnector_ = null;
74
75 /**
76 * @private
77 */
78 this.bound_ = {
79 onStateChange : this.onStateChange_.bind(this)
80 };
81
82 // Initialize/declare per-connection state.
83 this.reset();
84 };
85 19
86 /** 20 /**
87 * Reset the per-connection state so that the object can be re-used for a 21 * Reset the per-connection state so that the object can be re-used for a
88 * second connection. Note the none of the shared WCS state is reset. 22 * second connection. Note the none of the shared WCS state is reset.
89 */ 23 */
90 remoting.SessionConnector.prototype.reset = function() { 24 remoting.SessionConnector.prototype.reset = function() {};
91 /**
92 * Set to true to indicate that the user requested pairing when entering
93 * their PIN for a Me2Me connection.
94 *
95 * @type {boolean}
96 */
97 this.pairingRequested = false;
98
99 /**
100 * String used to identify the host to which to connect. For IT2Me, this is
101 * the first 7 digits of the access code; for Me2Me it is the host identifier.
102 *
103 * @type {string}
104 * @private
105 */
106 this.hostId_ = '';
107
108 /**
109 * For paired connections, the client id of this device, issued by the host.
110 *
111 * @type {string}
112 * @private
113 */
114 this.clientPairingId_ = '';
115
116 /**
117 * For paired connections, the paired secret for this device, issued by the
118 * host.
119 *
120 * @type {string}
121 * @private
122 */
123 this.clientPairedSecret_ = '';
124
125 /**
126 * String used to authenticate to the host on connection. For IT2Me, this is
127 * the access code; for Me2Me it is the PIN.
128 *
129 * @type {string}
130 * @private
131 */
132 this.passPhrase_ = '';
133
134 /**
135 * @type {string}
136 * @private
137 */
138 this.hostJid_ = '';
139
140 /**
141 * @type {string}
142 * @private
143 */
144 this.hostPublicKey_ = '';
145
146 /**
147 * @type {boolean}
148 * @private
149 */
150 this.refreshHostJidIfOffline_ = false;
151
152 /**
153 * @type {remoting.ClientSession}
154 * @private
155 */
156 this.clientSession_ = null;
157
158 /**
159 * @type {XMLHttpRequest}
160 * @private
161 */
162 this.pendingXhr_ = null;
163
164 /**
165 * Function to interactively obtain the PIN from the user.
166 * @type {function(boolean, function(string):void):void}
167 * @private
168 */
169 this.fetchPin_ = function(onPinFetched) {};
170
171 /**
172 * @type {function(string, string, string,
173 * function(string, string):void): void}
174 * @private
175 */
176 this.fetchThirdPartyToken_ = function(
177 tokenUrl, scope, onThirdPartyTokenFetched) {};
178
179 /**
180 * Host 'name', as displayed in the client tool-bar. For a Me2Me connection,
181 * this is the name of the host; for an IT2Me connection, it is the email
182 * address of the person sharing their computer.
183 *
184 * @type {string}
185 * @private
186 */
187 this.hostDisplayName_ = '';
188 };
189 25
190 /** 26 /**
191 * Initiate a Me2Me connection. 27 * Initiate a Me2Me connection.
192 * 28 *
193 * @param {remoting.Host} host The Me2Me host to which to connect. 29 * @param {remoting.Host} host The Me2Me host to which to connect.
194 * @param {function(boolean, function(string):void):void} fetchPin Function to 30 * @param {function(boolean, function(string):void):void} fetchPin Function to
195 * interactively obtain the PIN from the user. 31 * interactively obtain the PIN from the user.
196 * @param {function(string, string, string, 32 * @param {function(string, string, string,
197 * function(string, string): void): void} 33 * function(string, string): void): void}
198 * fetchThirdPartyToken Function to obtain a token from a third party 34 * fetchThirdPartyToken Function to obtain a token from a third party
199 * authenticaiton server. 35 * authenticaiton server.
200 * @param {string} clientPairingId The client id issued by the host when 36 * @param {string} clientPairingId The client id issued by the host when
201 * this device was paired, if it is already paired. 37 * this device was paired, if it is already paired.
202 * @param {string} clientPairedSecret The shared secret issued by the host when 38 * @param {string} clientPairedSecret The shared secret issued by the host when
203 * this device was paired, if it is already paired. 39 * this device was paired, if it is already paired.
204 * @return {void} Nothing. 40 * @return {void} Nothing.
205 */ 41 */
206 remoting.SessionConnector.prototype.connectMe2Me = 42 remoting.SessionConnector.prototype.connectMe2Me =
207 function(host, fetchPin, fetchThirdPartyToken, 43 function(host, fetchPin, fetchThirdPartyToken,
208 clientPairingId, clientPairedSecret) { 44 clientPairingId, clientPairedSecret) {};
209 this.connectMe2MeInternal_(
210 host.hostId, host.jabberId, host.publicKey, host.hostName,
211 fetchPin, fetchThirdPartyToken,
212 clientPairingId, clientPairedSecret, true);
213 };
214 45
215 /** 46 /**
216 * Update the pairing info so that the reconnect function will work correctly. 47 * Update the pairing info so that the reconnect function will work correctly.
217 * 48 *
218 * @param {string} clientId The paired client id. 49 * @param {string} clientId The paired client id.
219 * @param {string} sharedSecret The shared secret. 50 * @param {string} sharedSecret The shared secret.
220 */ 51 */
221 remoting.SessionConnector.prototype.updatePairingInfo = 52 remoting.SessionConnector.prototype.updatePairingInfo =
222 function(clientId, sharedSecret) { 53 function(clientId, sharedSecret) {};
223 this.clientPairingId_ = clientId;
224 this.clientPairedSecret_ = sharedSecret;
225 };
226
227 /**
228 * Initiate a Me2Me connection.
229 *
230 * @param {string} hostId ID of the Me2Me host.
231 * @param {string} hostJid XMPP JID of the host.
232 * @param {string} hostPublicKey Public Key of the host.
233 * @param {string} hostDisplayName Display name (friendly name) of the host.
234 * @param {function(boolean, function(string):void):void} fetchPin Function to
235 * interactively obtain the PIN from the user.
236 * @param {function(string, string, string,
237 * function(string, string): void): void}
238 * fetchThirdPartyToken Function to obtain a token from a third party
239 * authenticaiton server.
240 * @param {string} clientPairingId The client id issued by the host when
241 * this device was paired, if it is already paired.
242 * @param {string} clientPairedSecret The shared secret issued by the host when
243 * this device was paired, if it is already paired.
244 * @param {boolean} refreshHostJidIfOffline Whether to refresh the JID and retry
245 * the connection if the current JID is offline.
246 * @return {void} Nothing.
247 * @private
248 */
249 remoting.SessionConnector.prototype.connectMe2MeInternal_ =
250 function(hostId, hostJid, hostPublicKey, hostDisplayName,
251 fetchPin, fetchThirdPartyToken,
252 clientPairingId, clientPairedSecret,
253 refreshHostJidIfOffline) {
254 // Cancel any existing connect operation.
255 this.cancel();
256
257 this.hostId_ = hostId;
258 this.hostJid_ = hostJid;
259 this.hostPublicKey_ = hostPublicKey;
260 this.fetchPin_ = fetchPin;
261 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
262 this.hostDisplayName_ = hostDisplayName;
263 this.connectionMode_ = remoting.ClientSession.Mode.ME2ME;
264 this.refreshHostJidIfOffline_ = refreshHostJidIfOffline;
265 this.updatePairingInfo(clientPairingId, clientPairedSecret);
266
267 this.connectSignaling_();
268 }
269 54
270 /** 55 /**
271 * Initiate an IT2Me connection. 56 * Initiate an IT2Me connection.
272 * 57 *
273 * @param {string} accessCode The access code as entered by the user. 58 * @param {string} accessCode The access code as entered by the user.
274 * @return {void} Nothing. 59 * @return {void} Nothing.
275 */ 60 */
276 remoting.SessionConnector.prototype.connectIT2Me = function(accessCode) { 61 remoting.SessionConnector.prototype.connectIT2Me =
277 var kSupportIdLen = 7; 62 function(accessCode) {};
278 var kHostSecretLen = 5;
279 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
280
281 // Cancel any existing connect operation.
282 this.cancel();
283
284 var normalizedAccessCode = this.normalizeAccessCode_(accessCode);
285 if (normalizedAccessCode.length != kAccessCodeLen) {
286 this.onError_(remoting.Error.INVALID_ACCESS_CODE);
287 return;
288 }
289
290 this.hostId_ = normalizedAccessCode.substring(0, kSupportIdLen);
291 this.passPhrase_ = normalizedAccessCode;
292 this.connectionMode_ = remoting.ClientSession.Mode.IT2ME;
293 remoting.identity.callWithToken(this.connectIT2MeWithToken_.bind(this),
294 this.onError_);
295 };
296 63
297 /** 64 /**
298 * Reconnect a closed connection. 65 * Reconnect a closed connection.
299 * 66 *
300 * @return {void} Nothing. 67 * @return {void} Nothing.
301 */ 68 */
302 remoting.SessionConnector.prototype.reconnect = function() { 69 remoting.SessionConnector.prototype.reconnect = function() {};
303 if (this.connectionMode_ == remoting.ClientSession.Mode.IT2ME) {
304 console.error('reconnect not supported for IT2Me.');
305 return;
306 }
307 this.connectMe2MeInternal_(
308 this.hostId_, this.hostJid_, this.hostPublicKey_, this.hostDisplayName_,
309 this.fetchPin_, this.fetchThirdPartyToken_,
310 this.clientPairingId_, this.clientPairedSecret_, true);
311 };
312 70
313 /** 71 /**
314 * Cancel a connection-in-progress. 72 * Cancel a connection-in-progress.
315 */ 73 */
316 remoting.SessionConnector.prototype.cancel = function() { 74 remoting.SessionConnector.prototype.cancel = function() {};
317 if (this.clientSession_) {
318 this.clientSession_.removePlugin();
319 this.clientSession_ = null;
320 }
321 if (this.pendingXhr_) {
322 this.pendingXhr_.abort();
323 this.pendingXhr_ = null;
324 }
325 this.reset();
326 };
327 75
328 /** 76 /**
329 * Get the connection mode (Me2Me or IT2Me) 77 * Get the connection mode (Me2Me or IT2Me)
330 * 78 *
331 * @return {remoting.ClientSession.Mode} 79 * @return {remoting.ClientSession.Mode}
332 */ 80 */
333 remoting.SessionConnector.prototype.getConnectionMode = function() { 81 remoting.SessionConnector.prototype.getConnectionMode = function() {};
334 return this.connectionMode_;
335 };
336 82
337 /** 83 /**
338 * Get host ID. 84 * Get host ID.
339 * 85 *
340 * @return {string} 86 * @return {string}
341 */ 87 */
342 remoting.SessionConnector.prototype.getHostId = function() { 88 remoting.SessionConnector.prototype.getHostId = function() {};
343 return this.hostId_; 89
344 };
345 90
346 /** 91 /**
347 * @private 92 * @interface
348 */ 93 */
349 remoting.SessionConnector.prototype.connectSignaling_ = function() { 94 remoting.SessionConnectorFactory = function() {};
350 base.dispose(this.signalStrategy_);
351 this.signalStrategy_ = null;
352
353 /** @type {remoting.SessionConnector} */
354 var that = this;
355
356 /** @param {string} token */
357 function connectSignalingWithToken(token) {
358 remoting.identity.getEmail(
359 connectSignalingWithTokenAndEmail.bind(null, token), that.onError_);
360 }
361
362 /**
363 * @param {string} token
364 * @param {string} email
365 */
366 function connectSignalingWithTokenAndEmail(token, email) {
367 that.signalStrategy_.connect(
368 remoting.settings.XMPP_SERVER_ADDRESS, email, token);
369 }
370
371 // Only use XMPP when TCP API is available and TLS support is enabled. That's
372 // not the case for V1 app (socket API is available only to platform apps)
373 // and for Chrome releases before 38.
374 if (chrome.socket && chrome.socket.secure) {
375 this.signalStrategy_ = /** @type {remoting.SignalStrategy} */
376 (new remoting.XmppConnection(this.onSignalingState_.bind(this)));
377 } else {
378 this.signalStrategy_ = /** @type {remoting.SignalStrategy} */
379 (new remoting.WcsAdapter(this.onSignalingState_.bind(this)));
380 }
381
382 remoting.identity.callWithToken(connectSignalingWithToken, this.onError_);
383 };
384 95
385 /** 96 /**
386 * @private 97 * @param {HTMLElement} clientContainer Container element for the client view.
387 * @param {remoting.SignalStrategy.State} state 98 * @param {function(remoting.ClientSession):void} onConnected Callback on
99 * success.
100 * @param {function(remoting.Error):void} onError Callback on error.
101 * @param {function(string, string):boolean} onExtensionMessage The handler for
102 * protocol extension messages. Returns true if a message is recognized;
103 * false otherwise.
104 * @return {remoting.SessionConnector}
388 */ 105 */
389 remoting.SessionConnector.prototype.onSignalingState_ = function(state) { 106 remoting.SessionConnectorFactory.prototype.createConnector =
390 switch (state) { 107 function(clientContainer, onConnected, onError, onExtensionMessage) {};
391 case remoting.SignalStrategy.State.CONNECTED:
392 // Proceed only if the connection hasn't been canceled.
393 if (this.hostJid_) {
394 this.createSession_();
395 }
396 break;
397
398 case remoting.SignalStrategy.State.FAILED:
399 this.onError_(this.signalStrategy_.getError());
400 break;
401 }
402 };
403 108
404 /** 109 /**
405 * Continue an IT2Me connection once an access token has been obtained. 110 * @type {remoting.SessionConnectorFactory}
406 *
407 * @param {string} token An OAuth2 access token.
408 * @return {void} Nothing.
409 * @private
410 */ 111 */
411 remoting.SessionConnector.prototype.connectIT2MeWithToken_ = function(token) { 112 remoting.SessionConnector.factory = null;
412 // Resolve the host id to get the host JID.
413 this.pendingXhr_ = remoting.xhr.get(
414 remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
415 encodeURIComponent(this.hostId_),
416 this.onIT2MeHostInfo_.bind(this),
417 '',
418 { 'Authorization': 'OAuth ' + token });
419 };
420
421 /**
422 * Continue an IT2Me connection once the host JID has been looked up.
423 *
424 * @param {XMLHttpRequest} xhr The server response to the support-hosts query.
425 * @return {void} Nothing.
426 * @private
427 */
428 remoting.SessionConnector.prototype.onIT2MeHostInfo_ = function(xhr) {
429 this.pendingXhr_ = null;
430 if (xhr.status == 200) {
431 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
432 jsonParseSafe(xhr.responseText);
433 if (host && host.data && host.data.jabberId && host.data.publicKey) {
434 this.hostJid_ = host.data.jabberId;
435 this.hostPublicKey_ = host.data.publicKey;
436 this.hostDisplayName_ = this.hostJid_.split('/')[0];
437 this.createSession_();
438 return;
439 } else {
440 console.error('Invalid "support-hosts" response from server.');
441 }
442 } else {
443 this.onError_(this.translateSupportHostsError(xhr.status));
444 }
445 };
446
447 /**
448 * Creates ClientSession object.
449 */
450 remoting.SessionConnector.prototype.createSession_ = function() {
451 // In some circumstances, the WCS <iframe> can get reloaded, which results
452 // in a new clientJid and a new callback. In this case, remove the old
453 // client plugin before instantiating a new one.
454 if (this.clientSession_) {
455 this.clientSession_.removePlugin();
456 this.clientSession_ = null;
457 }
458
459 var authenticationMethods =
460 'third_party,spake2_pair,spake2_hmac,spake2_plain';
461 this.clientSession_ = new remoting.ClientSession(
462 this.signalStrategy_, this.clientContainer_, this.hostDisplayName_,
463 this.passPhrase_, this.fetchPin_, this.fetchThirdPartyToken_,
464 authenticationMethods, this.hostId_, this.hostJid_, this.hostPublicKey_,
465 this.connectionMode_, this.clientPairingId_, this.clientPairedSecret_);
466 this.clientSession_.logHostOfflineErrors(!this.refreshHostJidIfOffline_);
467 this.clientSession_.addEventListener(
468 remoting.ClientSession.Events.stateChanged,
469 this.bound_.onStateChange);
470 this.clientSession_.createPluginAndConnect(this.onExtensionMessage_);
471 };
472
473 /**
474 * Handle a change in the state of the client session prior to successful
475 * connection (after connection, this class no longer handles state change
476 * events). Errors that occur while connecting either trigger a reconnect
477 * or notify the onError handler.
478 *
479 * @param {remoting.ClientSession.StateEvent} event
480 * @return {void} Nothing.
481 * @private
482 */
483 remoting.SessionConnector.prototype.onStateChange_ = function(event) {
484 switch (event.current) {
485 case remoting.ClientSession.State.CONNECTED:
486 // When the connection succeeds, deregister for state-change callbacks
487 // and pass the session to the onConnected callback. It is expected that
488 // it will register a new state-change callback to handle disconnect
489 // or error conditions.
490 this.clientSession_.removeEventListener(
491 remoting.ClientSession.Events.stateChanged,
492 this.bound_.onStateChange);
493
494 base.dispose(this.reconnector_);
495 if (this.connectionMode_ != remoting.ClientSession.Mode.IT2ME) {
496 this.reconnector_ =
497 new remoting.SmartReconnector(this, this.clientSession_);
498 }
499 this.onConnected_(this.clientSession_);
500 break;
501
502 case remoting.ClientSession.State.CREATED:
503 console.log('Created plugin');
504 break;
505
506 case remoting.ClientSession.State.CONNECTING:
507 console.log('Connecting as ' + remoting.identity.getCachedEmail());
508 break;
509
510 case remoting.ClientSession.State.INITIALIZING:
511 console.log('Initializing connection');
512 break;
513
514 case remoting.ClientSession.State.CLOSED:
515 // This class deregisters for state-change callbacks when the CONNECTED
516 // state is reached, so it only sees the CLOSED state in exceptional
517 // circumstances. For example, a CONNECTING -> CLOSED transition happens
518 // if the host closes the connection without an error message instead of
519 // accepting it. Since there's no way of knowing exactly what went wrong,
520 // we rely on server-side logs in this case and report a generic error
521 // message.
522 this.onError_(remoting.Error.UNEXPECTED);
523 break;
524
525 case remoting.ClientSession.State.FAILED:
526 var error = this.clientSession_.getError();
527 console.error('Client plugin reported connection failed: ' + error);
528 if (error == null) {
529 error = remoting.Error.UNEXPECTED;
530 }
531 if (error == remoting.Error.HOST_IS_OFFLINE &&
532 this.refreshHostJidIfOffline_) {
533 // The plugin will be re-created when the host finished refreshing
534 remoting.hostList.refresh(this.onHostListRefresh_.bind(this));
535 } else {
536 this.onError_(error);
537 }
538 break;
539
540 default:
541 console.error('Unexpected client plugin state: ' + event.current);
542 // This should only happen if the web-app and client plugin get out of
543 // sync, and even then the version check should ensure compatibility.
544 this.onError_(remoting.Error.MISSING_PLUGIN);
545 }
546 };
547
548 /**
549 * @param {boolean} success True if the host list was successfully refreshed;
550 * false if an error occurred.
551 * @private
552 */
553 remoting.SessionConnector.prototype.onHostListRefresh_ = function(success) {
554 if (success) {
555 var host = remoting.hostList.getHostForId(this.hostId_);
556 if (host) {
557 this.connectMe2MeInternal_(
558 host.hostId, host.jabberId, host.publicKey, host.hostName,
559 this.fetchPin_, this.fetchThirdPartyToken_,
560 this.clientPairingId_, this.clientPairedSecret_, false);
561 return;
562 }
563 }
564 this.onError_(remoting.Error.HOST_IS_OFFLINE);
565 };
566
567 /**
568 * @param {number} error An HTTP error code returned by the support-hosts
569 * endpoint.
570 * @return {remoting.Error} The equivalent remoting.Error code.
571 * @private
572 */
573 remoting.SessionConnector.prototype.translateSupportHostsError =
574 function(error) {
575 switch (error) {
576 case 0: return remoting.Error.NETWORK_FAILURE;
577 case 404: return remoting.Error.INVALID_ACCESS_CODE;
578 case 502: // No break
579 case 503: return remoting.Error.SERVICE_UNAVAILABLE;
580 default: return remoting.Error.UNEXPECTED;
581 }
582 };
583
584 /**
585 * Normalize the access code entered by the user.
586 *
587 * @param {string} accessCode The access code, as entered by the user.
588 * @return {string} The normalized form of the code (whitespace removed).
589 */
590 remoting.SessionConnector.prototype.normalizeAccessCode_ =
591 function(accessCode) {
592 // Trim whitespace.
593 return accessCode.replace(/\s/g, '');
594 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698