Skip to content
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

fix request encoding in generic proxy listener chain and forwarding #6070

Merged
merged 2 commits into from May 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions localstack/http/request.py
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
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
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
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
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
38 changes: 36 additions & 2 deletions tests/integration/test_edge.py
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,40 @@ 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}"
relay_proxy = start_proxy_server(port_relay_proxy, forward_url=forward_url, use_ssl=True)

# Contact the relay proxy
query = "%2B=%3B%2C%2F%3F%3A%40%26%3D%2B%24%21%2A%27%28%29%23"
path = f"/foo/bar%3B%2C%2F%3F%3A%40%26%3D%2B%24%21%2A%27%28%29%23baz?{query}"
url = f"https://localhost:{port_relay_proxy}{path}"
response = requests.post(url, verify=False)

# Expect the response from the HTTP echo server
expected = {
"method": "POST",
"path": path,
"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 +337,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