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