From 1f2af1f6c4c53b77a5b6c09eb0000dfebed38269 Mon Sep 17 00:00:00 2001 From: Hugo Chinchilla Date: Thu, 30 Apr 2020 08:40:53 +0200 Subject: [PATCH 1/4] Handle CORS requests --- config/services.yaml | 8 ++ public/index.php | 0 src/EventListener/CorsResponseListener.php | 52 +++++++++++ .../CorsResponseListenerTest.php | 91 +++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 public/index.php create mode 100644 src/EventListener/CorsResponseListener.php create mode 100644 tests/Unit/EventListener/CorsResponseListenerTest.php diff --git a/config/services.yaml b/config/services.yaml index 473b3d0..1401ae3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -3,6 +3,7 @@ parameters: env(SWAGGER_MOCK_CACHE_STRATEGY): 'disabled' env(SWAGGER_MOCK_CACHE_DIRECTORY): '/dev/shm/openapi-cache' env(SWAGGER_MOCK_CACHE_TTL): 0 + env(SWAGGER_MOCK_CORS_ENABLE): False locale: 'en' specification_url: '%env(SWAGGER_MOCK_SPECIFICATION_URL)%' @@ -10,6 +11,7 @@ parameters: cache_directory: '%env(SWAGGER_MOCK_CACHE_DIRECTORY)%' cache_ttl: '%env(SWAGGER_MOCK_CACHE_TTL)%' log_level: '%env(SWAGGER_MOCK_LOG_LEVEL)%' + cors_enable: '%env(SWAGGER_MOCK_CORS_ENABLE)%' type_parser_map: object: 'App\OpenAPI\Parsing\Type\Composite\ObjectTypeParser' @@ -63,6 +65,12 @@ services: tags: - { name: kernel.event_listener, event: kernel.request, priority: 48 } + App\EventListener\CorsResponseListener: + arguments: + - '%cors_enable%' + tags: + - { name: kernel.event_listener, event: kernel.response, priority: 46 } + App\Mock\EndpointRepository: arguments: - '@specification_loader' diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..e69de29 diff --git a/src/EventListener/CorsResponseListener.php b/src/EventListener/CorsResponseListener.php new file mode 100644 index 0000000..2d73514 --- /dev/null +++ b/src/EventListener/CorsResponseListener.php @@ -0,0 +1,52 @@ +cors_enable = $cors_enable; + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$this->cors_enable) { + return; + } + + $request = $event->getRequest(); + + if (!$request->isMethod('OPTIONS')) { + return; + } + + $response = $event->getResponse(); + + if ($response->getStatusCode() !== Response::HTTP_NOT_FOUND) { + return; + } + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin', '*')); + + $response->headers->set( + 'Access-Control-Allow-Methods', + $request->headers->get('Access-Control-Request-Method', 'GET,POST,PUT,DELETE') + ); + + if ($requestHeaders = $request->headers->get('Access-Control-Request-Headers')) { + $response->headers->set('Access-Control-Allow-Headers', $requestHeaders); + } + + $response->setContent(''); + $response->setStatusCode(Response::HTTP_NO_CONTENT); + } +} diff --git a/tests/Unit/EventListener/CorsResponseListenerTest.php b/tests/Unit/EventListener/CorsResponseListenerTest.php new file mode 100644 index 0000000..cc02c5c --- /dev/null +++ b/tests/Unit/EventListener/CorsResponseListenerTest.php @@ -0,0 +1,91 @@ +event = \Phake::mock(ResponseEvent::class); + } + + /** @test */ + public function onUnhandledCorsRequest_ifFeatureEnabled_handlesCors(): void + { + $listener = new CorsResponseListener(true); + $request = new Request(); + $request->setMethod('OPTIONS'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_NOT_FOUND); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode()); + $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET,POST,PUT,DELETE', $response->headers->get('Access-Control-Allow-Methods')); + $this->assertEquals('', $response->getContent()); + } + + /** @test */ + public function onHandledCorsRequest_ifFeatureEnabled_doesNothing(): void + { + $listener = new CorsResponseListener(true); + $request = new Request(); + $request->setMethod('OPTIONS'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_OK); + $response->setContent('something'); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); + $this->assertEquals('something', $response->getContent()); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Methods')); + } + + /** @test */ + public function onUnhandledCorsRequest_ifFeatureDisabled_doesNothing(): void + { + $listener = new CorsResponseListener(false); + $request = new Request(); + $request->setMethod('OPTIONS'); + $response = new Response(); + $response->setStatusCode(Response::HTTP_NOT_FOUND); + \Phake::when($this->event) + ->getRequest() + ->thenReturn($request); + \Phake::when($this->event) + ->getResponse() + ->thenReturn($response); + + $listener->onKernelResponse($this->event); + + $this->assertEquals(Response::HTTP_NOT_FOUND, $response->getStatusCode()); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('', $response->headers->get('Access-Control-Allow-Methods')); + } +} From 6198162f5068c38ba73916b97da2d416ff8874a8 Mon Sep 17 00:00:00 2001 From: Hugo Chinchilla Date: Thu, 30 Apr 2020 11:18:56 +0200 Subject: [PATCH 2/4] add CORS headers for all cross orign requests, not only OPTIONS --- src/EventListener/CorsResponseListener.php | 15 +++++++------ .../CorsResponseListenerTest.php | 22 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/EventListener/CorsResponseListener.php b/src/EventListener/CorsResponseListener.php index 2d73514..bd90d33 100644 --- a/src/EventListener/CorsResponseListener.php +++ b/src/EventListener/CorsResponseListener.php @@ -25,17 +25,14 @@ public function onKernelResponse(ResponseEvent $event): void $request = $event->getRequest(); - if (!$request->isMethod('OPTIONS')) { + if (!$request->headers->has('Origin')) { return; } $response = $event->getResponse(); - if ($response->getStatusCode() !== Response::HTTP_NOT_FOUND) { - return; - } - $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin', '*')); + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); $response->headers->set( 'Access-Control-Allow-Methods', @@ -46,7 +43,11 @@ public function onKernelResponse(ResponseEvent $event): void $response->headers->set('Access-Control-Allow-Headers', $requestHeaders); } - $response->setContent(''); - $response->setStatusCode(Response::HTTP_NO_CONTENT); + if ($request->isMethod('OPTIONS')) { + if ($response->getStatusCode() === Response::HTTP_NOT_FOUND) { + $response->setContent(''); + $response->setStatusCode(Response::HTTP_NO_CONTENT); + } + } } } diff --git a/tests/Unit/EventListener/CorsResponseListenerTest.php b/tests/Unit/EventListener/CorsResponseListenerTest.php index cc02c5c..c53ba1d 100644 --- a/tests/Unit/EventListener/CorsResponseListenerTest.php +++ b/tests/Unit/EventListener/CorsResponseListenerTest.php @@ -21,11 +21,12 @@ protected function setUp(): void } /** @test */ - public function onUnhandledCorsRequest_ifFeatureEnabled_handlesCors(): void + public function onUnhandledOptionsRequest_ifFeatureEnabled_handlesCors(): void { $listener = new CorsResponseListener(true); $request = new Request(); $request->setMethod('OPTIONS'); + $request->headers->set('Origin', 'http://example.tld'); $response = new Response(); $response->setStatusCode(Response::HTTP_NOT_FOUND); \Phake::when($this->event) @@ -38,20 +39,21 @@ public function onUnhandledCorsRequest_ifFeatureEnabled_handlesCors(): void $listener->onKernelResponse($this->event); $this->assertEquals(Response::HTTP_NO_CONTENT, $response->getStatusCode()); - $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('http://example.tld', $response->headers->get('Access-Control-Allow-Origin')); $this->assertEquals('GET,POST,PUT,DELETE', $response->headers->get('Access-Control-Allow-Methods')); $this->assertEquals('', $response->getContent()); } /** @test */ - public function onHandledCorsRequest_ifFeatureEnabled_doesNothing(): void + public function onCorsRequest_ifFeatureEnabled_addsCorsHeaders(): void { $listener = new CorsResponseListener(true); $request = new Request(); - $request->setMethod('OPTIONS'); + $request->setMethod('GET'); + $request->headers->set('Origin', 'http://example.tld'); $response = new Response(); $response->setStatusCode(Response::HTTP_OK); - $response->setContent('something'); + $response->setContent('a content'); \Phake::when($this->event) ->getRequest() ->thenReturn($request); @@ -62,17 +64,17 @@ public function onHandledCorsRequest_ifFeatureEnabled_doesNothing(): void $listener->onKernelResponse($this->event); $this->assertEquals(Response::HTTP_OK, $response->getStatusCode()); - $this->assertEquals('something', $response->getContent()); - $this->assertEquals('', $response->headers->get('Access-Control-Allow-Origin')); - $this->assertEquals('', $response->headers->get('Access-Control-Allow-Methods')); + $this->assertEquals('a content', $response->getContent()); + $this->assertEquals('http://example.tld', $response->headers->get('Access-Control-Allow-Origin')); + $this->assertEquals('GET,POST,PUT,DELETE', $response->headers->get('Access-Control-Allow-Methods')); } /** @test */ - public function onUnhandledCorsRequest_ifFeatureDisabled_doesNothing(): void + public function onCorsRequest_ifFeatureDisabled_doesNothing(): void { $listener = new CorsResponseListener(false); $request = new Request(); - $request->setMethod('OPTIONS'); + $request->headers->set('Origin', 'http://example.tld'); $response = new Response(); $response->setStatusCode(Response::HTTP_NOT_FOUND); \Phake::when($this->event) From 6f00369275bef2032efd5ced4270989a0b9e25e1 Mon Sep 17 00:00:00 2001 From: Hugo Chinchilla Date: Thu, 30 Apr 2020 11:20:55 +0200 Subject: [PATCH 3/4] fix issue with code style --- src/EventListener/CorsResponseListener.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EventListener/CorsResponseListener.php b/src/EventListener/CorsResponseListener.php index bd90d33..42cd2dc 100644 --- a/src/EventListener/CorsResponseListener.php +++ b/src/EventListener/CorsResponseListener.php @@ -31,7 +31,6 @@ public function onKernelResponse(ResponseEvent $event): void $response = $event->getResponse(); - $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); $response->headers->set( From 0fb3d8909e6b306fdaed47902829eb55e134c274 Mon Sep 17 00:00:00 2001 From: Hugo Chinchilla Carbonell Date: Sun, 3 May 2020 22:19:53 +0200 Subject: [PATCH 4/4] Document SWAGGER_MOCK_CORS_ENABLE in README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7688e72..fd26416 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ Mock server options can be set via environment variables. * _Default value_: `disabled` * _Possible values_: `disabled`, `url_md5`, `url_and_timestamp_md5` +#### SWAGGER_MOCK_CORS_ENABLE + + * When enabled, CORS request will automatically be handled + * _Default value_: `False` + * _Possible values_: `True` or `False` + ### Specification cache To speed up server response time you can use caching mechanism for OpenAPI. There are several caching strategies. Specific strategy can be set by environment variable `SWAGGER_MOCK_CACHE_STRATEGY`.