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 an exception if entering the password fails to enter/resume the user | |
119 session. | |
Ilya Sherman
2015/03/25 23:17:29
The Python style is to list "Raises:" just as you
Tim Song
2015/03/27 18:05:26
Done.
| |
120 """ | |
121 assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or | |
122 self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD) | |
123 oobe = self._oobe | |
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), | |
131 5) | |
132 | |
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) | |
137 | |
138 | |
139 class SmartLockSettings(object): | |
140 """ Wrapper for the Smart Lock settings in chromeos://settings. | |
141 """ | |
142 def __init__(self, tab, chromeos): | |
143 """ | |
144 Args: | |
145 tab: Inspector page of the chromeos://settings tag. | |
146 chromeos: The parent Chrome wrapper. | |
147 """ | |
148 self._tab = tab | |
149 self._chromeos = chromeos | |
150 | |
151 @property | |
152 def 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') | |
156 | |
157 def TurnOffSmartLock(self): | |
158 """ Turns off Smart Lock. | |
159 | |
160 Smart Lock is turned off by clicking the turn-off button and navigating | |
161 through the resulting overlay. | |
162 | |
163 Raises an exception if turning off Smart Lock fails. | |
164 """ | |
165 assert(self.smart_lock_enabled) | |
166 tab = self._tab | |
167 tab.EvaluateJavaScript( | |
168 'document.getElementById("easy-unlock-turn-off-button").click()') | |
169 util.WaitFor(lambda: tab.EvaluateJavaScript( | |
170 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && ' | |
171 'document.getElementById("easy-unlock-turn-off-confirm") != null'), | |
172 10) | |
173 tab.EvaluateJavaScript( | |
174 'document.getElementById("easy-unlock-turn-off-confirm").click()') | |
175 util.WaitFor(lambda: tab.EvaluateJavaScript( | |
176 '!document.getElementById("easy-unlock-disabled").hidden'), 15) | |
177 | |
178 def StartSetup(self): | |
179 """ Starts the Smart Lock setup flow by clicking the button. | |
180 """ | |
181 assert(not self.smart_lock_enabled) | |
182 self._tab.EvaluateJavaScript( | |
183 'document.getElementById("easy-unlock-setup-button").click()') | |
184 | |
185 def StartSetupAndReturnApp(self): | |
186 """ Runs the setup and returns the wrapper to the setup app. | |
187 | |
188 After clicking the setup button in the settings page, enter the password to | |
189 reauthenticate the user before the app launches. | |
190 | |
191 Raises an exception if setup fails. | |
192 | |
193 Returns: | |
194 A SmartLockApp object of the app that was launched. | |
195 """ | |
196 self.StartSetup() | |
197 util.WaitFor(lambda: (self._chromeos.session_state == | |
198 ChromeOS.SessionState.LOCK_SCREEN), | |
199 5) | |
200 lock_screen = self._chromeos.GetAccountPickerScreen() | |
201 lock_screen.EnterPassword() | |
202 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10) | |
203 return self._chromeos.GetSmartLockApp() | |
204 | |
205 | |
206 class SmartLockApp(object): | |
207 """ Wrapper for the Smart Lock setup dialog. | |
208 | |
209 Note: This does not include the app's background page. | |
210 """ | |
211 | |
212 class PairingState: | |
213 """ The current state of the setup flow. """ | |
214 SCAN = 'scan' | |
215 PAIR = 'pair' | |
216 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run' | |
217 TRIAL_RUN_COMPLETED = 'trial_run_completed' | |
218 | |
219 def __init__(self, app_page, chromeos): | |
220 """ | |
221 Args: | |
222 app_page: Inspector page of the app window. | |
223 chromeos: The parent Chrome wrapper. | |
224 """ | |
225 self._app_page = app_page | |
226 self._chromeos = chromeos | |
227 | |
228 @property | |
229 def pairing_state(self): | |
230 ''' Returns the state the app is currently in. | |
231 | |
232 Raises an exception if the state is unknown. | |
233 ''' | |
234 state = self._app_page.EvaluateJavaScript( | |
235 'document.body.getAttribute("step")') | |
236 if state == 'scan': | |
237 return SmartLockApp.PairingState.SCAN | |
238 elif state == 'pair': | |
239 return SmartLockApp.PairingState.PAIR | |
240 elif state == 'complete': | |
241 button_text = self._app_page.EvaluateJavaScript( | |
242 'document.getElementById("pairing-button").textContent') | |
243 button_text = button_text.strip().lower() | |
244 if button_text == 'try it out': | |
245 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN | |
246 elif button_text == 'done': | |
247 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED | |
248 else: | |
249 raise ValueError('Unknown button text: %s', button_text) | |
250 else: | |
251 raise ValueError('Unknown pairing state: %s' % state) | |
252 | |
253 def FindPhone(self, retries=3): | |
254 """ Starts the initial step to find nearby phones. | |
255 | |
256 The app must be in the SCAN state. | |
257 | |
258 Args: | |
259 retries: The number of times to retry if no phones are found. | |
260 Returns: | |
261 True if a phone is found, else False. | |
262 """ | |
263 assert(self.pairing_state == self.PairingState.SCAN) | |
264 for _ in xrange(retries): | |
265 self._ClickPairingButton() | |
266 if self.pairing_state == self.PairingState.PAIR: | |
267 return True | |
268 # Wait a few seconds before retrying. | |
269 time.sleep(10) | |
270 return False | |
271 | |
272 def PairPhone(self): | |
273 """ Starts the step of finding nearby phones. | |
274 | |
275 The app must be in the PAIR state. | |
276 | |
277 Returns: | |
278 True if pairing succeeded, else False. | |
279 """ | |
280 assert(self.pairing_state == self.PairingState.PAIR) | |
281 self._ClickPairingButton() | |
282 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN | |
283 | |
284 def StartTrialRun(self): | |
285 """ Starts the trial run. | |
286 | |
287 The app must be in the CLICK_FOR_TRIAL_RUN state. | |
288 Raises an exception if starting the trial run fails. | |
289 """ | |
290 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN) | |
291 self._app_page.EvaluateJavaScript( | |
292 'document.getElementById("pairing-button").click()') | |
293 util.WaitFor(lambda: (self._chromeos.session_state == | |
294 ChromeOS.SessionState.LOCK_SCREEN), | |
295 10) | |
296 | |
297 def DismissApp(self): | |
298 """ Dismisses the app after setup is completed. | |
299 | |
300 The app must be in the TRIAL_RUN_COMPLETED state. | |
301 """ | |
302 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED) | |
303 self._app_page.EvaluateJavaScript( | |
304 'document.getElementById("pairing-button").click()') | |
305 | |
306 def _ClickPairingButton(self): | |
307 self._app_page.EvaluateJavaScript( | |
308 'document.getElementById("pairing-button").click()') | |
309 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( | |
310 '!document.getElementById("pairing-button").disabled'), 60) | |
311 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( | |
312 '!document.getElementById("pairing-button-title")' | |
313 '.classList.contains("animated-fade-out")'), 5) | |
314 util.WaitFor(lambda: self._app_page.EvaluateJavaScript( | |
315 '!document.getElementById("pairing-button-title")' | |
316 '.classList.contains("animated-fade-in")'), 5) | |
317 | |
318 | |
319 class ChromeOS(object): | |
320 """ Wrapper for a remote ChromeOS device. | |
321 | |
322 Operations performed through this wrapper are sent through the network to | |
323 Chrome using the Chrome DevTools API. Therefore, any function may throw an | |
324 exception if the communication to the remote device is severed. | |
325 """ | |
326 | |
327 class SessionState: | |
328 """ The state of the user session. | |
329 """ | |
330 SIGNIN_SCREEN = 'signin_screen' | |
331 IN_SESSION = 'in_session' | |
332 LOCK_SCREEN = 'lock_screen' | |
333 | |
334 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock' | |
335 | |
336 def __init__(self, remote_address, username, password, ssh_port=None): | |
337 """ | |
338 Args: | |
339 remote_address: The remote address of the cros device. | |
340 username: The username of the account to test. | |
341 password: The password of the account to test. | |
342 ssh_port: The ssh port to connect to. | |
343 """ | |
344 self._remote_address = remote_address; | |
345 self._username = username | |
346 self._password = password | |
347 self._ssh_port = ssh_port | |
348 self._browser = None | |
349 self._cros_interface = None | |
350 self._background_page = None | |
351 self._processes = [] | |
352 | |
353 @property | |
354 def username(self): | |
355 ''' Returns the username of the user to login. ''' | |
356 return self._username | |
357 | |
358 @property | |
359 def password(self): | |
360 ''' Returns the password of the user to login. ''' | |
361 return self._password | |
362 | |
363 @property | |
364 def session_state(self): | |
365 ''' Returns the state of the user session. ''' | |
366 assert(self._browser is not None) | |
367 if self._browser.oobe_exists: | |
368 if self._cros_interface.IsCryptohomeMounted(self.username, False): | |
369 return self.SessionState.LOCK_SCREEN | |
370 else: | |
371 return self.SessionState.SIGNIN_SCREEN | |
372 else: | |
373 return self.SessionState.IN_SESSION; | |
374 | |
375 @property | |
376 def cryptauth_access_token(self): | |
377 try: | |
378 util.WaitFor(lambda: self._background_page.EvaluateJavaScript( | |
379 'var __token = __token || null; ' | |
380 'chrome.identity.getAuthToken(function(token) {' | |
381 ' __token = token;' | |
382 '}); ' | |
383 '__token != null'), 5) | |
384 return self._background_page.EvaluateJavaScript('__token'); | |
385 except exceptions.TimeoutException: | |
386 logger.error('Failed to get access token.'); | |
387 return '' | |
388 | |
389 def __enter__(self): | |
390 return self | |
391 | |
392 def __exit__(self, *args): | |
393 if self._browser is not None: | |
394 self._browser.Close() | |
395 if self._cros_interface is not None: | |
396 self._cros_interface.CloseConnection() | |
397 for process in self._processes: | |
398 process.terminate() | |
399 | |
400 def Start(self, local_app_path=None): | |
401 """ Connects to the ChromeOS device and logs in. | |
402 Args: | |
403 local_app_path: A path on the local device containing the Smart Lock app | |
404 to use instead of the app on the ChromeOS device. | |
405 Return: | |
406 |self| for using in a "with" statement. | |
407 """ | |
408 assert(self._browser is None) | |
409 | |
410 finder_opts = browser_options.BrowserFinderOptions('cros-chrome') | |
411 finder_opts.CreateParser().parse_args(args=[]) | |
412 finder_opts.cros_remote = self._remote_address | |
413 if self._ssh_port is not None: | |
414 finder_opts.cros_remote_ssh_port = self._ssh_port | |
415 finder_opts.verbosity = 1 | |
416 | |
417 browser_opts = finder_opts.browser_options | |
418 browser_opts.create_browser_with_oobe = True | |
419 browser_opts.disable_component_extensions_with_background_pages = False | |
420 browser_opts.gaia_login = True | |
421 browser_opts.username = self._username | |
422 browser_opts.password = self._password | |
423 browser_opts.auto_login = True | |
424 | |
425 self._cros_interface = cros_interface.CrOSInterface( | |
426 finder_opts.cros_remote, | |
427 finder_opts.cros_remote_ssh_port, | |
428 finder_opts.cros_ssh_identity) | |
429 | |
430 browser_opts.disable_default_apps = local_app_path is not None | |
431 if local_app_path is not None: | |
432 easy_unlock_app = extension_to_load.ExtensionToLoad( | |
433 path=local_app_path, | |
434 browser_type='cros-chrome', | |
435 is_component=True) | |
436 finder_opts.extensions_to_load.append(easy_unlock_app) | |
437 | |
438 retries = 3 | |
439 while self._browser is not None or retries > 0: | |
440 try: | |
441 browser_to_create = browser_finder.FindBrowser(finder_opts) | |
442 self._browser = browser_to_create.Create(finder_opts); | |
443 break; | |
444 except (exceptions.LoginException) as e: | |
445 logger.error('Timed out logging in: %s' % e); | |
446 if retries == 1: | |
447 raise | |
448 | |
449 bg_page_path = '/_generated_background_page.html' | |
450 util.WaitFor( | |
451 lambda: self._FindSmartLockAppPage(bg_page_path) is not None, | |
452 10); | |
453 self._background_page = self._FindSmartLockAppPage(bg_page_path) | |
454 return self | |
455 | |
456 def GetAccountPickerScreen(self): | |
457 """ Returns the wrapper for the lock screen or sign-in screen. | |
458 | |
459 Return: | |
460 An instance of AccountPickerScreen. | |
461 An exception will be raised if the wrapper can't be created. | |
462 """ | |
463 assert(self._browser is not None) | |
464 assert(self.session_state == self.SessionState.LOCK_SCREEN or | |
465 self.session_state == self.SessionState.SIGNIN_SCREEN) | |
466 oobe = self._browser.oobe | |
467 def IsLockScreenResponsive(): | |
468 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and | |
469 oobe.EvaluateJavaScript( | |
470 "typeof Oobe.authenticateForTesting == 'function'")) | |
471 util.WaitFor(IsLockScreenResponsive, 10) | |
472 util.WaitFor(lambda: oobe.EvaluateJavaScript( | |
473 'document.getElementById("pod-row") && ' | |
474 'document.getElementById("pod-row").pods && ' | |
475 'document.getElementById("pod-row").pods.length > 0'), 10) | |
476 return AccountPickerScreen(oobe, self) | |
477 | |
478 def GetSmartLockSettings(self): | |
479 """ Returns the wrapper for the Smart Lock settings. | |
480 A tab will be navigated to chrome://settings if it does not exist. | |
481 | |
482 Return: | |
483 An instance of SmartLockSettings. | |
484 An exception will be raised if the wrapper can't be created. | |
485 """ | |
486 if not len(self._browser.tabs): | |
487 self._browser.New() | |
488 tab = self._browser.tabs[0] | |
489 url = tab.EvaluateJavaScript('document.location.href') | |
490 if url != self._SMART_LOCK_SETTINGS_URL: | |
491 tab.Navigate(self._SMART_LOCK_SETTINGS_URL) | |
492 | |
493 # Wait for settings page to be responsive. | |
494 util.WaitFor(lambda: tab.EvaluateJavaScript( | |
495 'document.getElementById("easy-unlock-disabled") && ' | |
496 'document.getElementById("easy-unlock-enabled") && ' | |
497 '(!document.getElementById("easy-unlock-disabled").hidden || ' | |
498 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10) | |
499 settings = SmartLockSettings(tab, self) | |
500 logger.info('Started Smart Lock settings: enabled=%s' % | |
501 settings.smart_lock_enabled) | |
502 return settings | |
503 | |
504 def GetSmartLockApp(self): | |
505 """ Returns the wrapper for the Smart Lock setup app. | |
506 | |
507 Return: | |
508 An instance of SmartLockApp or None if the app window does not exist. | |
509 """ | |
510 app_page = self._FindSmartLockAppPage('/pairing.html') | |
511 if app_page is not None: | |
512 # Wait for app window to be responsive. | |
513 util.WaitFor(lambda: app_page.EvaluateJavaScript( | |
514 'document.getElementById("pairing-button") != null'), 10) | |
515 return SmartLockApp(app_page, self) | |
516 return None | |
517 | |
518 def RunBtmon(self): | |
519 """ Runs the btmon command. | |
520 Return: | |
521 A subprocess.Popen object of the btmon process. | |
522 """ | |
523 assert(self._cros_interface) | |
524 cmd = self._cros_interface.FormSSHCommandLine(['btmon']) | |
525 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, | |
526 stderr=subprocess.PIPE) | |
527 self._processes.append(process) | |
528 return process | |
529 | |
530 def _FindSmartLockAppPage(self, page_name): | |
531 try: | |
532 extensions = self._browser.extensions.GetByExtensionId( | |
533 'mkaemigholebcgchlkbankmihknojeak') | |
534 except KeyError: | |
535 return None | |
536 for extension_page in extensions: | |
537 pathname = extension_page.EvaluateJavaScript('document.location.pathname') | |
538 if pathname == page_name: | |
539 return extension_page | |
540 return None | |
OLD | NEW |