Index: tools/chrome_proxy/webdriver/protocol_fuzz.py |
diff --git a/tools/chrome_proxy/webdriver/protocol_fuzz.py b/tools/chrome_proxy/webdriver/protocol_fuzz.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7a28a34c2ba92aabcc1f65dcc9202a6ab7643d03 |
--- /dev/null |
+++ b/tools/chrome_proxy/webdriver/protocol_fuzz.py |
@@ -0,0 +1,359 @@ |
+# Copyright 2017 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+# |
+# |
+# This test checks that Chrome does not crash or otherwise wildly misbehave when |
+# it receives unexpected Chrome-Proxy header values and/or directives. This is |
+# done by fuzz testing which is configured through a dictionary object at the |
+# top of this file. |
+# |
+# The fuzz testing generates URLs which are requested from the Chrome Proxy test |
+# server which will modify its response based on the encoded data in the URL. |
+# |
+# The fuzz testing is configured in a dictionary-type object of the following |
+# format: |
+# { |
+# # This specifies a header that will be fuzzed. Only one header is fuzzed |
+# # per test. When a header is being fuzz tested, any entry it has in the |
+# # STATIC_RESPONSE_HEADERS object will be overwritten. |
+# "chrome-proxy": { |
+# # This specifies the directive key values that are possible to use in |
+# # fuzzing. |
+# "directive_keys: [ |
+# "page-policy", |
+# "foo", |
+# ], |
+# # This specifies the directive values that are possible to use in |
+# # fuzzing. Any one of these values may end up mapped to any one of the |
+# # directive_keys above. |
+# "directive_values": [ |
+# "empty-image", |
+# "bar", |
+# ], |
+# # The maximum number of directives to use in this header. If this value |
+# # is greater than the number of directives given above, that will be |
+# # used instead. |
+# "max_directives": 10, |
+# |
+# # The maximum number of directive values to use per directive. If this |
+# # value is greater than the number of directive values given above, that |
+# # will be used instead. |
+# "max_directive_values": 10, |
+# |
+# # This is used to join the directive values when multiple are used. |
+# "directive_value_joiner": "|", |
+# }, |
+# } |
+# |
+# If the above configuration was given, the following values would be generated |
+# for the chrome-proxy header: |
+# <empty-string> |
+# foo |
+# foo=bar |
+# foo=empty-image |
+# foo=bar|empty-image |
+# page_policies |
+# page_policies=bar |
+# page_policies=empty-image |
+# page_policies=bar|empty-image |
+# foo,page_policies |
+# foo=bar,page_policies=bar |
+# foo=empty-image,page_policies=empty-image |
+# foo=bar|empty-image,page_policies=bar|empty-image |
+# |
+# Randomly generated values are also supported in the fuzz_header field |
+# ("chrome-proxy" in the above example), directive_keys, and directive_values. |
+# Randomly generated values can be specified using these formats: |
+# {{RAND_STR(N)}} |
+# Creates a random string of lowercase and digit characters of length N |
+# |
+# {{RAND_INT(N)}} |
+# Creates a random integer in the range [0, 10^N+1) with leading zeros |
+# Example: "Num cookies: {{RAND_INT(4)}}" yields "Num cookies: 0123" |
+# |
+# {{RAND_DBL(P.Q)}} |
+# Creates a random double in the range [0, 10^P+1) with leading zeros and |
+# up to Q places after the decimal point |
+# Example: "My money = ${{RAND_DBL(3,2)}}" yields "My money = $001.53" |
+# |
+# {{RAND_BOOL}} |
+# Creates a random boolean, either 'true' or 'false' |
+# Example: "I am awesome: {{RAND_BOOL}}" yields "I am awesome: true" |
+ |
+import BaseHTTPServer |
+import base64 |
+import itertools |
+import json |
+import random |
+import re |
+import string |
+ |
+from common import TestDriver |
+from common import IntegrationTest |
+from decorators import Slow |
+ |
+# This dict configures how the fuzzing will operate. See documentation above for |
+# more information. |
+FUZZ_HEADERS = { |
+ "chrome-proxy": { |
+ "directive_keys": [ |
+ "{{RAND_STR(10)}}", |
+ "{{RAND_STR(10)}}", |
Tom Bergan
2017/06/08 15:00:18
Do we actually need two of these?
Robert Ogden
2017/06/08 17:17:58
I was thinking yes so that we get three directives
|
+ "page-policies", |
+ ], |
+ "directive_values": [ |
+ "empty-image", |
+ "{{RAND_INT(1)}}", |
+ "{{RAND_INT(4)}}", |
+ "{{RAND_STR(10)}}", |
+ "{{RAND_STR(10)}}", |
+ "{{RAND_STR(10)}}", |
Tom Bergan
2017/06/08 15:00:18
Ditto here
Robert Ogden
2017/06/08 17:17:58
Yes, so that there are three random strings in at
|
+ ], |
+ "max_directives": 3, |
+ "max_directive_values": 3, |
+ "directive_value_joiner": "|", |
+ }, |
+} |
+ |
+TEST_SERVER = "chromeproxy-test.appspot.com" |
+ |
+# These headers will be present in every test server response. If one of these |
+# entries is also a fuzzed header above, then the fuzzed value will take the |
+# place of the static one instead. |
+STATIC_RESPONSE_HEADERS = { |
+ "content-type": ["text/html"], |
+ "via": ["1.1 Chrome-Compression-Proxy"], |
+ "cache-control": ["no-cache, no-store, must-revalidate"], |
+ "pragma": ["no-cache"], |
+ "expires": ["0"], |
+} |
+ |
+# This string will be used as the response body in every test and will be |
+# checked for existance on the final loaded page. |
+STATIC_RESPONSE_BODY = 'ok' |
+ |
+rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}') |
+rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}') |
+rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}') |
+rand_bool_re = re.compile(r'{{RAND_BOOL}}') |
+ |
+def ParseRand(key, val): |
+ """This helper function parses the {{RAND}} expressions in the given values |
+ and returns them with random values subsituted in place. |
+ |
+ Args: |
+ key: the header key with 0 or more {{RAND}} expressions |
+ val: the header value with 0 or more {{RAND}} expressions |
+ Returns: |
+ A key, value tuple with subsituted random values |
+ """ |
+ def GenerateRand(length, charset): |
+ return ''.join(random.choice(charset) for _ in range(length)) |
+ def _parse_rand(v): |
+ result = v |
+ had_match = True |
+ while had_match: |
+ had_match = False |
+ str_match = rand_str_re.search(result) |
+ if str_match: |
+ had_match = True |
+ mag = int(result[str_match.start(1):str_match.end(1)]) |
+ rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits) |
+ result = (result[:str_match.start()] + rand_str |
+ + result[str_match.end():]) |
+ int_match = rand_int_re.search(result) |
+ if int_match: |
+ had_match = True |
+ mag = int(result[int_match.start(1):int_match.end(1)]) |
+ rand_int = GenerateRand(mag, string.digits) |
+ result = (result[:int_match.start()] + rand_int |
+ + result[int_match.end():]) |
+ dbl_match = rand_dbl_re.search(result) |
+ if dbl_match: |
+ had_match = True |
+ magN = int(result[dbl_match.start(1):dbl_match.end(1)]) |
+ magD = int(result[dbl_match.start(2):dbl_match.end(2)]) |
+ rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD, |
+ string.digits) |
+ result = (result[:dbl_match.start()] + rand_dbl |
+ + result[dbl_match.end():]) |
+ bool_match = rand_bool_re.search(result) |
+ if bool_match: |
+ had_match = True |
+ rand_bool = bool(random.getrandbits(1)) |
+ result = (result[:bool_match.start()] + str(rand_bool).lower() |
+ + result[bool_match.end():]) |
+ return result |
+ return (_parse_rand(key), _parse_rand(val)) |
+ |
+def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS): |
+ """This function yields header key value pairs which can be used to update a |
+ Python dict representing HTTP headers. See file level documentation for more |
+ information. |
+ |
+ Args: |
+ cfg: the configuration dict that specifies how to fuzz the proxy headers |
+ Yields: |
+ one header key value pair |
+ """ |
+ for header_key in cfg: |
+ fuzz = cfg[header_key] |
+ dirs = fuzz['directive_keys'] |
+ vals = fuzz['directive_values'] |
+ max_dirs = min(fuzz['max_directives'], len(dirs)) |
+ max_vals = min(fuzz['max_directive_values'], len(vals)) |
+ def GenerateFuzzedValues(): |
+ for n in range(0, max_vals + 1): |
+ for c in itertools.combinations(vals, n): |
+ yield c |
+ # Yield an empty header key,value pair before doing all the combinations. |
+ yield (header_key, '') |
+ for num_dirs in range(1, max_dirs + 1): |
+ for directive_set in itertools.combinations(dirs, num_dirs): |
+ for values in GenerateFuzzedValues(): |
+ value_list = list(values) |
+ if '' in value_list: |
+ value_list.remove('') |
+ value_str = fuzz['directive_value_joiner'].join(value_list) |
+ header = [] |
+ for directive in directive_set: |
+ if len(value_str) == 0: |
+ header.append(directive) |
+ else: |
+ header.append('%s=%s' % (directive, value_str)) |
+ yield ParseRand(header_key, ','.join(header)) |
+ |
+class FuzzUnitTests(IntegrationTest): |
+ |
+ def testParseRand(self): |
+ tests = { |
+ "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}", |
+ "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}", |
+ "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}", |
+ "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}", |
+ "{{RAND_BOOL}": r"{{RAND_BOOL}", |
+ "{{RAND_STR(0)}}": "", |
+ "hi{{RAND_STR(0)}}": "hi", |
+ "{{RAND_STR(0)}}there": "there", |
+ "hi{{RAND_STR(0)}}there": "hithere", |
+ "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]", |
+ "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there", |
+ "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]", |
+ "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there", |
+ "{{RAND_INT(0)}}": "", |
+ "hi{{RAND_INT(0)}}": "hi", |
+ "{{RAND_INT(0)}}there": "there", |
+ "hi{{RAND_INT(0)}}there": "hithere", |
+ "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]", |
+ "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there", |
+ "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]", |
+ "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there", |
+ "{{RAND_DBL(0.0)}}": r"\.", |
+ "hi{{RAND_DBL(0.0)}}": r"hi\.", |
+ "{{RAND_DBL(0.0)}}there": r"\.there", |
+ "hi{{RAND_DBL(0.0)}}there": r"hi\.there", |
+ "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]", |
+ "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]", |
+ "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", |
+ "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", |
+ "{{RAND_BOOL}}": r"(true|false)", |
+ "{{RAND_BOOL}}there": r"(true|false)there", |
+ "hi{{RAND_BOOL}}": r"hi(true|false)", |
+ "hi{{RAND_BOOL}}there": r"hi(true|false)there", |
+ "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]", |
+ "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]", |
+ "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]", |
+ "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)", |
+ "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]", |
+ "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]", |
+ "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]", |
+ "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)", |
+ "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]", |
+ "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]", |
+ "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]", |
+ "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)", |
+ "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]", |
+ "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]", |
+ "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]", |
+ "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)", |
+ } |
+ for t in tests: |
+ expected = re.compile('^' + tests[t] + '$') |
+ gotK, gotV = ParseRand(t, t) |
+ if not expected.match(gotK): |
+ self.fail("%s doesn't match /%s/" % (gotK, tests[t])) |
+ if not expected.match(gotV): |
+ self.fail("%s doesn't match /%s/" % (gotK, tests[t])) |
+ |
+ def testGenerator(self): |
+ test_cfg = { |
+ "chrome-proxy": { |
+ "directive_keys": [ |
+ "foo", |
+ "page_policies", |
+ ], |
+ "directive_values": [ |
+ "bar", |
+ "empty-image", |
+ ], |
+ "max_directives": 10, |
+ "max_directive_values": 10, |
+ "directive_value_joiner": "|", |
+ }, |
+ } |
+ expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image', |
+ 'foo=bar|empty-image', 'page_policies', 'page_policies=bar', |
+ 'page_policies=empty-image', 'page_policies=bar|empty-image', |
+ 'foo,page_policies', 'foo=bar,page_policies=bar', |
+ 'foo=empty-image,page_policies=empty-image', |
+ 'foo=bar|empty-image,page_policies=bar|empty-image', |
+ ] |
+ actual_headers = [] |
+ for h in GenerateFuzzedHeaders(cfg=test_cfg): |
+ actual_headers.append(h[1]) |
+ expected_headers.sort() |
+ actual_headers.sort() |
+ self.assertEqual(expected_headers, actual_headers) |
+ |
+class ProtocolFuzzer(IntegrationTest): |
+ |
+ def GenerateTestURLs(self): |
+ """This function yields test URLs which will cause the test server to |
+ respond with the given given headers and body. |
+ |
+ Yields: |
+ URLs suitable for testing fuzzed response headers |
+ """ |
+ for fz_key, fz_val in GenerateFuzzedHeaders(): |
+ headers = {} |
+ headers.update(STATIC_RESPONSE_HEADERS) |
+ headers.update({fz_key: [fz_val]}) |
+ json_headers = json.dumps(headers) |
+ b64_headers = base64.b64encode(json_headers) |
+ url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER, |
+ base64.b64encode(STATIC_RESPONSE_BODY), b64_headers) |
+ yield (json_headers, url) |
+ |
+ @Slow |
+ def testFuzzing(self): |
+ with TestDriver() as t: |
+ t.AddChromeArg('--enable-spdy-proxy-auth') |
+ t.AddChromeArg('--data-reduction-proxy-http-proxies=' |
+ 'https://chromeproxy-test.appspot.com') |
+ for headers, url in self.GenerateTestURLs(): |
+ try: |
+ t.LoadURL(url) |
+ # The main test is to make sure Chrome doesn't crash after loading a |
+ # page with fuzzed headers, which would be raised as a ChromeDriver |
+ # exception. Otherwise, we'll do a simple check and make sure the page |
+ # body is correct and Chrome isn't displaying some kind of error page. |
+ body = t.ExecuteJavascriptStatement('document.body.innerHTML') |
+ self.assertEqual(body, STATIC_RESPONSE_BODY) |
+ except Exception as e: |
+ print 'Response headers: ' + headers |
+ print 'URL: ' + url |
+ raise e |
+ |
+if __name__ == '__main__': |
+ IntegrationTest.RunAllTests() |