1
1
import logging
2
+ from http import HTTPMethod
2
3
3
4
from werkzeug .datastructures import Headers
4
5
5
6
from localstack .aws .api .apigateway import Integration , IntegrationType
6
7
from localstack .constants import APPLICATION_JSON
7
- from localstack .http import Response
8
+ from localstack .http import Request , Response
8
9
from localstack .utils .collections import merge_recursive
9
- from localstack .utils .strings import to_bytes , to_str
10
+ from localstack .utils .strings import short_uid , to_bytes , to_str
10
11
11
12
from ..api import RestApiGatewayHandler , RestApiGatewayHandlerChain
12
13
from ..context import IntegrationRequest , InvocationRequest , RestApiInvocationContext
13
- from ..gateway_response import UnsupportedMediaTypeError
14
+ from ..gateway_response import InternalServerError , UnsupportedMediaTypeError
15
+ from ..header_utils import drop_headers , set_default_headers
14
16
from ..helpers import render_integration_uri
15
17
from ..parameters_mapping import ParametersMapper , RequestDataMapping
16
18
from ..template_mapping import (
23
25
24
26
LOG = logging .getLogger (__name__ )
25
27
28
+ # Illegal headers to include in transformation
29
+ ILLEGAL_INTEGRATION_REQUESTS_COMMON = [
30
+ "content-length" ,
31
+ "transfer-encoding" ,
32
+ "x-amzn-trace-id" ,
33
+ "X-Amzn-Apigateway-Api-Id" ,
34
+ ]
35
+ ILLEGAL_INTEGRATION_REQUESTS_AWS = [
36
+ * ILLEGAL_INTEGRATION_REQUESTS_COMMON ,
37
+ "authorization" ,
38
+ "connection" ,
39
+ "expect" ,
40
+ "proxy-authenticate" ,
41
+ "te" ,
42
+ ]
43
+
44
+ # These are dropped after the templates override were applied. they will never make it to the requests.
45
+ DROPPED_FROM_INTEGRATION_REQUESTS_COMMON = ["Expect" , "Proxy-Authenticate" , "TE" ]
46
+ DROPPED_FROM_INTEGRATION_REQUESTS_AWS = [* DROPPED_FROM_INTEGRATION_REQUESTS_COMMON , "Referer" ]
47
+ DROPPED_FROM_INTEGRATION_REQUESTS_HTTP = [* DROPPED_FROM_INTEGRATION_REQUESTS_COMMON , "Via" ]
48
+
49
+ # Default headers
50
+ DEFAULT_REQUEST_HEADERS = {"Accept" : APPLICATION_JSON , "Connection" : "keep-alive" }
51
+
26
52
27
53
class PassthroughBehavior (str ):
28
54
# TODO maybe this class should be moved where it can also be used for validation in
@@ -48,7 +74,7 @@ def __call__(
48
74
context : RestApiInvocationContext ,
49
75
response : Response ,
50
76
):
51
- integration : Integration = context .resource_method [ "methodIntegration" ]
77
+ integration : Integration = context .integration
52
78
integration_type = integration ["type" ]
53
79
54
80
integration_request_parameters = integration ["requestParameters" ] or {}
@@ -59,7 +85,8 @@ def __call__(
59
85
60
86
if integration_type in (IntegrationType .AWS_PROXY , IntegrationType .HTTP_PROXY ):
61
87
# `PROXY` types cannot use integration mapping templates, they pass most of the data straight
62
- headers = context .invocation_request ["headers" ]
88
+ # We make a copy to avoid modifying the invocation headers and keep a cleaner history
89
+ headers = context .invocation_request ["headers" ].copy ()
63
90
query_string_parameters : dict [str , list [str ]] = context .invocation_request [
64
91
"multi_value_query_string_parameters"
65
92
]
@@ -68,31 +95,22 @@ def __call__(
68
95
# HTTP_PROXY still make uses of the request data mappings, and merges it with the invocation request
69
96
# this is undocumented but validated behavior
70
97
if integration_type == IntegrationType .HTTP_PROXY :
71
- headers = headers .copy ()
72
- to_merge = {
73
- k : v
74
- for k , v in request_data_mapping ["header" ].items ()
75
- if k not in ("Content-Type" , "Accept" )
76
- }
77
- headers .update (to_merge )
98
+ # These headers won't be passed through by default from the invocation.
99
+ # They can however be added through request mappings.
100
+ drop_headers (headers , ["Host" , "Content-Encoding" ])
101
+ headers .update (request_data_mapping ["header" ])
78
102
79
103
query_string_parameters = self ._merge_http_proxy_query_string (
80
104
query_string_parameters , request_data_mapping ["querystring" ]
81
105
)
82
106
83
107
else :
108
+ self ._set_proxy_headers (headers , context .request )
84
109
# AWS_PROXY does not allow URI path rendering
85
110
# TODO: verify this
86
111
path_parameters = {}
87
112
88
113
else :
89
- # default values, can be overridden with right casing
90
- default_headers = {
91
- "Content-Type" : APPLICATION_JSON ,
92
- "Accept" : APPLICATION_JSON ,
93
- }
94
- request_data_mapping ["header" ] = default_headers | request_data_mapping ["header" ]
95
-
96
114
# find request template to raise UnsupportedMediaTypeError early
97
115
request_template = self .get_request_template (
98
116
integration = integration , request = context .invocation_request
@@ -107,6 +125,13 @@ def __call__(
107
125
headers = Headers (request_data_mapping ["header" ])
108
126
query_string_parameters = request_data_mapping ["querystring" ]
109
127
128
+ # Some headers can't be modified by parameter mappings or mapping templates.
129
+ # Aws will raise in those were present. Even for AWS_PROXY, where it is not applying them.
130
+ if header_mappings := request_data_mapping ["header" ]:
131
+ self ._validate_headers_mapping (header_mappings , integration_type )
132
+
133
+ self ._apply_header_transforms (headers , integration_type , context )
134
+
110
135
# looks like the stageVariables rendering part is done in the Integration part in AWS
111
136
# but we can avoid duplication by doing it here for now
112
137
# TODO: if the integration if of AWS Lambda type and the Lambda is in another account, we cannot render
@@ -223,3 +248,54 @@ def _merge_http_proxy_query_string(
223
248
new_query_string_parameters [param ] = value
224
249
225
250
return new_query_string_parameters
251
+
252
+ @staticmethod
253
+ def _set_proxy_headers (headers : Headers , request : Request ):
254
+ headers .set ("X-Forwarded-For" , request .remote_addr )
255
+ headers .set ("X-Forwarded-Port" , request .environ .get ("SERVER_PORT" ))
256
+ headers .set (
257
+ "X-Forwarded-Proto" ,
258
+ request .environ .get ("SERVER_PROTOCOL" , "" ).split ("/" )[0 ],
259
+ )
260
+
261
+ @staticmethod
262
+ def _apply_header_transforms (
263
+ headers : Headers , integration_type : IntegrationType , context : RestApiInvocationContext
264
+ ):
265
+ # Dropping matching headers for the provided integration type
266
+ match integration_type :
267
+ case IntegrationType .AWS :
268
+ drop_headers (headers , DROPPED_FROM_INTEGRATION_REQUESTS_AWS )
269
+ case IntegrationType .HTTP | IntegrationType .HTTP_PROXY :
270
+ drop_headers (headers , DROPPED_FROM_INTEGRATION_REQUESTS_HTTP )
271
+ case _:
272
+ drop_headers (headers , DROPPED_FROM_INTEGRATION_REQUESTS_COMMON )
273
+
274
+ # Adding default headers to the requests headers
275
+ default_headers = {
276
+ ** DEFAULT_REQUEST_HEADERS ,
277
+ "User-Agent" : f"AmazonAPIGateway_{ context .api_id } " ,
278
+ }
279
+ if (
280
+ content_type := context .request .headers .get ("Content-Type" )
281
+ ) and context .request .method not in {HTTPMethod .OPTIONS , HTTPMethod .GET , HTTPMethod .HEAD }:
282
+ default_headers ["Content-Type" ] = content_type
283
+
284
+ set_default_headers (headers , default_headers )
285
+ headers .set ("X-Amzn-Trace-Id" , short_uid ()) # TODO
286
+ if integration_type not in (IntegrationType .AWS_PROXY , IntegrationType .AWS ):
287
+ headers .set ("X-Amzn-Apigateway-Api-Id" , context .api_id )
288
+
289
+ @staticmethod
290
+ def _validate_headers_mapping (headers : dict [str , str ], integration_type : IntegrationType ):
291
+ """Validates and raises an error when attempting to set an illegal header"""
292
+ to_validate = ILLEGAL_INTEGRATION_REQUESTS_COMMON
293
+ if integration_type in {IntegrationType .AWS , IntegrationType .AWS_PROXY }:
294
+ to_validate = ILLEGAL_INTEGRATION_REQUESTS_AWS
295
+
296
+ for header in headers .keys ():
297
+ if header .lower () in to_validate :
298
+ LOG .debug (
299
+ "Execution failed due to configuration error: %s header already present" , header
300
+ )
301
+ raise InternalServerError ("Internal server error" )
0 commit comments