1020 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1020 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import unicode_literals
 | |
| 
 | |
| import functools
 | |
| from collections import defaultdict
 | |
| import datetime
 | |
| import json
 | |
| import logging
 | |
| import re
 | |
| import io
 | |
| import requests
 | |
| 
 | |
| import pytz
 | |
| 
 | |
| from moto.core.exceptions import DryRunClientError
 | |
| 
 | |
| from jinja2 import Environment, DictLoader, TemplateNotFound
 | |
| 
 | |
| import six
 | |
| from six.moves.urllib.parse import parse_qs, parse_qsl, urlparse
 | |
| 
 | |
| import xmltodict
 | |
| from werkzeug.exceptions import HTTPException
 | |
| 
 | |
| import boto3
 | |
| from moto.compat import OrderedDict
 | |
| from moto.core.utils import camelcase_to_underscores, method_names_from_class
 | |
| from moto import settings
 | |
| 
 | |
| log = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| def _decode_dict(d):
 | |
|     decoded = OrderedDict()
 | |
|     for key, value in d.items():
 | |
|         if isinstance(key, six.binary_type):
 | |
|             newkey = key.decode("utf-8")
 | |
|         elif isinstance(key, (list, tuple)):
 | |
|             newkey = []
 | |
|             for k in key:
 | |
|                 if isinstance(k, six.binary_type):
 | |
|                     newkey.append(k.decode("utf-8"))
 | |
|                 else:
 | |
|                     newkey.append(k)
 | |
|         else:
 | |
|             newkey = key
 | |
| 
 | |
|         if isinstance(value, six.binary_type):
 | |
|             newvalue = value.decode("utf-8")
 | |
|         elif isinstance(value, (list, tuple)):
 | |
|             newvalue = []
 | |
|             for v in value:
 | |
|                 if isinstance(v, six.binary_type):
 | |
|                     newvalue.append(v.decode("utf-8"))
 | |
|                 else:
 | |
|                     newvalue.append(v)
 | |
|         else:
 | |
|             newvalue = value
 | |
| 
 | |
|         decoded[newkey] = newvalue
 | |
|     return decoded
 | |
| 
 | |
| 
 | |
| class DynamicDictLoader(DictLoader):
 | |
|     """
 | |
|       Note: There's a bug in jinja2 pre-2.7.3 DictLoader where caching does not work.
 | |
|         Including the fixed (current) method version here to ensure performance benefit
 | |
|         even for those using older jinja versions.
 | |
|     """
 | |
| 
 | |
|     def get_source(self, environment, template):
 | |
|         if template in self.mapping:
 | |
|             source = self.mapping[template]
 | |
|             return source, None, lambda: source == self.mapping.get(template)
 | |
|         raise TemplateNotFound(template)
 | |
| 
 | |
|     def update(self, mapping):
 | |
|         self.mapping.update(mapping)
 | |
| 
 | |
|     def contains(self, template):
 | |
|         return bool(template in self.mapping)
 | |
| 
 | |
| 
 | |
| class _TemplateEnvironmentMixin(object):
 | |
|     LEFT_PATTERN = re.compile(r"[\s\n]+<")
 | |
|     RIGHT_PATTERN = re.compile(r">[\s\n]+")
 | |
| 
 | |
|     def __init__(self):
 | |
|         super(_TemplateEnvironmentMixin, self).__init__()
 | |
|         self.loader = DynamicDictLoader({})
 | |
|         self.environment = Environment(
 | |
|             loader=self.loader, autoescape=self.should_autoescape
 | |
|         )
 | |
| 
 | |
|     @property
 | |
|     def should_autoescape(self):
 | |
|         # Allow for subclass to overwrite
 | |
|         return False
 | |
| 
 | |
|     def contains_template(self, template_id):
 | |
|         return self.loader.contains(template_id)
 | |
| 
 | |
|     def response_template(self, source):
 | |
|         template_id = id(source)
 | |
|         if not self.contains_template(template_id):
 | |
|             collapsed = re.sub(
 | |
|                 self.RIGHT_PATTERN, ">", re.sub(self.LEFT_PATTERN, "<", source)
 | |
|             )
 | |
|             self.loader.update({template_id: collapsed})
 | |
|             self.environment = Environment(
 | |
|                 loader=self.loader,
 | |
|                 autoescape=self.should_autoescape,
 | |
|                 trim_blocks=True,
 | |
|                 lstrip_blocks=True,
 | |
|             )
 | |
|         return self.environment.get_template(template_id)
 | |
| 
 | |
| 
 | |
| class ActionAuthenticatorMixin(object):
 | |
| 
 | |
|     request_count = 0
 | |
| 
 | |
|     def _authenticate_and_authorize_action(self, iam_request_cls):
 | |
|         if (
 | |
|             ActionAuthenticatorMixin.request_count
 | |
|             >= settings.INITIAL_NO_AUTH_ACTION_COUNT
 | |
|         ):
 | |
|             iam_request = iam_request_cls(
 | |
|                 method=self.method, path=self.path, data=self.data, headers=self.headers
 | |
|             )
 | |
|             iam_request.check_signature()
 | |
|             iam_request.check_action_permitted()
 | |
|         else:
 | |
|             ActionAuthenticatorMixin.request_count += 1
 | |
| 
 | |
|     def _authenticate_and_authorize_normal_action(self):
 | |
|         from moto.iam.access_control import IAMRequest
 | |
