1# Copyright 2013 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.
4import logging
5
6from telemetry.core import util
7from telemetry.core.backends.chrome import timeline_recorder
8from telemetry.timeline import inspector_timeline_data
9
10
11class InspectorNetworkException(Exception):
12  pass
13
14
15class InspectorNetworkResponseData(object):
16  def __init__(self, inspector_network, params):
17    self._inspector_network = inspector_network
18    self._request_id = params['requestId']
19    self._timestamp = params['timestamp']
20
21    self._response = params['response']
22    if not self._response:
23      raise InspectorNetworkException('response must exist')
24
25    # Response headers.
26    headers = self._response['headers']
27    self._header_map = {}
28    for k, v in headers.iteritems():
29      # Camel-case header keys.
30      self._header_map[k.title()] = v
31
32    # Request headers.
33    self._request_header_map = {}
34    if 'requestHeaders' in self._response:
35      # Camel-case header keys.
36      for k, v in self._response['requestHeaders'].iteritems():
37        self._request_header_map[k.title()] = v
38
39    self._body = None
40    self._base64_encoded = False
41    if self._inspector_network:
42      self._served_from_cache = (
43          self._inspector_network.HTTPResponseServedFromCache(self._request_id))
44    else:
45      self._served_from_cache = False
46
47    # Whether constructed from a timeline event.
48    self._from_event = False
49
50  @property
51  def status(self):
52    return self._response['status']
53
54  def status_text(self):
55    return self._response['status_text']
56
57  @property
58  def headers(self):
59    return self._header_map
60
61  @property
62  def request_headers(self):
63    return self._request_header_map
64
65  @property
66  def timestamp(self):
67    return self._timestamp
68
69  @property
70  def timing(self):
71    if 'timing' in self._response:
72      return self._response['timing']
73    return None
74
75  @property
76  def url(self):
77    return self._response['url']
78
79  @property
80  def request_id(self):
81    return self._request_id
82
83  @property
84  def served_from_cache(self):
85    return self._served_from_cache
86
87  def GetHeader(self, name):
88    if name in self.headers:
89      return self.headers[name]
90    return None
91
92  def GetBody(self, timeout=60):
93    if not self._body and not self._from_event:
94      self._body, self._base64_encoded = (
95        self._inspector_network.GetHTTPResponseBody(self._request_id, timeout))
96    return self._body, self._base64_encoded
97
98  def AsTimelineEvent(self):
99    event = {}
100    event['type'] = 'HTTPResponse'
101    event['startTime'] = self.timestamp
102    # There is no end time. Just return the timestamp instead.
103    event['endTime'] = self.timestamp
104    event['requestId'] = self.request_id
105    event['response'] = self._response
106    event['body'], event['base64_encoded_body'] = self.GetBody()
107    event['served_from_cache'] = self.served_from_cache
108    return event
109
110  @staticmethod
111  def FromTimelineEvent(event):
112    assert event.name == 'HTTPResponse'
113    params = {}
114    params['timestamp'] = event.start
115    params['requestId'] = event.args['requestId']
116    params['response'] = event.args['response']
117    recorded = InspectorNetworkResponseData(None, params)
118    recorded._body = event.args['body']
119    recorded._base64_encoded = event.args['base64_encoded_body']
120    recorded._served_from_cache = event.args['served_from_cache']
121    recorded._from_event = True
122    return recorded
123
124
125class InspectorNetwork(object):
126  def __init__(self, inspector_backend):
127    self._inspector_backend = inspector_backend
128    self._http_responses = []
129    self._served_from_cache = set()
130    self._timeline_recorder = None
131
132  def ClearCache(self, timeout=60):
133    """Clears the browser's disk and memory cache."""
134    res = self._inspector_backend.SyncRequest({
135        'method': 'Network.canClearBrowserCache'
136        }, timeout)
137    assert res['result'], 'Cache clearing is not supported by this browser.'
138    self._inspector_backend.SyncRequest({
139        'method': 'Network.clearBrowserCache'
140        }, timeout)
141
142  def StartMonitoringNetwork(self):
143    """Starts monitoring network notifications and recording HTTP responses."""
144    self.ClearResponseData()
145    self._inspector_backend.RegisterDomain(
146        'Network',
147        self._OnNetworkNotification,
148        self._OnClose)
149    request = {
150        'method': 'Network.enable'
151        }
152    self._inspector_backend.SyncRequest(request)
153
154  def StopMonitoringNetwork(self):
155    """Stops monitoring network notifications and recording HTTP responses."""
156    self._inspector_backend.UnregisterDomain('Network')
157    request = {
158        'method': 'Network.disable'
159        }
160    self._inspector_backend.SyncRequest(request)
161
162  def GetResponseData(self):
163    """Returns all recorded HTTP responses."""
164    return self._http_responses
165
166  def ClearResponseData(self):
167    """Clears recorded HTTP responses."""
168    self._http_responses = []
169    self._served_from_cache.clear()
170
171  def _OnNetworkNotification(self, msg):
172    if msg['method'] == 'Network.responseReceived':
173      self._RecordHTTPResponse(msg['params'])
174    elif msg['method'] == 'Network.requestServedFromCache':
175      self._served_from_cache.add(msg['params']['requestId'])
176
177  def _RecordHTTPResponse(self, params):
178    required_fields = ['requestId', 'timestamp', 'response']
179    for field in required_fields:
180      if field not in params:
181        logging.waring('HTTP Response missing required field: %s', field)
182        return
183    self._http_responses.append(InspectorNetworkResponseData(self, params))
184
185  def GetHTTPResponseBody(self, request_id, timeout=60):
186    try:
187      res =  self._inspector_backend.SyncRequest({
188          'method': 'Network.getResponseBody',
189          'params': {
190              'requestId': request_id,
191              }
192          }, timeout)
193    except util.TimeoutException:
194      logging.warning('Timeout during fetching body for %s' % request_id)
195      return None, False
196    if 'error' in res:
197      return None, False
198    return res['result']['body'], res['result']['base64Encoded']
199
200  def HTTPResponseServedFromCache(self, request_id):
201    return request_id and request_id in self._served_from_cache
202
203  def _OnClose(self):
204    pass
205
206  @property
207  def timeline_recorder (self):
208    if not self._timeline_recorder:
209      self._timeline_recorder = TimelineRecorder(self)
210    return self._timeline_recorder
211
212
213class TimelineRecorder(timeline_recorder.TimelineRecorder):
214  def __init__(self, inspector_network):
215    super(TimelineRecorder, self).__init__()
216    self._inspector_network = inspector_network
217    self._is_recording = False
218
219  def Start(self):
220    assert not self._is_recording, 'Start should only be called once.'
221    self._is_recording = True
222    self._inspector_network.StartMonitoringNetwork()
223
224  def Stop(self):
225    if not self._is_recording:
226      return None
227    responses = self._inspector_network.GetResponseData()
228    events = [r.AsTimelineEvent() for r in list(responses)]
229    self._inspector_network.StopMonitoringNetwork()
230    self._is_recording = False
231    if len(events) == 0:
232      return None
233    return inspector_timeline_data.InspectorTimelineData(events)
234