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. 4 5'use strict'; 6 7/** @suppress {duplicate} */ 8var remoting = remoting || {}; 9 10/** @constructor */ 11remoting.HostController = function() { 12 this.hostDaemonFacade_ = this.createDaemonFacade_(); 13}; 14 15// Note that the values in the enums below are copied from 16// daemon_controller.h and must be kept in sync. 17/** @enum {number} */ 18remoting.HostController.State = { 19 NOT_IMPLEMENTED: -1, 20 NOT_INSTALLED: 0, 21 INSTALLING: 1, 22 STOPPED: 2, 23 STARTING: 3, 24 STARTED: 4, 25 STOPPING: 5, 26 UNKNOWN: 6 27}; 28 29/** 30 * @param {string} state The host controller state name. 31 * @return {remoting.HostController.State} The state enum value. 32 */ 33remoting.HostController.State.fromString = function(state) { 34 if (!remoting.HostController.State.hasOwnProperty(state)) { 35 throw "Invalid HostController.State: " + state; 36 } 37 return remoting.HostController.State[state]; 38} 39 40/** @enum {number} */ 41remoting.HostController.AsyncResult = { 42 OK: 0, 43 FAILED: 1, 44 CANCELLED: 2, 45 FAILED_DIRECTORY: 3 46}; 47 48/** 49 * @param {string} result The async result name. 50 * @return {remoting.HostController.AsyncResult} The result enum value. 51 */ 52remoting.HostController.AsyncResult.fromString = function(result) { 53 if (!remoting.HostController.AsyncResult.hasOwnProperty(result)) { 54 throw "Invalid HostController.AsyncResult: " + result; 55 } 56 return remoting.HostController.AsyncResult[result]; 57} 58 59/** 60 * @return {remoting.HostDaemonFacade} 61 * @private 62 */ 63remoting.HostController.prototype.createDaemonFacade_ = function() { 64 /** @type {remoting.HostDaemonFacade} @private */ 65 var hostDaemonFacade = new remoting.HostDaemonFacade(); 66 67 /** @param {string} version */ 68 var printVersion = function(version) { 69 if (version == '') { 70 console.log('Host not installed.'); 71 } else { 72 console.log('Host version: ' + version); 73 } 74 }; 75 76 hostDaemonFacade.getDaemonVersion(printVersion, function() { 77 console.log('Host version not available.'); 78 }); 79 80 return hostDaemonFacade; 81}; 82 83/** 84 * Set of features for which hasFeature() can be used to test. 85 * 86 * @enum {string} 87 */ 88remoting.HostController.Feature = { 89 PAIRING_REGISTRY: 'pairingRegistry', 90 OAUTH_CLIENT: 'oauthClient' 91}; 92 93/** 94 * @param {remoting.HostController.Feature} feature The feature to test for. 95 * @param {function(boolean):void} callback 96 * @return {void} 97 */ 98remoting.HostController.prototype.hasFeature = function(feature, callback) { 99 // TODO(rmsousa): This could synchronously return a boolean, provided it were 100 // only called after native messaging is completely initialized. 101 this.hostDaemonFacade_.hasFeature(feature, callback); 102}; 103 104/** 105 * @param {function(boolean, boolean, boolean):void} onDone Callback to be 106 * called when done. 107 * @param {function(remoting.Error):void} onError Callback to be called on 108 * error. 109 */ 110remoting.HostController.prototype.getConsent = function(onDone, onError) { 111 this.hostDaemonFacade_.getUsageStatsConsent(onDone, onError); 112}; 113 114/** 115 * Registers and starts the host. 116 * 117 * @param {string} hostPin Host PIN. 118 * @param {boolean} consent The user's consent to crash dump reporting. 119 * @param {function():void} onDone Callback to be called when done. 120 * @param {function(remoting.Error):void} onError Callback to be called on 121 * error. 122 * @return {void} Nothing. 123 */ 124remoting.HostController.prototype.start = function(hostPin, consent, onDone, 125 onError) { 126 /** @type {remoting.HostController} */ 127 var that = this; 128 129 /** @return {string} */ 130 function generateUuid() { 131 var random = new Uint16Array(8); 132 window.crypto.getRandomValues(random); 133 /** @type {Array.<string>} */ 134 var e = new Array(); 135 for (var i = 0; i < 8; i++) { 136 e[i] = (/** @type {number} */random[i] + 0x10000). 137 toString(16).substring(1); 138 } 139 return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' + 140 e[4] + '-' + e[5] + e[6] + e[7]; 141 }; 142 143 var newHostId = generateUuid(); 144 145 /** @param {remoting.Error} error */ 146 function onStartError(error) { 147 // Unregister the host if we failed to start it. 148 remoting.HostList.unregisterHostById(newHostId); 149 onError(error); 150 } 151 152 /** 153 * @param {string} hostName 154 * @param {string} publicKey 155 * @param {remoting.HostController.AsyncResult} result 156 */ 157 function onStarted(hostName, publicKey, result) { 158 if (result == remoting.HostController.AsyncResult.OK) { 159 remoting.hostList.onLocalHostStarted(hostName, newHostId, publicKey); 160 onDone(); 161 } else if (result == remoting.HostController.AsyncResult.CANCELLED) { 162 onStartError(remoting.Error.CANCELLED); 163 } else { 164 onStartError(remoting.Error.UNEXPECTED); 165 } 166 } 167 168 /** 169 * @param {string} hostName 170 * @param {string} publicKey 171 * @param {string} privateKey 172 * @param {string} xmppLogin 173 * @param {string} refreshToken 174 * @param {string} clientBaseJid 175 * @param {string} hostSecretHash 176 */ 177 function startHostWithHash(hostName, publicKey, privateKey, xmppLogin, 178 refreshToken, clientBaseJid, hostSecretHash) { 179 var hostConfig = { 180 xmpp_login: xmppLogin, 181 oauth_refresh_token: refreshToken, 182 host_id: newHostId, 183 host_name: hostName, 184 host_secret_hash: hostSecretHash, 185 private_key: privateKey 186 }; 187 var hostOwner = clientBaseJid; 188 var hostOwnerEmail = remoting.identity.getCachedEmail(); 189 if (hostOwner != xmppLogin) { 190 hostConfig['host_owner'] = hostOwner; 191 if (hostOwnerEmail != hostOwner) { 192 hostConfig['host_owner_email'] = hostOwnerEmail; 193 } 194 } 195 that.hostDaemonFacade_.startDaemon( 196 hostConfig, consent, onStarted.bind(null, hostName, publicKey), 197 onStartError); 198 } 199 200 /** 201 * @param {string} hostName 202 * @param {string} publicKey 203 * @param {string} privateKey 204 * @param {string} email 205 * @param {string} refreshToken 206 * @param {string} clientBaseJid 207 */ 208 function onClientBaseJid( 209 hostName, publicKey, privateKey, email, refreshToken, clientBaseJid) { 210 that.hostDaemonFacade_.getPinHash( 211 newHostId, hostPin, 212 startHostWithHash.bind(null, hostName, publicKey, privateKey, 213 email, refreshToken, clientBaseJid), 214 onError); 215 } 216 217 /** 218 * @param {string} hostName 219 * @param {string} publicKey 220 * @param {string} privateKey 221 * @param {string} email 222 * @param {string} refreshToken 223 */ 224 function onServiceAccountCredentials( 225 hostName, publicKey, privateKey, email, refreshToken) { 226 that.getClientBaseJid_( 227 onClientBaseJid.bind( 228 null, hostName, publicKey, privateKey, email, refreshToken), 229 onStartError); 230 } 231 232 /** 233 * @param {string} hostName 234 * @param {string} publicKey 235 * @param {string} privateKey 236 * @param {XMLHttpRequest} xhr 237 */ 238 function onRegistered( 239 hostName, publicKey, privateKey, xhr) { 240 var success = (xhr.status == 200); 241 242 if (success) { 243 var result = jsonParseSafe(xhr.responseText); 244 if ('data' in result && 'authorizationCode' in result['data']) { 245 that.hostDaemonFacade_.getCredentialsFromAuthCode( 246 result['data']['authorizationCode'], 247 onServiceAccountCredentials.bind( 248 null, hostName, publicKey, privateKey), 249 onError); 250 } else { 251 // No authorization code returned, use regular user credential flow. 252 that.hostDaemonFacade_.getPinHash( 253 newHostId, hostPin, startHostWithHash.bind( 254 null, hostName, publicKey, privateKey, 255 remoting.identity.getCachedEmail(), 256 remoting.oauth2.getRefreshToken()), 257 onError); 258 } 259 } else { 260 console.log('Failed to register the host. Status: ' + xhr.status + 261 ' response: ' + xhr.responseText); 262 onError(remoting.Error.REGISTRATION_FAILED); 263 } 264 } 265 266 /** 267 * @param {string} hostName 268 * @param {string} privateKey 269 * @param {string} publicKey 270 * @param {string} hostClientId 271 * @param {string} oauthToken 272 */ 273 function doRegisterHost( 274 hostName, privateKey, publicKey, hostClientId, oauthToken) { 275 var headers = { 276 'Authorization': 'OAuth ' + oauthToken, 277 'Content-type' : 'application/json; charset=UTF-8' 278 }; 279 280 var newHostDetails = { data: { 281 hostId: newHostId, 282 hostName: hostName, 283 publicKey: publicKey 284 } }; 285 286 var registerHostUrl = 287 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts'; 288 289 if (hostClientId) { 290 registerHostUrl += '?' + remoting.xhr.urlencodeParamHash( 291 { hostClientId: hostClientId }); 292 } 293 294 remoting.xhr.post( 295 registerHostUrl, 296 onRegistered.bind(null, hostName, publicKey, privateKey), 297 JSON.stringify(newHostDetails), 298 headers); 299 } 300 301 /** 302 * @param {string} hostName 303 * @param {string} privateKey 304 * @param {string} publicKey 305 * @param {string} hostClientId 306 */ 307 function onHostClientId( 308 hostName, privateKey, publicKey, hostClientId) { 309 remoting.identity.callWithToken( 310 doRegisterHost.bind( 311 null, hostName, privateKey, publicKey, hostClientId), onError); 312 } 313 314 /** 315 * @param {string} hostName 316 * @param {string} privateKey 317 * @param {string} publicKey 318 * @param {boolean} hasFeature 319 */ 320 function onHasFeatureOAuthClient( 321 hostName, privateKey, publicKey, hasFeature) { 322 if (hasFeature) { 323 that.hostDaemonFacade_.getHostClientId( 324 onHostClientId.bind(null, hostName, privateKey, publicKey), onError); 325 } else { 326 remoting.identity.callWithToken( 327 doRegisterHost.bind( 328 null, hostName, privateKey, publicKey, null), onError); 329 } 330 } 331 332 /** 333 * @param {string} hostName 334 * @param {string} privateKey 335 * @param {string} publicKey 336 */ 337 function onKeyGenerated(hostName, privateKey, publicKey) { 338 that.hasFeature( 339 remoting.HostController.Feature.OAUTH_CLIENT, 340 onHasFeatureOAuthClient.bind(null, hostName, privateKey, publicKey)); 341 } 342 343 /** 344 * @param {string} hostName 345 * @return {void} Nothing. 346 */ 347 function startWithHostname(hostName) { 348 that.hostDaemonFacade_.generateKeyPair(onKeyGenerated.bind(null, hostName), 349 onError); 350 } 351 352 this.hostDaemonFacade_.getHostName(startWithHostname, onError); 353}; 354 355/** 356 * Stop the daemon process. 357 * @param {function():void} onDone Callback to be called when done. 358 * @param {function(remoting.Error):void} onError Callback to be called on 359 * error. 360 * @return {void} Nothing. 361 */ 362remoting.HostController.prototype.stop = function(onDone, onError) { 363 /** @type {remoting.HostController} */ 364 var that = this; 365 366 /** @param {string?} hostId The host id of the local host. */ 367 function unregisterHost(hostId) { 368 if (hostId) { 369 remoting.HostList.unregisterHostById(hostId); 370 } 371 onDone(); 372 } 373 374 /** @param {remoting.HostController.AsyncResult} result */ 375 function onStopped(result) { 376 if (result == remoting.HostController.AsyncResult.OK) { 377 that.getLocalHostId(unregisterHost); 378 } else if (result == remoting.HostController.AsyncResult.CANCELLED) { 379 onError(remoting.Error.CANCELLED); 380 } else { 381 onError(remoting.Error.UNEXPECTED); 382 } 383 } 384 385 this.hostDaemonFacade_.stopDaemon(onStopped, onError); 386}; 387 388/** 389 * Check the host configuration is valid (non-null, and contains both host_id 390 * and xmpp_login keys). 391 * @param {Object} config The host configuration. 392 * @return {boolean} True if it is valid. 393 */ 394function isHostConfigValid_(config) { 395 return !!config && typeof config['host_id'] == 'string' && 396 typeof config['xmpp_login'] == 'string'; 397} 398 399/** 400 * @param {string} newPin The new PIN to set 401 * @param {function():void} onDone Callback to be called when done. 402 * @param {function(remoting.Error):void} onError Callback to be called on 403 * error. 404 * @return {void} Nothing. 405 */ 406remoting.HostController.prototype.updatePin = function(newPin, onDone, 407 onError) { 408 /** @type {remoting.HostController} */ 409 var that = this; 410 411 /** @param {remoting.HostController.AsyncResult} result */ 412 function onConfigUpdated(result) { 413 if (result == remoting.HostController.AsyncResult.OK) { 414 onDone(); 415 } else if (result == remoting.HostController.AsyncResult.CANCELLED) { 416 onError(remoting.Error.CANCELLED); 417 } else { 418 onError(remoting.Error.UNEXPECTED); 419 } 420 } 421 422 /** @param {string} pinHash */ 423 function updateDaemonConfigWithHash(pinHash) { 424 var newConfig = { 425 host_secret_hash: pinHash 426 }; 427 that.hostDaemonFacade_.updateDaemonConfig(newConfig, onConfigUpdated, 428 onError); 429 } 430 431 /** @param {Object} config */ 432 function onConfig(config) { 433 if (!isHostConfigValid_(config)) { 434 onError(remoting.Error.UNEXPECTED); 435 return; 436 } 437 /** @type {string} */ 438 var hostId = config['host_id']; 439 that.hostDaemonFacade_.getPinHash( 440 hostId, newPin, updateDaemonConfigWithHash, onError); 441 } 442 443 // TODO(sergeyu): When crbug.com/121518 is fixed: replace this call 444 // with an unprivileged version if that is necessary. 445 this.hostDaemonFacade_.getDaemonConfig(onConfig, onError); 446}; 447 448/** 449 * Get the state of the local host. 450 * 451 * @param {function(remoting.HostController.State):void} onDone Completion 452 * callback. 453 */ 454remoting.HostController.prototype.getLocalHostState = function(onDone) { 455 /** @param {remoting.Error} error */ 456 function onError(error) { 457 onDone((error == remoting.Error.MISSING_PLUGIN) ? 458 remoting.HostController.State.NOT_INSTALLED : 459 remoting.HostController.State.UNKNOWN); 460 } 461 this.hostDaemonFacade_.getDaemonState(onDone, onError); 462}; 463 464/** 465 * Get the id of the local host, or null if it is not registered. 466 * 467 * @param {function(string?):void} onDone Completion callback. 468 */ 469remoting.HostController.prototype.getLocalHostId = function(onDone) { 470 /** @type {remoting.HostController} */ 471 var that = this; 472 /** @param {Object} config */ 473 function onConfig(config) { 474 var hostId = null; 475 if (isHostConfigValid_(config)) { 476 hostId = /** @type {string} */ config['host_id']; 477 } 478 onDone(hostId); 479 }; 480 481 this.hostDaemonFacade_.getDaemonConfig(onConfig, function(error) { 482 onDone(null); 483 }); 484}; 485 486/** 487 * Fetch the list of paired clients for this host. 488 * 489 * @param {function(Array.<remoting.PairedClient>):void} onDone 490 * @param {function(remoting.Error):void} onError 491 * @return {void} 492 */ 493remoting.HostController.prototype.getPairedClients = function(onDone, 494 onError) { 495 this.hostDaemonFacade_.getPairedClients(onDone, onError); 496}; 497 498/** 499 * Delete a single paired client. 500 * 501 * @param {string} client The client id of the pairing to delete. 502 * @param {function():void} onDone Completion callback. 503 * @param {function(remoting.Error):void} onError Error callback. 504 * @return {void} 505 */ 506remoting.HostController.prototype.deletePairedClient = function( 507 client, onDone, onError) { 508 this.hostDaemonFacade_.deletePairedClient(client, onDone, onError); 509}; 510 511/** 512 * Delete all paired clients. 513 * 514 * @param {function():void} onDone Completion callback. 515 * @param {function(remoting.Error):void} onError Error callback. 516 * @return {void} 517 */ 518remoting.HostController.prototype.clearPairedClients = function( 519 onDone, onError) { 520 this.hostDaemonFacade_.clearPairedClients(onDone, onError); 521}; 522 523/** 524 * Gets the host owner's base JID, used by the host for client authorization. 525 * In most cases this is the same as the owner's email address, but for 526 * non-Gmail accounts, it may be different. 527 * 528 * @private 529 * @param {function(string): void} onSuccess 530 * @param {function(remoting.Error): void} onError 531 */ 532remoting.HostController.prototype.getClientBaseJid_ = function( 533 onSuccess, onError) { 534 var signalStrategy = null; 535 536 var onState = function(state) { 537 switch (state) { 538 case remoting.SignalStrategy.State.CONNECTED: 539 var jid = signalStrategy.getJid().split('/')[0].toLowerCase(); 540 base.dispose(signalStrategy); 541 signalStrategy = null; 542 onSuccess(jid); 543 break; 544 545 case remoting.SignalStrategy.State.FAILED: 546 var error = signalStrategy.getError(); 547 base.dispose(signalStrategy); 548 signalStrategy = null; 549 onError(error); 550 break; 551 } 552 }; 553 554 signalStrategy = remoting.SignalStrategy.create(onState); 555 556 /** @param {string} token */ 557 function connectSignalingWithToken(token) { 558 remoting.identity.getEmail( 559 connectSignalingWithTokenAndEmail.bind(null, token), onError); 560 } 561 562 /** 563 * @param {string} token 564 * @param {string} email 565 */ 566 function connectSignalingWithTokenAndEmail(token, email) { 567 signalStrategy.connect( 568 remoting.settings.XMPP_SERVER_ADDRESS, email, token); 569 } 570 571 remoting.identity.callWithToken(connectSignalingWithToken, onError); 572}; 573 574/** @type {remoting.HostController} */ 575remoting.hostController = null; 576