Пример #1
0
def api_handler(event, context):
    """The handler for create URLs calls that come through API Gateway"""
    if is_verbose():
        print(f'Request: {event}')

    default_api_info = DefaultApiInfo(region=BOTO3_SESSION.region_name,
                                      api_id=event['requestContext']['apiId'],
                                      stage=event['requestContext']['stage'])

    def response_formatter(statusCode, headers, body):
        h = {
            'Content-Type': 'application/json',
        }
        h.update(headers)
        return {
            'statusCode': statusCode,
            'headers': headers,
            'body': json.dumps(body) if body else ''
        }

    # Only allow POST
    if event['httpMethod'] != 'POST':
        return {'statusCode': 405, 'headers': {'Allow': 'POST'}}

    # Require JSON content type
    if get_header(event, 'content-type') != 'application/json':
        return {'statusCode': 415, 'headers': {}}

    try:
        event = json.loads(event['body'])
    except json.JSONDecodeError as e:
        return {
            'statusCode': 400,
            'headers': {
                'Content-Type': 'application/json',
            },
            'body': json.dumps({
                'error': 'InvalidJSON',
                'message': f'{str(e)}',
            })
        }

    return process_event(event, context, default_api_info, response_formatter)
Пример #2
0
def process_event(event, context, default_api_info, response_formatter):
    if is_verbose():
        print(f'Input: {event}')

    try:
        jsonschema.validate(event, create_urls_input_schema)
    except jsonschema.ValidationError as e:
        return response_formatter(400, {}, {
            'error': 'InvalidJSON',
            'message': f'{str(e)}',
        })

    transaction_id = uuid.uuid4().hex
    timestamp = datetime.datetime.now()

    log_event = {
        'transaction_id': transaction_id,
        'timestamp': timestamp.isoformat(),
        'actions': [],
    }

    try:
        # Allow the user to specify another URL endpoint, either for a separate sfn-callback-urls
        # deployment, for example in a multi-region or multi-account scenario. The user is on
        # their own for getting the same KMS key in both places.
        if 'base_url' in event:
            if isinstance(event['base_url'], str):
                base_url = event['base_url']
            else:
                api_spec = event['base_url']
                region = api_spec.get('region', default_api_info.region)
                api_id = api_spec['api_id']
                stage = api_spec['stage']

                base_url = get_api_gateway_url(api_id, stage, region)
        else:
            region = default_api_info.region
            api_id = default_api_info.api_id
            stage = default_api_info.stage
            base_url = get_api_gateway_url(api_id, stage, region)

        log_event.update({
            'api_id': api_id,
            'stage': stage,
            'region': region,
        })

        response = {
            'transaction_id': transaction_id,
            'urls': {},
        }

        expiration = None
        if 'expiration' in event:
            try:
                expiration = dateutil.parser.parse(event['expiration'])
            except Exception as e:
                raise InvalidDate(f'Invalid expiration: {str(e)}')
            expiration_delta = (expiration - timestamp).total_seconds()
            log_event['expiration_delta'] = expiration_delta
            if expiration_delta <= 0:
                raise InvalidDate('Expiration is in the past')
            response['expiration'] = expiration.isoformat()

        payload_builder = PayloadBuilder(
            transaction_id,
            timestamp,
            event['token'],
            enable_output_parameters=event.get('enable_output_parameters'),
            expiration=expiration,
            issuer=getattr(context, 'invoked_function_arn', None))

        actions_for_log = {}
        for action in event['actions']:
            action_name = action['name']
            action_type = action['type']

            if action_name in actions_for_log:
                raise DuplicateActionName(
                    f'Action {action_name} provided more than once')

            if action_type == 'post':
                validate_post_action(action)

            actions_for_log[action_name] = action_type

            action_response = action.get('response', {})
            if 'redirect' in action_response:
                log_event['redirect'] = True
            elif any(v in action_response for v in ['json', 'html', 'text']):
                log_event['response_override'] = True

            payload = payload_builder.build(action, log_event=log_event)

            encoded_payload = encode_payload(payload, MASTER_KEY_PROVIDER)

            response['urls'][action_name] = get_url(base_url,
                                                    action_name,
                                                    action_type,
                                                    encoded_payload,
                                                    log_event=log_event)

        log_event['actions'] = actions_for_log

        return_value = response_formatter(200, {}, response)

        send_log_event(log_event)

        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')

        return return_value
    except BaseError as e:
        response = {
            'transaction_id': transaction_id,
            'error': e.code(),
            'message': e.message(),
        }
        log_event['error'] = {
            'type': 'RequestError',
            'error': e.code(),
            'message': e.message(),
        }
        return_value = response_formatter(400, {}, response)
        send_log_event(log_event)
        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')
        return return_value
    except Exception as e:
        traceback.print_exc()
        error_class_name = type(e).__module__ + '.' + type(e).__name__
        response = {
            'error': 'ServiceError',
            'message': f'{error_class_name}: {str(e)}'
        }
        log_event['error'] = {
            'type': 'Unexpected',
            'error': error_class_name,
            'message': str(e),
        }
        return_value = response_formatter(500, {}, response)
        send_log_event(log_event)
        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')
        return return_value
