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