Skip to content

Commit

Permalink
Cleanup, add docblocs, add test cases, raise psalm error level (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
devanych committed Nov 18, 2021
1 parent 0ec9604 commit 53961f9
Show file tree
Hide file tree
Showing 15 changed files with 759 additions and 367 deletions.
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
errorLevel="2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
Expand Down
167 changes: 96 additions & 71 deletions src/BasicNetworkResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;

use function count;
use function array_map;
use function in_array;
use function is_array;
use function is_callable;
use function is_string;
use function strtolower;
Expand All @@ -32,60 +31,13 @@ final class BasicNetworkResolver implements MiddlewareInterface
];

/**
* @psalm-var array<string, array|callable|null>
* @psalm-var array<string, array|callable>
*/
private array $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).
* Returns a new instance with added the specified protocol 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.
Expand All @@ -94,27 +46,31 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
* - 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.
* });
* ->withProtocolHeader(
* 'x-forwarded-proto',
* function (array $values, string $header, ServerRequestInterface $request): ?string {
* 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.
* - 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']
* 'http' => ['http'],
* 'https' => ['https'],
* ]);
* ```
*
* @param string $header
* @param array|callable|null $values
*
* @return self
* @param string $header The protocol header name.
* @param array|callable|null $values The protocol header values.
*
* @see DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES
*
* @return self
*/
public function withAddedProtocolHeader(string $header, $values = null): self
public function withAddedProtocolHeader(string $header, array|callable $values = null): self
{
$new = clone $this;
$header = strtolower($header);
Expand All @@ -129,11 +85,7 @@ public function withAddedProtocolHeader(string $header, $values = null): self
return $new;
}

if (!is_array($values)) {
throw new RuntimeException('Accepted values is not array nor callable.');
}

if (count($values) === 0) {
if (empty($values)) {
throw new RuntimeException('Accepted values cannot be an empty array.');
}

Expand All @@ -145,22 +97,40 @@ public function withAddedProtocolHeader(string $header, $values = null): self
}

if ($protocol === '') {
throw new RuntimeException('The protocol cannot be an empty string');
throw new RuntimeException('The protocol cannot be an empty string.');
}

$new->protocolHeaders[$header][$protocol] = array_map('strtolower', (array) $acceptedValues);
$new->protocolHeaders[$header][$protocol] = array_map('\strtolower', (array) $acceptedValues);
}

return $new;
}

/**
* Returns a new instance without the specified protocol header.
*
* @param string $header The protocol header name.
*
* @see withAddedProtocolHeader()
*
* @return self
*/
public function withoutProtocolHeader(string $header): self
{
$new = clone $this;
unset($new->protocolHeaders[strtolower($header)]);
return $new;
}

/**
* Returns a new instance without the specified protocol headers.
*
* @param string[] $headers The protocol header names. If `null` is specified all protocol headers will be removed.
*
* @see withoutProtocolHeader()
*
* @return self
*/
public function withoutProtocolHeaders(?array $headers = null): self
{
$new = clone $this;
Expand All @@ -176,4 +146,59 @@ public function withoutProtocolHeaders(?array $headers = null): self

return $new;
}

/**
* {@inheritDoc}
*
* @throws RuntimeException If wrong URI scheme protocol.
*/
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((string) $newScheme));
}

return $handler->handle($request);
}
}
71 changes: 45 additions & 26 deletions src/ForceSecureConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,11 @@ public function __construct(ResponseFactoryInterface $responseFactory)
$this->responseFactory = $responseFactory;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
$url = (string) $request->getUri()->withScheme('https')->withPort($this->port);

return $this->addHSTS(
$this->responseFactory
->createResponse($this->statusCode)
->withHeader(Header::LOCATION, $url)
);
}

return $this->addHSTS($this->addCSP($handler->handle($request)));
}

/**
* Redirects from HTTP to HTTPS
* Returns a new instance and enables redirection from HTTP to HTTPS.
*
* @param int $statusCode
* @param int|null $port
* @param int $statusCode The response status code of redirection.
* @param int|null $port The redirection port.
*
* @return self
*/
Expand All @@ -81,6 +66,13 @@ public function withRedirection(int $statusCode = Status::MOVED_PERMANENTLY, int
return $new;
}

/**
* Returns a new instance and disables redirection from HTTP to HTTPS.
*
* @see withRedirection()
*
* @return self
*/
public function withoutRedirection(): self
{
$new = clone $this;
Expand All @@ -89,11 +81,11 @@ public function withoutRedirection(): self
}

/**
* Add Content-Security-Policy header to response.
* Returns a new instance with added the `Content-Security-Policy` header to response.
*
* @see Header::CONTENT_SECURITY_POLICY
* @param string $directives The directives {@see DEFAULT_CSP_DIRECTIVES}.
*
* @param string $directives
* @see Header::CONTENT_SECURITY_POLICY
*
* @return self
*/
Expand All @@ -105,6 +97,13 @@ public function withCSP(string $directives = self::DEFAULT_CSP_DIRECTIVES): self
return $new;
}

/**
* Returns a new instance without the `Content-Security-Policy` header in response.
*
* @see withCSP()
*
* @return self
*/
public function withoutCSP(): self
{
$new = clone $this;
Expand All @@ -113,12 +112,10 @@ public function withoutCSP(): self
}

/**
* Add Strict-Transport-Security header to each response.
* Returns a new instance with added the `Strict-Transport-Security` header to response.
*
* @see Header::STRICT_TRANSPORT_SECURITY
*
* @param int $maxAge
* @param bool $subDomains
* @param int $maxAge The max age {@see DEFAULT_HSTS_MAX_AGE}.
* @param bool $subDomains Whether to add the `includeSubDomains` option to the header value.
*
* @return self
*/
Expand All @@ -131,13 +128,35 @@ public function withHSTS(int $maxAge = self::DEFAULT_HSTS_MAX_AGE, bool $subDoma
return $new;
}

/**
* Returns a new instance without the `Strict-Transport-Security` header in response.
*
* @see withHSTS()
*
* @return self
*/
public function withoutHSTS(): self
{
$new = clone $this;
$new->addHSTS = false;
return $new;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->redirect && strcasecmp($request->getUri()->getScheme(), 'http') === 0) {
$url = (string) $request->getUri()->withScheme('https')->withPort($this->port);

return $this->addHSTS(
$this->responseFactory
->createResponse($this->statusCode)
->withHeader(Header::LOCATION, $url)
);
}

return $this->addHSTS($this->addCSP($handler->handle($request)));
}

private function addCSP(ResponseInterface $response): ResponseInterface
{
return $this->addCSP
Expand Down
Loading

0 comments on commit 53961f9

Please sign in to comment.