| 
 | |
|         self._authenticate_and_authorize_action(IAMRequest)
 | |
| 
 | |
|     def _authenticate_and_authorize_s3_action(self):
 | |
|         from moto.iam.access_control import S3IAMRequest
 | |
| 
 | |
|         self._authenticate_and_authorize_action(S3IAMRequest)
 | |
| 
 | |
|     @staticmethod
 | |
|     def set_initial_no_auth_action_count(initial_no_auth_action_count):
 | |
|         def decorator(function):
 | |
|             def wrapper(*args, **kwargs):
 | |
|                 if settings.TEST_SERVER_MODE:
 | |
|                     response = requests.post(
 | |
|                         "http://localhost:5000/moto-api/reset-auth",
 | |
|                         data=str(initial_no_auth_action_count).encode(),
 | |
|                     )
 | |
|                     original_initial_no_auth_action_count = response.json()[
 | |
|                         "PREVIOUS_INITIAL_NO_AUTH_ACTION_COUNT"
 | |
|                     ]
 | |
|                 else:
 | |
|                     original_initial_no_auth_action_count = (
 | |
|                         settings.INITIAL_NO_AUTH_ACTION_COUNT
 | |
|                     )
 | |
|                     original_request_count = ActionAuthenticatorMixin.request_count
 | |
|                     settings.INITIAL_NO_AUTH_ACTION_COUNT = initial_no_auth_action_count
 | |
|                     ActionAuthenticatorMixin.request_count = 0
 | |
|                 try:
 | |
|                     result = function(*args, **kwargs)
 | |
|                 finally:
 | |
|                     if settings.TEST_SERVER_MODE:
 | |
|                         requests.post(
 | |
|                             "http://localhost:5000/moto-api/reset-auth",
 | |
|                             data=str(original_initial_no_auth_action_count).encode(),
 | |
|                         )
 | |
|                     else:
 | |
|                         ActionAuthenticatorMixin.request_count = original_request_count
 | |
|                         settings.INITIAL_NO_AUTH_ACTION_COUNT = (
 | |
|                             original_initial_no_auth_action_count
 | |
|                         )
 | |
|                 return result
 | |
| 
 | |
|             functools.update_wrapper(wrapper, function)
 | |
|             wrapper.__wrapped__ = function
 | |
|             return wrapper
 | |
| 
 | |
|         return decorator
 | |
| 
 | |
| 
 | |
| class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
 | |
| 
 | |
|     default_region = "us-east-1"
 | |
|     # to extract region, use [^.]
 | |
|     region_regex = re.compile(r"\.(?P<region>[a-z]{2}-[a-z]+-\d{1})\.amazonaws\.com")
 | |
|     region_from_useragent_regex = re.compile(
 | |
|         r"region/(?P<region>[a-z]{2}-[a-z]+-\d{1})"
 | |
|     )
 | |
|     param_list_regex = re.compile(r"(.*)\.(\d+)\.")
 | |
|     access_key_regex = re.compile(
 | |
|         r"AWS.*(?P<access_key>(?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9]))[:/]"
 | |
|     )
 | |
|     aws_service_spec = None
 | |
| 
 | |
|     @classmethod
 | |
|     def dispatch(cls, *args, **kwargs):
 | |
|         return cls()._dispatch(*args, **kwargs)
 | |
| 
 | |
|     def setup_class(self, request, full_url, headers):
 | |
|         querystring = OrderedDict()
 | |
|         if hasattr(request, "body"):
 | |
|             # Boto
 | |
|             self.body = request.body
 | |
|         else:
 | |
|             # Flask server
 | |
| 
 | |
|             # FIXME: At least in Flask==0.10.1, request.data is an empty string
 | |
|             # and the information we want is in request.form. Keeping self.body
 | |
|             # definition for back-compatibility
 | |
|             self.body = request.data
 | |
| 
 | |
|             querystring = OrderedDict()
 | |
|             for key, value in request.form.items():
 | |
|                 querystring[key] = [value]
 | |
| 
 | |
|         raw_body = self.body
 | |
|         if isinstance(self.body, six.binary_type):
 | |
|             self.body = self.body.decode("utf-8")
 | |
| 
 | |
|         if not querystring:
 | |
|             querystring.update(
 | |
|                 parse_qs(urlparse(full_url).query, keep_blank_values=True)
 | |
|             )
 | |
|         if not querystring:
 | |
|             if (
 | |
|                 "json" in request.headers.get("content-type", [])
 | |
|                 and self.aws_service_spec
 | |
|             ):
 | |
|                 decoded = json.loads(self.body)
 | |
| 
 | |
|                 target = request.headers.get("x-amz-target") or request.headers.get(
 | |
|                     "X-Amz-Target"
 | |
|                 )
 | |
|                 service, method = target.split(".")
 | |
|                 input_spec = self.aws_service_spec.input_spec(method)
 | |
|                 flat = flatten_json_request_body("", decoded, input_spec)
 | |
|                 for key, value in flat.items():
 | |
|                     querystring[key] = [value]
 | |
|             elif self.body:
 | |
|                 try:
 | |
|                     querystring.update(
 | |
|                         OrderedDict(
 | |
|                             (key, [value])
 | |
|                             for key, value in parse_qsl(
 | |
|                                 raw_body, keep_blank_values=True
 | |
|                             )
 | |
|                         )
 | |
|                     )
 | |
