Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions app/Connections/ConnectionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion app/Contracts/ConnectionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/ControlMessageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand Down
99 changes: 99 additions & 0 deletions tests/Feature/Server/ConnectionManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Tests\Feature\Server;

use Expose\Server\Connections\ConnectionManager;
use Expose\Server\Connections\ControlConnection;
use Expose\Server\Contracts\LoggerRepository;
use Expose\Server\Contracts\StatisticsCollector;
use Expose\Server\Contracts\SubdomainGenerator;
use Expose\Server\Contracts\UserRepository;
use Mockery;
use React\EventLoop\LoopInterface;
use Tests\Feature\TestCase;

class ConnectionManagerTest extends TestCase
{
/** @test */
public function it_does_not_apply_connection_length_limits_to_users_that_can_specify_subdomains()
{
$loop = Mockery::mock(LoopInterface::class);
$loop->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();
}
}
Loading