Skip to content

fix APIGW path converter with prepended multiple slashes #11268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion localstack-core/localstack/aws/protocol/op_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
from werkzeug.routing import Map, MapAdapter

from localstack.aws.protocol.routing import (
GreedyPathConverter,
StrictMethodRule,
path_param_regex,
post_process_arg_name,
transform_path_params_to_rule_vars,
)
from localstack.http import Request
from localstack.http.request import get_raw_path
from localstack.http.router import GreedyPathConverter


class _HttpOperation(NamedTuple):
Expand Down
16 changes: 1 addition & 15 deletions localstack-core/localstack/aws/protocol/routing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
from typing import AnyStr

from werkzeug.routing import PathConverter, Rule
from werkzeug.routing import Rule

# Regex to find path parameters in requestUris of AWS service specs (f.e. /{param1}/{param2+})
path_param_regex = re.compile(r"({.+?})")
Expand All @@ -12,20 +12,6 @@
_rule_replacement_table = str.maketrans(_rule_replacements)


class GreedyPathConverter(PathConverter):
"""
This converter makes sure that the path ``/mybucket//mykey`` can be matched to the pattern
``<Bucket>/<path:Key>`` and will result in `Key` being `/mykey`.
"""

regex = ".*?"

part_isolating = False
"""From the werkzeug docs: If a custom converter can match a forward slash, /, it should have the
attribute part_isolating set to False. This will ensure that rules using the custom converter are
correctly matched."""


class StrictMethodRule(Rule):
"""
Small extension to Werkzeug's Rule class which reverts unwanted assumptions made by Werkzeug.
Expand Down
17 changes: 17 additions & 0 deletions localstack-core/localstack/http/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,28 @@
route,
)
from rolo.routing.router import Dispatcher, call_endpoint
from werkzeug.routing import PathConverter

HTTP_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE")

E = TypeVar("E")
RequestArguments = Mapping[str, Any]


class GreedyPathConverter(PathConverter):
"""
This converter makes sure that the path ``/mybucket//mykey`` can be matched to the pattern
``<Bucket>/<path:Key>`` and will result in `Key` being `/mykey`.
"""

regex = ".*?"

part_isolating = False
"""From the werkzeug docs: If a custom converter can match a forward slash, /, it should have the
attribute part_isolating set to False. This will ensure that rules using the custom converter are
correctly matched."""


__all__ = [
"RequestArguments",
"HTTP_METHODS",
Expand All @@ -32,4 +48,5 @@
"RuleAdapter",
"WithHost",
"RuleGroup",
"GreedyPathConverter",
]
3 changes: 2 additions & 1 deletion localstack-core/localstack/services/apigateway/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,8 @@ def extract_path_params(path: str, extracted_path: str) -> Dict[str, str]:

def extract_query_string_params(path: str) -> Tuple[str, Dict[str, str]]:
parsed_path = urlparse.urlparse(path)
path = parsed_path.path
if not path.startswith("//"):
path = parsed_path.path
parsed_query_string_params = urlparse.parse_qs(parsed_path.query)

query_string_params = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def construct_invocation_event(
headers = canonicalize_headers(headers)

return {
"path": path,
"path": "/" + path.lstrip("/"),
"headers": headers,
"multiValueHeaders": multi_value_dict_for_list(headers),
"body": data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ def create_context_variables(context: RestApiInvocationContext) -> ContextVariab
userAgent=invocation_request["headers"].get("User-Agent"),
userArn=None,
),
# TODO: check if we need the raw path? with forward slashes
path=f"/{context.stage}{invocation_request['path']}",
path=f"/{context.stage}{invocation_request['raw_path']}",
protocol="HTTP/1.1",
requestId=long_uid(),
requestTime=timestamp(time=now, format=REQUEST_TIME_DATE_FORMAT),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

from localstack.aws.api.apigateway import Resource
from localstack.aws.protocol.routing import (
GreedyPathConverter,
path_param_regex,
post_process_arg_name,
transform_path_params_to_rule_vars,
)
from localstack.http import Response
from localstack.http.router import GreedyPathConverter
from localstack.services.apigateway.models import RestApiDeployment

from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def register_routes(self) -> None:
strict_slashes=False,
),
self.router.add(
path="/<stage>/<path:path>",
path="/<stage>/<greedy_path:path>",
host=host_pattern,
endpoint=self.handler,
strict_slashes=True,
Expand All @@ -119,7 +119,7 @@ def register_routes(self) -> None:
defaults={"path": ""},
),
self.router.add(
path="/restapis/<api_id>/<stage>/_user_request_/<path:path>",
path="/restapis/<api_id>/<stage>/_user_request_/<greedy_path:path>",
endpoint=self.handler,
strict_slashes=True,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def register_routes(self) -> None:
strict_slashes=False,
)
self.router.add(
"/<stage>/<path:path>",
"/<stage>/<greedy_path:path>",
host=host_pattern,
endpoint=self.invoke_rest_api,
strict_slashes=True,
Expand All @@ -142,7 +142,7 @@ def register_routes(self) -> None:
defaults={"path": ""},
)
self.router.add(
"/restapis/<api_id>/<stage>/_user_request_/<path:path>",
"/restapis/<api_id>/<stage>/_user_request_/<greedy_path:path>",
endpoint=self.invoke_rest_api,
strict_slashes=True,
)
Expand Down
5 changes: 4 additions & 1 deletion localstack-core/localstack/services/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from localstack.http import Router
from localstack.http.dispatcher import Handler, handler_dispatcher
from localstack.http.router import GreedyPathConverter
from localstack.utils.collections import split_list_by
from localstack.utils.net import get_free_tcp_port
from localstack.utils.run import is_root, run
Expand All @@ -23,7 +24,9 @@
LOG = logging.getLogger(__name__)


ROUTER: Router[Handler] = Router(dispatcher=handler_dispatcher())
ROUTER: Router[Handler] = Router(
dispatcher=handler_dispatcher(), converters={"greedy_path": GreedyPathConverter}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding this here is 👌

)
"""This special Router is part of the edge proxy. Use the router to inject custom handlers that are handled before
the actual AWS service call is made."""

Expand Down
20 changes: 20 additions & 0 deletions tests/aws/services/apigateway/test_apigateway_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,26 @@ def invoke_api(url):
response_trailing_slash = retry(invoke_api, sleep=2, retries=10, url=f"{invocation_url}/")
snapshot.match("invocation-payload-with-trailing-slash", response_trailing_slash.json())

# invoke rest api with double slash in proxy param
response_double_slash = retry(
invoke_api, sleep=2, retries=10, url=f"{invocation_url}//test-path"
)
snapshot.match("invocation-payload-with-double-slash", response_double_slash.json())

# invoke rest api with prepended slash to the stage (//<stage>/<path>)
double_slash_before_stage = invocation_url.replace(f"/{stage_name}/", f"//{stage_name}/")
response_prepend_slash = retry(invoke_api, sleep=2, retries=10, url=double_slash_before_stage)
snapshot.match(
"invocation-payload-with-prepended-slash-to-stage", response_prepend_slash.json()
)

# invoke rest api with prepended slash
slash_between_stage_and_path = invocation_url.replace("/test-path", "//test-path")
response_prepend_slash = retry(
invoke_api, sleep=2, retries=10, url=slash_between_stage_and_path
)
snapshot.match("invocation-payload-with-prepended-slash", response_prepend_slash.json())

response_no_trailing_slash = retry(
invoke_api, sleep=2, retries=10, url=f"{invocation_url}?urlparam=test"
)
Expand Down
Loading
Loading