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