def test_wsgi_path_info(self): # Test no parameters (site.com/) event = { "body": {}, "headers": {}, "params": {}, "method": "GET", "query": {} } request = create_wsgi_request(event, trailing_slash=True) self.assertEqual("/", request['PATH_INFO']) request = create_wsgi_request(event, trailing_slash=False) self.assertEqual("/", request['PATH_INFO']) # Test parameters (site.com/asdf1/asdf2 or site.com/asdf1/asdf2/) event = { "body": {}, "headers": {}, "params": { "parameter_1": "asdf1", "parameter_2": "asdf2", }, "method": "GET", "query": {} } request = create_wsgi_request(event, trailing_slash=True) self.assertEqual("/asdf1/asdf2/", request['PATH_INFO']) request = create_wsgi_request(event, trailing_slash=False) self.assertEqual("/asdf1/asdf2", request['PATH_INFO'])
def test_should_allow_empty_query_params(self): event = { u'httpMethod': u'GET', u'queryStringParameters': {}, u'multiValueQueryStringParameters': {}, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u"resourceId": u"123456", u"apiId": u"1234567890", u"resourcePath": u"/{proxy+}", u"httpMethod": u"POST", u"requestId": u"c6af9ac6-7b61-11e6-9a41-93e8deadbeef", u"accountId": u"123456789012", u"identity": { u"userAgent": u"Custom User Agent String", u"cognitoIdentityPoolId": u"userpoolID", u"cognitoIdentityId": u"myCognitoID", u"sourceIp": u"127.0.0.1", }, "stage": "prod" }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) self.assertEqual(environ['QUERY_STRING'], u'')
def test_wsgi_event(self): event = { "body": "", "headers": { "Via": "1.1 e604e934e9195aaf3e36195adbcb3e18.cloudfront.net (CloudFront)", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Forwarded-Proto": "https", "X-Forwarded-For": "109.81.209.118, 216.137.58.43", "CloudFront-Viewer-Country": "CZ", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "X-Forwarded-Proto": "https", "X-Amz-Cf-Id": "LZeP_TZxBgkDt56slNUr_H9CHu1Us5cqhmRSswOh1_3dEGpks5uW-g==", "CloudFront-Is-Tablet-Viewer": "false", "X-Forwarded-Port": "443", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-Desktop-Viewer": "true", "Content-Type": "application/json" }, "params": { "parameter_1": "asdf1", "parameter_2": "asdf2", }, "method": "POST", "query": { "dead": "beef" } } request = create_wsgi_request(event)
def test_should_allow_empty_query_params(self): event = { "httpMethod": "GET", "queryStringParameters": {}, "multiValueQueryStringParameters": {}, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "resourceId": "123456", "apiId": "1234567890", "resourcePath": "/{proxy+}", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "identity": { "userAgent": "Custom User Agent String", "cognitoIdentityPoolId": "userpoolID", "cognitoIdentityId": "myCognitoID", "sourceIp": "127.0.0.1", }, "stage": "prod", }, "query": {}, } environ = create_wsgi_request(event, script_name="http://zappa.com/", trailing_slash=False) self.assertEqual(environ["QUERY_STRING"], "")
def test_wsgi_path_info_unquoted(self): event = { "body": {}, "headers": {}, "pathParameters": {}, "path": '/path%3A1', # encoded /path:1 "httpMethod": "GET", "queryStringParameters": {}, "requestContext": {} } request = create_wsgi_request(event, trailing_slash=True) self.assertEqual("/path:1", request['PATH_INFO'])
def test_wsgi_path_info(self): # Test no parameters (site.com/) event = { "body": {}, "headers": {}, "params": {}, "method": "GET", "query": {} } request = create_wsgi_request(event, trailing_slash=True) self.assertEqual("/", request['PATH_INFO']) request = create_wsgi_request(event, trailing_slash=False) self.assertEqual("/", request['PATH_INFO']) # Test parameters (site.com/asdf1/asdf2 or site.com/asdf1/asdf2/) event = { "body": {}, "headers": {}, "params": { "parameter_1": "asdf1", "parameter_2": "asdf2", }, "method": "GET", "query": {} } request = create_wsgi_request(event, trailing_slash=True) self.assertEqual("/asdf1/asdf2/", request['PATH_INFO']) request = create_wsgi_request(event, trailing_slash=False) self.assertEqual("/asdf1/asdf2", request['PATH_INFO']) request = create_wsgi_request(event, trailing_slash=False, script_name='asdf1') self.assertEqual("/asdf1/asdf2", request['PATH_INFO'])
def lambda_handler(event, context): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zappa_settings") import django django.setup() # This is a normal HTTP request if event.get('method', None): handler = LambdaHandler() environ = create_wsgi_request(event) response = handler(environ) returnme = {'Content': response.content} for item in response.items(): returnme[item[0]] = item[1] returnme['Status'] = response.status_code if response.status_code != 200: # So that we can always match on the first few characters # ex '{"AAA": "404' # returnme['AAA'] = str(response.status_code) # returnme['errorMessage'] = str(response.status_code) # returnme['errorType'] = str(response.status_code) # returnme['stackTrace'] = str(response.status_code) # error_json = json.dumps(returnme, sort_keys=True) # print "Error JSON:" # print error_json content = response.content content = "<!DOCTYPE html>" + str(response.status_code) + response.content b64_content = base64.b64encode(content) print b64_content raise Exception(b64_content) print returnme return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {}
def test_wsgi_logging(self): event = { "body": {}, "headers": {}, "params": { "parameter_1": "asdf1", "parameter_2": "asdf2", }, "method": "GET", "query": {} } environ = create_wsgi_request(event, trailing_slash=False) response_tuple = collections.namedtuple('Response', ['status_code', 'content']) response = response_tuple(200, 'hello') le = common_log(environ, response, response_time=True) le = common_log(environ, response, response_time=False)
def test_wsgi_logging(self): # event = { # "body": {}, # "headers": {}, # "params": { # "parameter_1": "asdf1", # "parameter_2": "asdf2", # }, # "httpMethod": "GET", # "query": {} # } event = {u'body': None, u'resource': u'/{proxy+}', u'requestContext': {u'resourceId': u'dg451y', u'apiId': u'79gqbxq31c', u'resourcePath': u'/{proxy+}', u'httpMethod': u'GET', u'requestId': u'766df67f-8991-11e6-b2c4-d120fedb94e5', u'accountId': u'724336686645', u'identity': {u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'96.90.37.59', u'accountId': None}, u'stage': u'devorr'}, u'queryStringParameters': None, u'httpMethod': u'GET', u'pathParameters': {u'proxy': u'asdf1/asdf2'}, u'headers': {u'Via': u'1.1 b2aeb492548a8a2d4036401355f928dd.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'X-Forwarded-Port': u'443', u'X-Forwarded-For': u'96.90.37.59, 54.240.144.50', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'Upgrade-Insecure-Requests': u'1', u'Host': u'79gqbxq31c.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'X-Amz-Cf-Id': u'BBFP-RhGDrQGOzoCqjnfB2I_YzWt_dac9S5vBcSAEaoM4NfYhAQy7Q==', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'CloudFront-Forwarded-Proto': u'https'}, u'stageVariables': None, u'path': u'/asdf1/asdf2'} environ = create_wsgi_request(event, trailing_slash=False) response_tuple = collections.namedtuple('Response', ['status_code', 'content']) response = response_tuple(200, 'hello') le = common_log(environ, response, response_time=True) le = common_log(environ, response, response_time=False)
def test_wsgi_multipart(self): event = { u'body': u'LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03Njk1MjI4NDg0Njc4MTc2NTgwNjMwOTYxDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9Im15c3RyaW5nIg0KDQpkZGQNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tNzY5NTIyODQ4NDY3ODE3NjU4MDYzMDk2MS0tDQo=', u'headers': { u'Content-Type': u'multipart/form-data; boundary=---------------------------7695228484678176580630961', u'Via': u'1.1 38205a04d96d60185e88658d3185ccee.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'71.231.27.57, 104.246.180.51', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0', u'Host': u'xo2z7zafjh.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'Cookie': u'zappa=AQ4', u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'Referer': u'https://xo8z7zafjh.execute-api.us-east-1.amazonaws.com/former/post', u'CloudFront-Is-Mobile-Viewer': u'false', u'X-Amz-Cf-Id': u'31zxcUcVyUxBOMk320yh5NOhihn5knqrlYQYpGGyOngKKwJb0J0BAQ==', u'CloudFront-Is-Desktop-Viewer': u'true' }, u'params': { u'parameter_1': u'post' }, u'method': u'POST', u'query': {} } environ = create_wsgi_request(event, trailing_slash=False) response_tuple = collections.namedtuple('Response', ['status_code', 'content']) response = response_tuple(200, 'hello')
def test_wsgi_without_body(self): event = { u'body': None, u'resource': u'/', u'requestContext': { u'resourceId': u'6cqjw9qu0b', u'apiId': u'9itr2lba55', u'resourcePath': u'/', u'httpMethod': u'POST', u'requestId': u'c17cb1bf-867c-11e6-b938-ed697406e3b5', u'accountId': u'724336686645', u'identity': { u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:48.0) Gecko/20100101 Firefox/48.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'50.191.225.98', u'accountId': None, }, u'stage': u'devorr', }, u'queryStringParameters': None, u'httpMethod': u'POST', u'pathParameters': None, u'headers': {u'Via': u'1.1 38205a04d96d60185e88658d3185ccee.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'71.231.27.57, 104.246.180.51', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0', u'Host': u'xo2z7zafjh.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'Cookie': u'zappa=AQ4', u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'Referer': u'https://xo8z7zafjh.execute-api.us-east-1.amazonaws.com/former/post', u'CloudFront-Is-Mobile-Viewer': u'false', u'X-Amz-Cf-Id': u'31zxcUcVyUxBOMk320yh5NOhihn5knqrlYQYpGGyOngKKwJb0J0BAQ==', u'CloudFront-Is-Desktop-Viewer': u'true'}, u'stageVariables': None, u'path': u'/', } environ = create_wsgi_request(event, trailing_slash=False) response_tuple = collections.namedtuple('Response', ['status_code', 'content']) response = response_tuple(200, 'hello')
def test_should_handle_multi_value_query_string_params(self): event = { 'httpMethod': 'GET', 'queryStringParameters': {}, 'multiValueQueryStringParameters': { 'foo': [1, 2] }, 'path': '/v1/runs', 'params': {}, 'body': {}, 'headers': { 'Content-Type': 'application/json' }, 'pathParameters': { 'proxy': 'v1/runs' }, 'requestContext': { "resourceId": "123456", "apiId": "1234567890", "resourcePath": "/{proxy+}", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "identity": { "userAgent": "Custom User Agent String", "cognitoIdentityPoolId": "userpoolID", "cognitoIdentityId": "myCognitoID", "sourceIp": "127.0.0.1", }, "stage": "prod" }, 'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) self.assertEqual(environ['QUERY_STRING'], 'foo=1&foo=2')
def lambda_handler(event, context=None, settings_name="zappa_settings"): # NoQA """ An AWS Lambda function which parses specific API Gateway input into a WSGI request. The request get fed it to Django, processes the Django response, and returns that back to the API Gateway. """ time_start = datetime.datetime.now() # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.info('Zappa Event: {}'.format(event)) # This is a normal HTTP request if event.get('method', None): # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' wrap_me = get_wsgi_application() app = ZappaWSGIMiddleware(wrap_me) # Execute the application response = Response.from_app(app, environ) response.content = response.data # Prepare the special dictionary which will be returned to the API GW. returnme = {'Content': response.data} # Pack the WSGI response into our special dictionary. for (header_name, header_value) in response.headers: returnme[header_name] = header_value returnme['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in ERROR_CODES: content = u"<!DOCTYPE html>" + unicode(response.status_code) + unicode('<meta charset="utf-8" />') + response.data.encode('utf-8') b64_content = base64.b64encode(content) exception = (b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif 300 <= response.status_code < 400 and response.has_header('Location'): location = returnme['Location'] location = '/' + location.replace("http://zappa/", "") exception = location # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: raise Exception(exception) else: return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {} elif event.get('detail'): module, function = event['detail'].rsplit('.', 1) app_module = importlib.import_module(module) app_function = getattr(app_module, function) # Execute the function! app_function() return else: logger.error('Unhandled event: {}'.format(json.dumps(event)))
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split( '-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function( whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overriden by unsanitized, non-admin user input. elif event.get('raw_command', None): raw_command = event['raw_command'] exec(raw_command) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function( whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error( "Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get('type') == u'TOKEN': whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function( whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error( "Cannot find a function to process the authorization request." ) raise Exception('Unauthorized') # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get('httpMethod', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true # event_echo = getattr(settings, "EVENT_ECHO", True) # if event_echo and 'event_echo' in event['params'].values(): # return {'Content': str(event) + '\n' + str(context), 'Status': 200} if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, trailing_slash=self.trailing_slash) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context # Execute the application response = Response.from_app(self.wsgi_app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() if response.data: zappa_returndict['body'] = response.data zappa_returndict['statusCode'] = response.status_code zappa_returndict['headers'] = {} for key, value in response.headers: zappa_returndict['headers'][key] = value # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None # this variable never used # if response.status_code in ERROR_CODES: # content = collections.OrderedDict() # content['http_status'] = response.status_code # content['content'] = base64.b64encode(response.data.encode('utf-8')) # exception = json.dumps(content) # # Internal are changed to become relative redirects # # so they still work for apps on raw APIGW and on a domain. # elif 300 <= response.status_code < 400 and hasattr(response, 'Location'): # # Location is by default relative on Flask. Location is by default # # absolute on Werkzeug. We can set autocorrect_location_header on # # the response to False, but it doesn't work. We have to manually # # remove the host part. # location = response.location # hostname = 'https://' + environ['HTTP_HOST'] # if location.startswith(hostname): # exception = location[len(hostname):] # else: # exception = location # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ( 'An uncaught exception happened while servicing this request. ' 'You can investigate this with the `zappa tail` command.') # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['statusCode'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception( *exc_info) # traceback as a list for readability. content['body'] = json.dumps(body, sort_keys=True, indent=4).encode('utf-8') return content
def test_wsgi_map_context_headers_handling(self): # Validate a single context value mapping is translated into a HTTP header event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u'authorizer': { u'principalId': u'user1' }, }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False, context_header_mappings={'PrincipalId': 'authorizer.principalId'}) self.assertEqual(environ['HTTP_PRINCIPALID'], u'user1') # Validate multiple mappings with an invalid mapping # Invalid mapping should be ignored event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u"resourceId": u"123456", u"apiId": u"1234567890", u"resourcePath": u"/{proxy+}", u"httpMethod": u"POST", u"requestId": u"c6af9ac6-7b61-11e6-9a41-93e8deadbeef", u"accountId": u"123456789012", u"identity": { u"userAgent": u"Custom User Agent String", u"cognitoIdentityPoolId": u"userpoolID", u"cognitoIdentityId": u"myCognitoID", u"sourceIp": u"127.0.0.1", }, "stage": "prod" }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False, context_header_mappings={'CognitoIdentityID': 'identity.cognitoIdentityId', 'APIStage': 'stage', 'InvalidValue': 'identity.cognitoAuthenticationType', 'OtherInvalid': 'nothinghere'}) self.assertEqual(environ['HTTP_COGNITOIDENTITYID'], u'myCognitoID') self.assertEqual(environ['HTTP_APISTAGE'], u'prod') self.assertNotIn('HTTP_INVALIDVALUE', environ) self.assertNotIn('HTTP_OTHERINVALID', environ)
def lambda_handler(event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to Django, procceses the Django response, and returns that back to the API Gateway. """ # Django requires settings and an explicit call to setup() # if being used from inside a python context. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zappa_settings") import django django.setup() from django.conf import settings # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in event['params'].values(): return { 'Content': str(event) + '\n' + str(context), 'Status': 200 } # This doesn't matter, but Django's handler requires it. def start(a, b): return # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME) handler = WSGIHandler() response = handler(environ, start) # Prepare the special dictionary which will be returned to the API GW. returnme = {'Content': response.content} # Pack the WSGI response into our special dictionary. for item in response.items(): returnme[item[0]] = item[1] returnme['Status'] = response.status_code # Parse the WSGI Cookie and pack it. cookie = response.cookies.output() if ': ' in cookie: returnme['Set-Cookie'] = response.cookies.output().split(': ')[1] # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. if response.status_code in [400, 401, 403, 500]: content = response.content content = "<!DOCTYPE html>" + str( response.status_code) + response.content b64_content = base64.b64encode(content) raise Exception(b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: location = returnme['Location'] location = '/' + location.replace("http://zappa/", "") raise Exception(location) else: return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {}
def test_wsgi_map_context_headers_handling(self): # Validate a single context value mapping is translated into a HTTP header event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "authorizer": { "principalId": "user1" }, }, "query": {}, } environ = create_wsgi_request( event, script_name="http://zappa.com/", trailing_slash=False, context_header_mappings={"PrincipalId": "authorizer.principalId"}, ) self.assertEqual(environ["HTTP_PRINCIPALID"], "user1") # Validate multiple mappings with an invalid mapping # Invalid mapping should be ignored event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "resourceId": "123456", "apiId": "1234567890", "resourcePath": "/{proxy+}", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "identity": { "userAgent": "Custom User Agent String", "cognitoIdentityPoolId": "userpoolID", "cognitoIdentityId": "myCognitoID", "sourceIp": "127.0.0.1", }, "stage": "prod", }, "query": {}, } environ = create_wsgi_request( event, script_name="http://zappa.com/", trailing_slash=False, context_header_mappings={ "CognitoIdentityID": "identity.cognitoIdentityId", "APIStage": "stage", "InvalidValue": "identity.cognitoAuthenticationType", "OtherInvalid": "nothinghere", }, ) self.assertEqual(environ["HTTP_COGNITOIDENTITYID"], "myCognitoID") self.assertEqual(environ["HTTP_APISTAGE"], "prod") self.assertNotIn("HTTP_INVALIDVALUE", environ) self.assertNotIn("HTTP_OTHERINVALID", environ)
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ # This is the result of a keep alive # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1] module, function = whole_function.rsplit('.', 1) app_module = importlib.import_module(module) app_function = getattr(app_module, function) # Execute the function! app_function() return # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] module, function = whole_function.rsplit('.', 1) app_module = importlib.import_module(module) app_function = getattr(app_module, function) result = app_function() print("Result of %s:" % whole_function) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} try: # Timing time_start = datetime.datetime.now() settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # Custom log level if settings.LOG_LEVEL: level = logging.getLevelName(settings.LOG_LEVEL) logger.setLevel(level) # Django gets special treatment. if not settings.DJANGO_SETTINGS: # The app module app_module = importlib.import_module(settings.APP_MODULE) # The application app_function = getattr(app_module, settings.APP_FUNCTION) trailing_slash = False else: try: # Support both for tests from zappa.ext.django import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension app_function = get_django_wsgi(settings.DJANGO_SETTINGS) trailing_slash = True app = ZappaWSGIMiddleware(app_function) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo and 'event_echo' in event['params'].values(): return {'Content': str(event) + '\n' + str(context), 'Status': 200} if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=script_name, trailing_slash=trailing_slash ) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' # Execute the application response = Response.from_app(app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict(response.headers) if 'Content' not in zappa_returndict and response.data: zappa_returndict['Content'] = response.data zappa_returndict['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in ERROR_CODES: content = u"<!DOCTYPE html>" + unicode(response.status_code) + unicode('<meta charset="utf-8" />') + response.data.encode('utf-8') exception = base64.b64encode(content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif 300 <= response.status_code < 400 and hasattr(response, 'Location'): # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): exception = location[len(hostname):] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: # pragma: no cover raise LambdaException(exception) else: return zappa_returndict except LambdaException as e: # pragma: no cover raise e except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: app_module except NameError: raise e # Print the error to the browser upon failure? if settings.DEBUG: # Return this unspecified exception as a 500. content = "<!DOCTYPE html>500. From Zappa: <pre>" + str(e) + "</pre><br /><pre>" + traceback.format_exc().replace('\n', '<br />') + "</pre>" exception = base64.b64encode(content) raise Exception(exception) else: raise e
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split('-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function(whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overriden by unsanitized, non-admin user input. elif event.get('raw_command', None): raw_command = event['raw_command'] exec(raw_command) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get('type') == u'TOKEN': whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function(whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error("Cannot find a function to process the authorization request.") raise Exception('Unauthorized') # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get('httpMethod', None): if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT ) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context # Execute the application response = Response.from_app(self.wsgi_app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() if response.data: if settings.BINARY_SUPPORT: if not response.mimetype.startswith("text/") \ or response.mimetype != "application/json": zappa_returndict['body'] = base64.b64encode(response.data) zappa_returndict["isBase64Encoded"] = "true" else: zappa_returndict['body'] = response.data else: zappa_returndict['body'] = response.data zappa_returndict['statusCode'] = response.status_code zappa_returndict['headers'] = {} for key, value in response.headers: zappa_returndict['headers'][key] = value # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ('An uncaught exception happened while servicing this request. ' 'You can investigate this with the `zappa tail` command.') # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['statusCode'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception(*exc_info) # traceback as a list for readability. content['body'] = json.dumps(body, sort_keys=True, indent=4).encode('utf-8') return content
def lambda_handler(event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to Django, procceses the Django response, and returns that back to the API Gateway. """ # Django requires settings and an explicit call to setup() # if being used from inside a python context. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zappa_settings") import django django.setup() from django.conf import settings # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in event['params'].values(): return {'Content': str(event) + '\n' + str(context), 'Status': 200} # This doesn't matter, but Django's handler requires it. def start(a, b): return # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME) handler = WSGIHandler() response = handler(environ, start) # Prepare the special dictionary which will be returned to the API GW. returnme = {'Content': response.content} # Pack the WSGI response into our special dictionary. for item in response.items(): returnme[item[0]] = item[1] returnme['Status'] = response.status_code # Parse the WSGI Cookie and pack it. cookie = response.cookies.output() if ': ' in cookie: returnme['Set-Cookie'] = response.cookies.output().split(': ')[1] # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. if response.status_code in [400, 401, 403, 500]: content = response.content content = "<!DOCTYPE html>" + str(response.status_code) + response.content b64_content = base64.b64encode(content) raise Exception(b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: location = returnme['Location'] location = '/' + location.replace("http://zappa/", "") raise Exception(location) else: return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {}
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: print('Zappa Event: {}'.format(event)) logger.debug('Zappa Event: {}'.format(event)) # Custom log level if settings.LOG_LEVEL: level = logging.getLevelName(settings.LOG_LEVEL) logger.setLevel(level) # This is the result of a keep alive # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split( '-')[-1] # This is a scheduled, non-keep-alive function. # This is not the best way to do this but it'll do. if '.' in whole_function: app_function = self.import_module_and_get_function( whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') event_types = ['dynamodb', 'kinesis', 's3', 'sns', 'events'] for record in records: for event_type in event_types: if record.has_key(event_type): whole_function = record[event_type]['configurationId'] app_function = self.import_module_and_get_function( whole_function) result = self.run_function(app_function, event, context) print(result) return result try: # Timing time_start = datetime.datetime.now() # Django gets special treatment. if not settings.DJANGO_SETTINGS: # The app module app_module = importlib.import_module(settings.APP_MODULE) # The application app_function = getattr(app_module, settings.APP_FUNCTION) trailing_slash = False else: try: # Support both for tests from zappa.ext.django import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension app_function = get_django_wsgi(settings.DJANGO_SETTINGS) trailing_slash = True app = ZappaWSGIMiddleware(app_function) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo and 'event_echo' in event['params'].values(): return { 'Content': str(event) + '\n' + str(context), 'Status': 200 } if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=script_name, trailing_slash=trailing_slash) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context # Execute the application response = Response.from_app(app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict(response.headers) if 'Content' not in zappa_returndict and response.data: zappa_returndict['Content'] = response.data zappa_returndict['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in ERROR_CODES: content = u"<!DOCTYPE html>" + unicode( response.status_code) + unicode( '<meta charset="utf-8" />') + response.data.encode( 'utf-8') exception = base64.b64encode(content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif 300 <= response.status_code < 400 and hasattr( response, 'Location'): # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): exception = location[len(hostname):] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: # pragma: no cover raise LambdaException(exception) else: return zappa_returndict except LambdaException as e: # pragma: no cover raise e except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: app_module except NameError: raise e # Print the error to the browser upon failure? if settings.DEBUG: # Return this unspecified exception as a 500. content = "<!DOCTYPE html>500. From Zappa: <pre>" + str( e) + "</pre><br /><pre>" + traceback.format_exc().replace( '\n', '<br />') + "</pre>" exception = base64.b64encode(content) raise Exception(exception) else: raise e
def test_wsgi_map_context_headers_handling(self): # Validate a single context value mapping is translated into a HTTP header event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u'authorizer': { u'principalId': u'user1' }, }, u'query': {} } environ = create_wsgi_request( event, script_name='http://zappa.com/', trailing_slash=False, context_header_mappings={'PrincipalId': 'authorizer.principalId'}) self.assertEqual(environ['HTTP_PRINCIPALID'], u'user1') # Validate multiple mappings with an invalid mapping # Invalid mapping should be ignored event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u"resourceId": u"123456", u"apiId": u"1234567890", u"resourcePath": u"/{proxy+}", u"httpMethod": u"POST", u"requestId": u"c6af9ac6-7b61-11e6-9a41-93e8deadbeef", u"accountId": u"123456789012", u"identity": { u"userAgent": u"Custom User Agent String", u"cognitoIdentityPoolId": u"userpoolID", u"cognitoIdentityId": u"myCognitoID", u"sourceIp": u"127.0.0.1", }, "stage": "prod" }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False, context_header_mappings={ 'CognitoIdentityID': 'identity.cognitoIdentityId', 'APIStage': 'stage', 'InvalidValue': 'identity.cognitoAuthenticationType', 'OtherInvalid': 'nothinghere' }) self.assertEqual(environ['HTTP_COGNITOIDENTITYID'], u'myCognitoID') self.assertEqual(environ['HTTP_APISTAGE'], u'prod') self.assertNotIn('HTTP_INVALIDVALUE', environ) self.assertNotIn('HTTP_OTHERINVALID', environ)
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, processes the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug("Zappa Event: {}".format(event)) # Set any API Gateway defined Stage Variables # as env vars if event.get("stageVariables"): for key in event["stageVariables"].keys(): os.environ[str(key)] = event["stageVariables"][key] # This is the result of a keep alive, recertify # or scheduled event. if event.get("detail-type") == "Scheduled Event": whole_function = event["resources"][0].split("/")[-1].split("-")[-1] # This is a scheduled function. if "." in whole_function: app_function = self.import_module_and_get_function(whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get("command", None): whole_function = event["command"] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overridden by unsanitized, non-admin user input. elif event.get("raw_command", None): raw_command = event["raw_command"] exec(raw_command) return # This is a Django management command invocation. elif event.get("manage", None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event["manage"].split(" ")) return {} # This is an AWS-event triggered invocation. elif event.get("Records", None): records = event.get("Records") result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # this is an AWS-event triggered from Lex bot's intent elif event.get("bot"): result = None whole_function = self.get_function_from_bot_intent_trigger(event) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get("type") == "TOKEN": whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function(whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error( "Cannot find a function to process the authorization request." ) raise Exception("Unauthorized") # This is an AWS Cognito Trigger Event elif event.get("triggerSource", None): triggerSource = event.get("triggerSource") whole_function = self.get_function_for_cognito_trigger(triggerSource) result = event if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error( "Cannot find a function to handle cognito trigger {}".format( triggerSource ) ) return result # This is a CloudWatch event # Related: https://github.com/Miserlou/Zappa/issues/1924 elif event.get("awslogs", None): result = None whole_function = "{}.{}".format(settings.APP_MODULE, settings.APP_FUNCTION) app_function = self.import_module_and_get_function(whole_function) if app_function: result = self.run_function(app_function, event, context) logger.debug("Result of %s:" % whole_function) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get("httpMethod", None): script_name = "" is_elb_context = False headers = merge_headers(event) if event.get("requestContext", None) and event["requestContext"].get( "elb", None ): # Related: https://github.com/Miserlou/Zappa/issues/1715 # inputs/outputs for lambda loadbalancer # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html is_elb_context = True # host is lower-case when forwarded from ELB host = headers.get("host") # TODO: pathParameters is a first-class citizen in apigateway but not available without # some parsing work for ELB (is this parameter used for anything?) event["pathParameters"] = "" else: if headers: host = headers.get("Host") else: host = None logger.debug("host found: [{}]".format(host)) if host: if "amazonaws.com" in host: logger.debug("amazonaws found in host") # The path provided in th event doesn't include the # stage, so we must tell Flask to include the API # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 script_name = "/" + settings.API_STAGE else: # This is a test request sent from the AWS console if settings.DOMAIN: # Assume the requests received will be on the specified # domain. No special handling is required pass else: # Assume the requests received will be to the # amazonaws.com endpoint, so tell Flask to include the # API stage script_name = "/" + settings.API_STAGE base_path = getattr(settings, "BASE_PATH", None) # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, base_path=base_path, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT, context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS, ) # We are always on https on Lambda, so tell our wsgi app that. environ["HTTPS"] = "on" environ["wsgi.url_scheme"] = "https" environ["lambda.context"] = context environ["lambda.event"] = event # Execute the application with Response.from_app(self.wsgi_app, environ) as response: # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() # Issue #1715: ALB support. ALB responses must always include # base64 encoding and status description if is_elb_context: zappa_returndict.setdefault("isBase64Encoded", False) zappa_returndict.setdefault( "statusDescription", response.status ) if response.data: if ( settings.BINARY_SUPPORT and not response.mimetype.startswith("text/") and response.mimetype != "application/json" ): zappa_returndict["body"] = base64.b64encode( response.data ).decode("utf-8") zappa_returndict["isBase64Encoded"] = True else: zappa_returndict["body"] = response.get_data(as_text=True) zappa_returndict["statusCode"] = response.status_code if "headers" in event: zappa_returndict["headers"] = {} for key, value in response.headers: zappa_returndict["headers"][key] = value if "multiValueHeaders" in event: zappa_returndict["multiValueHeaders"] = {} for key, value in response.headers: zappa_returndict["multiValueHeaders"][ key ] = response.headers.getlist(key) # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ( "An uncaught exception happened while servicing this request. " "You can investigate this with the `zappa tail` command." ) # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = "Failed to import module: {}".format(ne.message) # Call exception handler for unhandled exceptions exception_handler = self.settings.EXCEPTION_HANDLER self._process_exception( exception_handler=exception_handler, event=event, context=context, exception=e, ) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content["statusCode"] = 500 body = {"message": message} if settings.DEBUG: # only include traceback if debug is on. body["traceback"] = traceback.format_exception( *exc_info ) # traceback as a list for readability. content["body"] = json.dumps(str(body), sort_keys=True, indent=4) return content
def test_wsgi_event(self): ## This is a pre-proxy+ event # event = { # "body": "", # "headers": { # "Via": "1.1 e604e934e9195aaf3e36195adbcb3e18.cloudfront.net (CloudFront)", # "Accept-Language": "en-US,en;q=0.5", # "Accept-Encoding": "gzip", # "CloudFront-Is-SmartTV-Viewer": "false", # "CloudFront-Forwarded-Proto": "https", # "X-Forwarded-For": "109.81.209.118, 216.137.58.43", # "CloudFront-Viewer-Country": "CZ", # "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", # "X-Forwarded-Proto": "https", # "X-Amz-Cf-Id": "LZeP_TZxBgkDt56slNUr_H9CHu1Us5cqhmRSswOh1_3dEGpks5uW-g==", # "CloudFront-Is-Tablet-Viewer": "false", # "X-Forwarded-Port": "443", # "CloudFront-Is-Mobile-Viewer": "false", # "CloudFront-Is-Desktop-Viewer": "true", # "Content-Type": "application/json" # }, # "params": { # "parameter_1": "asdf1", # "parameter_2": "asdf2", # }, # "method": "POST", # "query": { # "dead": "beef" # } # } event = { u'body': None, u'resource': u'/', u'requestContext': { u'resourceId': u'6cqjw9qu0b', u'apiId': u'9itr2lba55', u'resourcePath': u'/', u'httpMethod': u'GET', u'requestId': u'c17cb1bf-867c-11e6-b938-ed697406e3b5', u'accountId': u'724336686645', u'identity': { u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:48.0) Gecko/20100101 Firefox/48.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'50.191.225.98', u'accountId': None, }, u'stage': u'devorr', }, u'queryStringParameters': None, u'httpMethod': u'GET', u'pathParameters': None, u'headers': { u'Via': u'1.1 6801928d54163af944bf854db8d5520e.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'50.191.225.98, 204.246.168.101', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'Upgrade-Insecure-Requests': u'1', u'Host': u'9itr2lba55.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'X-Amz-Cf-Id': u'qgNdqKT0_3RMttu5KjUdnvHI3OKm1BWF8mGD2lX8_rVrJQhhp-MLDw==', u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:48.0) Gecko/20100101 Firefox/48.0', u'CloudFront-Is-Mobile-Viewer': u'false', u'CloudFront-Is-Desktop-Viewer': u'true', }, u'stageVariables': None, u'path': u'/', } request = create_wsgi_request(event)
def test_wsgi_authorizer_handling(self): # With user event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "authorizer": { "principalId": "user1" } }, "query": {}, } environ = create_wsgi_request(event, script_name="http://zappa.com/", trailing_slash=False) self.assertEqual(environ["REMOTE_USER"], "user1") # With empty authorizer, should not include REMOTE_USER event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "authorizer": { "principalId": "" } }, "query": {}, } environ = create_wsgi_request(event, script_name="http://zappa.com/", trailing_slash=False) user = environ.get("REMOTE_USER", "no_user") self.assertEqual(user, "no_user") # With missing authorizer, should not include REMOTE_USER event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": {}, "query": {}, } environ = create_wsgi_request(event, script_name="http://zappa.com/", trailing_slash=False) user = environ.get("REMOTE_USER", "no_user") self.assertEqual(user, "no_user") # With empty authorizer, should not include REMOTE_USER event = { "httpMethod": "GET", "queryStringParameters": None, "path": "/v1/runs", "params": {}, "body": {}, "headers": { "Content-Type": "application/json" }, "pathParameters": { "proxy": "v1/runs" }, "requestContext": { "authorizer": {} }, "query": {}, } environ = create_wsgi_request(event, script_name="http://zappa.com/", trailing_slash=False) user = environ.get("REMOTE_USER", "no_user") self.assertEqual(user, "no_user")
def lambda_handler(event, context, settings_name="zappa_settings"): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to Flask, procceses the Flask response, and returns that back to the API Gateway. """ # Loading settings from a python module settings = importlib.import_module(settings_name) # The flask-app module app_module = importlib.import_module(settings.APP_MODULE) # The flask-app app = getattr(app_module, settings.APP_OBJECT) app.config.from_object('zappa_settings') app.wsgi_app = ZappaWSGIMiddleware(app.wsgi_app) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in list(event['params'].values()): return {'Content': str(event) + '\n' + str(context), 'Status': 200} # TODO: Enable Let's Encrypt # # If Let's Encrypt is defined in the settings, # # and the path is your.domain.com/.well-known/acme-challenge/{{lets_encrypt_challenge_content}}, # # return a 200 of lets_encrypt_challenge_content. # lets_encrypt_challenge_path = getattr(settings, "LETS_ENCRYPT_CHALLENGE_PATH", None) # lets_encrypt_challenge_content = getattr(settings, "LETS_ENCRYPT_CHALLENGE_CONTENT", None) # if lets_encrypt_challenge_path: # if len(event['params']) == 3: # if event['params']['parameter_1'] == '.well-known' and \ # event['params']['parameter_2'] == 'acme-challenge' and \ # event['params']['parameter_3'] == lets_encrypt_challenge_path: # return {'Content': lets_encrypt_challenge_content, 'Status': 200} # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME, trailing_slash=False) # We are always on https on Lambda, so tell our wsgi app that. environ['wsgi.url_scheme'] = 'https' response = Response.from_app(app, environ) # This doesn't work. It should probably be set right after creation, not # at such a late stage. # response.autocorrect_location_header = False zappa_returndict = dict() if response.data: zappa_returndict['Content'] = response.data # Pack the WSGI response into our special dictionary. for (header_name, header_value) in response.headers: zappa_returndict[header_name] = header_value zappa_returndict['Status'] = response.status_code # TODO: No clue how to handle the flask-equivalent of this. Or is this # something entirely specified by the middleware? # # Parse the WSGI Cookie and pack it. # cookie = response.cookies.output() # if ': ' in cookie: # zappa_returndict['Set-Cookie'] = response.cookies.output().split(': ')[1] # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. if response.status_code in [400, 401, 403, 404, 500]: content = "<!DOCTYPE html>" + str(response.status_code) + response.data b64_content = base64.b64encode(content) raise Exception(b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): location = location[len(hostname):] raise Exception(location) else: return zappa_returndict
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: print('Zappa Event: {}'.format(event)) logger.debug('Zappa Event: {}'.format(event)) # Custom log level if settings.LOG_LEVEL: level = logging.getLevelName(settings.LOG_LEVEL) logger.setLevel(level) # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split('-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function(whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None for record in records: whole_function = self.get_function_for_aws_event(record) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result try: # Timing time_start = datetime.datetime.now() # Django gets special treatment. if not settings.DJANGO_SETTINGS: # The app module app_module = importlib.import_module(settings.APP_MODULE) # The application app_function = getattr(app_module, settings.APP_FUNCTION) trailing_slash = False else: try: # Support both for tests from zappa.ext.django import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension app_function = get_django_wsgi(settings.DJANGO_SETTINGS) trailing_slash = True app = ZappaWSGIMiddleware(app_function) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo and 'event_echo' in event['params'].values(): return {'Content': str(event) + '\n' + str(context), 'Status': 200} if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, trailing_slash=trailing_slash ) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context # Execute the application response = Response.from_app(app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict(response.headers) if 'Content' not in zappa_returndict and response.data: zappa_returndict['Content'] = response.data zappa_returndict['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in ERROR_CODES: content = collections.OrderedDict() content['http_status'] = response.status_code content['content'] = base64.b64encode(response.data.encode('utf-8')) exception = json.dumps(content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif 300 <= response.status_code < 400 and hasattr(response, 'Location'): # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): exception = location[len(hostname):] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: # pragma: no cover raise WSGIException(exception) else: return zappa_returndict except WSGIException as e: # pragma: no cover raise e except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = 'An uncaught exception happened while servicing this request.' # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['http_status'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception(*exc_info) # traceback as a list for readability. content['content'] = base64.b64encode(json.dumps(body, sort_keys=True, indent=4).encode('utf-8')) exception = json.dumps(content) raise UncaughtWSGIException(exception, original=e), None, exc_info[2] # Keep original traceback.
def test_wsgi_middleware_realcall(self): print("1: Setting the cookies.") event = { u'method': u'POST', u'params': { u'parameter_1': u'set_cookie' }, u'body': u'foo=xxx&bar=yyy', u'headers': {}, u'query': {} } def set_cookies(environ, start_response): status = '200 OK' print environ response_headers = [('Set-Cookie', 'foo=123'), ('Set-Cookie', 'bar=456'), ('Set-Cookie', 'baz=789')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(set_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie0 = zappa_cookie[0] self.assertTrue(zappa_cookie0.startswith('zappa=')) print("2: Changing 1 cookie") event = { u'method': u'POST', u'params': { u'parameter_1': u'set_cookie' }, u'body': u'foo=qwe', u'headers': { u'Cookie': zappa_cookie0 }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) def change_cookie(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [('Set-Cookie', 'foo=new_value')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(change_cookie) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict zdict2 = json.loads(base58.b58decode(zdict['zappa'])) print 'zdict2', zdict2 self.assertEqual(len(zdict2), 3) self.assertEqual(zdict2['foo'], 'new_value') self.assertEqual(zdict2['bar'], '456') self.assertEqual(zdict2['baz'], '789') # We have changed foo, so they should be different self.assertNotEqual(zappa_cookie0, zappa_cookie1) print("3: Reading the cookies") event['headers']['Cookie'] = zappa_cookie1 def read_cookies(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [] start_response(status, response_headers) return [environ['HTTP_COOKIE']] app = ZappaWSGIMiddleware(read_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) print "response", response # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict cookies = json.loads(base58.b58decode(zdict['zappa'])) self.assertEqual(cookies['foo'], 'new_value') self.assertEqual(cookies['bar'], '456') self.assertEqual(cookies['baz'], '789')
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # Set any API Gateway defined Stage Variables # as env vars if event.get('stageVariables'): for key in event['stageVariables'].keys(): os.environ[str(key)] = event['stageVariables'][key] # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split('-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function(whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overridden by unsanitized, non-admin user input. elif event.get('raw_command', None): raw_command = event['raw_command'] exec(raw_command) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # this is an AWS-event triggered from Lex bot's intent elif event.get('bot'): result = None whole_function = self.get_function_from_bot_intent_trigger(event) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get('type') == u'TOKEN': whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function(whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error("Cannot find a function to process the authorization request.") raise Exception('Unauthorized') # This is an AWS Cognito Trigger Event elif event.get('triggerSource', None): triggerSource = event.get('triggerSource') whole_function = self.get_function_for_cognito_trigger(triggerSource) result = event if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to handle cognito trigger {}".format(triggerSource)) return result # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get('httpMethod', None): script_name = '' headers = event.get('headers') if headers: host = headers.get('Host') else: host = None if host: if 'amazonaws.com' in host: # The path provided in th event doesn't include the # stage, so we must tell Flask to include the API # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 script_name = '/' + settings.API_STAGE else: # This is a test request sent from the AWS console if settings.DOMAIN: # Assume the requests received will be on the specified # domain. No special handling is required pass else: # Assume the requests received will be to the # amazonaws.com endpoint, so tell Flask to include the # API stage script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT, context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS ) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context environ['lambda.event'] = event # Execute the application response = Response.from_app(self.wsgi_app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() if response.data: if settings.BINARY_SUPPORT: if not response.mimetype.startswith("text/") \ or response.mimetype != "application/json": zappa_returndict['body'] = base64.b64encode(response.data).decode('utf-8') zappa_returndict["isBase64Encoded"] = True else: zappa_returndict['body'] = response.data else: zappa_returndict['body'] = response.get_data(as_text=True) zappa_returndict['statusCode'] = response.status_code zappa_returndict['headers'] = {} for key, value in response.headers: zappa_returndict['headers'][key] = value # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ('An uncaught exception happened while servicing this request. ' 'You can investigate this with the `zappa tail` command.') # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Call exception handler for unhandled exceptions exception_handler = self.settings.EXCEPTION_HANDLER self._process_exception(exception_handler=exception_handler, event=event, context=context, exception=e) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['statusCode'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception(*exc_info) # traceback as a list for readability. content['body'] = json.dumps(str(body), sort_keys=True, indent=4) return content
def test_wsgi_middleware_realcall(self): print("1: Setting the cookies.") event = {u'body': None, u'resource': u'/{proxy+}', u'requestContext': {u'resourceId': u'dg451y', u'apiId': u'79gqbxq31c', u'resourcePath': u'/{proxy+}', u'httpMethod': u'GET', u'requestId': u'766df67f-8991-11e6-b2c4-d120fedb94e5', u'accountId': u'724336686645', u'identity': {u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'96.90.37.59', u'accountId': None}, u'stage': u'devorr'}, u'queryStringParameters': None, u'httpMethod': u'GET', u'pathParameters': {u'proxy': u'asdf1/asdf2'}, u'headers': {u'Via': u'1.1 b2aeb492548a8a2d4036401355f928dd.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'X-Forwarded-Port': u'443', u'X-Forwarded-For': u'96.90.37.59, 54.240.144.50', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'Upgrade-Insecure-Requests': u'1', u'Host': u'79gqbxq31c.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'X-Amz-Cf-Id': u'BBFP-RhGDrQGOzoCqjnfB2I_YzWt_dac9S5vBcSAEaoM4NfYhAQy7Q==', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'CloudFront-Forwarded-Proto': u'https'}, u'stageVariables': None, u'path': u'/asdf1/asdf2'} def set_cookies(environ, start_response): status = '200 OK' print environ response_headers = [('Set-Cookie', 'foo=123'), ('Set-Cookie', 'bar=456'), ('Set-Cookie', 'baz=789')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(set_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie0 = zappa_cookie[0] self.assertTrue(zappa_cookie0.startswith('zappa=')) print("2: Changing 1 cookie") # event = { # u'httpMethod': u'POST', # u'params': {u'parameter_1': u'set_cookie'}, # u'body': u'foo=qwe', # u'headers': { # u'Cookie': zappa_cookie0 # }, # u'query': {} # } event = { u'body': u'LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03Njk1MjI4NDg0Njc4MTc2NTgwNjMwOTYxDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9Im15c3RyaW5nIg0KDQpkZGQNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tNzY5NTIyODQ4NDY3ODE3NjU4MDYzMDk2MS0tDQo=', u'resource': u'/', u'requestContext': { u'resourceId': u'6cqjw9qu0b', u'apiId': u'9itr2lba55', u'resourcePath': u'/', u'httpMethod': u'POST', u'requestId': u'c17cb1bf-867c-11e6-b938-ed697406e3b5', u'accountId': u'724336686645', u'identity': { u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:48.0) Gecko/20100101 Firefox/48.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'50.191.225.98', u'accountId': None, }, u'stage': u'devorr', }, u'queryStringParameters': None, u'httpMethod': u'POST', u'pathParameters': None, u'headers': {u'Content-Type': u'multipart/form-data; boundary=---------------------------7695228484678176580630961', u'Via': u'1.1 38205a04d96d60185e88658d3185ccee.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'71.231.27.57, 104.246.180.51', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0', u'Host': u'xo2z7zafjh.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'Cookie': zappa_cookie0, u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'Referer': u'https://xo8z7zafjh.execute-api.us-east-1.amazonaws.com/former/post', u'CloudFront-Is-Mobile-Viewer': u'false', u'X-Amz-Cf-Id': u'31zxcUcVyUxBOMk320yh5NOhihn5knqrlYQYpGGyOngKKwJb0J0BAQ==', u'CloudFront-Is-Desktop-Viewer': u'true'}, u'stageVariables': None, u'path': u'/', } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) def change_cookie(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [('Set-Cookie', 'foo=new_value')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(change_cookie) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict zdict2 = json.loads(base58.b58decode(zdict['zappa'])) print 'zdict2', zdict2 self.assertEqual(len(zdict2), 3) self.assertEqual(zdict2['foo'], 'new_value') self.assertEqual(zdict2['bar'], '456') self.assertEqual(zdict2['baz'], '789') # We have changed foo, so they should be different self.assertNotEqual(zappa_cookie0, zappa_cookie1) print("3: Reading the cookies") event['headers']['Cookie'] = zappa_cookie1 def read_cookies(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [] start_response(status, response_headers) return [environ['HTTP_COOKIE']] app = ZappaWSGIMiddleware(read_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) print "response", response # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict cookies = json.loads(base58.b58decode(zdict['zappa'])) self.assertEqual(cookies['foo'], 'new_value') self.assertEqual(cookies['bar'], '456') self.assertEqual(cookies['baz'], '789')
def test_wsgi_middleware_realcall(self): print("1: Setting the cookies.") event = { u'body': None, u'resource': u'/{proxy+}', u'requestContext': { u'resourceId': u'dg451y', u'apiId': u'79gqbxq31c', u'resourcePath': u'/{proxy+}', u'httpMethod': u'GET', u'requestId': u'766df67f-8991-11e6-b2c4-d120fedb94e5', u'accountId': u'724336686645', u'identity': { u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'96.90.37.59', u'accountId': None }, u'stage': u'devorr' }, u'queryStringParameters': None, u'httpMethod': u'GET', u'pathParameters': { u'proxy': u'asdf1/asdf2' }, u'headers': { u'Via': u'1.1 b2aeb492548a8a2d4036401355f928dd.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'X-Forwarded-Port': u'443', u'X-Forwarded-For': u'96.90.37.59, 54.240.144.50', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'Upgrade-Insecure-Requests': u'1', u'Host': u'79gqbxq31c.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'X-Amz-Cf-Id': u'BBFP-RhGDrQGOzoCqjnfB2I_YzWt_dac9S5vBcSAEaoM4NfYhAQy7Q==', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:49.0) Gecko/20100101 Firefox/49.0', u'CloudFront-Forwarded-Proto': u'https' }, u'stageVariables': None, u'path': u'/asdf1/asdf2' } def set_cookies(environ, start_response): status = '200 OK' print environ response_headers = [('Set-Cookie', 'foo=123'), ('Set-Cookie', 'bar=456'), ('Set-Cookie', 'baz=789')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(set_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie0 = zappa_cookie[0] self.assertTrue(zappa_cookie0.startswith('zappa=')) print("2: Changing 1 cookie") # event = { # u'httpMethod': u'POST', # u'params': {u'parameter_1': u'set_cookie'}, # u'body': u'foo=qwe', # u'headers': { # u'Cookie': zappa_cookie0 # }, # u'query': {} # } event = { u'body': u'LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03Njk1MjI4NDg0Njc4MTc2NTgwNjMwOTYxDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9Im15c3RyaW5nIg0KDQpkZGQNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tNzY5NTIyODQ4NDY3ODE3NjU4MDYzMDk2MS0tDQo=', u'resource': u'/', u'requestContext': { u'resourceId': u'6cqjw9qu0b', u'apiId': u'9itr2lba55', u'resourcePath': u'/', u'httpMethod': u'POST', u'requestId': u'c17cb1bf-867c-11e6-b938-ed697406e3b5', u'accountId': u'724336686645', u'identity': { u'apiKey': None, u'userArn': None, u'cognitoAuthenticationType': None, u'caller': None, u'userAgent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:48.0) Gecko/20100101 Firefox/48.0', u'user': None, u'cognitoIdentityPoolId': None, u'cognitoIdentityId': None, u'cognitoAuthenticationProvider': None, u'sourceIp': u'50.191.225.98', u'accountId': None, }, u'stage': u'devorr', }, u'queryStringParameters': None, u'httpMethod': u'POST', u'pathParameters': None, u'headers': { u'Content-Type': u'multipart/form-data; boundary=---------------------------7695228484678176580630961', u'Via': u'1.1 38205a04d96d60185e88658d3185ccee.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'71.231.27.57, 104.246.180.51', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0', u'Host': u'xo2z7zafjh.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'Cookie': zappa_cookie0, u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'Referer': u'https://xo8z7zafjh.execute-api.us-east-1.amazonaws.com/former/post', u'CloudFront-Is-Mobile-Viewer': u'false', u'X-Amz-Cf-Id': u'31zxcUcVyUxBOMk320yh5NOhihn5knqrlYQYpGGyOngKKwJb0J0BAQ==', u'CloudFront-Is-Desktop-Viewer': u'true' }, u'stageVariables': None, u'path': u'/', } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) def change_cookie(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [('Set-Cookie', 'foo=new_value')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(change_cookie) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict zdict2 = json.loads(base58.b58decode(zdict['zappa'])) print 'zdict2', zdict2 self.assertEqual(len(zdict2), 3) self.assertEqual(zdict2['foo'], 'new_value') self.assertEqual(zdict2['bar'], '456') self.assertEqual(zdict2['baz'], '789') # We have changed foo, so they should be different self.assertNotEqual(zappa_cookie0, zappa_cookie1) print("3: Reading the cookies") event['headers']['Cookie'] = zappa_cookie1 def read_cookies(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [] start_response(status, response_headers) return [environ['HTTP_COOKIE']] app = ZappaWSGIMiddleware(read_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) print "response", response # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict cookies = json.loads(base58.b58decode(zdict['zappa'])) self.assertEqual(cookies['foo'], 'new_value') self.assertEqual(cookies['bar'], '456') self.assertEqual(cookies['baz'], '789')
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ time_start = datetime.datetime.now() settings = self.settings # The app module app_module = importlib.import_module(settings.APP_MODULE) # The application app_function = getattr(app_module, settings.APP_FUNCTION) app = ZappaWSGIMiddleware(app_function) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in list(event['params'].values()): return {'Content': str(event) + '\n' + str(context), 'Status': 200} # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name='', trailing_slash=False) # We are always on https on Lambda, so tell our wsgi app that. environ['wsgi.url_scheme'] = 'https' response = Response.from_app(app, environ) zappa_returndict = dict() if response.data: zappa_returndict['Content'] = response.data # Pack the WSGI response into our special dictionary. for (header_name, header_value) in response.headers: zappa_returndict[header_name] = header_value zappa_returndict['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in [400, 401, 403, 404, 500]: content = "<!DOCTYPE html>" + str(response.status_code) + response.data exception = base64.b64encode(content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): exception = location[len(hostname):] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: # pragma: no cover raise Exception(exception) else: return zappa_returndict
def test_wsgi_authorizer_handling(self): # With user event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u'authorizer': { u'principalId': u'user1' } }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) self.assertEqual(environ['REMOTE_USER'], u'user1') # With empty authorizer, should not include REMOTE_USER event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u'authorizer': { u'principalId': u'' } }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) user = environ.get('REMOTE_USER', u'no_user') self.assertEqual(user, u'no_user') # With missing authorizer, should not include REMOTE_USER event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': {}, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) user = environ.get('REMOTE_USER', u'no_user') self.assertEqual(user, u'no_user') # With empty authorizer, should not include REMOTE_USER event = { u'httpMethod': u'GET', u'queryStringParameters': None, u'path': u'/v1/runs', u'params': {}, u'body': {}, u'headers': { u'Content-Type': u'application/json' }, u'pathParameters': { u'proxy': 'v1/runs' }, u'requestContext': { u'authorizer': {} }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) user = environ.get('REMOTE_USER', u'no_user') self.assertEqual(user, u'no_user')
def lambda_handler(event, context=None, settings_name="zappa_settings"): # NoQA """ An AWS Lambda function which parses specific API Gateway input into a WSGI request. The request get fed it to Django, processes the Django response, and returns that back to the API Gateway. """ time_start = datetime.datetime.now() # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.info('Zappa Event: {}'.format(event)) # This is a normal HTTP request if event.get('method', None): # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' wrap_me = get_wsgi_application() app = ZappaWSGIMiddleware(wrap_me) # Execute the application response = Response.from_app(app, environ) response.content = response.data # Prepare the special dictionary which will be returned to the API GW. returnme = {'Content': response.data} # Pack the WSGI response into our special dictionary. for (header_name, header_value) in response.headers: returnme[header_name] = header_value returnme['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in ERROR_CODES: content = u"<!DOCTYPE html>" + unicode( response.status_code) + unicode( '<meta charset="utf-8" />') + response.data.encode('utf-8') b64_content = base64.b64encode(content) exception = (b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif 300 <= response.status_code < 400 and response.has_header( 'Location'): location = returnme['Location'] location = '/' + location.replace("http://zappa/", "") exception = location # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: raise Exception(exception) else: return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {} elif event.get('detail'): module, function = event['detail'].rsplit('.', 1) app_module = importlib.import_module(module) app_function = getattr(app_module, function) # Execute the function! app_function() return else: logger.error('Unhandled event: {}'.format(json.dumps(event)))
def test_wsgi_middleware_realcall(self): print("1: Setting the cookies.") event = { u'method': u'POST', u'params': {u'parameter_1': u'set_cookie'}, u'body': u'foo=xxx&bar=yyy', u'headers': {}, u'query': {}} def set_cookies(environ, start_response): status = '200 OK' print environ response_headers = [('Set-Cookie', 'foo=123'), ('Set-Cookie', 'bar=456'), ('Set-Cookie', 'baz=789')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(set_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie0 = zappa_cookie[0] self.assertTrue(zappa_cookie0.startswith('zappa=')) print("2: Changing 1 cookie") event = { u'method': u'POST', u'params': {u'parameter_1': u'set_cookie'}, u'body': u'foo=qwe', u'headers': { u'Cookie': zappa_cookie0 }, u'query': {} } environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) def change_cookie(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [('Set-Cookie', 'foo=new_value')] start_response(status, response_headers) return ['Set cookies!'] app = ZappaWSGIMiddleware(change_cookie) response = Response.from_app(app, environ) # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict zdict2 = json.loads(base58.b58decode(zdict['zappa'])) print 'zdict2', zdict2 self.assertEqual(len(zdict2), 3) self.assertEqual(zdict2['foo'], 'new_value') self.assertEqual(zdict2['bar'], '456') self.assertEqual(zdict2['baz'], '789') # We have changed foo, so they should be different self.assertNotEqual(zappa_cookie0, zappa_cookie1) print("3: Reading the cookies") event['headers']['Cookie'] = zappa_cookie1 def read_cookies(environ, start_response): status = '200 OK' print 'environ', environ response_headers = [] start_response(status, response_headers) return [environ['HTTP_COOKIE']] app = ZappaWSGIMiddleware(read_cookies) environ = create_wsgi_request(event, script_name='http://zappa.com/', trailing_slash=False) response = Response.from_app(app, environ) print "response", response # Filter the headers for Set-Cookie header zappa_cookie = [x[1] for x in response.headers if x[0] == 'Set-Cookie'] self.assertEqual(len(zappa_cookie), 1) zappa_cookie1 = zappa_cookie[0] self.assertTrue(zappa_cookie1.startswith('zappa=')) zdict = parse_cookie(zappa_cookie1) print 'zdict', zdict cookies = json.loads(base58.b58decode(zdict['zappa'])) self.assertEqual(cookies['foo'], 'new_value') self.assertEqual(cookies['bar'], '456') self.assertEqual(cookies['baz'], '789')
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ if os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda_'): xray_recorder.begin_subsegment("pre_handle") settings = self.settings if 'multiValueHeaders' in event: event['headers'] = dict([(key, value[-1]) for (key, value) in (event.get('multiValueHeaders') or {}).items()]) if 'multiValueQueryStringParameters' in event: event['queryStringParameters'] = dict([(key, value[-1]) for (key, value) in (event.get('multiValueQueryStringParameters') or {}).items()]) # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # Set any API Gateway defined Stage Variables # as env vars if event.get('stageVariables'): for key in event['stageVariables'].keys(): os.environ[str(key)] = event['stageVariables'][key] # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split('-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function(whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overridden by unsanitized, non-admin user input. elif event.get('raw_command', None): raw_command = event['raw_command'] exec(raw_command) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # this is an AWS-event triggered from Lex bot's intent elif event.get('bot'): result = None whole_function = self.get_function_from_bot_intent_trigger(event) if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get('type') == u'TOKEN': whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function(whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error("Cannot find a function to process the authorization request.") raise Exception('Unauthorized') # This is an AWS Cognito Trigger Event elif event.get('triggerSource', None): triggerSource = event.get('triggerSource') whole_function = self.get_function_for_cognito_trigger(triggerSource) result = event if whole_function: app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error("Cannot find a function to handle cognito trigger {}".format(triggerSource)) return result # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get('httpMethod', None): script_name = '' is_elb_context = False headers = event.get('headers') if event.get('requestContext', None) and event['requestContext'].get('elb', None): # Related: https://github.com/Miserlou/Zappa/issues/1715 # inputs/outputs for lambda loadbalancer # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html is_elb_context = True # host is lower-case when forwarded from ELB host = headers.get('host') # TODO: pathParameters is a first-class citizen in apigateway but not available without # some parsing work for ELB (is this parameter used for anything?) event['pathParameters'] = '' else: if headers: host = headers.get('Host') else: host = None logger.debug('host found: [{}]'.format(host)) if host: if 'amazonaws.com' in host: logger.debug('amazonaws found in host') # The path provided in th event doesn't include the # stage, so we must tell Flask to include the API # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 script_name = '/' + settings.API_STAGE else: # This is a test request sent from the AWS console if settings.DOMAIN: # Assume the requests received will be on the specified # domain. No special handling is required pass else: # Assume the requests received will be to the # amazonaws.com endpoint, so tell Flask to include the # API stage script_name = '/' + settings.API_STAGE base_path = getattr(settings, 'BASE_PATH', None) # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, base_path=base_path, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT, context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS, should_transform_to_iso_8859_1=not settings.DJANGO_SETTINGS ) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context environ['lambda.event'] = event if os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda_'): xray_recorder.end_subsegment() # Execute the application response = Response.from_app(self.wsgi_app, environ) if os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda_'): xray_recorder.begin_subsegment("post_handle") # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() # Issue #1715: ALB support. ALB responses must always include # base64 encoding and status description if is_elb_context: zappa_returndict.setdefault('isBase64Encoded', False) zappa_returndict.setdefault('statusDescription', response.status) if response.data: if settings.BINARY_SUPPORT: if not response.mimetype.startswith("text/") \ or response.mimetype != "application/json": zappa_returndict['body'] = base64.b64encode(response.data).decode('utf-8') zappa_returndict["isBase64Encoded"] = True else: zappa_returndict['body'] = response.data else: zappa_returndict['body'] = response.get_data(as_text=True) zappa_returndict['statusCode'] = response.status_code zappa_returndict['headers'] = {} for key, value in response.headers: zappa_returndict['headers'][key] = value zappa_returndict['multiValueHeaders'] = {} for key, value in response.headers: zappa_returndict['multiValueHeaders'][key] = [value] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) if os.environ.get('AWS_EXECUTION_ENV', '').startswith('AWS_Lambda_'): xray_recorder.end_subsegment() return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ('An uncaught exception happened while servicing this request. ' 'You can investigate this with the `zappa tail` command.') # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Call exception handler for unhandled exceptions exception_handler = self.settings.EXCEPTION_HANDLER self._process_exception(exception_handler=exception_handler, event=event, context=context, exception=e) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['statusCode'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception(*exc_info) # traceback as a list for readability. content['body'] = json.dumps(str(body), sort_keys=True, indent=4) return content
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ # TODO If this is invoked from a non-APIGW/Scheduled event source, # extract the method and process separately. if event.get('detail-type', None) == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1] module, function = whole_function.rsplit('.', 1) app_module = importlib.import_module(module) app_function = getattr(app_module, function) # Execute the function! app_function() return try: # Timing time_start = datetime.datetime.now() settings = self.settings # Custom log level if settings.LOG_LEVEL: level = logging.getLevelName(settings.LOG_LEVEL) logger.setLevel(level) # The app module app_module = importlib.import_module(settings.APP_MODULE) # The application app_function = getattr(app_module, settings.APP_FUNCTION) app = ZappaWSGIMiddleware(app_function) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo and 'event_echo' in event['params'].values(): return {'Content': str(event) + '\n' + str(context), 'Status': 200} if settings.DOMAIN: # If we're on a domain, we operate normally script_name = '' else: # But if we're not, then our base URL # will be something like # https://blahblahblah.execute-api.us-east-1.amazonaws.com/dev # So, we need to make sure the WSGI app knows this. script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=script_name, trailing_slash=False ) # We are always on https on Lambda, so tell our wsgi app that. environ['wsgi.url_scheme'] = 'https' # Execute the application response = Response.from_app(app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict(response.headers) if 'Content' not in zappa_returndict and response.data: zappa_returndict['Content'] = response.data zappa_returndict['Status'] = response.status_code # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in [400, 401, 403, 404, 500]: content = "<!DOCTYPE html>" + str(response.status_code) + response.data exception = base64.b64encode(content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): exception = location[len(hostname):] # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: # pragma: no cover raise LambdaException(exception) else: return zappa_returndict except LambdaException as e: # pragma: nocover raise e except Exception as e: # pragma: nocover # Print statements are visible in the logs either way print(e) # Print the error to the browser upon failure? debug = bool(getattr(app_module, settings.DEBUG, True)) if debug: # Return this unspecified exception as a 500. content = "<!DOCTYPE html>500. From Zappa: <pre>" + str(e) + "</pre><br /><pre>" + traceback.format_exc().replace('\n', '<br />') + "</pre>" exception = base64.b64encode(content) raise Exception(exception) else: raise e
def lambda_handler(event, context, settings_name="zappa_settings"): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to Flask, procceses the Flask response, and returns that back to the API Gateway. """ # Loading settings from a python module settings = importlib.import_module(settings_name) # The flask-app module app_module = importlib.import_module(settings.APP_MODULE) # The flask-app app = getattr(app_module, settings.APP_OBJECT) app.config.from_object('zappa_settings') app.wsgi_app = ZappaWSGIMiddleware(app.wsgi_app) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in list(event['params'].values()): return { 'Content': str(event) + '\n' + str(context), 'Status': 200 } # TODO: Enable Let's Encrypt # # If Let's Encrypt is defined in the settings, # # and the path is your.domain.com/.well-known/acme-challenge/{{lets_encrypt_challenge_content}}, # # return a 200 of lets_encrypt_challenge_content. # lets_encrypt_challenge_path = getattr(settings, "LETS_ENCRYPT_CHALLENGE_PATH", None) # lets_encrypt_challenge_content = getattr(settings, "LETS_ENCRYPT_CHALLENGE_CONTENT", None) # if lets_encrypt_challenge_path: # if len(event['params']) == 3: # if event['params']['parameter_1'] == '.well-known' and \ # event['params']['parameter_2'] == 'acme-challenge' and \ # event['params']['parameter_3'] == lets_encrypt_challenge_path: # return {'Content': lets_encrypt_challenge_content, 'Status': 200} # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME, trailing_slash=False) # We are always on https on Lambda, so tell our wsgi app that. environ['wsgi.url_scheme'] = 'https' response = Response.from_app(app, environ) # This doesn't work. It should probably be set right after creation, not # at such a late stage. # response.autocorrect_location_header = False zappa_returndict = dict() if response.data: zappa_returndict['Content'] = response.data # Pack the WSGI response into our special dictionary. for (header_name, header_value) in response.headers: zappa_returndict[header_name] = header_value zappa_returndict['Status'] = response.status_code # TODO: No clue how to handle the flask-equivalent of this. Or is this # something entirely specified by the middleware? # # Parse the WSGI Cookie and pack it. # cookie = response.cookies.output() # if ': ' in cookie: # zappa_returndict['Set-Cookie'] = response.cookies.output().split(': ')[1] # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. if response.status_code in [400, 401, 403, 404, 500]: content = "<!DOCTYPE html>" + str( response.status_code) + response.data b64_content = base64.b64encode(content) raise Exception(b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: # Location is by default relative on Flask. Location is by default # absolute on Werkzeug. We can set autocorrect_location_header on # the response to False, but it doesn't work. We have to manually # remove the host part. location = response.location hostname = 'https://' + environ['HTTP_HOST'] if location.startswith(hostname): location = location[len(hostname):] raise Exception(location) else: return zappa_returndict
def test_wsgi_multipart(self): event = {u'body': u'LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS03Njk1MjI4NDg0Njc4MTc2NTgwNjMwOTYxDQpDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9Im15c3RyaW5nIg0KDQpkZGQNCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tNzY5NTIyODQ4NDY3ODE3NjU4MDYzMDk2MS0tDQo=', u'headers': {u'Content-Type': u'multipart/form-data; boundary=---------------------------7695228484678176580630961', u'Via': u'1.1 38205a04d96d60185e88658d3185ccee.cloudfront.net (CloudFront)', u'Accept-Language': u'en-US,en;q=0.5', u'Accept-Encoding': u'gzip, deflate, br', u'CloudFront-Is-SmartTV-Viewer': u'false', u'CloudFront-Forwarded-Proto': u'https', u'X-Forwarded-For': u'71.231.27.57, 104.246.180.51', u'CloudFront-Viewer-Country': u'US', u'Accept': u'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', u'User-Agent': u'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Firefox/45.0', u'Host': u'xo2z7zafjh.execute-api.us-east-1.amazonaws.com', u'X-Forwarded-Proto': u'https', u'Cookie': u'zappa=AQ4', u'CloudFront-Is-Tablet-Viewer': u'false', u'X-Forwarded-Port': u'443', u'Referer': u'https://xo8z7zafjh.execute-api.us-east-1.amazonaws.com/former/post', u'CloudFront-Is-Mobile-Viewer': u'false', u'X-Amz-Cf-Id': u'31zxcUcVyUxBOMk320yh5NOhihn5knqrlYQYpGGyOngKKwJb0J0BAQ==', u'CloudFront-Is-Desktop-Viewer': u'true'}, u'params': {u'parameter_1': u'post'}, u'method': u'POST', u'query': {}} environ = create_wsgi_request(event, trailing_slash=False) response_tuple = collections.namedtuple('Response', ['status_code', 'content']) response = response_tuple(200, 'hello')
def handler(self, event, context): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to our WSGI app, procceses the response, and returns that back to the API Gateway. """ settings = self.settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.debug('Zappa Event: {}'.format(event)) # This is the result of a keep alive, recertify # or scheduled event. if event.get('detail-type') == u'Scheduled Event': whole_function = event['resources'][0].split('/')[-1].split( '-')[-1] # This is a scheduled function. if '.' in whole_function: app_function = self.import_module_and_get_function( whole_function) # Execute the function! return self.run_function(app_function, event, context) # Else, let this execute as it were. # This is a direct command invocation. elif event.get('command', None): whole_function = event['command'] app_function = self.import_module_and_get_function(whole_function) result = self.run_function(app_function, event, context) print("Result of %s:" % whole_function) print(result) return result # This is a direct, raw python invocation. # It's _extremely_ important we don't allow this event source # to be overriden by unsanitized, non-admin user input. elif event.get('raw_command', None): raw_command = event['raw_command'] exec(raw_command) return # This is a Django management command invocation. elif event.get('manage', None): from django.core import management try: # Support both for tests from zappa.ext.django_zappa import get_django_wsgi except ImportError as e: # pragma: no cover from django_zappa_app import get_django_wsgi # Get the Django WSGI app from our extension # We don't actually need the function, # but we do need to do all of the required setup for it. app_function = get_django_wsgi(self.settings.DJANGO_SETTINGS) # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['manage'].split(' ')) return {} # This is an AWS-event triggered invokation. elif event.get('Records', None): records = event.get('Records') result = None whole_function = self.get_function_for_aws_event(records[0]) if whole_function: app_function = self.import_module_and_get_function( whole_function) result = self.run_function(app_function, event, context) logger.debug(result) else: logger.error( "Cannot find a function to process the triggered event.") return result # This is an API Gateway authorizer event elif event.get('type') == u'TOKEN': whole_function = self.settings.AUTHORIZER_FUNCTION if whole_function: app_function = self.import_module_and_get_function( whole_function) policy = self.run_function(app_function, event, context) return policy else: logger.error( "Cannot find a function to process the authorization request." ) raise Exception('Unauthorized') # Normal web app flow try: # Timing time_start = datetime.datetime.now() # This is a normal HTTP request if event.get('httpMethod', None): script_name = '' headers = event.get('headers') if headers: host = headers.get('Host') else: host = None if host: if 'amazonaws.com' in host: # The path provided in th event doesn't include the # stage, so we must tell Flask to include the API # stage in the url it calculates. See https://github.com/Miserlou/Zappa/issues/1014 script_name = '/' + settings.API_STAGE else: # This is a test request sent from the AWS console if settings.DOMAIN: # Assume the requests received will be on the specified # domain. No special handling is required pass else: # Assume the requests received will be to the # amazonaws.com endpoint, so tell Flask to include the # API stage script_name = '/' + settings.API_STAGE # Create the environment for WSGI and handle the request environ = create_wsgi_request( event, script_name=script_name, trailing_slash=self.trailing_slash, binary_support=settings.BINARY_SUPPORT, context_header_mappings=settings.CONTEXT_HEADER_MAPPINGS) # We are always on https on Lambda, so tell our wsgi app that. environ['HTTPS'] = 'on' environ['wsgi.url_scheme'] = 'https' environ['lambda.context'] = context # Execute the application response = Response.from_app(self.wsgi_app, environ) # This is the object we're going to return. # Pack the WSGI response into our special dictionary. zappa_returndict = dict() if response.data: if settings.BINARY_SUPPORT: if not response.mimetype.startswith("text/") \ or response.mimetype != "application/json": zappa_returndict['body'] = base64.b64encode( response.data).decode('utf-8') zappa_returndict["isBase64Encoded"] = "true" else: zappa_returndict['body'] = response.data else: zappa_returndict['body'] = response.get_data( as_text=True) zappa_returndict['statusCode'] = response.status_code zappa_returndict['headers'] = {} for key, value in response.headers: zappa_returndict['headers'][key] = value # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 response.content = response.data common_log(environ, response, response_time=response_time_ms) return zappa_returndict except Exception as e: # pragma: no cover # Print statements are visible in the logs either way print(e) exc_info = sys.exc_info() message = ( 'An uncaught exception happened while servicing this request. ' 'You can investigate this with the `zappa tail` command.') # If we didn't even build an app_module, just raise. if not settings.DJANGO_SETTINGS: try: self.app_module except NameError as ne: message = 'Failed to import module: {}'.format(ne.message) # Return this unspecified exception as a 500, using template that API Gateway expects. content = collections.OrderedDict() content['statusCode'] = 500 body = {'message': message} if settings.DEBUG: # only include traceback if debug is on. body['traceback'] = traceback.format_exception( *exc_info) # traceback as a list for readability. content['body'] = json.dumps(str(body), sort_keys=True, indent=4) return content
def lambda_handler(event, context, settings_name="zappa_settings"): """ An AWS Lambda function which parses specific API Gateway input into a WSGI request, feeds it to Django, procceses the Django response, and returns that back to the API Gateway. """ time_start = datetime.datetime.now() logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.INFO) # Django requires settings and an explicit call to setup() # if being used from inside a python context. os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_name) import django django.setup() from django.conf import settings # If in DEBUG mode, log all raw incoming events. if settings.DEBUG: logger.info('Zappa Event: {}'.format(event)) # This is a normal HTTP request if event.get('method', None): # If we just want to inspect this, # return this event instead of processing the request # https://your_api.aws-api.com/?event_echo=true event_echo = getattr(settings, "EVENT_ECHO", True) if event_echo: if 'event_echo' in event['params'].values(): return {'Content': str(event) + '\n' + str(context), 'Status': 200} # If Let's Encrypt is defined in the settings, # and the path is your.domain.com/.well-known/acme-challenge/{{lets_encrypt_challenge_content}}, # return a 200 of lets_encrypt_challenge_content. lets_encrypt_challenge_path = getattr(settings, "LETS_ENCRYPT_CHALLENGE_PATH", None) lets_encrypt_challenge_content = getattr(settings, "LETS_ENCRYPT_CHALLENGE_CONTENT", None) if lets_encrypt_challenge_path: if len(event['params']) == 3: if event['params']['parameter_1'] == '.well-known' and \ event['params']['parameter_2'] == 'acme-challenge' and \ event['params']['parameter_3'] == lets_encrypt_challenge_path: return {'Content': lets_encrypt_challenge_content, 'Status': 200} # This doesn't matter, but Django's handler requires it. def start(a, b): return # Create the environment for WSGI and handle the request environ = create_wsgi_request(event, script_name=settings.SCRIPT_NAME) handler = WSGIHandler() response = handler(environ, start) # Prepare the special dictionary which will be returned to the API GW. returnme = {'Content': response.content} # Pack the WSGI response into our special dictionary. for item in response.items(): returnme[item[0]] = item[1] returnme['Status'] = response.status_code # Parse the WSGI Cookie and pack it. cookie = response.cookies.output() if ': ' in cookie: returnme['Set-Cookie'] = response.cookies.output().split(': ')[1] # To ensure correct status codes, we need to # pack the response as a deterministic B64 string and raise it # as an error to match our APIGW regex. # The DOCTYPE ensures that the page still renders in the browser. exception = None if response.status_code in [400, 401, 403, 500]: content = response.content content = "<!DOCTYPE html>" + str(response.status_code) + response.content b64_content = base64.b64encode(content) exception = (b64_content) # Internal are changed to become relative redirects # so they still work for apps on raw APIGW and on a domain. elif response.status_code in [301, 302]: location = returnme['Location'] location = '/' + location.replace("http://zappa/", "") exception = location # Calculate the total response time, # and log it in the Common Log format. time_end = datetime.datetime.now() delta = time_end - time_start response_time_ms = delta.total_seconds() * 1000 common_log(environ, response, response_time=response_time_ms) # Finally, return the response to API Gateway. if exception: raise Exception(exception) else: return returnme # This is a management command invocation. elif event.get('command', None): from django.core import management # Couldn't figure out how to get the value into stdout with StringIO.. # Read the log for now. :[] management.call_command(*event['command'].split(' ')) return {}