From 513404b8112524de78002c2a84c2540ba0f76e92 Mon Sep 17 00:00:00 2001
From: Joost de Bruijn <joostdebruijn1993@gmail.com>
Date: Wed, 3 Aug 2022 15:08:04 +0200
Subject: [PATCH] feat: handle private network requests

---
 README.md                 |  4 +++-
 src/CorsService.php       | 14 ++++++++++++++
 tests/CorsServiceTest.php | 14 ++++++++++++++
 tests/CorsTest.php        | 38 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index b0a0d11..f223cd8 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ This package can be used as a library. You can use it in your framework using:
 
  - [Stack middleware](http://stackphp.com/): https://github.com/asm89/stack-cors
  - [Laravel](https://laravel.com): https://github.com/fruitcake/laravel-cors
- 
+
 
 ### Options
 
@@ -38,6 +38,7 @@ This package can be used as a library. You can use it in your framework using:
 | exposedHeaders         | Sets the Access-Control-Expose-Headers response header.    | `[]`          |
 | maxAge                 | Sets the Access-Control-Max-Age response header.           | `0`           |
 | supportsCredentials    | Sets the Access-Control-Allow-Credentials header.          | `false`       |
+| allowPrivateNetwork    | Sets the Access-Control-Allow-Private-Network header.      | `false`       |
 
 The _allowedMethods_ and _allowedHeaders_ options are case-insensitive.
 
@@ -62,6 +63,7 @@ $cors = new CorsService([
     'exposedHeaders'         => ['Content-Encoding'],
     'maxAge'                 => 0,
     'supportsCredentials'    => false,
+    'allowPrivateNetwork'    => false,
 ]);
 
 $cors->addActualRequestHeaders(Response $response, $origin);
diff --git a/src/CorsService.php b/src/CorsService.php
index 35a3f73..2fde8c5 100644
--- a/src/CorsService.php
+++ b/src/CorsService.php
@@ -21,6 +21,7 @@
  *  'allowedOrigins'?: string[],
  *  'allowedOriginsPatterns'?: string[],
  *  'supportsCredentials'?: bool,
+ *  'allowPrivateNetwork'? : bool,
  *  'allowedHeaders'?: string[],
  *  'allowedMethods'?: string[],
  *  'exposedHeaders'?: string[]|false,
@@ -28,6 +29,7 @@
  *  'allowed_origins'?: string[],
  *  'allowed_origins_patterns'?: string[],
  *  'supports_credentials'?: bool,
+ *  'allow_private_network'? : bool,
  *  'allowed_headers'?: string[],
  *  'allowed_methods'?: string[],
  *  'exposed_headers'?: string[]|false,
@@ -48,6 +50,7 @@ class CorsService
     /** @var string[] */
     private array $exposedHeaders = [];
     private bool $supportsCredentials = false;
+    private bool $allowPrivateNetwork = false;
     private ?int $maxAge = 0;
 
     private bool $allowAllOrigins = false;
@@ -76,6 +79,8 @@ public function setOptions(array $options): void
         $this->allowedHeaders = $options['allowedHeaders'] ?? $options['allowed_headers'] ?? $this->allowedHeaders;
         $this->supportsCredentials =
             $options['supportsCredentials'] ?? $options['supports_credentials'] ?? $this->supportsCredentials;
+        $this->allowPrivateNetwork =
+            $options['allowPrivateNetwork'] ?? $options['allow_private_network'] ?? $this->allowPrivateNetwork;
 
         $maxAge = $this->maxAge;
         if (array_key_exists('maxAge', $options)) {
@@ -162,6 +167,8 @@ public function addPreflightRequestHeaders(Response $response, Request $request)
             $this->configureAllowedHeaders($response, $request);
 
             $this->configureMaxAge($response, $request);
+
+            $this->configurePrivateNetwork($response, $request);
         }
 
         return $response;
@@ -258,6 +265,13 @@ private function configureAllowCredentials(Response $response, Request $request)
         }
     }
 
+    private function configurePrivateNetwork(Response $response, Request $request): void
+    {
+        if ($request->headers->get('Access-Control-Request-Private-Network') === 'true' && $this->allowPrivateNetwork) {
+            $response->headers->set('Access-Control-Allow-Private-Network', 'true');
+        }
+    }
+
     private function configureExposedHeaders(Response $response, Request $request): void
     {
         if ($this->exposedHeaders) {
diff --git a/tests/CorsServiceTest.php b/tests/CorsServiceTest.php
index ee6957a..c771b6b 100644
--- a/tests/CorsServiceTest.php
+++ b/tests/CorsServiceTest.php
@@ -21,6 +21,7 @@
  *  'allowedOrigins': string[],
  *  'allowedOriginsPatterns': string[],
  *  'supportsCredentials': bool,
+ *  'allowPrivateNetwork' : bool,
  *  'allowedHeaders': string[],
  *  'allowedMethods': string[],
  *  'exposedHeaders': string[],
@@ -44,6 +45,7 @@ public function itCanHaveOptions(): void
             'allowedMethods' => ['PUT'],
             'maxAge' => 684,
             'supportsCredentials' => true,
+            'allowPrivateNetwork' => true,
             'exposedHeaders' => ['x-custom-2'],
         ];
 
@@ -59,6 +61,7 @@ public function itCanHaveOptions(): void
         $this->assertEquals($options['allowedMethods'], $normalized['allowedMethods']);
         $this->assertEquals($options['maxAge'], $normalized['maxAge']);
         $this->assertEquals($options['supportsCredentials'], $normalized['supportsCredentials']);
+        $this->assertEquals($options['allowPrivateNetwork'], $normalized['allowPrivateNetwork']);
         $this->assertEquals($options['exposedHeaders'], $normalized['exposedHeaders']);
     }
 
