Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(27)

Unified Diff: tools/chrome_proxy/webdriver/protocol_fuzz.py

Issue 2928863002: Add client protocol fuzzer test (Closed)
Patch Set: tombergan comments Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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()
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698