Add statistic tracking

This commit is contained in:
Marcel Pociot
2021-05-31 14:47:48 +02:00
parent 7f6be8cae2
commit 9444d1aacb
15 changed files with 308 additions and 7 deletions

View File

@@ -8,13 +8,13 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
public function handle() public function handle()
{ {
$subdomain = $this->detectName(); $folderName = $this->detectName();
$host = $this->prepareSharedHost($subdomain.'.'.$this->detectTld()); $host = $this->prepareSharedHost($folderName.'.'.$this->detectTld());
$this->input->setArgument('host', $host); $this->input->setArgument('host', $host);
if (! $this->option('subdomain')) { if (! $this->option('subdomain')) {
$this->input->setOption('subdomain', $subdomain); $this->input->setOption('subdomain', str_replace('.', '-', $folderName));
} }
parent::handle(); parent::handle();
@@ -56,7 +56,7 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
} }
} }
return str_replace('.', '-', basename($projectPath)); return basename($projectPath);
} }
protected function detectProtocol($host): string protected function detectProtocol($host): string

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Contracts;
interface StatisticsCollector
{
public function siteShared($authToken = null);
public function portShared($authToken = null);
public function incomingRequest();
public function flush();
public function save();
public function shouldCollectStatistics(): bool;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Contracts;
use React\Promise\PromiseInterface;
interface StatisticsRepository
{
public function getStatistics($from, $until): PromiseInterface;
}

View File

@@ -19,4 +19,6 @@ interface UserRepository
public function deleteUser($id): PromiseInterface; public function deleteUser($id): PromiseInterface;
public function getUsersByTokens(array $authTokens): PromiseInterface; public function getUsersByTokens(array $authTokens): PromiseInterface;
public function updateLastSharedAt($id): PromiseInterface;
} }

View File

@@ -3,6 +3,7 @@
namespace App\Server\Connections; namespace App\Server\Connections;
use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\StatisticsCollector;
use App\Contracts\SubdomainGenerator; use App\Contracts\SubdomainGenerator;
use App\Http\QueryParameters; use App\Http\QueryParameters;
use App\Server\Exceptions\NoFreePortAvailable; use App\Server\Exceptions\NoFreePortAvailable;
@@ -25,10 +26,14 @@ class ConnectionManager implements ConnectionManagerContract
/** @var LoopInterface */ /** @var LoopInterface */
protected $loop; protected $loop;
public function __construct(SubdomainGenerator $subdomainGenerator, LoopInterface $loop) /** @var StatisticsCollector */
protected $statisticsCollector;
public function __construct(SubdomainGenerator $subdomainGenerator, StatisticsCollector $statisticsCollector, LoopInterface $loop)
{ {
$this->subdomainGenerator = $subdomainGenerator; $this->subdomainGenerator = $subdomainGenerator;
$this->loop = $loop; $this->loop = $loop;
$this->statisticsCollector = $statisticsCollector;
} }
public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength) public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength)
@@ -60,6 +65,8 @@ class ConnectionManager implements ConnectionManagerContract
$this->connections[] = $storedConnection; $this->connections[] = $storedConnection;
$this->statisticsCollector->siteShared($this->getAuthTokenFromConnection($connection));
return $storedConnection; return $storedConnection;
} }
@@ -79,6 +86,8 @@ class ConnectionManager implements ConnectionManagerContract
$this->connections[] = $storedConnection; $this->connections[] = $storedConnection;
$this->statisticsCollector->portShared($this->getAuthTokenFromConnection($connection));
return $storedConnection; return $storedConnection;
} }

View File