def handler(request, context):
    if is_verbose():
        print(f'Request: {json.dumps(request)}')

    timestamp = datetime.datetime.now()

    log_event = {
        'timestamp': timestamp.isoformat(),
    }

    try:
        response = OrderedDict(
        )  # ordered so it appears sensibly in the HTML output

        (action_name_from_url, action_type_from_url, encoded_payload,
         parameters) = load_from_request(request)

        decode_start = time.perf_counter()
        payload = decode_payload(encoded_payload, MASTER_KEY_PROVIDER)
        decode_finish = time.perf_counter()
        log_event['decode_time'] = (decode_finish - decode_start)

        validate_payload_schema(payload)

        if is_verbose():
            print(f'Payload: {json.dumps(payload)}')

        # use the same transaction id given out in the create urls call
        log_event['transaction_id'] = payload['tid']
        response['transaction_id'] = payload['tid']

        validate_payload_expiration(payload, timestamp)

        # we put the action name and type in the query string directly for convenience
        # but we only trust the version that's in the payload. If the query string
        # versions differ from the payload, something funny is going on and we reject
        # the request. But if they are absent, it's not a problem.

        action_name_in_payload = payload['action']['name']
        if action_name_from_url and action_name_from_url != action_name_in_payload:
            raise ActionMismatched(
                f'The action name says {action_name_from_url} in the url but {action_name_in_payload} in the payload'
            )
        action_name = action_name_in_payload

        action_type_in_payload = payload['action']['type']
        if action_type_from_url and action_type_from_url != action_type_in_payload:
            raise ActionMismatched(
                f'The action type says {action_type_in_payload} in the url but {action_type_in_payload} in the payload'
            )
        action_type = action_type_in_payload

        log_event['action'] = {'name': action_name, 'type': action_type}
        response['action'] = OrderedDict((
            ('name', action_name),
            ('type', action_type),
        ))

        # If parameters are disabled, refuse to service a request
        # that has parameters enabled, even though it was presumably
        # valid at creation time to have parameters enabled.
        force_disable_parameters = get_force_disable_parameters()
        use_parameters = payload.get('param', False)
        if use_parameters and force_disable_parameters:
            raise ParametersDisabled('Parameters are disabled')
        if not use_parameters:
            parameters = None

        action = payload['action']

        response_spec = action.get('response', {})

        outcome_name = action_name
        outcome_type = action_type

        if action_type == 'post':
            (post_outcome_name, post_outcome_type, outcome_response_spec,
             method_params) = process_post_action(action, request, parameters,
                                                  log_event)
            outcome_name = outcome_name + '.' + post_outcome_name
            outcome_type = post_outcome_type

            if outcome_response_spec is not None:
                response_spec = outcome_response_spec
        else:
            method_params = prepare_method_params(action,
                                                  parameters,
                                                  log_event=log_event)

        log_event['outcome_name'] = outcome_name
        log_event['outcome_type'] = outcome_type

        if is_verbose():
            print(f'Input for {outcome_type}: {json.dumps(method_params)}')

        method = f'send_task_{outcome_type}'
        method_params['taskToken'] = payload['token']

        try:
            sfn_call_start = time.perf_counter()
            sfn_response = getattr(STEP_FUNCTIONS_CLIENT,
                                   method)(**method_params)
            sfn_call_finish = time.perf_counter()
            log_event['sfn_call_time'] = (sfn_call_finish - sfn_call_start)
        except botocore.exceptions.ClientError as e:
            error_code = e.response['Error']['Code']
            error_msg = e.response['Error']['Message']
            # These errors are related to the state machine itself, and
            # should be 400 errors.
            # Other ClientErrors, like invalid permissions, should be
            # considered 500 errors.
            errors = [
                'InvalidOutput',
                'InvalidToken',
                'TaskDoesNotExist',
                'TaskTimedOut',
            ]
            if error_code in errors:
                raise StepFunctionsError(f'{error_code}:{error_msg}')
            raise

        return_value = format_response(200, response, request, response_spec,
                                       parameters, log_event)

        send_log_event(log_event)

        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')

        return return_value
    except ReturnHttpResponse as e:
        log_event['error'] = {
            'type': e.TYPE,
            'error': e.code(),
            'message': e.message(),
        }
        return_value = e.get_response()
        send_log_event(log_event)
        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')
        return return_value
    except BaseError as e:
        response = OrderedDict((
            ('error', e.code()),
            ('message', e.message()),
        ))
        log_event['error'] = {
            'type': e.TYPE,
            'error': e.code(),
            'message': e.message(),
        }
        return_value = format_response(400, response, request, {}, None,
                                       log_event)
        send_log_event(log_event)
        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')
        return return_value
    except Exception as e:
        traceback.print_exc()
        error_class_name = type(e).__module__ + '.' + type(e).__name__
        response = OrderedDict((
            ('error', 'ServiceError'),
            ('message', f'{error_class_name}: {str(e)}'),
        ))
        log_event['error'] = {
            'type': 'Unexpected',
            'error': error_class_name,
            'message': str(e),
        }
        return_value = format_response(500, response, request, {}, None,
                                       log_event)
        send_log_event(log_event)
        if is_verbose():
            print(f'Response: {json.dumps(return_value)}')
        return return_value