mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
Add statistic tracking
This commit is contained in:
@@ -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
|
||||
|
||||
18
app/Contracts/StatisticsCollector.php
Normal file
18
app/Contracts/StatisticsCollector.php
Normal 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;
|
||||
}
|
||||
10
app/Contracts/StatisticsRepository.php
Normal file
10
app/Contracts/StatisticsRepository.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface StatisticsRepository
|
||||
{
|
||||
public function getStatistics($from, $until): PromiseInterface;
|
||||
}
|
||||
@@ -19,4 +19,6 @@ interface UserRepository
|
||||
public function deleteUser($id): PromiseInterface;
|
||||
|
||||
public function getUsersByTokens(array $authTokens): PromiseInterface;
|
||||
|
||||
public function updateLastSharedAt($id): PromiseInterface;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,12 @@ class ControlMessageController implements MessageComponentInterface
|
||||
if (is_null($user)) {
|
||||
$deferred->reject();
|
||||
} else {
|
||||
$this->userRepository
|
||||
->updateLastSharedAt($user['id'])
|
||||
->then(function () use ($deferred, $user) {
|
||||
$deferred->resolve($user);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
107
app/Server/StatisticsCollector/DatabaseStatisticsCollector.php
Normal file
107
app/Server/StatisticsCollector/DatabaseStatisticsCollector.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD last_shared_at DATETIME;
|
||||
9
database/migrations/07_create_statistics_table.sql
Normal file
9
database/migrations/07_create_statistics_table.sql
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user