Skip to content

Commit

Permalink
feat: implement handler for event with format version 2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rehanhwr committed Oct 22, 2022
1 parent b1bde2a commit 69f3b71
Show file tree
Hide file tree
Showing 4 changed files with 366 additions and 31 deletions.
32 changes: 32 additions & 0 deletions tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,3 +577,35 @@ def test_cloudwatch_subscription_event(self):
response = lh.handler(event, None)

self.assertEqual(response, True)

def test_wsgi_script_name_on_v2_formatted_event(self):
"""
Ensure that requests with payload format version 2.0 succeed
"""
lh = LambdaHandler("tests.test_wsgi_script_name_settings")

event = {
"version": "2.0",
"routeKey": "$default",
"rawPath": "/",
"rawQueryString": "",
"headers": {
"host": "1234567890.execute-api.us-east-1.amazonaws.com",
},
"requestContext": {
"http": {
"method": "GET",
"path": "/return/request/url",
},
},
"isBase64Encoded": False,
"body": "",
"cookies": ["Cookie_1=Value1; Expires=21 Oct 2021 07:48 GMT", "Cookie_2=Value2; Max-Age=78000"],
}
response = lh.handler(event, None)

self.assertEqual(response["statusCode"], 200)
self.assertEqual(
response["body"],
"https://1234567890.execute-api.us-east-1.amazonaws.com/dev/return/request/url",
)
152 changes: 152 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,158 @@ def test_wsgi_from_apigateway_testbutton(self):
response_tuple = collections.namedtuple("Response", ["status_code", "content"])
response = response_tuple(200, "hello")

def test_wsgi_from_v2_event(self):
event = {
"version": "2.0",
"routeKey": "ANY /{proxy+}",
"rawPath": "/",
"rawQueryString": "",
"headers": {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"content-length": "0",
"dnt": "1",
"host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"pragma": "no-cache",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"x-forwarded-for": "50.191.225.98",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
},
"requestContext": {
"accountId": "724336686645",
"apiId": "qw8klxioji",
"domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"domainPrefix": "qw8klxioji",
"http": {
"method": "GET",
"path": "/",
"protocol": "HTTP/1.1",
"sourceIp": "50.191.225.98",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
},
"requestId": "xTG4wqXdSQ0RHpA=",
"routeKey": "ANY /{proxy+}",
"stage": "$default",
"time": "16/Oct/2022:11:17:12 +0000",
"timeEpoch": 1665919032135,
},
"pathParameters": {"proxy": ""},
"isBase64Encoded": False,
}
environ = create_wsgi_request(event, event_version="2.0")
self.assertTrue(environ)

def test_wsgi_from_v2_event_with_lambda_authorizer(self):
principal_id = "user|a1b2c3d4"
authorizer = {"lambda": {"bool": True, "key": "value", "number": 1, "principalId": principal_id}}
event = {
"version": "2.0",
"routeKey": "ANY /{proxy+}",
"rawPath": "/",
"rawQueryString": "",
"headers": {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"authorization": "Bearer 1232314343",
"content-length": "28",
"content-type": "application/json",
"host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"x-forwarded-for": "50.191.225.98",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
},
"requestContext": {
"accountId": "724336686645",
"apiId": "qw8klxioji",
"authorizer": authorizer,
"domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"domainPrefix": "qw8klxioji",
"http": {
"method": "POST",
"path": "/",
"protocol": "HTTP/1.1",
"sourceIp": "50.191.225.98",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
},
"requestId": "aJ6Rqi93zQ0GPng=",
"routeKey": "ANY /{proxy+}",
"stage": "$default",
"time": "17/Oct/2022:14:58:44 +0000",
"timeEpoch": 1666018724000,
},
"pathParameters": {"proxy": ""},
"body": "{'data':'0123456789'}",
"isBase64Encoded": False,
}
environ = create_wsgi_request(event, event_version="2.0")
self.assertEqual(environ["API_GATEWAY_AUTHORIZER"], authorizer)
self.assertEqual(environ["REMOTE_USER"], principal_id)

