Added support for meta variable JSON files
Syntribos now allows the user to specify variables in their request templates by reading from a meta.json file. This is part 1 of 3 of the full effort, dealing primarily with the template parser itself. Change-Id: Id41d331f595cd3bc32f085ef49cb5d1b16779a5c
This commit is contained in:
parent
b1d47899d9
commit
c5a4dd083d
44
README.rst
44
README.rst
|
@ -865,6 +865,50 @@ follows:
|
|||
|
||||
The ID provided will remain static for every test.
|
||||
|
||||
Meta Variable File
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Syntribos allows for templates to read in variables from a user-specified
|
||||
meta variable file. These files contain JSON objects that define variables
|
||||
to be used in one or more request templates.
|
||||
|
||||
The file must be named `meta.json` and must be placed in the same
|
||||
directory as the template files that reference it. Meta variable files take
|
||||
the form:
|
||||
::
|
||||
|
||||
{
|
||||
"user_password": {
|
||||
"val": 1234
|
||||
},
|
||||
"user_name": {
|
||||
"type": config,
|
||||
"val": "user.username"
|
||||
},
|
||||
"user_token": {
|
||||
"type": "function",
|
||||
"val": "syntribos.extensions.identity:get_scoped_token_v3",
|
||||
"args": ["user"]
|
||||
}
|
||||
}
|
||||
|
||||
To reference a meta variable from a request template, reference the variable
|
||||
name surrounded by `|` (pipe). An example request template with meta
|
||||
variables is as follows:
|
||||
::
|
||||
|
||||
POST /user HTTP/1.1
|
||||
X-Auth-Token: |user_token|
|
||||
Accept: */*
|
||||
Content-type: application/json
|
||||
|
||||
{
|
||||
"user": {
|
||||
"username": "|user_name|",
|
||||
"password": "|user_password|"
|
||||
}
|
||||
}
|
||||
|
||||
Running a specific test
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -126,11 +126,6 @@ making HTTP requests.
|
|||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: syntribos.clients.http.models
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: syntribos.clients.http.parser
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
|
|
@ -145,6 +145,50 @@ follows:
|
|||
|
||||
The ID provided will remain static for every test.
|
||||
|
||||
Meta Variable File
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Syntribos allows for templates to read in variables from a user-specified
|
||||
meta variable file. These files contain JSON objects that define variables
|
||||
to be used in one or more request templates.
|
||||
|
||||
The file must be named `meta.json` and must be placed in the same
|
||||
directory as the template files that reference it. Meta variable files take
|
||||
the form:
|
||||
::
|
||||
|
||||
{
|
||||
"user_password": {
|
||||
"val": 1234
|
||||
},
|
||||
"user_name": {
|
||||
"type": config,
|
||||
"val": "user.username"
|
||||
},
|
||||
"user_token": {
|
||||
"type": "function",
|
||||
"val": "syntribos.extensions.identity:get_scoped_token_v3",
|
||||
"args": ["user"]
|
||||
}
|
||||
}
|
||||
|
||||
To reference a meta variable from a request template, reference the variable
|
||||
name surrounded by `|` (pipe). An example request template with meta
|
||||
variables is as follows:
|
||||
::
|
||||
|
||||
POST /user HTTP/1.1
|
||||
X-Auth-Token: |user_token|
|
||||
Accept: */*
|
||||
Content-type: application/json
|
||||
|
||||
{
|
||||
"user": {
|
||||
"username": "|user_name|",
|
||||
"password": "|user_password|"
|
||||
}
|
||||
}
|
||||
|
||||
Running a specific test
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -13,4 +13,5 @@
|
|||
# limitations under the License.
|
||||
# flake8: noqa
|
||||
from syntribos.clients.http.parser import RequestCreator as parser
|
||||
from syntribos.clients.http.parser import VariableObject
|
||||
from syntribos.clients.http.client import SynHTTPClient as client
|
||||
|
|
|
@ -60,7 +60,7 @@ class SynHTTPClient(HTTPClient):
|
|||
template files, and passed to this method to send the request.
|
||||
|
||||
:param request_obj: A RequestObject generated by a parser
|
||||
:type request_obj: :class:`syntribos.clients.http.models.RequestObject`
|
||||
:type request_obj: :class:`syntribos.clients.http.parser.RequestObject`
|
||||
:returns: tuple of (response, signals)
|
||||
"""
|
||||
response, signals = self.request(
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
# Copyright 2015 Rackspace
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# pylint: skip-file
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
import six
|
||||
from six.moves import html_parser
|
||||
|
||||
_iterators = {}
|
||||
|
||||
|
||||
class RequestHelperMixin(object):
|
||||
"""Class that helps with fuzzing requests."""
|
||||
|
||||
@classmethod
|
||||
def _run_iters(cls, data, action_field):
|
||||
"""Recursively fuzz variables in `data` and its children
|
||||
|
||||
:param data: The request data to be modified
|
||||
:param action_field: The name of the field to be replaced
|
||||
:returns: object or string with action_field fuzzed
|
||||
:rtype: `dict` OR `str` OR :class:`ElementTree.Element`
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return cls._run_iters_dict(data, action_field)
|
||||
elif isinstance(data, ElementTree.Element):
|
||||
return cls._run_iters_xml(data, action_field)
|
||||
elif isinstance(data, six.string_types):
|
||||
data = data.replace(action_field, "")
|
||||
return cls._replace_iter(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _run_iters_dict(cls, dic, action_field=""):
|
||||
"""Run fuzz iterators for a dict type."""
|
||||
for key, val in dic.items():
|
||||
dic[key] = val = cls._replace_iter(val)
|
||||
if isinstance(key, six.string_types):
|
||||
new_key = cls._replace_iter(key).replace(action_field, "")
|
||||
if new_key != key:
|
||||
del dic[key]
|
||||
dic[new_key] = val
|
||||
if isinstance(val, dict):
|
||||
cls._run_iters_dict(val, action_field)
|
||||
elif isinstance(val, list):
|
||||
cls._run_iters_list(val, action_field)
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def _run_iters_list(cls, val, action_field=""):
|
||||
"""Run fuzz iterators for a list type."""
|
||||
for i, v in enumerate(val):
|
||||
if isinstance(v, six.string_types):
|
||||
val[i] = v = cls._replace_iter(v).replace(action_field, "")
|
||||
elif isinstance(v, dict):
|
||||
val[i] = cls._run_iters_dict(v, action_field)
|
||||
elif isinstance(v, list):
|
||||
cls._run_iters_list(v, action_field)
|
||||
|
||||
@classmethod
|
||||
def _run_iters_xml(cls, ele, action_field=""):
|
||||
"""Run fuzz iterators for an XML element type."""
|
||||
if isinstance(ele.text, six.string_types):
|
||||
ele.text = cls._replace_iter(ele.text).replace(action_field, "")
|
||||
cls._run_iters_dict(ele.attrib, action_field)
|
||||
for i, v in enumerate(list(ele)):
|
||||
ele[i] = cls._run_iters_xml(v, action_field)
|
||||
return ele
|
||||
|
||||
@staticmethod
|
||||
def _string_data(data):
|
||||
"""Replace various objects types with string representations."""
|
||||
if isinstance(data, dict):
|
||||
return json.dumps(data)
|
||||
elif isinstance(data, ElementTree.Element):
|
||||
str_data = ElementTree.tostring(data)
|
||||
# No way to stop tostring from HTML escaping even if we wanted
|
||||
h = html_parser.HTMLParser()
|
||||
return h.unescape(str_data.decode())
|
||||
else:
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _replace_iter(string):
|
||||
"""Fuzz a string."""
|
||||
if not isinstance(string, six.string_types):
|
||||
return string
|
||||
for k, v in _iterators.items():
|
||||
if k in string:
|
||||
string = string.replace(k, six.next(v))
|
||||
return string
|
||||
|
||||
@staticmethod
|
||||
def _remove_braces(string):
|
||||
"""Remove braces from strings (in request templates)."""
|
||||
return re.sub(r"{([^}]*)}", "\\1", string)
|
||||
|
||||
@staticmethod
|
||||
def _remove_attr_names(string):
|
||||
"""removes identifiers from string substitution
|
||||
|
||||
If we are fuzzing example.com/{userid:123}, this method removes the
|
||||
identifier name so that the client only sees example.com/{123} when
|
||||
it sends the request
|
||||
"""
|
||||
return re.sub(r"{[\w]+:", "{", string)
|
||||
|
||||
def prepare_request(self):
|
||||
"""Prepare a request for sending off
|
||||
|
||||
It should be noted this function does not make a request copy,
|
||||
destroying iterators in request. A copy should be made if making
|
||||
multiple requests.
|
||||
"""
|
||||
self.data = self._run_iters(self.data, self.action_field)
|
||||
self.headers = self._run_iters(self.headers, self.action_field)
|
||||
self.params = self._run_iters(self.params, self.action_field)
|
||||
self.data = self._string_data(self.data)
|
||||
self.url = self._run_iters(self.url, self.action_field)
|
||||
self.url = self._remove_braces(self._remove_attr_names(self.url))
|
||||
|
||||
def get_prepared_copy(self):
|
||||
"""Create a copy of `self`, and prepare it for use by a fuzzer
|
||||
|
||||
:returns: Copy of request object that has been prepared for sending
|
||||
:rtype: :class:`RequestHelperMixin`
|
||||
"""
|
||||
local_copy = copy.deepcopy(self)
|
||||
local_copy.prepare_request()
|
||||
return local_copy
|
||||
|
||||
def get_copy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
class RequestObject(RequestHelperMixin):
|
||||
"""An object that holds information about an HTTP request.
|
||||
|
||||
:ivar str method: Request method
|
||||
:ivar str url: URL to request
|
||||
:ivar dict action_field: Action Fields
|
||||
:ivar dict headers: Dictionary of headers in name:value format
|
||||
:ivar dict params: Dictionary of params in name:value format
|
||||
:ivar data: Data to send as part of request body
|
||||
:ivar bool sanitize: Boolean variable used to filter secrets
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
method,
|
||||
url,
|
||||
action_field=None,
|
||||
headers=None,
|
||||
params=None,
|
||||
data=None,
|
||||
sanitize=False):
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.action_field = action_field
|
||||
self.headers = headers
|
||||
self.params = params
|
||||
self.data = data
|
||||
self.sanitize = sanitize
|
|
@ -11,36 +11,50 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import ast
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
from six.moves import html_parser
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from syntribos.clients.http.models import _iterators
|
||||
from syntribos.clients.http.models import RequestObject
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
_iterators = {}
|
||||
_string_var_objs = {}
|
||||
|
||||
|
||||
class RequestCreator(object):
|
||||
ACTION_FIELD = "ACTION_FIELD:"
|
||||
EXTERNAL = r"CALL_EXTERNAL\|([^:]+?):([^:]+?):([^|]+?)\|"
|
||||
request_model_type = RequestObject
|
||||
METAVAR = r"(\|[^\|]*\|)"
|
||||
FUNC_WITH_ARGS = r"([^:]+):([^:]+):(\[.+\])"
|
||||
FUNC_NO_ARGS = r"([^:]+):([^:]+)"
|
||||
|
||||
@classmethod
|
||||
def create_request(cls, string, endpoint):
|
||||
def create_request(cls, string, endpoint, meta_vars=None):
|
||||
"""Parse the HTTP request template into its components
|
||||
|
||||
:param str string: HTTP request template
|
||||
:param str endpoint: URL of the target to be tested
|
||||
:param dict meta_vars: default None, dict parsed from meta.json
|
||||
|
||||
:rtype: :class:`syntribos.clients.http.models.RequestObject`
|
||||
:rtype: :class:`syntribos.clients.http.parser.RequestObject`
|
||||
:returns: RequestObject with method, url, params, etc. for use by
|
||||
runner
|
||||
"""
|
||||
if meta_vars:
|
||||
cls.meta_vars = meta_vars
|
||||
string = cls.call_external_functions(string)
|
||||
action_field = str(uuid.uuid4()).replace("-", "")
|
||||
string = string.replace(cls.ACTION_FIELD, action_field)
|
||||
|
@ -53,10 +67,129 @@ class RequestCreator(object):
|
|||
method, url, params, _ = cls._parse_url_line(lines[0], endpoint)
|
||||
headers = cls._parse_headers(lines[1:index])
|
||||
data = cls._parse_data(lines[index + 1:])
|
||||
return cls.request_model_type(
|
||||
|
||||
return RequestObject(
|
||||
method=method, url=url, headers=headers, params=params, data=data,
|
||||
action_field=action_field)
|
||||
|
||||
@classmethod
|
||||
def _create_var_obj(cls, var):
|
||||
"""Given the name of a variable, creates VariableObject
|
||||
|
||||
:param str var: name of the variable in meta.json
|
||||
|
||||
:rtype: :class:`syntribos.clients.http.parser.VariableObject`
|
||||
:returns: VariableObject holding the attributes defined in the JSON
|
||||
object read in from meta.json
|
||||
"""
|
||||
if var not in cls.meta_vars:
|
||||
print("Expected to find {0} in meta.json, but didn't. "
|
||||
"Check your templates".format(var))
|
||||
return
|
||||
var_dict = cls.meta_vars[var]
|
||||
if "type" in var_dict:
|
||||
var_dict["var_type"] = var_dict.pop("type")
|
||||
var_obj = VariableObject(var, **var_dict)
|
||||
return var_obj
|
||||
|
||||
@classmethod
|
||||
def replace_one_variable(cls, var_obj):
|
||||
"""Evaluate a VariableObject according to its type
|
||||
|
||||
A meta variable's type is optional. If a type is given, the parser will
|
||||
interpret the variable in one of 3 ways according to its type, and
|
||||
returns that value.
|
||||
|
||||
* Type config: The parser will attempt to read the config value
|
||||
specified by the "val" attribute and returns that value.
|
||||
* Type function: The parser will call the function named in the "val"
|
||||
attribute with arguments given in the "args" attribute, and returns
|
||||
the value from calling the function. This value is cached, and
|
||||
will be returned on subsequent calls.
|
||||
* Type generator: works the same way as the function type, but its
|
||||
results are not cached and the function will be called every time.
|
||||
|
||||
Otherwise, the parser will interpret the variable as a static variable,
|
||||
and will return whatever is in the "val" attribute.
|
||||
|
||||
:param var_obj: A :class:`syntribos.clients.http.parser.VariableObject`
|
||||
:returns: The evaluated value according to its meta variable type
|
||||
"""
|
||||
if var_obj.var_type == 'config':
|
||||
try:
|
||||
return reduce(getattr, var_obj.val.split("."), CONF)
|
||||
except AttributeError:
|
||||
print("Meta json file contains reference to the config option "
|
||||
"{0}, which does not appear to exist.".format(
|
||||
var_obj.val))
|
||||
|
||||
elif var_obj.var_type == 'function':
|
||||
if var_obj.function_return_value:
|
||||
return var_obj.function_return_value
|
||||
|
||||
if not var_obj.val:
|
||||
print("The type of variable {0} is function, but there is no "
|
||||
"reference to the function.")
|
||||
return
|
||||
|
||||
var_obj.function_return_value = cls.call_one_external_function(
|
||||
var_obj.val, var_obj.args)
|
||||
return var_obj.function_return_value
|
||||
|
||||
elif var_obj.var_type == 'generator':
|
||||
if not var_obj.val:
|
||||
print("The type of variable {0} is generator, but there is no "
|
||||
"reference to the function.")
|
||||
return
|
||||
|
||||
return cls.call_one_external_function(var_obj.val, var_obj.args)
|
||||
|
||||
else:
|
||||
return var_obj.val
|
||||
|
||||
@classmethod
|
||||
def _replace_dict_variables(cls, dic):
|
||||
"""Recursively evaluates all meta variables in a given dict."""
|
||||
for (key, value) in six.iteritems(dic):
|
||||
# Keys dont get fuzzed, so can handle them here
|
||||
if re.search(cls.METAVAR, key):
|
||||
key = key.strip("|%s" % string.whitespace)
|
||||
key_obj = cls._create_var_obj(key)
|
||||
key = cls.replace_one_variable(key_obj)
|
||||
# Vals are fuzzed so they need to be passed to datagen as an object
|
||||
if not isinstance(value, dict):
|
||||
if re.search(cls.METAVAR, value):
|
||||
value = value.strip("|%s" % string.whitespace)
|
||||
val_obj = cls._create_var_obj(value)
|
||||
dic[key] = val_obj
|
||||
else:
|
||||
cls._replace_dict_variables(value)
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def _replace_str_variables(cls, string):
|
||||
"""Replaces all meta variable references in the string
|
||||
|
||||
For every meta variable reference found in the string, it generates
|
||||
a VariableObject. It then associates each VariableObject with a uuid,
|
||||
as a key value pair, which is storedin the global dict variable
|
||||
`_str_var_obs`. It then replaces all meta variable references in the
|
||||
string with the uuid key to the VariableObject
|
||||
|
||||
:param str string: String to be evaluated
|
||||
:returns: string with all metavariable references replaced
|
||||
"""
|
||||
while True:
|
||||
match = re.search(cls.METAVAR, string)
|
||||
if not match:
|
||||
break
|
||||
obj_ref_uuid = str(uuid.uuid4()).replace("-", "")
|
||||
var_name = match.group(1).strip("|")
|
||||
var_obj = cls._create_var_obj(var_name)
|
||||
_string_var_objs[obj_ref_uuid] = var_obj
|
||||
string = re.sub(cls.METAVAR, obj_ref_uuid, string, count=1)
|
||||
return string
|
||||
|
||||
@classmethod
|
||||
def _parse_url_line(cls, line, endpoint):
|
||||
"""Split first line of an HTTP request into its components
|
||||
|
@ -69,7 +202,6 @@ class RequestCreator(object):
|
|||
"""
|
||||
valid_methods = ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE",
|
||||
"TRACE", "CONNECT", "PATCH"]
|
||||
valid_versions = ["HTTP/1.1", "HTTP/1.0", "HTTP/0.9"]
|
||||
|
||||
params = {}
|
||||
method, url, version = line.split()
|
||||
|
@ -87,10 +219,8 @@ class RequestCreator(object):
|
|||
if method not in valid_methods:
|
||||
raise ValueError("Invalid HTTP method: {0}".format(method))
|
||||
|
||||
if version not in valid_versions:
|
||||
raise ValueError("Invalid HTTP version: {0}".format(version))
|
||||
|
||||
return method, url, params, version
|
||||
return (method, cls._replace_str_variables(url),
|
||||
cls._replace_dict_variables(params), version)
|
||||
|
||||
@classmethod
|
||||
def _parse_headers(cls, lines):
|
||||
|
@ -105,7 +235,7 @@ class RequestCreator(object):
|
|||
for line in lines:
|
||||
key, value = line.split(":", 1)
|
||||
headers[key] = value.strip()
|
||||
return headers
|
||||
return cls._replace_dict_variables(headers)
|
||||
|
||||
@classmethod
|
||||
def _parse_data(cls, lines):
|
||||
|
@ -124,6 +254,11 @@ class RequestCreator(object):
|
|||
# TODO(cneill): Make this less hacky
|
||||
if isinstance(data, list):
|
||||
data = json.dumps(data)
|
||||
|
||||
if isinstance(data, dict):
|
||||
return cls._replace_dict_variables(data)
|
||||
else:
|
||||
return cls._replace_str_variables(data)
|
||||
except Exception:
|
||||
try:
|
||||
data = ElementTree.fromstring(data)
|
||||
|
@ -163,3 +298,241 @@ class RequestCreator(object):
|
|||
else:
|
||||
string = re.sub(cls.EXTERNAL, str(val), string, count=1)
|
||||
return string
|
||||
|
||||
@classmethod
|
||||
def call_one_external_function(cls, string, args):
|
||||
"""Calls one function read in from templates and returns the result."""
|
||||
if not isinstance(string, six.string_types):
|
||||
return string
|
||||
|
||||
match = re.search(cls.FUNC_NO_ARGS, string)
|
||||
func_string_has_args = False
|
||||
|
||||
if not match:
|
||||
match = re.search(cls.FUNC_WITH_ARGS, string)
|
||||
func_string_has_args = True
|
||||
|
||||
if not match:
|
||||
print("The reference to the function {0} failed to parse "
|
||||
"correctly".format(string))
|
||||
return
|
||||
|
||||
dot_path = match.group(1)
|
||||
func_name = match.group(2)
|
||||
mod = importlib.import_module(dot_path)
|
||||
func = getattr(mod, func_name)
|
||||
|
||||
if func_string_has_args and not args:
|
||||
arg_list = match.group(3)
|
||||
args = json.loads(arg_list)
|
||||
else:
|
||||
args = ast.literal_eval(args)
|
||||
|
||||
val = func(*args)
|
||||
|
||||
if isinstance(val, types.GeneratorType):
|
||||
local_uuid = str(uuid.uuid4()).replace("-", "")
|
||||
string = local_uuid
|
||||
_iterators[local_uuid] = val
|
||||
else:
|
||||
string = str(val)
|
||||
|
||||
return string
|
||||
|
||||
|
||||
class VariableObject(object):
|
||||
VAR_TYPES = ["function", "generator", "config"]
|
||||
FUZZ_TYPES = ["int", "str", "uuid"]
|
||||
|
||||
def __init__(self, name, var_type="", args=[], val="", fuzz=True,
|
||||
fuzz_type="", min_length=0, max_length=sys.maxsize, **kwargs):
|
||||
if var_type and var_type.lower() not in self.VAR_TYPES:
|
||||
print("The variable {0} has a type of {1} which syntribos does not"
|
||||
" recognize".format(name, var_type))
|
||||
self.name = name
|
||||
self.var_type = var_type.lower()
|
||||
self.val = val
|
||||
self.args = args
|
||||
self.fuzz_type = fuzz_type
|
||||
self.fuzz = fuzz
|
||||
self.min_length = min_length
|
||||
self.max_length = max_length
|
||||
self.function_return_value = None
|
||||
|
||||
def __repr__(self):
|
||||
return str(vars(self))
|
||||
|
||||
|
||||
class RequestHelperMixin(object):
|
||||
"""Class that helps with fuzzing requests."""
|
||||
|
||||
def __init__(self):
|
||||
self.data = ""
|
||||
self.headers = ""
|
||||
self.params = ""
|
||||
self.data = ""
|
||||
self.url = ""
|
||||
self.url = ""
|
||||
|
||||
@classmethod
|
||||
def _run_iters(cls, data, action_field):
|
||||
"""Recursively fuzz variables in `data` and its children
|
||||
|
||||
:param data: The request data to be modified
|
||||
:param action_field: The name of the field to be replaced
|
||||
:returns: object or string with action_field fuzzed
|
||||
:rtype: `dict` OR `str` OR :class:`ElementTree.Element`
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return cls._run_iters_dict(data, action_field)
|
||||
elif isinstance(data, ElementTree.Element):
|
||||
return cls._run_iters_xml(data, action_field)
|
||||
elif isinstance(data, VariableObject):
|
||||
return RequestCreator.replace_one_variable(data)
|
||||
elif isinstance(data, six.string_types):
|
||||
data = data.replace(action_field, "")
|
||||
return cls._replace_iter(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _run_iters_dict(cls, dic, action_field=""):
|
||||
"""Run fuzz iterators for a dict type."""
|
||||
for key, val in six.iteritems(dic):
|
||||
dic[key] = val = cls._replace_iter(val)
|
||||
if isinstance(key, six.string_types):
|
||||
new_key = cls._replace_iter(key).replace(action_field, "")
|
||||
if new_key != key:
|
||||
del dic[key]
|
||||
dic[new_key] = val
|
||||
if isinstance(val, VariableObject):
|
||||
if key in dic:
|
||||
dic[key] = RequestCreator.replace_one_variable(val)
|
||||
elif new_key in dic:
|
||||
dic[new_key] = RequestCreator.replace_one_variable(val)
|
||||
if isinstance(val, dict):
|
||||
cls._run_iters_dict(val, action_field)
|
||||
elif isinstance(val, list):
|
||||
cls._run_iters_list(val, action_field)
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def _run_iters_list(cls, val, action_field=""):
|
||||
"""Run fuzz iterators for a list type."""
|
||||
for i, v in enumerate(val):
|
||||
if isinstance(v, six.string_types):
|
||||
val[i] = v = cls._replace_iter(v).replace(action_field, "")
|
||||
if isinstance(v, VariableObject):
|
||||
val[i] = v = RequestCreator.replace_one_variable(v)
|
||||
elif isinstance(v, dict):
|
||||
val[i] = cls._run_iters_dict(v, action_field)
|
||||
elif isinstance(v, list):
|
||||
cls._run_iters_list(v, action_field)
|
||||
|
||||
@classmethod
|
||||
def _run_iters_xml(cls, ele, action_field=""):
|
||||
"""Run fuzz iterators for an XML element type."""
|
||||
if isinstance(ele.text, six.string_types):
|
||||
ele.text = cls._replace_iter(ele.text).replace(action_field, "")
|
||||
cls._run_iters_dict(ele.attrib, action_field)
|
||||
for i, v in enumerate(list(ele)):
|
||||
ele[i] = cls._run_iters_xml(v, action_field)
|
||||
return ele
|
||||
|
||||
@staticmethod
|
||||
def _string_data(data):
|
||||
"""Replace various objects types with string representations."""
|
||||
if isinstance(data, dict):
|
||||
return json.dumps(data)
|
||||
elif isinstance(data, ElementTree.Element):
|
||||
str_data = ElementTree.tostring(data)
|
||||
# No way to stop tostring from HTML escaping even if we wanted
|
||||
h = html_parser.HTMLParser()
|
||||
return h.unescape(str_data.decode())
|
||||
else:
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _replace_iter(string):
|
||||
"""Fuzz a string."""
|
||||
if not isinstance(string, six.string_types):
|
||||
return string
|
||||
for k, v in list(six.iteritems(_iterators)):
|
||||
if k in string:
|
||||
string = string.replace(k, six.next(v))
|
||||
for k, v in _string_var_objs.items():
|
||||
if k in string:
|
||||
str_val = str(RequestCreator.replace_one_variable(v))
|
||||
string = string.replace(k, str_val)
|
||||
return string
|
||||
|
||||
@staticmethod
|
||||
def _remove_braces(string):
|
||||
"""Remove braces from strings (in request templates)."""
|
||||
return re.sub(r"{([^}]*)}", "\\1", string)
|
||||
|
||||
@staticmethod
|
||||
def _remove_attr_names(string):
|
||||
"""removes identifiers from string substitution
|
||||
|
||||
If we are fuzzing example.com/{userid:123}, this method removes the
|
||||
identifier name so that the client only sees example.com/{123} when
|
||||
it sends the request
|
||||
"""
|
||||
return re.sub(r"{[\w]+:", "{", string)
|
||||
|
||||
def prepare_request(self):
|
||||
"""Prepare a request for sending off
|
||||
|
||||
It should be noted this function does not make a request copy,
|
||||
destroying iterators in request. A copy should be made if making
|
||||
multiple requests.
|
||||
"""
|
||||
self.data = self._run_iters(self.data, self.action_field)
|
||||
self.headers = self._run_iters(self.headers, self.action_field)
|
||||
self.params = self._run_iters(self.params, self.action_field)
|
||||
self.data = self._string_data(self.data)
|
||||
self.url = self._run_iters(self.url, self.action_field)
|
||||
self.url = self._remove_braces(self._remove_attr_names(self.url))
|
||||
|
||||
def get_prepared_copy(self):
|
||||
"""Create a copy of `self`, and prepare it for use by a fuzzer
|
||||
|
||||
:returns: Copy of request object that has been prepared for sending
|
||||
:rtype: :class:`RequestHelperMixin`
|
||||
"""
|
||||
local_copy = copy.deepcopy(self)
|
||||
local_copy.prepare_request()
|
||||
return local_copy
|
||||
|
||||
def get_copy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
class RequestObject(RequestHelperMixin):
|
||||
"""An object that holds information about an HTTP request.
|
||||
|
||||
:ivar str method: Request method
|
||||
:ivar str url: URL to request
|
||||
:ivar dict action_field: Action Fields
|
||||
:ivar dict headers: Dictionary of headers in name:value format
|
||||
:ivar dict params: Dictionary of params in name:value format
|
||||
:ivar data: Data to send as part of request body
|
||||
:ivar bool sanitize: Boolean variable used to filter secrets
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
method,
|
||||
url,
|
||||
action_field=None,
|
||||
headers=None,
|
||||
params=None,
|
||||
data=None,
|
||||
sanitize=False):
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.action_field = action_field
|
||||
self.headers = headers
|
||||
self.params = params
|
||||
self.data = data
|
||||
self.sanitize = sanitize
|
||||
|
|
|
@ -11,11 +11,13 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import unittest
|
||||
|
||||
from oslo_config import cfg
|
||||
|
@ -222,7 +224,21 @@ class Runner(object):
|
|||
exit(1)
|
||||
|
||||
print("\nPress Ctrl-C to pause or exit...\n")
|
||||
# TODO(mdong): Make this handle inheritence and all that. For now, just
|
||||
# pass the first meta file it sees to the parser. Also, find a better
|
||||
# way to pass meta_vars
|
||||
meta_vars = None
|
||||
templates_dir = list(templates_dir)
|
||||
for file_path, req_str in templates_dir:
|
||||
if "meta.json" not in file_path:
|
||||
continue
|
||||
else:
|
||||
meta_vars = json.loads(req_str)
|
||||
break
|
||||
|
||||
for file_path, req_str in templates_dir:
|
||||
if "meta.json" in file_path:
|
||||
continue
|
||||
LOG = cls.get_logger(file_path)
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
if not file_path.endswith(".template"):
|
||||
|
@ -243,9 +259,11 @@ class Runner(object):
|
|||
print(syntribos.SEP)
|
||||
|
||||
if CONF.sub_command.name == "run":
|
||||
cls.run_given_tests(list_of_tests, file_path, req_str)
|
||||
cls.run_given_tests(list_of_tests, file_path,
|
||||
req_str, meta_vars)
|
||||
elif CONF.sub_command.name == "dry_run":
|
||||
cls.dry_run(list_of_tests, file_path, req_str, dry_run_output)
|
||||
cls.dry_run(list_of_tests, file_path,
|
||||
req_str, dry_run_output, meta_vars)
|
||||
|
||||
if CONF.sub_command.name == "run":
|
||||
result.print_result(cls.start_time)
|
||||
|
@ -254,7 +272,8 @@ class Runner(object):
|
|||
cls.dry_run_report(dry_run_output)
|
||||
|
||||
@classmethod
|
||||
def dry_run(cls, list_of_tests, file_path, req_str, output):
|
||||
def dry_run(cls, list_of_tests, file_path, req_str, output,
|
||||
meta_vars=None):
|
||||
"""Runs debug test to check all steps leading up to executing a test
|
||||
|
||||
This method does not run any checks, but does parse the template files
|
||||
|
@ -272,10 +291,10 @@ class Runner(object):
|
|||
for _, test_class in list_of_tests:
|
||||
try:
|
||||
print("\nParsing template file...")
|
||||
test_class.create_init_request(file_path, req_str)
|
||||
test_class.create_init_request(file_path, req_str, meta_vars)
|
||||
except Exception as e:
|
||||
print("Error in parsing template:\n \t{0}: {1}\n".format(
|
||||
type(e).__name__, e))
|
||||
print("Error in parsing template:\n \t{0}\n".format(
|
||||
traceback.format_exc()))
|
||||
LOG.exception("Error in parsing template:")
|
||||
output["failures"].append({
|
||||
"file": file_path,
|
||||
|
@ -304,7 +323,8 @@ class Runner(object):
|
|||
print(syntribos.SEP)
|
||||
|
||||
@classmethod
|
||||
def run_given_tests(cls, list_of_tests, file_path, req_str):
|
||||
def run_given_tests(cls, list_of_tests, file_path, req_str,
|
||||
meta_vars=None):
|
||||
"""Loads all the templates and runs all the given tests
|
||||
|
||||
This method calls run_test method to run each of the tests one
|
||||
|
@ -335,7 +355,13 @@ class Runner(object):
|
|||
else:
|
||||
result_string = result_string.ljust(60)
|
||||
LOG.debug(log_string)
|
||||
test_class.send_init_request(file_path, req_str)
|
||||
try:
|
||||
test_class.send_init_request(file_path, req_str, meta_vars)
|
||||
except Exception:
|
||||
print("Error in parsing template:\n \t{0}\n".format(
|
||||
traceback.format_exc()))
|
||||
LOG.exception("Error in parsing template:")
|
||||
break
|
||||
test_cases = list(
|
||||
test_class.get_test_cases(file_path, req_str))
|
||||
if len(test_cases) > 0:
|
||||
|
|
|
@ -46,8 +46,9 @@ class AuthTestCase(base.BaseTestCase):
|
|||
data=cls.request.data)
|
||||
|
||||
@classmethod
|
||||
def send_init_request(cls, filename, file_content):
|
||||
super(AuthTestCase, cls).send_init_request(filename, file_content)
|
||||
def send_init_request(cls, filename, file_content, meta_vars):
|
||||
super(AuthTestCase, cls).send_init_request(filename,
|
||||
file_content, meta_vars)
|
||||
cls.request = cls.init_req.get_prepared_copy()
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -122,7 +122,7 @@ class BaseTestCase(unittest.TestCase):
|
|||
yield cls
|
||||
|
||||
@classmethod
|
||||
def create_init_request(cls, filename, file_content):
|
||||
def create_init_request(cls, filename, file_content, meta_vars):
|
||||
"""Parses template and creates init request object
|
||||
|
||||
This method does not send the initial request, instead, it only creates
|
||||
|
@ -132,13 +132,13 @@ class BaseTestCase(unittest.TestCase):
|
|||
:param str file_content: content of template file as string
|
||||
"""
|
||||
request_obj = parser.create_request(
|
||||
file_content, CONF.syntribos.endpoint)
|
||||
file_content, CONF.syntribos.endpoint, meta_vars)
|
||||
cls.init_req = request_obj
|
||||
cls.init_resp = None
|
||||
cls.init_signals = None
|
||||
|
||||
@classmethod
|
||||
def send_init_request(cls, filename, file_content):
|
||||
def send_init_request(cls, filename, file_content, meta_vars):
|
||||
"""Parses template, creates init request object, and sends init request
|
||||
|
||||
This method sends the initial request, which is the request created
|
||||
|
@ -149,7 +149,7 @@ class BaseTestCase(unittest.TestCase):
|
|||
:param str file_content: content of template file as string
|
||||
"""
|
||||
cls.init_req = parser.create_request(
|
||||
file_content, CONF.syntribos.endpoint)
|
||||
file_content, CONF.syntribos.endpoint, meta_vars)
|
||||
|
||||
prepared_copy = cls.init_req.get_prepared_copy()
|
||||
cls.init_resp, cls.init_signals = cls.client.send_request(
|
||||
|
@ -212,7 +212,7 @@ class BaseTestCase(unittest.TestCase):
|
|||
self.test_case()
|
||||
except Exception as e:
|
||||
self.errors += e
|
||||
raise e
|
||||
raise
|
||||
if self.failures:
|
||||
raise AssertionError
|
||||
|
||||
|
|
|
@ -57,10 +57,6 @@ class BaseFuzzTestCase(base.BaseTestCase):
|
|||
"exiting...".format(cls.test_name))
|
||||
exit(1)
|
||||
|
||||
@classmethod
|
||||
def send_init_request(cls, filename, file_content):
|
||||
super(BaseFuzzTestCase, cls).send_init_request(filename, file_content)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""being used as a setup test not."""
|
||||
|
|
|
@ -25,7 +25,7 @@ def fuzz_request(req, strings, fuzz_type, name_prefix):
|
|||
creates a RequestObject from the parameters of the model.
|
||||
|
||||
:param req: The RequestObject to be fuzzed
|
||||
:type req: :class:`syntribos.clients.http.models.RequestObject`
|
||||
:type req: :class:`syntribos.clients.http.parser.RequestObject`
|
||||
:param list strings: List of strings to fuzz with
|
||||
:param str fuzz_type: What attribute of the RequestObject to fuzz
|
||||
:param name_prefix: (Used for ImpactedParameter)
|
||||
|
|
|
@ -17,7 +17,7 @@ from xml.etree import ElementTree
|
|||
import six
|
||||
import testtools
|
||||
|
||||
from syntribos.clients.http.models import RequestObject
|
||||
from syntribos.clients.http.parser import RequestObject
|
||||
import syntribos.tests.fuzz.datagen as fuzz_datagen
|
||||
|
||||
action_field = "ACTION_FIELD:"
|
||||
|
|
|
@ -18,12 +18,12 @@ import xml.etree.ElementTree as ElementTree
|
|||
import six
|
||||
import testtools
|
||||
|
||||
import syntribos.clients.http.models as mod
|
||||
from syntribos.clients.http.parser import _iterators
|
||||
from syntribos.clients.http.parser import RequestHelperMixin as rhm
|
||||
from syntribos.clients.http.parser import RequestObject as ro
|
||||
|
||||
endpoint = "http://test.com"
|
||||
action_field = "ACTION_FIELD:"
|
||||
rhm = mod.RequestHelperMixin
|
||||
ro = mod.RequestObject
|
||||
|
||||
|
||||
def get_fake_generator():
|
||||
|
@ -139,7 +139,7 @@ class HTTPModelsUnittest(testtools.TestCase):
|
|||
def test_run_iters_global_iterators(self):
|
||||
"""Tests _replace_iter by modifying _iterators global object."""
|
||||
u = str(uuid.uuid4()).replace("-", "")
|
||||
mod._iterators[u] = get_fake_generator()
|
||||
_iterators[u] = get_fake_generator()
|
||||
_str = "/v1/{0}/test".format(u)
|
||||
res = rhm._run_iters(_str, action_field)
|
||||
self.assertEqual("/v1/{0}/test".format(0), res)
|
||||
|
|
|
@ -38,11 +38,6 @@ class HTTPParserUnittest(testtools.TestCase):
|
|||
self.assertEqual({"var": "val", "var2": "val2"}, params)
|
||||
self.assertEqual("HTTP/1.1", version)
|
||||
|
||||
def test_url_line_parser_invalid_version(self):
|
||||
"""Tests parsing an invalid HTTP version."""
|
||||
line = "GET /path?var=val&var2=val2 HTTP"
|
||||
self.assertRaises(ValueError, parser._parse_url_line, line, endpoint)
|
||||
|
||||
def test_url_line_parser_invalid_method(self):
|
||||
"""Tests parsing an invalid HTTP method."""
|
||||
line = "DERP /path?var=val&var2=val2 HTTP/1.1"
|
||||
|
|
Loading…
Reference in New Issue