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()
{
$subdomain = $this->detectName();
$host = $this->prepareSharedHost($subdomain.'.'.$this->detectTld());
$folderName = $this->detectName();
$host = $this->prepareSharedHost($folderName.'.'.$this->detectTld());
$this->input->setArgument('host', $host);
if (! $this->option('subdomain')) {
$this->input->setOption('subdomain', $subdomain);
$this->input->setOption('subdomain', str_replace('.', '-', $folderName));
}
parent::handle();
@@ -56,7 +56,7 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
}
}
return str_replace('.', '-', basename($projectPath));
return basename($projectPath);
}
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 getUsersByTokens(array $authTokens): PromiseInterface;
public function updateLastSharedAt($id): PromiseInterface;
}

View File

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

View File

@@ -3,6 +3,8 @@
namespace App\Server;
use App\Contracts\ConnectionManager as ConnectionManagerContract;
use App\Contracts\StatisticsCollector;
use App\Contracts\StatisticsRepository;
use App\Contracts\SubdomainGenerator;
use App\Contracts\SubdomainRepository;
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\GetSettingsController;
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\GetUserDetailsController;
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\TunnelMessageController;
use App\Server\Http\Router;
use App\Server\StatisticsCollector\DatabaseStatisticsCollector;
use App\Server\StatisticsRepository\DatabaseStatisticsRepository;
use Clue\React\SQLite\DatabaseInterface;
use Phar;
use Ratchet\Server\IoServer;
@@ -128,6 +133,7 @@ class Factory
$this->router->get('/sites', ListSitesController::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->post('/api/settings', StoreSettingsController::class, $adminCondition);
$this->router->get('/api/users', GetUsersController::class, $adminCondition);
@@ -179,6 +185,7 @@ class Factory
->bindSubdomainRepository()
->bindDatabase()
->ensureDatabaseIsInitialized()
->registerStatisticsCollector()
->bindConnectionManager()
->addAdminRoutes();
@@ -265,4 +272,26 @@ class Factory
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)) {
$deferred->reject();
} else {
$deferred->resolve($user);
$this->userRepository
->updateLastSharedAt($user['id'])
->then(function () use ($deferred, $user) {
$deferred->resolve($user);
});
}
});

View File

@@ -3,6 +3,7 @@
namespace App\Server\Http\Controllers;
use App\Contracts\ConnectionManager;
use App\Contracts\StatisticsCollector;
use App\Http\Controllers\Controller;
use App\Server\Configuration;
use App\Server\Connections\ControlConnection;
@@ -27,10 +28,14 @@ class TunnelMessageController extends Controller
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->configuration = $configuration;
$this->statisticsCollector = $statisticsCollector;
}
public function handle(Request $request, ConnectionInterface $httpConnection)
@@ -57,6 +62,8 @@ class TunnelMessageController extends Controller
return;
}
$this->statisticsCollector->incomingRequest();
$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();
}
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
{
$deferred = new Deferred();

View File

@@ -327,5 +327,13 @@ return [
'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
)