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