From e5a6ef7a7e78d81ad657d712f0608dd1a4b589d5 Mon Sep 17 00:00:00 2001 From: Konrad Rokicki Date: Tue, 23 Jun 2026 14:35:37 -0400 Subject: [PATCH] feat(cors): grant Private Network Access preflights for /files/ data links Chromium (Chrome/Edge) sends a CORS preflight before any request from a public-origin page (e.g. https://neuroglancer-demo.appspot.com) to a private-network address, and only proceeds if the response echoes `Access-Control-Allow-Private-Network: true`. Starlette's CORSMiddleware does not emit this, so Chromium blocks cross-origin viewers (Neuroglancer/N5/Vizarr) from loading data hosted on an internal host. Add a pure-ASGI PrivateNetworkAccessMiddleware that echoes the grant header on preflights carrying `Access-Control-Request-Private-Network: true`, registered outside CORSMiddleware so it wraps the preflight response. Note: this does not affect Firefox, which uses a user-permission model (Local Network Access) rather than this header. Co-Authored-By: Claude Opus 4.8 (1M context) --- fileglancer/server.py | 54 +++++++++++++++++++++++++++++++++++++++++ tests/test_endpoints.py | 22 +++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/fileglancer/server.py b/fileglancer/server.py index afd849b8..18f689f1 100644 --- a/fileglancer/server.py +++ b/fileglancer/server.py @@ -87,6 +87,56 @@ async def send_with_request_id(message): await self.app(scope, receive, send_with_request_id) +class PrivateNetworkAccessMiddleware: + """Pure ASGI middleware that grants browser Private Network Access (PNA) + preflights. + + Chromium browsers (Chrome/Edge) send a CORS preflight before any request from + a public-origin page (e.g. https://neuroglancer-demo.appspot.com) to a + private-network address (e.g. an internal host serving Fileglancer's /files/ + data links). The preflight carries `Access-Control-Request-Private-Network: true`, + and the request only proceeds if the response echoes + `Access-Control-Allow-Private-Network: true`. Starlette's CORSMiddleware does not + emit this header, so without it Chromium blocks cross-origin viewers + (Neuroglancer/N5/Vizarr) from loading data hosted on an internal network. + + (Firefox uses a separate user-permission model -- Local Network Access -- rather + than this header, so this neither helps nor harms Firefox.) + + Registered outside CORSMiddleware so it can append the header to the preflight + response that CORSMiddleware generates. Implemented as pure ASGI so it only + touches response headers without re-wrapping the body. The header is added only + when the PNA request header is present, which the browser sends solely on + preflights, so it never appears on normal data responses. + """ + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + # ASGI lowercases header names; the request header value is the ASCII "true". + requested = any( + name == b"access-control-request-private-network" + and value.strip().lower() == b"true" + for name, value in scope.get("headers", []) + ) + if not requested: + await self.app(scope, receive, send) + return + + async def send_with_pna(message): + if message["type"] == "http.response.start": + headers = message.setdefault("headers", []) + headers.append((b"access-control-allow-private-network", b"true")) + await send(message) + + await self.app(scope, receive, send_with_pna) + + # Read version once at module load time def _read_version() -> str: """Read version from package metadata or package.json file""" @@ -540,6 +590,10 @@ def mask_password(url: str) -> str: expose_headers=["Range", "Content-Range", "x-amz-request-id"], ) + # Echo Access-Control-Allow-Private-Network on PNA preflights. Added after + # (i.e. outside) CORSMiddleware so it wraps the preflight response CORS emits. + app.add_middleware(PrivateNetworkAccessMiddleware) + @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index a290f90d..ab43517b 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -132,6 +132,28 @@ def test_version_endpoint(test_client): assert isinstance(data["version"], str) +def test_pna_preflight_grants_private_network(test_client): + """A CORS preflight carrying Access-Control-Request-Private-Network must be + answered with Access-Control-Allow-Private-Network: true so Chromium permits + public-origin viewers (e.g. Neuroglancer) to load data from an internal host.""" + response = test_client.options( + "/files/somekey/some.zarr/.zattrs", + headers={ + "Origin": "https://neuroglancer-demo.appspot.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Private-Network": "true", + }, + ) + assert response.headers.get("access-control-allow-private-network") == "true" + + +def test_pna_header_absent_without_request(test_client): + """The PNA grant header must not leak onto responses that did not ask for it.""" + response = test_client.get("/api/version") + assert response.status_code == 200 + assert "access-control-allow-private-network" not in response.headers + + def test_root_endpoint(test_client): """Test root endpoint - should serve SPA index.html""" response = test_client.get("/", follow_redirects=False)