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.
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
)
18 from telemetry
.internal
.browser
import browser_options
19 from telemetry
.internal
.browser
import browser_finder
20 from telemetry
.core
import exceptions
21 from telemetry
.core
import util
22 from telemetry
.core
import cros_interface
23 from telemetry
.internal
.browser
import extension_to_load
25 logger
= logging
.getLogger('proximity_auth.%s' % __name__
)
27 class AccountPickerScreen(object):
28 """ Wrapper for the ChromeOS account picker screen.
30 The account picker screen is the WebContents page used for both the lock
31 screen and signin screen.
33 Note: This class assumes the account picker screen only has one user. If there
34 are multiple user pods, the first one will be used.
38 """ The authentication type expected for a user pod. """
43 EXPAND_THEN_USER_CLICK
= 4
44 FORCE_OFFLINE_PASSWORD
= 5
47 """ The state of the Smart Lock icon on a user pod.
49 NOT_SHOWN
= 'not_shown'
50 AUTHENTICATED
= 'authenticated'
52 HARD_LOCKED
= 'hardlocked'
53 TO_BE_ACTIVATED
= 'to_be_activated'
56 # JavaScript expression for getting the user pod on the page
57 _GET_POD_JS
= 'document.getElementById("pod-row").pods[0]'
59 def __init__(self
, oobe
, chromeos
):
62 oobe: Inspector page of the OOBE WebContents.
63 chromeos: The parent Chrome wrapper.
66 self
._chromeos
= chromeos
69 def is_lockscreen(self
):
70 return self
._oobe
.EvaluateJavaScript(
71 '!document.getElementById("sign-out-user-item").hidden')
75 return self
._oobe
.EvaluateJavaScript('%s.authType' % self
._GET
_POD
_JS
)
78 def smart_lock_state(self
):
79 icon_shown
= self
._oobe
.EvaluateJavaScript(
80 '!%s.customIconElement.hidden' % self
._GET
_POD
_JS
)
82 return self
.SmartLockState
.NOT_SHOWN
83 class_list_dict
= self
._oobe
.EvaluateJavaScript(
84 '%s.customIconElement.querySelector(".custom-icon")'
85 '.classList' % self
._GET
_POD
_JS
)
86 class_list
= [v
for k
,v
in class_list_dict
.items() if k
!= 'length']
88 if 'custom-icon-unlocked' in class_list
:
89 return self
.SmartLockState
.AUTHENTICATED
90 if 'custom-icon-locked' in class_list
:
91 return self
.SmartLockState
.LOCKED
92 if 'custom-icon-hardlocked' in class_list
:
93 return self
.SmartLockState
.HARD_LOCKED
94 if 'custom-icon-locked-to-be-activated' in class_list
:
95 return self
.SmartLockState
.TO_BE_ACTIVATED
96 if 'custom-icon-spinner' in class_list
:
97 return self
.SmartLockState
.SPINNER
99 def WaitForSmartLockState(self
, state
, wait_time_secs
=60):
100 """ Waits for the Smart Lock icon to reach the given state.
103 state: A value in AccountPickerScreen.SmartLockState
104 wait_time_secs: The time to wait
106 True if the state is reached within the wait time, else False.
109 util
.WaitFor(lambda: self
.smart_lock_state
== state
, wait_time_secs
)
111 except exceptions
.TimeoutException
:
114 def EnterPassword(self
):
115 """ Enters the password to unlock or sign-in.
118 TimeoutException: entering the password fails to enter/resume the user
121 assert(self
.auth_type
== self
.AuthType
.OFFLINE_PASSWORD
or
122 self
.auth_type
== self
.AuthType
.FORCE_OFFLINE_PASSWORD
)
124 oobe
.EvaluateJavaScript(
125 '%s.passwordElement.value = "%s"' % (
126 self
._GET
_POD
_JS
, self
._chromeos
.password
))
127 oobe
.EvaluateJavaScript(
128 '%s.activate()' % self
._GET
_POD
_JS
)
129 util
.WaitFor(lambda: (self
._chromeos
.session_state
==
130 ChromeOS
.SessionState
.IN_SESSION
),
133 def UnlockWithClick(self
):
134 """ Clicks the user pod to unlock or sign-in. """
135 assert(self
.auth_type
== self
.AuthType
.USER_CLICK
)
136 self
._oobe
.EvaluateJavaScript('%s.activate()' % self
._GET
_POD
_JS
)
139 class SmartLockSettings(object):
140 """ Wrapper for the Smart Lock settings in chromeos://settings.
142 def __init__(self
, tab
, chromeos
):
145 tab: Inspector page of the chromeos://settings tag.
146 chromeos: The parent Chrome wrapper.
149 self
._chromeos
= chromeos
152 def is_smart_lock_enabled(self
):
153 ''' Returns true if the settings show that Smart Lock is enabled. '''
154 return self
._tab
.EvaluateJavaScript(
155 '!document.getElementById("easy-unlock-enabled").hidden')
157 def TurnOffSmartLock(self
):
158 """ Turns off Smart Lock.
160 Smart Lock is turned off by clicking the turn-off button and navigating
161 through the resulting overlay.
164 TimeoutException: Timed out waiting for Smart Lock to be turned off.
166 assert(self
.is_smart_lock_enabled
)
168 tab
.EvaluateJavaScript(
169 'document.getElementById("easy-unlock-turn-off-button").click()')
170 util
.WaitFor(lambda: tab
.EvaluateJavaScript(
171 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && '
172 'document.getElementById("easy-unlock-turn-off-confirm") != null'),
174 tab
.EvaluateJavaScript(
175 'document.getElementById("easy-unlock-turn-off-confirm").click()')
176 util
.WaitFor(lambda: tab
.EvaluateJavaScript(
177 '!document.getElementById("easy-unlock-disabled").hidden'), 15)
179 def StartSetup(self
):
180 """ Starts the Smart Lock setup flow by clicking the button.
182 assert(not self
.is_smart_lock_enabled
)
183 self
._tab
.EvaluateJavaScript(
184 'document.getElementById("easy-unlock-setup-button").click()')
186 def StartSetupAndReturnApp(self
):
187 """ Runs the setup and returns the wrapper to the setup app.
189 After clicking the setup button in the settings page, enter the password to
190 reauthenticate the user before the app launches.
193 A SmartLockApp object of the app that was launched.
196 TimeoutException: Timed out waiting for app.
199 util
.WaitFor(lambda: (self
._chromeos
.session_state
==
200 ChromeOS
.SessionState
.LOCK_SCREEN
),
202 lock_screen
= self
._chromeos
.GetAccountPickerScreen()
203 lock_screen
.EnterPassword()
204 util
.WaitFor(lambda: self
._chromeos
.GetSmartLockApp() is not None, 10)
205 return self
._chromeos
.GetSmartLockApp()
208 class SmartLockApp(object):
209 """ Wrapper for the Smart Lock setup dialog.
211 Note: This does not include the app's background page.
215 """ The current state of the setup flow. """
218 CLICK_FOR_TRIAL_RUN
= 'click_for_trial_run'
219 TRIAL_RUN_COMPLETED
= 'trial_run_completed'
220 PROMOTE_SMARTLOCK_FOR_ANDROID
= 'promote-smart-lock-for-android'
222 def __init__(self
, app_page
, chromeos
):
225 app_page: Inspector page of the app window.
226 chromeos: The parent Chrome wrapper.
228 self
._app
_page
= app_page
229 self
._chromeos
= chromeos
232 def pairing_state(self
):
233 ''' Returns the state the app is currently in.
236 ValueError: The current state is unknown.
238 state
= self
._app
_page
.EvaluateJavaScript(
239 'document.body.getAttribute("step")')
241 return SmartLockApp
.PairingState
.SCAN
242 elif state
== 'pair':
243 return SmartLockApp
.PairingState
.PAIR
244 elif state
== 'promote-smart-lock-for-android':
245 return SmartLockApp
.PairingState
.PROMOTE_SMARTLOCK_FOR_ANDROID
246 elif state
== 'complete':
247 button_text
= self
._app
_page
.EvaluateJavaScript(
248 'document.getElementById("pairing-button").textContent')
249 button_text
= button_text
.strip().lower()
250 if button_text
== 'try it out':
251 return SmartLockApp
.PairingState
.CLICK_FOR_TRIAL_RUN
252 elif button_text
== 'done':
253 return SmartLockApp
.PairingState
.TRIAL_RUN_COMPLETED
255 raise ValueError('Unknown button text: %s', button_text
)
257 raise ValueError('Unknown pairing state: %s' % state
)
259 def FindPhone(self
, retries
=3):
260 """ Starts the initial step to find nearby phones.
262 The app must be in the SCAN state.
265 retries: The number of times to retry if no phones are found.
267 True if a phone is found, else False.
269 assert(self
.pairing_state
== self
.PairingState
.SCAN
)
270 for _
in xrange(retries
):
271 self
._ClickPairingButton
()
272 if self
.pairing_state
== self
.PairingState
.PAIR
:
274 # Wait a few seconds before retrying.
279 """ Starts the step of finding nearby phones.
281 The app must be in the PAIR state.
284 True if pairing succeeded, else False.
286 assert(self
.pairing_state
== self
.PairingState
.PAIR
)
287 self
._ClickPairingButton
()
288 if self
.pairing_state
== self
.PairingState
.PROMOTE_SMARTLOCK_FOR_ANDROID
:
289 self
._ClickPairingButton
()
290 return self
.pairing_state
== self
.PairingState
.CLICK_FOR_TRIAL_RUN
292 def StartTrialRun(self
):
293 """ Starts the trial run.
295 The app must be in the CLICK_FOR_TRIAL_RUN state.
298 TimeoutException: Timed out starting the trial run.
300 assert(self
.pairing_state
== self
.PairingState
.CLICK_FOR_TRIAL_RUN
)
301 self
._app
_page
.EvaluateJavaScript(
302 'document.getElementById("pairing-button").click()')
303 util
.WaitFor(lambda: (self
._chromeos
.session_state
==
304 ChromeOS
.SessionState
.LOCK_SCREEN
),
307 def DismissApp(self
):
308 """ Dismisses the app after setup is completed.
310 The app must be in the TRIAL_RUN_COMPLETED state.
312 assert(self
.pairing_state
== self
.PairingState
.TRIAL_RUN_COMPLETED
)
313 self
._app
_page
.EvaluateJavaScript(
314 'document.getElementById("pairing-button").click()')
316 def _ClickPairingButton(self
):
317 # Waits are needed because the clicks occur before the button label changes.
319 self
._app
_page
.EvaluateJavaScript(
320 'document.getElementById("pairing-button").click()')
322 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
323 '!document.getElementById("pairing-button").disabled'), 60)
325 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
326 '!document.getElementById("pairing-button-title")'
327 '.classList.contains("animated-fade-out")'), 5)
328 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
329 '!document.getElementById("pairing-button-title")'
330 '.classList.contains("animated-fade-in")'), 5)
333 class ChromeOS(object):
334 """ Wrapper for a remote ChromeOS device.
336 Operations performed through this wrapper are sent through the network to
337 Chrome using the Chrome DevTools API. Therefore, any function may throw an
338 exception if the communication to the remote device is severed.
342 """ The state of the user session.
344 SIGNIN_SCREEN
= 'signin_screen'
345 IN_SESSION
= 'in_session'
346 LOCK_SCREEN
= 'lock_screen'
348 _SMART_LOCK_SETTINGS_URL
= 'chrome://settings/search#Smart%20Lock'
350 def __init__(self
, remote_address
, username
, password
, ssh_port
=None):
353 remote_address: The remote address of the cros device.
354 username: The username of the account to test.
355 password: The password of the account to test.
356 ssh_port: The ssh port to connect to.
358 self
._remote
_address
= remote_address
359 self
._username
= username
360 self
._password
= password
361 self
._ssh
_port
= ssh_port
363 self
._cros
_interface
= None
364 self
._background
_page
= None
369 ''' Returns the username of the user to login. '''
370 return self
._username
374 ''' Returns the password of the user to login. '''
375 return self
._password
378 def session_state(self
):
379 ''' Returns the state of the user session. '''
380 assert(self
._browser
is not None)
381 if self
._browser
.oobe_exists
:
382 if self
._cros
_interface
.IsCryptohomeMounted(self
.username
, False):
383 return self
.SessionState
.LOCK_SCREEN
385 return self
.SessionState
.SIGNIN_SCREEN
387 return self
.SessionState
.IN_SESSION
;
390 def cryptauth_access_token(self
):
392 util
.WaitFor(lambda: self
._background
_page
.EvaluateJavaScript(
393 'var __token = __token || null; '
394 'chrome.identity.getAuthToken(function(token) {'
397 '__token != null'), 5)
398 return self
._background
_page
.EvaluateJavaScript('__token');
399 except exceptions
.TimeoutException
:
400 logger
.error('Failed to get access token.');
406 def __exit__(self
, *args
):
407 if self
._browser
is not None:
408 self
._browser
.Close()
409 if self
._cros
_interface
is not None:
410 self
._cros
_interface
.CloseConnection()
411 for process
in self
._processes
:
414 def Start(self
, local_app_path
=None):
415 """ Connects to the ChromeOS device and logs in.
417 local_app_path: A path on the local device containing the Smart Lock app
418 to use instead of the app on the ChromeOS device.
420 |self| for using in a "with" statement.
422 assert(self
._browser
is None)
424 finder_opts
= browser_options
.BrowserFinderOptions('cros-chrome')
425 finder_opts
.CreateParser().parse_args(args
=[])
426 finder_opts
.cros_remote
= self
._remote
_address
427 if self
._ssh
_port
is not None:
428 finder_opts
.cros_remote_ssh_port
= self
._ssh
_port
429 finder_opts
.verbosity
= 1
431 browser_opts
= finder_opts
.browser_options
432 browser_opts
.create_browser_with_oobe
= True
433 browser_opts
.disable_component_extensions_with_background_pages
= False
434 browser_opts
.gaia_login
= True
435 browser_opts
.username
= self
._username
436 browser_opts
.password
= self
._password
437 browser_opts
.auto_login
= True
439 self
._cros
_interface
= cros_interface
.CrOSInterface(
440 finder_opts
.cros_remote
,
441 finder_opts
.cros_remote_ssh_port
,
442 finder_opts
.cros_ssh_identity
)
444 browser_opts
.disable_default_apps
= local_app_path
is not None
445 if local_app_path
is not None:
446 easy_unlock_app
= extension_to_load
.ExtensionToLoad(
448 browser_type
='cros-chrome',
450 finder_opts
.extensions_to_load
.append(easy_unlock_app
)
453 while self
._browser
is not None or retries
> 0:
455 browser_to_create
= browser_finder
.FindBrowser(finder_opts
)
456 self
._browser
= browser_to_create
.Create(finder_opts
);
458 except (exceptions
.LoginException
) as e
:
459 logger
.error('Timed out logging in: %s' % e
);
463 bg_page_path
= '/_generated_background_page.html'
465 lambda: self
._FindSmartLockAppPage
(bg_page_path
) is not None,
467 self
._background
_page
= self
._FindSmartLockAppPage
(bg_page_path
)
470 def GetAccountPickerScreen(self
):
471 """ Returns the wrapper for the lock screen or sign-in screen.
474 An instance of AccountPickerScreen.
476 TimeoutException: Timed out waiting for account picker screen to load.
478 assert(self
._browser
is not None)
479 assert(self
.session_state
== self
.SessionState
.LOCK_SCREEN
or
480 self
.session_state
== self
.SessionState
.SIGNIN_SCREEN
)
481 oobe
= self
._browser
.oobe
482 def IsLockScreenResponsive():
483 return (oobe
.EvaluateJavaScript("typeof Oobe == 'function'") and
484 oobe
.EvaluateJavaScript(
485 "typeof Oobe.authenticateForTesting == 'function'"))
486 util
.WaitFor(IsLockScreenResponsive
, 10)
487 util
.WaitFor(lambda: oobe
.EvaluateJavaScript(
488 'document.getElementById("pod-row") && '
489 'document.getElementById("pod-row").pods && '
490 'document.getElementById("pod-row").pods.length > 0'), 10)
491 return AccountPickerScreen(oobe
, self
)
493 def GetSmartLockSettings(self
):
494 """ Returns the wrapper for the Smart Lock settings.
495 A tab will be navigated to chrome://settings if it does not exist.
498 An instance of SmartLockSettings.
500 TimeoutException: Timed out waiting for settings page.
502 if not len(self
._browser
.tabs
):
504 tab
= self
._browser
.tabs
[0]
505 url
= tab
.EvaluateJavaScript('document.location.href')
506 if url
!= self
._SMART
_LOCK
_SETTINGS
_URL
:
507 tab
.Navigate(self
._SMART
_LOCK
_SETTINGS
_URL
)
509 # Wait for settings page to be responsive.
510 util
.WaitFor(lambda: tab
.EvaluateJavaScript(
511 'document.getElementById("easy-unlock-disabled") && '
512 'document.getElementById("easy-unlock-enabled") && '
513 '(!document.getElementById("easy-unlock-disabled").hidden || '
514 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
515 settings
= SmartLockSettings(tab
, self
)
516 logger
.info('Started Smart Lock settings: enabled=%s' %
517 settings
.is_smart_lock_enabled
)
520 def GetSmartLockApp(self
):
521 """ Returns the wrapper for the Smart Lock setup app.
524 An instance of SmartLockApp or None if the app window does not exist.
526 app_page
= self
._FindSmartLockAppPage
('/pairing.html')
527 if app_page
is not None:
528 # Wait for app window to be responsive.
529 util
.WaitFor(lambda: app_page
.EvaluateJavaScript(
530 'document.getElementById("pairing-button") != null'), 10)
531 return SmartLockApp(app_page
, self
)
534 def SetCryptAuthStaging(self
, cryptauth_staging_url
):
535 logger
.info('Setting CryptAuth to Staging')
537 self
._background
_page
.ExecuteJavaScript(
538 'var key = app.CryptAuthClient.GOOGLE_API_URL_OVERRIDE_;'
539 'var __complete = false;'
540 'chrome.storage.local.set({key: "%s"}, function() {'
541 ' __complete = true;'
542 '});' % cryptauth_staging_url
)
543 util
.WaitFor(lambda: self
._background
_page
.EvaluateJavaScript(
544 '__complete == true'), 10)
545 except exceptions
.TimeoutException
:
546 logger
.error('Failed to override CryptAuth to staging url.')
549 """ Runs the btmon command.
551 A subprocess.Popen object of the btmon process.
553 assert(self
._cros
_interface
)
554 cmd
= self
._cros
_interface
.FormSSHCommandLine(['btmon'])
555 process
= subprocess
.Popen(args
=cmd
, stdout
=subprocess
.PIPE
,
556 stderr
=subprocess
.PIPE
)
557 self
._processes
.append(process
)
560 def _FindSmartLockAppPage(self
, page_name
):
562 extensions
= self
._browser
.extensions
.GetByExtensionId(
563 'mkaemigholebcgchlkbankmihknojeak')
566 for extension_page
in extensions
:
567 pathname
= extension_page
.EvaluateJavaScript('document.location.pathname')
568 if pathname
== page_name
:
569 return extension_page