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
.core
import browser_options
19 from telemetry
.core
import browser_finder
20 from telemetry
.core
import exceptions
21 from telemetry
.core
import util
22 from telemetry
.core
.platform
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'
221 def __init__(self
, app_page
, chromeos
):
224 app_page: Inspector page of the app window.
225 chromeos: The parent Chrome wrapper.
227 self
._app
_page
= app_page
228 self
._chromeos
= chromeos
231 def pairing_state(self
):
232 ''' Returns the state the app is currently in.
235 ValueError: The current state is unknown.
237 state
= self
._app
_page
.EvaluateJavaScript(
238 'document.body.getAttribute("step")')
240 return SmartLockApp
.PairingState
.SCAN
241 elif state
== 'pair':
242 return SmartLockApp
.PairingState
.PAIR
243 elif state
== 'complete':
244 button_text
= self
._app
_page
.EvaluateJavaScript(
245 'document.getElementById("pairing-button").textContent')
246 button_text
= button_text
.strip().lower()
247 if button_text
== 'try it out':
248 return SmartLockApp
.PairingState
.CLICK_FOR_TRIAL_RUN
249 elif button_text
== 'done':
250 return SmartLockApp
.PairingState
.TRIAL_RUN_COMPLETED
252 raise ValueError('Unknown button text: %s', button_text
)
254 raise ValueError('Unknown pairing state: %s' % state
)
256 def FindPhone(self
, retries
=3):
257 """ Starts the initial step to find nearby phones.
259 The app must be in the SCAN state.
262 retries: The number of times to retry if no phones are found.
264 True if a phone is found, else False.
266 assert(self
.pairing_state
== self
.PairingState
.SCAN
)
267 for _
in xrange(retries
):
268 self
._ClickPairingButton
()
269 if self
.pairing_state
== self
.PairingState
.PAIR
:
271 # Wait a few seconds before retrying.
276 """ Starts the step of finding nearby phones.
278 The app must be in the PAIR state.
281 True if pairing succeeded, else False.
283 assert(self
.pairing_state
== self
.PairingState
.PAIR
)
284 self
._ClickPairingButton
()
285 return self
.pairing_state
== self
.PairingState
.CLICK_FOR_TRIAL_RUN
287 def StartTrialRun(self
):
288 """ Starts the trial run.
290 The app must be in the CLICK_FOR_TRIAL_RUN state.
293 TimeoutException: Timed out starting the trial run.
295 assert(self
.pairing_state
== self
.PairingState
.CLICK_FOR_TRIAL_RUN
)
296 self
._app
_page
.EvaluateJavaScript(
297 'document.getElementById("pairing-button").click()')
298 util
.WaitFor(lambda: (self
._chromeos
.session_state
==
299 ChromeOS
.SessionState
.LOCK_SCREEN
),
302 def DismissApp(self
):
303 """ Dismisses the app after setup is completed.
305 The app must be in the TRIAL_RUN_COMPLETED state.
307 assert(self
.pairing_state
== self
.PairingState
.TRIAL_RUN_COMPLETED
)
308 self
._app
_page
.EvaluateJavaScript(
309 'document.getElementById("pairing-button").click()')
311 def _ClickPairingButton(self
):
312 self
._app
_page
.EvaluateJavaScript(
313 'document.getElementById("pairing-button").click()')
314 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
315 '!document.getElementById("pairing-button").disabled'), 60)
316 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
317 '!document.getElementById("pairing-button-title")'
318 '.classList.contains("animated-fade-out")'), 5)
319 util
.WaitFor(lambda: self
._app
_page
.EvaluateJavaScript(
320 '!document.getElementById("pairing-button-title")'
321 '.classList.contains("animated-fade-in")'), 5)
324 class ChromeOS(object):
325 """ Wrapper for a remote ChromeOS device.
327 Operations performed through this wrapper are sent through the network to
328 Chrome using the Chrome DevTools API. Therefore, any function may throw an
329 exception if the communication to the remote device is severed.
333 """ The state of the user session.
335 SIGNIN_SCREEN
= 'signin_screen'
336 IN_SESSION
= 'in_session'
337 LOCK_SCREEN
= 'lock_screen'
339 _SMART_LOCK_SETTINGS_URL
= 'chrome://settings/search#Smart%20Lock'
341 def __init__(self
, remote_address
, username
, password
, ssh_port
=None):
344 remote_address: The remote address of the cros device.
345 username: The username of the account to test.
346 password: The password of the account to test.
347 ssh_port: The ssh port to connect to.
349 self
._remote
_address
= remote_address
350 self
._username
= username
351 self
._password
= password
352 self
._ssh
_port
= ssh_port
354 self
._cros
_interface
= None
355 self
._background
_page
= None
360 ''' Returns the username of the user to login. '''
361 return self
._username
365 ''' Returns the password of the user to login. '''
366 return self
._password
369 def session_state(self
):
370 ''' Returns the state of the user session. '''
371 assert(self
._browser
is not None)
372 if self
._browser
.oobe_exists
:
373 if self
._cros
_interface
.IsCryptohomeMounted(self
.username
, False):
374 return self
.SessionState
.LOCK_SCREEN
376 return self
.SessionState
.SIGNIN_SCREEN
378 return self
.SessionState
.IN_SESSION
;
381 def cryptauth_access_token(self
):
383 util
.WaitFor(lambda: self
._background
_page
.EvaluateJavaScript(
384 'var __token = __token || null; '
385 'chrome.identity.getAuthToken(function(token) {'
388 '__token != null'), 5)
389 return self
._background
_page
.EvaluateJavaScript('__token');
390 except exceptions
.TimeoutException
:
391 logger
.error('Failed to get access token.');
397 def __exit__(self
, *args
):
398 if self
._browser
is not None:
399 self
._browser
.Close()
400 if self
._cros
_interface
is not None:
401 self
._cros
_interface
.CloseConnection()
402 for process
in self
._processes
:
405 def Start(self
, local_app_path
=None):
406 """ Connects to the ChromeOS device and logs in.
408 local_app_path: A path on the local device containing the Smart Lock app
409 to use instead of the app on the ChromeOS device.
411 |self| for using in a "with" statement.
413 assert(self
._browser
is None)
415 finder_opts
= browser_options
.BrowserFinderOptions('cros-chrome')
416 finder_opts
.CreateParser().parse_args(args
=[])
417 finder_opts
.cros_remote
= self
._remote
_address
418 if self
._ssh
_port
is not None:
419 finder_opts
.cros_remote_ssh_port
= self
._ssh
_port
420 finder_opts
.verbosity
= 1
422 browser_opts
= finder_opts
.browser_options
423 browser_opts
.create_browser_with_oobe
= True
424 browser_opts
.disable_component_extensions_with_background_pages
= False
425 browser_opts
.gaia_login
= True
426 browser_opts
.username
= self
._username
427 browser_opts
.password
= self
._password
428 browser_opts
.auto_login
= True
430 self
._cros
_interface
= cros_interface
.CrOSInterface(
431 finder_opts
.cros_remote
,
432 finder_opts
.cros_remote_ssh_port
,
433 finder_opts
.cros_ssh_identity
)
435 browser_opts
.disable_default_apps
= local_app_path
is not None
436 if local_app_path
is not None:
437 easy_unlock_app
= extension_to_load
.ExtensionToLoad(
439 browser_type
='cros-chrome',
441 finder_opts
.extensions_to_load
.append(easy_unlock_app
)
444 while self
._browser
is not None or retries
> 0:
446 browser_to_create
= browser_finder
.FindBrowser(finder_opts
)
447 self
._browser
= browser_to_create
.Create(finder_opts
);
449 except (exceptions
.LoginException
) as e
:
450 logger
.error('Timed out logging in: %s' % e
);
454 bg_page_path
= '/_generated_background_page.html'
456 lambda: self
._FindSmartLockAppPage
(bg_page_path
) is not None,
458 self
._background
_page
= self
._FindSmartLockAppPage
(bg_page_path
)
461 def GetAccountPickerScreen(self
):
462 """ Returns the wrapper for the lock screen or sign-in screen.
465 An instance of AccountPickerScreen.
467 TimeoutException: Timed out waiting for account picker screen to load.
469 assert(self
._browser
is not None)
470 assert(self
.session_state
== self
.SessionState
.LOCK_SCREEN
or
471 self
.session_state
== self
.SessionState
.SIGNIN_SCREEN
)
472 oobe
= self
._browser
.oobe
473 def IsLockScreenResponsive():
474 return (oobe
.EvaluateJavaScript("typeof Oobe == 'function'") and
475 oobe
.EvaluateJavaScript(
476 "typeof Oobe.authenticateForTesting == 'function'"))
477 util
.WaitFor(IsLockScreenResponsive
, 10)
478 util
.WaitFor(lambda: oobe
.EvaluateJavaScript(
479 'document.getElementById("pod-row") && '
480 'document.getElementById("pod-row").pods && '
481 'document.getElementById("pod-row").pods.length > 0'), 10)
482 return AccountPickerScreen(oobe
, self
)
484 def GetSmartLockSettings(self
):
485 """ Returns the wrapper for the Smart Lock settings.
486 A tab will be navigated to chrome://settings if it does not exist.
489 An instance of SmartLockSettings.
491 TimeoutException: Timed out waiting for settings page.
493 if not len(self
._browser
.tabs
):
495 tab
= self
._browser
.tabs
[0]
496 url
= tab
.EvaluateJavaScript('document.location.href')
497 if url
!= self
._SMART
_LOCK
_SETTINGS
_URL
:
498 tab
.Navigate(self
._SMART
_LOCK
_SETTINGS
_URL
)
500 # Wait for settings page to be responsive.
501 util
.WaitFor(lambda: tab
.EvaluateJavaScript(
502 'document.getElementById("easy-unlock-disabled") && '
503 'document.getElementById("easy-unlock-enabled") && '
504 '(!document.getElementById("easy-unlock-disabled").hidden || '
505 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
506 settings
= SmartLockSettings(tab
, self
)
507 logger
.info('Started Smart Lock settings: enabled=%s' %
508 settings
.is_smart_lock_enabled
)
511 def GetSmartLockApp(self
):
512 """ Returns the wrapper for the Smart Lock setup app.
515 An instance of SmartLockApp or None if the app window does not exist.
517 app_page
= self
._FindSmartLockAppPage
('/pairing.html')
518 if app_page
is not None:
519 # Wait for app window to be responsive.
520 util
.WaitFor(lambda: app_page
.EvaluateJavaScript(
521 'document.getElementById("pairing-button") != null'), 10)
522 return SmartLockApp(app_page
, self
)
525 def SetCryptAuthStaging(self
, cryptauth_staging_url
):
526 logger
.info('Setting CryptAuth to Staging')
528 self
._background
_page
.ExecuteJavaScript(
529 'var key = app.CryptAuthClient.GOOGLE_API_URL_OVERRIDE_;'
530 'var __complete = false;'
531 'chrome.storage.local.set({key: "%s"}, function() {'
532 ' __complete = true;'
533 '});' % cryptauth_staging_url
)
534 util
.WaitFor(lambda: self
._background
_page
.EvaluateJavaScript(
535 '__complete == true'), 10)
536 except exceptions
.TimeoutException
:
537 logger
.error('Failed to override CryptAuth to staging url.')
540 """ Runs the btmon command.
542 A subprocess.Popen object of the btmon process.
544 assert(self
._cros
_interface
)
545 cmd
= self
._cros
_interface
.FormSSHCommandLine(['btmon'])
546 process
= subprocess
.Popen(args
=cmd
, stdout
=subprocess
.PIPE
,
547 stderr
=subprocess
.PIPE
)
548 self
._processes
.append(process
)
551 def _FindSmartLockAppPage(self
, page_name
):
553 extensions
= self
._browser
.extensions
.GetByExtensionId(
554 'mkaemigholebcgchlkbankmihknojeak')
557 for extension_page
in extensions
:
558 pathname
= extension_page
.EvaluateJavaScript('document.location.pathname')
559 if pathname
== page_name
:
560 return extension_page