|                 except UnicodeEncodeError:
 | |
|                     pass  # ignore encoding errors, as the body may not contain a legitimate querystring
 | |
|         if not querystring:
 | |
|             querystring.update(headers)
 | |
| 
 | |
|         try:
 | |
|             querystring = _decode_dict(querystring)
 | |
|         except UnicodeDecodeError:
 | |
|             pass  # ignore decoding errors, as the body may not contain a legitimate querystring
 | |
| 
 | |
|         self.uri = full_url
 | |
|         self.path = urlparse(full_url).path
 | |
|         self.querystring = querystring
 | |
|         self.data = querystring
 | |
|         self.method = request.method
 | |
|         self.region = self.get_region_from_url(request, full_url)
 | |
|         self.uri_match = None
 | |
| 
 | |
|         self.headers = request.headers
 | |
|         if "host" not in self.headers:
 | |
|             self.headers["host"] = urlparse(full_url).netloc
 | |
|         self.response_headers = {"server": "amazon.com"}
 | |
| 
 | |
|     def get_region_from_url(self, request, full_url):
 | |
|         url_match = self.region_regex.search(full_url)
 | |
|         user_agent_match = self.region_from_useragent_regex.search(
 | |
|             request.headers.get("User-Agent", "")
 | |
|         )
 | |
|         if url_match:
 | |
|             region = url_match.group(1)
 | |
|         elif user_agent_match:
 | |
|             region = user_agent_match.group(1)
 | |
|         elif (
 | |
|             "Authorization" in request.headers
 | |
|             and "AWS4" in request.headers["Authorization"]
 | |
|         ):
 | |
|             region = request.headers["Authorization"].split(",")[0].split("/")[2]
 | |
|         else:
 | |
|             region = self.default_region
 | |
|         return region
 | |
| 
 | |
|     def get_current_user(self):
 | |
|         """
 | |
|         Returns the access key id used in this request as the current user id
 | |
|         """
 | |
|         if "Authorization" in self.headers:
 | |
|             match = self.access_key_regex.search(self.headers["Authorization"])
 | |
|             if match:
 | |
|                 return match.group(1)
 | |
| 
 | |
|         if self.querystring.get("AWSAccessKeyId"):
 | |
|             return self.querystring.get("AWSAccessKeyId")
 | |
|         else:
 | |
|             # Should we raise an unauthorized exception instead?
 | |
|             return "111122223333"
 | |
| 
 | |
|     def _dispatch(self, request, full_url, headers):
 | |
|         self.setup_class(request, full_url, headers)
 | |
|         return self.call_action()
 | |
| 
 | |
|     def uri_to_regexp(self, uri):
 | |
|         """converts uri w/ placeholder to regexp
 | |
|           '/cars/{carName}/drivers/{DriverName}'
 | |
|         -> '^/cars/.*/drivers/[^/]*$'
 | |
| 
 | |
|           '/cars/{carName}/drivers/{DriverName}/drive'
 | |
|         -> '^/cars/.*/drivers/.*/drive$'
 | |
| 
 | |
|         """
 | |
| 
 | |
|         def _convert(elem, is_last):
 | |
|             if not re.match("^{.*}$", elem):
 | |
|                 return elem
 | |
|             name = elem.replace("{", "").replace("}", "").replace("+", "")
 | |
|             if is_last:
 | |
|                 return "(?P<%s>[^/]*)" % name
 | |
|             return "(?P<%s>.*)" % name
 | |
| 
 | |
|         elems = uri.split("/")
 | |
|         num_elems = len(elems)
 | |
|         regexp = "^{}$".format(
 | |
|             "/".join(
 | |
|                 [_convert(elem, (i == num_elems - 1)) for i, elem in enumerate(elems)]
 | |
|             )
 | |
|         )
 | |
|         return regexp
 | |
| 
 | |
|     def _get_action_from_method_and_request_uri(self, method, request_uri):
 | |
|         """basically used for `rest-json` APIs
 | |
|         You can refer to example from link below
 | |
|         https://github.com/boto/botocore/blob/develop/botocore/data/iot/2015-05-28/service-2.json
 | |
|         """
 | |
| 
 | |
|         # service response class should have 'SERVICE_NAME' class member,
 | |
|         # if you want to get action from method and url
 | |
|         if not hasattr(self, "SERVICE_NAME"):
 | |
|             return None
 | |
|         service = self.SERVICE_NAME
 | |
|         conn = boto3.client(service, region_name=self.region)
 | |
| 
 | |
|         # make cache if it does not exist yet
 | |
|         if not hasattr(self, "method_urls"):
 | |
|             self.method_urls = defaultdict(lambda: defaultdict(str))
 | |
|             op_names = conn._service_model.operation_names
 | |
|             for op_name in op_names:
 | |
|                 op_model = conn._service_model.operation_model(op_name)
 | |
|                 _method = op_model.http["method"]
 | |
|                 uri_regexp = self.uri_to_regexp(op_model.http["requestUri"])
 | |
|                 self.method_urls[_method][uri_regexp] = op_model.name
 | |
|         regexp_and_names = self.method_urls[method]
 | |
|         for regexp, name in regexp_and_names.items():
 | |
|             match = re.match(regexp, request_uri)
 | |
|             self.uri_match = match
 | |
|             if match:
 | |
|                 return name
 | |
|         return None
 | |
| 
 | |
|     def _get_action(self):
 | |
