220 lines
7.7 KiB
Python
220 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from urllib.parse import urlencode
|
|
|
|
from mangum.handlers.utils import (
|
|
get_server_and_port,
|
|
handle_base64_response_body,
|
|
handle_exclude_headers,
|
|
handle_multi_value_headers,
|
|
maybe_encode_body,
|
|
strip_api_gateway_path,
|
|
)
|
|
from mangum.types import (
|
|
Headers,
|
|
LambdaConfig,
|
|
LambdaContext,
|
|
LambdaEvent,
|
|
QueryParams,
|
|
Response,
|
|
Scope,
|
|
)
|
|
|
|
|
|
def _encode_query_string_for_apigw(event: LambdaEvent) -> bytes:
|
|
params: QueryParams = event.get("multiValueQueryStringParameters", {})
|
|
if not params:
|
|
params = event.get("queryStringParameters", {})
|
|
if not params:
|
|
return b""
|
|
|
|
return urlencode(params, doseq=True).encode()
|
|
|
|
|
|
def _handle_multi_value_headers_for_request(event: LambdaEvent) -> dict[str, str]:
|
|
headers = event.get("headers", {}) or {}
|
|
headers = {k.lower(): v for k, v in headers.items()}
|
|
if event.get("multiValueHeaders"):
|
|
headers.update(
|
|
{
|
|
k.lower(): ", ".join(v) if isinstance(v, list) else ""
|
|
for k, v in event.get("multiValueHeaders", {}).items()
|
|
}
|
|
)
|
|
|
|
return headers
|
|
|
|
|
|
def _combine_headers_v2(
|
|
input_headers: Headers,
|
|
) -> tuple[dict[str, str], list[str]]:
|
|
output_headers: dict[str, str] = {}
|
|
cookies: list[str] = []
|
|
for key, value in input_headers:
|
|
normalized_key: str = key.decode().lower()
|
|
normalized_value: str = value.decode()
|
|
if normalized_key == "set-cookie":
|
|
cookies.append(normalized_value)
|
|
else:
|
|
if normalized_key in output_headers:
|
|
normalized_value = f"{output_headers[normalized_key]},{normalized_value}"
|
|
output_headers[normalized_key] = normalized_value
|
|
|
|
return output_headers, cookies
|
|
|
|
|
|
class APIGateway:
|
|
@classmethod
|
|
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
|
|
return "resource" in event and "requestContext" in event
|
|
|
|
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
|
|
self.event = event
|
|
self.context = context
|
|
self.config = config
|
|
|
|
@property
|
|
def body(self) -> bytes:
|
|
return maybe_encode_body(
|
|
self.event.get("body", b""),
|
|
is_base64=self.event.get("isBase64Encoded", False),
|
|
)
|
|
|
|
@property
|
|
def scope(self) -> Scope:
|
|
headers = _handle_multi_value_headers_for_request(self.event)
|
|
return {
|
|
"type": "http",
|
|
"http_version": "1.1",
|
|
"method": self.event["httpMethod"],
|
|
"headers": [[k.encode(), v.encode()] for k, v in headers.items()],
|
|
"path": strip_api_gateway_path(
|
|
self.event["path"],
|
|
api_gateway_base_path=self.config["api_gateway_base_path"],
|
|
),
|
|
"raw_path": None,
|
|
"root_path": "",
|
|
"scheme": headers.get("x-forwarded-proto", "https"),
|
|
"query_string": _encode_query_string_for_apigw(self.event),
|
|
"server": get_server_and_port(headers),
|
|
"client": (
|
|
self.event["requestContext"].get("identity", {}).get("sourceIp"),
|
|
0,
|
|
),
|
|
"asgi": {"version": "3.0", "spec_version": "2.0"},
|
|
"aws.event": self.event,
|
|
"aws.context": self.context,
|
|
}
|
|
|
|
def __call__(self, response: Response) -> dict[str, Any]:
|
|
finalized_headers, multi_value_headers = handle_multi_value_headers(response["headers"])
|
|
finalized_body, is_base64_encoded = handle_base64_response_body(
|
|
response["body"], finalized_headers, self.config["text_mime_types"]
|
|
)
|
|
|
|
return {
|
|
"statusCode": response["status"],
|
|
"headers": handle_exclude_headers(finalized_headers, self.config),
|
|
"multiValueHeaders": handle_exclude_headers(multi_value_headers, self.config),
|
|
"body": finalized_body,
|
|
"isBase64Encoded": is_base64_encoded,
|
|
}
|
|
|
|
|
|
class HTTPGateway:
|
|
@classmethod
|
|
def infer(cls, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> bool:
|
|
return "version" in event and "requestContext" in event
|
|
|
|
def __init__(self, event: LambdaEvent, context: LambdaContext, config: LambdaConfig) -> None:
|
|
self.event = event
|
|
self.context = context
|
|
self.config = config
|
|
|
|
@property
|
|
def body(self) -> bytes:
|
|
return maybe_encode_body(
|
|
self.event.get("body", b""),
|
|
is_base64=self.event.get("isBase64Encoded", False),
|
|
)
|
|
|
|
@property
|
|
def scope(self) -> Scope:
|
|
request_context = self.event["requestContext"]
|
|
event_version = self.event["version"]
|
|
|
|
# API Gateway v2
|
|
if event_version == "2.0":
|
|
headers = {k.lower(): v for k, v in self.event.get("headers", {}).items()}
|
|
source_ip = request_context["http"]["sourceIp"]
|
|
path = request_context["http"]["path"]
|
|
http_method = request_context["http"]["method"]
|
|
query_string = self.event.get("rawQueryString", "").encode()
|
|
|
|
if self.event.get("cookies"):
|
|
headers["cookie"] = "; ".join(self.event.get("cookies", []))
|
|
|
|
# API Gateway v1
|
|
else:
|
|
headers = _handle_multi_value_headers_for_request(self.event)
|
|
source_ip = request_context.get("identity", {}).get("sourceIp")
|
|
path = self.event["path"]
|
|
http_method = self.event["httpMethod"]
|
|
query_string = _encode_query_string_for_apigw(self.event)
|
|
|
|
path = strip_api_gateway_path(
|
|
path,
|
|
api_gateway_base_path=self.config["api_gateway_base_path"],
|
|
)
|
|
server = get_server_and_port(headers)
|
|
client = (source_ip, 0)
|
|
|
|
return {
|
|
"type": "http",
|
|
"method": http_method,
|
|
"http_version": "1.1",
|
|
"headers": [[k.encode(), v.encode()] for k, v in headers.items()],
|
|
"path": path,
|
|
"raw_path": None,
|
|
"root_path": "",
|
|
"scheme": headers.get("x-forwarded-proto", "https"),
|
|
"query_string": query_string,
|
|
"server": server,
|
|
"client": client,
|
|
"asgi": {"version": "3.0", "spec_version": "2.0"},
|
|
"aws.event": self.event,
|
|
"aws.context": self.context,
|
|
}
|
|
|
|
def __call__(self, response: Response) -> dict[str, Any]:
|
|
if self.scope["aws.event"]["version"] == "2.0":
|
|
finalized_headers, cookies = _combine_headers_v2(response["headers"])
|
|
|
|
if "content-type" not in finalized_headers and response["body"] is not None:
|
|
finalized_headers["content-type"] = "application/json"
|
|
|
|
finalized_body, is_base64_encoded = handle_base64_response_body(
|
|
response["body"], finalized_headers, self.config["text_mime_types"]
|
|
)
|
|
response_out = {
|
|
"statusCode": response["status"],
|
|
"body": finalized_body,
|
|
"headers": finalized_headers or None,
|
|
"cookies": cookies or None,
|
|
"isBase64Encoded": is_base64_encoded,
|
|
}
|
|
return {key: value for key, value in response_out.items() if value is not None}
|
|
|
|
finalized_headers, multi_value_headers = handle_multi_value_headers(response["headers"])
|
|
finalized_body, is_base64_encoded = handle_base64_response_body(
|
|
response["body"], finalized_headers, self.config["text_mime_types"]
|
|
)
|
|
return {
|
|
"statusCode": response["status"],
|
|
"headers": finalized_headers,
|
|
"multiValueHeaders": multi_value_headers,
|
|
"body": finalized_body,
|
|
"isBase64Encoded": is_base64_encoded,
|
|
}
|