def test_wsgi_from_v2_event_with_iam_authorizer(self):
user_arn = "arn:aws:sts::724336686645:assumed-role/SAMLUSER/user.name"
authorizer = {
"iam": {
"accessKey": "AWSACCESSKEYID",
"accountId": "724336686645",
"callerId": "KFDJSURSUC8FU3ITCWEDJ:user.name",
"cognitoIdentity": None,
"principalOrgId": "aws:PrincipalOrgID",
"userArn": user_arn,
"userId": "KFDJSURSUC8FU3ITCWEDJ:user.name",
}
}
event = {
"version": "2.0",
"routeKey": "ANY /{proxy+}",
"rawPath": "/",
"rawQueryString": "",
"headers": {
"accept": "*/*",
"accept-encoding": "gzip, deflate",
"authorization": "AWS4-HMAC-SHA256 Credential=AWSACCESSKEYID/20221017/eu-west-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=foosignature",
"content-length": "17",
"content-type": "application/json",
"host": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"user-agent": "python-requests/2.28.1",
"x-amz-content-sha256": "foobar",
"x-amz-date": "20221017T150616Z",
"x-amz-security-token": "footoken",
"x-forwarded-for": "50.191.225.98",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
},
"requestContext": {
"accountId": "724336686645",
"apiId": "qw8klxioji",
"authorizer": authorizer,
"domainName": "qw8klxioji.execute-api.eu-west-1.amazonaws.com",
"domainPrefix": "qw8klxioji",
"http": {
"method": "POST",
"path": "/",
"protocol": "HTTP/1.1",
"sourceIp": "50.191.225.98",
"userAgent": "python-requests/2.28.1",
},
"requestId": "aJ5ZZgeYiQ0Rz-A=",
"routeKey": "ANY /{proxy+}",
"stage": "$default",
"time": "17/Oct/2022:15:06:16 +0000",
"timeEpoch": 1666019176656,
},
"pathParameters": {"proxy": ""},
"body": "{'data': '12345'}",
"isBase64Encoded": False,
}
environ = create_wsgi_request(event, event_version="2.0")
self.assertEqual(environ["API_GATEWAY_AUTHORIZER"], authorizer)
self.assertEqual(environ["REMOTE_USER"], user_arn)

##
# Handler
##
Expand Down
121 changes: 121 additions & 0 deletions zappa/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,127 @@ def handler(self, event, context):
logger.error("Cannot find a function to process the triggered event.")
return result

# This is an HTTP-protocol API Gateway event or Lambda url event with payload format version 2.0
elif "version" in event and event["version"] == "2.0":
try:
time_start = datetime.datetime.now()

script_name = ""
host = event.get("headers", {}).get("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 = f"/{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 = f"/{settings.API_STAGE}"

base_path = getattr(settings, "BASE_PATH", None)
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,
event_version="2.0",
)

# 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:
response_body = None
response_is_base_64_encoded = False
if response.data:
if (
settings.BINARY_SUPPORT
and not response.mimetype.startswith("text/")
and response.mimetype != "application/json"
):
response_body = base64.b64encode(response.data).decode("utf-8")
response_is_base_64_encoded = True
else:
response_body = response.get_data(as_text=True)

response_status_code = response.status_code

cookies = []
response_headers = {}
for key, value in response.headers:
if key.lower() == "set-cookie":
cookies.append(value)
else:
if key in response_headers:
updated_value = f"{response_headers[key]},{value}"
response_headers[key] = updated_value
else:
response_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 {
"cookies": cookies,
"isBase64Encoded": response_is_base_64_encoded,
"statusCode": response_status_code,
"headers": response_headers,
"body": response_body,
}
except Exception as e:
# 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

# Normal web app flow
try:
# Timing
Expand Down
Loading

0 comments on commit 69f3b71

Please sign in to comment.