|         action = self.querystring.get("Action", [""])[0]
 | |
|         if not action:  # Some services use a header for the action
 | |
|             # Headers are case-insensitive. Probably a better way to do this.
 | |
|             match = self.headers.get("x-amz-target") or self.headers.get("X-Amz-Target")
 | |
|             if match:
 | |
|                 action = match.split(".")[-1]
 | |
|         # get action from method and uri
 | |
|         if not action:
 | |
|             return self._get_action_from_method_and_request_uri(self.method, self.path)
 | |
|         return action
 | |
| 
 | |
|     def call_action(self):
 | |
|         headers = self.response_headers
 | |
| 
 | |
|         try:
 | |
|             self._authenticate_and_authorize_normal_action()
 | |
|         except HTTPException as http_error:
 | |
|             response = http_error.description, dict(status=http_error.code)
 | |
|             return self._send_response(headers, response)
 | |
| 
 | |
|         action = camelcase_to_underscores(self._get_action())
 | |
|         method_names = method_names_from_class(self.__class__)
 | |
|         if action in method_names:
 | |
|             method = getattr(self, action)
 | |
|             try:
 | |
|                 response = method()
 | |
|             except HTTPException as http_error:
 | |
|                 response = http_error.description, dict(status=http_error.code)
 | |
| 
 | |
|             if isinstance(response, six.string_types):
 | |
|                 return 200, headers, response
 | |
|             else:
 | |
|                 return self._send_response(headers, response)
 | |
| 
 | |
|         if not action:
 | |
|             return 404, headers, ""
 | |
| 
 | |
|         raise NotImplementedError(
 | |
|             "The {0} action has not been implemented".format(action)
 | |
|         )
 | |
| 
 | |
|     @staticmethod
 | |
|     def _send_response(headers, response):
 | |
|         if len(response) == 2:
 | |
|             body, new_headers = response
 | |
|         else:
 | |
|             status, new_headers, body = response
 | |
|         status = new_headers.get("status", 200)
 | |
|         headers.update(new_headers)
 | |
|         # Cast status to string
 | |
|         if "status" in headers:
 | |
|             headers["status"] = str(headers["status"])
 | |
|         return status, headers, body
 | |
| 
 | |
|     def _get_param(self, param_name, if_none=None):
 | |
|         val = self.querystring.get(param_name)
 | |
|         if val is not None:
 | |
|             return val[0]
 | |
| 
 | |
|         # try to get json body parameter
 | |
|         if self.body is not None:
 | |
|             try:
 | |
|                 return json.loads(self.body)[param_name]
 | |
|             except ValueError:
 | |
|                 pass
 | |
|             except KeyError:
 | |
|                 pass
 | |
|         # try to get path parameter
 | |
|         if self.uri_match:
 | |
|             try:
 | |
|                 return self.uri_match.group(param_name)
 | |
|             except IndexError:
 | |
|                 # do nothing if param is not found
 | |
|                 pass
 | |
|         return if_none
 | |
| 
 | |
|     def _get_int_param(self, param_name, if_none=None):
 | |
|         val = self._get_param(param_name)
 | |
|         if val is not None:
 | |
|             return int(val)
 | |
|         return if_none
 | |
| 
 | |
|     def _get_bool_param(self, param_name, if_none=None):
 | |
|         val = self._get_param(param_name)
 | |
|         if val is not None:
 | |
|             if val.lower() == "true":
 | |
|                 return True
 | |
|             elif val.lower() == "false":
 | |
|                 return False
 | |
|         return if_none
 | |
| 
 | |
|     def _get_multi_param_helper(self, param_prefix):
 | |
|         value_dict = dict()
 | |
|         tracked_prefixes = set()  # prefixes which have already been processed
 | |
| 
 | |
|         def is_tracked(name_param):
 | |
|             for prefix_loop in tracked_prefixes:
 | |
|                 if name_param.startswith(prefix_loop):
 | |
|                     return True
 | |
|             return False
 | |
| 
 | |
|         for name, value in self.querystring.items():
 | |
|             if is_tracked(name) or not name.startswith(param_prefix):
 | |
|                 continue
 | |
| 
 | |
|             if len(name) > len(param_prefix) and not name[
 | |
|                 len(param_prefix) :
 | |
|             ].startswith("."):
 | |
|                 continue
 | |
| 
 | |
|             match = (
 | |
|                 self.param_list_regex.search(name[len(param_prefix) :])
 | |
|                 if len(name) > len(param_prefix)
 | |
|                 else None
 | |
|             )
 | |
|             if match:
 | |
|                 prefix = param_prefix + match.group(1)
 | |
|                 value = self._get_multi_param(prefix)
 | |
|                 tracked_prefixes.add(prefix)
 | |
|                 name = prefix
 | |
|                 value_dict[name] = value
 | |
|             else:
 | |
|                 value_dict[name] = value[0]
 | |
| 
 | |
|         if not value_dict:
 | |
|             return None
 | |
| 
 | |
|         if len(value_dict) > 1:
 | |
|             # strip off period prefix
 | |
|             value_dict = {
 | |
|                 name[len(param_prefix) + 1 :]: value
 | |
|                 for name, value in value_dict.items()
 | |
|             }
 | |
|         else:
 | |
|             value_dict = list(value_dict.values())[0]
 | |
| 
 | |
|         return value_dict
 | |
| 
 | |
