1# Copyright 2014 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 5import logging 6import os 7import shutil 8import tempfile 9 10from telemetry.internal import forwarders 11from telemetry.internal.util import webpagereplay 12from telemetry.util import wpr_modes 13 14import certutils 15import platformsettings 16 17 18class ArchiveDoesNotExistError(Exception): 19 """Raised when the archive path does not exist for replay mode.""" 20 pass 21 22 23class ReplayAndBrowserPortsError(Exception): 24 """Raised an existing browser would get different remote replay ports.""" 25 pass 26 27 28class NetworkControllerBackend(object): 29 """Control network settings and servers to simulate the Web. 30 31 Network changes include forwarding device ports to host platform ports. 32 Web Page Replay is used to record and replay HTTP/HTTPS responses. 33 """ 34 35 def __init__(self, platform_backend): 36 self._platform_backend = platform_backend 37 self._wpr_mode = None 38 self._extra_wpr_args = None 39 self._wpr_port_pairs = None 40 self._archive_path = None 41 self._make_javascript_deterministic = None 42 self._forwarder = None 43 self._wpr_ca_cert_path = None 44 self._wpr_server = None 45 46 @property 47 def is_open(self): 48 return self._wpr_mode is not None 49 50 @property 51 def is_replay_active(self): 52 return self._forwarder is not None 53 54 @property 55 def host_ip(self): 56 return self._platform_backend.forwarder_factory.host_ip 57 58 @property 59 def wpr_device_ports(self): 60 try: 61 return self._forwarder.port_pairs.remote_ports 62 except AttributeError: 63 return None 64 65 @property 66 def is_test_ca_installed(self): 67 return self._wpr_ca_cert_path is not None 68 69 def Open(self, wpr_mode, extra_wpr_args): 70 """Configure and prepare target platform for network control. 71 72 This may, e.g., install test certificates and perform any needed setup 73 on the target platform. 74 75 After network interactions are over, clients should call the Close method. 76 77 Args: 78 wpr_mode: a mode for web page replay; available modes are 79 wpr_modes.WPR_OFF, wpr_modes.APPEND, wpr_modes.WPR_REPLAY, or 80 wpr_modes.WPR_RECORD. 81 extra_wpr_args: an list of extra arguments for web page replay. 82 """ 83 assert not self.is_open, 'Network controller is already open' 84 self._wpr_mode = wpr_mode 85 self._extra_wpr_args = extra_wpr_args 86 self._wpr_port_pairs = self._platform_backend.GetWprPortPairs() 87 self._InstallTestCa() 88 89 def Close(self): 90 """Undo changes in the target platform used for network control. 91 92 Implicitly stops replay if currently active. 93 """ 94 self.StopReplay() 95 self._RemoveTestCa() 96 self._make_javascript_deterministic = None 97 self._archive_path = None 98 self._wpr_port_pairs = None 99 self._extra_wpr_args = None 100 self._wpr_mode = None 101 102 def _InstallTestCa(self): 103 if not self._platform_backend.supports_test_ca: 104 return 105 assert not self.is_test_ca_installed, 'Test CA is already installed' 106 if certutils.openssl_import_error: 107 logging.warning( 108 'The OpenSSL module is unavailable. ' 109 'Browsers may fall back to ignoring certificate errors.') 110 return 111 if not platformsettings.HasSniSupport(): 112 logging.warning( 113 'Web Page Replay requires SNI support (pyOpenSSL 0.13 or greater) ' 114 'to generate certificates from a test CA. ' 115 'Browsers may fall back to ignoring certificate errors.') 116 return 117 self._wpr_ca_cert_path = os.path.join(tempfile.mkdtemp(), 'testca.pem') 118 try: 119 certutils.write_dummy_ca_cert(*certutils.generate_dummy_ca_cert(), 120 cert_path=self._wpr_ca_cert_path) 121 self._platform_backend.InstallTestCa(self._wpr_ca_cert_path) 122 logging.info('Test certificate authority installed on target platform.') 123 except Exception: 124 logging.exception( 125 'Failed to install test certificate authority on target platform. ' 126 'Browsers may fall back to ignoring certificate errors.') 127 self._RemoveTestCa() 128 129 def _RemoveTestCa(self): 130 if not self.is_test_ca_installed: 131 return 132 try: 133 self._platform_backend.RemoveTestCa() 134 except Exception: 135 # Best effort cleanup - show the error and continue. 136 logging.exception( 137 'Error trying to remove certificate authority from target platform.') 138 try: 139 shutil.rmtree(os.path.dirname(self._wpr_ca_cert_path), ignore_errors=True) 140 finally: 141 self._wpr_ca_cert_path = None 142 143 def StartReplay(self, archive_path, make_javascript_deterministic=False): 144 """Start web page replay from a given replay archive. 145 146 Starts as needed, and reuses if possible, the replay server on the host and 147 a forwarder from the host to the target platform. 148 149 Implementation details 150 ---------------------- 151 152 The local host is where Telemetry is run. The remote is host where 153 the target application is run. The local and remote hosts may be 154 the same (e.g., testing a desktop browser) or different (e.g., testing 155 an android browser). 156 157 A replay server is started on the local host using the local ports, while 158 a forwarder ties the local to the remote ports. 159 160 Both local and remote ports may be zero. In that case they are determined 161 by the replay server and the forwarder respectively. Setting dns to None 162 disables DNS traffic. 163 164 Args: 165 archive_path: a path to a specific WPR archive. 166 make_javascript_deterministic: True if replay should inject a script 167 to make JavaScript behave deterministically (e.g., override Date()). 168 """ 169 assert self.is_open, 'Network controller is not open' 170 if self._wpr_mode == wpr_modes.WPR_OFF: 171 return 172 if not archive_path: 173 # TODO(slamm, tonyg): Ideally, replay mode should be stopped when there is 174 # no archive path. However, if the replay server already started, and 175 # a file URL is tested with the 176 # telemetry.core.local_server.LocalServerController, then the 177 # replay server forwards requests to it. (Chrome is configured to use 178 # fixed ports fo all HTTP/HTTPS requests.) 179 return 180 if (self._wpr_mode == wpr_modes.WPR_REPLAY and 181 not os.path.exists(archive_path)): 182 raise ArchiveDoesNotExistError( 183 'Archive path does not exist: %s' % archive_path) 184 if (self._wpr_server is not None and 185 self._archive_path == archive_path and 186 self._make_javascript_deterministic == make_javascript_deterministic): 187 return # We may reuse the existing replay server. 188 189 self._archive_path = archive_path 190 self._make_javascript_deterministic = make_javascript_deterministic 191 local_ports = self._StartReplayServer() 192 self._StartForwarder(local_ports) 193 194 def StopReplay(self): 195 """Stop web page replay. 196 197 Stops both the replay server and the forwarder if currently active. 198 """ 199 if self._forwarder: 200 self._forwarder.Close() 201 self._forwarder = None 202 self._StopReplayServer() 203 204 def _StartReplayServer(self): 205 """Start the replay server and return the started local_ports.""" 206 self._StopReplayServer() # In case it was already running. 207 local_ports = self._wpr_port_pairs.local_ports 208 self._wpr_server = webpagereplay.ReplayServer( 209 self._archive_path, 210 self.host_ip, 211 local_ports.http, 212 local_ports.https, 213 local_ports.dns, 214 self._ReplayCommandLineArgs()) 215 return self._wpr_server.StartServer() 216 217 def _StopReplayServer(self): 218 """Stop the replay server only.""" 219 if self._wpr_server: 220 self._wpr_server.StopServer() 221 self._wpr_server = None 222 223 def _ReplayCommandLineArgs(self): 224 wpr_args = list(self._extra_wpr_args) 225 if self._wpr_mode == wpr_modes.WPR_APPEND: 226 wpr_args.append('--append') 227 elif self._wpr_mode == wpr_modes.WPR_RECORD: 228 wpr_args.append('--record') 229 if not self._make_javascript_deterministic: 230 wpr_args.append('--inject_scripts=') 231 if self._wpr_ca_cert_path: 232 wpr_args.extend([ 233 '--should_generate_certs', 234 '--https_root_ca_cert_path=%s' % self._wpr_ca_cert_path]) 235 return wpr_args 236 237 def _StartForwarder(self, local_ports): 238 """Start a forwarder from local_ports to the set WPR remote_ports.""" 239 if self._forwarder is not None: 240 if local_ports == self._forwarder.port_pairs.local_ports: 241 return # Safe to reuse existing forwarder. 242 self._forwarder.Close() 243 self._forwarder = self._platform_backend.forwarder_factory.Create( 244 forwarders.PortPairs.Zip(local_ports, 245 self._wpr_port_pairs.remote_ports)) 246 # Override port pairts with values after defaults have been resolved; 247 # we should use the same set of ports when restarting replay. 248 self._wpr_port_pairs = self._forwarder.port_pairs 249