@@ -80,6 +83,7 @@ public function itCanSetOptions(): void
             'allowedMethods' => ['PUT'],
             'maxAge' => 684,
             'supportsCredentials' => true,
+            'allowPrivateNetwork' => true,
             'exposedHeaders' => ['x-custom-2'],
         ];
 
@@ -93,6 +97,7 @@ public function itCanSetOptions(): void
         $this->assertEquals($options['allowedMethods'], $normalized['allowedMethods']);
         $this->assertEquals($options['maxAge'], $normalized['maxAge']);
         $this->assertEquals($options['supportsCredentials'], $normalized['supportsCredentials']);
+        $this->assertEquals($options['allowPrivateNetwork'], $normalized['allowPrivateNetwork']);
         $this->assertEquals($options['exposedHeaders'], $normalized['exposedHeaders']);
     }
 
@@ -115,6 +120,7 @@ public function itCanOverwriteSetOptions(): void
             'allowedMethods' => ['PUT'],
             'maxAge' => 684,
             'supportsCredentials' => true,
+            'allowPrivateNetwork' => true,
             'exposedHeaders' => ['x-custom-2'],
         ];
 
@@ -128,6 +134,7 @@ public function itCanOverwriteSetOptions(): void
         $this->assertEquals($options['allowedMethods'], $normalized['allowedMethods']);
         $this->assertEquals($options['maxAge'], $normalized['maxAge']);
         $this->assertEquals($options['supportsCredentials'], $normalized['supportsCredentials']);
+        $this->assertEquals($options['allowPrivateNetwork'], $normalized['allowPrivateNetwork']);
         $this->assertEquals($options['exposedHeaders'], $normalized['exposedHeaders']);
     }
 
@@ -148,6 +155,7 @@ public function itCanHaveNoOptions(): void
         $this->assertEquals([], $normalized['exposedHeaders']);
         $this->assertEquals(0, $normalized['maxAge']);
         $this->assertEquals(false, $normalized['supportsCredentials']);
+        $this->assertEquals(false, $normalized['allowPrivateNetwork']);
     }
 
     /**
@@ -167,6 +175,7 @@ public function itCanHaveEmptyOptions(): void
         $this->assertEquals([], $normalized['exposedHeaders']);
         $this->assertEquals(0, $normalized['maxAge']);
         $this->assertEquals(false, $normalized['supportsCredentials']);
+        $this->assertEquals(false, $normalized['allowPrivateNetwork']);
     }
 
     /**
@@ -275,6 +284,7 @@ public function itNormalizesUnderscoreOptions(): void
             'allowed_methods' => ['PUT'],
             'max_age' => 684,
             'supports_credentials' => true,
+            'allow_private_network' => true,
             'exposed_headers' => ['x-custom-2'],
         ];
 
@@ -294,6 +304,10 @@ public function itNormalizesUnderscoreOptions(): void
             $options['supports_credentials'],
             $this->getOptionsFromService($service)['supportsCredentials']
         );
+        $this->assertEquals(
+            $options['allow_private_network'],
+            $this->getOptionsFromService($service)['allowPrivateNetwork']
+        );
     }
 
     /**
diff --git a/tests/CorsTest.php b/tests/CorsTest.php
index 8838a32..a8486fa 100644
--- a/tests/CorsTest.php
+++ b/tests/CorsTest.php
@@ -535,6 +535,44 @@ public function itDoesntSetAccessControlAllowOriginWithoutOrigin(): void
         $this->assertFalse($response->headers->has('Access-Control-Allow-Origin'));
     }
 
+    /**
+     * @test
+     */
+    public function itSetsAllowPrivateNetworkWhenAllowed(): void
+    {
+        $app     = $this->createStackedApp(array('allowPrivateNetwork' => true));
+        $request = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Private-Network', 'true');
+
+        $response = $app->handle($request);
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Private-Network'));
+    }
+
+    /**
+     * @test
+     */
+    public function itDoesntSetAllowPrivateNetworkWhenNotAllowed(): void
+    {
+        $app     = $this->createStackedApp(array('allowPrivateNetwork' => false));
+        $request = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Private-Network', 'true');
+
+        $response = $app->handle($request);
+        $this->assertFalse($response->headers->has('Access-Control-Allow-Private-Network'));
+    }
+
+    /**
+     * @test
+     */
+    public function itDoesntSetAllowPrivateNetworkWhenNotRequested(): void
+    {
+        $app     = $this->createStackedApp(array('allowPrivateNetwork' => true));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+        $this->assertFalse($response->headers->has('Access-Control-Allow-Private-Network'));
+    }
+
     private function createValidActualRequest(): Request
     {
         $request  = new Request();