|     def _get_multi_param(self, param_prefix):
 | |
|         """
 | |
|         Given a querystring of ?LaunchConfigurationNames.member.1=my-test-1&LaunchConfigurationNames.member.2=my-test-2
 | |
|         this will return ['my-test-1', 'my-test-2']
 | |
|         """
 | |
|         if param_prefix.endswith("."):
 | |
|             prefix = param_prefix
 | |
|         else:
 | |
|             prefix = param_prefix + "."
 | |
|         values = []
 | |
|         index = 1
 | |
|         while True:
 | |
|             value_dict = self._get_multi_param_helper(prefix + str(index))
 | |
|             if not value_dict and value_dict != "":
 | |
|                 break
 | |
| 
 | |
|             values.append(value_dict)
 | |
|             index += 1
 | |
| 
 | |
|         return values
 | |
| 
 | |
|     def _get_dict_param(self, param_prefix):
 | |
|         """
 | |
|         Given a parameter dict of
 | |
|         {
 | |
|             'Instances.SlaveInstanceType': ['m1.small'],
 | |
|             'Instances.InstanceCount': ['1']
 | |
|         }
 | |
| 
 | |
|         returns
 | |
|         {
 | |
|             "SlaveInstanceType": "m1.small",
 | |
|             "InstanceCount": "1",
 | |
|         }
 | |
|         """
 | |
|         params = {}
 | |
|         for key, value in self.querystring.items():
 | |
|             if key.startswith(param_prefix):
 | |
|                 params[camelcase_to_underscores(key.replace(param_prefix, ""))] = value[
 | |
|                     0
 | |
|                 ]
 | |
|         return params
 | |
| 
 | |
|     def _get_list_prefix(self, param_prefix):
 | |
|         """
 | |
|         Given a query dict like
 | |
|         {
 | |
|             'Steps.member.1.Name': ['example1'],
 | |
|             'Steps.member.1.ActionOnFailure': ['TERMINATE_JOB_FLOW'],
 | |
|             'Steps.member.1.HadoopJarStep.Jar': ['streaming1.jar'],
 | |
|             'Steps.member.2.Name': ['example2'],
 | |
|             'Steps.member.2.ActionOnFailure': ['TERMINATE_JOB_FLOW'],
 | |
|             'Steps.member.2.HadoopJarStep.Jar': ['streaming2.jar'],
 | |
|         }
 | |
| 
 | |
|         returns
 | |
|         [{
 | |
|             'name': u'example1',
 | |
|             'action_on_failure': u'TERMINATE_JOB_FLOW',
 | |
|             'hadoop_jar_step._jar': u'streaming1.jar',
 | |
|         }, {
 | |
|             'name': u'example2',
 | |
|             'action_on_failure': u'TERMINATE_JOB_FLOW',
 | |
|             'hadoop_jar_step._jar': u'streaming2.jar',
 | |
|         }]
 | |
|         """
 | |
|         results = []
 | |
|         param_index = 1
 | |
|         while True:
 | |
|             index_prefix = "{0}.{1}.".format(param_prefix, param_index)
 | |
|             new_items = {}
 | |
|             for key, value in self.querystring.items():
 | |
|                 if key.startswith(index_prefix):
 | |
|                     new_items[
 | |
|                         camelcase_to_underscores(key.replace(index_prefix, ""))
 | |
|                     ] = value[0]
 | |
|             if not new_items:
 | |
|                 break
 | |
|             results.append(new_items)
 | |
|             param_index += 1
 | |
|         return results
 | |
| 
 | |
|     def _get_map_prefix(self, param_prefix, key_end=".key", value_end=".value"):
 | |
|         results = {}
 | |
|         param_index = 1
 | |
|         while 1:
 | |
|             index_prefix = "{0}.{1}.".format(param_prefix, param_index)
 | |
| 
 | |
|             k, v = None, None
 | |
|             for key, value in self.querystring.items():
 | |
|                 if key.startswith(index_prefix):
 | |
|                     if key.endswith(key_end):
 | |
|                         k = value[0]
 | |
|                     elif key.endswith(value_end):
 | |
|                         v = value[0]
 | |
| 
 | |
|             if not (k and v):
 | |
|                 break
 | |
| 
 | |
|             results[k] = v
 | |
|             param_index += 1
 | |
| 
 | |
|         return results
 | |
| 
 | |
|     def _parse_tag_specification(self, param_prefix):
 | |
|         tags = self._get_list_prefix(param_prefix)
 | |
| 
 | |
|         results = defaultdict(dict)
 | |
|         for tag in tags:
 | |
|             resource_type = tag.pop("resource_type")
 | |
| 
 | |
|             param_index = 1
 | |
|             while True:
 | |
|                 key_name = "tag.{0}._key".format(param_index)
 | |
|                 value_name = "tag.{0}._value".format(param_index)
 | |
| 
 | |
|                 try:
 | |
|                     results[resource_type][tag[key_name]] = tag[value_name]
 | |
|                 except KeyError:
 | |
|                     break
 | |
|                 param_index += 1
 | |
| 
 | |
|         return results
 | |
| 
 | |
|     def _get_object_map(self, prefix, name="Name", value="Value"):
 | |
