diff --git a/.woodpecker/code-style.yml b/.woodpecker/code-style.yml new file mode 100644 index 0000000..bfd94b0 --- /dev/null +++ b/.woodpecker/code-style.yml @@ -0,0 +1,8 @@ +pipeline: + code-style: + image: composer + commands: + - composer global config repositories.repo-name vcs https://git.digital-competence.de/Packages/php-codesniffer + - composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + - composer global require digicomp/php-codesniffer:@dev + - composer global exec -- phpcs --runtime-set ignore_warnings_on_exit 1 --standard=DigiComp Classes/ Tests/ diff --git a/.woodpecker/functional-tests.yml b/.woodpecker/functional-tests.yml new file mode 100644 index 0000000..02969f7 --- /dev/null +++ b/.woodpecker/functional-tests.yml @@ -0,0 +1,25 @@ +workspace: + base: /woodpecker + path: package + +matrix: + FLOW_VERSION: + - 7.3 + +pipeline: + functional-tests: + image: thecodingmachine/php:7.4-v4-cli + environment: + # Enable the PDO_SQLITE extension + - "PHP_EXTENSION_PDO_SQLITE=1" + - "FLOW_VERSION=${FLOW_VERSION}" + - "NEOS_BUILD_DIR=/woodpecker/Build-${FLOW_VERSION}" + commands: + - "sudo mkdir $NEOS_BUILD_DIR" + - "sudo chown -R docker:docker $NEOS_BUILD_DIR" + - "cd $NEOS_BUILD_DIR" + - "composer create-project --no-install neos/flow-base-distribution:^$FLOW_VERSION ." + - "composer config repositories.repo-name path /woodpecker/package" + - "composer config --no-plugins allow-plugins.neos/composer-plugin true" + - "composer require digicomp/flowsessionlock:@dev" + - "bin/phpunit --configuration Build/BuildEssentials/PhpUnit/FunctionalTests.xml Packages/Application/DigiComp.FlowSessionLock/Tests/Functional" diff --git a/Classes/Aspects/ReadOnlyAspect.php b/Classes/Aspects/ReadOnlyAspect.php index 3ece8df..3728566 100644 --- a/Classes/Aspects/ReadOnlyAspect.php +++ b/Classes/Aspects/ReadOnlyAspect.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace DigiComp\FlowSessionLock\Aspects; -use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent; +use DigiComp\FlowSessionLock\Http\SessionLockRequestMiddleware; use Neos\Flow\Annotations as Flow; use Neos\Flow\Aop\JoinPointInterface; use Neos\Flow\Core\Bootstrap; @@ -52,9 +52,8 @@ class ReadOnlyAspect $this->readOnly = true; /** @var Lock|null $lock */ - $lock = $activeRequestHandler->getComponentContext()->getParameter( - SessionLockRequestComponent::class, - SessionLockRequestComponent::PARAMETER_NAME + $lock = $activeRequestHandler->getHttpRequest()->getAttribute( + SessionLockRequestMiddleware::class . '.' . SessionLockRequestMiddleware::PARAMETER_NAME ); if ($lock !== null) { $this->logger->debug('SessionLock: Release, as this is marked read only.'); diff --git a/Classes/Http/SessionLockRequestComponent.php b/Classes/Http/SessionLockRequestMiddleware.php similarity index 77% rename from Classes/Http/SessionLockRequestComponent.php rename to Classes/Http/SessionLockRequestMiddleware.php index 43109ad..ecad355 100644 --- a/Classes/Http/SessionLockRequestComponent.php +++ b/Classes/Http/SessionLockRequestMiddleware.php @@ -5,14 +5,16 @@ declare(strict_types=1); namespace DigiComp\FlowSessionLock\Http; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Http\Component\ComponentContext; -use Neos\Flow\Http\Component\ComponentInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Lock\Exception\LockAcquiringException; use Symfony\Component\Lock\Key; use Symfony\Component\Lock\LockFactory; -class SessionLockRequestComponent implements ComponentInterface +class SessionLockRequestMiddleware implements MiddlewareInterface { public const PARAMETER_NAME = 'sessionLock'; @@ -55,13 +57,13 @@ class SessionLockRequestComponent implements ComponentInterface /** * @inheritDoc */ - public function handle(ComponentContext $componentContext): void + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $sessionCookieName = $this->sessionSettings['name']; - $cookies = $componentContext->getHttpRequest()->getCookieParams(); + $cookies = $request->getCookieParams(); if (!isset($cookies[$sessionCookieName])) { - return; + return $handler->handle($request); } // TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier @@ -69,7 +71,7 @@ class SessionLockRequestComponent implements ComponentInterface $lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease); - $componentContext->setParameter(SessionLockRequestComponent::class, static::PARAMETER_NAME, $lock); + $request = $request->withAttribute(SessionLockRequestMiddleware::class . '.' . static::PARAMETER_NAME, $lock); $this->logger->debug('SessionLock: Try to get "' . $key . '"'); $timedOut = \time() + $this->secondsToWait; @@ -83,5 +85,6 @@ class SessionLockRequestComponent implements ComponentInterface \usleep(100000); } $this->logger->debug('SessionLock: Acquired "' . $key . '"'); + return $handler->handle($request); } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index f283d84..08c69fc 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -9,9 +9,7 @@ DigiComp: Neos: Flow: http: - chain: - preprocess: - chain: - lockSession: - position: "before getSessionCookieFromRequest" - component: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestComponent" + middlewares: + lockSession: + position: "before session" + middleware: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestMiddleware" diff --git a/Configuration/Testing/Settings.yaml b/Configuration/Testing/Settings.yaml index 07b1b05..31d150d 100644 --- a/Configuration/Testing/Settings.yaml +++ b/Configuration/Testing/Settings.yaml @@ -1,3 +1,5 @@ DigiComp: FlowSessionLock: lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/" + readOnlyExpressions: + TestUnprotected: "method(DigiComp\\FlowSessionLock\\Tests\\Functional\\Fixtures\\Controller\\ExampleController->unprotectedByConfigurationAction())" diff --git a/README.md b/README.md index 0717b21..a35537f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ DigiComp.FlowSessionLock ------------------------ +![Build status](https://ci.digital-competence.de/api/badges/Packages/DigiComp.FlowSessionLock/status.svg) + By default, the session established by Flow is not "protected" in any way. This package restricts every request to load the session only, if there are no other requests having it in access currently. It allows to set custom pointcut which will set the session in "ReadOnly" mode, which allows concurrent requests to read, but disallows the current request to diff --git a/Tests/Functional/Fixtures/Controller/ExampleController.php b/Tests/Functional/Fixtures/Controller/ExampleController.php new file mode 100644 index 0000000..a5ecab9 --- /dev/null +++ b/Tests/Functional/Fixtures/Controller/ExampleController.php @@ -0,0 +1,43 @@ +serverRequestFactory = $this->objectManager->get(ServerRequestFactoryInterface::class); + $route = new Route(); + $route->setName('Functional Test - SessionRequestComponent::Restricted'); + $route->setUriPattern('test/sessionlock/{@action}'); + $route->setDefaults([ + '@package' => 'DigiComp.FlowSessionLock', + '@subpackage' => 'Tests\Functional\Fixtures', + '@controller' => 'Example', + '@action' => 'protected', + '@format' => 'html', + ]); + $route->setAppendExceedingArguments(true); + $this->router->addRoute($route); + } + + public function expectedDuration(): array + { + $parallelChecker = function ($allRequests, $oneRequest) { + self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000); + self::assertLessThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000); + }; + return [ + [ + 'http://localhost/test/sessionlock/protected', + function ($allRequests, $oneRequest) { + self::assertGreaterThan(ExampleController::CONTROLLER_TIME, $oneRequest * 1000); + self::assertGreaterThan(ExampleController::CONTROLLER_TIME * 4, $allRequests * 1000); + }, + ], + [ + 'http://localhost/test/sessionlock/unprotectedbyannotation', + $parallelChecker, + ], + [ + 'http://localhost/test/sessionlock/unprotectedbyconfiguration', + $parallelChecker, + ], + ]; + } + + /** + * @dataProvider expectedDuration + * @test + */ + public function itDoesNotAllowToEnterMoreThanOneWithTheSameSession(string $url, \Closure $checker): void + { + // Functional tests are currently broken, until a version containing + // https://github.com/neos/flow-development-collection/commit/bebfc4e6566bc4ba2ba28330344105adb2d6ada0 + // gets released + $request = $this->serverRequestFactory + ->createServerRequest('GET', new Uri($url)); + $start = \microtime(true); + $response = $this->browser->sendRequest($request); + $neededForOne = \microtime(true) - $start; + + $sessionCookies = \array_map(static function ($cookie) { + return Cookie::createFromRawSetCookieHeader($cookie); + }, $response->getHeader('Set-Cookie')); + self::assertNotEmpty($sessionCookies); + + $cookies = \array_reduce($sessionCookies, static function ($out, $cookie) { + $out[$cookie->getName()] = $cookie->getValue(); + return $out; + }, []); + $nextRequest = $this->serverRequestFactory + ->createServerRequest('GET', new Uri($url)) + ->withCookieParams($cookies); + $childs = []; + $start = \microtime(true); + for ($i = 0; $i < 4; $i++) { + $child = \pcntl_fork(); + if ($child === 0) { + $this->browser->sendRequest($nextRequest); + exit(); + } + $childs[] = $child; + } + foreach ($childs as $child) { + \pcntl_waitpid($child, $status); + } + $neededForAll = \microtime(true) - $start; + + $checker($neededForAll, $neededForOne); + } +} diff --git a/composer.json b/composer.json index 567cb4a..f3ff4d7 100644 --- a/composer.json +++ b/composer.json @@ -3,10 +3,13 @@ "description": "Session locking for Neos Flow - it secures the session becoming corrupted by concurrent access to the same session by different requests", "type": "neos-package", "require": { - "neos/flow": "^6.3.0", + "neos/flow": "^7.3.0", "php": ">=7.4", "symfony/lock": "^5.2.0" }, + "require-dev": { + "ext-pcntl": "*" + }, "autoload": { "psr-4": { "DigiComp\\FlowSessionLock\\": "Classes/" @@ -17,8 +20,20 @@ "package-key": "DigiComp.FlowSessionLock" }, "branch-alias": { - "dev-develop": "2.0.x-dev" - } + "dev-develop": "3.0.x-dev", + "dev-version/2.x-dev": "2.1.x-dev" + }, + "applied-flow-migrations": [ + "Neos.SwiftMailer-20161130105617", + "Neos.Flow-20180415105700", + "Neos.Flow-20190425144900", + "Neos.Flow-20190515215000", + "Neos.Flow-20200813181400", + "Neos.Flow-20201003165200", + "Neos.Flow-20201109224100", + "Neos.Flow-20201205172733", + "Neos.Flow-20201207104500" + ] }, "authors": [ {