OLD | NEW |
(Empty) | |
| 1 # Copyright 2015 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 import logging |
| 6 import os |
| 7 import subprocess |
| 8 import sys |
| 9 import time |
| 10 |
| 11 # Add the telemetry directory to Python's search paths. |
| 12 current_directory = os.path.dirname(os.path.realpath(__file__)) |
| 13 telemetry_dir = os.path.realpath( |
| 14 os.path.join(current_directory, '..', '..', '..', 'tools', 'telemetry')) |
| 15 if telemetry_dir not in sys.path: |
| 16 sys.path.append(telemetry_dir) |
| 17 |
| 18 from telemetry.core import browser_options |
| 19 from telemetry.core import browser_finder |
| 20 from telemetry.core import extension_to_load |
| 21 from telemetry.core import exceptions |
| 22 from telemetry.core import util |
| 23 from telemetry.core.platform import cros_interface |
| 24 |
| 25 logger = logging.getLogger('proximity_auth.%s' % __name__) |
| 26 |
| 27 |
| 28 class AccountPickerScreen(object): |
| 29 """ Wrapper for the ChromeOS account picker screen. |
| 30 |
| 31 The account picker screen is the WebContents page used for both the lock |
| 32 screen and signin screen. |
| 33 |
| 34 Note: This class assumes the account picker screen only has one user. If there |
| 35 are multiple user pods, the first one will be used. |
| 36 """ |
| 37 |
| 38 class AuthType: |
| 39 """ The authentication type expected for a user pod. """ |
| 40 OFFLINE_PASSWORD = 0 |
| 41 ONLINE_SIGN_IN = 1 |
| 42 NUMERIC_PIN = 2 |
| 43 USER_CLICK = 3 |
| 44 EXPAND_THEN_USER_CLICK = 4 |
| 45 FORCE_OFFLINE_PASSWORD = 5 |
| 46 |
| 47 class SmartLockState: |
| 48 """ The state of the Smart Lock icon on a user pod. |
| 49 """ |
| 50 NOT_SHOWN = 'not_shown' |
| 51 AUTHENTICATED = 'authenticated' |
| 52 LOCKED = 'locked' |
| 53 HARD_LOCKED = 'hardlocked' |
| 54 TO_BE_ACTIVATED = 'to_be_activated' |
| 55 SPINNER = 'spinner' |
| 56 |
| 57 # JavaScript expression for getting the user pod on the page |
| 58 _GET_POD_JS = 'document.getElementById("pod-row").pods[0]' |
| 59 |
| 60 def __init__(self, oobe, chromeos): |
| 61 """ |
| 62 Args: |
| 63 oobe: Inspector page of the OOBE WebContents. |
| 64 chromeos: The parent Chrome wrapper. |
| 65 """ |
| 66 self._oobe = oobe |
| 67 self._chromeos = chromeos |
| 68 |
| 69 @property |
| 70 def is_lockscreen(self): |
| 71 return self._oobe.EvaluateJavaScript( |
| 72 '!document.getElementById("sign-out-user-item").hidden') |
| 73 |
| 74 @property |
| 75 def auth_type(self): |
| 76 return self._oobe.EvaluateJavaScript('%s.authType' % self._GET_POD_JS) |
| 77 |
| 78 @property |
| 79 def smart_lock_state(self): |
| 80 icon_shown = self._oobe.EvaluateJavaScript( |
| 81 '!%s.customIconElement.hidden' % self._GET_POD_JS) |
| 82 if not icon_shown: |
| 83 return self.SmartLockState.NOT_SHOWN |
| 84 class_list_dict = self._oobe.EvaluateJavaScript( |
| 85 '%s.customIconElement.querySelector(".custom-icon")' |
| 86 '.classList' % self._GET_POD_JS) |
| 87 class_list = [v for k,v in class_list_dict.items() if k != 'length'] |
| 88 |
| 89 if 'custom-icon-unlocked' in class_list: |
| 90 return self.SmartLockState.AUTHENTICATED |
| 91 if 'custom-icon-locked' in class_list: |
| 92 return self.SmartLockState.LOCKED |
| 93 if 'custom-icon-hardlocked' in class_list: |
| 94 return self.SmartLockState.HARD_LOCKED |
| 95 if 'custom-icon-locked-to-be-activated' in class_list: |
| 96 return self.SmartLockState.TO_BE_ACTIVATED |
| 97 if 'custom-icon-spinner' in class_list: |
| 98 return self.SmartLockState.SPINNER |
| 99 |
| 100 def WaitForSmartLockState(self, state, wait_time_secs=60): |
| 101 """ Waits for the Smart Lock icon to reach the given state. |
| 102 |
| 103 Args: |
| 104 state: A value in AccountPickerScreen.SmartLockState |
| 105 wait_time_secs: The time to wait |
| 106 Returns: |
| 107 True if the state is reached within the wait time, else False. |
| 108 """ |
| 109 try: |
| 110 util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs) |
| 111 return True |
| 112 except exceptions.TimeoutException: |
| 113 return False |
| 114 |
| 115 def EnterPassword(self): |
| 116 """ Enters the password to unlock or sign-in. |
| 117 |
| 118 Raises: |
| 119 TimeoutException: entering the password fails to enter/resume the user |
| 120 session. |
| 121 """ |
| 122 assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or |
| 123 self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD) |
| 124 oobe = self._oobe |
| 125 oobe.EvaluateJavaScript( |
| 126 '%s.passwordElement.value = "%s"' % ( |
| 127 self._GET_POD_JS, self._chromeos.password)) |
| 128 oobe.EvaluateJavaScript( |
| 129 '%s.activate()' % self._GET_POD_JS) |
| 130 util.WaitFor(lambda: (self._chromeos.session_state == |
| 131 ChromeOS.SessionState.IN_SESSION), |
| 132 5) |
| 133 |
| 134 def UnlockWithClick(self): |
| 135 """ Clicks the user pod to unlock or sign-in. """ |
| 136 assert(self.auth_type == self.AuthType.USER_CLICK) |
| 137 self._oobe.EvaluateJavaScript('%s.activate()' % self._GET_POD_JS) |
| 138 |
| 139 |
| 140 class SmartLockSettings(object): |
| 141 """ Wrapper for the Smart Lock settings in chromeos://settings. |
| 142 """ |
| 143 def __init__(self, tab, chromeos): |
| 144 """ |
| 145 Args: |
| 146 tab: Inspector page of the chromeos://settings tag. |
| 147 chromeos: The parent Chrome wrapper. |
| 148 """ |
| 149 self._tab = tab |
| 150 self._chromeos = chromeos |
| 151 |
| 152 @property |
| 153 def is_smart_lock_enabled(self): |
| 154 ''' Returns true if the settings show that Smart Lock is enabled. ''' |
| 155 return self._tab.EvaluateJavaScript( |
| 156 '!document.getElementById("easy-unlock-enabled").hidden') |
| 157 |
| 158 def TurnOffSmartLock(self): |
| 159 """ Turns off Smart Lock. |
| 160 |
| 161 Smart Lock is turned off by clicking the turn-off button and navigating |
| 162 through the resulting overlay. |
| 163 |
| 164 Raises: |
| 165 TimeoutException: Timed out waiting for Smart Lock to be turned off. |
| 166 """ |
| 167 assert(self.is_smart_lock_enabled) |
| 168 tab = self._tab |
| 169 tab.EvaluateJavaScript( |
| 170 'document.getElementById("easy-unlock-turn-off-button").click()') |
| 171 util.WaitFor(lambda: tab.EvaluateJavaScript( |
| 172 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && ' |
| 173 'document.getElementById("easy-unlock-turn-off-confirm") != null'), |
| 174 10) |
| 175 tab.EvaluateJavaScript( |
| 176 'document.getElementById("easy-unlock-turn-off-confirm").click()') |
| 177 util.WaitFor(lambda: tab.EvaluateJavaScript( |
| 178 '!document.getElementById("easy-unlock-disabled").hidden'), 15) |
| 179 |
| 180 def StartSetup(self): |
| 181 """ Starts the Smart Lock setup flow by clicking the button. |
| 182 """ |
| 183 assert(not self.is_smart_lock_enabled) |
| 184 self._tab.EvaluateJavaScript( |
| 185 'document.getElementById("easy-unlock-setup-button").click()') |
| 186 |
| 187 def StartSetupAndReturnApp(self): |
| 188 """ Runs the setup and returns the wrapper to the setup app. |
| 189 |
| 190 After clicking the setup button in the settings page, enter the password to |
| 191 reauthenticate the user before the app launches. |
| 192 |
| 193 Returns: |
| 194 A SmartLockApp object of the app that was launched. |
| 195 |
| 196 Raises: |
| 197 TimeoutException: Timed out waiting for app. |
| 198 """ |
| 199 self.StartSetup() |
| 200 util.WaitFor(lambda: (self._chromeos.session_state == |
| 201 ChromeOS.SessionState.LOCK_SCREEN), |
| 202 5) |
| 203 lock_screen = self._chromeos.GetAccountPickerScreen() |
| 204 lock_screen.EnterPassword() |
| 205 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10) |
| 206 return self._chromeos.GetSmartLockApp() |
| 207 |
| 208 |
| 209 class SmartLockApp(object): |
| 210 """ Wrapper for the Smart Lock setup dialog. |
| 211 |
| 212 Note: This does not include the app's background page. |
| 213 """ |
| 214 |
| 215 class PairingState: |
| 216 """ The current state of the setup flow. """ |
| 217 SCAN = 'scan' |
| 218 PAIR = 'pair' |
| 219 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run' |
| 220 TRIAL_RUN_COMPLETED = 'trial_run_completed' |
| 221 |
| 222 def __init__(self, app_page, chromeos): |
| 223 """ |
| 224 Args: |
| 225 app_page: Inspector page of the app window. |
| 226 chromeos: The parent Chrome wrapper. |
| 227 """ |
| 228 self._app_page = app_page |
| 229 self._chromeos = chromeos |
| 230 |
| 231 @property |
| 232 def pairing_state(self): |
| 233 ''' Returns the state the app is currently in. |
| 234 |
| 235 Raises: |
| 236 ValueError: The current state is unknown. |
| 237 ''' |
| 238 state = self._app_page.EvaluateJavaScript( |
| 239 'document.body.getAttribute("step")') |
| 240 if state == 'scan': |
| 241 return SmartLockApp.PairingState.SCAN |
| 242 elif state == 'pair': |
| 243 return SmartLockApp.PairingState.PAIR |
| 244 elif state == 'complete': |
| 245 button_text = self._app_page.EvaluateJavaScript( |
| 246 'document.getElementById("pairing-button").textContent') |
| 247 button_text = button_text.strip().lower() |
| 248 if button_text == 'try it out': |
| 249 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN |
| 250 elif button_text == 'done': |
| 251 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED |
| 252 else: |
| 253 raise ValueError('Unknown button text: %s', button_text) |
| 254 else: |
| 255 raise ValueError('Unknown pairing state: %s' % state) |
| 256 |
| 257 def FindPhone(self, retries=3): |
| 258 """ Starts the initial step to find nearby phones. |
| 259 |
| 260 The app must be in the SCAN state. |
| 261 |
| 262 Args: |
| 263 retries: The number of times to retry if no phones are found. |
| 264 Returns: |
| 265 True if a phone is found, else False. |
| 266 """ |
| 267 assert(self.pairing_state == self.PairingState.SCAN) |
| 268 for _ in xrange(retries): |
| 269 self._ClickPairingButton() |
| 270 if self.pairing_state == self.PairingState.PAIR: |
| 271 return True |
| 272 # Wait a few seconds before retrying. |
| 273 time.sleep(10) |
| 274 return False |
| 275 |
| 276 def PairPhone(self): |
| 277 """ Starts the step of finding nearby phones. |
| 278 |
| 279 The app must be in the PAIR state. |
| 280 |
| 281 Returns: |
| 282 True if pairing succeeded, else False. |
| 283 """ |
| 284 assert(self.pairing_state == self.PairingState.PAIR) |
| 285 self._ClickPairingButton() |
| 286 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN |
| 287 |
| 288 def StartTrialRun(self): |
| 289 """ Starts the trial run. |
| 290 |
| 291 The app must be in the CLICK_FOR_TRIAL_RUN state. |
| 292 |
| 293 Raises: |
| 294 TimeoutException: Timed out starting the trial run. |
| 295 """ |
| 296 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN) |
| 297 self._app_page.EvaluateJavaScript( |
| 298 'document.getElementById("pairing-button").click()') |
| 299 util.WaitFor(lambda: (self._chromeos.session_state == |
| 300 ChromeOS.SessionState.LOCK_SCREEN), |
| 301 10) |
| 302 |
| 303 def DismissApp(self): |
| 304 """ Dismisses the app after setup is completed. |
| 305 |
| 306 The app must be in the TRIAL_RUN_COMPLETED state. |
| 307 """ |
| 308 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED) |
| 309 self._app_page.EvaluateJavaScript( |
| 310 'document.getElementById("pairing-button").click()') |
| 311 |
| 312 def _ClickPairingButton(self): |
| 313 self._app_page.EvaluateJavaScript( |
| 314 'document.getElementById("pairing-button").click()') |
| 315 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| 316 '!document.getElementById("pairing-button").disabled'), 60) |
| 317 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| 318 '!document.getElementById("pairing-button-title")' |
| 319 '.classList.contains("animated-fade-out")'), 5) |
| 320 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| 321 '!document.getElementById("pairing-button-title")' |
| 322 '.classList.contains("animated-fade-in")'), 5) |
| 323 |
| 324 |
| 325 class ChromeOS(object): |
| 326 """ Wrapper for a remote ChromeOS device. |
| 327 |
| 328 Operations performed through this wrapper are sent through the network to |
| 329 Chrome using the Chrome DevTools API. Therefore, any function may throw an |
| 330 exception if the communication to the remote device is severed. |
| 331 """ |
| 332 |
| 333 class SessionState: |
| 334 """ The state of the user session. |
| 335 """ |
| 336 SIGNIN_SCREEN = 'signin_screen' |
| 337 IN_SESSION = 'in_session' |
| 338 LOCK_SCREEN = 'lock_screen' |
| 339 |
| 340 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock' |
| 341 |
| 342 def __init__(self, remote_address, username, password, ssh_port=None): |
| 343 """ |
| 344 Args: |
| 345 remote_address: The remote address of the cros device. |
| 346 username: The username of the account to test. |
| 347 password: The password of the account to test. |
| 348 ssh_port: The ssh port to connect to. |
| 349 """ |
| 350 self._remote_address = remote_address; |
| 351 self._username = username |
| 352 self._password = password |
| 353 self._ssh_port = ssh_port |
| 354 self._browser = None |
| 355 self._cros_interface = None |
| 356 self._background_page = None |
| 357 self._processes = [] |
| 358 |
| 359 @property |
| 360 def username(self): |
| 361 ''' Returns the username of the user to login. ''' |
| 362 return self._username |
| 363 |
| 364 @property |
| 365 def password(self): |
| 366 ''' Returns the password of the user to login. ''' |
| 367 return self._password |
| 368 |
| 369 @property |
| 370 def session_state(self): |
| 371 ''' Returns the state of the user session. ''' |
| 372 assert(self._browser is not None) |
| 373 if self._browser.oobe_exists: |
| 374 if self._cros_interface.IsCryptohomeMounted(self.username, False): |
| 375 return self.SessionState.LOCK_SCREEN |
| 376 else: |
| 377 return self.SessionState.SIGNIN_SCREEN |
| 378 else: |
| 379 return self.SessionState.IN_SESSION; |
| 380 |
| 381 @property |
| 382 def cryptauth_access_token(self): |
| 383 try: |
| 384 util.WaitFor(lambda: self._background_page.EvaluateJavaScript( |
| 385 'var __token = __token || null; ' |
| 386 'chrome.identity.getAuthToken(function(token) {' |
| 387 ' __token = token;' |
| 388 '}); ' |
| 389 '__token != null'), 5) |
| 390 return self._background_page.EvaluateJavaScript('__token'); |
| 391 except exceptions.TimeoutException: |
| 392 logger.error('Failed to get access token.'); |
| 393 return '' |
| 394 |
| 395 def __enter__(self): |
| 396 return self |
| 397 |
| 398 def __exit__(self, *args): |
| 399 if self._browser is not None: |
| 400 self._browser.Close() |
| 401 if self._cros_interface is not None: |
| 402 self._cros_interface.CloseConnection() |
| 403 for process in self._processes: |
| 404 process.terminate() |
| 405 |
| 406 def Start(self, local_app_path=None): |
| 407 """ Connects to the ChromeOS device and logs in. |
| 408 Args: |
| 409 local_app_path: A path on the local device containing the Smart Lock app |
| 410 to use instead of the app on the ChromeOS device. |
| 411 Return: |
| 412 |self| for using in a "with" statement. |
| 413 """ |
| 414 assert(self._browser is None) |
| 415 |
| 416 finder_opts = browser_options.BrowserFinderOptions('cros-chrome') |
| 417 finder_opts.CreateParser().parse_args(args=[]) |
| 418 finder_opts.cros_remote = self._remote_address |
| 419 if self._ssh_port is not None: |
| 420 finder_opts.cros_remote_ssh_port = self._ssh_port |
| 421 finder_opts.verbosity = 1 |
| 422 |
| 423 browser_opts = finder_opts.browser_options |
| 424 browser_opts.create_browser_with_oobe = True |
| 425 browser_opts.disable_component_extensions_with_background_pages = False |
| 426 browser_opts.gaia_login = True |
| 427 browser_opts.username = self._username |
| 428 browser_opts.password = self._password |
| 429 browser_opts.auto_login = True |
| 430 |
| 431 self._cros_interface = cros_interface.CrOSInterface( |
| 432 finder_opts.cros_remote, |
| 433 finder_opts.cros_remote_ssh_port, |
| 434 finder_opts.cros_ssh_identity) |
| 435 |
| 436 browser_opts.disable_default_apps = local_app_path is not None |
| 437 if local_app_path is not None: |
| 438 easy_unlock_app = extension_to_load.ExtensionToLoad( |
| 439 path=local_app_path, |
| 440 browser_type='cros-chrome', |
| 441 is_component=True) |
| 442 finder_opts.extensions_to_load.append(easy_unlock_app) |
| 443 |
| 444 retries = 3 |
| 445 while self._browser is not None or retries > 0: |
| 446 try: |
| 447 browser_to_create = browser_finder.FindBrowser(finder_opts) |
| 448 self._browser = browser_to_create.Create(finder_opts); |
| 449 break; |
| 450 except (exceptions.LoginException) as e: |
| 451 logger.error('Timed out logging in: %s' % e); |
| 452 if retries == 1: |
| 453 raise |
| 454 |
| 455 bg_page_path = '/_generated_background_page.html' |
| 456 util.WaitFor( |
| 457 lambda: self._FindSmartLockAppPage(bg_page_path) is not None, |
| 458 10); |
| 459 self._background_page = self._FindSmartLockAppPage(bg_page_path) |
| 460 return self |
| 461 |
| 462 def GetAccountPickerScreen(self): |
| 463 """ Returns the wrapper for the lock screen or sign-in screen. |
| 464 |
| 465 Return: |
| 466 An instance of AccountPickerScreen. |
| 467 Raises: |
| 468 TimeoutException: Timed out waiting for account picker screen to load. |
| 469 """ |
| 470 assert(self._browser is not None) |
| 471 assert(self.session_state == self.SessionState.LOCK_SCREEN or |
| 472 self.session_state == self.SessionState.SIGNIN_SCREEN) |
| 473 oobe = self._browser.oobe |
| 474 def IsLockScreenResponsive(): |
| 475 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and |
| 476 oobe.EvaluateJavaScript( |
| 477 "typeof Oobe.authenticateForTesting == 'function'")) |
| 478 util.WaitFor(IsLockScreenResponsive, 10) |
| 479 util.WaitFor(lambda: oobe.EvaluateJavaScript( |
| 480 'document.getElementById("pod-row") && ' |
| 481 'document.getElementById("pod-row").pods && ' |
| 482 'document.getElementById("pod-row").pods.length > 0'), 10) |
| 483 return AccountPickerScreen(oobe, self) |
| 484 |
| 485 def GetSmartLockSettings(self): |
| 486 """ Returns the wrapper for the Smart Lock settings. |
| 487 A tab will be navigated to chrome://settings if it does not exist. |
| 488 |
| 489 Return: |
| 490 An instance of SmartLockSettings. |
| 491 Raises: |
| 492 TimeoutException: Timed out waiting for settings page. |
| 493 """ |
| 494 if not len(self._browser.tabs): |
| 495 self._browser.New() |
| 496 tab = self._browser.tabs[0] |
| 497 url = tab.EvaluateJavaScript('document.location.href') |
| 498 if url != self._SMART_LOCK_SETTINGS_URL: |
| 499 tab.Navigate(self._SMART_LOCK_SETTINGS_URL) |
| 500 |
| 501 # Wait for settings page to be responsive. |
| 502 util.WaitFor(lambda: tab.EvaluateJavaScript( |
| 503 'document.getElementById("easy-unlock-disabled") && ' |
| 504 'document.getElementById("easy-unlock-enabled") && ' |
| 505 '(!document.getElementById("easy-unlock-disabled").hidden || ' |
| 506 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10) |
| 507 settings = SmartLockSettings(tab, self) |
| 508 logger.info('Started Smart Lock settings: enabled=%s' % |
| 509 settings.is_smart_lock_enabled) |
| 510 return settings |
| 511 |
| 512 def GetSmartLockApp(self): |
| 513 """ Returns the wrapper for the Smart Lock setup app. |
| 514 |
| 515 Return: |
| 516 An instance of SmartLockApp or None if the app window does not exist. |
| 517 """ |
| 518 app_page = self._FindSmartLockAppPage('/pairing.html') |
| 519 if app_page is not None: |
| 520 # Wait for app window to be responsive. |
| 521 util.WaitFor(lambda: app_page.EvaluateJavaScript( |
| 522 'document.getElementById("pairing-button") != null'), 10) |
| 523 return SmartLockApp(app_page, self) |
| 524 return None |
| 525 |
| 526 def RunBtmon(self): |
| 527 """ Runs the btmon command. |
| 528 Return: |
| 529 A subprocess.Popen object of the btmon process. |
| 530 """ |
| 531 assert(self._cros_interface) |
| 532 cmd = self._cros_interface.FormSSHCommandLine(['btmon']) |
| 533 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, |
| 534 stderr=subprocess.PIPE) |
| 535 self._processes.append(process) |
| 536 return process |
| 537 |
| 538 def _FindSmartLockAppPage(self, page_name): |
| 539 try: |
| 540 extensions = self._browser.extensions.GetByExtensionId( |
| 541 'mkaemigholebcgchlkbankmihknojeak') |
| 542 except KeyError: |
| 543 return None |
| 544 for extension_page in extensions: |
| 545 pathname = extension_page.EvaluateJavaScript('document.location.pathname') |
| 546 if pathname == page_name: |
| 547 return extension_page |
| 548 return None |
OLD | NEW |