Skip to content

Commit

Permalink
fix request encoding in generic proxy listener chain and forwarding
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrashed committed May 16, 2022
1 parent 02e7c5d commit fe610cf
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 20 deletions.
11 changes: 11 additions & 0 deletions localstack/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,14 @@ def get_raw_path(request) -> str:
return request.scope.get("raw_path", request.path).decode("utf-8")

raise ValueError("cannot extract raw path from request object %s" % request)


def get_full_raw_path(request) -> str:
"""
Returns the full raw request path (with original URL encoding), including the query string.
This is _not_ equal to request.url, since there the path section would be url-encoded while the query part will be
(partly) url-decoded.
"""
query_str = f"?{strings.to_str(request.query_string)}" if request.query_string else ""
raw_path = f"{get_raw_path(request)}{query_str}"
return raw_path
14 changes: 2 additions & 12 deletions localstack/services/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import threading
from typing import Dict

from quart import request as quart_request
from requests.models import Response

from localstack import config
Expand All @@ -24,7 +23,6 @@
from localstack.http import Router
from localstack.http.adapters import create_request_from_parts
from localstack.http.dispatcher import Handler, handler_dispatcher
from localstack.http.request import get_raw_path
from localstack.http.router import RegexConverter
from localstack.runtime import events
from localstack.services.generic_proxy import ProxyListener, modify_and_forward, start_proxy_server
Expand All @@ -39,7 +37,7 @@
from localstack.utils.net import is_port_open
from localstack.utils.run import is_root, run
from localstack.utils.server.http2_server import HTTPErrorResponse
from localstack.utils.strings import to_bytes, to_str, truncate
from localstack.utils.strings import to_bytes, truncate
from localstack.utils.sync import sleep_forever
from localstack.utils.threads import TMP_THREADS, start_thread

Expand Down Expand Up @@ -80,9 +78,8 @@ def forward_request(self, method, path, data, headers):
return 503

if config.EDGE_FORWARD_URL:
raw_path = self.get_full_raw_path(quart_request)
return do_forward_request_network(
0, method, raw_path, data, headers, target_url=config.EDGE_FORWARD_URL
0, method, path, data, headers, target_url=config.EDGE_FORWARD_URL
)

target = headers.get("x-amz-target", "")
Expand Down Expand Up @@ -222,13 +219,6 @@ def _require_service(self, api):
except Exception as e:
raise HTTPErrorResponse("failed to get service for %s: %s" % (api, e), code=500)

@staticmethod
def get_full_raw_path(request) -> str:
"""Returns the full raw request path (with original URL encoding), including the query string"""
query_str = f"?{to_str(request.query_string)}" if request.query_string else ""
raw_path = f"{get_raw_path(request)}{query_str}"
return raw_path


def do_forward_request(api, method, path, data, headers, port=None):
if config.FORWARD_EDGE_INMEM:
Expand Down
3 changes: 2 additions & 1 deletion localstack/services/generic_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
HEADER_LOCALSTACK_AUTHORIZATION,
HEADER_LOCALSTACK_REQUEST_URL,
)
from localstack.http.request import get_full_raw_path
from localstack.services.messages import Headers, MessagePayload
from localstack.services.messages import Request as RoutingRequest
from localstack.services.messages import Response as RoutingResponse
Expand Down Expand Up @@ -964,7 +965,7 @@ def start_proxy_server(

def handler(request, data):
parsed_url = urlparse(request.url)
path_with_params = request.full_path.strip("?")
path_with_params = get_full_raw_path(request)
method = request.method
headers = request.headers
headers[HEADER_LOCALSTACK_REQUEST_URL] = str(request.url)
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

os.environ["LOCALSTACK_INTERNAL_TEST_RUN"] = "1"

pytest_plugins = [
"localstack.testing.pytest.fixtures",
"localstack.testing.pytest.snapshot",
]


@pytest.hookimpl
def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager):
Expand Down
5 changes: 0 additions & 5 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@
# collection of functions that should be executed to initialize tests
test_init_functions = set()

pytest_plugins = [
"localstack.testing.pytest.fixtures",
"localstack.testing.pytest.snapshot",
]


@pytest.hookimpl()
def pytest_configure(config):
Expand Down
32 changes: 30 additions & 2 deletions tests/integration/test_edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from localstack import config
from localstack.constants import APPLICATION_JSON, HEADER_LOCALSTACK_EDGE_URL, TEST_AWS_ACCOUNT_ID
from localstack.services.edge import ProxyListenerEdge
from localstack.http.request import get_full_raw_path
from localstack.services.generic_proxy import (
MessageModifyingProxyListener,
ProxyListener,
Expand Down Expand Up @@ -160,6 +160,34 @@ def forward_request(self, method, path, data, headers):
assert json.loads(to_str(response.content)) == expected
proxy.stop()

def test_http2_relay_traffic(self):
"""Tests if HTTP2 traffic can correctly be forwarded (including url-encoded characters)."""

# Create a simple HTTP echo server
class MyListener(ProxyListener):
def forward_request(self, method, path, data, headers):
return {"method": method, "path": path, "data": data}

listener = MyListener()
port_http_server = get_free_tcp_port()
http_server = start_proxy_server(port_http_server, update_listener=listener, use_ssl=True)

# Create a relay proxy which forwards request to the HTTP echo server
port_relay_proxy = get_free_tcp_port()
forward_url = f"https://localhost:{port_http_server}/foo/bar%23baz"
relay_proxy = start_proxy_server(port_relay_proxy, forward_url=forward_url, use_ssl=True)

# Contact the relay proxy
url = f"https://localhost:{port_relay_proxy}/foo/bar%23baz"
response = requests.post(url, verify=False)

# Expect the response from the HTTP echo server
expected = {"method": "POST", "path": "/foo/bar%23baz", "data": ""}
assert json.loads(to_str(response.content)) == expected

http_server.stop()
relay_proxy.stop()

def test_invoke_sns_sqs_integration_using_edge_port(
self, sqs_create_queue, sqs_client, sns_client, sns_create_topic, sns_subscription
):
Expand Down Expand Up @@ -303,7 +331,7 @@ def test_request_with_custom_host_header(self):
def test_forward_raw_path(self, monkeypatch):
class MyListener(ProxyListener):
def forward_request(self, method, path, data, headers):
_path = ProxyListenerEdge.get_full_raw_path(quart_request)
_path = get_full_raw_path(quart_request)
return {"method": method, "path": _path}

# start listener and configure EDGE_FORWARD_URL
Expand Down

0 comments on commit fe610cf

Please sign in to comment.