def test_message_transformation(): template = APIGATEWAY_TRANSFORMATION_TEMPLATE records = [{ 'data': { 'foo': 'foo1', 'bar': 'bar2' } }, { 'data': { 'foo': 'foo1', 'bar': 'bar2' }, 'partitionKey': 'key123' }] context = {'records': records} # try rendering the template result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads(base64.b64decode(result['Records'][0]['Data'])) assert result_decoded == records[0]['data'] assert len(result['Records'][0]['PartitionKey']) > 0 assert result['Records'][1]['PartitionKey'] == 'key123' # try again with context as string context = json.dumps(context) result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads(base64.b64decode(result['Records'][0]['Data'])) assert result_decoded == records[0]['data'] assert len(result['Records'][0]['PartitionKey']) > 0 assert result['Records'][1]['PartitionKey'] == 'key123' # test with empty array records = [] context = {'records': records} # try rendering the template result = render_velocity_template(template, context, as_json=True) assert result['Records'] == []
def test_special_chars(self): template1 = 'test#${foo.bar}' template2 = 'test#$foo.bar' context = {'foo': {'bar': 'baz'}} result = render_velocity_template(template1, {}, variables=context) self.assertEqual('test#baz', result) result = render_velocity_template(template2, {}, variables=context) self.assertEqual('test#baz', result)
def test_array_in_set_expr(self): template = "#set ($bar = $input.path('$.foo')[1]) \n $bar" context = {'foo': ['e1', 'e2', 'e3', 'e4']} result = render_velocity_template(template, context).strip() self.assertEqual('e2', result) template = "#set ($bar = $input.path('$.foo')[1][1][1]) $bar" context = {'foo': [['e1'], ['e2', ['e3', 'e4']]]} result = render_velocity_template(template, context).strip() self.assertEqual('e4', result)
def test_array_in_set_expr(self): template = "#set ($bar = $input.path('$.foo')[1]) \n $bar" context = {"foo": ["e1", "e2", "e3", "e4"]} result = render_velocity_template(template, context).strip() self.assertEqual("e2", result) template = "#set ($bar = $input.path('$.foo')[1][1][1]) $bar" context = {"foo": [["e1"], ["e2", ["e3", "e4"]]]} result = render_velocity_template(template, context).strip() self.assertEqual("e4", result)
def test_array_size(): template = "#set($list = $input.path('$.records')) $list.size()" context = { 'records': [{ 'data': {'foo': 'bar1'} }, { 'data': {'foo': 'bar2'} }] } result = render_velocity_template(template, context) assert(result == ' 2') result = render_velocity_template(template, json.dumps(context)) assert(result == ' 2')
def do_test(context): result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads( to_str(base64.b64decode(result["Records"][0]["Data"]))) self.assertEqual(records[0]["data"], result_decoded) self.assertGreater(len(result["Records"][0]["PartitionKey"]), 0) self.assertEqual("key123", result["Records"][1]["PartitionKey"])
def apply_template( integration: Dict[str, Any], req_res_type: str, data: InvocationPayload, path_params={}, query_params={}, headers={}, context={}, ): integration_type = integration.get("type") or integration.get( "integrationType") if integration_type in ["HTTP", "AWS"]: # apply custom request template content_type = APPLICATION_JSON # TODO: make configurable! template = integration.get("%sTemplates" % req_res_type, {}).get(content_type) if template: variables = {"context": context or {}} input_ctx = {"body": data} def _params(name=None): # See https://docs.aws.amazon.com/apigateway/latest/developerguide/ # api-gateway-mapping-template-reference.html#input-variable-reference # Returns "request parameter from the path, query string, or header value (searched in that order)" combined = {} combined.update(path_params or {}) combined.update(query_params or {}) combined.update(headers or {}) return combined if not name else combined.get(name) input_ctx["params"] = _params data = aws_stack.render_velocity_template(template, input_ctx, variables=variables) return data
def update_apigateway(method, path, data, headers, response=None, return_forward_info=False): if return_forward_info: regex1 = r'^/restapis/[A-Za-z0-9\-]+/deployments$' if method == 'POST' and re.match(regex1, path): # this is a request to deploy the API gateway, simply return HTTP code 200 return 200 regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/([^/]+)$' % PATH_USER_REQUEST if method == 'POST' and re.match(regex2, path): api_id = re.search(regex2, path).group(1) sub_path = '/%s' % re.search(regex2, path).group(3) integration = aws_stack.get_apigateway_integration( api_id, method, sub_path) template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to our main kinesis stream # TODO check whether the target of this API method is 'kinesis' headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = KINESIS_ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return 200 return True
def do_test(context): result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads( to_str(base64.b64decode(result['Records'][0]['Data']))) self.assertEqual(records[0]['data'], result_decoded) self.assertGreater(len(result['Records'][0]['PartitionKey']), 0) self.assertEqual('key123', result['Records'][1]['PartitionKey'])
def test_string_methods(self): context = {"foo": {"bar": "BAZ baz"}} template1 = "${foo.bar.strip().lower().replace(' ','-')}" template2 = "${foo.bar.trim().toLowerCase().replace(' ','-')}" for template in [template1, template2]: result = render_velocity_template(template, {}, variables=context) self.assertEqual("baz-baz", result)
def test_message_transformation(self): template = APIGATEWAY_TRANSFORMATION_TEMPLATE records = [{ 'data': { 'foo': 'foo1', 'bar': 'bar2' } }, { 'data': { 'foo': 'foo1', 'bar': 'bar2' }, 'partitionKey': 'key123' }] context = {'records': records} def do_test(context): result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads( to_str(base64.b64decode(result['Records'][0]['Data']))) self.assertEqual(records[0]['data'], result_decoded) self.assertGreater(len(result['Records'][0]['PartitionKey']), 0) self.assertEqual('key123', result['Records'][1]['PartitionKey']) # try rendering the template do_test(context) # try again with context as string do_test(json.dumps(context)) # test with empty array records = [] context = {'records': records} # try rendering the template result = render_velocity_template(template, context, as_json=True) self.assertEqual([], result['Records'])
def apply_template(integration, req_res_type, data, path_params={}, query_params={}, headers={}): if integration['type'] in ['HTTP', 'AWS']: # apply custom request template template = integration.get('%sTemplates' % req_res_type, {}).get(APPLICATION_JSON) if template: context = {} context['body'] = data def _params(name=None): # See https://docs.aws.amazon.com/apigateway/latest/developerguide/ # api-gateway-mapping-template-reference.html#input-variable-reference # Returns "request parameter from the path, query string, or header value (searched in that order)" combined = {} combined.update(path_params or {}) combined.update(query_params or {}) combined.update(headers or {}) return combined if not name else combined.get(name) context['params'] = _params data = aws_stack.render_velocity_template(template, context) return data
def test_array_size(self): template = "#set($list = $input.path('$.records')) $list.size()" context = { "records": [{ "data": { "foo": "bar1" } }, { "data": { "foo": "bar2" } }] } result = render_velocity_template(template, context) self.assertEqual(" 2", result) result = render_velocity_template(template, json.dumps(context)) self.assertEqual(" 2", result)
def apply_template( integration: Dict[str, Any], req_res_type: str, data: InvocationPayload, path_params=None, query_params=None, headers=None, context=None, ): if path_params is None: path_params = {} if query_params is None: query_params = {} if headers is None: headers = {} if context is None: context = {} integration_type = integration.get("type") or integration.get( "integrationType") if integration_type in ["HTTP", "AWS"]: # apply custom request template content_type = APPLICATION_JSON # TODO: make configurable! template = integration.get("%sTemplates" % req_res_type, {}).get(content_type) if template: variables = {"context": context or {}} input_ctx = {"body": data} # little trick to flatten the input context so velocity templates # work from the root. # orig - { "body": '{"action": "$default","message":"foobar"}' # after - { # "body": '{"action": "$default","message":"foobar"}', # "action": "$default", # "message": "foobar" # } if data: dict_pack = try_json(data) if isinstance(dict_pack, dict): for k, v in dict_pack.items(): input_ctx.update({k: v}) def _params(name=None): # See https://docs.aws.amazon.com/apigateway/latest/developerguide/ # api-gateway-mapping-template-reference.html#input-variable-reference # Returns "request parameter from the path, query string, or header value (searched in that order)" combined = {} combined.update(path_params or {}) combined.update(query_params or {}) combined.update(headers or {}) return combined if not name else combined.get(name) input_ctx["params"] = _params data = aws_stack.render_velocity_template(template, input_ctx, variables=variables) return data
def test_message_transformation(): template = APIGATEWAY_TRANSFORMATION_TEMPLATE records = [ { 'data': { 'foo': 'foo1', 'bar': 'bar2' } }, { 'data': { 'foo': 'foo1', 'bar': 'bar2' }, 'partitionKey': 'key123' } ] context = { 'records': records } # try rendering the template result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads(to_str(base64.b64decode(result['Records'][0]['Data']))) assert result_decoded == records[0]['data'] assert len(result['Records'][0]['PartitionKey']) > 0 assert result['Records'][1]['PartitionKey'] == 'key123' # try again with context as string context = json.dumps(context) result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads(to_str(base64.b64decode(result['Records'][0]['Data']))) assert result_decoded == records[0]['data'] assert len(result['Records'][0]['PartitionKey']) > 0 assert result['Records'][1]['PartitionKey'] == 'key123' # test with empty array records = [] context = { 'records': records } # try rendering the template result = render_velocity_template(template, context, as_json=True) assert result['Records'] == []
def update_apigateway(method, path, data, headers, response=None, return_forward_info=False): if return_forward_info: regex1 = r'^/restapis/[A-Za-z0-9\-]+/deployments$' if method == 'POST' and re.match(regex1, path): # this is a request to deploy the API gateway, simply return HTTP code 200 return 200 regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/([^/]+)$' % PATH_USER_REQUEST if method == 'POST' and re.match(regex2, path): api_id = re.search(regex2, path).group(1) sub_path = '/%s' % re.search(regex2, path).group(3) integration = aws_stack.get_apigateway_integration( api_id, method, sub_path) if integration['type'] == 'AWS': if integration['uri'].endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][ APPLICATION_JSON] new_request = aws_stack.render_velocity_template( template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers( service='kinesis') headers['X-Amz-Target'] = KINESIS_ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: LOGGER.warning( 'API Gateway action uri "%s" not yet implemented' % integration['uri']) else: LOGGER.warning( 'API Gateway integration type "%s" not yet implemented' % integration['type']) return 200 return True
def test_message_transformation(self): template = APIGATEWAY_TRANSFORMATION_TEMPLATE records = [ { "data": { "foo": "foo1", "bar": "bar2" } }, { "data": { "foo": "foo1", "bar": "bar2" }, "partitionKey": "key123" }, ] context = {"records": records} def do_test(context): result = render_velocity_template(template, context, as_json=True) result_decoded = json.loads( to_str(base64.b64decode(result["Records"][0]["Data"]))) self.assertEqual(records[0]["data"], result_decoded) self.assertGreater(len(result["Records"][0]["PartitionKey"]), 0) self.assertEqual("key123", result["Records"][1]["PartitionKey"]) # try rendering the template do_test(context) # try again with context as string do_test(json.dumps(context)) # test with empty array records = [] context = {"records": records} # try rendering the template result = render_velocity_template(template, context, as_json=True) self.assertEqual([], result["Records"])
def apply_request_response_templates( data: Union[Response, bytes], templates: Dict[str, str], content_type: str = None, as_json: bool = False, ): """Apply the matching request/response template (if it exists) to the payload data and return the result""" content_type = content_type or APPLICATION_JSON is_response = isinstance(data, Response) templates = templates or {} template = templates.get(content_type) if not template: return data content = (data.content if is_response else data) or "" result = aws_stack.render_velocity_template(template, content, as_json=as_json) if is_response: data._content = result update_content_length(data) return data return result
def test_render_urlencoded_string_data(self): template = "MessageBody=$util.base64Encode($input.json('$'))" result = render_velocity_template(template, b'{"spam": "eggs"}') self.assertEqual("MessageBody=eyJzcGFtIjogImVnZ3MifQ==", result)
def invoke_rest_api(api_id, stage, method, invocation_path, data, headers, path=None): path = path or invocation_path relative_path, query_string_params = extract_query_string_params(path=invocation_path) path_map = helpers.get_rest_api_paths(rest_api_id=api_id) try: extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map) except Exception: return make_error('Unable to find path %s' % path, 404) integrations = resource.get('resourceMethods', {}) integration = integrations.get(method, {}) if not integration: integration = integrations.get('ANY', {}) integration = integration.get('methodIntegration') if not integration: if method == 'OPTIONS' and 'Origin' in headers: # default to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) return make_error('Unable to find integration for path %s' % path, 404) uri = integration.get('uri') if method == 'POST' and integration['type'] == 'AWS': if uri.endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = kinesis_listener.ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result elif uri.startswith('arn:aws:apigateway:') and ':sqs:path' in uri: template = integration['requestTemplates'][APPLICATION_JSON] account_id, queue = uri.split('/')[-2:] region_name = uri.split(':')[3] new_request = aws_stack.render_velocity_template(template, data) + '&QueueName=%s' % queue headers = aws_stack.mock_aws_request_headers(service='sqs', region_name=region_name) url = urljoin(TEST_SQS_URL, '%s/%s' % (account_id, queue)) result = common.make_http_request(url, method='POST', headers=headers, data=new_request) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split('functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, (dict, list)) else data account_id = uri.split(':lambda:path')[1].split(':function:')[0].split(':')[-1] source_ip = headers['X-Forwarded-For'].split(',')[-2] # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = { 'path': relative_path, 'accountId': account_id, 'resourceId': resource.get('id'), 'stage': stage, 'identity': { 'accountId': account_id, 'sourceIp': source_ip, 'userAgent': headers['User-Agent'], } } try: path_params = extract_path_params(path=relative_path, extracted_path=extracted_path) except Exception: path_params = {} result = lambda_api.process_apigateway_invocation(func_arn, relative_path, data_str, headers, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=path, request_context=request_context) if isinstance(result, FlaskResponse): return flask_to_requests_response(result) if isinstance(result, Response): return result response = Response() parsed_result = result if isinstance(result, dict) else json.loads(result) parsed_result = common.json_safe(parsed_result) parsed_result = {} if parsed_result is None else parsed_result response.status_code = int(parsed_result.get('statusCode', 200)) response.headers.update(parsed_result.get('headers', {})) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps(parsed_result['body']) else: response._content = to_bytes(parsed_result['body']) except Exception: response._content = '{}' response.headers['Content-Length'] = len(response._content) return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ('API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200
def invoke_rest_api_integration(api_id, stage, integration, method, path, invocation_path, data, headers, resource_path, context={}, resource_id=None, response_templates={}): relative_path, query_string_params = extract_query_string_params( path=invocation_path) integration_type = integration.get('type') or integration.get( 'integrationType') uri = integration.get('uri') or integration.get('integrationUri') if (uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri) or uri.startswith('arn:aws:lambda'): if integration_type in ['AWS', 'AWS_PROXY']: func_arn = uri if ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split( 'functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, (dict, list)) else to_str(data) try: path_params = extract_path_params(path=relative_path, extracted_path=resource_path) except Exception: path_params = {} # apply custom request template data_str = apply_template(integration, 'request', data_str, path_params=path_params, query_params=query_string_params, headers=headers) # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = get_lambda_event_request_context( method, path, data, headers, integration_uri=uri, resource_id=resource_id) stage_variables = get_stage_variables(api_id, stage) result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, stage, api_id, headers, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=path, request_context=request_context, event_context=context, stage_variables=stage_variables) if isinstance(result, FlaskResponse): response = flask_to_requests_response(result) elif isinstance(result, Response): response = result else: response = LambdaResponse() parsed_result = result if isinstance( result, dict) else json.loads(str(result or '{}')) parsed_result = common.json_safe(parsed_result) parsed_result = {} if parsed_result is None else parsed_result response.status_code = int(parsed_result.get( 'statusCode', 200)) parsed_headers = parsed_result.get('headers', {}) if parsed_headers is not None: response.headers.update(parsed_headers) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps(parsed_result['body']) else: response._content = to_bytes(parsed_result['body']) except Exception: response._content = '{}' update_content_length(response) response.multi_value_headers = parsed_result.get( 'multiValueHeaders') or {} # apply custom response template response._content = apply_template(integration, 'response', response._content) response.headers['Content-Length'] = str( len(response.content or '')) return response msg = 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % ( uri, method) LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type == 'AWS': if 'kinesis:action/' in uri: if uri.endswith('kinesis:action/PutRecords'): target = kinesis_listener.ACTION_PUT_RECORDS if uri.endswith('kinesis:action/ListStreams'): target = kinesis_listener.ACTION_LIST_STREAMS template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = target result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) # TODO apply response template..? return result elif 'states:action/' in uri: if uri.endswith('states:action/StartExecution'): action = 'StartExecution' decoded_data = data.decode() if 'stateMachineArn' in decoded_data and 'input' in decoded_data: payload = json.loads(decoded_data) elif APPLICATION_JSON in integration.get('requestTemplates', {}): template = integration['requestTemplates'][APPLICATION_JSON] payload = aws_stack.render_velocity_template(template, data, as_json=True) client = aws_stack.connect_to_service('stepfunctions') kwargs = {'name': payload['name']} if 'name' in payload else {} result = client.start_execution( stateMachineArn=payload['stateMachineArn'], input=payload['input'], **kwargs) response = requests_response( content={ 'executionArn': result['executionArn'], 'startDate': str(result['startDate']) }, headers=aws_stack.mock_aws_request_headers()) return response if method == 'POST': if uri.startswith('arn:aws:apigateway:') and ':sqs:path' in uri: template = integration['requestTemplates'][APPLICATION_JSON] account_id, queue = uri.split('/')[-2:] region_name = uri.split(':')[3] new_request = '%s&QueueName=%s' % ( aws_stack.render_velocity_template(template, data), queue) headers = aws_stack.mock_aws_request_headers( service='sqs', region_name=region_name) url = urljoin(TEST_SQS_URL, '%s/%s' % (TEST_AWS_ACCOUNT_ID, queue)) result = common.make_http_request(url, method='POST', headers=headers, data=new_request) return result msg = 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % ( uri, method) LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':dynamodb:action' in uri: # arn:aws:apigateway:us-east-1:dynamodb:action/PutItem&Table=MusicCollection table_name = uri.split(':dynamodb:action')[1].split('&Table=')[1] action = uri.split(':dynamodb:action')[1].split('&Table=')[0] if 'PutItem' in action and method == 'PUT': response_template = response_templates.get( 'application/json', None) if response_template is None: msg = 'Invalid response template defined in integration response.' return make_error_response(msg, 404) response_template = json.loads(response_template) if response_template['TableName'] != table_name: msg = 'Invalid table name specified in integration response template.' return make_error_response(msg, 404) dynamo_client = aws_stack.connect_to_resource('dynamodb') table = dynamo_client.Table(table_name) event_data = {} data_dict = json.loads(data) for key, _ in response_template['Item'].items(): event_data[key] = data_dict[key] table.put_item(Item=event_data) response = requests_response( event_data, headers=aws_stack.mock_aws_request_headers()) return response else: msg = 'API Gateway action uri "%s", integration type %s not yet implemented' % ( uri, integration_type) LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type in ['HTTP_PROXY', 'HTTP']: function = getattr(requests, method.lower()) # apply custom request template data = apply_template(integration, 'request', data) if isinstance(data, dict): data = json.dumps(data) result = function(uri, data=data, headers=headers) # apply custom response template data = apply_template(integration, 'response', data) return result elif integration_type == 'MOCK': # TODO: add logic for MOCK responses pass if method == 'OPTIONS': # fall back to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) msg = ( 'API Gateway integration type "%s", method "%s", URI "%s" not yet implemented' % (integration_type, method, uri)) LOGGER.warning(msg) return make_error_response(msg, 404)
def forward_request(self, method, path, data, headers): data = data and json.loads(data) # Paths to match regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/(.*)$' % PATH_USER_REQUEST if re.match(regex2, path): search_match = re.search(regex2, path) api_id = search_match.group(1) relative_path = '/%s' % search_match.group(3) try: integration = aws_stack.get_apigateway_integration( api_id, method, path=relative_path) assert integration except Exception: # if we have no exact match, try to find an API resource that contains path parameters path_map = get_rest_api_paths(rest_api_id=api_id) extracted_path, resource = get_resource_for_path( path=relative_path, path_map=path_map) or {} integrations = resource.get('resourceMethods', {}) integration = integrations.get(method, {}) integration = integration.get('methodIntegration') if not integration: if method == 'OPTIONS' and 'Origin' in headers: # default to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) return make_error( 'Unable to find integration for path %s' % path, 404) uri = integration.get('uri') if method == 'POST' and integration['type'] == 'AWS': if uri.endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][ APPLICATION_JSON] new_request = aws_stack.render_velocity_template( template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers( service='kinesis') headers[ 'X-Amz-Target'] = kinesis_listener.ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'AWS_PROXY': if uri.startswith( 'arn:aws:apigateway:') and ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split( 'functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, dict) else data try: path_params = extract_path_params( path=relative_path, extracted_path=extracted_path) except Exception: path_params = {} result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, headers, path_params=path_params, method=method, resource_path=path) response = Response() parsed_result = result if isinstance( result, dict) else json.loads(result) parsed_result = common.json_safe(parsed_result) response.status_code = int( parsed_result.get('statusCode', 200)) response.headers.update(parsed_result.get('headers', {})) try: response_body = parsed_result['body'] response._content = json.dumps(response_body) except Exception: response._content = '{}' return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ( 'API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200 if re.match(PATH_REGEX_AUTHORIZERS, path): return handle_authorizers(method, path, data, headers) return True
def forward_request(self, method, path, data, headers): regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/(.*)$' % PATH_USER_REQUEST if re.match(regex2, path): search_match = re.search(regex2, path) api_id = search_match.group(1) path = '/%s' % search_match.group(3) try: integration = aws_stack.get_apigateway_integration(api_id, method, path) except Exception as e: msg = ('API Gateway endpoint "%s" for method "%s" not found' % (path, method)) LOGGER.warning(msg) return make_error(msg, 404) uri = integration.get('uri') if method == 'POST' and integration['type'] in ['AWS']: if uri.endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = kinesis_listener.ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split('functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, dict) else data result = lambda_api.process_apigateway_invocation(func_arn, path, data_str, headers) response = Response() parsed_result = json.loads(result) response.status_code = int(parsed_result['statusCode']) response.headers.update(parsed_result['headers']) response_body = parsed_result['body'] response._content = json.dumps(response_body) return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ('API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200 if re.match(PATH_REGEX_AUTHORIZERS, path): return handle_authorizers(method, path, data, headers) return True
def forward_request(self, method, path, data, headers): data = data and json.loads(to_str(data)) # Paths to match regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/(.*)$' % PATH_USER_REQUEST if re.match(regex2, path): search_match = re.search(regex2, path) api_id = search_match.group(1) stage = search_match.group(2) relative_path_w_query_params = '/%s' % search_match.group(3) relative_path, query_string_params = extract_query_string_params( path=relative_path_w_query_params) path_map = get_rest_api_paths(rest_api_id=api_id) try: extracted_path, resource = get_resource_for_path( path=relative_path, path_map=path_map) except Exception: return make_error('Unable to find path %s' % path, 404) integrations = resource.get('resourceMethods', {}) integration = integrations.get(method, {}) if not integration: integration = integrations.get('ANY', {}) integration = integration.get('methodIntegration') if not integration: if method == 'OPTIONS' and 'Origin' in headers: # default to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) return make_error( 'Unable to find integration for path %s' % path, 404) uri = integration.get('uri') if method == 'POST' and integration['type'] == 'AWS': if uri.endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][ APPLICATION_JSON] new_request = aws_stack.render_velocity_template( template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers( service='kinesis') headers[ 'X-Amz-Target'] = kinesis_listener.ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'AWS_PROXY': if uri.startswith( 'arn:aws:apigateway:') and ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split( 'functions/')[1].split('/invocations')[0] account_id = uri.split(':lambda:path')[1].split( ':function:')[0].split(':')[-1] data_str = json.dumps(data) if isinstance(data, dict) else data source_ip = headers['X-Forwarded-For'].split(',')[-2] # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = { 'path': relative_path, 'accountId': account_id, 'resourceId': resource.get('id'), 'stage': stage, 'identity': { 'accountId': account_id, 'sourceIp': source_ip, 'userAgent': headers['User-Agent'], } } try: path_params = extract_path_params( path=relative_path, extracted_path=extracted_path) except Exception: path_params = {} result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, headers, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=path, request_context=request_context) if isinstance(result, FlaskResponse): return flask_to_requests_response(result) response = Response() parsed_result = result if isinstance( result, dict) else json.loads(result) parsed_result = common.json_safe(parsed_result) response.status_code = int( parsed_result.get('statusCode', 200)) response.headers.update(parsed_result.get('headers', {})) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps( parsed_result['body']) else: response._content = parsed_result['body'] except Exception: response._content = '{}' return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ( 'API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200 if re.match(PATH_REGEX_AUTHORIZERS, path): return handle_authorizers(method, path, data, headers) return True
def invoke_rest_api_integration_backend(api_id, stage, integration, method, path, invocation_path, data, headers, resource_path, context={}, resource_id=None, response_templates={}): relative_path, query_string_params = extract_query_string_params( path=invocation_path) integration_type_orig = integration.get('type') or integration.get( 'integrationType') or '' integration_type = integration_type_orig.upper() uri = integration.get('uri') or integration.get('integrationUri') or '' try: path_params = extract_path_params(path=relative_path, extracted_path=resource_path) except Exception: path_params = {} if (uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri) or uri.startswith('arn:aws:lambda'): if integration_type in ['AWS', 'AWS_PROXY']: func_arn = uri if ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split( 'functions/')[1].split('/invocations')[0] # apply custom request template data_str = data try: data_str = json.dumps(data) if isinstance( data, (dict, list)) else to_str(data) data_str = apply_template(integration, 'request', data_str, path_params=path_params, query_params=query_string_params, headers=headers) except Exception: pass # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = get_lambda_event_request_context( method, path, data, headers, integration_uri=uri, resource_id=resource_id, resource_path=resource_path) stage_variables = get_stage_variables(api_id, stage) result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, stage, api_id, headers, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=resource_path, request_context=request_context, event_context=context, stage_variables=stage_variables) if isinstance(result, FlaskResponse): response = flask_to_requests_response(result) elif isinstance(result, Response): response = result else: response = LambdaResponse() parsed_result = result if isinstance( result, dict) else json.loads(str(result or '{}')) parsed_result = common.json_safe(parsed_result) parsed_result = {} if parsed_result is None else parsed_result response.status_code = int(parsed_result.get( 'statusCode', 200)) parsed_headers = parsed_result.get('headers', {}) if parsed_headers is not None: response.headers.update(parsed_headers) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps(parsed_result['body']) else: body_bytes = to_bytes(parsed_result.get('body') or '') if parsed_result.get('isBase64Encoded', False): body_bytes = base64.b64decode(body_bytes) response._content = body_bytes except Exception as e: LOG.warning("Couldn't set lambda response content: %s" % e) response._content = '{}' update_content_length(response) response.multi_value_headers = parsed_result.get( 'multiValueHeaders') or {} # apply custom response template response._content = apply_template(integration, 'response', response._content) response.headers['Content-Length'] = str( len(response.content or '')) return response raise Exception( 'API Gateway %s integration action "%s", method "%s" not yet implemented' % (integration_type, uri, method)) elif integration_type == 'AWS': if 'kinesis:action/' in uri: if uri.endswith('kinesis:action/PutRecord'): target = kinesis_listener.ACTION_PUT_RECORD if uri.endswith('kinesis:action/PutRecords'): target = kinesis_listener.ACTION_PUT_RECORDS if uri.endswith('kinesis:action/ListStreams'): target = kinesis_listener.ACTION_LIST_STREAMS template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = target result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) # TODO apply response template..? return result elif 'states:action/' in uri: if uri.endswith('states:action/StartExecution'): action = 'StartExecution' decoded_data = data.decode() payload = {} if 'stateMachineArn' in decoded_data and 'input' in decoded_data: payload = json.loads(decoded_data) elif APPLICATION_JSON in integration.get('requestTemplates', {}): template = integration['requestTemplates'][APPLICATION_JSON] payload = aws_stack.render_velocity_template(template, data, as_json=True) client = aws_stack.connect_to_service('stepfunctions') kwargs = {'name': payload['name']} if 'name' in payload else {} result = client.start_execution( stateMachineArn=payload['stateMachineArn'], input=payload['input'], **kwargs) response = requests_response( content={ 'executionArn': result['executionArn'], 'startDate': str(result['startDate']) }, headers=aws_stack.mock_aws_request_headers()) response.headers['content-type'] = APPLICATION_JSON return response elif 's3:path/' in uri and method == 'GET': s3 = aws_stack.connect_to_service('s3') uri_match = re.match(TARGET_REGEX_S3_URI, uri) if uri_match: bucket, object_key = uri_match.group('bucket', 'object') LOG.debug('Getting request for bucket %s object %s', bucket, object_key) try: object = s3.get_object(Bucket=bucket, Key=object_key) except s3.exceptions.NoSuchKey: msg = 'Object %s not found' % object_key LOG.debug(msg) return make_error_response(msg, 404) headers = aws_stack.mock_aws_request_headers(service='s3') if object.get('ContentType'): headers['Content-Type'] = object['ContentType'] # stream used so large files do not fill memory response = request_response_stream(stream=object['Body'], headers=headers) return response else: msg = 'Request URI does not match s3 specifications' LOG.warning(msg) return make_error_response(msg, 400) if method == 'POST': if uri.startswith('arn:aws:apigateway:') and ':sqs:path' in uri: template = integration['requestTemplates'][APPLICATION_JSON] account_id, queue = uri.split('/')[-2:] region_name = uri.split(':')[3] new_request = '%s&QueueName=%s' % ( aws_stack.render_velocity_template(template, data), queue) headers = aws_stack.mock_aws_request_headers( service='sqs', region_name=region_name) url = urljoin(TEST_SQS_URL, '%s/%s' % (TEST_AWS_ACCOUNT_ID, queue)) result = common.make_http_request(url, method='POST', headers=headers, data=new_request) return result raise Exception( 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % (uri, method)) elif integration_type == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':dynamodb:action' in uri: # arn:aws:apigateway:us-east-1:dynamodb:action/PutItem&Table=MusicCollection table_name = uri.split(':dynamodb:action')[1].split('&Table=')[1] action = uri.split(':dynamodb:action')[1].split('&Table=')[0] if 'PutItem' in action and method == 'PUT': response_template = response_templates.get('application/json') if response_template is None: msg = 'Invalid response template defined in integration response.' LOG.info('%s Existing: %s' % (msg, response_templates)) return make_error_response(msg, 404) response_template = json.loads(response_template) if response_template['TableName'] != table_name: msg = 'Invalid table name specified in integration response template.' return make_error_response(msg, 404) dynamo_client = aws_stack.connect_to_resource('dynamodb') table = dynamo_client.Table(table_name) event_data = {} data_dict = json.loads(data) for key, _ in response_template['Item'].items(): event_data[key] = data_dict[key] table.put_item(Item=event_data) response = requests_response( event_data, headers=aws_stack.mock_aws_request_headers()) return response else: raise Exception( 'API Gateway action uri "%s", integration type %s not yet implemented' % (uri, integration_type)) elif integration_type in ['HTTP_PROXY', 'HTTP']: if ':servicediscovery:' in uri: # check if this is a servicediscovery integration URI client = aws_stack.connect_to_service('servicediscovery') service_id = uri.split('/')[-1] instances = client.list_instances( ServiceId=service_id)['Instances'] instance = (instances or [None])[0] if instance and instance.get('Id'): uri = 'http://%s/%s' % (instance['Id'], invocation_path.lstrip('/')) # apply custom request template data = apply_template(integration, 'request', data) if isinstance(data, dict): data = json.dumps(data) uri = apply_request_parameter(integration=integration, path_params=path_params) function = getattr(requests, method.lower()) result = function(uri, data=data, headers=headers) # apply custom response template data = apply_template(integration, 'response', data) return result elif integration_type == 'MOCK': # return empty response - details filled in via responseParameters above... return requests_response({}) if method == 'OPTIONS': # fall back to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) raise Exception( 'API Gateway integration type "%s", method "%s", URI "%s" not yet implemented' % (integration_type, method, uri))
def invoke_rest_api(api_id, stage, method, invocation_path, data, headers, path=None): path = path or invocation_path relative_path, query_string_params = extract_query_string_params( path=invocation_path) # run gateway authorizers for this request authorize_invocation(api_id, headers) path_map = helpers.get_rest_api_paths(rest_api_id=api_id) try: extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map) except Exception: return make_error_response('Unable to find path %s' % path, 404) api_key_required = resource.get('resourceMethods', {}).get(method, {}).get('apiKeyRequired') if not is_api_key_valid(api_key_required, headers, stage): return make_error_response('Access denied - invalid API key', 403) integrations = resource.get('resourceMethods', {}) integration = integrations.get(method, {}) if not integration: integration = integrations.get('ANY', {}) integration = integration.get('methodIntegration') if not integration: if method == 'OPTIONS' and 'Origin' in headers: # default to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) return make_error_response( 'Unable to find integration for path %s' % path, 404) uri = integration.get('uri') or '' integration_type = integration['type'].upper() if uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri: if integration_type in ['AWS', 'AWS_PROXY']: func_arn = uri.split(':lambda:path')[1].split( 'functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, (dict, list)) else to_str(data) account_id = uri.split(':lambda:path')[1].split( ':function:')[0].split(':')[-1] source_ip = headers['X-Forwarded-For'].split(',')[-2] try: path_params = extract_path_params( path=relative_path, extracted_path=extracted_path) except Exception: path_params = {} # apply custom request template data_str = apply_template(integration, 'request', data_str, path_params=path_params, query_params=query_string_params, headers=headers) # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = { # adding stage to the request context path. # https://github.com/localstack/localstack/issues/2210 'path': '/' + stage + relative_path, 'accountId': account_id, 'resourceId': resource.get('id'), 'stage': stage, 'identity': { 'accountId': account_id, 'sourceIp': source_ip, 'userAgent': headers['User-Agent'], }, 'httpMethod': method, 'protocol': 'HTTP/1.1', 'requestTime': datetime.datetime.utcnow(), 'requestTimeEpoch': int(time.time() * 1000), } result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, stage, api_id, headers, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=path, request_context=request_context) if isinstance(result, FlaskResponse): response = flask_to_requests_response(result) elif isinstance(result, Response): response = result else: response = LambdaResponse() parsed_result = result if isinstance( result, dict) else json.loads(str(result or '{}')) parsed_result = common.json_safe(parsed_result) parsed_result = {} if parsed_result is None else parsed_result response.status_code = int(parsed_result.get( 'statusCode', 200)) parsed_headers = parsed_result.get('headers', {}) if parsed_headers is not None: response.headers.update(parsed_headers) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps(parsed_result['body']) else: response._content = to_bytes(parsed_result['body']) except Exception: response._content = '{}' update_content_length(response) response.multi_value_headers = parsed_result.get( 'multiValueHeaders') or {} # apply custom response template response._content = apply_template(integration, 'response', response._content) response.headers['Content-Length'] = str( len(response.content or '')) return response msg = 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % ( uri, method) LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type == 'AWS': if 'kinesis:action/' in uri: if uri.endswith('kinesis:action/PutRecords'): target = kinesis_listener.ACTION_PUT_RECORDS if uri.endswith('kinesis:action/ListStreams'): target = kinesis_listener.ACTION_LIST_STREAMS template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = target result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) # TODO apply response template..? return result if method == 'POST': if uri.startswith('arn:aws:apigateway:') and ':sqs:path' in uri: template = integration['requestTemplates'][APPLICATION_JSON] account_id, queue = uri.split('/')[-2:] region_name = uri.split(':')[3] new_request = '%s&QueueName=%s' % ( aws_stack.render_velocity_template(template, data), queue) headers = aws_stack.mock_aws_request_headers( service='sqs', region_name=region_name) url = urljoin(TEST_SQS_URL, '%s/%s' % (TEST_AWS_ACCOUNT_ID, queue)) result = common.make_http_request(url, method='POST', headers=headers, data=new_request) return result msg = 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % ( uri, method) LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':dynamodb:action' in uri: # arn:aws:apigateway:us-east-1:dynamodb:action/PutItem&Table=MusicCollection table_name = uri.split(':dynamodb:action')[1].split('&Table=')[1] action = uri.split(':dynamodb:action')[1].split('&Table=')[0] if 'PutItem' in action and method == 'PUT': response_template = path_map.get(relative_path, {}).get('resourceMethods', {})\ .get(method, {}).get('methodIntegration', {}).\ get('integrationResponses', {}).get('200', {}).get('responseTemplates', {})\ .get('application/json', None) if response_template is None: msg = 'Invalid response template defined in integration response.' return make_error_response(msg, 404) response_template = json.loads(response_template) if response_template['TableName'] != table_name: msg = 'Invalid table name specified in integration response template.' return make_error_response(msg, 404) dynamo_client = aws_stack.connect_to_resource('dynamodb') table = dynamo_client.Table(table_name) event_data = {} data_dict = json.loads(data) for key, _ in response_template['Item'].items(): event_data[key] = data_dict[key] table.put_item(Item=event_data) response = requests_response( event_data, headers=aws_stack.mock_aws_request_headers()) return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error_response(msg, 404) elif integration_type in ['HTTP_PROXY', 'HTTP']: function = getattr(requests, method.lower()) # apply custom request template data = apply_template(integration, 'request', data) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) # apply custom response template data = apply_template(integration, 'response', data) return result elif integration_type == 'MOCK': # TODO: add logic for MOCK responses pass if method == 'OPTIONS': # fall back to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) msg = ( 'API Gateway integration type "%s", method "%s", URI "%s" not yet implemented' % (integration['type'], method, integration.get('uri'))) LOGGER.warning(msg) return make_error_response(msg, 404)
def invoke_rest_api_integration_backend( invocation_context: ApiInvocationContext, integration: Dict): # define local aliases from invocation context invocation_path = invocation_context.path_with_query_string method = invocation_context.method path = invocation_context.path data = invocation_context.data headers = invocation_context.headers api_id = invocation_context.api_id stage = invocation_context.stage resource_path = invocation_context.resource_path response_templates = invocation_context.response_templates # extract integration type and path parameters relative_path, query_string_params = extract_query_string_params( path=invocation_path) integration_type_orig = integration.get("type") or integration.get( "integrationType") or "" integration_type = integration_type_orig.upper() uri = integration.get("uri") or integration.get("integrationUri") or "" try: path_params = extract_path_params(path=relative_path, extracted_path=resource_path) except Exception: path_params = {} if (uri.startswith("arn:aws:apigateway:") and ":lambda:path" in uri) or uri.startswith("arn:aws:lambda"): if integration_type in ["AWS", "AWS_PROXY"]: func_arn = uri if ":lambda:path" in uri: func_arn = (uri.split(":lambda:path")[1].split("functions/") [1].split("/invocations")[0]) # apply custom request template data_str = data is_base64_encoded = False try: data_str = json.dumps(data) if isinstance( data, (dict, list)) else to_str(data) data_str = apply_template( integration, "request", data_str, path_params=path_params, query_params=query_string_params, headers=headers, ) except UnicodeDecodeError: data_str = base64.b64encode(data_str) is_base64_encoded = True except Exception as e: LOG.warning( "Unable to convert API Gateway payload to str: %s" % (e)) pass # Sample request context: # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html#api-gateway-create-api-as-simple-proxy-for-lambda-test request_context = get_lambda_event_request_context( invocation_context) stage_variables = (get_stage_variables(api_id, stage) if not is_test_invoke_method(method, path) else None) # TODO: change this signature to InvocationContext as well! result = lambda_api.process_apigateway_invocation( func_arn, relative_path, data_str, stage, api_id, headers, is_base64_encoded=is_base64_encoded, path_params=path_params, query_string_params=query_string_params, method=method, resource_path=resource_path, request_context=request_context, event_context=invocation_context.context, stage_variables=stage_variables, ) if isinstance(result, FlaskResponse): response = flask_to_requests_response(result) elif isinstance(result, Response): response = result else: response = LambdaResponse() parsed_result = (result if isinstance(result, dict) else json.loads(str(result or "{}"))) parsed_result = common.json_safe(parsed_result) parsed_result = {} if parsed_result is None else parsed_result response.status_code = int(parsed_result.get( "statusCode", 200)) parsed_headers = parsed_result.get("headers", {}) if parsed_headers is not None: response.headers.update(parsed_headers) try: result_body = parsed_result.get("body") if isinstance(result_body, dict): response._content = json.dumps(result_body) else: body_bytes = to_bytes(to_str(result_body or "")) if parsed_result.get("isBase64Encoded", False): body_bytes = base64.b64decode(body_bytes) response._content = body_bytes except Exception as e: LOG.warning("Couldn't set Lambda response content: %s" % e) response._content = "{}" update_content_length(response) response.multi_value_headers = parsed_result.get( "multiValueHeaders") or {} # apply custom response template response._content = apply_template(integration, "response", response._content) response.headers["Content-Length"] = str( len(response.content or "")) return response raise Exception( 'API Gateway integration type "%s", action "%s", method "%s" invalid or not yet implemented' % (integration_type, uri, method)) elif integration_type == "AWS": if "kinesis:action/" in uri: if uri.endswith("kinesis:action/PutRecord"): target = kinesis_listener.ACTION_PUT_RECORD elif uri.endswith("kinesis:action/PutRecords"): target = kinesis_listener.ACTION_PUT_RECORDS elif uri.endswith("kinesis:action/ListStreams"): target = kinesis_listener.ACTION_LIST_STREAMS else: LOG.info( "Unexpected API Gateway integration URI '%s' for integration type %s", uri, integration_type, ) target = "" # apply request templates new_data = apply_request_response_templates( data, integration.get("requestTemplates"), content_type=APPLICATION_JSON) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service="kinesis") headers["X-Amz-Target"] = target result = common.make_http_request(url=config.TEST_KINESIS_URL, method="POST", data=new_data, headers=headers) # apply response template result = apply_request_response_templates( result, response_templates, content_type=APPLICATION_JSON) return result elif "states:action/" in uri: action = uri.split("/")[-1] payload = {} if APPLICATION_JSON in integration.get("requestTemplates", {}): payload = apply_request_response_templates( data, integration.get("requestTemplates"), content_type=APPLICATION_JSON, as_json=True, ) else: payload = json.loads(data.decode("utf-8")) client = aws_stack.connect_to_service("stepfunctions") # Hot fix since step functions local package responses: Unsupported Operation: 'StartSyncExecution' method_name = (camel_to_snake_case(action) if action != "StartSyncExecution" else "start_execution") try: method = getattr(client, method_name) except AttributeError: msg = "Invalid step function action: %s" % method_name LOG.error(msg) return make_error_response(msg, 400) result = method(**payload, ) result = json_safe( {k: result[k] for k in result if k not in "ResponseMetadata"}) response = requests_response( content=result, headers=aws_stack.mock_aws_request_headers(), ) if action == "StartSyncExecution": # poll for the execution result and return it result = await_sfn_execution_result(result["executionArn"]) result_status = result.get("status") if result_status != "SUCCEEDED": return make_error_response( "StepFunctions execution %s failed with status '%s'" % (result["executionArn"], result_status), 500, ) result = json_safe(result) response = requests_response(content=result) # apply response templates response = apply_request_response_templates( response, response_templates, content_type=APPLICATION_JSON) return response elif "s3:path/" in uri and method == "GET": s3 = aws_stack.connect_to_service("s3") uri_match = re.match(TARGET_REGEX_S3_URI, uri) if uri_match: bucket, object_key = uri_match.group("bucket", "object") LOG.debug("Getting request for bucket %s object %s", bucket, object_key) try: object = s3.get_object(Bucket=bucket, Key=object_key) except s3.exceptions.NoSuchKey: msg = "Object %s not found" % object_key LOG.debug(msg) return make_error_response(msg, 404) headers = aws_stack.mock_aws_request_headers(service="s3") if object.get("ContentType"): headers["Content-Type"] = object["ContentType"] # stream used so large files do not fill memory response = request_response_stream(stream=object["Body"], headers=headers) return response else: msg = "Request URI does not match s3 specifications" LOG.warning(msg) return make_error_response(msg, 400) if method == "POST": if uri.startswith("arn:aws:apigateway:") and ":sqs:path" in uri: template = integration["requestTemplates"][APPLICATION_JSON] account_id, queue = uri.split("/")[-2:] region_name = uri.split(":")[3] new_request = "%s&QueueName=%s" % ( aws_stack.render_velocity_template(template, data), queue, ) headers = aws_stack.mock_aws_request_headers( service="sqs", region_name=region_name) url = urljoin(config.TEST_SQS_URL, "%s/%s" % (TEST_AWS_ACCOUNT_ID, queue)) result = common.make_http_request(url, method="POST", headers=headers, data=new_request) return result raise Exception( 'API Gateway AWS integration action URI "%s", method "%s" not yet implemented' % (uri, method)) elif integration_type == "AWS_PROXY": if uri.startswith("arn:aws:apigateway:") and ":dynamodb:action" in uri: # arn:aws:apigateway:us-east-1:dynamodb:action/PutItem&Table=MusicCollection table_name = uri.split(":dynamodb:action")[1].split("&Table=")[1] action = uri.split(":dynamodb:action")[1].split("&Table=")[0] if "PutItem" in action and method == "PUT": response_template = response_templates.get("application/json") if response_template is None: msg = "Invalid response template defined in integration response." LOG.info("%s Existing: %s" % (msg, response_templates)) return make_error_response(msg, 404) response_template = json.loads(response_template) if response_template["TableName"] != table_name: msg = "Invalid table name specified in integration response template." return make_error_response(msg, 404) dynamo_client = aws_stack.connect_to_resource("dynamodb") table = dynamo_client.Table(table_name) event_data = {} data_dict = json.loads(data) for key, _ in response_template["Item"].items(): event_data[key] = data_dict[key] table.put_item(Item=event_data) response = requests_response(event_data) return response else: raise Exception( 'API Gateway action uri "%s", integration type %s not yet implemented' % (uri, integration_type)) elif integration_type in ["HTTP_PROXY", "HTTP"]: if ":servicediscovery:" in uri: # check if this is a servicediscovery integration URI client = aws_stack.connect_to_service("servicediscovery") service_id = uri.split("/")[-1] instances = client.list_instances( ServiceId=service_id)["Instances"] instance = (instances or [None])[0] if instance and instance.get("Id"): uri = "http://%s/%s" % (instance["Id"], invocation_path.lstrip("/")) # apply custom request template data = apply_template(integration, "request", data) if isinstance(data, dict): data = json.dumps(data) uri = apply_request_parameter(uri, integration=integration, path_params=path_params) result = requests.request(method=method, url=uri, data=data, headers=headers) # apply custom response template result = apply_template(integration, "response", result) return result elif integration_type == "MOCK": # return empty response - details filled in via responseParameters above... return requests_response({}) if method == "OPTIONS": # fall back to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) raise Exception( 'API Gateway integration type "%s", method "%s", URI "%s" not yet implemented' % (integration_type, method, uri))
def update_apigateway(method, path, data, headers, response=None, return_forward_info=False): if return_forward_info: regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/([^/]*)$' % PATH_USER_REQUEST if re.match(regex2, path): search_match = re.search(regex2, path) api_id = search_match.group(1) sub_path = '/%s' % search_match.group(3) try: integration = aws_stack.get_apigateway_integration( api_id, method, sub_path) except Exception as e: msg = ('API Gateway endpoint "%s" for method "%s" not found' % (path, method)) LOGGER.warning(msg) return make_error(msg, 404) if method == 'POST' and integration['type'] == 'AWS': if integration['uri'].endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][ APPLICATION_JSON] new_request = aws_stack.render_velocity_template( template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers( service='kinesis') headers['X-Amz-Target'] = KINESIS_ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % integration[ 'uri'] LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ( 'API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200 return True
def forward_request(self, method, path, data, headers): data = data and json.loads(to_str(data)) # Paths to match regex2 = r'^/restapis/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)/%s/(.*)$' % PATH_USER_REQUEST if re.match(regex2, path): search_match = re.search(regex2, path) api_id = search_match.group(1) relative_path = '/%s' % search_match.group(3) try: integration = aws_stack.get_apigateway_integration(api_id, method, path=relative_path) assert integration except Exception: # if we have no exact match, try to find an API resource that contains path parameters path_map = get_rest_api_paths(rest_api_id=api_id) try: extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map) except Exception: return make_error('Unable to find path %s' % path, 404) integrations = resource.get('resourceMethods', {}) integration = integrations.get(method, {}) integration = integration.get('methodIntegration') if not integration: if method == 'OPTIONS' and 'Origin' in headers: # default to returning CORS headers if this is an OPTIONS request return get_cors_response(headers) return make_error('Unable to find integration for path %s' % path, 404) uri = integration.get('uri') if method == 'POST' and integration['type'] == 'AWS': if uri.endswith('kinesis:action/PutRecords'): template = integration['requestTemplates'][APPLICATION_JSON] new_request = aws_stack.render_velocity_template(template, data) # forward records to target kinesis stream headers = aws_stack.mock_aws_request_headers(service='kinesis') headers['X-Amz-Target'] = kinesis_listener.ACTION_PUT_RECORDS result = common.make_http_request(url=TEST_KINESIS_URL, method='POST', data=new_request, headers=headers) return result else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'AWS_PROXY': if uri.startswith('arn:aws:apigateway:') and ':lambda:path' in uri: func_arn = uri.split(':lambda:path')[1].split('functions/')[1].split('/invocations')[0] data_str = json.dumps(data) if isinstance(data, dict) else data try: path_params = extract_path_params(path=relative_path, extracted_path=extracted_path) except Exception: path_params = {} result = lambda_api.process_apigateway_invocation(func_arn, relative_path, data_str, headers, path_params=path_params, method=method, resource_path=path) if isinstance(result, FlaskResponse): return flask_to_requests_response(result) response = Response() parsed_result = result if isinstance(result, dict) else json.loads(result) parsed_result = common.json_safe(parsed_result) response.status_code = int(parsed_result.get('statusCode', 200)) response.headers.update(parsed_result.get('headers', {})) try: if isinstance(parsed_result['body'], dict): response._content = json.dumps(parsed_result['body']) else: response._content = parsed_result['body'] except Exception: response._content = '{}' return response else: msg = 'API Gateway action uri "%s" not yet implemented' % uri LOGGER.warning(msg) return make_error(msg, 404) elif integration['type'] == 'HTTP': function = getattr(requests, method.lower()) if isinstance(data, dict): data = json.dumps(data) result = function(integration['uri'], data=data, headers=headers) return result else: msg = ('API Gateway integration type "%s" for method "%s" not yet implemented' % (integration['type'], method)) LOGGER.warning(msg) return make_error(msg, 404) return 200 if re.match(PATH_REGEX_AUTHORIZERS, path): return handle_authorizers(method, path, data, headers) return True