diff --git a/dashboard/http_server_head.py b/dashboard/http_server_head.py index 0deffe5c8aad8..2b5aab3181521 100644 --- a/dashboard/http_server_head.py +++ b/dashboard/http_server_head.py @@ -146,6 +146,20 @@ async def path_clean_middleware(self, request, handler): raise aiohttp.web.HTTPForbidden() return await handler(request) + @aiohttp.web.middleware + async def browsers_no_post_put_middleware(self, request, handler): + if ( + # A best effort test for browser traffic. All common browsers + # start with Mozilla at the time of writing. + request.headers["User-Agent"].startswith("Mozilla") + and request.method in [hdrs.METH_POST, hdrs.METH_PUT] + ): + return aiohttp.web.Response( + status=405, text="Method Not Allowed for browser traffic." + ) + + return await handler(request) + @aiohttp.web.middleware async def metrics_middleware(self, request, handler): start_time = time.monotonic() @@ -185,7 +199,11 @@ async def run(self, modules): # working_dir uploads for job submission can be up to 100MiB. app = aiohttp.web.Application( client_max_size=100 * 1024**2, - middlewares=[self.metrics_middleware, self.path_clean_middleware], + middlewares=[ + self.metrics_middleware, + self.path_clean_middleware, + self.browsers_no_post_put_middleware, + ], ) app.add_routes(routes=routes.bound_routes()) diff --git a/dashboard/tests/test_dashboard.py b/dashboard/tests/test_dashboard.py index 2530e7d4b0112..c7b4a7d30b8f8 100644 --- a/dashboard/tests/test_dashboard.py +++ b/dashboard/tests/test_dashboard.py @@ -19,7 +19,7 @@ import ray.dashboard.modules import ray.dashboard.utils as dashboard_utils from click.testing import CliRunner -from requests.exceptions import ConnectionError +from requests.exceptions import ConnectionError, HTTPError from ray._private import ray_constants from ray._private.ray_constants import ( DEBUG_AUTOSCALING_ERROR, @@ -370,6 +370,54 @@ def test_http_get(enable_test_module, ray_start_with_dashboard): raise Exception("Timed out while testing.") +@pytest.mark.skipif( + os.environ.get("RAY_MINIMAL") == "1", + reason="This test is not supposed to work for minimal installation.", +) +def test_browser_no_post_no_put(enable_test_module, ray_start_with_dashboard): + assert wait_until_server_available(ray_start_with_dashboard["webui_url"]) is True + webui_url = ray_start_with_dashboard["webui_url"] + webui_url = format_web_url(webui_url) + + timeout_seconds = 30 + start_time = time.time() + while True: + time.sleep(3) + try: + # Starting and getting jobs should be fine from API clients + response = requests.post( + webui_url + "/api/jobs/", json={"entrypoint": "ls"} + ) + response.raise_for_status() + response = requests.get(webui_url + "/api/jobs/") + response.raise_for_status() + + # Starting job should be blocked for browsers + response = requests.post( + webui_url + "/api/jobs/", + json={"entrypoint": "ls"}, + headers={ + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/119.0.0.0 Safari/537.36" + ) + }, + ) + with pytest.raises(HTTPError): + response.raise_for_status() + + # Getting jobs should be fine for browsers + response = requests.get(webui_url + "/api/jobs/") + response.raise_for_status() + break + except (AssertionError, requests.exceptions.ConnectionError) as e: + logger.info("Retry because of %s", e) + finally: + if time.time() > start_time + timeout_seconds: + raise Exception("Timed out while testing.") + + @pytest.mark.skipif( os.environ.get("RAY_MINIMAL") == "1", reason="This test is not supposed to work for minimal installation.",