Compare commits

..

No commits in common. "master" and "2.1.1" have entirely different histories.

11 changed files with 25 additions and 230 deletions

View file

@ -1,8 +0,0 @@
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/

View file

@ -1,31 +0,0 @@
workspace:
base: /woodpecker
path: package
matrix:
include:
- FLOW_VERSION: 7.3
PHP_VERSION: 7.4
- FLOW_VERSION: 7.3
PHP_VERSION: 8.1
- FLOW_VERSION: 8.2
PHP_VERSION: 8.1
pipeline:
functional-tests:
image: "thecodingmachine/php:${PHP_VERSION}-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"

View file

@ -8,7 +8,6 @@ namespace DigiComp\FlowSessionLock\Annotations;
* @Annotation
* @Target({"METHOD"})
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Unlock
final class ReadOnly
{
}

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Aspects;
use DigiComp\FlowSessionLock\Http\SessionLockRequestMiddleware;
use DigiComp\FlowSessionLock\Http\SessionLockRequestComponent;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Flow\Core\Bootstrap;
@ -36,7 +36,7 @@ class ReadOnlyAspect
protected bool $readOnly = false;
/**
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\Unlock) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @Flow\Around("methodAnnotatedWith(DigiComp\FlowSessionLock\Annotations\ReadOnly) || filter(DigiComp\FlowSessionLock\Aspects\ReadOnlyFilter)")
* @param JoinPointInterface $joinPoint
* @return mixed
*/
@ -52,8 +52,9 @@ class ReadOnlyAspect
$this->readOnly = true;
/** @var Lock|null $lock */
$lock = $activeRequestHandler->getHttpRequest()->getAttribute(
SessionLockRequestMiddleware::class . '.' . SessionLockRequestMiddleware::PARAMETER_NAME
$lock = $activeRequestHandler->getComponentContext()->getParameter(
SessionLockRequestComponent::class,
SessionLockRequestComponent::PARAMETER_NAME
);
if ($lock !== null) {
$this->logger->debug('SessionLock: Release, as this is marked read only.');

View file

@ -5,16 +5,14 @@ declare(strict_types=1);
namespace DigiComp\FlowSessionLock\Http;
use Neos\Flow\Annotations as Flow;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Neos\Flow\Http\Component\ComponentContext;
use Neos\Flow\Http\Component\ComponentInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory;
class SessionLockRequestMiddleware implements MiddlewareInterface
class SessionLockRequestComponent implements ComponentInterface
{
public const PARAMETER_NAME = 'sessionLock';
@ -57,13 +55,13 @@ class SessionLockRequestMiddleware implements MiddlewareInterface
/**
* @inheritDoc
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function handle(ComponentContext $componentContext): void
{
$sessionCookieName = $this->sessionSettings['name'];
$cookies = $request->getCookieParams();
$cookies = $componentContext->getHttpRequest()->getCookieParams();
if (!isset($cookies[$sessionCookieName])) {
return $handler->handle($request);
return;
}
// TODO: sessionIdentifier might be wrong, probably it should probably be storage identifier
@ -71,20 +69,19 @@ class SessionLockRequestMiddleware implements MiddlewareInterface
$lock = $this->lockFactory->createLockFromKey($key, $this->timeToLive, $this->autoRelease);
$request = $request->withAttribute(SessionLockRequestMiddleware::class . '.' . static::PARAMETER_NAME, $lock);
$componentContext->setParameter(SessionLockRequestComponent::class, static::PARAMETER_NAME, $lock);
$this->logger->debug('SessionLock: Try to get "' . $key . '"');
$timedOut = \time() + $this->secondsToWait;
while (!$lock->acquire()) {
\usleep(100000);
if (\time() >= $timedOut) {
throw new LockAcquiringException(
'Could not acquire the lock for "' . $key . '" in ' . $this->secondsToWait . ' seconds.',
1652687960
);
}
\usleep(100000);
}
$this->logger->debug('SessionLock: Acquired "' . $key . '"');
return $handler->handle($request);
}
}

View file

@ -9,7 +9,9 @@ DigiComp:
Neos:
Flow:
http:
middlewares:
lockSession:
position: "before session"
middleware: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestMiddleware"
chain:
preprocess:
chain:
lockSession:
position: "before getSessionCookieFromRequest"
component: "DigiComp\\FlowSessionLock\\Http\\SessionLockRequestComponent"

View file

@ -1,5 +1,3 @@
DigiComp:
FlowSessionLock:
lockStoreConnection: "flock://%FLOW_PATH_DATA%Temporary/Testing/SessionLocks/"
readOnlyExpressions:
TestUnprotected: "method(DigiComp\\FlowSessionLock\\Tests\\Functional\\Fixtures\\Controller\\ExampleController->unprotectedByConfigurationAction())"

View file

@ -1,8 +1,6 @@
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

View file

@ -1,43 +0,0 @@
<?php
namespace DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller;
use DigiComp\FlowSessionLock\Annotations as FlowSessionLock;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;
class ExampleController extends ActionController
{
public const CONTROLLER_TIME = 200;
/**
* @Flow\Session(autoStart=true);
* @return string
*/
public function protectedAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
/**
* @Flow\Session(autoStart=true);
* @FlowSessionLock\Unlock
* @return string
*/
public function unprotectedByAnnotationAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
/**
* @Flow\Session(autoStart=true);
* @return string
*/
public function unprotectedByConfigurationAction()
{
\usleep(static::CONTROLLER_TIME * 1000);
return 'Hello World!';
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace DigiComp\FlowSessionLock\Tests\Functional;
use DigiComp\FlowSessionLock\Tests\Functional\Fixtures\Controller\ExampleController;
use GuzzleHttp\Psr7\Uri;
use Neos\Flow\Http\Cookie;
use Neos\Flow\Mvc\Routing\Route;
use Neos\Flow\Tests\FunctionalTestCase;
use Psr\Http\Message\ServerRequestFactoryInterface;
class SessionLockRequestComponentTest extends FunctionalTestCase
{
protected ServerRequestFactoryInterface $serverRequestFactory;
protected function setUp(): void
{
parent::setUp();
$this->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);
}
}

View file

@ -3,12 +3,9 @@
"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": "^7.3.0 | ^8.2",
"php": "^7.4 | ^8.1",
"symfony/lock": "^5.2.0 | ^6.2.0"
},
"require-dev": {
"ext-pcntl": "*"
"neos/flow": "^6.3.0",
"php": ">=7.4",
"symfony/lock": "^5.2.0"
},
"autoload": {
"psr-4": {
@ -20,20 +17,8 @@
"package-key": "DigiComp.FlowSessionLock"
},
"branch-alias": {
"dev-develop": "4.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"
]
"dev-develop": "2.0.x-dev"
}
},
"authors": [
{