1
1
import json
2
2
import logging
3
- import re
4
- from typing import Any , Dict , Union
5
- from urllib .parse import urljoin
3
+ from typing import Dict
6
4
7
- import requests
8
5
from jsonschema import ValidationError , validate
9
6
from requests .models import Response
10
7
11
- from localstack import config
12
- from localstack .aws .accounts import get_aws_account_id
13
8
from localstack .constants import APPLICATION_JSON
14
9
from localstack .services .apigateway import helpers
15
10
from localstack .services .apigateway .context import ApiInvocationContext
21
16
)
22
17
from localstack .services .apigateway .integration import (
23
18
DynamoDBIntegration ,
19
+ HTTPIntegration ,
24
20
KinesisIntegration ,
25
21
LambdaIntegration ,
26
22
LambdaProxyIntegration ,
27
23
MockIntegration ,
28
- SnsIntegration ,
24
+ S3Integration ,
25
+ SNSIntegration ,
26
+ SQSIntegration ,
29
27
StepFunctionIntegration ,
30
28
)
31
- from localstack .services .apigateway .templates import (
32
- RequestTemplates ,
33
- ResponseTemplates ,
34
- VtlTemplate ,
35
- )
36
- from localstack .utils import common
37
29
from localstack .utils .aws import aws_stack
38
- from localstack .utils .aws .aws_responses import request_response_stream
39
- from localstack .utils .http import add_query_params_to_url
40
30
41
31
LOG = logging .getLogger (__name__ )
42
32
43
- # target ARN patterns
44
- TARGET_REGEX_PATH_S3_URI = (
45
- r"^arn:aws:apigateway:[a-zA-Z0-9\-]+:s3:path/(?P<bucket>[^/]+)/(?P<object>.+)$"
46
- )
47
- TARGET_REGEX_ACTION_S3_URI = r"^arn:aws:apigateway:[a-zA-Z0-9\-]+:s3:action/(?:GetObject&Bucket\=(?P<bucket>[^&]+)&Key\=(?P<object>.+))$"
48
-
49
- # TODO: refactor / split up this file into suitable submodules
50
-
51
33
52
34
class AuthorizationError (Exception ):
53
35
pass
@@ -183,30 +165,6 @@ def update_content_length(response: Response):
183
165
response .headers ["Content-Length" ] = str (len (response .content ))
184
166
185
167
186
- # TODO: remove once we migrate all usages to `apply_request_parameters` on BackendIntegration
187
- def apply_request_parameters (
188
- uri : str , integration : Dict [str , Any ], path_params : Dict [str , str ], query_params : Dict [str , str ]
189
- ):
190
- request_parameters = integration .get ("requestParameters" )
191
- uri = uri or integration .get ("uri" ) or integration .get ("integrationUri" ) or ""
192
- if request_parameters :
193
- for key in path_params :
194
- # check if path_params is present in the integration request parameters
195
- request_param_key = f"integration.request.path.{ key } "
196
- request_param_value = f"method.request.path.{ key } "
197
- if request_parameters .get (request_param_key ) == request_param_value :
198
- uri = uri .replace (f"{{{ key } }}" , path_params [key ])
199
-
200
- if integration .get ("type" ) != "HTTP_PROXY" and request_parameters :
201
- for key in query_params .copy ():
202
- request_query_key = f"integration.request.querystring.{ key } "
203
- request_param_val = f"method.request.querystring.{ key } "
204
- if request_parameters .get (request_query_key , None ) != request_param_val :
205
- query_params .pop (key )
206
-
207
- return add_query_params_to_url (uri , query_params )
208
-
209
-
210
168
def apply_response_parameters (invocation_context : ApiInvocationContext ):
211
169
response = invocation_context .response
212
170
integration = invocation_context .integration
@@ -300,31 +258,27 @@ def invoke_rest_api_integration(invocation_context: ApiInvocationContext):
300
258
return make_error_response (msg , 400 )
301
259
302
260
303
- # TODO: refactor this to have a class per integration type to make it easy to
304
- # test the encapsulated logic
305
-
306
- # this call is patched downstream for backend integrations that are only available
307
- # in the PRO version
261
+ # This function is patched downstream for backend integrations that are only available
262
+ # in Pro (potentially to be replaced with a runtime hook in the future).
308
263
def invoke_rest_api_integration_backend (invocation_context : ApiInvocationContext ):
309
264
# define local aliases from invocation context
310
265
invocation_path = invocation_context .path_with_query_string
311
266
method = invocation_context .method
312
267
headers = invocation_context .headers
313
268
resource_path = invocation_context .resource_path
314
269
integration = invocation_context .integration
315
- integration_response = integration .get ("integrationResponses" , {})
316
- response_templates = integration_response .get ("200" , {}).get ("responseTemplates" , {})
317
270
# extract integration type and path parameters
318
271
relative_path , query_string_params = extract_query_string_params (path = invocation_path )
319
272
integration_type_orig = integration .get ("type" ) or integration .get ("integrationType" ) or ""
320
273
integration_type = integration_type_orig .upper ()
321
274
uri = integration .get ("uri" ) or integration .get ("integrationUri" ) or ""
322
275
323
276
try :
324
- path_params = extract_path_params (path = relative_path , extracted_path = resource_path )
325
- invocation_context .path_params = path_params
277
+ invocation_context .path_params = extract_path_params (
278
+ path = relative_path , extracted_path = resource_path
279
+ )
326
280
except Exception :
327
- path_params = {}
281
+ invocation_context . path_params = {}
328
282
329
283
if (uri .startswith ("arn:aws:apigateway:" ) and ":lambda:path" in uri ) or uri .startswith (
330
284
"arn:aws:lambda"
@@ -334,10 +288,6 @@ def invoke_rest_api_integration_backend(invocation_context: ApiInvocationContext
334
288
elif integration_type == "AWS" :
335
289
return LambdaIntegration ().invoke (invocation_context )
336
290
337
- raise Exception (
338
- f'Unsupported API Gateway integration type "{ integration_type } ", action "{ uri } ", method "{ method } "'
339
- )
340
-
341
291
elif integration_type == "AWS" :
342
292
if "kinesis:action/" in uri :
343
293
return KinesisIntegration ().invoke (invocation_context )
@@ -348,109 +298,20 @@ def invoke_rest_api_integration_backend(invocation_context: ApiInvocationContext
348
298
if ":dynamodb:action" in uri :
349
299
return DynamoDBIntegration ().invoke (invocation_context )
350
300
351
- # https://docs.aws.amazon.com/apigateway/api-reference/resource/integration/
352
- if ("s3:path/" in uri or "s3:action/" in uri ) and method == "GET" :
353
- s3 = aws_stack .connect_to_service ("s3" )
354
- uri = apply_request_parameters (
355
- uri ,
356
- integration = integration ,
357
- path_params = path_params ,
358
- query_params = query_string_params ,
359
- )
360
- uri_match = re .match (TARGET_REGEX_PATH_S3_URI , uri ) or re .match (
361
- TARGET_REGEX_ACTION_S3_URI , uri
362
- )
363
- if uri_match :
364
- bucket , object_key = uri_match .group ("bucket" , "object" )
365
- LOG .debug ("Getting request for bucket %s object %s" , bucket , object_key )
366
- try :
367
- object = s3 .get_object (Bucket = bucket , Key = object_key )
368
- except s3 .exceptions .NoSuchKey :
369
- msg = "Object %s not found" % object_key
370
- LOG .debug (msg )
371
- return make_error_response (msg , 404 )
372
-
373
- headers = aws_stack .mock_aws_request_headers (service = "s3" )
374
-
375
- if object .get ("ContentType" ):
376
- headers ["Content-Type" ] = object ["ContentType" ]
377
-
378
- # stream used so large files do not fill memory
379
- response = request_response_stream (stream = object ["Body" ], headers = headers )
380
- return response
381
- else :
382
- msg = "Request URI does not match s3 specifications"
383
- LOG .warning (msg )
384
- return make_error_response (msg , 400 )
301
+ if "s3:path/" in uri or "s3:action/" in uri :
302
+ return S3Integration ().invoke (invocation_context )
385
303
386
304
if method == "POST" and ":sqs:path" in uri :
387
- template = integration ["requestTemplates" ].get (APPLICATION_JSON )
388
- account_id , queue = uri .split ("/" )[- 2 :]
389
- region_name = uri .split (":" )[3 ]
390
- if "GetQueueUrl" in template or "CreateQueue" in template :
391
- request_templates = RequestTemplates ()
392
- payload = request_templates .render (invocation_context )
393
- new_request = f"{ payload } &QueueName={ queue } "
394
- else :
395
- request_templates = RequestTemplates ()
396
- payload = request_templates .render (invocation_context )
397
- queue_url = f"{ config .get_edge_url ()} /{ account_id } /{ queue } "
398
- new_request = f"{ payload } &QueueUrl={ queue_url } "
399
- headers = aws_stack .mock_aws_request_headers (service = "sqs" , region_name = region_name )
400
-
401
- url = urljoin (config .service_url ("sqs" ), f"{ get_aws_account_id ()} /{ queue } " )
402
- result = common .make_http_request (url , method = "POST" , headers = headers , data = new_request )
403
- return result
305
+ return SQSIntegration ().invoke (invocation_context )
404
306
405
307
if method == "POST" and ":sns:path" in uri :
406
- invocation_context .context = helpers .get_event_request_context (invocation_context )
407
- invocation_context .stage_variables = helpers .get_stage_variables (invocation_context )
408
-
409
- integration_response = SnsIntegration ().invoke (invocation_context )
410
- return apply_request_response_templates (
411
- integration_response , response_templates , content_type = APPLICATION_JSON
412
- )
413
-
414
- raise Exception (
415
- f'API Gateway action uri "{ uri } ", integration type { integration_type } not yet implemented'
416
- )
308
+ return SNSIntegration ().invoke (invocation_context )
417
309
418
310
elif integration_type in ["HTTP_PROXY" , "HTTP" ]:
419
-
420
- if ":servicediscovery:" in uri :
421
- # check if this is a servicediscovery integration URI
422
- client = aws_stack .connect_to_service ("servicediscovery" )
423
- service_id = uri .split ("/" )[- 1 ]
424
- instances = client .list_instances (ServiceId = service_id )["Instances" ]
425
- instance = (instances or [None ])[0 ]
426
- if instance and instance .get ("Id" ):
427
- uri = "http://%s/%s" % (instance ["Id" ], invocation_path .lstrip ("/" ))
428
-
429
- # apply custom request template
430
- invocation_context .context = helpers .get_event_request_context (invocation_context )
431
- invocation_context .stage_variables = helpers .get_stage_variables (invocation_context )
432
- request_templates = RequestTemplates ()
433
- payload = request_templates .render (invocation_context )
434
-
435
- if isinstance (payload , dict ):
436
- payload = json .dumps (payload )
437
-
438
- uri = apply_request_parameters (
439
- uri ,
440
- integration = integration ,
441
- path_params = path_params ,
442
- query_params = query_string_params ,
443
- )
444
- result = requests .request (method = method , url = uri , data = payload , headers = headers )
445
- # apply custom response template
446
- invocation_context .response = result
447
- response_templates = ResponseTemplates ()
448
- response_templates .render (invocation_context )
449
- return invocation_context .response
311
+ return HTTPIntegration ().invoke (invocation_context )
450
312
451
313
elif integration_type == "MOCK" :
452
- mock_integration = MockIntegration ()
453
- return mock_integration .invoke (invocation_context )
314
+ return MockIntegration ().invoke (invocation_context )
454
315
455
316
if method == "OPTIONS" :
456
317
# fall back to returning CORS headers if this is an OPTIONS request
@@ -459,26 +320,3 @@ def invoke_rest_api_integration_backend(invocation_context: ApiInvocationContext
459
320
raise Exception (
460
321
f'API Gateway integration type "{ integration_type } ", method "{ method } ", URI "{ uri } " not yet implemented'
461
322
)
462
-
463
-
464
- def apply_request_response_templates (
465
- data : Union [Response , bytes ],
466
- templates : Dict [str , str ],
467
- content_type : str = None ,
468
- as_json : bool = False ,
469
- ):
470
- """Apply the matching request/response template (if it exists) to the payload data and return the result"""
471
-
472
- content_type = content_type or APPLICATION_JSON
473
- is_response = isinstance (data , Response )
474
- templates = templates or {}
475
- template = templates .get (content_type )
476
- if not template :
477
- return data
478
- content = (data .content if is_response else data ) or ""
479
- result = VtlTemplate ().render_vtl (template , content , as_json = as_json )
480
- if is_response :
481
- data ._content = result
482
- update_content_length (data )
483
- return data
484
- return result
0 commit comments