|         """
 | |
|         Given a query dict like
 | |
|         {
 | |
|             Prefix.1.Name: [u'event'],
 | |
|             Prefix.1.Value.StringValue: [u'order_cancelled'],
 | |
|             Prefix.1.Value.DataType: [u'String'],
 | |
|             Prefix.2.Name: [u'store'],
 | |
|             Prefix.2.Value.StringValue: [u'example_corp'],
 | |
|             Prefix.2.Value.DataType [u'String'],
 | |
|         }
 | |
| 
 | |
|         returns
 | |
|         {
 | |
|             'event': {
 | |
|                 'DataType': 'String',
 | |
|                 'StringValue': 'example_corp'
 | |
|             },
 | |
|             'store': {
 | |
|                 'DataType': 'String',
 | |
|                 'StringValue': 'order_cancelled'
 | |
|             }
 | |
|         }
 | |
|         """
 | |
|         object_map = {}
 | |
|         index = 1
 | |
|         while True:
 | |
|             # Loop through looking for keys representing object name
 | |
|             name_key = "{0}.{1}.{2}".format(prefix, index, name)
 | |
|             obj_name = self.querystring.get(name_key)
 | |
|             if not obj_name:
 | |
|                 # Found all keys
 | |
|                 break
 | |
| 
 | |
|             obj = {}
 | |
|             value_key_prefix = "{0}.{1}.{2}.".format(prefix, index, value)
 | |
|             for k, v in self.querystring.items():
 | |
|                 if k.startswith(value_key_prefix):
 | |
|                     _, value_key = k.split(value_key_prefix, 1)
 | |
|                     obj[value_key] = v[0]
 | |
| 
 | |
|             object_map[obj_name[0]] = obj
 | |
| 
 | |
|             index += 1
 | |
| 
 | |
|         return object_map
 | |
| 
 | |
|     @property
 | |
|     def request_json(self):
 | |
|         return "JSON" in self.querystring.get("ContentType", [])
 | |
| 
 | |
|     def is_not_dryrun(self, action):
 | |
|         if "true" in self.querystring.get("DryRun", ["false"]):
 | |
|             message = (
 | |
|                 "An error occurred (DryRunOperation) when calling the %s operation: Request would have succeeded, but DryRun flag is set"
 | |
|                 % action
 | |
|             )
 | |
|             raise DryRunClientError(error_type="DryRunOperation", message=message)
 | |
|         return True
 | |
| 
 | |
| 
 | |
| class MotoAPIResponse(BaseResponse):
 | |
|     def reset_response(self, request, full_url, headers):
 | |
|         if request.method == "POST":
 | |
|             from .models import moto_api_backend
 | |
| 
 | |
|             moto_api_backend.reset()
 | |
|             return 200, {}, json.dumps({"status": "ok"})
 | |
|         return 400, {}, json.dumps({"Error": "Need to POST to reset Moto"})
 | |
| 
 | |
|     def reset_auth_response(self, request, full_url, headers):
 | |
|         if request.method == "POST":
 | |
|             previous_initial_no_auth_action_count = (
 | |
|                 settings.INITIAL_NO_AUTH_ACTION_COUNT
 | |
|             )
 | |
|             settings.INITIAL_NO_AUTH_ACTION_COUNT = float(request.data.decode())
 | |
|             ActionAuthenticatorMixin.request_count = 0
 | |
|             return (
 | |
|                 200,
 | |
|                 {},
 | |
|                 json.dumps(
 | |
|                     {
 | |
|                         "status": "ok",
 | |
|                         "PREVIOUS_INITIAL_NO_AUTH_ACTION_COUNT": str(
 | |
|                             previous_initial_no_auth_action_count
 | |
|                         ),
 | |
|                     }
 | |
|                 ),
 | |
|             )
 | |
|         return 400, {}, json.dumps({"Error": "Need to POST to reset Moto Auth"})
 | |
| 
 | |
|     def model_data(self, request, full_url, headers):
 | |
|         from moto.core.models import model_data
 | |
| 
 | |
|         results = {}
 | |
|         for service in sorted(model_data):
 | |
|             models = model_data[service]
 | |
|             results[service] = {}
 | |
|             for name in sorted(models):
 | |
|                 model = models[name]
 | |
|                 results[service][name] = []
 | |
|                 for instance in model.instances:
 | |
|                     inst_result = {}
 | |
|                     for attr in dir(instance):
 | |
|                         if not attr.startswith("_"):
 | |
|                             try:
 | |
|                                 json.dumps(getattr(instance, attr))
 | |
|                             except TypeError:
 | |
|                                 pass
 | |
|                             else:
 | |
|                                 inst_result[attr] = getattr(instance, attr)
 | |
|                     results[service][name].append(inst_result)
 | |
|         return 200, {"Content-Type": "application/javascript"}, json.dumps(results)
 | |
| 
 | |
|     def dashboard(self, request, full_url, headers):
 | |
|         from flask import render_template
 | |
| 
 | |
|         return render_template("dashboard.html")
 | |
| 
 | |
| 
 | |
| class _RecursiveDictRef(object):
 | |
|     """Store a recursive reference to dict."""
 | |
| 
 | |
|     def __init__(self):
 | |
|         self.key = None
 | |
|         self.dic = {}
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "{!r}".format(self.dic)
 | |
| 
 | |
|     def __getattr__(self, key):
 | |
|         return self.dic.__getattr__(key)
 | |
| 
 | |
|     def __getitem__(self, key):
 | |
|         return self.dic.__getitem__(key)
 | |
| 
 | |
|     def set_reference(self, key, dic):
 | |
