+++ /dev/null
-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)
-
--- /dev/null
+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()
+
--- /dev/null
+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)
+
+++ /dev/null
-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()
-
'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',
],
},