def invoke_rest_api(api_id, stage, method, invocation_path, data, headers, path=None, context={}): 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) res_methods = path_map.get(relative_path, {}).get('resourceMethods', {}) meth_integration = res_methods.get(method, {}).get('methodIntegration', {}) int_responses = meth_integration.get('integrationResponses', {}) response_templates = int_responses.get('200', {}).get('responseTemplates', {}) return invoke_rest_api_integration(api_id, stage, integration, method, path, invocation_path, data, headers, resource_path=extracted_path, context=context, resource_id=resource.get('id'), response_templates=response_templates)
def _test_api_gateway_lambda_proxy_integration(self, fn_name, path): self.create_lambda_function(fn_name) # create API Gateway and connect it to the Lambda proxy backend lambda_uri = aws_stack.lambda_function_arn(fn_name) invocation_uri = 'arn:aws:apigateway:%s:lambda:path/2015-03-31/functions/%s/invocations' target_uri = invocation_uri % (config.DEFAULT_REGION, lambda_uri) result = self.connect_api_gateway_to_http_with_lambda_proxy( 'test_gateway2', target_uri, path=path) api_id = result['id'] path_map = get_rest_api_paths(api_id) _, resource = get_resource_for_path('/lambda/foo1', path_map) # make test request to gateway and check response path = path.replace('{test_param1}', 'foo1') path = path + '?foo=foo&bar=bar&bar=baz' url = self.gateway_request_url( api_id=api_id, stage_name=self.TEST_STAGE_NAME, path=path) data = {'return_status_code': 203, 'return_headers': {'foo': 'bar123'}} result = requests.post(url, data=json.dumps(data), headers={'User-Agent': 'python-requests/testing'}) self.assertEqual(result.status_code, 203) self.assertEqual(result.headers.get('foo'), 'bar123') self.assertIn('set-cookie', result.headers) parsed_body = json.loads(to_str(result.content)) self.assertEqual(parsed_body.get('return_status_code'), 203) self.assertDictEqual(parsed_body.get('return_headers'), {'foo': 'bar123'}) self.assertDictEqual(parsed_body.get('queryStringParameters'), {'foo': 'foo', 'bar': ['bar', 'baz']}) request_context = parsed_body.get('requestContext') source_ip = request_context['identity'].pop('sourceIp') self.assertTrue(re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', source_ip)) self.assertEqual(request_context['path'], '/' + self.TEST_STAGE_NAME + '/lambda/foo1') self.assertEqual(request_context.get('stageVariables'), None) self.assertEqual(request_context['accountId'], TEST_AWS_ACCOUNT_ID) self.assertEqual(request_context['resourceId'], resource.get('id')) self.assertEqual(request_context['stage'], self.TEST_STAGE_NAME) self.assertEqual(request_context['identity']['userAgent'], 'python-requests/testing') self.assertEqual(request_context['httpMethod'], 'POST') self.assertEqual(request_context['protocol'], 'HTTP/1.1') self.assertIn('requestTimeEpoch', request_context) self.assertIn('requestTime', request_context) result = requests.delete(url, data=json.dumps(data)) self.assertEqual(result.status_code, 204) # send message with non-ASCII chars body_msg = '🙀 - 参よ' result = requests.post(url, data=json.dumps({'return_raw_body': body_msg})) self.assertEqual(to_str(result.content), body_msg)
def get_target_resource_details( invocation_context: ApiInvocationContext) -> Tuple[str, Dict]: """Look up and return the API GW resource (path pattern + resource dict) for the given invocation context.""" path_map = helpers.get_rest_api_paths( rest_api_id=invocation_context.api_id) relative_path = invocation_context.invocation_path try: extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map) return extracted_path, resource except Exception: return None, None
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(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 test_api_gateway_lambda_proxy_integration(self): # create lambda function zip_file = testutil.create_lambda_archive( load_file(TEST_LAMBDA_PYTHON), get_content=True, libs=TEST_LAMBDA_LIBS, runtime=LAMBDA_RUNTIME_PYTHON27) testutil.create_lambda_function( func_name=self.TEST_LAMBDA_PROXY_BACKEND, zip_file=zip_file, runtime=LAMBDA_RUNTIME_PYTHON27) # create API Gateway and connect it to the Lambda proxy backend lambda_uri = aws_stack.lambda_function_arn( self.TEST_LAMBDA_PROXY_BACKEND) invocation_uri = 'arn:aws:apigateway:%s:lambda:path/2015-03-31/functions/%s/invocations' target_uri = invocation_uri % (DEFAULT_REGION, lambda_uri) result = self.connect_api_gateway_to_http_with_lambda_proxy( 'test_gateway2', target_uri, path=self.API_PATH_LAMBDA_PROXY_BACKEND) api_id = result['id'] path_map = get_rest_api_paths(api_id) _, resource = get_resource_for_path('/lambda/foo1', path_map) # make test request to gateway and check response path = self.API_PATH_LAMBDA_PROXY_BACKEND.replace( '{test_param1}', 'foo1') path = path + '?foo=foo&bar=bar&bar=baz' url = INBOUND_GATEWAY_URL_PATTERN.format( api_id=api_id, stage_name=self.TEST_STAGE_NAME, path=path) data = {'return_status_code': 203, 'return_headers': {'foo': 'bar123'}} result = requests.post( url, data=json.dumps(data), headers={'User-Agent': 'python-requests/testing'}) self.assertEqual(result.status_code, 203) self.assertEqual(result.headers.get('foo'), 'bar123') parsed_body = json.loads(to_str(result.content)) self.assertEqual(parsed_body.get('return_status_code'), 203) self.assertDictEqual(parsed_body.get('return_headers'), {'foo': 'bar123'}) self.assertDictEqual(parsed_body.get('pathParameters'), {'test_param1': 'foo1'}) self.assertDictEqual(parsed_body.get('queryStringParameters'), { 'foo': 'foo', 'bar': ['bar', 'baz'] }) request_context = parsed_body.get('requestContext') source_ip = request_context['identity'].pop('sourceIp') self.assertTrue( re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', source_ip)) self.assertEqual(request_context['path'], '/lambda/foo1') self.assertEqual(request_context['accountId'], TEST_AWS_ACCOUNT_ID) self.assertEqual(request_context['resourceId'], resource.get('id')) self.assertEqual(request_context['stage'], self.TEST_STAGE_NAME) self.assertEqual(request_context['identity']['userAgent'], 'python-requests/testing') result = requests.delete(url, data=json.dumps(data)) self.assertEqual(result.status_code, 404)