mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
Allow specifying maximum connection counts per user
This commit is contained in:
@@ -29,4 +29,8 @@ interface ConnectionManager
|
||||
public function getConnectionsForAuthToken(string $authToken): array;
|
||||
|
||||
public function getTcpConnectionsForAuthToken(string $authToken): array;
|
||||
|
||||
public function findControlConnectionsForIp(string $ip): array;
|
||||
|
||||
public function findControlConnectionsForAuthToken(string $token): array;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\ConnectionManager as ConnectionManagerContract;
|
||||
use App\Contracts\SubdomainGenerator;
|
||||
use App\Http\QueryParameters;
|
||||
use App\Server\Exceptions\NoFreePortAvailable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Socket\Server;
|
||||
@@ -157,6 +158,20 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
});
|
||||
}
|
||||
|
||||
public function findControlConnectionsForIp(string $ip): array
|
||||
{
|
||||
return collect($this->connections)->filter(function (ControlConnection $connection) use ($ip) {
|
||||
return $connection->socket->remoteAddress == $ip;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function findControlConnectionsForAuthToken(string $token): array
|
||||
{
|
||||
return collect($this->connections)->filter(function (ControlConnection $connection) use ($token) {
|
||||
return $connection->authToken === $token;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
public function getConnections(): array
|
||||
{
|
||||
return $this->connections;
|
||||
|
||||
@@ -41,6 +41,7 @@ class StoreUsersController extends AdminController
|
||||
'auth_token' => (string) Str::uuid(),
|
||||
'can_specify_subdomains' => (int) $request->get('can_specify_subdomains'),
|
||||
'can_share_tcp_ports' => (int) $request->get('can_share_tcp_ports'),
|
||||
'max_connections' => (int) $request->get('max_connections'),
|
||||
];
|
||||
|
||||
$this->userRepository
|
||||
|
||||
@@ -7,11 +7,13 @@ use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Http\QueryParameters;
|
||||
use App\Server\Exceptions\NoFreePortAvailable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\WebSocket\MessageComponentInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
use function React\Promise\reject;
|
||||
|
||||
class ControlMessageController implements MessageComponentInterface
|
||||
{
|
||||
@@ -85,7 +87,35 @@ class ControlMessageController implements MessageComponentInterface
|
||||
|
||||
protected function authenticate(ConnectionInterface $connection, $data)
|
||||
{
|
||||
if (! isset($data->subdomain)) {
|
||||
$data->subdomain = null;
|
||||
}
|
||||
|
||||
$this->verifyAuthToken($connection)
|
||||
->then(function ($user) use ($connection) {
|
||||
$maximumConnectionCount = config('expose.admin.maximum_open_connections_per_user', 0);
|
||||
|
||||
if (is_null($user)) {
|
||||
$connectionCount = count($this->connectionManager->findControlConnectionsForIp($connection->remoteAddress));
|
||||
} else {
|
||||
$maximumConnectionCount = Arr::get($user, 'max_connections', $maximumConnectionCount);
|
||||
|
||||
$connectionCount = count($this->connectionManager->findControlConnectionsForAuthToken($user['auth_token']));
|
||||
}
|
||||
|
||||
if ($maximumConnectionCount > 0 && $connectionCount + 1 > $maximumConnectionCount) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.maximum_connection_count'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
reject(null);
|
||||
}
|
||||
return $user;
|
||||
})
|
||||
->then(function ($user) use ($connection, $data) {
|
||||
if ($data->type === 'http') {
|
||||
$this->handleHttpConnection($connection, $data, $user);
|
||||
|
||||
@@ -132,8 +132,8 @@ class DatabaseUserRepository implements UserRepository
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query("
|
||||
INSERT INTO users (name, auth_token, can_specify_subdomains, can_share_tcp_ports, created_at)
|
||||
VALUES (:name, :auth_token, :can_specify_subdomains, :can_share_tcp_ports, DATETIME('now'))
|
||||
INSERT INTO users (name, auth_token, can_specify_subdomains, can_share_tcp_ports, max_connections, created_at)
|
||||
VALUES (:name, :auth_token, :can_specify_subdomains, :can_share_tcp_ports, :max_connections, DATETIME('now'))
|
||||
", $data)
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $result->insertId])
|
||||
|
||||
BIN
builds/expose
BIN
builds/expose
Binary file not shown.
@@ -224,6 +224,21 @@ return [
|
||||
*/
|
||||
'maximum_connection_length' => 0,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum number of open connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can limit the amount of connections that one client/user can have
|
||||
| open. A maximum connection count of 0 means that clients can open
|
||||
| as many connections as they want.
|
||||
|
|
||||
| When creating users with the API/admin interface, you can
|
||||
| override this setting per user.
|
||||
|
|
||||
*/
|
||||
'maximum_open_connections_per_user' => 0,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Subdomain
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD max_connections INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -63,6 +63,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<label for="max_connections"
|
||||
class="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
|
||||
Maximum Open Connections
|
||||
</label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<input id="max_connections"
|
||||
type="number"
|
||||
min="0"
|
||||
v-model="userForm.max_connections"
|
||||
class="flex-1 border-gray-300 block w-full rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 border-t border-gray-200 pt-5">
|
||||
<div class="flex justify-end">
|
||||
@@ -99,6 +114,9 @@
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Auth-Token
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Max Connections
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Custom Subdomains
|
||||
</th>
|
||||
@@ -119,6 +137,9 @@
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
@{ user.auth_token }
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
@{ user.max_connections }
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
<span v-if="user.can_specify_subdomains === 0">
|
||||
No
|
||||
@@ -186,6 +207,7 @@
|
||||
name: '',
|
||||
can_specify_subdomains: true,
|
||||
can_share_tcp_ports: true,
|
||||
max_connections: 0,
|
||||
errors: {},
|
||||
},
|
||||
paginated: {{ paginated|json_encode|raw }}
|
||||
@@ -242,6 +264,7 @@
|
||||
this.userForm.name = '';
|
||||
this.userForm.can_specify_subdomains = true;
|
||||
this.userForm.can_share_tcp_ports = true;
|
||||
this.userForm.max_connections = 0;
|
||||
this.userForm.errors = {};
|
||||
this.users.unshift(data.user);
|
||||
}
|
||||
|
||||
@@ -135,16 +135,10 @@ class TunnelTest extends TestCase
|
||||
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_share_tcp_ports' => 0,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
|
||||
@@ -163,16 +157,10 @@ class TunnelTest extends TestCase
|
||||
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_share_tcp_ports' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
/**
|
||||
* We create an expose client that connects to our server and shares
|
||||
@@ -213,16 +201,10 @@ class TunnelTest extends TestCase
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
@@ -241,16 +223,10 @@ class TunnelTest extends TestCase
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 0,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
@@ -269,16 +245,10 @@ class TunnelTest extends TestCase
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
@@ -289,16 +259,10 @@ class TunnelTest extends TestCase
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Test-User',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
@@ -318,16 +282,10 @@ class TunnelTest extends TestCase
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
]);
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
@@ -350,20 +308,11 @@ class TunnelTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_clients_to_use_random_subdomains_if_custom_subdomains_are_forbidden()
|
||||
public function it_rejects_clients_with_too_many_connections()
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 0,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = false;
|
||||
$this->app['config']['expose.admin.maximum_open_connections_per_user'] = 1;
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
@@ -372,9 +321,51 @@ class TunnelTest extends TestCase
|
||||
* the created test HTTP server.
|
||||
*/
|
||||
$client = $this->createClient();
|
||||
$response = $this->await($client->connectToServer('127.0.0.1:8085', '', $user->auth_token));
|
||||
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel-1'));
|
||||
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel-2'));
|
||||
}
|
||||
|
||||
$this->assertInstanceOf(\stdClass::class, $response);
|
||||
/** @test */
|
||||
public function it_rejects_users_with_custom_max_connection_limit()
|
||||
{
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
$this->app['config']['expose.admin.maximum_open_connections_per_user'] = 5;
|
||||
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 1,
|
||||
'max_connections' => 2,
|
||||
]);
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
/**
|
||||
* We create an expose client that connects to our server and shares
|
||||
* the created test HTTP server.
|
||||
*/
|
||||
$client = $this->createClient();
|
||||
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel-1', $user->auth_token));
|
||||
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel-2', $user->auth_token));
|
||||
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel-3', $user->auth_token));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_clients_to_use_random_subdomains_if_custom_subdomains_are_forbidden()
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$user = $this->createUser([
|
||||
'name' => 'Marcel',
|
||||
'can_specify_subdomains' => 0,
|
||||
]);
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
$client = $this->createClient();
|
||||
$response = $this->await($client->connectToServer('127.0.0.1:8085', 'foo', $user->auth_token));
|
||||
|
||||
$this->assertNotSame('foo', $response->subdomain);
|
||||
}
|
||||
|
||||
protected function startServer()
|
||||
@@ -405,6 +396,17 @@ class TunnelTest extends TestCase
|
||||
return app(Client::class);
|
||||
}
|
||||
|
||||
protected function createUser(array $data)
|
||||
{
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode($data)));
|
||||
|
||||
return json_decode($response->getBody()->getContents())->user;
|
||||
}
|
||||
|
||||
protected function createTestHttpServer()
|
||||
{
|
||||
$server = new Server($this->loop, function (ServerRequestInterface $request) {
|
||||
|
||||
Reference in New Issue
Block a user