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
def process_event(event, context, default_api_info, response_formatter): if is_verbose: print(f'Input: {event}') try: jsonschema.validate(event, CREATE_URL_EVENT_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: if 'api' in event: api_spec = event['api'] region = api_spec.get('region', default_api_info.region) api_id = api_spec.get('api_id') stage = api_spec.get('stage') missing = [] if not api_id: missing.append('API id') if not stage: missing.append('stage') if missing: message = 'Missing ' + ' and '.join(missing) raise MissingApiParametersError(message) else: region = default_api_info.region api_id = default_api_info.api_id stage = default_api_info.stage 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 InvalidDateError(f'Invalid expiration: {str(e)}') expiration_delta = (expiration - timestamp).total_seconds() if expiration_delta <= 0: raise InvalidDateError('Expiration is in the past') log_event['expiration_delta'] = expiration_delta response['expiration'] = expiration.isoformat() payload_builder = PayloadBuilder( transaction_id, timestamp, event['token'], enable_output_parameters=event.get('enable_output_parameters'), expiration=expiration) actions = {} for action_name, action_data in event['actions'].items(): action_type = action_data['type'] actions[action_name] = action_type action_payload_data = {} if action_type == 'success': action_payload_data['output'] = action_data['output'] elif action_type == 'failure': for key in ['error', 'cause']: if key in action_data: action_payload_data[key] = action_data[key] elif action_type != 'heartbeat': raise InvalidActionError( f'Unexpected action type {action_type}') action_response_data = action_data.get('response', {}) payload = payload_builder.build(action_name, action_type, action_payload_data, response=action_response_data, log_event=log_event) encoded_payload = encode_payload(payload, MASTER_KEY_PROVIDER) response['urls'][action_name] = get_url(action_name, action_type, encoded_payload, api_id, stage, region, log_event=log_event) log_event['actions'] = actions 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() (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)}') log_event['transaction_id'] = payload['tid'] response['transaction_id'] = payload['tid'] validate_payload_expiration(payload, timestamp) action_name_in_payload = payload['name'] if action_name_from_url and action_name_from_url != action_name_in_payload: raise ActionMismatchedError( 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['act'] if action_type_from_url and action_type_from_url != action_type_in_payload: raise ActionMismatchedError( 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), )) force_disable_parameters = get_force_disable_parameters() use_parameters = payload.get('par', False) if use_parameters and force_disable_parameters: raise ParametersDisabledError('Parameters are disabled') if not use_parameters: parameters = None action_data = payload.get('data', {}) response_spec = payload.get('resp', {}) return_value = format_response(200, response, request, response_spec, parameters, log_event) method = f'send_task_{action_type}' method_params = {'taskToken': payload['token']} if action_type == 'success': output = action_data.get('output', {}) output = format_output(output, parameters) method_params['output'] = json.dumps(output) elif action_type == 'failure': for key in ['error', 'cause']: if key in action_data: method_params[key] = action_data[key] 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'] errors = [ 'InvalidOutput', 'InvalidToken', 'TaskDoesNotExist', 'TaskTimedOut', ] if error_code in errors: raise StepFunctionsError(error_msg) raise 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