This repository has been archived by the owner on Jun 29, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
3 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
<?php | ||
|
||
|
||
namespace Yiisoft\Yii\Web\Middleware; | ||
|
||
|
||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\MiddlewareInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
|
||
/** | ||
* Basic network resolver updates an instance of server request with protocol from special headers. | ||
* | ||
* It can be used in the following cases: | ||
* - not required IP resolve to access the user's IP | ||
* - user's IP is already resolved (eg `ngx_http_realip_module` or similar) | ||
*/ | ||
class BasicNetworkResolver implements MiddlewareInterface | ||
{ | ||
private const DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES = [ | ||
'http' => ['http'], | ||
'https' => ['https', 'on'], | ||
]; | ||
|
||
private $protocolHeaders = []; | ||
|
||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface | ||
{ | ||
$newScheme = null; | ||
foreach ($this->protocolHeaders as $header => $data) { | ||
if (!$request->hasHeader($header)) { | ||
continue; | ||
} | ||
$headerValues = $request->getHeader($header); | ||
if (is_callable($data)) { | ||
$newScheme = $data($headerValues, $header, $request); | ||
if ($newScheme === null) { | ||
continue; | ||
} | ||
if (!is_string($newScheme)) { | ||
throw new \RuntimeException('The scheme is neither string nor null!'); | ||
} | ||
if ($newScheme === '') { | ||
throw new \RuntimeException('The scheme cannot be an empty string!'); | ||
} | ||
break; | ||
} | ||
$headerValue = strtolower($headerValues[0]); | ||
foreach ($data as $protocol => $acceptedValues) { | ||
if (!in_array($headerValue, $acceptedValues, true)) { | ||
continue; | ||
} | ||
$newScheme = $protocol; | ||
break 2; | ||
} | ||
} | ||
$uri = $request->getUri(); | ||
if ($newScheme !== null && $newScheme !== $uri->getScheme()) { | ||
$request = $request->withUri($uri->withScheme($newScheme)); | ||
} | ||
return $handler->handle($request); | ||
} | ||
|
||
/** | ||
* With added header to check for determining whether the connection is made via HTTP or HTTPS (or any protocol). | ||
* | ||
* The match of header names and values is case-insensitive. | ||
* It's not advisable to put insecure/untrusted headers here. | ||
* | ||
* Accepted types of values: | ||
* - NULL (default): {{DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES}} | ||
* - callable: custom function for getting the protocol | ||
* ```php | ||
* ->withProtocolHeader('x-forwarded-proto', function(array $values, string $header, ServerRequestInterface $request) { | ||
* return $values[0] === 'https' ? 'https' : 'http'; | ||
* return null; // If it doesn't make sense. | ||
* }); | ||
* ``` | ||
* - array: The array keys are protocol string and the array value is a list of header values that indicate the protocol. | ||
* ```php | ||
* ->withProtocolHeader('x-forwarded-proto', [ | ||
* 'http' => ['http'], | ||
* 'https' => ['https'] | ||
* ]); | ||
* ``` | ||
* @param string $header | ||
* @param callable|array|null $values | ||
* @see DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES | ||
*/ | ||
public function withAddedProtocolHeader(string $header, $values = null): self | ||
{ | ||
$new = clone $this; | ||
$header = strtolower($header); | ||
if ($values === null) { | ||
$new->protocolHeaders[$header] = self::DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES; | ||
return $new; | ||
} | ||
if (is_callable($values)) { | ||
$new->protocolHeaders[$header] = $values; | ||
return $new; | ||
} | ||
if (!is_array($values)) { | ||
throw new \RuntimeException('Accepted values is not array nor callable!'); | ||
} | ||
if (count($values) === 0) { | ||
throw new \RuntimeException('Accepted values cannot be an empty array!'); | ||
} | ||
$new->protocolHeaders[$header] = []; | ||
foreach ($values as $protocol => $acceptedValues) { | ||
if (!is_string($protocol)) { | ||
throw new \RuntimeException('The protocol must be type of string!'); | ||
} | ||
if ($protocol === '') { | ||
throw new \RuntimeException('The protocol cannot be an empty string!'); | ||
} | ||
$new->protocolHeaders[$header][$protocol] = array_map('strtolower', (array)$acceptedValues); | ||
} | ||
return $new; | ||
} | ||
|
||
public function withoutProtocolHeader(string $header): self | ||
{ | ||
$new = clone $this; | ||
unset($new->protocolHeaders[strtolower($header)]); | ||
return $new; | ||
} | ||
|
||
public function withoutProtocolHeaders(?array $headers = null): self | ||
{ | ||
$new = clone $this; | ||
if ($headers === null) { | ||
$new->protocolHeaders = []; | ||
} else { | ||
foreach ($headers as $header) { | ||
$new = $new->withoutProtocolHeader($header); | ||
} | ||
} | ||
return $new; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
<?php | ||
|
||
|
||
namespace Yiisoft\Yii\Web\Tests\Middleware; | ||
|
||
use Nyholm\Psr7\ServerRequest; | ||
use PHPUnit\Framework\TestCase; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Yiisoft\Yii\Web\Middleware\BasicNetworkResolver; | ||
use Yiisoft\Yii\Web\Tests\Middleware\Mock\MockRequestHandler; | ||
|
||
class BasicNetworkResolverTest extends TestCase | ||
{ | ||
|
||
public function schemeDataProvider(): array | ||
{ | ||
return [ | ||
'httpNotModify' => ['http', [], null, 'http'], | ||
'httpsNotModify' => ['https', [], null, 'https'], | ||
'httpNotMatchedProtocolHeader' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['test' => ['https' => 'https']], | ||
'http' | ||
], | ||
'httpNotMatchedProtocolHeaderValue' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['x-forwarded-proto' => ['https' => 'test']], | ||
'http' | ||
], | ||
'httpToHttps' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['x-forwarded-proto' => ['https' => 'https']], | ||
'https' | ||
], | ||
'httpToHttpsDefault' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['x-forwarded-proto' => null], | ||
'https' | ||
], | ||
'httpToHttpsUpperCase' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['x-forwarded-proto' => ['https' => 'HTTPS']], | ||
'https' | ||
], | ||
'httpToHttpsMultiValue' => [ | ||
'http', | ||
['x-forwarded-proto' => ['https']], | ||
['x-forwarded-proto' => ['https' => ['on', 's', 'https']]], | ||
'https' | ||
], | ||
'httpsToHttp' => [ | ||
'https', | ||
['x-forwarded-proto' => 'http'], | ||
['x-forwarded-proto' => ['http' => 'http']], | ||
'http' | ||
], | ||
'httpToHttpsWithCallback' => [ | ||
'http', | ||
['x-forwarded-proto' => 'test any-https **'], | ||
[ | ||
'x-forwarded-proto' => function (array $values, String $header, ServerRequestInterface $request) { | ||
return stripos($values[0], 'https') !== false ? 'https' : 'http'; | ||
}, | ||
], | ||
'https', | ||
], | ||
'httpWithCallbackNull' => [ | ||
'http', | ||
['x-forwarded-proto' => 'test any-https **'], | ||
[ | ||
'x-forwarded-proto' => function (array $values, String $header, ServerRequestInterface $request) { | ||
return null; | ||
}, | ||
], | ||
'http', | ||
] | ||
]; | ||
} | ||
|
||
protected function newRequestWithSchemaAndHeaders( | ||
string $scheme = 'http', | ||
array $headers = [] | ||
): ServerRequestInterface { | ||
$request = new ServerRequest('GET', '/', $headers); | ||
$uri = $request->getUri()->withScheme($scheme); | ||
return $request->withUri($uri); | ||
} | ||
|
||
/** | ||
* @dataProvider schemeDataProvider | ||
*/ | ||
public function testScheme(string $scheme, array $headers, ?array $protocolHeaders, string $expectedScheme): void | ||
{ | ||
$request = $this->newRequestWithSchemaAndHeaders($scheme, $headers); | ||
$requestHandler = new MockRequestHandler(); | ||
|
||
$middleware = new BasicNetworkResolver(); | ||
if ($protocolHeaders !== null) { | ||
foreach ($protocolHeaders as $header => $values) { | ||
$middleware = $middleware->withAddedProtocolHeader($header, $values); | ||
} | ||
} | ||
$middleware->process($request, $requestHandler); | ||
$resultRequest = $requestHandler->processedRequest; | ||
/* @var $resultRequest ServerRequestInterface */ | ||
$this->assertSame($expectedScheme, $resultRequest->getUri()->getScheme()); | ||
} | ||
|
||
public function testWithoutProtocolHeaders(): void | ||
{ | ||
$request = $this->newRequestWithSchemaAndHeaders('http', [ | ||
'x-forwarded-proto' => ['https'], | ||
]); | ||
$requestHandler = new MockRequestHandler(); | ||
|
||
$middleware = (new BasicNetworkResolver()) | ||
->withAddedProtocolHeader('x-forwarded-proto') | ||
->withoutProtocolHeaders(); | ||
$middleware->process($request, $requestHandler); | ||
$resultRequest = $requestHandler->processedRequest; | ||
/* @var $resultRequest ServerRequestInterface */ | ||
$this->assertSame('http', $resultRequest->getUri()->getScheme()); | ||
} | ||
|
||
public function testWithoutProtocolHeadersMulti(): void | ||
{ | ||
$request = $this->newRequestWithSchemaAndHeaders('http', [ | ||
'x-forwarded-proto' => ['https'], | ||
'x-forwarded-proto-2' => ['https'], | ||
]); | ||
$requestHandler = new MockRequestHandler(); | ||
|
||
$middleware = (new BasicNetworkResolver()) | ||
->withAddedProtocolHeader('x-forwarded-proto') | ||
->withAddedProtocolHeader('x-forwarded-proto-2') | ||
->withoutProtocolHeaders([ | ||
'x-forwarded-proto', | ||
'x-forwarded-proto-2', | ||
]); | ||
$middleware->process($request, $requestHandler); | ||
$resultRequest = $requestHandler->processedRequest; | ||
/* @var $resultRequest ServerRequestInterface */ | ||
$this->assertSame('http', $resultRequest->getUri()->getScheme()); | ||
} | ||
|
||
public function testWithoutProtocolHeader(): void | ||
{ | ||
$request = $this->newRequestWithSchemaAndHeaders('https', [ | ||
'x-forwarded-proto' => ['https'], | ||
'x-forwarded-proto-2' => ['http'], | ||
]); | ||
$requestHandler = new MockRequestHandler(); | ||
|
||
$middleware = (new BasicNetworkResolver()) | ||
->withAddedProtocolHeader('x-forwarded-proto') | ||
->withAddedProtocolHeader('x-forwarded-proto-2') | ||
->withoutProtocolHeader('x-forwarded-proto'); | ||
$middleware->process($request, $requestHandler); | ||
$resultRequest = $requestHandler->processedRequest; | ||
/* @var $resultRequest ServerRequestInterface */ | ||
$this->assertSame('http', $resultRequest->getUri()->getScheme()); | ||
|
||
$middleware = $middleware->withoutProtocolHeader('x-forwarded-proto-2'); | ||
$middleware->process($request, $requestHandler); | ||
$resultRequest = $requestHandler->processedRequest; | ||
/* @var $resultRequest ServerRequestInterface */ | ||
$this->assertSame('https', $resultRequest->getUri()->getScheme()); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
|
||
namespace Yiisoft\Yii\Web\Tests\Middleware\Mock; | ||
|
||
|
||
use Nyholm\Psr7\Response; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\ServerRequestInterface; | ||
use Psr\Http\Server\RequestHandlerInterface; | ||
|
||
class MockRequestHandler implements RequestHandlerInterface | ||
{ | ||
/** | ||
* @var ServerRequestInterface | ||
*/ | ||
public $processedRequest; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $responseStatus; | ||
|
||
public function __construct(int $responseStatus = 200) | ||
{ | ||
$this->responseStatus = $responseStatus; | ||
} | ||
|
||
public function handle(ServerRequestInterface $request): ResponseInterface | ||
{ | ||
$this->processedRequest = $request; | ||
return new Response($this->responseStatus); | ||
} | ||
} |