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)
Example #2
0
    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)
Example #3
0
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
Example #4
0
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)