|         """Set the RecursiveDictRef object to keep reference to dict object
 | |
|         (dic) at the key.
 | |
| 
 | |
|         """
 | |
|         self.key = key
 | |
|         self.dic = dic
 | |
| 
 | |
| 
 | |
| class AWSServiceSpec(object):
 | |
|     """Parse data model from botocore. This is used to recover type info
 | |
|     for fields in AWS API XML response.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, path):
 | |
|         # Importing pkg_resources takes ~60ms; keep it local
 | |
|         from pkg_resources import resource_filename  # noqa
 | |
| 
 | |
|         self.path = resource_filename("botocore", path)
 | |
|         with io.open(self.path, "r", encoding="utf-8") as f:
 | |
|             spec = json.load(f)
 | |
|         self.metadata = spec["metadata"]
 | |
|         self.operations = spec["operations"]
 | |
|         self.shapes = spec["shapes"]
 | |
| 
 | |
|     def input_spec(self, operation):
 | |
|         try:
 | |
|             op = self.operations[operation]
 | |
|         except KeyError:
 | |
|             raise ValueError("Invalid operation: {}".format(operation))
 | |
|         if "input" not in op:
 | |
|             return {}
 | |
|         shape = self.shapes[op["input"]["shape"]]
 | |
|         return self._expand(shape)
 | |
| 
 | |
|     def output_spec(self, operation):
 | |
|         """Produce a JSON with a valid API response syntax for operation, but
 | |
|         with type information. Each node represented by a key has the
 | |
|         value containing field type, e.g.,
 | |
| 
 | |
|           output_spec["SomeBooleanNode"] => {"type": "boolean"}
 | |
| 
 | |
|         """
 | |
|         try:
 | |
|             op = self.operations[operation]
 | |
|         except KeyError:
 | |
|             raise ValueError("Invalid operation: {}".format(operation))
 | |
|         if "output" not in op:
 | |
|             return {}
 | |
|         shape = self.shapes[op["output"]["shape"]]
 | |
|         return self._expand(shape)
 | |
| 
 | |
|     def _expand(self, shape):
 | |
|         def expand(dic, seen=None):
 | |
|             seen = seen or {}
 | |
|             if dic["type"] == "structure":
 | |
|                 nodes = {}
 | |
|                 for k, v in dic["members"].items():
 | |
|                     seen_till_here = dict(seen)
 | |
|                     if k in seen_till_here:
 | |
|                         nodes[k] = seen_till_here[k]
 | |
|                         continue
 | |
|                     seen_till_here[k] = _RecursiveDictRef()
 | |
|                     nodes[k] = expand(self.shapes[v["shape"]], seen_till_here)
 | |
|                     seen_till_here[k].set_reference(k, nodes[k])
 | |
|                 nodes["type"] = "structure"
 | |
|                 return nodes
 | |
| 
 | |
|             elif dic["type"] == "list":
 | |
|                 seen_till_here = dict(seen)
 | |
|                 shape = dic["member"]["shape"]
 | |
|                 if shape in seen_till_here:
 | |
|                     return seen_till_here[shape]
 | |
|                 seen_till_here[shape] = _RecursiveDictRef()
 | |
|                 expanded = expand(self.shapes[shape], seen_till_here)
 | |
|                 seen_till_here[shape].set_reference(shape, expanded)
 | |
|                 return {"type": "list", "member": expanded}
 | |
| 
 | |
|             elif dic["type"] == "map":
 | |
|                 seen_till_here = dict(seen)
 | |
|                 node = {"type": "map"}
 | |
| 
 | |
|                 if "shape" in dic["key"]:
 | |
|                     shape = dic["key"]["shape"]
 | |
|                     seen_till_here[shape] = _RecursiveDictRef()
 | |
|                     node["key"] = expand(self.shapes[shape], seen_till_here)
 | |
|                     seen_till_here[shape].set_reference(shape, node["key"])
 | |
|                 else:
 | |
|                     node["key"] = dic["key"]["type"]
 | |
| 
 | |
|                 if "shape" in dic["value"]:
 | |
|                     shape = dic["value"]["shape"]
 | |
|                     seen_till_here[shape] = _RecursiveDictRef()
 | |
|                     node["value"] = expand(self.shapes[shape], seen_till_here)
 | |
|                     seen_till_here[shape].set_reference(shape, node["value"])
 | |
|                 else:
 | |
|                     node["value"] = dic["value"]["type"]
 | |
| 
 | |
|                 return node
 | |
| 
 | |
|             else:
 | |
|                 return {"type": dic["type"]}
 | |
| 
 | |
|         return expand(shape)
 | |
| 
 | |
| 
 | |
| def to_str(value, spec):
 | |
|     vtype = spec["type"]
 | |
|     if vtype == "boolean":
 | |
|         return "true" if value else "false"
 | |
|     elif vtype == "integer":
 | |
|         return str(value)
 | |
|     elif vtype == "float":
 | |
|         return str(value)
 | |
|     elif vtype == "double":
 | |
|         return str(value)
 | |
|     elif vtype == "timestamp":
 | |
|         return (
 | |
|             datetime.datetime.utcfromtimestamp(value)
 | |
|             .replace(tzinfo=pytz.utc)
 | |
|             .isoformat()
 | |
|         )
 | |
|     elif vtype == "string":
 | |
|         return str(value)
 | |
|     elif value is None:
 | |
|         return "null"
 | |
|     else:
 | |
|         raise TypeError("Unknown type {}".format(vtype))
 | |
| 
 | |
| 
 | |
| def from_str(value, spec):
 | |
|     vtype = spec["type"]
 | |
|     if vtype == "boolean":
 | |
|         return True if value == "true" else False
 | |
|     elif vtype == "integer":
 | |
|         return int(value)
 | |
|     elif vtype == "float":
 | |
|         return float(value)
 | |
|     elif vtype == "double":
 | |
|         return float(value)
 | |
|     elif vtype == "timestamp":
 | |
|         return value
 | |
|     elif vtype == "string":
 | |
|         return value
 | |
|     raise TypeError("Unknown type {}".format(vtype))
 | |
| 
 | |
| 
 | |
| def flatten_json_request_body(prefix, dict_body, spec):
 | |
|     """Convert a JSON request body into query params."""
 | |
|     if len(spec) == 1 and "type" in spec:
 | |
|         return {prefix: to_str(dict_body, spec)}
 | |
| 
 | |
|     flat = {}
 | |
|     for key, value in dict_body.items():
 | |
|         node_type = spec[key]["type"]
 | |
|         if node_type == "list":
 | |
|             for idx, v in enumerate(value, 1):
 | |
|                 pref = key + ".member." + str(idx)
 | |
|                 flat.update(flatten_json_request_body(pref, v, spec[key]["member"]))
 | |
|         elif node_type == "map":
 | |
|             for idx, (k, v) in enumerate(value.items(), 1):
 | |
|                 pref = key + ".entry." + str(idx)
 | |
|                 flat.update(
 | |
|                     flatten_json_request_body(pref + ".key", k, spec[key]["key"])
 | |
|                 )
 | |
|                 flat.update(
 | |
|                     flatten_json_request_body(pref + ".value", v, spec[key]["value"])
 | |
|                 )
 | |
|         else:
 | |
|             flat.update(flatten_json_request_body(key, value, spec[key]))
 | |
| 
 | |
|     if prefix:
 | |
|         prefix = prefix + "."
 | |
|     return dict((prefix + k, v) for k, v in flat.items())
 | |
| 
 | |
| 
 | |
| def xml_to_json_response(service_spec, operation, xml, result_node=None):
 | |
|     """Convert rendered XML response to JSON for use with boto3."""
 | |
| 
 | |
|     def transform(value, spec):
 | |
|         """Apply transformations to make the output JSON comply with the
 | |
