|
6 | 6 | from dataclasses import dataclass, InitVar |
7 | 7 | from contextlib import ExitStack |
8 | 8 |
|
9 | | -from mangum.types import ASGIApp |
| 9 | +from mangum.types import ASGIApp, Scope |
10 | 10 | from mangum.protocols.lifespan import LifespanCycle |
11 | 11 | from mangum.protocols.http import HTTPCycle |
12 | 12 | from mangum.exceptions import ConfigurationError |
@@ -91,89 +91,102 @@ def __call__(self, event: dict, context: "LambdaContext") -> dict: |
91 | 91 | ) |
92 | 92 | stack.enter_context(lifespan_cycle) |
93 | 93 |
|
94 | | - request_context = event["requestContext"] |
95 | | - |
96 | | - if event.get("multiValueHeaders"): |
97 | | - headers = { |
98 | | - k.lower(): ", ".join(v) if isinstance(v, list) else "" |
99 | | - for k, v in event.get("multiValueHeaders", {}).items() |
100 | | - } |
101 | | - elif event.get("headers"): |
102 | | - headers = {k.lower(): v for k, v in event.get("headers", {}).items()} |
103 | | - else: |
104 | | - headers = {} |
105 | | - |
106 | | - # API Gateway v2 |
107 | | - if event.get("version") == "2.0": |
108 | | - source_ip = request_context["http"]["sourceIp"] |
109 | | - path = request_context["http"]["path"] |
110 | | - http_method = request_context["http"]["method"] |
111 | | - query_string = event.get("rawQueryString", "").encode() |
112 | | - |
113 | | - if event.get("cookies"): |
114 | | - headers["cookie"] = "; ".join(event.get("cookies", [])) |
115 | | - |
116 | | - # API Gateway v1 / ELB |
117 | | - else: |
118 | | - if "elb" in request_context: |
119 | | - # NOTE: trust only the most right side value |
120 | | - source_ip = headers.get("x-forwarded-for", "").split(", ")[-1] |
121 | | - else: |
122 | | - source_ip = request_context.get("identity", {}).get("sourceIp") |
123 | | - |
124 | | - path = event["path"] |
125 | | - http_method = event["httpMethod"] |
126 | | - |
127 | | - if event.get("multiValueQueryStringParameters"): |
128 | | - query_string = urllib.parse.urlencode( |
129 | | - event.get("multiValueQueryStringParameters", {}), doseq=True |
130 | | - ).encode() |
131 | | - elif event.get("queryStringParameters"): |
132 | | - query_string = urllib.parse.urlencode( |
133 | | - event.get("queryStringParameters", {}) |
134 | | - ).encode() |
135 | | - else: |
136 | | - query_string = b"" |
137 | | - |
138 | | - server_name = headers.get("host", "mangum") |
139 | | - if ":" not in server_name: |
140 | | - server_port = headers.get("x-forwarded-port", 80) |
141 | | - else: |
142 | | - server_name, server_port = server_name.split(":") # pragma: no cover |
143 | | - server = (server_name, int(server_port)) |
144 | | - client = (source_ip, 0) |
145 | | - |
146 | | - if not path: # pragma: no cover |
147 | | - path = "/" |
148 | | - elif self.api_gateway_base_path: |
149 | | - if path.startswith(self.api_gateway_base_path): |
150 | | - path = path[len(self.api_gateway_base_path) :] |
151 | | - |
152 | | - scope = { |
153 | | - "type": "http", |
154 | | - "http_version": "1.1", |
155 | | - "method": http_method, |
156 | | - "headers": [[k.encode(), v.encode()] for k, v in headers.items()], |
157 | | - "path": urllib.parse.unquote(path), |
158 | | - "raw_path": None, |
159 | | - "root_path": "", |
160 | | - "scheme": headers.get("x-forwarded-proto", "https"), |
161 | | - "query_string": query_string, |
162 | | - "server": server, |
163 | | - "client": client, |
164 | | - "asgi": {"version": "3.0"}, |
165 | | - "aws.event": event, |
166 | | - "aws.context": context, |
167 | | - } |
168 | | - |
169 | 94 | is_binary = event.get("isBase64Encoded", False) |
170 | 95 | initial_body = event.get("body") or b"" |
171 | 96 | if is_binary: |
172 | 97 | initial_body = base64.b64decode(initial_body) |
173 | 98 | elif not isinstance(initial_body, bytes): |
174 | 99 | initial_body = initial_body.encode() |
175 | 100 |
|
| 101 | + scope = self._create_scope(event, context) |
176 | 102 | http_cycle = HTTPCycle(scope, text_mime_types=self.text_mime_types) |
177 | 103 | response = http_cycle(self.app, initial_body) |
178 | 104 |
|
179 | 105 | return response |
| 106 | + |
| 107 | + def _create_scope(self, event: dict, context: "LambdaContext") -> Scope: |
| 108 | + """Creates a scope object according to ASGI specification from a Lambda Event. |
| 109 | +
|
| 110 | + https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope |
| 111 | +
|
| 112 | + The event comes from various sources: AWS ALB, AWS API Gateway of different |
| 113 | + versions and configurations(multivalue header, etc). |
| 114 | + Thus, some heuristics is applied to guess an event type. |
| 115 | +
|
| 116 | + """ |
| 117 | + request_context = event["requestContext"] |
| 118 | + |
| 119 | + if event.get("multiValueHeaders"): |
| 120 | + headers = { |
| 121 | + k.lower(): ", ".join(v) if isinstance(v, list) else "" |
| 122 | + for k, v in event.get("multiValueHeaders", {}).items() |
| 123 | + } |
| 124 | + elif event.get("headers"): |
| 125 | + headers = {k.lower(): v for k, v in event.get("headers", {}).items()} |
| 126 | + else: |
| 127 | + headers = {} |
| 128 | + |
| 129 | + # API Gateway v2 |
| 130 | + if event.get("version") == "2.0": |
| 131 | + source_ip = request_context["http"]["sourceIp"] |
| 132 | + path = request_context["http"]["path"] |
| 133 | + http_method = request_context["http"]["method"] |
| 134 | + query_string = event.get("rawQueryString", "").encode() |
| 135 | + |
| 136 | + if event.get("cookies"): |
| 137 | + headers["cookie"] = "; ".join(event.get("cookies", [])) |
| 138 | + |
| 139 | + # API Gateway v1 / ELB |
| 140 | + else: |
| 141 | + if "elb" in request_context: |
| 142 | + # NOTE: trust only the most right side value |
| 143 | + source_ip = headers.get("x-forwarded-for", "").split(", ")[-1] |
| 144 | + else: |
| 145 | + source_ip = request_context.get("identity", {}).get("sourceIp") |
| 146 | + |
| 147 | + path = event["path"] |
| 148 | + http_method = event["httpMethod"] |
| 149 | + |
| 150 | + if event.get("multiValueQueryStringParameters"): |
| 151 | + query_string = urllib.parse.urlencode( |
| 152 | + event.get("multiValueQueryStringParameters", {}), doseq=True |
| 153 | + ).encode() |
| 154 | + elif event.get("queryStringParameters"): |
| 155 | + query_string = urllib.parse.urlencode( |
| 156 | + event.get("queryStringParameters", {}) |
| 157 | + ).encode() |
| 158 | + else: |
| 159 | + query_string = b"" |
| 160 | + |
| 161 | + server_name = headers.get("host", "mangum") |
| 162 | + if ":" not in server_name: |
| 163 | + server_port = headers.get("x-forwarded-port", 80) |
| 164 | + else: |
| 165 | + server_name, server_port = server_name.split(":") # pragma: no cover |
| 166 | + server = (server_name, int(server_port)) |
| 167 | + client = (source_ip, 0) |
| 168 | + |
| 169 | + if not path: # pragma: no cover |
| 170 | + path = "/" |
| 171 | + elif self.api_gateway_base_path: |
| 172 | + if path.startswith(self.api_gateway_base_path): |
| 173 | + path = path[len(self.api_gateway_base_path) :] |
| 174 | + |
| 175 | + scope = { |
| 176 | + "type": "http", |
| 177 | + "http_version": "1.1", |
| 178 | + "method": http_method, |
| 179 | + "headers": [[k.encode(), v.encode()] for k, v in headers.items()], |
| 180 | + "path": urllib.parse.unquote(path), |
| 181 | + "raw_path": None, |
| 182 | + "root_path": "", |
| 183 | + "scheme": headers.get("x-forwarded-proto", "https"), |
| 184 | + "query_string": query_string, |
| 185 | + "server": server, |
| 186 | + "client": client, |
| 187 | + "asgi": {"version": "3.0"}, |
| 188 | + "aws.event": event, |
| 189 | + "aws.context": context, |
| 190 | + } |
| 191 | + |
| 192 | + return scope |
0 commit comments