@@ -3,6 +3,8 @@
namespace App\Server; namespace App\Server;
use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\StatisticsCollector;
use App\Contracts\StatisticsRepository;
use App\Contracts\SubdomainGenerator; use App\Contracts\SubdomainGenerator;
use App\Contracts\SubdomainRepository; use App\Contracts\SubdomainRepository;
use App\Contracts\UserRepository; use App\Contracts\UserRepository;
@@ -15,6 +17,7 @@ use App\Server\Http\Controllers\Admin\DisconnectSiteController;
use App\Server\Http\Controllers\Admin\DisconnectTcpConnectionController; use App\Server\Http\Controllers\Admin\DisconnectTcpConnectionController;
use App\Server\Http\Controllers\Admin\GetSettingsController; use App\Server\Http\Controllers\Admin\GetSettingsController;
use App\Server\Http\Controllers\Admin\GetSitesController; use App\Server\Http\Controllers\Admin\GetSitesController;
use App\Server\Http\Controllers\Admin\GetStatisticsController;
use App\Server\Http\Controllers\Admin\GetTcpConnectionsController; use App\Server\Http\Controllers\Admin\GetTcpConnectionsController;
use App\Server\Http\Controllers\Admin\GetUserDetailsController; use App\Server\Http\Controllers\Admin\GetUserDetailsController;
use App\Server\Http\Controllers\Admin\GetUsersController; use App\Server\Http\Controllers\Admin\GetUsersController;
@@ -29,6 +32,8 @@ use App\Server\Http\Controllers\Admin\StoreUsersController;
use App\Server\Http\Controllers\ControlMessageController; use App\Server\Http\Controllers\ControlMessageController;
use App\Server\Http\Controllers\TunnelMessageController; use App\Server\Http\Controllers\TunnelMessageController;
use App\Server\Http\Router; use App\Server\Http\Router;
use App\Server\StatisticsCollector\DatabaseStatisticsCollector;
use App\Server\StatisticsRepository\DatabaseStatisticsRepository;
use Clue\React\SQLite\DatabaseInterface; use Clue\React\SQLite\DatabaseInterface;
use Phar; use Phar;
use Ratchet\Server\IoServer; use Ratchet\Server\IoServer;
@@ -128,6 +133,7 @@ class Factory
$this->router->get('/sites', ListSitesController::class, $adminCondition); $this->router->get('/sites', ListSitesController::class, $adminCondition);
$this->router->get('/tcp', ListTcpConnectionsController::class, $adminCondition); $this->router->get('/tcp', ListTcpConnectionsController::class, $adminCondition);
$this->router->get('/api/statistics', GetStatisticsController::class, $adminCondition);
$this->router->get('/api/settings', GetSettingsController::class, $adminCondition); $this->router->get('/api/settings', GetSettingsController::class, $adminCondition);
$this->router->post('/api/settings', StoreSettingsController::class, $adminCondition); $this->router->post('/api/settings', StoreSettingsController::class, $adminCondition);
$this->router->get('/api/users', GetUsersController::class, $adminCondition); $this->router->get('/api/users', GetUsersController::class, $adminCondition);
@@ -179,6 +185,7 @@ class Factory
->bindSubdomainRepository() ->bindSubdomainRepository()
->bindDatabase() ->bindDatabase()
->ensureDatabaseIsInitialized() ->ensureDatabaseIsInitialized()
->registerStatisticsCollector()
->bindConnectionManager() ->bindConnectionManager()
->addAdminRoutes(); ->addAdminRoutes();
@@ -265,4 +272,26 @@ class Factory
return $this; return $this;
} }
protected function registerStatisticsCollector()
{
if (config('expose.admin.statistics.enable_statistics', true) === false) {
return;
}
app()->singleton(StatisticsRepository::class, function () {
return app(config('expose.admin.statistics.repository', DatabaseStatisticsRepository::class));
});
app()->singleton(StatisticsCollector::class, function () {
return app(DatabaseStatisticsCollector::class);
});
$intervalInSeconds = config('expose.admin.statistics.interval_in_seconds', 3600);
$this->loop->addPeriodicTimer($intervalInSeconds, function () {
app(StatisticsCollector::class)->save();
});
return $this;
}
} }

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Server\Http\Controllers\Admin;
use App\Contracts\ConnectionManager;
use App\Contracts\StatisticsRepository;
use App\Contracts\UserRepository;
use App\Server\Configuration;
use App\Server\Connections\ControlConnection;
use Illuminate\Http\Request;
use Ratchet\ConnectionInterface;
class GetStatisticsController extends AdminController
{
protected $keepConnectionOpen = true;
/** @var StatisticsRepository */
protected $statisticsRepository;
public function __construct(StatisticsRepository $statisticsRepository)
{
$this->statisticsRepository = $statisticsRepository;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$from = today()->subWeek()->toDateString();
$until = today()->toDateString();
$this->statisticsRepository->getStatistics($request->get('from', $from), $request->get('until', $until))
->then(function ($statistics) use ($httpConnection) {
$httpConnection->send(
respond_json([
'statistics' => $statistics,
])
);
$httpConnection->close();
});
}
}

View File

@@ -235,7 +235,12 @@ class ControlMessageController implements MessageComponentInterface
if (is_null($user)) { if (is_null($user)) {
$deferred->reject(); $deferred->reject();
} else { } else {
$this->userRepository
->updateLastSharedAt($user['id'])
->then(function () use ($deferred, $user) {
$deferred->resolve($user); $deferred->resolve($user);
});
} }
}); });

View File

