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 HasExtraViaHeader(self
, extra_header
):
51 via_header
= self
.response
.GetHeader('Via')
54 vias
= [v
.strip(' ') for v
in via_header
.split(',')]
55 return any(v
== extra_header
for v
in vias
)
57 def IsValidByViaHeader(self
):
58 return (not self
.ShouldHaveChromeProxyViaHeader() or
59 self
.HasChromeProxyViaHeader())
61 def GetChromeProxyClientType(self
):
62 """Get the client type directive from the Chrome-Proxy request header.
65 The client type directive from the Chrome-Proxy request header for the
66 request that lead to this response. For example, if the request header
67 "Chrome-Proxy: c=android" is present, then this method would return
68 "android". Returns None if no client type directive is present.
70 if 'Chrome-Proxy' not in self
.response
.request_headers
:
73 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
74 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
76 kvp
= value
.split('=', 1)
77 if len(kvp
) == 2 and kvp
[0].strip() == 'c':
81 def HasChromeProxyLoFi(self
):
82 if 'Chrome-Proxy' not in self
.response
.request_headers
:
84 chrome_proxy_request_header
= self
.response
.request_headers
['Chrome-Proxy']
85 values
= [v
.strip() for v
in chrome_proxy_request_header
.split(',')]
87 if len(value
) == 5 and value
== 'q=low':
91 class ChromeProxyMetric(network_metrics
.NetworkMetric
):
92 """A Chrome proxy timeline metric."""
95 super(ChromeProxyMetric
, self
).__init
__()
96 self
.compute_data_saving
= True
98 def SetEvents(self
, events
):
99 """Used for unittest."""
100 self
._events
= events
102 def ResponseFromEvent(self
, event
):
103 return ChromeProxyResponse(event
)
105 def AddResults(self
, tab
, results
):
106 raise NotImplementedError
108 def AddResultsForDataSaving(self
, tab
, results
):
109 resources_via_proxy
= 0
110 resources_from_cache
= 0
113 super(ChromeProxyMetric
, self
).AddResults(tab
, results
)
114 for resp
in self
.IterResponses(tab
):
115 if resp
.response
.served_from_cache
:
116 resources_from_cache
+= 1
117 if resp
.HasChromeProxyViaHeader():
118 resources_via_proxy
+= 1
120 resources_direct
+= 1
122 if resources_from_cache
+ resources_via_proxy
+ resources_direct
== 0:
123 raise ChromeProxyMetricException
, (
124 'Expected at least one response, but zero responses were received.')
126 results
.AddValue(scalar
.ScalarValue(
127 results
.current_page
, 'resources_via_proxy', 'count',
128 resources_via_proxy
))
129 results
.AddValue(scalar
.ScalarValue(
130 results
.current_page
, 'resources_from_cache', 'count',
131 resources_from_cache
))
132 results
.AddValue(scalar
.ScalarValue(
133 results
.current_page
, 'resources_direct', 'count', resources_direct
))
135 def AddResultsForHeaderValidation(self
, tab
, results
):
138 for resp
in self
.IterResponses(tab
):
139 if resp
.IsValidByViaHeader():
143 raise ChromeProxyMetricException
, (
144 '%s: Via header (%s) is not valid (refer=%s, status=%d)' % (
145 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
148 raise ChromeProxyMetricException
, (
149 'Expected at least one response through the proxy, but zero such '
150 'responses were received.')
151 results
.AddValue(scalar
.ScalarValue(
152 results
.current_page
, 'checked_via_header', 'count', via_count
))
154 def AddResultsForLatency(self
, tab
, results
):
155 # TODO(bustamante): This is a hack to workaround crbug.com/467174,
156 # once fixed just pull down window.performance.timing object and
157 # reference that everywhere.
158 load_event_start
= tab
.EvaluateJavaScript(
159 'window.performance.timing.loadEventStart')
160 navigation_start
= tab
.EvaluateJavaScript(
161 'window.performance.timing.navigationStart')
162 dom_content_loaded_event_start
= tab
.EvaluateJavaScript(
163 'window.performance.timing.domContentLoadedEventStart')
164 fetch_start
= tab
.EvaluateJavaScript(
165 'window.performance.timing.fetchStart')
166 request_start
= tab
.EvaluateJavaScript(
167 'window.performance.timing.requestStart')
168 domain_lookup_end
= tab
.EvaluateJavaScript(
169 'window.performance.timing.domainLookupEnd')
170 domain_lookup_start
= tab
.EvaluateJavaScript(
171 'window.performance.timing.domainLookupStart')
172 connect_end
= tab
.EvaluateJavaScript(
173 'window.performance.timing.connectEnd')
174 connect_start
= tab
.EvaluateJavaScript(
175 'window.performance.timing.connectStart')
176 response_end
= tab
.EvaluateJavaScript(
177 'window.performance.timing.responseEnd')
178 response_start
= tab
.EvaluateJavaScript(
179 'window.performance.timing.responseStart')
181 # NavigationStart relative markers in milliseconds.
182 load_start
= (float(load_event_start
) - navigation_start
)
183 results
.AddValue(scalar
.ScalarValue(
184 results
.current_page
, 'load_start', 'ms', load_start
))
186 dom_content_loaded_start
= (
187 float(dom_content_loaded_event_start
) - navigation_start
)
188 results
.AddValue(scalar
.ScalarValue(
189 results
.current_page
, 'dom_content_loaded_start', 'ms',
190 dom_content_loaded_start
))
192 fetch_start
= (float(fetch_start
) - navigation_start
)
193 results
.AddValue(scalar
.ScalarValue(
194 results
.current_page
, 'fetch_start', 'ms', fetch_start
,
197 request_start
= (float(request_start
) - navigation_start
)
198 results
.AddValue(scalar
.ScalarValue(
199 results
.current_page
, 'request_start', 'ms', request_start
,
202 # Phase measurements in milliseconds.
203 domain_lookup_duration
= (float(domain_lookup_end
) - domain_lookup_start
)
204 results
.AddValue(scalar
.ScalarValue(
205 results
.current_page
, 'domain_lookup_duration', 'ms',
206 domain_lookup_duration
, important
=False))
208 connect_duration
= (float(connect_end
) - connect_start
)
209 results
.AddValue(scalar
.ScalarValue(
210 results
.current_page
, 'connect_duration', 'ms', connect_duration
,
213 request_duration
= (float(response_start
) - request_start
)
214 results
.AddValue(scalar
.ScalarValue(
215 results
.current_page
, 'request_duration', 'ms', request_duration
,
218 response_duration
= (float(response_end
) - response_start
)
219 results
.AddValue(scalar
.ScalarValue(
220 results
.current_page
, 'response_duration', 'ms', response_duration
,
223 def AddResultsForExtraViaHeader(self
, tab
, results
, extra_via_header
):
226 for resp
in self
.IterResponses(tab
):
227 if resp
.HasChromeProxyViaHeader():
228 if resp
.HasExtraViaHeader(extra_via_header
):
231 raise ChromeProxyMetricException
, (
232 '%s: Should have via header %s.' % (resp
.response
.url
,
235 results
.AddValue(scalar
.ScalarValue(
236 results
.current_page
, 'extra_via_header', 'count', extra_via_count
))
238 def AddResultsForClientVersion(self
, tab
, results
):
240 for resp
in self
.IterResponses(tab
):
242 if resp
.response
.status
!= 200:
243 raise ChromeProxyMetricException
, ('%s: Response is not 200: %d' %
245 if not resp
.IsValidByViaHeader():
246 raise ChromeProxyMetricException
, ('%s: Response missing via header' %
251 raise ChromeProxyMetricException
, (
252 'Expected at least one response through the proxy, but zero such '
253 'responses were received.')
254 results
.AddValue(scalar
.ScalarValue(
255 results
.current_page
, 'responses_via_proxy', 'count', via_count
))
257 def GetClientTypeFromRequests(self
, tab
):
258 """Get the Chrome-Proxy client type value from requests made in this tab.
261 The client type value from the first request made in this tab that
262 specifies a client type in the Chrome-Proxy request header. See
263 ChromeProxyResponse.GetChromeProxyClientType for more details about the
264 Chrome-Proxy client type. Returns None if none of the requests made in
265 this tab specify a client type.
267 for resp
in self
.IterResponses(tab
):
268 client_type
= resp
.GetChromeProxyClientType()
273 def AddResultsForClientType(self
, tab
, results
, client_type
,
274 bypass_for_client_type
):
278 for resp
in self
.IterResponses(tab
):
279 if resp
.HasChromeProxyViaHeader():
281 if client_type
.lower() == bypass_for_client_type
.lower():
282 raise ChromeProxyMetricException
, (
283 '%s: Response for client of type "%s" has via header, but should '
284 'be bypassed.' % (resp
.response
.url
, bypass_for_client_type
))
285 elif resp
.ShouldHaveChromeProxyViaHeader():
287 if client_type
.lower() != bypass_for_client_type
.lower():
288 raise ChromeProxyMetricException
, (
289 '%s: Response missing via header. Only "%s" clients should '
290 'bypass for this page, but this client is "%s".' % (
291 resp
.response
.url
, bypass_for_client_type
, client_type
))
293 if via_count
+ bypass_count
== 0:
294 raise ChromeProxyMetricException
, (
295 'Expected at least one response that was eligible to be proxied, but '
296 'zero such responses were received.')
298 results
.AddValue(scalar
.ScalarValue(
299 results
.current_page
, 'via', 'count', via_count
))
300 results
.AddValue(scalar
.ScalarValue(
301 results
.current_page
, 'bypass', 'count', bypass_count
))
303 def AddResultsForLoFi(self
, tab
, results
):
306 for resp
in self
.IterResponses(tab
):
307 if resp
.HasChromeProxyLoFi():
310 raise ChromeProxyMetricException
, (
311 '%s: LoFi not in request header.' % (resp
.response
.url
))
313 if resp
.content_length
> 100:
314 raise ChromeProxyMetricException
, (
315 'Image %s is %d bytes. Expecting less than 100 bytes.' %
316 (resp
.response
.url
, resp
.content_length
))
319 raise ChromeProxyMetricException
, (
320 'Expected at least one LoFi response, but zero such responses were '
323 results
.AddValue(scalar
.ScalarValue(
324 results
.current_page
, 'lo_fi', 'count', lo_fi_count
))
325 super(ChromeProxyMetric
, self
).AddResults(tab
, results
)
327 def AddResultsForBypass(self
, tab
, results
):
330 for resp
in self
.IterResponses(tab
):
331 if resp
.HasChromeProxyViaHeader():
333 raise ChromeProxyMetricException
, (
334 '%s: Should not have Via header (%s) (refer=%s, status=%d)' % (
335 r
.url
, r
.GetHeader('Via'), r
.GetHeader('Referer'), r
.status
))
338 if bypass_count
== 0:
339 raise ChromeProxyMetricException
, (
340 'Expected at least one response to be bypassed, but zero such '
341 'responses were received.')
342 results
.AddValue(scalar
.ScalarValue(
343 results
.current_page
, 'bypass', 'count', bypass_count
))
345 def AddResultsForCorsBypass(self
, tab
, results
):
346 eligible_response_count
= 0
349 for resp
in self
.IterResponses(tab
):
350 logging
.warn('got a resource %s' % (resp
.response
.url
))
352 for resp
in self
.IterResponses(tab
):
353 if resp
.ShouldHaveChromeProxyViaHeader():
354 eligible_response_count
+= 1
355 if not resp
.HasChromeProxyViaHeader():
357 elif resp
.response
.status
== 502:
358 bypasses
[resp
.response
.url
] = 0
360 for resp
in self
.IterResponses(tab
):
361 if resp
.ShouldHaveChromeProxyViaHeader():
362 if not resp
.HasChromeProxyViaHeader():
363 if resp
.response
.status
== 200:
364 if (bypasses
.has_key(resp
.response
.url
)):
365 bypasses
[resp
.response
.url
] = bypasses
[resp
.response
.url
] + 1
368 if bypasses
[url
] == 0:
369 raise ChromeProxyMetricException
, (
370 '%s: Got a 502 without a subsequent 200' % (url
))
371 elif bypasses
[url
] > 1:
372 raise ChromeProxyMetricException
, (
373 '%s: Got a 502 and multiple 200s: %d' % (url
, bypasses
[url
]))
374 if bypass_count
== 0:
375 raise ChromeProxyMetricException
, (
376 'At least one response should be bypassed. '
377 '(eligible_response_count=%d, bypass_count=%d)\n' % (
378 eligible_response_count
, bypass_count
))
380 results
.AddValue(scalar
.ScalarValue(
381 results
.current_page
, 'cors_bypass', 'count', bypass_count
))
383 def AddResultsForBlockOnce(self
, tab
, results
):
384 eligible_response_count
= 0
387 for resp
in self
.IterResponses(tab
):
388 if resp
.ShouldHaveChromeProxyViaHeader():
389 eligible_response_count
+= 1
390 if not resp
.HasChromeProxyViaHeader():
393 if eligible_response_count
<= 1:
394 raise ChromeProxyMetricException
, (
395 'There should be more than one DRP eligible response '
396 '(eligible_response_count=%d, bypass_count=%d)\n' % (
397 eligible_response_count
, bypass_count
))
398 elif bypass_count
!= 1:
399 raise ChromeProxyMetricException
, (
400 'Exactly one response should be bypassed. '
401 '(eligible_response_count=%d, bypass_count=%d)\n' % (
402 eligible_response_count
, bypass_count
))
404 results
.AddValue(scalar
.ScalarValue(
405 results
.current_page
, 'eligible_responses', 'count',
406 eligible_response_count
))
407 results
.AddValue(scalar
.ScalarValue(
408 results
.current_page
, 'bypass', 'count', bypass_count
))
410 def AddResultsForSafebrowsingOn(self
, tab
, results
):
411 results
.AddValue(scalar
.ScalarValue(
412 results
.current_page
, 'safebrowsing', 'timeout responses', 1))
414 def AddResultsForSafebrowsingOff(self
, tab
, results
):
416 for resp
in self
.IterResponses(tab
):
417 # Data reduction proxy should return the real response for sites with
420 if not resp
.HasChromeProxyViaHeader():
422 raise ChromeProxyMetricException
, (
423 '%s: Safebrowsing feature should be off for desktop and webview.\n'
424 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
425 r
.url
, r
.status
, r
.status_text
, r
.headers
))
427 if response_count
== 0:
428 raise ChromeProxyMetricException
, (
429 'Safebrowsing test failed: No valid responses received')
431 results
.AddValue(scalar
.ScalarValue(
432 results
.current_page
, 'safebrowsing', 'responses', response_count
))
434 def AddResultsForHTTPFallback(self
, tab
, results
):
435 via_fallback_count
= 0
437 for resp
in self
.IterResponses(tab
):
438 if resp
.ShouldHaveChromeProxyViaHeader():
439 # All responses should have come through the HTTP fallback proxy, which
440 # means that they should have the via header, and if a remote port is
441 # defined, it should be port 80.
442 if (not resp
.HasChromeProxyViaHeader() or
443 (resp
.remote_port
and resp
.remote_port
!= 80)):
445 raise ChromeProxyMetricException
, (
446 '%s: Should have come through the fallback proxy.\n'
447 'Reponse: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
448 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
450 via_fallback_count
+= 1
452 if via_fallback_count
== 0:
453 raise ChromeProxyMetricException
, (
454 'Expected at least one response through the fallback proxy, but zero '
455 'such responses were received.')
456 results
.AddValue(scalar
.ScalarValue(
457 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
459 def AddResultsForHTTPToDirectFallback(self
, tab
, results
,
460 fallback_response_host
):
461 via_fallback_count
= 0
463 responses
= self
.IterResponses(tab
)
465 # The first response(s) coming from fallback_response_host should be
466 # through the HTTP fallback proxy.
467 resp
= next(responses
, None)
468 while resp
and fallback_response_host
in resp
.response
.url
:
469 if fallback_response_host
in resp
.response
.url
:
470 if (not resp
.HasChromeProxyViaHeader() or resp
.remote_port
!= 80):
472 raise ChromeProxyMetricException
, (
473 'Response for %s should have come through the fallback proxy.\n'
474 'Response: remote_port=%s status=(%d, %s)\nHeaders:\n %s' % (
475 r
.url
, str(resp
.remote_port
), r
.status
, r
.status_text
,
478 via_fallback_count
+= 1
479 resp
= next(responses
, None)
481 # All other responses should be bypassed.
483 if resp
.HasChromeProxyViaHeader():
485 raise ChromeProxyMetricException
, (
486 'Response for %s should not have via header.\n'
487 'Response: status=(%d, %s)\nHeaders:\n %s' % (
488 r
.url
, r
.status
, r
.status_text
, r
.headers
))
491 resp
= next(responses
, None)
493 # At least one response should go through the http proxy and be bypassed.
494 if via_fallback_count
== 0 or bypass_count
== 0:
495 raise ChromeProxyMetricException(
496 'There should be at least one response through the fallback proxy '
497 '(actual %s) and at least one bypassed response (actual %s)' %
498 (via_fallback_count
, bypass_count
))
500 results
.AddValue(scalar
.ScalarValue(
501 results
.current_page
, 'via_fallback', 'count', via_fallback_count
))
502 results
.AddValue(scalar
.ScalarValue(
503 results
.current_page
, 'bypass', 'count', bypass_count
))
505 def AddResultsForReenableAfterBypass(
506 self
, tab
, results
, bypass_seconds_min
, bypass_seconds_max
):
507 """Verify results for a re-enable after bypass test.
510 tab: the tab for the test.
511 results: the results object to add the results values to.
512 bypass_seconds_min: the minimum duration of the bypass.
513 bypass_seconds_max: the maximum duration of the bypass.
518 for resp
in self
.IterResponses(tab
):
519 if resp
.HasChromeProxyViaHeader():
521 raise ChromeProxyMetricException
, (
522 'Response for %s should not have via header.\n'
523 'Reponse: status=(%d, %s)\nHeaders:\n %s' % (
524 r
.url
, r
.status
, r
.status_text
, r
.headers
))
528 # Wait until 30 seconds before the bypass should expire, and fetch a page.
529 # It should not have the via header because the proxy should still be
531 time
.sleep(bypass_seconds_min
- 30)
533 tab
.ClearCache(force
=True)
534 before_metrics
= ChromeProxyMetric()
535 before_metrics
.Start(results
.current_page
, tab
)
536 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
537 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
538 before_metrics
.Stop(results
.current_page
, tab
)
540 for resp
in before_metrics
.IterResponses(tab
):
541 if resp
.HasChromeProxyViaHeader():
543 raise ChromeProxyMetricException
, (
544 'Response for %s should not have via header; proxy should still '
545 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
546 r
.url
, r
.status
, r
.status_text
, r
.headers
))
549 if bypass_count
== 0:
550 raise ChromeProxyMetricException
, (
551 'Expected at least one response to be bypassed before the bypass '
552 'expired, but zero such responses were received.')
554 # Wait until 30 seconds after the bypass should expire, and fetch a page. It
555 # should have the via header since the proxy should no longer be bypassed.
556 time
.sleep((bypass_seconds_max
+ 30) - (bypass_seconds_min
- 30))
558 tab
.ClearCache(force
=True)
559 after_metrics
= ChromeProxyMetric()
560 after_metrics
.Start(results
.current_page
, tab
)
561 tab
.Navigate('http://chromeproxy-test.appspot.com/default')
562 tab
.WaitForJavaScriptExpression('performance.timing.loadEventStart', 10)
563 after_metrics
.Stop(results
.current_page
, tab
)
565 for resp
in after_metrics
.IterResponses(tab
):
566 if not resp
.HasChromeProxyViaHeader():
568 raise ChromeProxyMetricException
, (
569 'Response for %s should have via header; proxy should no longer '
570 'be bypassed.\nReponse: status=(%d, %s)\nHeaders:\n %s' % (
571 r
.url
, r
.status
, r
.status_text
, r
.headers
))
575 raise ChromeProxyMetricException
, (
576 'Expected at least one response through the proxy after the bypass '
577 'expired, but zero such responses were received.')
579 results
.AddValue(scalar
.ScalarValue(
580 results
.current_page
, 'bypass', 'count', bypass_count
))
581 results
.AddValue(scalar
.ScalarValue(
582 results
.current_page
, 'via', 'count', via_count
))