diff --git a/app/Commands/ShareCurrentWorkingDirectoryCommand.php b/app/Commands/ShareCurrentWorkingDirectoryCommand.php index 9c26afc..a901495 100644 --- a/app/Commands/ShareCurrentWorkingDirectoryCommand.php +++ b/app/Commands/ShareCurrentWorkingDirectoryCommand.php @@ -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 diff --git a/app/Contracts/StatisticsCollector.php b/app/Contracts/StatisticsCollector.php new file mode 100644 index 0000000..1e4e285 --- /dev/null +++ b/app/Contracts/StatisticsCollector.php @@ -0,0 +1,18 @@ +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; } diff --git a/app/Server/Factory.php b/app/Server/Factory.php index cb9d967..9d04931 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -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; + } } diff --git a/app/Server/Http/Controllers/Admin/GetStatisticsController.php b/app/Server/Http/Controllers/Admin/GetStatisticsController.php new file mode 100644 index 0000000..8e7c7c4 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/GetStatisticsController.php @@ -0,0 +1,41 @@ +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(); + }); + } +} diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 1b3a8a5..e123818 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -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); + }); + } }); diff --git a/app/Server/Http/Controllers/TunnelMessageController.php b/app/Server/Http/Controllers/TunnelMessageController.php index a592868..3b58ae0 100644 --- a/app/Server/Http/Controllers/TunnelMessageController.php +++ b/app/Server/Http/Controllers/TunnelMessageController.php @@ -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); } diff --git a/app/Server/StatisticsCollector/DatabaseStatisticsCollector.php b/app/Server/StatisticsCollector/DatabaseStatisticsCollector.php new file mode 100644 index 0000000..2f08045 --- /dev/null +++ b/app/Server/StatisticsCollector/DatabaseStatisticsCollector.php @@ -0,0 +1,107 @@ +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); + } +} diff --git a/app/Server/StatisticsRepository/DatabaseStatisticsRepository.php b/app/Server/StatisticsRepository/DatabaseStatisticsRepository.php new file mode 100644 index 0000000..4b5e667 --- /dev/null +++ b/app/Server/StatisticsRepository/DatabaseStatisticsRepository.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/app/Server/UserRepository/DatabaseUserRepository.php b/app/Server/UserRepository/DatabaseUserRepository.php index e5a44e6..23622c2 100644 --- a/app/Server/UserRepository/DatabaseUserRepository.php +++ b/app/Server/UserRepository/DatabaseUserRepository.php @@ -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(); diff --git a/config/expose.php b/config/expose.php index 39dfb58..3ca54fb 100644 --- a/config/expose.php +++ b/config/expose.php @@ -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, + ] ], ]; diff --git a/database/migrations/06_add_last_shared_at_to_users_table.sql b/database/migrations/06_add_last_shared_at_to_users_table.sql new file mode 100644 index 0000000..17bbfe7 --- /dev/null +++ b/database/migrations/06_add_last_shared_at_to_users_table.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD last_shared_at DATETIME; diff --git a/database/migrations/07_create_statistics_table.sql b/database/migrations/07_create_statistics_table.sql new file mode 100644 index 0000000..97e1f60 --- /dev/null +++ b/database/migrations/07_create_statistics_table.sql @@ -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 +)