@@ -3,6 +3,7 @@
namespace App\Server\Http\Controllers; namespace App\Server\Http\Controllers;
use App\Contracts\ConnectionManager; use App\Contracts\ConnectionManager;
use App\Contracts\StatisticsCollector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Server\Configuration; use App\Server\Configuration;
use App\Server\Connections\ControlConnection; use App\Server\Connections\ControlConnection;
@@ -27,10 +28,14 @@ class TunnelMessageController extends Controller
protected $modifiers = []; protected $modifiers = [];
public function __construct(ConnectionManager $connectionManager, Configuration $configuration) /** @var StatisticsCollector */
protected $statisticsCollector;
public function __construct(ConnectionManager $connectionManager, StatisticsCollector $statisticsCollector, Configuration $configuration)
{ {
$this->connectionManager = $connectionManager; $this->connectionManager = $connectionManager;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->statisticsCollector = $statisticsCollector;
} }
public function handle(Request $request, ConnectionInterface $httpConnection) public function handle(Request $request, ConnectionInterface $httpConnection)
@@ -57,6 +62,8 @@ class TunnelMessageController extends Controller
return; return;
} }
$this->statisticsCollector->incomingRequest();
$this->sendRequestToClient($request, $controlConnection, $httpConnection); $this->sendRequestToClient($request, $controlConnection, $httpConnection);
} }

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Server\StatisticsCollector;
use App\Contracts\ConnectionManager;
use App\Contracts\StatisticsCollector;
use Clue\React\SQLite\DatabaseInterface;
class DatabaseStatisticsCollector implements StatisticsCollector
{
/** @var DatabaseInterface */
protected $database;
/** @var array */
protected $sharedPorts = [];
/** @var array */
protected $sharedSites = [];
/** @var int */
protected $requests = 0;
public function __construct(DatabaseInterface $database,)
{
$this->database = $database;
}
/**
* Flush the stored statistics.
*
* @return void
*/
public function flush()
{
$this->sharedPorts = [];
$this->sharedSites = [];
$this->requests = 0;
}
public function siteShared($authToken = null)
{
if (! $this->shouldCollectStatistics()) {
return;
}
if (! isset($this->sharedSites[$authToken])) {
$this->sharedSites[$authToken] = 0;
}
$this->sharedSites[$authToken]++;
}
public function portShared($authToken = null)
{
if (! $this->shouldCollectStatistics()) {
return;
}
if (! isset($this->sharedPorts[$authToken])) {
$this->sharedPorts[$authToken] = 0;
}
$this->sharedPorts[$authToken]++;
}
public function incomingRequest()
{
if (! $this->shouldCollectStatistics()) {
return;
}
$this->requests++;
}
public function save()
{
$sharedSites = 0;
collect($this->sharedSites)->map(function ($numSites) use (&$sharedSites) {
$sharedSites += $numSites;
});
$sharedPorts = 0;
collect($this->sharedPorts)->map(function ($numPorts) use (&$sharedPorts) {
$sharedPorts += $numPorts;
});
$this->database->query("
INSERT INTO statistics (timestamp, shared_sites, shared_ports, unique_shared_sites, unique_shared_ports, incoming_requests)
VALUES (:timestamp, :shared_sites, :shared_ports, :unique_shared_sites, :unique_shared_ports, :incoming_requests)
", [
'timestamp' => today()->toDateString(),
'shared_sites' => $sharedSites,
'shared_ports' => $sharedPorts,
'unique_shared_sites' => count($this->sharedSites),
'unique_shared_ports' => count($this->sharedPorts),
'incoming_requests' => $this->requests,
])
->then(function () {
$this->flush();
});
}
public function shouldCollectStatistics(): bool
{
return config('expose.admin.statistics.enable_statistics', true);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Server\StatisticsRepository;
use App\Contracts\StatisticsRepository;
use Clue\React\SQLite\DatabaseInterface;
use Clue\React\SQLite\Result;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
class DatabaseStatisticsRepository implements StatisticsRepository
{
/** @var DatabaseInterface */
protected $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getStatistics($from, $until): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query('SELECT
timestamp,
SUM(shared_sites) as shared_sites,
SUM(shared_ports) as shared_ports,
SUM(unique_shared_sites) as unique_shared_sites,
SUM(unique_shared_ports) as unique_shared_ports,
SUM(incoming_requests) as incoming_requests
FROM statistics
WHERE
`timestamp` >= "' . $from . '" AND `timestamp` <= "' . $until . '"')
->then(function (Result $result) use ($deferred) {
$deferred->resolve($result->rows);
});
return $deferred->promise();
}
}

View File

@@ -114,6 +114,19 @@ class DatabaseUserRepository implements UserRepository
return $deferred->promise(); return $deferred->promise();
} }
public function updateLastSharedAt($id): PromiseInterface
{
$deferred = new Deferred();
$this->database
->query("UPDATE users SET last_shared_at = date('now') WHERE id = :id", ["id" => $id])
->then(function (Result $result) use ($deferred) {
$deferred->resolve();
});
return $deferred->promise();
}
public function getUserByToken(string $authToken): PromiseInterface public function getUserByToken(string $authToken): PromiseInterface
{ {
$deferred = new Deferred(); $deferred = new Deferred();

View File

@@ -327,5 +327,13 @@ return [
'tcp_port_sharing_disabled' => 'TCP port sharing is not available on this Expose server.', 'tcp_port_sharing_disabled' => 'TCP port sharing is not available on this Expose server.',
], ],
'statistics' => [
'enable_statistics' => true,
'interval_in_seconds' => 3600,
'repository' => \App\Server\StatisticsRepository\DatabaseStatisticsRepository::class,
]
], ],
]; ];

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD last_shared_at DATETIME;

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS statistics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATE,
shared_sites INTEGER,
shared_ports INTEGER,
unique_shared_sites INTEGER,
unique_shared_ports INTEGER,
incoming_requests INTEGER
)