diff --git a/app/Connections/ConnectionManager.php b/app/Connections/ConnectionManager.php index 1aee53d..94dafc3 100644 --- a/app/Connections/ConnectionManager.php +++ b/app/Connections/ConnectionManager.php @@ -40,9 +40,9 @@ public function __construct(SubdomainGenerator $subdomainGenerator, StatisticsCo $this->logger = $logger; } - public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength) + public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength, ?array $user = null) { - if ($maximumConnectionLength === 0) { + if ($maximumConnectionLength === 0 || $this->userIsExemptFromConnectionLimit($user)) { return; } @@ -61,6 +61,11 @@ public function limitConnectionLength(ControlConnection $connection, int $maximu }); } + protected function userIsExemptFromConnectionLimit(?array $user): bool + { + return ! is_null($user) && (int) ($user['can_specify_subdomains'] ?? 0) === 1; + } + public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection { $clientId = (string) uniqid(); diff --git a/app/Contracts/ConnectionManager.php b/app/Contracts/ConnectionManager.php index 5e7191f..77fda40 100644 --- a/app/Contracts/ConnectionManager.php +++ b/app/Contracts/ConnectionManager.php @@ -12,7 +12,7 @@ public function storeConnection(string $host, ?string $subdomain, ?string $serve public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection; - public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength); + public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength, ?array $user = null); public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection; diff --git a/app/Http/Controllers/ControlMessageController.php b/app/Http/Controllers/ControlMessageController.php index 7f4426d..d748e8a 100644 --- a/app/Http/Controllers/ControlMessageController.php +++ b/app/Http/Controllers/ControlMessageController.php @@ -190,7 +190,7 @@ protected function handleHttpConnection(ConnectionInterface $connection, $data, $connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection); - $this->connectionManager->limitConnectionLength($connectionInfo, config('expose-server.maximum_connection_length')); + $this->connectionManager->limitConnectionLength($connectionInfo, config('expose-server.maximum_connection_length'), $user); return $this->resolveConnectionMessage($connectionInfo, $user); }) diff --git a/tests/Feature/Server/ConnectionManagerTest.php b/tests/Feature/Server/ConnectionManagerTest.php new file mode 100644 index 0000000..d727be1 --- /dev/null +++ b/tests/Feature/Server/ConnectionManagerTest.php @@ -0,0 +1,99 @@ +shouldNotReceive('addTimer'); + + $statisticsCollector = Mockery::mock(StatisticsCollector::class); + $statisticsCollector->shouldNotReceive('cooldownTriggered'); + + $connection = Mockery::mock(ControlConnection::class); + $connection->authToken = 'pro-user-token'; + $connection->shouldNotReceive('setMaximumConnectionLength'); + $connection->shouldNotReceive('closeWithoutReconnect'); + + $this->app->instance(UserRepository::class, Mockery::mock(UserRepository::class)); + + $manager = new ConnectionManager( + Mockery::mock(SubdomainGenerator::class), + $statisticsCollector, + Mockery::mock(LoggerRepository::class), + $loop + ); + + $manager->limitConnectionLength($connection, 60, [ + 'can_specify_subdomains' => 1, + ]); + } + + /** @test */ + public function it_still_applies_connection_length_limits_to_users_without_custom_subdomains() + { + config()->set('expose-server.connection_cooldown_period', 10); + + $timerCallback = null; + + $loop = Mockery::mock(LoopInterface::class); + $loop->shouldReceive('addTimer') + ->once() + ->withArgs(function ($seconds, $callback) use (&$timerCallback) { + $this->assertSame(60, $seconds); + $timerCallback = $callback; + + return is_callable($callback); + }); + + $statisticsCollector = Mockery::mock(StatisticsCollector::class); + $statisticsCollector->shouldReceive('cooldownTriggered')->once(); + + $connection = Mockery::mock(ControlConnection::class); + $connection->authToken = 'regular-user-token'; + $connection->shouldReceive('setMaximumConnectionLength')->once()->with(1); + $connection->shouldReceive('closeWithoutReconnect')->once(); + + $userRepository = Mockery::mock(UserRepository::class); + $userRepository->shouldReceive('setCooldownForToken') + ->once() + ->withArgs(function ($authToken, $cooldownEndsAt) { + $this->assertSame('regular-user-token', $authToken); + $this->assertIsInt($cooldownEndsAt); + $this->assertGreaterThan(time(), $cooldownEndsAt); + + return true; + }) + ->andReturn(\React\Promise\resolve(null)); + + $this->app->instance(UserRepository::class, $userRepository); + + $manager = new ConnectionManager( + Mockery::mock(SubdomainGenerator::class), + $statisticsCollector, + Mockery::mock(LoggerRepository::class), + $loop + ); + + $manager->limitConnectionLength($connection, 1, [ + 'can_specify_subdomains' => 0, + ]); + + $this->assertNotNull($timerCallback); + + $timerCallback(); + } +}