Skip to content
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
54 changes: 54 additions & 0 deletions fileglancer/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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):
Expand Down
22 changes: 22 additions & 0 deletions tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading