From: Tommi Virtanen Date: Thu, 13 Oct 2011 20:34:23 +0000 (-0700) Subject: Move fuzzer under s3tests/fuzz. X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=27d2e40b7d324b2a1a67bba9e24abc15b392e23a;p=s3-tests.git Move fuzzer under s3tests/fuzz. This way, its unit tests are separate from s3tests/functional, and s3tests/functional is the thing that actually talks to an S3 implementation over the wire. To actually run the fuzzer, use ./virtualenv/bin/s3tests-fuzz-headers --- diff --git a/s3tests/functional/test_fuzzer.py b/s3tests/functional/test_fuzzer.py deleted file mode 100644 index 717b1db1..00000000 --- a/s3tests/functional/test_fuzzer.py +++ /dev/null @@ -1,390 +0,0 @@ -mport sys -import itertools -import nose -import random -import string -import yaml - -from s3tests.fuzz_headers import * - -from nose.tools import eq_ as eq -from nose.tools import assert_true -from nose.plugins.attrib import attr - -from .utils import assert_raises - -_decision_graph = {} - -def check_access_denied(fn, *args, **kwargs): - e = assert_raises(boto.exception.S3ResponseError, fn, *args, **kwargs) - eq(e.status, 403) - eq(e.reason, 'Forbidden') - eq(e.error_code, 'AccessDenied') - - -def build_graph(): - graph = {} - graph['start'] = { - 'set': {}, - 'choices': ['node2'] - } - graph['leaf'] = { - 'set': { - 'key1': 'value1', - 'key2': 'value2' - }, - 'headers': [ - ['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}'] - ], - 'choices': [] - } - graph['node1'] = { - 'set': { - 'key3': 'value3', - 'header_val': [ - '3 h1', - '2 h2', - 'h3' - ] - }, - 'headers': [ - ['1-1', 'my-header', '{header_val}'], - ], - 'choices': ['leaf'] - } - graph['node2'] = { - 'set': { - 'randkey': 'value-{random 10-15 printable}', - 'path': '/{bucket_readable}', - 'indirect_key1': '{key1}' - }, - 'choices': ['leaf'] - } - graph['bad_node'] = { - 'set': { - 'key1': 'value1' - }, - 'choices': ['leaf'] - } - graph['nonexistant_child_node'] = { - 'set': {}, - 'choices': ['leafy_greens'] - } - graph['weighted_node'] = { - 'set': { - 'k1': [ - 'foo', - '2 bar', - '1 baz' - ] - }, - 'choices': [ - 'foo', - '2 bar', - '1 baz' - ] - } - graph['null_choice_node'] = { - 'set': {}, - 'choices': [None] - } - graph['repeated_headers_node'] = { - 'set': {}, - 'headers': [ - ['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}'] - ], - 'choices': ['leaf'] - } - graph['weighted_null_choice_node'] = { - 'set': {}, - 'choices': ['3 null'] - } - return graph - - -#def test_foo(): - #graph_file = open('request_decision_graph.yml', 'r') - #graph = yaml.safe_load(graph_file) - #eq(graph['bucket_put_simple']['set']['grantee'], 0) - - -def test_load_graph(): - graph_file = open('request_decision_graph.yml', 'r') - graph = yaml.safe_load(graph_file) - graph['start'] - - -def test_descend_leaf_node(): - graph = build_graph() - prng = random.Random(1) - decision = descend_graph(graph, 'leaf', prng) - - eq(decision['key1'], 'value1') - eq(decision['key2'], 'value2') - e = assert_raises(KeyError, lambda x: decision[x], 'key3') - - -def test_descend_node(): - graph = build_graph() - prng = random.Random(1) - decision = descend_graph(graph, 'node1', prng) - - eq(decision['key1'], 'value1') - eq(decision['key2'], 'value2') - eq(decision['key3'], 'value3') - - -def test_descend_bad_node(): - graph = build_graph() - prng = random.Random(1) - assert_raises(DecisionGraphError, descend_graph, graph, 'bad_node', prng) - - -def test_descend_nonexistant_child(): - graph = build_graph() - prng = random.Random(1) - assert_raises(KeyError, descend_graph, graph, 'nonexistant_child_node', prng) - - -def test_expand_random_printable(): - prng = random.Random(1) - got = expand({}, '{random 10-15 printable}', prng) - eq(got, '[/pNI$;92@') - - -def test_expand_random_binary(): - prng = random.Random(1) - got = expand({}, '{random 10-15 binary}', prng) - eq(got, '\xdfj\xf1\xd80>a\xcd\xc4\xbb') - - -def test_expand_random_printable_no_whitespace(): - prng = random.Random(1) - for _ in xrange(1000): - got = expand({}, '{random 500 printable_no_whitespace}', prng) - assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace and x in string.printable for x in got])) - - -def test_expand_random_binary(): - prng = random.Random(1) - for _ in xrange(1000): - got = expand({}, '{random 500 binary_no_whitespace}', prng) - assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace for x in got])) - - -def test_expand_random_no_args(): - prng = random.Random(1) - for _ in xrange(1000): - got = expand({}, '{random}', prng) - assert_true(0 <= len(got) <= 1000) - assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got])) - - -def test_expand_random_no_charset(): - prng = random.Random(1) - for _ in xrange(1000): - got = expand({}, '{random 10-30}', prng) - assert_true(10 <= len(got) <= 30) - assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got])) - - -def test_expand_random_exact_length(): - prng = random.Random(1) - for _ in xrange(1000): - got = expand({}, '{random 10 digits}', prng) - assert_true(len(got) == 10) - assert_true(reduce(lambda x, y: x and y, [x in string.digits for x in got])) - - -def test_expand_random_bad_charset(): - prng = random.Random(1) - assert_raises(KeyError, expand, {}, '{random 10-30 foo}', prng) - - -def test_expand_random_missing_length(): - prng = random.Random(1) - assert_raises(ValueError, expand, {}, '{random printable}', prng) - - -def test_assemble_decision(): - graph = build_graph() - prng = random.Random(1) - decision = assemble_decision(graph, prng) - - eq(decision['key1'], 'value1') - eq(decision['key2'], 'value2') - eq(decision['randkey'], 'value-{random 10-15 printable}') - eq(decision['indirect_key1'], '{key1}') - eq(decision['path'], '/{bucket_readable}') - assert_raises(KeyError, lambda x: decision[x], 'key3') - - -def test_expand_escape(): - prng = random.Random(1) - decision = dict( - foo='{{bar}}', - ) - got = expand(decision, '{foo}', prng) - eq(got, '{bar}') - - -def test_expand_indirect(): - prng = random.Random(1) - decision = dict( - foo='{bar}', - bar='quux', - ) - got = expand(decision, '{foo}', prng) - eq(got, 'quux') - - -def test_expand_indirect_double(): - prng = random.Random(1) - decision = dict( - foo='{bar}', - bar='{quux}', - quux='thud', - ) - got = expand(decision, '{foo}', prng) - eq(got, 'thud') - - -def test_expand_recursive(): - prng = random.Random(1) - decision = dict( - foo='{foo}', - ) - e = assert_raises(RecursionError, expand, decision, '{foo}', prng) - eq(str(e), "Runaway recursion in string formatting: 'foo'") - - -def test_expand_recursive_mutual(): - prng = random.Random(1) - decision = dict( - foo='{bar}', - bar='{foo}', - ) - e = assert_raises(RecursionError, expand, decision, '{foo}', prng) - eq(str(e), "Runaway recursion in string formatting: 'foo'") - - -def test_expand_recursive_not_too_eager(): - prng = random.Random(1) - decision = dict( - foo='bar', - ) - got = expand(decision, 100*'{foo}', prng) - eq(got, 100*'bar') - - -def test_make_choice_unweighted_with_space(): - prng = random.Random(1) - choice = make_choice(['foo bar'], prng) - eq(choice, 'foo bar') - -def test_weighted_choices(): - graph = build_graph() - prng = random.Random(1) - - choices_made = {} - for _ in xrange(1000): - choice = make_choice(graph['weighted_node']['choices'], prng) - if choices_made.has_key(choice): - choices_made[choice] += 1 - else: - choices_made[choice] = 1 - - foo_percentage = choices_made['foo'] / 1000.0 - bar_percentage = choices_made['bar'] / 1000.0 - baz_percentage = choices_made['baz'] / 1000.0 - nose.tools.assert_almost_equal(foo_percentage, 0.25, 1) - nose.tools.assert_almost_equal(bar_percentage, 0.50, 1) - nose.tools.assert_almost_equal(baz_percentage, 0.25, 1) - - -def test_null_choices(): - graph = build_graph() - prng = random.Random(1) - choice = make_choice(graph['null_choice_node']['choices'], prng) - - eq(choice, '') - - -def test_weighted_null_choices(): - graph = build_graph() - prng = random.Random(1) - choice = make_choice(graph['weighted_null_choice_node']['choices'], prng) - - eq(choice, '') - - -def test_null_child(): - graph = build_graph() - prng = random.Random(1) - decision = descend_graph(graph, 'null_choice_node', prng) - - eq(decision, {}) - - -def test_weighted_set(): - graph = build_graph() - prng = random.Random(1) - - choices_made = {} - for _ in xrange(1000): - choice = make_choice(graph['weighted_node']['set']['k1'], prng) - if choices_made.has_key(choice): - choices_made[choice] += 1 - else: - choices_made[choice] = 1 - - foo_percentage = choices_made['foo'] / 1000.0 - bar_percentage = choices_made['bar'] / 1000.0 - baz_percentage = choices_made['baz'] / 1000.0 - nose.tools.assert_almost_equal(foo_percentage, 0.25, 1) - nose.tools.assert_almost_equal(bar_percentage, 0.50, 1) - nose.tools.assert_almost_equal(baz_percentage, 0.25, 1) - - -def test_header_presence(): - graph = build_graph() - prng = random.Random(1) - decision = descend_graph(graph, 'node1', prng) - - c1 = itertools.count() - c2 = itertools.count() - for header, value in decision['headers']: - if header == 'my-header': - eq(value, '{header_val}') - assert_true(next(c1) < 1) - elif header == 'random-header-{random 5-10 printable}': - eq(value, '{random 20-30 punctuation}') - assert_true(next(c2) < 2) - else: - raise KeyError('unexpected header found: %s' % header) - - assert_true(next(c1)) - assert_true(next(c2)) - - -def test_duplicate_header(): - graph = build_graph() - prng = random.Random(1) - assert_raises(DecisionGraphError, descend_graph, graph, 'repeated_headers_node', prng) - - -def test_expand_headers(): - graph = build_graph() - prng = random.Random(1) - decision = descend_graph(graph, 'node1', prng) - expanded_headers = expand_headers(decision, prng) - - for header, value in expanded_headers.iteritems(): - if header == 'my-header': - assert_true(value in ['h1', 'h2', 'h3']) - elif header.startswith('random-header-'): - assert_true(20 <= len(value) <= 30) - assert_true(string.strip(value, RepeatExpandingFormatter.charsets['punctuation']) is '') - else: - raise DecisionGraphError('unexpected header found: "%s"' % header) - diff --git a/s3tests/fuzz/__init__.py b/s3tests/fuzz/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/s3tests/fuzz/headers.py b/s3tests/fuzz/headers.py new file mode 100644 index 00000000..a4919283 --- /dev/null +++ b/s3tests/fuzz/headers.py @@ -0,0 +1,376 @@ +from boto.s3.connection import S3Connection +from boto.exception import BotoServerError +from boto.s3.key import Key +from httplib import BadStatusLine +from optparse import OptionParser +from .. import common + +import traceback +import itertools +import random +import string +import struct +import yaml +import sys +import re + + +class DecisionGraphError(Exception): + """ Raised when a node in a graph tries to set a header or + key that was previously set by another node + """ + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) + + +class RecursionError(Exception): + """Runaway recursion in string formatting""" + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return '{0.__doc__}: {0.msg!r}'.format(self) + + +def assemble_decision(decision_graph, prng): + """ Take in a graph describing the possible decision space and a random + number generator and traverse the graph to build a decision + """ + return descend_graph(decision_graph, 'start', prng) + + +def descend_graph(decision_graph, node_name, prng): + """ Given a graph and a particular node in that graph, set the values in + the node's "set" list, pick a choice from the "choice" list, and + recurse. Finally, return dictionary of values + """ + node = decision_graph[node_name] + + try: + choice = make_choice(node['choices'], prng) + if choice == '': + decision = {} + else: + decision = descend_graph(decision_graph, choice, prng) + except IndexError: + decision = {} + + for key, choices in node['set'].iteritems(): + if key in decision: + raise DecisionGraphError("Node %s tried to set '%s', but that key was already set by a lower node!" %(node_name, key)) + decision[key] = make_choice(choices, prng) + + if 'headers' in node: + decision.setdefault('headers', []) + + for desc in node['headers']: + try: + (repetition_range, header, value) = desc + except ValueError: + (header, value) = desc + repetition_range = '1' + + try: + size_min, size_max = repetition_range.split('-', 1) + except ValueError: + size_min = size_max = repetition_range + + size_min = int(size_min) + size_max = int(size_max) + + num_reps = prng.randint(size_min, size_max) + if header in [h for h, v in decision['headers']]: + raise DecisionGraphError("Node %s tried to add header '%s', but that header already exists!" %(node_name, header)) + for _ in xrange(num_reps): + decision['headers'].append([header, value]) + + return decision + + +def make_choice(choices, prng): + """ Given a list of (possibly weighted) options or just a single option!, + choose one of the options taking weights into account and return the + choice + """ + if isinstance(choices, str): + return choices + weighted_choices = [] + for option in choices: + if option is None: + weighted_choices.append('') + continue + try: + (weight, value) = option.split(None, 1) + weight = int(weight) + except ValueError: + weight = 1 + value = option + + if value == 'null' or value == 'None': + value = '' + + for _ in xrange(weight): + weighted_choices.append(value) + + return prng.choice(weighted_choices) + + +def expand_headers(decision, prng): + expanded_headers = {} + for header in decision['headers']: + h = expand(decision, header[0], prng) + v = expand(decision, header[1], prng) + expanded_headers[h] = v + return expanded_headers + + +def expand(decision, value, prng): + c = itertools.count() + fmt = RepeatExpandingFormatter(prng) + new = fmt.vformat(value, [], decision) + return new + + +class RepeatExpandingFormatter(string.Formatter): + charsets = { + 'printable_no_whitespace': string.printable.translate(None, string.whitespace), + 'printable': string.printable, + 'punctuation': string.punctuation, + 'whitespace': string.whitespace, + 'digits': string.digits + } + + def __init__(self, prng, _recursion=0): + super(RepeatExpandingFormatter, self).__init__() + # this class assumes it is always instantiated once per + # formatting; use that to detect runaway recursion + self.prng = prng + self._recursion = _recursion + + def get_value(self, key, args, kwargs): + fields = key.split(None, 1) + fn = getattr(self, 'special_{name}'.format(name=fields[0]), None) + if fn is not None: + if len(fields) == 1: + fields.append('') + return fn(fields[1]) + + val = super(RepeatExpandingFormatter, self).get_value(key, args, kwargs) + if self._recursion > 5: + raise RecursionError(key) + fmt = self.__class__(self.prng, _recursion=self._recursion+1) + + n = fmt.vformat(val, args, kwargs) + return n + + def special_random(self, args): + arg_list = args.split() + try: + size_min, size_max = arg_list[0].split('-', 1) + except ValueError: + size_min = size_max = arg_list[0] + except IndexError: + size_min = '0' + size_max = '1000' + + size_min = int(size_min) + size_max = int(size_max) + length = self.prng.randint(size_min, size_max) + + try: + charset_arg = arg_list[1] + except IndexError: + charset_arg = 'printable' + + if charset_arg == 'binary' or charset_arg == 'binary_no_whitespace': + num_bytes = length + 8 + tmplist = [self.prng.getrandbits(64) for _ in xrange(num_bytes / 8)] + tmpstring = struct.pack((num_bytes / 8) * 'Q', *tmplist) + if charset_arg == 'binary_no_whitespace': + tmpstring = ''.join(c for c in tmpstring if c not in string.whitespace) + return tmpstring[0:length] + else: + charset = self.charsets[charset_arg] + return ''.join([self.prng.choice(charset) for _ in xrange(length)]) # Won't scale nicely + + +def parse_options(): + parser = OptionParser() + parser.add_option('-O', '--outfile', help='write output to FILE. Defaults to STDOUT', metavar='FILE') + parser.add_option('--seed', dest='seed', type='int', help='initial seed for the random number generator') + parser.add_option('--seed-file', dest='seedfile', help='read seeds for specific requests from FILE', metavar='FILE') + parser.add_option('-n', dest='num_requests', type='int', help='issue NUM requests before stopping', metavar='NUM') + parser.add_option('-v', '--verbose', dest='verbose', action="store_true", help='turn on verbose output') + parser.add_option('-d', '--debug', dest='debug', action="store_true", help='turn on debugging (very verbose) output') + parser.add_option('--decision-graph', dest='graph_filename', help='file in which to find the request decision graph') + parser.add_option('--no-cleanup', dest='cleanup', action="store_false", help='turn off teardown so you can peruse the state of buckets after testing') + + parser.set_defaults(num_requests=5) + parser.set_defaults(cleanup=True) + parser.set_defaults(graph_filename='request_decision_graph.yml') + return parser.parse_args() + + +def randomlist(seed=None): + """ Returns an infinite generator of random numbers + """ + rng = random.Random(seed) + while True: + yield rng.randint(0,100000) #100,000 seeds is enough, right? + + +def populate_buckets(conn, alt): + """ Creates buckets and keys for fuzz testing and sets appropriate + permissions. Returns a dictionary of the bucket and key names. + """ + breadable = common.get_new_bucket(alt) + bwritable = common.get_new_bucket(alt) + bnonreadable = common.get_new_bucket(alt) + + oreadable = Key(breadable) + owritable = Key(bwritable) + ononreadable = Key(breadable) + oreadable.set_contents_from_string('oreadable body') + owritable.set_contents_from_string('owritable body') + ononreadable.set_contents_from_string('ononreadable body') + + breadable.set_acl('public-read') + bwritable.set_acl('public-read-write') + bnonreadable.set_acl('private') + oreadable.set_acl('public-read') + owritable.set_acl('public-read-write') + ononreadable.set_acl('private') + + return dict( + bucket_readable=breadable.name, + bucket_writable=bwritable.name, + bucket_not_readable=bnonreadable.name, + bucket_not_writable=breadable.name, + object_readable=oreadable.key, + object_writable=owritable.key, + object_not_readable=ononreadable.key, + object_not_writable=oreadable.key, + ) + + +def _main(): + """ The main script + """ + (options, args) = parse_options() + random.seed(options.seed if options.seed else None) + s3_connection = common.s3.main + alt_connection = common.s3.alt + + if options.outfile: + OUT = open(options.outfile, 'w') + else: + OUT = sys.stderr + + VERBOSE = DEBUG = open('/dev/null', 'w') + if options.verbose: + VERBOSE = OUT + if options.debug: + DEBUG = OUT + VERBOSE = OUT + + request_seeds = None + if options.seedfile: + FH = open(options.seedfile, 'r') + request_seeds = [int(line) for line in FH if line != '\n'] + print>>OUT, 'Seedfile: %s' %options.seedfile + print>>OUT, 'Number of requests: %d' %len(request_seeds) + else: + if options.seed: + print>>OUT, 'Initial Seed: %d' %options.seed + print>>OUT, 'Number of requests: %d' %options.num_requests + random_list = randomlist(options.seed) + request_seeds = itertools.islice(random_list, options.num_requests) + + print>>OUT, 'Decision Graph: %s' %options.graph_filename + + graph_file = open(options.graph_filename, 'r') + decision_graph = yaml.safe_load(graph_file) + + constants = populate_buckets(s3_connection, alt_connection) + print>>VERBOSE, "Test Buckets/Objects:" + for key, value in constants.iteritems(): + print>>VERBOSE, "\t%s: %s" %(key, value) + + print>>OUT, "Begin Fuzzing..." + print>>VERBOSE, '='*80 + for request_seed in request_seeds: + print>>VERBOSE, 'Seed is: %r' %request_seed + prng = random.Random(request_seed) + decision = assemble_decision(decision_graph, prng) + decision.update(constants) + + method = expand(decision, decision['method'], prng) + path = expand(decision, decision['urlpath'], prng) + + try: + body = expand(decision, decision['body'], prng) + except KeyError: + body = '' + + try: + headers = expand_headers(decision, prng) + except KeyError: + headers = {} + + print>>VERBOSE, "%r %r" %(method[:100], path[:100]) + for h, v in headers.iteritems(): + print>>VERBOSE, "%r: %r" %(h[:50], v[:50]) + print>>VERBOSE, "%r\n" % body[:100] + + print>>DEBUG, 'FULL REQUEST' + print>>DEBUG, 'Method: %r' %method + print>>DEBUG, 'Path: %r' %path + print>>DEBUG, 'Headers:' + for h, v in headers.iteritems(): + print>>DEBUG, "\t%r: %r" %(h, v) + print>>DEBUG, 'Body: %r\n' %body + + failed = False # Let's be optimistic, shall we? + try: + response = s3_connection.make_request(method, path, data=body, headers=headers, override_num_retries=1) + body = response.read() + except BotoServerError, e: + response = e + body = e.body + failed = True + except BadStatusLine, e: + print>>OUT, 'FAILED: failed to parse response (BadStatusLine); probably a NUL byte in your request?' + print>>VERBOSE, '='*80 + continue + + if failed: + print>>OUT, 'FAILED:' + OLD_VERBOSE = VERBOSE + OLD_DEBUG = DEBUG + VERBOSE = DEBUG = OUT + print>>VERBOSE, 'Seed was: %r' %request_seed + print>>VERBOSE, 'Response status code: %d %s' %(response.status, response.reason) + print>>DEBUG, 'Body:\n%s' %body + print>>VERBOSE, '='*80 + if failed: + VERBOSE = OLD_VERBOSE + DEBUG = OLD_DEBUG + + print>>OUT, '...done fuzzing' + + if options.cleanup: + common.teardown() + + +def main(): + common.setup() + try: + _main() + except Exception as e: + traceback.print_exc() + common.teardown() + diff --git a/s3tests/fuzz/test/__init__.py b/s3tests/fuzz/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/s3tests/fuzz/test/test_fuzzer.py b/s3tests/fuzz/test/test_fuzzer.py new file mode 100644 index 00000000..fc408005 --- /dev/null +++ b/s3tests/fuzz/test/test_fuzzer.py @@ -0,0 +1,390 @@ +import sys +import itertools +import nose +import random +import string +import yaml + +from ..headers import * + +from nose.tools import eq_ as eq +from nose.tools import assert_true +from nose.plugins.attrib import attr + +from ...functional.utils import assert_raises + +_decision_graph = {} + +def check_access_denied(fn, *args, **kwargs): + e = assert_raises(boto.exception.S3ResponseError, fn, *args, **kwargs) + eq(e.status, 403) + eq(e.reason, 'Forbidden') + eq(e.error_code, 'AccessDenied') + + +def build_graph(): + graph = {} + graph['start'] = { + 'set': {}, + 'choices': ['node2'] + } + graph['leaf'] = { + 'set': { + 'key1': 'value1', + 'key2': 'value2' + }, + 'headers': [ + ['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}'] + ], + 'choices': [] + } + graph['node1'] = { + 'set': { + 'key3': 'value3', + 'header_val': [ + '3 h1', + '2 h2', + 'h3' + ] + }, + 'headers': [ + ['1-1', 'my-header', '{header_val}'], + ], + 'choices': ['leaf'] + } + graph['node2'] = { + 'set': { + 'randkey': 'value-{random 10-15 printable}', + 'path': '/{bucket_readable}', + 'indirect_key1': '{key1}' + }, + 'choices': ['leaf'] + } + graph['bad_node'] = { + 'set': { + 'key1': 'value1' + }, + 'choices': ['leaf'] + } + graph['nonexistant_child_node'] = { + 'set': {}, + 'choices': ['leafy_greens'] + } + graph['weighted_node'] = { + 'set': { + 'k1': [ + 'foo', + '2 bar', + '1 baz' + ] + }, + 'choices': [ + 'foo', + '2 bar', + '1 baz' + ] + } + graph['null_choice_node'] = { + 'set': {}, + 'choices': [None] + } + graph['repeated_headers_node'] = { + 'set': {}, + 'headers': [ + ['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}'] + ], + 'choices': ['leaf'] + } + graph['weighted_null_choice_node'] = { + 'set': {}, + 'choices': ['3 null'] + } + return graph + + +#def test_foo(): + #graph_file = open('request_decision_graph.yml', 'r') + #graph = yaml.safe_load(graph_file) + #eq(graph['bucket_put_simple']['set']['grantee'], 0) + + +def test_load_graph(): + graph_file = open('request_decision_graph.yml', 'r') + graph = yaml.safe_load(graph_file) + graph['start'] + + +def test_descend_leaf_node(): + graph = build_graph() + prng = random.Random(1) + decision = descend_graph(graph, 'leaf', prng) + + eq(decision['key1'], 'value1') + eq(decision['key2'], 'value2') + e = assert_raises(KeyError, lambda x: decision[x], 'key3') + + +def test_descend_node(): + graph = build_graph() + prng = random.Random(1) + decision = descend_graph(graph, 'node1', prng) + + eq(decision['key1'], 'value1') + eq(decision['key2'], 'value2') + eq(decision['key3'], 'value3') + + +def test_descend_bad_node(): + graph = build_graph() + prng = random.Random(1) + assert_raises(DecisionGraphError, descend_graph, graph, 'bad_node', prng) + + +def test_descend_nonexistant_child(): + graph = build_graph() + prng = random.Random(1) + assert_raises(KeyError, descend_graph, graph, 'nonexistant_child_node', prng) + + +def test_expand_random_printable(): + prng = random.Random(1) + got = expand({}, '{random 10-15 printable}', prng) + eq(got, '[/pNI$;92@') + + +def test_expand_random_binary(): + prng = random.Random(1) + got = expand({}, '{random 10-15 binary}', prng) + eq(got, '\xdfj\xf1\xd80>a\xcd\xc4\xbb') + + +def test_expand_random_printable_no_whitespace(): + prng = random.Random(1) + for _ in xrange(1000): + got = expand({}, '{random 500 printable_no_whitespace}', prng) + assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace and x in string.printable for x in got])) + + +def test_expand_random_binary(): + prng = random.Random(1) + for _ in xrange(1000): + got = expand({}, '{random 500 binary_no_whitespace}', prng) + assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace for x in got])) + + +def test_expand_random_no_args(): + prng = random.Random(1) + for _ in xrange(1000): + got = expand({}, '{random}', prng) + assert_true(0 <= len(got) <= 1000) + assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got])) + + +def test_expand_random_no_charset(): + prng = random.Random(1) + for _ in xrange(1000): + got = expand({}, '{random 10-30}', prng) + assert_true(10 <= len(got) <= 30) + assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got])) + + +def test_expand_random_exact_length(): + prng = random.Random(1) + for _ in xrange(1000): + got = expand({}, '{random 10 digits}', prng) + assert_true(len(got) == 10) + assert_true(reduce(lambda x, y: x and y, [x in string.digits for x in got])) + + +def test_expand_random_bad_charset(): + prng = random.Random(1) + assert_raises(KeyError, expand, {}, '{random 10-30 foo}', prng) + + +def test_expand_random_missing_length(): + prng = random.Random(1) + assert_raises(ValueError, expand, {}, '{random printable}', prng) + + +def test_assemble_decision(): + graph = build_graph() + prng = random.Random(1) + decision = assemble_decision(graph, prng) + + eq(decision['key1'], 'value1') + eq(decision['key2'], 'value2') + eq(decision['randkey'], 'value-{random 10-15 printable}') + eq(decision['indirect_key1'], '{key1}') + eq(decision['path'], '/{bucket_readable}') + assert_raises(KeyError, lambda x: decision[x], 'key3') + + +def test_expand_escape(): + prng = random.Random(1) + decision = dict( + foo='{{bar}}', + ) + got = expand(decision, '{foo}', prng) + eq(got, '{bar}') + + +def test_expand_indirect(): + prng = random.Random(1) + decision = dict( + foo='{bar}', + bar='quux', + ) + got = expand(decision, '{foo}', prng) + eq(got, 'quux') + + +def test_expand_indirect_double(): + prng = random.Random(1) + decision = dict( + foo='{bar}', + bar='{quux}', + quux='thud', + ) + got = expand(decision, '{foo}', prng) + eq(got, 'thud') + + +def test_expand_recursive(): + prng = random.Random(1) + decision = dict( + foo='{foo}', + ) + e = assert_raises(RecursionError, expand, decision, '{foo}', prng) + eq(str(e), "Runaway recursion in string formatting: 'foo'") + + +def test_expand_recursive_mutual(): + prng = random.Random(1) + decision = dict( + foo='{bar}', + bar='{foo}', + ) + e = assert_raises(RecursionError, expand, decision, '{foo}', prng) + eq(str(e), "Runaway recursion in string formatting: 'foo'") + + +def test_expand_recursive_not_too_eager(): + prng = random.Random(1) + decision = dict( + foo='bar', + ) + got = expand(decision, 100*'{foo}', prng) + eq(got, 100*'bar') + + +def test_make_choice_unweighted_with_space(): + prng = random.Random(1) + choice = make_choice(['foo bar'], prng) + eq(choice, 'foo bar') + +def test_weighted_choices(): + graph = build_graph() + prng = random.Random(1) + + choices_made = {} + for _ in xrange(1000): + choice = make_choice(graph['weighted_node']['choices'], prng) + if choices_made.has_key(choice): + choices_made[choice] += 1 + else: + choices_made[choice] = 1 + + foo_percentage = choices_made['foo'] / 1000.0 + bar_percentage = choices_made['bar'] / 1000.0 + baz_percentage = choices_made['baz'] / 1000.0 + nose.tools.assert_almost_equal(foo_percentage, 0.25, 1) + nose.tools.assert_almost_equal(bar_percentage, 0.50, 1) + nose.tools.assert_almost_equal(baz_percentage, 0.25, 1) + + +def test_null_choices(): + graph = build_graph() + prng = random.Random(1) + choice = make_choice(graph['null_choice_node']['choices'], prng) + + eq(choice, '') + + +def test_weighted_null_choices(): + graph = build_graph() + prng = random.Random(1) + choice = make_choice(graph['weighted_null_choice_node']['choices'], prng) + + eq(choice, '') + + +def test_null_child(): + graph = build_graph() + prng = random.Random(1) + decision = descend_graph(graph, 'null_choice_node', prng) + + eq(decision, {}) + + +def test_weighted_set(): + graph = build_graph() + prng = random.Random(1) + + choices_made = {} + for _ in xrange(1000): + choice = make_choice(graph['weighted_node']['set']['k1'], prng) + if choices_made.has_key(choice): + choices_made[choice] += 1 + else: + choices_made[choice] = 1 + + foo_percentage = choices_made['foo'] / 1000.0 + bar_percentage = choices_made['bar'] / 1000.0 + baz_percentage = choices_made['baz'] / 1000.0 + nose.tools.assert_almost_equal(foo_percentage, 0.25, 1) + nose.tools.assert_almost_equal(bar_percentage, 0.50, 1) + nose.tools.assert_almost_equal(baz_percentage, 0.25, 1) + + +def test_header_presence(): + graph = build_graph() + prng = random.Random(1) + decision = descend_graph(graph, 'node1', prng) + + c1 = itertools.count() + c2 = itertools.count() + for header, value in decision['headers']: + if header == 'my-header': + eq(value, '{header_val}') + assert_true(next(c1) < 1) + elif header == 'random-header-{random 5-10 printable}': + eq(value, '{random 20-30 punctuation}') + assert_true(next(c2) < 2) + else: + raise KeyError('unexpected header found: %s' % header) + + assert_true(next(c1)) + assert_true(next(c2)) + + +def test_duplicate_header(): + graph = build_graph() + prng = random.Random(1) + assert_raises(DecisionGraphError, descend_graph, graph, 'repeated_headers_node', prng) + + +def test_expand_headers(): + graph = build_graph() + prng = random.Random(1) + decision = descend_graph(graph, 'node1', prng) + expanded_headers = expand_headers(decision, prng) + + for header, value in expanded_headers.iteritems(): + if header == 'my-header': + assert_true(value in ['h1', 'h2', 'h3']) + elif header.startswith('random-header-'): + assert_true(20 <= len(value) <= 30) + assert_true(string.strip(value, RepeatExpandingFormatter.charsets['punctuation']) is '') + else: + raise DecisionGraphError('unexpected header found: "%s"' % header) + diff --git a/s3tests/fuzz_headers.py b/s3tests/fuzz_headers.py deleted file mode 100644 index e49713f6..00000000 --- a/s3tests/fuzz_headers.py +++ /dev/null @@ -1,376 +0,0 @@ -from boto.s3.connection import S3Connection -from boto.exception import BotoServerError -from boto.s3.key import Key -from httplib import BadStatusLine -from optparse import OptionParser -from . import common - -import traceback -import itertools -import random -import string -import struct -import yaml -import sys -import re - - -class DecisionGraphError(Exception): - """ Raised when a node in a graph tries to set a header or - key that was previously set by another node - """ - def __init__(self, value): - self.value = value - - def __str__(self): - return repr(self.value) - - -class RecursionError(Exception): - """Runaway recursion in string formatting""" - - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return '{0.__doc__}: {0.msg!r}'.format(self) - - -def assemble_decision(decision_graph, prng): - """ Take in a graph describing the possible decision space and a random - number generator and traverse the graph to build a decision - """ - return descend_graph(decision_graph, 'start', prng) - - -def descend_graph(decision_graph, node_name, prng): - """ Given a graph and a particular node in that graph, set the values in - the node's "set" list, pick a choice from the "choice" list, and - recurse. Finally, return dictionary of values - """ - node = decision_graph[node_name] - - try: - choice = make_choice(node['choices'], prng) - if choice == '': - decision = {} - else: - decision = descend_graph(decision_graph, choice, prng) - except IndexError: - decision = {} - - for key, choices in node['set'].iteritems(): - if key in decision: - raise DecisionGraphError("Node %s tried to set '%s', but that key was already set by a lower node!" %(node_name, key)) - decision[key] = make_choice(choices, prng) - - if 'headers' in node: - decision.setdefault('headers', []) - - for desc in node['headers']: - try: - (repetition_range, header, value) = desc - except ValueError: - (header, value) = desc - repetition_range = '1' - - try: - size_min, size_max = repetition_range.split('-', 1) - except ValueError: - size_min = size_max = repetition_range - - size_min = int(size_min) - size_max = int(size_max) - - num_reps = prng.randint(size_min, size_max) - if header in [h for h, v in decision['headers']]: - raise DecisionGraphError("Node %s tried to add header '%s', but that header already exists!" %(node_name, header)) - for _ in xrange(num_reps): - decision['headers'].append([header, value]) - - return decision - - -def make_choice(choices, prng): - """ Given a list of (possibly weighted) options or just a single option!, - choose one of the options taking weights into account and return the - choice - """ - if isinstance(choices, str): - return choices - weighted_choices = [] - for option in choices: - if option is None: - weighted_choices.append('') - continue - try: - (weight, value) = option.split(None, 1) - weight = int(weight) - except ValueError: - weight = 1 - value = option - - if value == 'null' or value == 'None': - value = '' - - for _ in xrange(weight): - weighted_choices.append(value) - - return prng.choice(weighted_choices) - - -def expand_headers(decision, prng): - expanded_headers = {} - for header in decision['headers']: - h = expand(decision, header[0], prng) - v = expand(decision, header[1], prng) - expanded_headers[h] = v - return expanded_headers - - -def expand(decision, value, prng): - c = itertools.count() - fmt = RepeatExpandingFormatter(prng) - new = fmt.vformat(value, [], decision) - return new - - -class RepeatExpandingFormatter(string.Formatter): - charsets = { - 'printable_no_whitespace': string.printable.translate(None, string.whitespace), - 'printable': string.printable, - 'punctuation': string.punctuation, - 'whitespace': string.whitespace, - 'digits': string.digits - } - - def __init__(self, prng, _recursion=0): - super(RepeatExpandingFormatter, self).__init__() - # this class assumes it is always instantiated once per - # formatting; use that to detect runaway recursion - self.prng = prng - self._recursion = _recursion - - def get_value(self, key, args, kwargs): - fields = key.split(None, 1) - fn = getattr(self, 'special_{name}'.format(name=fields[0]), None) - if fn is not None: - if len(fields) == 1: - fields.append('') - return fn(fields[1]) - - val = super(RepeatExpandingFormatter, self).get_value(key, args, kwargs) - if self._recursion > 5: - raise RecursionError(key) - fmt = self.__class__(self.prng, _recursion=self._recursion+1) - - n = fmt.vformat(val, args, kwargs) - return n - - def special_random(self, args): - arg_list = args.split() - try: - size_min, size_max = arg_list[0].split('-', 1) - except ValueError: - size_min = size_max = arg_list[0] - except IndexError: - size_min = '0' - size_max = '1000' - - size_min = int(size_min) - size_max = int(size_max) - length = self.prng.randint(size_min, size_max) - - try: - charset_arg = arg_list[1] - except IndexError: - charset_arg = 'printable' - - if charset_arg == 'binary' or charset_arg == 'binary_no_whitespace': - num_bytes = length + 8 - tmplist = [self.prng.getrandbits(64) for _ in xrange(num_bytes / 8)] - tmpstring = struct.pack((num_bytes / 8) * 'Q', *tmplist) - if charset_arg == 'binary_no_whitespace': - tmpstring = ''.join(c for c in tmpstring if c not in string.whitespace) - return tmpstring[0:length] - else: - charset = self.charsets[charset_arg] - return ''.join([self.prng.choice(charset) for _ in xrange(length)]) # Won't scale nicely - - -def parse_options(): - parser = OptionParser() - parser.add_option('-O', '--outfile', help='write output to FILE. Defaults to STDOUT', metavar='FILE') - parser.add_option('--seed', dest='seed', type='int', help='initial seed for the random number generator') - parser.add_option('--seed-file', dest='seedfile', help='read seeds for specific requests from FILE', metavar='FILE') - parser.add_option('-n', dest='num_requests', type='int', help='issue NUM requests before stopping', metavar='NUM') - parser.add_option('-v', '--verbose', dest='verbose', action="store_true", help='turn on verbose output') - parser.add_option('-d', '--debug', dest='debug', action="store_true", help='turn on debugging (very verbose) output') - parser.add_option('--decision-graph', dest='graph_filename', help='file in which to find the request decision graph') - parser.add_option('--no-cleanup', dest='cleanup', action="store_false", help='turn off teardown so you can peruse the state of buckets after testing') - - parser.set_defaults(num_requests=5) - parser.set_defaults(cleanup=True) - parser.set_defaults(graph_filename='request_decision_graph.yml') - return parser.parse_args() - - -def randomlist(seed=None): - """ Returns an infinite generator of random numbers - """ - rng = random.Random(seed) - while True: - yield rng.randint(0,100000) #100,000 seeds is enough, right? - - -def populate_buckets(conn, alt): - """ Creates buckets and keys for fuzz testing and sets appropriate - permissions. Returns a dictionary of the bucket and key names. - """ - breadable = common.get_new_bucket(alt) - bwritable = common.get_new_bucket(alt) - bnonreadable = common.get_new_bucket(alt) - - oreadable = Key(breadable) - owritable = Key(bwritable) - ononreadable = Key(breadable) - oreadable.set_contents_from_string('oreadable body') - owritable.set_contents_from_string('owritable body') - ononreadable.set_contents_from_string('ononreadable body') - - breadable.set_acl('public-read') - bwritable.set_acl('public-read-write') - bnonreadable.set_acl('private') - oreadable.set_acl('public-read') - owritable.set_acl('public-read-write') - ononreadable.set_acl('private') - - return dict( - bucket_readable=breadable.name, - bucket_writable=bwritable.name, - bucket_not_readable=bnonreadable.name, - bucket_not_writable=breadable.name, - object_readable=oreadable.key, - object_writable=owritable.key, - object_not_readable=ononreadable.key, - object_not_writable=oreadable.key, - ) - - -def _main(): - """ The main script - """ - (options, args) = parse_options() - random.seed(options.seed if options.seed else None) - s3_connection = common.s3.main - alt_connection = common.s3.alt - - if options.outfile: - OUT = open(options.outfile, 'w') - else: - OUT = sys.stderr - - VERBOSE = DEBUG = open('/dev/null', 'w') - if options.verbose: - VERBOSE = OUT - if options.debug: - DEBUG = OUT - VERBOSE = OUT - - request_seeds = None - if options.seedfile: - FH = open(options.seedfile, 'r') - request_seeds = [int(line) for line in FH if line != '\n'] - print>>OUT, 'Seedfile: %s' %options.seedfile - print>>OUT, 'Number of requests: %d' %len(request_seeds) - else: - if options.seed: - print>>OUT, 'Initial Seed: %d' %options.seed - print>>OUT, 'Number of requests: %d' %options.num_requests - random_list = randomlist(options.seed) - request_seeds = itertools.islice(random_list, options.num_requests) - - print>>OUT, 'Decision Graph: %s' %options.graph_filename - - graph_file = open(options.graph_filename, 'r') - decision_graph = yaml.safe_load(graph_file) - - constants = populate_buckets(s3_connection, alt_connection) - print>>VERBOSE, "Test Buckets/Objects:" - for key, value in constants.iteritems(): - print>>VERBOSE, "\t%s: %s" %(key, value) - - print>>OUT, "Begin Fuzzing..." - print>>VERBOSE, '='*80 - for request_seed in request_seeds: - print>>VERBOSE, 'Seed is: %r' %request_seed - prng = random.Random(request_seed) - decision = assemble_decision(decision_graph, prng) - decision.update(constants) - - method = expand(decision, decision['method'], prng) - path = expand(decision, decision['urlpath'], prng) - - try: - body = expand(decision, decision['body'], prng) - except KeyError: - body = '' - - try: - headers = expand_headers(decision, prng) - except KeyError: - headers = {} - - print>>VERBOSE, "%r %r" %(method[:100], path[:100]) - for h, v in headers.iteritems(): - print>>VERBOSE, "%r: %r" %(h[:50], v[:50]) - print>>VERBOSE, "%r\n" % body[:100] - - print>>DEBUG, 'FULL REQUEST' - print>>DEBUG, 'Method: %r' %method - print>>DEBUG, 'Path: %r' %path - print>>DEBUG, 'Headers:' - for h, v in headers.iteritems(): - print>>DEBUG, "\t%r: %r" %(h, v) - print>>DEBUG, 'Body: %r\n' %body - - failed = False # Let's be optimistic, shall we? - try: - response = s3_connection.make_request(method, path, data=body, headers=headers, override_num_retries=1) - body = response.read() - except BotoServerError, e: - response = e - body = e.body - failed = True - except BadStatusLine, e: - print>>OUT, 'FAILED: failed to parse response (BadStatusLine); probably a NUL byte in your request?' - print>>VERBOSE, '='*80 - continue - - if failed: - print>>OUT, 'FAILED:' - OLD_VERBOSE = VERBOSE - OLD_DEBUG = DEBUG - VERBOSE = DEBUG = OUT - print>>VERBOSE, 'Seed was: %r' %request_seed - print>>VERBOSE, 'Response status code: %d %s' %(response.status, response.reason) - print>>DEBUG, 'Body:\n%s' %body - print>>VERBOSE, '='*80 - if failed: - VERBOSE = OLD_VERBOSE - DEBUG = OLD_DEBUG - - print>>OUT, '...done fuzzing' - - if options.cleanup: - common.teardown() - - -def main(): - common.setup() - try: - _main() - except Exception as e: - traceback.print_exc() - common.teardown() - diff --git a/setup.py b/setup.py index edcab1ab..30a5df6a 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( 's3tests-generate-objects = s3tests.generate_objects:main', 's3tests-test-readwrite = s3tests.readwrite:main', 's3tests-test-roundtrip = s3tests.roundtrip:main', - 's3tests-fuzz-headers = s3tests.fuzz_headers:main', + 's3tests-fuzz-headers = s3tests.fuzz.headers:main', ], },