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.
8 from integration_tests
import network_metrics
9 from telemetry
.page
import page_test
10 from telemetry
.value
import scalar
13 class ChromeProxyMetricException(page_test
.MeasurementFailure
):
17 CHROME_PROXY_VIA_HEADER
= 'Chrome-Compression-Proxy'
20 class ChromeProxyResponse(network_metrics
.HTTPResponse
):
21 """ Represents an HTTP response from a timeleine event."""
22 def __init__(self
, event
):
23 super(ChromeProxyResponse
, self
).__init
__(event
)
25 def ShouldHaveChromeProxyViaHeader(self
):
27 # Ignore https and data url
28 if resp
.url
.startswith('https') or resp
.url
.startswith('data:'):
30 # Ignore 304 Not Modified and cache hit.
31 if resp
.status
== 304 or resp
.served_from_cache
:
33 # Ignore invalid responses that don't have any header. Log a warning.
35 logging
.warning('response for %s does not any have header '
36 '(refer=%s, status=%s)',
37 resp
.url
, resp
.GetHeader('Referer'), resp
.status
)
41 def HasChromeProxyViaHeader(self
):
42 via_header
= self
.response
.GetHeader('Via')
45 vias
= [v
.strip(' ') for v
in via_header
.split(',')]
46 # The Via header is valid if it has a 4-character version prefix followed by
47 # the proxy name, for example, "1.1 Chrome-Compression-Proxy".
48 return any(v
[4:] == CHROME_PROXY_VIA_HEADER
for v
in vias
)
50 def IsValidByViaHeader(self
):
51 return (not self
.ShouldHaveChromeProxyViaHeader() or
52 self
.HasChromeProxyViaHeader())
54 def GetChromeProxyClientType(self
):
55 """Get the client type directive from the Chrome-Proxy request header.
58 The client type directive from the Chrome-Proxy request header for the
59 request that lead to this response. For example, if the request header
60 "Chrome-Proxy: c=android" is present, then this method would return
61 "android". Returns None if no client type directive is present.
63 if 'Chrome-Proxy' not in self
.response
.request_headers
:
66 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
67 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
69 kvp
= value
.split('=', 1)
70 if len(kvp
) == 2 and kvp
[0].strip() == 'c':
74 def HasChromeProxyLoFi(self
):
75 if 'Chrome-Proxy' not in self
.response
.request_headers
:
77 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
78 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
80 if len(value
) == 5 and value
== 'q=low':
84 class ChromeProxyMetric(network_metrics
.NetworkMetric
):
85 """A Chrome proxy timeline metric."""
88 super(ChromeProxyMetric
, self
).__init
__()
89 self
.compute_data_saving
= True
91 def SetEvents(self
, events
):
92 """Used for unittest."""
95 def ResponseFromEvent(self
, event
):
96 return ChromeProxyResponse(event
)
98 def AddResults(self
, tab
, results
):
99 raise NotImplementedError
101 def AddResultsForDataSaving(self
, tab
, results
):
102 resources_via_proxy
= 0
103 resources_from_cache
= 0
106 super(ChromeProxyMetric
, self
).AddResults(tab
, results
)
107 for resp
in self
.IterResponses(tab
):
108 if resp
.response
.served_from_cache
:
109 resources_from_cache
+= 1
110 if resp
.HasChromeProxyViaHeader():
111 resources_via_proxy
+= 1
113 resources_direct
+= 1
115 results
.AddValue(scalar
.ScalarValue(
116 results
.current_page
, 'resources_via_proxy', 'count',
117 resources_via_proxy
))
118 results
.AddValue(scalar
.ScalarValue(
119 results
.current_page
, 'resources_from_cache', 'count',
120 resources_from_cache
))
121 results
.AddValue(scalar
.ScalarValue(
122 results
.current_page
, 'resources_direct', 'count', resources_direct
))
124 def AddResultsForHeaderValidation(self
, tab
, results
):
127 for resp
in self
.IterResponses(tab
):
128 if resp
.IsValidByViaHeader():
132 raise ChromeProxyMetricException
, (
133 '%s: Via header (%s) is not valid (refer=%s, status=%d)' % (
134 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
135 results
.AddValue(scalar
.ScalarValue(
136 results
.current_page
, 'checked_via_header', 'count', via_count
))
138 def AddResultsForClientVersion(self
, tab
, results
):
139 for resp
in self
.IterResponses(tab
):
141 if resp
.response
.status
!= 200:
142 raise ChromeProxyMetricException
, ('%s: Response is not 200: %d' %
144 if not resp
.IsValidByViaHeader():
145 raise ChromeProxyMetricException
, ('%s: Response missing via header' %
147 results
.AddValue(scalar
.ScalarValue(
148 results
.current_page
, 'version_test', 'count', 1))
150 def GetClientTypeFromRequests(self
, tab
):
151 """Get the Chrome-Proxy client type value from requests made in this tab.
154 The client type value from the first request made in this tab that
155 specifies a client type in the Chrome-Proxy request header. See
156 ChromeProxyResponse.GetChromeProxyClientType for more details about the
157 Chrome-Proxy client type. Returns None if none of the requests made in
158 this tab specify a client type.
160 for resp
in self
.IterResponses(tab
):
161 client_type
= resp
.GetChromeProxyClientType()
166 def AddResultsForClientType(self
, tab
, results
, client_type
,
167 bypass_for_client_type
):
171 for resp
in self
.IterResponses(tab
):
172 if resp
.HasChromeProxyViaHeader():
174 if client_type
.lower() == bypass_for_client_type
.lower():
175 raise ChromeProxyMetricException
, (
176 '%s: Response for client of type "%s" has via header, but should '
178 resp
.response
.url
, bypass_for_client_type
, client_type
))
179 elif resp
.ShouldHaveChromeProxyViaHeader():
181 if client_type
.lower() != bypass_for_client_type
.lower():
182 raise ChromeProxyMetricException
, (
183 '%s: Response missing via header. Only "%s" clients should '
184 'bypass for this page, but this client is "%s".' % (
185 resp
.response
.url
, bypass_for_client_type
, client_type
))
187 results
.AddValue(scalar
.ScalarValue(
188 results
.current_page
, 'via', 'count', via_count
))
189 results
.AddValue(scalar
.ScalarValue(
190 results
.current_page
, 'bypass', 'count', bypass_count
))
192 def AddResultsForLoFi(self
, tab
, results
):
195 for resp
in self
.IterResponses(tab
):
196 if resp
.HasChromeProxyViaHeader():
200 raise ChromeProxyMetricException
, (
201 '%s: LoFi not in request header.' % (r
.url
))
203 cl
= resp
.content_length
204 resource
= resp
.response
.url
205 results
.AddValue(scalar
.ScalarValue(
206 results
.current_page
, 'lo_fi', 'count', lo_fi_count
))
208 for resp
in self
.IterResponses(tab
):
210 cl
= resp
.content_length
211 ocl
= resp
.original_content_length
212 saving
= resp
.data_saving_rate
* 100
214 raise ChromeProxyMetricException
, (
215 'Image %s is %d bytes. Expecting less than 100 bytes.' %
218 results
.AddValue(scalar
.ScalarValue(
219 results
.current_page
, 'content_length', 'bytes', cl
))
220 results
.AddValue(scalar
.ScalarValue(
221 results
.current_page
, 'original_content_length', 'bytes', ocl
))
222 results
.AddValue(scalar
.ScalarValue(
223 results
.current_page
, 'data_saving', 'percent', saving
))
225 def AddResultsForBypass(self
, tab
, results
):
228 for resp
in self
.IterResponses(tab
):
229 if resp
.HasChromeProxyViaHeader():
231 raise ChromeProxyMetricException
, (
232 '%s: Should not have Via header (%s) (refer=%s, status=%d)' % (
233 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
236 results
.AddValue(scalar
.ScalarValue(
237 results
.current_page
, 'bypass', 'count', bypass_count
))
239 def AddResultsForCorsBypass(self
, tab
, results
):
240 eligible_response_count
= 0
243 for resp
in self
.IterResponses(tab
):
244 logging
.warn('got a resource %s' % (resp
.response
.url
))
246 for resp
in self
.IterResponses(tab
):
247 if resp
.ShouldHaveChromeProxyViaHeader():
248 eligible_response_count
+= 1
249 if not resp
.HasChromeProxyViaHeader():
251 elif resp
.response
.status
== 502:
252 bypasses
[resp
.response
.url
] = 0
254 for resp
in self
.IterResponses(tab
):
255 if resp
.ShouldHaveChromeProxyViaHeader():
256 if not resp
.HasChromeProxyViaHeader():
257 if resp
.response
.status
== 200:
258 if (bypasses
.has_key(resp
.response
.url
)):
259 bypasses
[resp
.response
.url
] = bypasses
[resp
.response
.url
] + 1
262 if bypasses
[url
] == 0:
263 raise ChromeProxyMetricException
, (
264 '%s: Got a 502 without a subsequent 200' % (url
))
265 elif bypasses
[url
] > 1:
266 raise ChromeProxyMetricException
, (
267 '%s: Got a 502 and multiple 200s: %d' % (url
, bypasses
[url
]))
268 if bypass_count
== 0:
269 raise ChromeProxyMetricException
, (
270 'At least one response should be bypassed. '
271 '(eligible_response_count=%d, bypass_count=%d)\n' % (
272 eligible_response_count
, bypass_count
))
274 results
.AddValue(scalar
.ScalarValue(
275 results
.current_page
, 'cors_bypass', 'count', bypass_count
))
277 def AddResultsForBlockOnce(self
, tab
, results
):
278 eligible_response_count
= 0
281 for resp
in self
.IterResponses(tab
):
282 if resp
.ShouldHaveChromeProxyViaHeader():
283 eligible_response_count
+= 1
284 if not resp
.HasChromeProxyViaHeader():
287 if eligible_response_count
<= 1:
288 raise ChromeProxyMetricException
, (
289 'There should be more than one DRP eligible response '
290 '(eligible_response_count=%d, bypass_count=%d)\n' % (
291 eligible_response_count
, bypass_count
))
292 elif bypass_count
!= 1:
293 raise ChromeProxyMetricException
, (
294 'Exactly one response should be bypassed. '
295 '(eligible_response_count=%d, bypass_count=%d)\n' % (
296 eligible_response_count
, bypass_count
))
298 results
.AddValue(scalar
.ScalarValue(
299 results
.current_page
, 'eligible_responses', 'count',
300 eligible_response_count
))
301 results
.AddValue(scalar
.ScalarValue(
302 results
.current_page
, 'bypass', 'count', bypass_count
))
304 def AddResultsForSafebrowsingOn(self
, tab
, results
):
305 results
.AddValue(scalar
.ScalarValue(
306 results
.current_page
, 'safebrowsing', 'timeout responses', 1))
308 def AddResultsForSafebrowsingOff(self
, tab
, results
):
310 for resp
in self
.IterResponses(tab
):
311 # Data reduction proxy should return the real response for sites with
314 if not resp
.HasChromeProxyViaHeader():
316 raise ChromeProxyMetricException
, (
317 '%s: Safebrowsing feature should be off for desktop and webview.\n'
318 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
319 r
.url
, r
.status
, r
.status_text
, r
.headers
))
321 if response_count
== 0:
322 raise ChromeProxyMetricException
, (
323 'Safebrowsing test failed: No valid responses received')
325 results
.AddValue(scalar
.ScalarValue(
326 results
.current_page
, 'safebrowsing', 'responses', response_count
))
328 def AddResultsForHTTPFallback(self
, tab
, results
):
329 via_fallback_count
= 0
331 for resp
in self
.IterResponses(tab
):
332 if resp
.ShouldHaveChromeProxyViaHeader():
333 # All responses should have come through the HTTP fallback proxy, which
334 # means that they should have the via header, and if a remote port is
335 # defined, it should be port 80.
336 if (not resp
.HasChromeProxyViaHeader() or
337 (resp
.remote_port
and resp
.remote_port
!= 80)):
339 raise ChromeProxyMetricException
, (
340 '%s: Should have come through the fallback proxy.\n'
341 'Reponse: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
342 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
344 via_fallback_count
+= 1
346 results
.AddValue(scalar
.ScalarValue(
347 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
349 def AddResultsForHTTPToDirectFallback(self
, tab
, results
,
350 fallback_response_host
):
351 via_fallback_count
= 0
353 responses
= self
.IterResponses(tab
)
355 # The first response(s) coming from fallback_response_host should be
356 # through the HTTP fallback proxy.
357 resp
= next(responses
, None)
358 while resp
and fallback_response_host
in resp
.response
.url
:
359 if fallback_response_host
in resp
.response
.url
:
360 if (not resp
.HasChromeProxyViaHeader() or resp
.remote_port
!= 80):
362 raise ChromeProxyMetricException
, (
363 'Response for %s should have come through the fallback proxy.\n'
364 'Response: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
365 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
368 via_fallback_count
+= 1
369 resp
= next(responses
, None)
371 # All other responses should be bypassed.
373 if resp
.HasChromeProxyViaHeader():
375 raise ChromeProxyMetricException
, (
376 'Response for %s should not have via header.\n'
377 'Response: status=(%d, %s)\nHeaders:\n %s' % (
378 r
.url
, r
.status
, r
.status_text
, r
.headers
))
381 resp
= next(responses
, None)
383 # At least one response should go through the http proxy and be bypassed.
384 if via_fallback_count
== 0 or bypass_count
== 0:
385 raise ChromeProxyMetricException(
386 'There should be at least one response through the fallback proxy '
387 '(actual %s) and at least one bypassed response (actual %s)' %
388 (via_fallback_count
, bypass_count
))
390 results
.AddValue(scalar
.ScalarValue(
391 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
392 results
.AddValue(scalar
.ScalarValue(
393 results
.current_page
, 'bypass', 'count', bypass_count
))
395 def AddResultsForReenableAfterBypass(
396 self
, tab
, results
, bypass_seconds_min
, bypass_seconds_max
):
397 """Verify results for a re-enable after bypass test.
400 tab: the tab for the test.
401 results: the results object to add the results values to.
402 bypass_seconds_min: the minimum duration of the bypass.
403 bypass_seconds_max: the maximum duration of the bypass.
408 for resp
in self
.IterResponses(tab
):
409 if resp
.HasChromeProxyViaHeader():
411 raise ChromeProxyMetricException
, (
412 'Response for %s should not have via header.\n'
413 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
414 r
.url
, r
.status
, r
.status_text
, r
.headers
))
418 # Wait until 30 seconds before the bypass should expire, and fetch a page.
419 # It should not have the via header because the proxy should still be
421 time
.sleep(bypass_seconds_min
- 30)
423 tab
.ClearCache(force
=True)
424 before_metrics
= ChromeProxyMetric()
425 before_metrics
.Start(results
.current_page
, tab
)
426 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
427 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
428 before_metrics
.Stop(results
.current_page
, tab
)
430 for resp
in before_metrics
.IterResponses(tab
):
431 if resp
.HasChromeProxyViaHeader():
433 raise ChromeProxyMetricException
, (
434 'Response for %s should not have via header; proxy should still '
435 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
436 r
.url
, r
.status
, r
.status_text
, r
.headers
))
440 # Wait until 30 seconds after the bypass should expire, and fetch a page. It
441 # should have the via header since the proxy should no longer be bypassed.
442 time
.sleep((bypass_seconds_max
+ 30) - (bypass_seconds_min
- 30))
444 tab
.ClearCache(force
=True)
445 after_metrics
= ChromeProxyMetric()
446 after_metrics
.Start(results
.current_page
, tab
)
447 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
448 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
449 after_metrics
.Stop(results
.current_page
, tab
)
451 for resp
in after_metrics
.IterResponses(tab
):
452 if not resp
.HasChromeProxyViaHeader():
454 raise ChromeProxyMetricException
, (
455 'Response for %s should have via header; proxy should no longer '
456 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
457 r
.url
, r
.status
, r
.status_text
, r
.headers
))
461 results
.AddValue(scalar
.ScalarValue(
462 results
.current_page
, 'bypass', 'count', bypass_count
))
463 results
.AddValue(scalar
.ScalarValue(
464 results
.current_page
, 'via', 'count', via_count
))