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 extension_to_load
21 from telemetry
.core
import exceptions
22 from telemetry
.core
import util
23 from telemetry
.core
.platform
import cros_interface
25 logger
= logging
.getLogger('proximity_auth.%s' % __name__
)
28 class AccountPickerScreen(object):
29 """ Wrapper for the ChromeOS account picker screen.
31 The account picker screen is the WebContents page used for both the lock
32 screen and signin screen.
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.
39 """ The authentication type expected for a user pod. """
44 EXPAND_THEN_USER_CLICK
= 4
45 FORCE_OFFLINE_PASSWORD
= 5
48 """ The state of the Smart Lock icon on a user pod.
50 NOT_SHOWN
= 'not_shown'
51 AUTHENTICATED
= 'authenticated'
53 HARD_LOCKED
= 'hardlocked'
54 TO_BE_ACTIVATED
= 'to_be_activated'
57 # JavaScript expression for getting the user pod on the page
58 _GET_POD_JS
= 'document.getElementById("pod-row").pods[0]'
60 def __init__(self
, oobe
, chromeos
):
63 oobe: Inspector page of the OOBE WebContents.
64 chromeos: The parent Chrome wrapper.
67 self
._chromeos
= chromeos
70 def is_lockscreen(self
):
71 return self
._oobe
.EvaluateJavaScript(
72 '!document.getElementById("sign-out-user-item").hidden')
76 return self
._oobe
.EvaluateJavaScript('%s.authType' % self
._GET
_POD
_JS
)
79 def smart_lock_state(self
):
80 icon_shown
= self
._oobe
.EvaluateJavaScript(
81 '!%s.customIconElement.hidden' % self
._GET
_POD
_JS
)
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']
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
100 def WaitForSmartLockState(self
, state
, wait_time_secs
=60):
101 """ Waits for the Smart Lock icon to reach the given state.
104 state: A value in AccountPickerScreen.SmartLockState
105 wait_time_secs: The time to wait
107 True if the state is reached within the wait time, else False.
110 util
.WaitFor(lambda: self
.smart_lock_state
== state
, wait_time_secs
)
112 except exceptions
.TimeoutException
:
115 def EnterPassword(self
):
116 """ Enters the password to unlock or sign-in.
119 TimeoutException: entering the password fails to enter/resume the user
122 assert(self
.auth_type
== self
.AuthType
.OFFLINE_PASSWORD
or
123 self
.auth_type
== self
.AuthType
.FORCE_OFFLINE_PASSWORD
)
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
),
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
)
140 class SmartLockSettings(object):
141 """ Wrapper for the Smart Lock settings in chromeos://settings.
143 def __init__(self
, tab
, chromeos
):
146 tab: Inspector page of the chromeos://settings tag.
147 chromeos: The parent Chrome wrapper.
150 self
._chromeos
= chromeos
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')
158 def TurnOffSmartLock(self
):
159 """ Turns off Smart Lock.
161 Smart Lock is turned off by clicking the turn-off button and navigating
162 through the resulting overlay.
165 TimeoutException: Timed out waiting for Smart Lock to be turned off.
167 assert(self
.is_smart_lock_enabled
)
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'),
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)
180 def StartSetup(self
):
181 """ Starts the Smart Lock setup flow by clicking the button.
183 assert(not self
.is_smart_lock_enabled
)
184 self
._tab
.EvaluateJavaScript(
185 'document.getElementById("easy-unlock-setup-button").click()')
187 def StartSetupAndReturnApp(self
):
188 """ Runs the setup and returns the wrapper to the setup app.
190 After clicking the setup button in the settings page, enter the password to
191 reauthenticate the user before the app launches.
194 A SmartLockApp object of the app that was launched.
197 TimeoutException: Timed out waiting for app.
200 util
.WaitFor(lambda: (self
._chromeos
.session_state
==
201 ChromeOS
.SessionState
.LOCK_SCREEN
),
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()
209 class SmartLockApp(object):
210 """ Wrapper for the Smart Lock setup dialog.
212 Note: This does not include the app's background page.
216 """ The current state of the setup flow. """
219 CLICK_FOR_TRIAL_RUN
= 'click_for_trial_run'
220 TRIAL_RUN_COMPLETED
= 'trial_run_completed'
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
== '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
253 raise ValueError('Unknown button text: %s', button_text
)
255 raise ValueError('Unknown pairing state: %s' % state
)
257 def FindPhone(self
, retries
=3):
258 """ Starts the initial step to find nearby phones.
260 The app must be in the SCAN state.
263 retries: The number of times to retry if no phones are found.
265 True if a phone is found, else False.
267 assert(self
.pairing_state
== self
.PairingState
.SCAN
)
268 for _
in xrange(retries
):
269 self
._ClickPairingButton
()
270 if self
.pairing_state
== self
.PairingState
.PAIR
:
272 # Wait a few seconds before retrying.
277 """ Starts the step of finding nearby phones.
279 The app must be in the PAIR state.
282 True if pairing succeeded, else False.
284 assert(self
.pairing_state
== self
.PairingState
.PAIR
)
285 self
._ClickPairingButton
()
286 return self
.pairing_state
== self
.PairingState
.CLICK_FOR_TRIAL_RUN
288 def StartTrialRun(self
):
289 """ Starts the trial run.
291 The app must be in the CLICK_FOR_TRIAL_RUN state.
294 TimeoutException: Timed out starting the trial run.
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
),
303 def DismissApp(self
):
304 """ Dismisses the app after setup is completed.
306 The app must be in the TRIAL_RUN_COMPLETED state.
308 assert(self
.pairing_state
== self
.PairingState
.TRIAL_RUN_COMPLETED
)
309 self
._app
_page
.EvaluateJavaScript(
310 'document.getElementById("pairing-button").click()')
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)
325 class ChromeOS(object):
326 """ Wrapper for a remote ChromeOS device.
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.
334 """ The state of the user session.
336 SIGNIN_SCREEN
= 'signin_screen'
337 IN_SESSION
= 'in_session'
338 LOCK_SCREEN
= 'lock_screen'
340 _SMART_LOCK_SETTINGS_URL
= 'chrome://settings/search#Smart%20Lock'
342 def __init__(self
, remote_address
, username
, password
, ssh_port
=None):
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.
350 self
._remote
_address
= remote_address
;
351 self
._username
= username
352 self
._password
= password
353 self
._ssh
_port
= ssh_port
355 self
._cros
_interface
= None
356 self
._background
_page
= None
361 ''' Returns the username of the user to login. '''
362 return self
._username
366 ''' Returns the password of the user to login. '''
367 return self
._password
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
377 return self
.SessionState
.SIGNIN_SCREEN
379 return self
.SessionState
.IN_SESSION
;
382 def cryptauth_access_token(self
):
384 util
.WaitFor(lambda: self
._background
_page
.EvaluateJavaScript(
385 'var __token = __token || null; '
386 'chrome.identity.getAuthToken(function(token) {'
389 '__token != null'), 5)
390 return self
._background
_page
.EvaluateJavaScript('__token');
391 except exceptions
.TimeoutException
:
392 logger
.error('Failed to get access token.');
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
:
406 def Start(self
, local_app_path
=None):
407 """ Connects to the ChromeOS device and logs in.
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.
412 |self| for using in a "with" statement.
414 assert(self
._browser
is None)
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
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
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
)
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(
440 browser_type
='cros-chrome',
442 finder_opts
.extensions_to_load
.append(easy_unlock_app
)
445 while self
._browser
is not None or retries
> 0:
447 browser_to_create
= browser_finder
.FindBrowser(finder_opts
)
448 self
._browser
= browser_to_create
.Create(finder_opts
);
450 except (exceptions
.LoginException
) as e
:
451 logger
.error('Timed out logging in: %s' % e
);
455 bg_page_path
= '/_generated_background_page.html'
457 lambda: self
._FindSmartLockAppPage
(bg_page_path
) is not None,
459 self
._background
_page
= self
._FindSmartLockAppPage
(bg_page_path
)
462 def GetAccountPickerScreen(self
):
463 """ Returns the wrapper for the lock screen or sign-in screen.
466 An instance of AccountPickerScreen.
468 TimeoutException: Timed out waiting for account picker screen to load.
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
)
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.
490 An instance of SmartLockSettings.
492 TimeoutException: Timed out waiting for settings page.
494 if not len(self
._browser
.tabs
):
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
)
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
)
512 def GetSmartLockApp(self
):
513 """ Returns the wrapper for the Smart Lock setup app.
516 An instance of SmartLockApp or None if the app window does not exist.
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
)
527 """ Runs the btmon command.
529 A subprocess.Popen object of the btmon process.
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
)
538 def _FindSmartLockAppPage(self
, page_name
):
540 extensions
= self
._browser
.extensions
.GetByExtensionId(
541 'mkaemigholebcgchlkbankmihknojeak')
544 for extension_page
in extensions
:
545 pathname
= extension_page
.EvaluateJavaScript('document.location.pathname')
546 if pathname
== page_name
:
547 return extension_page