Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class CORSMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
use ConfigAwareTrait;
|
||||
|
||||
public const PRIORITY = 1000;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $corsHeaders = [
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Authorization',
|
||||
'Access-Control-Allow-Methods' => 'PUT, GET, POST, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
'Access-Control-Max-Age' => 10 * 60 * 60, // 10 min, max age for Chrome
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $requestOriginIsValid = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $restrictCORSDomains = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $validCORSDomains = [];
|
||||
|
||||
/**
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->config = $this->getConfig();
|
||||
$this->restrictCORSDomains = array_key_exists('cors_restrict_domains', $this->config) ? (bool) $this->config['cors_restrict_domains'] : true;
|
||||
$this->validCORSDomains = array_key_exists('cors_valid_domains', $this->config) ? (array) $this->config['cors_valid_domains'] : [];
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
$this->corsHeaders['Access-Control-Allow-Origin'] = $this->getAllowOriginHeaderValue($request);
|
||||
|
||||
// Capture all OPTIONS requests
|
||||
if ('OPTIONS' === $request->getMethod()) {
|
||||
$response = new Response('', Response::HTTP_NO_CONTENT);
|
||||
|
||||
// If this is a valid OPTIONS request, set the CORS headers on the Response and exit.
|
||||
if (
|
||||
$this->requestOriginIsValid
|
||||
&& $request->headers->has('Access-Control-Request-Headers')
|
||||
&& $request->headers->has('Origin')
|
||||
) {
|
||||
foreach ($this->corsHeaders as $header => $value) {
|
||||
$response->headers->set($header, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response = $this->app->handle($request, $type, $catch);
|
||||
|
||||
// Add standard CORS headers to any XHR
|
||||
if ($request->isXmlHttpRequest()) {
|
||||
foreach ($this->corsHeaders as $header => $value) {
|
||||
$response->headers->set($header, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for the Access-Control-Allow-Origin header
|
||||
* based on the Request and local configuration options.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getAllowOriginHeaderValue(Request $request)
|
||||
{
|
||||
$origin = $request->headers->get('Origin');
|
||||
|
||||
// If we're not restricting domains, set the header to the request origin
|
||||
if (!$this->restrictCORSDomains || in_array($origin, $this->validCORSDomains)) {
|
||||
$this->requestOriginIsValid = true;
|
||||
|
||||
return $origin;
|
||||
}
|
||||
|
||||
// Check the domains using shell wildcard patterns
|
||||
$validCorsDomainFilter = function ($validCorsDomain) use ($origin) {
|
||||
if (null === $origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fnmatch($validCorsDomain, $origin, FNM_CASEFOLD);
|
||||
};
|
||||
|
||||
if (array_filter($this->validCORSDomains, $validCorsDomainFilter)) {
|
||||
$this->requestOriginIsValid = true;
|
||||
$this->corsHeaders['Vary'] = 'Origin';
|
||||
|
||||
return $origin;
|
||||
}
|
||||
|
||||
$this->requestOriginIsValid = false;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Mautic\CoreBundle\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class CatchExceptionMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
public const PRIORITY = 100;
|
||||
|
||||
/**
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
$content = 'The site is currently offline due to encountering an error. If the problem persists, please contact the system administrator. System administrators, check server logs for errors.';
|
||||
try {
|
||||
$response = $this->app->handle($request, $type, $catch);
|
||||
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$content = ErrorHandler::getHandler()->handleException($exception, true);
|
||||
}
|
||||
|
||||
return new Response($content, 500);
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Mautic\CoreBundle\Loader\ParameterLoader;
|
||||
|
||||
trait ConfigAwareTrait
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
// Include paths
|
||||
$root = realpath(__DIR__.'/..');
|
||||
$configBaseDir = ParameterLoader::getLocalConfigBaseDir($root);
|
||||
$projectRoot = ParameterLoader::getProjectDirByRoot($root);
|
||||
|
||||
/** @var array $paths */
|
||||
include $root.'/config/paths.php';
|
||||
|
||||
$localParameters = [];
|
||||
|
||||
$localConfig = ParameterLoader::getLocalConfigFile($root, false);
|
||||
|
||||
if (file_exists($localConfig)) {
|
||||
/** @var $parameters */
|
||||
include $localConfig;
|
||||
|
||||
$localParameters = $parameters;
|
||||
}
|
||||
|
||||
// check for parameter overrides
|
||||
if (file_exists($configBaseDir.'/config/parameters_local.php')) {
|
||||
include $configBaseDir.'/config/parameters_local.php';
|
||||
$localParameters = array_merge($localParameters, $parameters);
|
||||
}
|
||||
|
||||
return $localParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware\Dev;
|
||||
|
||||
use Mautic\Middleware\ConfigAwareTrait;
|
||||
use Mautic\Middleware\PrioritizedMiddlewareInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class IpRestrictMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
use ConfigAwareTrait;
|
||||
|
||||
public const PRIORITY = 20;
|
||||
|
||||
/**
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $allowedIps;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->allowedIps = ['127.0.0.1', 'fe80::1', '::1'];
|
||||
|
||||
$parameters = $this->getConfig();
|
||||
if (array_key_exists('dev_hosts', $parameters) && is_array($parameters['dev_hosts'])) {
|
||||
$this->allowedIps = array_merge($this->allowedIps, $parameters['dev_hosts']);
|
||||
}
|
||||
|
||||
if (isset($_SERVER['MAUTIC_CUSTOM_DEV_HOSTS'])) {
|
||||
$localIps = json_decode($_SERVER['MAUTIC_CUSTOM_DEV_HOSTS'], true);
|
||||
$this->allowedIps = array_merge($this->allowedIps, $localIps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This check prevents access to debug front controllers
|
||||
* that are deployed by accident to production servers.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
if (in_array($request->getClientIp(), $this->allowedIps) || false !== getenv('DDEV_TLD')) {
|
||||
return $this->app->handle($request, $type, $catch);
|
||||
}
|
||||
|
||||
return new Response('You are not allowed to access this file.', 403);
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class HSTSMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
use ConfigAwareTrait;
|
||||
|
||||
public const PRIORITY = 900;
|
||||
|
||||
protected bool $enableHSTS;
|
||||
|
||||
protected bool $includeDubDomains;
|
||||
|
||||
protected bool $preload;
|
||||
|
||||
protected int $expireTime;
|
||||
|
||||
protected HttpKernelInterface $app;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->config = $this->getConfig();
|
||||
$this->enableHSTS = array_key_exists('headers_sts', $this->config) && (bool) $this->config['headers_sts'];
|
||||
$this->includeDubDomains = array_key_exists('headers_sts_subdomains', $this->config) && (bool) $this->config['headers_sts_subdomains'];
|
||||
$this->preload = array_key_exists('headers_sts_preload', $this->config) && (bool) $this->config['headers_sts_preload'];
|
||||
$this->expireTime = $this->config['headers_sts_expire_time'] ?? 60;
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
$response = $this->app->handle($request, $type, $catch);
|
||||
|
||||
// Do not include the header in the sub-request response
|
||||
if (self::MAIN_REQUEST !== $type) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($this->enableHSTS && $this->expireTime) {
|
||||
$value = 'max-age='.$this->expireTime.($this->includeDubDomains ? '; includeSubDomains' : '').($this->preload ? '; preload' : '');
|
||||
$response->headers->set('Strict-Transport-Security', $value);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Mautic\CoreBundle\Cache\MiddlewareCacheWarmer;
|
||||
|
||||
class MiddlewareBuilder
|
||||
{
|
||||
private \AppKernel $app;
|
||||
|
||||
private string $cacheFile;
|
||||
|
||||
private \SplPriorityQueue $specs;
|
||||
|
||||
public function __construct(\AppKernel $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->cacheFile = sprintf('%s/middlewares.cache.php', $app->getCacheDir());
|
||||
$this->specs = new \SplPriorityQueue();
|
||||
}
|
||||
|
||||
public function resolve(): StackedHttpKernel
|
||||
{
|
||||
$this->loadMiddlewares();
|
||||
|
||||
$app = $this->app;
|
||||
$middlewares = [$app];
|
||||
|
||||
foreach ($this->specs as $spec) {
|
||||
$app = $spec->newInstanceArgs([$app]);
|
||||
|
||||
array_unshift($middlewares, $app);
|
||||
}
|
||||
|
||||
return new StackedHttpKernel($app, $middlewares);
|
||||
}
|
||||
|
||||
private function loadMiddlewares(): void
|
||||
{
|
||||
if (!$this->hasCacheFile()) {
|
||||
$this->warmUpCacheCommand();
|
||||
}
|
||||
|
||||
$this->loadCacheFile();
|
||||
}
|
||||
|
||||
private function warmUpCacheCommand(): void
|
||||
{
|
||||
$middlewareCacheWarmer = new MiddlewareCacheWarmer($this->app->getEnvironment());
|
||||
$middlewareCacheWarmer->warmUp($this->app->getCacheDir());
|
||||
}
|
||||
|
||||
private function hasCacheFile(): bool
|
||||
{
|
||||
return file_exists($this->cacheFile);
|
||||
}
|
||||
|
||||
private function loadCacheFile(): void
|
||||
{
|
||||
/** @var array $middlewares */
|
||||
$middlewares = include $this->cacheFile;
|
||||
|
||||
foreach ($middlewares as $middleware) {
|
||||
$this->push($middleware);
|
||||
}
|
||||
}
|
||||
|
||||
private function push(string $middlewareClass): void
|
||||
{
|
||||
try {
|
||||
$reflection = new \ReflectionClass($middlewareClass);
|
||||
$priority = $reflection->getConstant('PRIORITY');
|
||||
|
||||
$this->specs->insert($reflection, $priority);
|
||||
} catch (\ReflectionException $e) {
|
||||
/* If there's an error getting the kernel class, it's
|
||||
* an invalid middleware. If it's invalid, don't push
|
||||
* it to the stack
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
interface PrioritizedMiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* Get the middleware's priority.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority();
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\TerminableInterface;
|
||||
|
||||
/**
|
||||
* Provides a stacked HTTP kernel.
|
||||
*
|
||||
* Copied from https://github.com/stackphp/builder/ with added compatibility
|
||||
* for Symfony 6.
|
||||
*
|
||||
* @see https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21StackMiddleware%21StackedHttpKernel.php/class/StackedHttpKernel/11.x
|
||||
* @see \Drupal\Core\DependencyInjection\Compiler\StackedKernelPass
|
||||
*/
|
||||
class StackedHttpKernel implements HttpKernelInterface, TerminableInterface
|
||||
{
|
||||
/**
|
||||
* The decorated kernel.
|
||||
*
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
private $kernel;
|
||||
|
||||
/**
|
||||
* A set of middlewares that are wrapped around this kernel.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $middlewares = [];
|
||||
|
||||
/**
|
||||
* Constructs a stacked HTTP kernel.
|
||||
*
|
||||
* @param HttpKernelInterface $kernel
|
||||
* The decorated kernel
|
||||
* @param array $middlewares
|
||||
* An array of previous middleware services
|
||||
*/
|
||||
public function __construct(HttpKernelInterface $kernel, array $middlewares)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
$this->middlewares = $middlewares;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
return $this->kernel
|
||||
->handle($request, $type, $catch);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function terminate(Request $request, Response $response): void
|
||||
{
|
||||
$previous = null;
|
||||
foreach ($this->middlewares as $kernel) {
|
||||
// If the previous kernel was terminable we can assume this middleware
|
||||
// has already been called.
|
||||
if (!$previous instanceof TerminableInterface && $kernel instanceof TerminableInterface) {
|
||||
$kernel->terminate($request, $response);
|
||||
}
|
||||
$previous = $kernel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware\Tests\Dev;
|
||||
|
||||
use Mautic\Middleware\Dev\IpRestrictMiddleware;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class IpRestrictMiddlewareTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private mixed $originalDdevTldValue;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->originalDdevTldValue = getenv('DDEV_TLD');
|
||||
putenv('DDEV_TLD');
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
putenv('DDEV_TLD='.$this->originalDdevTldValue);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testWorkflowWithLocalhostIp(): void
|
||||
{
|
||||
$inputRequest = new Request();
|
||||
$inputRequest->server->set('REMOTE_ADDR', '127.0.0.1'); // 127.0.0.1 is always allowed.
|
||||
$httpKernel = new class implements HttpKernelInterface {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
};
|
||||
|
||||
$middleware = new IpRestrictMiddleware($httpKernel);
|
||||
$response = $middleware->handle($inputRequest);
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testWorkflowWithDisallowedIp(): void
|
||||
{
|
||||
$inputRequest = new Request();
|
||||
$inputRequest->server->set('REMOTE_ADDR', 'unallowed.ip.address');
|
||||
$httpKernel = new class implements HttpKernelInterface {
|
||||
public $handleWasCalled = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
$this->handleWasCalled = true;
|
||||
|
||||
return new Response();
|
||||
}
|
||||
};
|
||||
|
||||
$middleware = new IpRestrictMiddleware($httpKernel);
|
||||
$response = $middleware->handle($inputRequest);
|
||||
|
||||
Assert::assertSame(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
Assert::assertFalse($httpKernel->handleWasCalled);
|
||||
}
|
||||
|
||||
public function testWorkflowWithConfiguredIp(): void
|
||||
{
|
||||
// Remember original custom_dev_hosts value so we could return it afterwards.
|
||||
$originalDevHostsValue = $_SERVER['MAUTIC_CUSTOM_DEV_HOSTS'] ?? '[]';
|
||||
|
||||
$_SERVER['MAUTIC_CUSTOM_DEV_HOSTS'] = '["configured.ip.address"]';
|
||||
|
||||
$inputRequest = new Request();
|
||||
$inputRequest->server->set('REMOTE_ADDR', 'configured.ip.address');
|
||||
$httpKernel = new class($inputRequest) implements HttpKernelInterface {
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
return new Response();
|
||||
}
|
||||
};
|
||||
|
||||
$middleware = new IpRestrictMiddleware($httpKernel);
|
||||
$response = $middleware->handle($inputRequest);
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
|
||||
// Set the original value back.
|
||||
$_SERVER['MAUTIC_CUSTOM_DEV_HOSTS'] = $originalDevHostsValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\Middleware\Tests;
|
||||
|
||||
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
|
||||
use Mautic\Middleware\HSTSMiddleware;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\ExpectationFailedException as PHPUnitException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HSTSMiddlewareTest extends AbstractMauticTestCase
|
||||
{
|
||||
public const HSTS_KEY = 'strict-transport-security';
|
||||
|
||||
protected \ReflectionProperty $addHSTS;
|
||||
|
||||
protected \ReflectionProperty $includeDubDomains;
|
||||
|
||||
protected \ReflectionProperty $preload;
|
||||
|
||||
protected HSTSMiddleware $middleware;
|
||||
|
||||
protected \ReflectionClass $middlewareReflection;
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->middleware = new HSTSMiddleware($this->client->getKernel());
|
||||
$this->middlewareReflection = new \ReflectionClass($this->middleware);
|
||||
|
||||
$this->addHSTS = $this->middlewareReflection->getProperty('enableHSTS');
|
||||
$this->addHSTS->setAccessible(true);
|
||||
|
||||
$this->includeDubDomains = $this->middlewareReflection->getProperty('includeDubDomains');
|
||||
$this->includeDubDomains->setAccessible(true);
|
||||
|
||||
$this->preload = $this->middlewareReflection->getProperty('preload');
|
||||
$this->preload->setAccessible(true);
|
||||
}
|
||||
|
||||
protected function testResponseHeaders(): void
|
||||
{
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertNotEmpty($response->headers);
|
||||
}
|
||||
|
||||
public function testHSTSEnabled(): void
|
||||
{
|
||||
$this->setHSTS(true);
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertTrue(
|
||||
$response->headers->has(self::HSTS_KEY),
|
||||
'Strict-Transport-Security is enabled but is missing from the response headers'
|
||||
);
|
||||
}
|
||||
|
||||
public function testHSTSDisabled(): void
|
||||
{
|
||||
$this->setHSTS(false);
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertFalse(
|
||||
$response->headers->has(self::HSTS_KEY),
|
||||
'Strict-Transport-Security is disabled but is present in response headers'
|
||||
);
|
||||
}
|
||||
|
||||
public function testIncludeSubdomainsEnabled(): void
|
||||
{
|
||||
$needle = 'includeSubDomains';
|
||||
$this->setHSTS(true);
|
||||
$this->setIncludeDubDomainsValue(true);
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
$needle,
|
||||
$response->headers->get(self::HSTS_KEY),
|
||||
'Option include Subdomains is enabled but is missing from the HSTS value'
|
||||
);
|
||||
}
|
||||
|
||||
public function testIncludeSubdomainsDisabled(): void
|
||||
{
|
||||
$needle = 'includeSubDomains';
|
||||
$this->setHSTS(true);
|
||||
$this->setIncludeDubDomainsValue(false);
|
||||
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertStringNotContainsStringIgnoringCase(
|
||||
$needle,
|
||||
$this->getHSTSValue($response),
|
||||
'Option include Subdomains is disabled but is present in HSTS value'
|
||||
);
|
||||
}
|
||||
|
||||
public function testPreloadEnabled(): void
|
||||
{
|
||||
$needle = 'preload';
|
||||
$this->setHSTS(true);
|
||||
$this->setPreloadValue(true);
|
||||
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
$needle,
|
||||
$response->headers->get(self::HSTS_KEY),
|
||||
'Option preload is enabled but is missing from the HSTS value'
|
||||
);
|
||||
}
|
||||
|
||||
public function testPreloadDisabled(): void
|
||||
{
|
||||
$needle = 'preload';
|
||||
$this->setHSTS(true);
|
||||
$this->setPreloadValue(false);
|
||||
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertStringNotContainsStringIgnoringCase(
|
||||
$needle,
|
||||
$this->getHSTSValue($response),
|
||||
'Option preload is disabled but is present in HSTS value'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function testExpireTime(): void
|
||||
{
|
||||
$this->setHSTS(true);
|
||||
$expireTimeValue = 12345;
|
||||
$expireTime = $this->middlewareReflection->getProperty('expireTime');
|
||||
$expireTime->setAccessible(true);
|
||||
$expireTime->setValue($this->middleware, $expireTimeValue);
|
||||
|
||||
$response = $this->getMiddlewareResponse();
|
||||
|
||||
Assert::assertMatchesRegularExpression(
|
||||
'/max-age='.$expireTimeValue.'(; includeSubDomains)?/',
|
||||
$this->getHSTSValue($response),
|
||||
'Expire time does not match the configuration'
|
||||
);
|
||||
}
|
||||
|
||||
private function setHSTS(bool $value): void
|
||||
{
|
||||
$this->addHSTS->setValue($this->middleware, $value);
|
||||
}
|
||||
|
||||
private function setIncludeDubDomainsValue(bool $value): void
|
||||
{
|
||||
$this->includeDubDomains->setValue($this->middleware, $value);
|
||||
}
|
||||
|
||||
private function setPreloadValue(bool $value): void
|
||||
{
|
||||
$this->preload->setValue($this->middleware, $value);
|
||||
}
|
||||
|
||||
private function getHSTSValue(Response $response): string
|
||||
{
|
||||
return $response->headers->get(self::HSTS_KEY) ?? '';
|
||||
}
|
||||
|
||||
private function getMiddlewareResponse(): Response
|
||||
{
|
||||
try {
|
||||
return $this->middleware->handle(Request::create('s/login', Request::METHOD_GET));
|
||||
} catch (\Exception $e) {
|
||||
throw new PHPUnitException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class TrustMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
use ConfigAwareTrait;
|
||||
|
||||
public const PRIORITY = 0;
|
||||
|
||||
/**
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
private $app;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
$config = $this->getConfig();
|
||||
|
||||
if (!empty($config['trusted_proxies'])) {
|
||||
Request::setTrustedProxies($config['trusted_proxies'], Request::getTrustedHeaderSet());
|
||||
}
|
||||
|
||||
if (!empty($config['trusted_hosts'])) {
|
||||
Request::setTrustedHosts($config['trusted_hosts']);
|
||||
}
|
||||
|
||||
return $this->app->handle($request, $type, $catch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\Middleware;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
class VersionCheckMiddleware implements HttpKernelInterface, PrioritizedMiddlewareInterface
|
||||
{
|
||||
public const PRIORITY = 10;
|
||||
|
||||
/**
|
||||
* @var HttpKernelInterface
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $minimumPHPVersion;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $maximumPHPVersion;
|
||||
|
||||
public function __construct(HttpKernelInterface $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
|
||||
$metadata = json_decode(
|
||||
file_get_contents(__DIR__.'/../release_metadata.json'),
|
||||
true
|
||||
);
|
||||
|
||||
$this->minimumPHPVersion = $metadata['minimum_php_version'];
|
||||
$this->maximumPHPVersion = $metadata['maximum_php_version'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Minimum / Maximum PHP versions.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response
|
||||
{
|
||||
// Are we running the minimum version?
|
||||
if (version_compare(PHP_VERSION, $this->minimumPHPVersion, 'lt')) {
|
||||
return new Response('Your server does not meet the minimum PHP requirements. Mautic requires PHP version '.$this->minimumPHPVersion.' while your server has '.PHP_VERSION.'. Please contact your host to update your PHP installation.', 500);
|
||||
}
|
||||
|
||||
// Are we running a version newer than what Mautic supports?
|
||||
if (version_compare(PHP_VERSION, $this->maximumPHPVersion, 'gt')) {
|
||||
return new Response('Mautic does not support PHP version '.PHP_VERSION.' at this time. To use Mautic, you will need to downgrade to an earlier version.', 500);
|
||||
}
|
||||
|
||||
return $this->app->handle($request, $type, $catch);
|
||||
}
|
||||
|
||||
public function getPriority()
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user