diff --git a/falcon/middleware.py b/falcon/middleware.py index d457a44b8..c8767fa2a 100644 --- a/falcon/middleware.py +++ b/falcon/middleware.py @@ -17,15 +17,6 @@ class CORSMiddleware(object): * https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS * https://www.w3.org/TR/cors/#resource-processing-model - Note: - Falcon will automatically add OPTIONS responders if they are missing from the - responder instances added to the routes. When providing a custom ``on_options`` - method, the ``Allow`` headers in the response should be set to the allowed - method values. If the ``Allow`` header is missing from the response, - this middleware will deny the preflight request. - - This is also valid when using a sink function. - Keyword Arguments: allow_origins (Union[str, Iterable[str]]): List of origins to allow (case sensitive). The string ``'*'`` acts as a wildcard, matching every origin. @@ -54,7 +45,9 @@ def __init__( allow_origins: Union[str, Iterable[str]] = '*', expose_headers: Optional[Union[str, Iterable[str]]] = None, allow_credentials: Optional[Union[str, Iterable[str]]] = None, + allow_private_network: bool = False, ): + if allow_origins == '*': self.allow_origins = allow_origins else: @@ -84,6 +77,8 @@ def __init__( ) self.allow_credentials = allow_credentials + self.allow_private_network = allow_private_network + def process_response( self, req: Request, resp: Response, resource: object, req_succeeded: bool ) -> None: @@ -129,17 +124,13 @@ def process_response( 'Access-Control-Request-Headers', default='*' ) - if allow is None: - # there is no allow set, remove all access control headers - resp.delete_header('Access-Control-Allow-Methods') - resp.delete_header('Access-Control-Allow-Headers') - resp.delete_header('Access-Control-Max-Age') - resp.delete_header('Access-Control-Expose-Headers') - resp.delete_header('Access-Control-Allow-Origin') - else: - resp.set_header('Access-Control-Allow-Methods', allow) - resp.set_header('Access-Control-Allow-Headers', allow_headers) - resp.set_header('Access-Control-Max-Age', '86400') # 24 hours + resp.set_header('Access-Control-Allow-Methods', allow) + resp.set_header('Access-Control-Allow-Headers', allow_headers) + resp.set_header('Access-Control-Max-Age', '86400') # 24 hours + + if self.allow_private_network and req.get_header('Access-Control-Request-Private-Network') == 'true': + resp.set_header('Access-Control-Allow-Private-Network', 'true') + async def process_response_async(self, *args: Any) -> None: self.process_response(*args) diff --git a/tests/test_cors_middleware.py b/tests/test_cors_middleware.py index 9aff6abf6..70f5e6863 100644 --- a/tests/test_cors_middleware.py +++ b/tests/test_cors_middleware.py @@ -293,3 +293,68 @@ def test_expose_headers(self, make_cors_client, attr, exp): assert res.headers['Access-Control-Expose-Headers'] == exp h = dict(res.headers.lower_items()).keys() assert 'Access-Control-Allow-Credentials'.lower() not in h + + def test_allow_private_network(self, make_cors_client): + # Create a client with allow_private_network=True + client = make_cors_client(falcon.CORSMiddleware(allow_private_network=True)) + client.app.add_route('/', CORSHeaderResource()) + + # Preflight request with Access-Control-Request-Private-Network: true + result = client.simulate_options( + '/', + headers={ + 'Origin': 'localhost', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'X-PINGOTHER, Content-Type', + 'Access-Control-Request-Private-Network': 'true', + } + ) + assert result.headers['Access-Control-Allow-Private-Network'] == 'true' + + # Preflight request without Access-Control-Request-Private-Network + result = client.simulate_options( + '/', + headers={ + 'Origin': 'localhost', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'X-PINGOTHER, Content-Type', + } + ) + assert 'Access-Control-Allow-Private-Network' not in result.headers + + # Preflight request with Access-Control-Request-Private-Network: 'false' + result = client.simulate_options( + '/', + headers={ + 'Origin': 'localhost', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'X-PINGOTHER, Content-Type', + 'Access-Control-Request-Private-Network': 'false', + } + ) + assert 'Access-Control-Allow-Private-Network' not in result.headers + + # Non-preflight request + result = client.simulate_get( + '/', + headers={ + 'Origin': 'localhost', + } + ) + assert 'Access-Control-Allow-Private-Network' not in result.headers + + # Create a client with allow_private_network=False + client = make_cors_client(falcon.CORSMiddleware(allow_private_network=False)) + client.app.add_route('/', CORSHeaderResource()) + + # Preflight request with Access-Control-Request-Private-Network: true + result = client.simulate_options( + '/', + headers={ + 'Origin': 'localhost', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'X-PINGOTHER, Content-Type', + 'Access-Control-Request-Private-Network': 'true', + } + ) + assert 'Access-Control-Allow-Private-Network' not in result.headers