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

Side by Side Diff: tools/chrome_proxy/webdriver/protocol_fuzz.py

Issue 2928863002: Add client protocol fuzzer test (Closed)
Patch Set: 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 unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2017 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.
4 #
5 #
6 # This test checks that Chrome does not crash or otherwise wildly misbehave when
7 # it receives unexpected Chrome-Proxy header values and/or directives. This is
8 # done by fuzz testing which is configured through a dictionary object at the
9 # top of this file.
10 #
11 # The fuzz testing generates URLs which are requested from the Chrome Proxy test
12 # server which will modify its response based on the encoded data in the URL.
13 #
14 # The fuzz testing is configured in a dictionary-type object of the following
15 # format:
16 # {
17 # # This specifies a header that will be fuzzed. Only one header is fuzzed
18 # # per test. When a header is being fuzz tested, any entry it has in the
19 # # STATIC_RESPONSE_HEADERS object will be overwritten.
20 # "chrome-proxy": {
21 # # This specifies the directive key values that are possible to use in
22 # # fuzzing.
23 # "directive_keys: [
24 # "page-policy",
25 # "foo",
26 # ],
27 # # This specifies the directive values that are possible to use in
28 # # fuzzing. Any one of these values may end up mapped to any one of the
29 # # directive_keys above.
30 # "directive_values": [
31 # "empty-image",
32 # "bar",
33 # ],
34 # # The maximum number of directives to use in this header. If this value
35 # # is greater than the number of directives given above, that will be
36 # # used instead.
37 # "max_directives": 10,
38 #
39 # # The maximum number of directive values to use per directive. If this
40 # # value is greater than the number of directive values given above, that
41 # # will be used instead.
42 # "max_directive_values": 10,
43 #
44 # # This is used to join the directive values when multiple are used.
45 # "directive_value_joiner": "|",
46 # },
47 # }
48 #
49 # If the above configuration was given, the following values would be generated
50 # for the chrome-proxy header:
51 # <empty-string>
52 # foo
53 # foo=bar
54 # foo=empty-image
55 # foo=bar|empty-image
56 # page_policies
57 # page_policies=bar
58 # page_policies=empty-image
59 # page_policies=bar|empty-image
60 # foo page_policies
61 # foo=bar page_policies=bar
62 # foo=empty-image page_policies=empty-image
63 # foo=bar|empty-image page_policies=bar|empty-image
Tom Bergan 2017/06/07 22:18:08 Directives should be comma-separated, I think?
Robert Ogden 2017/06/07 23:38:35 Done.
64 #
65 # Randomly generated values are also supported in the fuzz_header field
66 # ("chrome-proxy" in the above example), directive_keys, and directive_values.
67 # Randomly generated values can be specified using these formats:
68 # {{RAND_STR(N)}}
69 # Creates a random string of lowercase and digit characters of length N
70 #
71 # {{RAND_INT(N)}}
72 # Creates a random integer in the range [0, 10^N+1) with leading zeros
73 # Example: "Num cookies: {{RAND_INT(4)}}" yields "Num cookies: 0123"
74 #
75 # {{RAND_DBL(P.Q)}}
76 # Creates a random double in the range [0, 10^P+1) with leading zeros and
77 # up to Q places after the decimal point
78 # Example: "My money = ${{RAND_DBL(3,2)}}" yields "My money = $001.53"
79 #
80 # {{RAND_BOOL}}
81 # Creates a random boolean, either 'true' or 'false'
82 # Example: "I am awesome: {{RAND_BOOL}}" yields "I am awesome: true"
83
84 import BaseHTTPServer
85 import base64
86 import itertools
87 import json
88 import random
89 import re
90 import string
91
92 from common import TestDriver
93 from common import IntegrationTest
94 from decorators import Slow
95
96 # This dict configures how the fuzzing will operate. See documentation above for
97 # more information.
98 FUZZ_HEADERS = {
99 "chrome-proxy": {
100 "directive_keys": [
101 "foo",
102 "{{RAND_STR(10)}}",
103 "page-policies",
104 "q",
105 "block-once",
106 "block",
107 "bypass",
108 ],
109 "directive_values": [
110 "bar",
111 "{{RAND_INT(1)}}",
112 "{{RAND_INT(4)}}",
113 "empty-image",
114 "{{RAND_STR(10)}}",
115 "low",
116 "preview",
117 ],
118 "max_directives": 10,
Tom Bergan 2017/06/07 22:18:08 You can probably shrink this to 3, since most bugs
Robert Ogden 2017/06/07 23:38:36 Done.
119 "max_directive_values": 10,
Tom Bergan 2017/06/07 22:18:08 Ditto here.
Robert Ogden 2017/06/07 23:38:35 Done.
120 "directive_value_joiner": "|",
121 },
122 }
123
124 TEST_SERVER = "chromeproxy-test.appspot.com"
125
126 # These headers will be present in every test server response. If one of these
127 # entries is also a fuzzed header above, then the fuzzed value will take the
128 # place of the static one instead.
129 STATIC_RESPONSE_HEADERS = {
130 "content-type": ["text/html"],
131 "via": ["1.1 Chrome-Compression-Proxy"],
132 "cache-control": ["no-cache, no-store, must-revalidate"],
133 "pragma": ["no-cache"],
134 "expires": ["0"],
135 }
136
137 # This string will be used as the response body in every test and will be
138 # checked for existance on the final loaded page.
139 STATIC_RESPONSE_BODY = 'ok'
140
141 rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}')
142 rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}')
143 rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}')
144 rand_bool_re = re.compile(r'{{RAND_BOOL}}')
145
146 def ParseRand(key, val):
147 """This helper function parses the {{RAND}} expressions in the given values
148 and returns them with random values subsituted in place.
149
150 Args:
151 key: the header key with 0 or more {{RAND}} expressions
152 val: the header value with 0 or more {{RAND}} expressions
153 Returns:
154 A key, value tuple with subsituted random values
155 """
156 def GenerateRand(length, charset):
157 return ''.join(random.choice(charset) for _ in range(length))
158 def _parse_rand(v):
159 result = v
160 had_match = True
161 while had_match:
162 had_match = False
163 str_match = rand_str_re.search(result)
164 if str_match:
165 had_match = True
166 mag = int(result[str_match.start(1):str_match.end(1)])
167 rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits)
168 result = (result[:str_match.start()] + rand_str
169 + result[str_match.end():])
170 int_match = rand_int_re.search(result)
171 if int_match:
172 had_match = True
173 mag = int(result[int_match.start(1):int_match.end(1)])
174 rand_int = GenerateRand(mag, string.digits)
175 result = (result[:int_match.start()] + rand_int
176 + result[int_match.end():])
177 dbl_match = rand_dbl_re.search(result)
178 if dbl_match:
179 had_match = True
180 magN = int(result[dbl_match.start(1):dbl_match.end(1)])
181 magD = int(result[dbl_match.start(2):dbl_match.end(2)])
182 rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD,
183 string.digits)
184 result = (result[:dbl_match.start()] + rand_dbl
185 + result[dbl_match.end():])
186 bool_match = rand_bool_re.search(result)
187 if bool_match:
188 had_match = True
189 rand_bool = bool(random.getrandbits(1))
190 result = (result[:bool_match.start()] + str(rand_bool).lower()
191 + result[bool_match.end():])
192 return result
193 return (_parse_rand(key), _parse_rand(val))
194
195 def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS):
196 """This function yields header key value pairs which can be used to update a
197 Python dict representing HTTP headers. See file level documentation for more
198 information.
199
200 Args:
201 cfg: the configuration dict that specifies how to fuzz the proxy headers
202 Yields:
203 one header key value pair
204 """
205 for header_key in cfg:
206 fuzz = cfg[header_key]
207 dirs = fuzz['directive_keys']
208 vals = fuzz['directive_values']
209 max_dirs = min(fuzz['max_directives'], len(dirs))
210 max_vals = min(fuzz['max_directive_values'], len(vals))
211 def GenerateFuzzedValues():
212 for n in range(0, max_vals + 1):
213 for c in itertools.combinations(vals, n):
214 yield c
215 # Yield an empty header key,value pair before doing all the combinations.
216 yield (header_key, '')
217 for num_dirs in range(1, max_dirs + 1):
218 for directive_set in itertools.combinations(dirs, num_dirs):
219 for values in GenerateFuzzedValues():
220 value_list = list(values)
221 if '' in value_list:
222 value_list.remove('')
223 value_str = fuzz['directive_value_joiner'].join(value_list)
224 header = []
225 for directive in directive_set:
226 if len(value_str) == 0:
227 header.append(directive)
228 else:
229 header.append('%s=%s' % (directive, value_str))
230 yield ParseRand(header_key, ' '.join(header))
231
232 class FuzzUnitTests(IntegrationTest):
233
234 def testParseRand(self):
235 tests = {
236 "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}",
237 "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}",
238 "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}",
239 "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}",
240 "{{RAND_BOOL}": r"{{RAND_BOOL}",
241 "{{RAND_STR(0)}}": "",
242 "hi{{RAND_STR(0)}}": "hi",
243 "{{RAND_STR(0)}}there": "there",
244 "hi{{RAND_STR(0)}}there": "hithere",
245 "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]",
246 "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there",
247 "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]",
248 "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there",
249 "{{RAND_INT(0)}}": "",
250 "hi{{RAND_INT(0)}}": "hi",
251 "{{RAND_INT(0)}}there": "there",
252 "hi{{RAND_INT(0)}}there": "hithere",
253 "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]",
254 "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there",
255 "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]",
256 "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there",
257 "{{RAND_DBL(0.0)}}": r"\.",
258 "hi{{RAND_DBL(0.0)}}": r"hi\.",
259 "{{RAND_DBL(0.0)}}there": r"\.there",
260 "hi{{RAND_DBL(0.0)}}there": r"hi\.there",
261 "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]",
262 "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]",
263 "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there",
264 "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there",
265 "{{RAND_BOOL}}": r"(true|false)",
266 "{{RAND_BOOL}}there": r"(true|false)there",
267 "hi{{RAND_BOOL}}": r"hi(true|false)",
268 "hi{{RAND_BOOL}}there": r"hi(true|false)there",
269 "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]",
270 "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]",
271 "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]",
272 "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)",
273 "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]",
274 "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]",
275 "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]",
276 "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)",
277 "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]",
278 "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]",
279 "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]",
280 "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)",
281 "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]",
282 "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]",
283 "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]",
284 "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)",
285 }
286 for t in tests:
287 expected = re.compile('^' + tests[t] + '$')
288 gotK, gotV = ParseRand(t, t)
289 if not expected.match(gotK):
290 self.fail("%s doesn't match /%s/" % (gotK, tests[t]))
291 if not expected.match(gotV):
292 self.fail("%s doesn't match /%s/" % (gotK, tests[t]))
293
294 def testGenerator(self):
295 test_cfg = {
296 "chrome-proxy": {
297 "directive_keys": [
298 "foo",
299 "page_policies",
300 ],
301 "directive_values": [
302 "bar",
303 "empty-image",
304 ],
305 "max_directives": 10,
306 "max_directive_values": 10,
307 "directive_value_joiner": "|",
308 },
309 }
310 expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image',
311 'foo=bar|empty-image', 'page_policies', 'page_policies=bar',
312 'page_policies=empty-image', 'page_policies=bar|empty-image',
313 'foo page_policies', 'foo=bar page_policies=bar',
314 'foo=empty-image page_policies=empty-image',
315 'foo=bar|empty-image page_policies=bar|empty-image',
316 ]
317 actual_headers = []
318 for h in GenerateFuzzedHeaders(cfg=test_cfg):
319 actual_headers.append(h[1])
320 expected_headers.sort()
321 actual_headers.sort()
322 self.assertEqual(expected_headers, actual_headers)
323
324 class ProtocolFuzzer(IntegrationTest):
325
326 def GenerateTestURLs(self):
327 """This function yields test URLs which will cause the test server to
328 respond with the given given headers and body.
329
330 Yields:
331 URLs suitable for testing fuzzed response headers
332 """
333 for fz_key, fz_val in GenerateFuzzedHeaders():
334 headers = {}
335 headers.update(STATIC_RESPONSE_HEADERS)
336 headers.update({fz_key: [fz_val]})
337 json_headers = json.dumps(headers)
338 b64_headers = base64.b64encode(json_headers)
339 url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER,
340 base64.b64encode(STATIC_RESPONSE_BODY), b64_headers)
341 yield (json_headers, url)
342
343 @Slow
344 def testFuzzing(self):
345 for headers, url in self.GenerateTestURLs():
346 with TestDriver() as t:
347 t.AddChromeArg('--enable-spdy-proxy-auth')
348 t.AddChromeArg('--data-reduction-proxy-http-proxies='
349 'https://chromeproxy-test.appspot.com')
350 try:
351 t.LoadURL(url)
352 # The main test is to make sure Chrome doesn't crash after loading a
353 # page with fuzzed headers, which would be raised as a ChromeDriver
354 # exception. Otherwise, we'll do a simple check and make sure the page
355 # body is correct and Chrome isn't displaying some kind of error page.
356 body = t.ExecuteJavascriptStatement('document.body.innerHTML')
357 self.assertEqual(body, STATIC_RESPONSE_BODY)
358 except Exception as e:
359 print 'Response headers: ' + headers
360 print 'URL: ' + url
361 raise e
362
363 if __name__ == '__main__':
364 IntegrationTest.RunAllTests()
OLDNEW
« 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