|         expected form. This function applies:
 | |
| 
 | |
|           (1) Type cast to nodes with "type" property (e.g., 'true' to
 | |
|               True). XML field values are all in text so this step is
 | |
|               necessary to convert it to valid JSON objects.
 | |
| 
 | |
|           (2) Squashes "member" nodes to lists.
 | |
| 
 | |
|         """
 | |
|         if len(spec) == 1:
 | |
|             return from_str(value, spec)
 | |
| 
 | |
|         od = OrderedDict()
 | |
|         for k, v in value.items():
 | |
|             if k.startswith("@"):
 | |
|                 continue
 | |
| 
 | |
|             if k not in spec:
 | |
|                 # this can happen when with an older version of
 | |
|                 # botocore for which the node in XML template is not
 | |
|                 # defined in service spec.
 | |
|                 log.warning("Field %s is not defined by the botocore version in use", k)
 | |
|                 continue
 | |
| 
 | |
|             if spec[k]["type"] == "list":
 | |
|                 if v is None:
 | |
|                     od[k] = []
 | |
|                 elif len(spec[k]["member"]) == 1:
 | |
|                     if isinstance(v["member"], list):
 | |
|                         od[k] = transform(v["member"], spec[k]["member"])
 | |
|                     else:
 | |
|                         od[k] = [transform(v["member"], spec[k]["member"])]
 | |
|                 elif isinstance(v["member"], list):
 | |
|                     od[k] = [transform(o, spec[k]["member"]) for o in v["member"]]
 | |
|                 elif isinstance(v["member"], OrderedDict):
 | |
|                     od[k] = [transform(v["member"], spec[k]["member"])]
 | |
|                 else:
 | |
|                     raise ValueError("Malformatted input")
 | |
|             elif spec[k]["type"] == "map":
 | |
|                 if v is None:
 | |
|                     od[k] = {}
 | |
|                 else:
 | |
|                     items = (
 | |
|                         [v["entry"]] if not isinstance(v["entry"], list) else v["entry"]
 | |
|                     )
 | |
|                     for item in items:
 | |
|                         key = from_str(item["key"], spec[k]["key"])
 | |
|                         val = from_str(item["value"], spec[k]["value"])
 | |
|                         if k not in od:
 | |
|                             od[k] = {}
 | |
|                         od[k][key] = val
 | |
|             else:
 | |
|                 if v is None:
 | |
|                     od[k] = None
 | |
|                 else:
 | |
|                     od[k] = transform(v, spec[k])
 | |
|         return od
 | |
| 
 | |
|     dic = xmltodict.parse(xml)
 | |
|     output_spec = service_spec.output_spec(operation)
 | |
|     try:
 | |
|         for k in result_node or (operation + "Response", operation + "Result"):
 | |
|             dic = dic[k]
 | |
|     except KeyError:
 | |
|         return None
 | |
|     else:
 | |
|         return transform(dic, output_spec)
 | |
|     return None
 |