Skip to content

Commit 6b22ac4

Browse files
feat(cors): support setting Access-Control-Allow-Private-Network in CORSMiddleware (#2475)
* feat(cors): support for private networks in `CORSMiddleware` Adds support for sending `Access-Control-Allow-Private-Network` header in CORS preflight responses. Its disabled by default, setting `allow_private_network` to True will enable support for private network requests. Other two parameters are also optional, they can be considered as "metadata" (pna name & ID). BREAKING CHANGE: nothing, all additional arguments have default value. Feature itself will not be injected unconditionally. * chore: drop extra CORS options, adjust tests & news fragment accordingly * perf: switch conditions related `allow_private_network` in `process_response` method
1 parent cacaa90 commit 6b22ac4

3 files changed

Lines changed: 63 additions & 1 deletion

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added support for private network requests in CORS middleware.
2+
This is off by default and can be enabled by passing the keyword argument
3+
``allow_private_network=True`` to :class:`falcon.middleware.CORSMiddleware`
4+
during initialization.

falcon/middleware.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,25 @@ class CORSMiddleware(UniversalMiddlewareWithProcessResponse):
5050
The string ``'*'`` acts as a wildcard, matching every allowed origin,
5151
while ``None`` disallows all origins. This parameter takes effect only
5252
if the origin is allowed by the ``allow_origins`` argument.
53-
(Default ``None``).
53+
(default ``None``).
54+
allow_private_network (bool):
55+
If ``True``, the server includes the
56+
``Access-Control-Allow-Private-Network`` header in responses to
57+
CORS preflight (OPTIONS) requests. This indicates that the resource is
58+
willing to respond to requests from less-public IP address spaces
59+
(e.g., from public site to private device).
60+
(default ``False``).
5461
62+
See also:
63+
https://wicg.github.io/private-network-access/#private-network-request-heading
5564
"""
5665

5766
def __init__(
5867
self,
5968
allow_origins: Union[str, Iterable[str]] = '*',
6069
expose_headers: Optional[Union[str, Iterable[str]]] = None,
6170
allow_credentials: Optional[Union[str, Iterable[str]]] = None,
71+
allow_private_network: bool = False,
6272
):
6373
if allow_origins == '*':
6474
self.allow_origins = allow_origins
@@ -88,6 +98,7 @@ def __init__(
8898
'as a string literal, not inside an iterable.'
8999
)
90100
self.allow_credentials = allow_credentials
101+
self.allow_private_network = allow_private_network
91102

92103
def process_response(
93104
self, req: Request, resp: Response, resource: object, req_succeeded: bool
@@ -146,6 +157,11 @@ def process_response(
146157
resp.set_header('Access-Control-Allow-Headers', allow_headers)
147158
resp.set_header('Access-Control-Max-Age', '86400') # 24 hours
148159

160+
if self.allow_private_network and (
161+
req.get_header('Access-Control-Request-Private-Network') == 'true'
162+
):
163+
resp.set_header('Access-Control-Allow-Private-Network', 'true')
164+
149165
async def process_response_async(
150166
self,
151167
req: AsgiRequest,

tests/test_cors_middleware.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,32 @@ def my_sink(req, resp):
161161
assert 'Access-Control-Expose-Headers' not in result.headers
162162
assert 'Access-Control-Allow-Origin' not in result.headers
163163

164+
@pytest.mark.parametrize('include_request_private_network', (True, False))
165+
def test_disabled_cors_private_network(
166+
self, cors_client, include_request_private_network
167+
):
168+
# default scenario for cors middleware, where
169+
# allow private network is off by default
170+
171+
cors_client.app.add_route('/', CORSHeaderResource())
172+
173+
headers = (
174+
('Origin', 'localhost'),
175+
('Access-Control-Request-Method', 'GET'),
176+
)
177+
178+
if include_request_private_network:
179+
headers = (
180+
*headers,
181+
('Access-Control-Request-Private-Network', 'true'),
182+
)
183+
184+
result = cors_client.simulate_options('/', headers=headers)
185+
186+
h = result.headers
187+
188+
assert 'Access-Control-Allow-Private-Network' not in h
189+
164190

165191
@pytest.fixture(scope='function')
166192
def make_cors_client(asgi, util):
@@ -293,3 +319,19 @@ def test_expose_headers(self, make_cors_client, attr, exp):
293319
assert res.headers['Access-Control-Expose-Headers'] == exp
294320
h = dict(res.headers.lower_items()).keys()
295321
assert 'Access-Control-Allow-Credentials'.lower() not in h
322+
323+
def test_enabled_cors_private_network_headers(self, make_cors_client):
324+
client = make_cors_client(falcon.CORSMiddleware(allow_private_network=True))
325+
326+
client.app.add_route('/', CORSHeaderResource())
327+
328+
res = client.simulate_options(
329+
'/',
330+
headers=(
331+
('Origin', 'localhost'),
332+
('Access-Control-Request-Method', 'GET'),
333+
('Access-Control-Request-Private-Network', 'true'),
334+
),
335+
)
336+
337+
assert res.headers['Access-Control-Allow-Private-Network'] == 'true'

0 commit comments

Comments
 (0)