mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 21:45:55 +00:00
Compare commits
227 Commits
associate-
...
2.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b0b98e85 | ||
|
|
814eb964d5 | ||
|
|
da82980286 | ||
|
|
9f03b8e5ac | ||
|
|
2d3b10b63d | ||
|
|
5d99a0d7d8 | ||
|
|
ecacc69d34 | ||
|
|
729d1fc817 | ||
|
|
177088dcf5 | ||
|
|
c44f14d28a | ||
|
|
e079a6320c | ||
|
|
c828c3c0d2 | ||
|
|
9aa6d5d3f9 | ||
|
|
119d0826e4 | ||
|
|
c2b0b62a8b | ||
|
|
428356badb | ||
|
|
e375d21e4c | ||
|
|
4e2eda036a | ||
|
|
7fadb687cc | ||
|
|
3cc290998d | ||
|
|
117424cf0e | ||
|
|
83f49d49c2 | ||
|
|
26541d4af9 | ||
|
|
00b379c417 | ||
|
|
1f09672b51 | ||
|
|
cd625e4840 | ||
|
|
813f742c20 | ||
|
|
89c9fa6742 | ||
|
|
3aa4847d33 | ||
|
|
76ce21aebb | ||
|
|
816652e527 | ||
|
|
a199aa8576 | ||
|
|
92c4c2ffe1 | ||
|
|
9304b93775 | ||
|
|
f2793bcef9 | ||
|
|
8b7df07b27 | ||
|
|
10e431cb26 | ||
|
|
408e9e470e | ||
|
|
9dd82bf1dc | ||
|
|
fd7f4ee43b | ||
|
|
22c2f090e2 | ||
|
|
d34f6d1300 | ||
|
|
12411c4fb5 | ||
|
|
42044f35d9 | ||
|
|
c8c47e8bf6 | ||
|
|
e292b1ad3d | ||
|
|
e169a3a7c9 | ||
|
|
f67cc87f75 | ||
|
|
70416dcb18 | ||
|
|
b9b07c9664 | ||
|
|
97993318e7 | ||
|
|
074051c4d1 | ||
|
|
5a1f3ab2ff | ||
|
|
3dd0148895 | ||
|
|
d83104567d | ||
|
|
9b3398db8f | ||
|
|
48c759a7d9 | ||
|
|
71ce328eb0 | ||
|
|
490365fe14 | ||
|
|
7797814ebf | ||
|
|
e553cbb957 | ||
|
|
19afa3cdea | ||
|
|
90acf38b08 | ||
|
|
1450342fcc | ||
|
|
4cbabeaff3 | ||
|
|
1947d1daab | ||
|
|
8dd1254555 | ||
|
|
147da22c0d | ||
|
|
cacdf1e268 | ||
|
|
5470fe8432 | ||
|
|
d8f482bf57 | ||
|
|
079c880fd1 | ||
|
|
780a25df91 | ||
|
|
bdc4493ff8 | ||
|
|
cbe5c3014f | ||
|
|
d3151fd12b | ||
|
|
73d0421c88 | ||
|
|
77e0b17151 | ||
|
|
136e435403 | ||
|
|
3a9d4fb6b6 | ||
|
|
c81cca6e0e | ||
|
|
856163e267 | ||
|
|
c2845a3e13 | ||
|
|
8d9500abeb | ||
|
|
bbeaa1f0f1 | ||
|
|
f72c0d546b | ||
|
|
9943132704 | ||
|
|
1a78982dcb | ||
|
|
dc29623bb4 | ||
|
|
bb87ef0adf | ||
|
|
4163975022 | ||
|
|
0b07c3b2a3 | ||
|
|
c395ec16ae | ||
|
|
a33aaccc84 | ||
|
|
dfc26570b2 | ||
|
|
19b6f35c48 | ||
|
|
7ff697a09d | ||
|
|
84936ae63f | ||
|
|
c8171de2d2 | ||
|
|
520a5afb1f | ||
|
|
5ad9f01e55 | ||
|
|
4aecb04397 | ||
|
|
d22e63701f | ||
|
|
ddf99cd7c8 | ||
|
|
19606a78af | ||
|
|
7742658527 | ||
|
|
c9cb29ed35 | ||
|
|
37349493ab | ||
|
|
d610705af7 | ||
|
|
b9200e3790 | ||
|
|
fd66366438 | ||
|
|
361f5f0b0d | ||
|
|
1d97d63d2b | ||
|
|
95098c180d | ||
|
|
6ffd5274b3 | ||
|
|
7c78f7e2b1 | ||
|
|
4d80b14551 | ||
|
|
98482a6ce2 | ||
|
|
c531d41e03 | ||
|
|
1f2e21548c | ||
|
|
aa08029fc3 | ||
|
|
afebe13f00 | ||
|
|
1fdcc50d4a | ||
|
|
c06bcb7119 | ||
|
|
eefd74e82c | ||
|
|
de9b85df49 | ||
|
|
8be8aff802 | ||
|
|
21a9117dd6 | ||
|
|
351253cc19 | ||
|
|
6b9fee9326 | ||
|
|
2a439371be | ||
|
|
fb05f23124 | ||
|
|
f565241740 | ||
|
|
8c5b52769e | ||
|
|
98ced10737 | ||
|
|
cf74165479 | ||
|
|
4131b6abb7 | ||
|
|
8664d7ea80 | ||
|
|
d4dbdba4c6 | ||
|
|
b9719ea420 | ||
|
|
c3896e0ba2 | ||
|
|
078a656a21 | ||
|
|
e8de146dc5 | ||
|
|
8d8297cf71 | ||
|
|
01843173bc | ||
|
|
2c90707e28 | ||
|
|
01ce0d09e3 | ||
|
|
2c8804cff3 | ||
|
|
74ac9d2d1a | ||
|
|
e2da5652e5 | ||
|
|
6d391c9246 | ||
|
|
c23af81668 | ||
|
|
44b100b340 | ||
|
|
a83a57ca34 | ||
|
|
fc5ac1c53f | ||
|
|
5e54d0a80f | ||
|
|
a29874e221 | ||
|
|
6b02eafc87 | ||
|
|
9623793df5 | ||
|
|
62aa85f092 | ||
|
|
3de6ee1a1e | ||
|
|
bbbabcebaf | ||
|
|
3c07660c2c | ||
|
|
21e47bde81 | ||
|
|
ad2ef94958 | ||
|
|
5de11e90f7 | ||
|
|
9444d1aacb | ||
|
|
de0ada67e3 | ||
|
|
400361dd71 | ||
|
|
7f6be8cae2 | ||
|
|
a3d1735b6e | ||
|
|
b44d5f9c46 | ||
|
|
6c0aa790e5 | ||
|
|
717e8cf05c | ||
|
|
9220e83798 | ||
|
|
9e67b5ef5d | ||
|
|
2faacd58c5 | ||
|
|
9342a5ce36 | ||
|
|
db57f83bdf | ||
|
|
c1f7125f72 | ||
|
|
96fa7c653f | ||
|
|
44dca53687 | ||
|
|
60af8bce19 | ||
|
|
60ce7816a2 | ||
|
|
6e9c3503e3 | ||
|
|
8d5a4410f7 | ||
|
|
cca03ab8b2 | ||
|
|
a9699eb254 | ||
|
|
21ed707718 | ||
|
|
9687c92463 | ||
|
|
5c141986fe | ||
|
|
986428fe00 | ||
|
|
2934731c7a | ||
|
|
e2e9edf769 | ||
|
|
06c1758916 | ||
|
|
ff232d9ef4 | ||
|
|
5b8cc4d985 | ||
|
|
26a805c552 | ||
|
|
28cc353c30 | ||
|
|
7bfb618d93 | ||
|
|
f6d04777e1 | ||
|
|
bded9f754e | ||
|
|
c92d4b258c | ||
|
|
eb8d1f4f91 | ||
|
|
da39fb8ad8 | ||
|
|
5b7a80bb0c | ||
|
|
548c29772a | ||
|
|
844a3cd15a | ||
|
|
f5c009eadd | ||
|
|
7459c0189b | ||
|
|
8b8426cd3b | ||
|
|
e773dfa689 | ||
|
|
c56f05c030 | ||
|
|
ce945e1326 | ||
|
|
880259657f | ||
|
|
538c7da446 | ||
|
|
26de32d375 | ||
|
|
2f57fa1952 | ||
|
|
c8cfe7b8b4 | ||
|
|
ab316f6bdc | ||
|
|
b071e81b1d | ||
|
|
d9ab55f308 | ||
|
|
0ebe6a4ce4 | ||
|
|
faa3309c70 | ||
|
|
74236b6863 | ||
|
|
b1d23e1f75 | ||
|
|
e52659bf59 |
36
.github/workflows/run-tests.yml
vendored
Normal file
36
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
php: [8.0, 8.1]
|
||||
stability: [prefer-stable]
|
||||
|
||||
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
|
||||
coverage: none
|
||||
|
||||
- name: Setup problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
|
||||
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||
- name: Install dependencies
|
||||
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction
|
||||
|
||||
- name: Execute tests
|
||||
run: vendor/bin/phpunit
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM php:7.4-cli
|
||||
FROM php:8.0-cli
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y git libzip-dev zip
|
||||
@@ -20,4 +20,6 @@ ENV username=username
|
||||
ENV password=password
|
||||
ENV exposeConfigPath=/src/config/expose.php
|
||||
|
||||
CMD sed -i "s|username|${username}|g" ${exposeConfigPath} && sed -i "s|password|${password}|g" ${exposeConfigPath} && php expose serve ${domain} --port ${port} --validateAuthTokens
|
||||
COPY docker-entrypoint.sh /usr/bin/
|
||||
RUN chmod 755 /usr/bin/docker-entrypoint.sh
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
12
README.md
12
README.md
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
# Expose
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
[](https://scrutinizer-ci.com/g/beyondcode/expose)
|
||||
[](https://packagist.org/packages/beyondcode/expose)
|
||||
|
||||
A completely open-source ngrok alternative - written in pure PHP.
|
||||
An open-source ngrok alternative - written in PHP.
|
||||
|
||||
## ⭐️ Managed Expose & Expose Pro ⭐️
|
||||
|
||||
You can use a managed version with our proprietary platform and our free (EU) test server at the [official website](https://expose.dev). Upgrade to Expose Pro to use our global server network with your own custom domains and get high-speed tunnels all over the world.
|
||||
|
||||
[Create an account](https://expose.dev)
|
||||
|
||||
## Documentation
|
||||
|
||||
For installation instructions, in-depth usage and deployment details, please take a look at the [official documentation](https://beyondco.de/docs/expose/).
|
||||
For installation instructions of your own server, in-depth usage and deployment details, please take a look at the [official documentation](https://expose.dev/docs).
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
31
app/Client/Callbacks/WebHookConnectionCallback.php
Normal file
31
app/Client/Callbacks/WebHookConnectionCallback.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Callbacks;
|
||||
|
||||
use App\Server\Connections\ControlConnection;
|
||||
use Clue\React\Buzz\Browser;
|
||||
|
||||
class WebHookConnectionCallback
|
||||
{
|
||||
/** @var Browser */
|
||||
protected $browser;
|
||||
|
||||
public function __construct(Browser $browser)
|
||||
{
|
||||
$this->browser = $browser;
|
||||
}
|
||||
|
||||
public function handle(ControlConnection $connection)
|
||||
{
|
||||
$this->browser->post(config('expose.admin.connection_callbacks.webhook.url'), [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'X-Signature' => $this->generateWebhookSigningSecret($connection),
|
||||
], json_encode($connection->toArray()));
|
||||
}
|
||||
|
||||
protected function generateWebhookSigningSecret(ControlConnection $connection)
|
||||
{
|
||||
return hash_hmac('sha256', $connection->client_id, config('expose.admin.connection_callbacks.webhook.secret'));
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ class Client
|
||||
/** @var int */
|
||||
protected $timeConnected = 0;
|
||||
|
||||
/** @var bool */
|
||||
protected $shouldExit = true;
|
||||
|
||||
public static $user = [];
|
||||
public static $subdomains = [];
|
||||
|
||||
public function __construct(LoopInterface $loop, Configuration $configuration, CliRequestLogger $logger)
|
||||
@@ -40,15 +44,25 @@ class Client
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function share(string $sharedUrl, array $subdomains = [])
|
||||
public function shouldExit($shouldExit = true)
|
||||
{
|
||||
$this->shouldExit = $shouldExit;
|
||||
}
|
||||
|
||||
public function share(string $sharedUrl, array $subdomains = [], $serverHost = null)
|
||||
{
|
||||
$sharedUrl = $this->prepareSharedUrl($sharedUrl);
|
||||
|
||||
foreach ($subdomains as $subdomain) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, config('expose.auth_token'));
|
||||
$this->connectToServer($sharedUrl, $subdomain, $serverHost, $this->configuration->auth());
|
||||
}
|
||||
}
|
||||
|
||||
public function sharePort(int $port)
|
||||
{
|
||||
$this->connectToServerAndShareTcp($port, $this->configuration->auth());
|
||||
}
|
||||
|
||||
protected function prepareSharedUrl(string $sharedUrl): string
|
||||
{
|
||||
if (! $parsedUrl = parse_url($sharedUrl)) {
|
||||
@@ -67,34 +81,34 @@ class Client
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function connectToServer(string $sharedUrl, $subdomain, $authToken = ''): PromiseInterface
|
||||
public function connectToServer(string $sharedUrl, $subdomain, $serverHost = null, $authToken = ''): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
$promise = $deferred->promise();
|
||||
|
||||
$exposeVersion = config('app.version');
|
||||
|
||||
$wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
|
||||
|
||||
connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}", [], [
|
||||
connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}&version={$exposeVersion}", [], [
|
||||
'X-Expose-Control' => 'enabled',
|
||||
], $this->loop)
|
||||
->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $deferred, $authToken) {
|
||||
->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain, $serverHost, $deferred, $authToken) {
|
||||
$this->connectionRetries = 0;
|
||||
|
||||
$connection = ControlConnection::create($clientConnection);
|
||||
|
||||
$connection->authenticate($sharedUrl, $subdomain);
|
||||
$connection->authenticate($sharedUrl, $subdomain, $serverHost);
|
||||
|
||||
$clientConnection->on('close', function () use ($sharedUrl, $subdomain, $authToken) {
|
||||
$clientConnection->on('close', function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
|
||||
$this->logger->error('Connection to server closed.');
|
||||
|
||||
$this->retryConnectionOrExit($sharedUrl, $subdomain, $authToken);
|
||||
$this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, $serverHost, $authToken);
|
||||
});
|
||||
});
|
||||
|
||||
$connection->on('authenticationFailed', function ($data) use ($deferred) {
|
||||
$this->logger->error($data->message);
|
||||
|
||||
$this->exit($deferred);
|
||||
});
|
||||
$this->attachCommonConnectionListeners($connection, $deferred);
|
||||
|
||||
$connection->on('subdomainTaken', function ($data) use ($deferred) {
|
||||
$this->logger->error($data->message);
|
||||
@@ -102,41 +116,32 @@ class Client
|
||||
$this->exit($deferred);
|
||||
});
|
||||
|
||||
$connection->on('setMaximumConnectionLength', function ($data) {
|
||||
$timeoutSection = $this->logger->getOutput()->section();
|
||||
|
||||
$this->loop->addPeriodicTimer(1, function () use ($data, $timeoutSection) {
|
||||
$this->timeConnected++;
|
||||
|
||||
$secondsRemaining = $data->length * 60 - $this->timeConnected;
|
||||
$remaining = Carbon::now()->diff(Carbon::now()->addSeconds($secondsRemaining));
|
||||
|
||||
$timeoutSection->clear();
|
||||
$timeoutSection->writeln('Remaining time: '.$remaining->format('%H:%I:%S'));
|
||||
});
|
||||
});
|
||||
|
||||
$connection->on('authenticated', function ($data) use ($deferred, $sharedUrl) {
|
||||
$httpProtocol = $this->configuration->port() === 443 ? 'https' : 'http';
|
||||
$host = $this->configuration->host();
|
||||
|
||||
if ($httpProtocol !== 'https') {
|
||||
$host .= ":{$this->configuration->port()}";
|
||||
}
|
||||
$httpPort = $httpProtocol === 'https' ? '' : ":{$this->configuration->port()}";
|
||||
|
||||
$host = $data->server_host ?? $this->configuration->host();
|
||||
|
||||
$this->configuration->setServerHost($host);
|
||||
|
||||
$this->logger->info($data->message);
|
||||
$this->logger->info("Local-URL:\t\t{$sharedUrl}");
|
||||
$this->logger->info("Dashboard-URL:\t\thttp://127.0.0.1:".config()->get('expose.dashboard_port'));
|
||||
$this->logger->info("Expose-URL:\t\t{$httpProtocol}://{$data->subdomain}.{$host}");
|
||||
$this->logger->info("Shared URL:\t\t<options=bold>{$sharedUrl}</>");
|
||||
$this->logger->info("Dashboard:\t\t<options=bold>http://127.0.0.1:".config()->get('expose.dashboard_port').'</>');
|
||||
$this->logger->info("Public HTTP:\t\t<options=bold>http://{$data->subdomain}.{$host}{$httpPort}</>");
|
||||
$this->logger->info("Public HTTPS:\t\t<options=bold>https://{$data->subdomain}.{$host}</>");
|
||||
$this->logger->line('');
|
||||
|
||||
static::$subdomains[] = "{$httpProtocol}://{$data->subdomain}.{$host}";
|
||||
static::$user = $data->user ?? ['can_specify_subdomains' => 0];
|
||||
|
||||
$deferred->resolve($data);
|
||||
});
|
||||
}, function (\Exception $e) use ($deferred, $sharedUrl, $subdomain, $authToken) {
|
||||
if ($this->connectionRetries > 0) {
|
||||
$this->retryConnectionOrExit($sharedUrl, $subdomain, $authToken);
|
||||
$this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $authToken) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, $authToken);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -149,24 +154,117 @@ class Client
|
||||
return $promise;
|
||||
}
|
||||
|
||||
public function connectToServerAndShareTcp(int $port, $authToken = ''): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
$promise = $deferred->promise();
|
||||
|
||||
$wsProtocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
|
||||
$exposeVersion = config('app.version');
|
||||
|
||||
connect($wsProtocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control?authToken={$authToken}&version={$exposeVersion}", [], [
|
||||
'X-Expose-Control' => 'enabled',
|
||||
], $this->loop)
|
||||
->then(function (WebSocket $clientConnection) use ($port, $deferred, $authToken) {
|
||||
$this->connectionRetries = 0;
|
||||
|
||||
$connection = ControlConnection::create($clientConnection);
|
||||
|
||||
$connection->authenticateTcp($port);
|
||||
|
||||
$this->attachCommonConnectionListeners($connection, $deferred);
|
||||
|
||||
$clientConnection->on('close', function () use ($port, $authToken) {
|
||||
$this->logger->error('Connection to server closed.');
|
||||
|
||||
$this->retryConnectionOrExit(function () use ($port, $authToken) {
|
||||
$this->connectToServerAndShareTcp($port, $authToken);
|
||||
});
|
||||
});
|
||||
|
||||
$connection->on('authenticated', function ($data) use ($deferred, $port) {
|
||||
$host = $this->configuration->host();
|
||||
|
||||
$this->logger->info($data->message);
|
||||
$this->logger->info("Local-Port:\t\t<options=bold>{$port}</>");
|
||||
$this->logger->info("Shared-Port:\t\t<options=bold>{$data->shared_port}</>");
|
||||
$this->logger->info("Expose-URL:\t\t<options=bold>tcp://{$host}:{$data->shared_port}</>");
|
||||
$this->logger->line('');
|
||||
|
||||
$deferred->resolve($data);
|
||||
});
|
||||
}, function (\Exception $e) use ($deferred, $port, $authToken) {
|
||||
if ($this->connectionRetries > 0) {
|
||||
$this->retryConnectionOrExit(function () use ($port, $authToken) {
|
||||
$this->connectToServerAndShareTcp($port, $authToken);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
$this->logger->error('Could not connect to the server.');
|
||||
$this->logger->error($e->getMessage());
|
||||
|
||||
$this->exit($deferred);
|
||||
});
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
protected function attachCommonConnectionListeners(ControlConnection $connection, Deferred $deferred)
|
||||
{
|
||||
$connection->on('info', function ($data) {
|
||||
$this->logger->info($data->message);
|
||||
});
|
||||
|
||||
$connection->on('warning', function ($data) {
|
||||
$this->logger->warn($data->message);
|
||||
});
|
||||
|
||||
$connection->on('error', function ($data) {
|
||||
$this->logger->error($data->message);
|
||||
});
|
||||
|
||||
$connection->on('authenticationFailed', function ($data) use ($deferred) {
|
||||
$this->logger->error($data->message);
|
||||
|
||||
$this->exit($deferred);
|
||||
});
|
||||
|
||||
$connection->on('setMaximumConnectionLength', function ($data) {
|
||||
$timeoutSection = $this->logger->getOutput()->section();
|
||||
|
||||
$this->loop->addPeriodicTimer(1, function () use ($data, $timeoutSection) {
|
||||
$this->timeConnected++;
|
||||
|
||||
$secondsRemaining = $data->length * 60 - $this->timeConnected;
|
||||
$remaining = Carbon::now()->diff(Carbon::now()->addSeconds($secondsRemaining));
|
||||
|
||||
$timeoutSection->clear();
|
||||
$timeoutSection->writeln('Remaining time: '.$remaining->format('%H:%I:%S'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected function exit(Deferred $deferred)
|
||||
{
|
||||
$deferred->reject();
|
||||
|
||||
$this->loop->futureTick(function () {
|
||||
exit(1);
|
||||
if ($this->shouldExit) {
|
||||
exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function retryConnectionOrExit(string $sharedUrl, $subdomain, $authToken = '')
|
||||
protected function retryConnectionOrExit(callable $retry)
|
||||
{
|
||||
$this->connectionRetries++;
|
||||
|
||||
if ($this->connectionRetries <= static::MAX_CONNECTION_RETRIES) {
|
||||
$this->loop->addTimer($this->connectionRetries, function () use ($sharedUrl, $subdomain, $authToken) {
|
||||
$this->loop->addTimer($this->connectionRetries, function () use ($retry) {
|
||||
$this->logger->info("Retrying connection ({$this->connectionRetries}/".static::MAX_CONNECTION_RETRIES.')');
|
||||
|
||||
$this->connectToServer($sharedUrl, $subdomain, $authToken);
|
||||
$retry();
|
||||
});
|
||||
} else {
|
||||
exit(1);
|
||||
|
||||
@@ -7,19 +7,27 @@ class Configuration
|
||||
/** @var string */
|
||||
protected $host;
|
||||
|
||||
/** @var string */
|
||||
protected $serverHost;
|
||||
|
||||
/** @var int */
|
||||
protected $port;
|
||||
|
||||
/** @var string|null */
|
||||
protected $auth;
|
||||
|
||||
public function __construct(string $host, int $port, ?string $auth = null)
|
||||
/** @var string|null */
|
||||
protected $basicAuth;
|
||||
|
||||
public function __construct(string $host, int $port, ?string $auth = null, ?string $basicAuth = null)
|
||||
{
|
||||
$this->host = $host;
|
||||
$this->serverHost = $this->host = $host;
|
||||
|
||||
$this->port = $port;
|
||||
|
||||
$this->auth = $auth;
|
||||
|
||||
$this->basicAuth = $basicAuth;
|
||||
}
|
||||
|
||||
public function host(): string
|
||||
@@ -27,13 +35,40 @@ class Configuration
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function serverHost(): string
|
||||
{
|
||||
return $this->serverHost;
|
||||
}
|
||||
|
||||
public function setServerHost($serverHost)
|
||||
{
|
||||
$this->serverHost = $serverHost;
|
||||
}
|
||||
|
||||
public function auth(): ?string
|
||||
{
|
||||
return $this->auth;
|
||||
}
|
||||
|
||||
public function basicAuth(): ?string
|
||||
{
|
||||
return $this->basicAuth;
|
||||
}
|
||||
|
||||
public function port(): int
|
||||
{
|
||||
return intval($this->port);
|
||||
}
|
||||
|
||||
public function getUrl(string $subdomain): string
|
||||
{
|
||||
$httpProtocol = $this->port() === 443 ? 'https' : 'http';
|
||||
$host = $this->serverHost();
|
||||
|
||||
if ($httpProtocol !== 'https') {
|
||||
$host .= ":{$this->port()}";
|
||||
}
|
||||
|
||||
return "{$subdomain}.{$host}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,35 @@ class ControlConnection
|
||||
$this->proxyManager->createProxy($this->clientId, $data);
|
||||
}
|
||||
|
||||
public function authenticate(string $sharedHost, string $subdomain)
|
||||
public function createTcpProxy($data)
|
||||
{
|
||||
$this->proxyManager->createTcpProxy($this->clientId, $data);
|
||||
}
|
||||
|
||||
public function authenticate(string $sharedHost, string $subdomain, $serverHost = null)
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'authenticate',
|
||||
'data' => [
|
||||
'type' => 'http',
|
||||
'host' => $sharedHost,
|
||||
'server_host' => $serverHost,
|
||||
'subdomain' => empty($subdomain) ? null : $subdomain,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function authenticateTcp(int $port)
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'authenticate',
|
||||
'data' => [
|
||||
'type' => 'tcp',
|
||||
'port' => $port,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function ping()
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
|
||||
13
app/Client/Exceptions/InvalidServerProvided.php
Normal file
13
app/Client/Exceptions/InvalidServerProvided.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Exceptions;
|
||||
|
||||
class InvalidServerProvided extends \Exception
|
||||
{
|
||||
public function __construct($server)
|
||||
{
|
||||
$message = "No such server {$server}.";
|
||||
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Client;
|
||||
|
||||
use App\Client\Fileserver\Fileserver;
|
||||
use App\Client\Http\Controllers\AttachDataToLogController;
|
||||
use App\Client\Http\Controllers\ClearLogsController;
|
||||
use App\Client\Http\Controllers\CreateTunnelController;
|
||||
@@ -27,12 +28,18 @@ class Factory
|
||||
/** @var string */
|
||||
protected $auth = '';
|
||||
|
||||
/** @var string */
|
||||
protected $basicAuth;
|
||||
|
||||
/** @var \React\EventLoop\LoopInterface */
|
||||
protected $loop;
|
||||
|
||||
/** @var App */
|
||||
protected $app;
|
||||
|
||||
/** @var Fileserver */
|
||||
protected $fileserver;
|
||||
|
||||
/** @var RouteGenerator */
|
||||
protected $router;
|
||||
|
||||
@@ -63,6 +70,13 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBasicAuth(?string $basicAuth)
|
||||
{
|
||||
$this->basicAuth = $basicAuth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLoop(LoopInterface $loop)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
@@ -73,7 +87,7 @@ class Factory
|
||||
protected function bindConfiguration()
|
||||
{
|
||||
app()->singleton(Configuration::class, function ($app) {
|
||||
return new Configuration($this->host, $this->port, $this->auth);
|
||||
return new Configuration($this->host, $this->port, $this->auth, $this->basicAuth);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,9 +116,25 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function share($sharedUrl, $subdomain = null)
|
||||
public function share($sharedUrl, $subdomain = null, $serverHost = null)
|
||||
{
|
||||
app('expose.client')->share($sharedUrl, $subdomain);
|
||||
app('expose.client')->share($sharedUrl, $subdomain, $serverHost);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sharePort(int $port)
|
||||
{
|
||||
app('expose.client')->sharePort($port);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function shareFolder(string $folder, string $name, $subdomain = null, $serverHost = null)
|
||||
{
|
||||
$host = $this->createFileServer($folder, $name);
|
||||
|
||||
$this->share($host, $subdomain, $serverHost);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -120,40 +150,54 @@ class Factory
|
||||
$this->router->post('/api/logs/{request_id}/data', AttachDataToLogController::class);
|
||||
$this->router->get('/api/logs/clear', ClearLogsController::class);
|
||||
|
||||
$this->app->route('/socket', new WsServer(new Socket()), ['*']);
|
||||
$this->app->route('/socket', new WsServer(new Socket()), ['*'], '');
|
||||
|
||||
foreach ($this->router->getRoutes()->all() as $name => $route) {
|
||||
$this->app->routes->add($name, $route);
|
||||
}
|
||||
}
|
||||
|
||||
protected function detectNextFreeDashboardPort($port = 4040): int
|
||||
protected function detectNextAvailablePort($startPort = 4040): int
|
||||
{
|
||||
while (is_resource(@fsockopen('127.0.0.1', $port))) {
|
||||
$port++;
|
||||
while (is_resource(@fsockopen('127.0.0.1', $startPort))) {
|
||||
$startPort++;
|
||||
}
|
||||
|
||||
return $port;
|
||||
return $startPort;
|
||||
}
|
||||
|
||||
public function createHttpServer()
|
||||
{
|
||||
$dashboardPort = $this->detectNextFreeDashboardPort();
|
||||
$dashboardPort = $this->detectNextAvailablePort();
|
||||
|
||||
config()->set('expose.dashboard_port', $dashboardPort);
|
||||
|
||||
$this->app = new App('127.0.0.1', $dashboardPort, '0.0.0.0', $this->loop);
|
||||
$this->app = new App('0.0.0.0', $dashboardPort, '0.0.0.0', $this->loop);
|
||||
|
||||
$this->addRoutes();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createFileServer(string $folder, string $name)
|
||||
{
|
||||
$port = $this->detectNextAvailablePort(8090);
|
||||
|
||||
$this->fileserver = new Fileserver($folder, $name, $port, '0.0.0.0', $this->loop);
|
||||
|
||||
return "127.0.0.1:{$port}";
|
||||
}
|
||||
|
||||
public function getApp(): App
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
public function getFileserver(): Fileserver
|
||||
{
|
||||
return $this->fileserver;
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->loop->run();
|
||||
|
||||
136
app/Client/Fileserver/ConnectionHandler.php
Normal file
136
app/Client/Fileserver/ConnectionHandler.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Fileserver;
|
||||
|
||||
use App\Http\Controllers\Concerns\LoadsViews;
|
||||
use App\Http\QueryParameters;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Http\Message\Response;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
|
||||
|
||||
class ConnectionHandler
|
||||
{
|
||||
use LoadsViews;
|
||||
|
||||
/** @var string */
|
||||
protected $rootFolder;
|
||||
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
/** @var LoopInterface */
|
||||
protected $loop;
|
||||
|
||||
public function __construct(string $rootFolder, string $name, LoopInterface $loop)
|
||||
{
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->name = $name;
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$request = $this->createLaravelRequest($request);
|
||||
$targetPath = realpath($this->rootFolder.DIRECTORY_SEPARATOR.$request->path());
|
||||
|
||||
if (! $this->isValidTarget($targetPath)) {
|
||||
return new Response(404);
|
||||
}
|
||||
|
||||
if (is_dir($targetPath)) {
|
||||
// Directory listing
|
||||
$directoryContent = Finder::create()
|
||||
->depth(0)
|
||||
->sort(function ($a, $b) {
|
||||
return strcmp(strtolower($a->getRealpath()), strtolower($b->getRealpath()));
|
||||
})
|
||||
->in($targetPath);
|
||||
|
||||
if ($this->name !== '') {
|
||||
$directoryContent->name($this->name);
|
||||
}
|
||||
|
||||
$parentPath = explode('/', $request->path());
|
||||
array_pop($parentPath);
|
||||
$parentPath = implode('/', $parentPath);
|
||||
|
||||
return new Response(
|
||||
200,
|
||||
['Content-Type' => 'text/html'],
|
||||
$this->getView(null, 'client.fileserver', [
|
||||
'currentPath' => $request->path(),
|
||||
'parentPath' => $parentPath,
|
||||
'directory' => $targetPath,
|
||||
'directoryContent' => $directoryContent,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (is_file($targetPath)) {
|
||||
return new Response(
|
||||
200,
|
||||
['Content-Type' => mime_content_type($targetPath)],
|
||||
new ReadableResourceStream(fopen($targetPath, 'r'), $this->loop)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isValidTarget(string $targetPath): bool
|
||||
{
|
||||
if (! file_exists($targetPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->name !== '') {
|
||||
$filter = new class(basename($targetPath), [$this->name]) extends FilenameFilterIterator
|
||||
{
|
||||
protected $filename;
|
||||
|
||||
public function __construct(string $filename, array $matchPatterns)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
foreach ($matchPatterns as $pattern) {
|
||||
$this->matchRegexps[] = $this->toRegex($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
public function accept()
|
||||
{
|
||||
return $this->isAccepted($this->filename);
|
||||
}
|
||||
};
|
||||
|
||||
return $filter->accept();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function createLaravelRequest(ServerRequestInterface $request): Request
|
||||
{
|
||||
try {
|
||||
parse_str($request->getBody(), $bodyParameters);
|
||||
} catch (\Throwable $e) {
|
||||
$bodyParameters = [];
|
||||
}
|
||||
|
||||
$serverRequest = (new ServerRequest(
|
||||
$request->getMethod(),
|
||||
$request->getUri(),
|
||||
$request->getHeaders(),
|
||||
$request->getBody(),
|
||||
$request->getProtocolVersion(),
|
||||
))
|
||||
->withQueryParams(QueryParameters::create($request)->all())
|
||||
->withParsedBody($bodyParameters);
|
||||
|
||||
return Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
||||
}
|
||||
}
|
||||
30
app/Client/Fileserver/Fileserver.php
Normal file
30
app/Client/Fileserver/Fileserver.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Fileserver;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Http\Server;
|
||||
use React\Socket\Server as SocketServer;
|
||||
|
||||
class Fileserver
|
||||
{
|
||||
/** @var SocketServer */
|
||||
protected $socket;
|
||||
|
||||
public function __construct($rootFolder, $name, $port, $address, LoopInterface $loop)
|
||||
{
|
||||
$server = new Server($loop, function (ServerRequestInterface $request) use ($rootFolder, $name, $loop) {
|
||||
return (new ConnectionHandler($rootFolder, $name, $loop))->handle($request);
|
||||
});
|
||||
|
||||
$this->socket = new SocketServer("{$address}:{$port}", $loop);
|
||||
|
||||
$server->listen($this->socket);
|
||||
}
|
||||
|
||||
public function getSocket(): SocketServer
|
||||
{
|
||||
return $this->socket;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ namespace App\Client\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Logger\RequestLogger;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -28,11 +28,11 @@ class AttachDataToLogController extends Controller
|
||||
|
||||
$this->requestLogger->pushLoggedRequest($loggedRequest);
|
||||
|
||||
$httpConnection->send(str(new Response(200)));
|
||||
$httpConnection->send(Message::toString(new Response(200)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$httpConnection->send(str(new Response(404)));
|
||||
$httpConnection->send(Message::toString(new Response(404)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Client\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -20,7 +20,7 @@ class CreateTunnelController extends Controller
|
||||
$httpConnection->send(respond_json($data));
|
||||
$httpConnection->close();
|
||||
}, function () use ($httpConnection) {
|
||||
$httpConnection->send(str(new Response(500)));
|
||||
$httpConnection->send(Message::toString(new Response(500)));
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class DashboardController extends Controller
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$httpConnection->send(respond_html($this->getView($httpConnection, 'client.dashboard', [
|
||||
'user' => Client::$user,
|
||||
'subdomains' => Client::$subdomains,
|
||||
'max_logs'=> config()->get('expose.max_logged_requests', 10),
|
||||
])));
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\Client\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\WebSockets\Socket;
|
||||
use Exception;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -23,9 +23,9 @@ class PushLogsToDashboardController extends Controller
|
||||
$webSocketConnection->send($request->getContent());
|
||||
}
|
||||
|
||||
$httpConnection->send(str(new Response(200)));
|
||||
$httpConnection->send(Message::toString(new Response(200)));
|
||||
} catch (Exception $e) {
|
||||
$httpConnection->send(str(new Response(500, [], $e->getMessage())));
|
||||
$httpConnection->send(Message::toString(new Response(500, [], $e->getMessage())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\Client\Http\Controllers;
|
||||
use App\Client\Http\HttpClient;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Logger\RequestLogger;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -29,13 +29,15 @@ class ReplayLogController extends Controller
|
||||
$loggedRequest = $this->requestLogger->findLoggedRequest($request->get('log'));
|
||||
|
||||
if (is_null($loggedRequest)) {
|
||||
$httpConnection->send(str(new Response(404)));
|
||||
$httpConnection->send(Message::toString(new Response(404)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->httpClient->performRequest($loggedRequest->getRequestData());
|
||||
$loggedRequest->refreshId();
|
||||
|
||||
$httpConnection->send(str(new Response(200)));
|
||||
$this->httpClient->performRequest($loggedRequest->getRequest()->toString());
|
||||
|
||||
$httpConnection->send(Message::toString(new Response(200)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
namespace App\Client\Http;
|
||||
|
||||
use App\Client\Configuration;
|
||||
use App\Client\Http\Modifiers\CheckBasicAuthentication;
|
||||
use App\Logger\RequestLogger;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use function GuzzleHttp\Psr7\parse_request;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Laminas\Http\Request;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -26,19 +27,26 @@ class HttpClient
|
||||
/** @var Request */
|
||||
protected $request;
|
||||
|
||||
protected $connectionData;
|
||||
|
||||
/** @var array */
|
||||
protected $modifiers = [
|
||||
CheckBasicAuthentication::class,
|
||||
];
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(LoopInterface $loop, RequestLogger $logger)
|
||||
public function __construct(LoopInterface $loop, RequestLogger $logger, Configuration $configuration)
|
||||
{
|
||||
$this->loop = $loop;
|
||||
$this->logger = $logger;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function performRequest(string $requestData, WebSocket $proxyConnection = null, string $requestId = null)
|
||||
public function performRequest(string $requestData, WebSocket $proxyConnection = null, $connectionData = null)
|
||||
{
|
||||
$this->connectionData = $connectionData;
|
||||
|
||||
$this->request = $this->parseRequest($requestData);
|
||||
|
||||
$this->logger->logRequest($requestData, $this->request);
|
||||
@@ -66,7 +74,7 @@ class HttpClient
|
||||
protected function createConnector(): Connector
|
||||
{
|
||||
return new Connector($this->loop, [
|
||||
'dns' => '127.0.0.1',
|
||||
'dns' => config('expose.dns', '127.0.0.1'),
|
||||
'tls' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
@@ -77,15 +85,19 @@ class HttpClient
|
||||
protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null)
|
||||
{
|
||||
(new Browser($this->loop, $this->createConnector()))
|
||||
->withOptions([
|
||||
'followRedirects' => false,
|
||||
'obeySuccessCode' => false,
|
||||
'streaming' => true,
|
||||
])
|
||||
->send($request)
|
||||
->withFollowRedirects(false)
|
||||
->withRejectErrorResponse(false)
|
||||
->requestStreaming(
|
||||
$request->getMethod(),
|
||||
$request->getUri(),
|
||||
$request->getHeaders(),
|
||||
$request->getBody()
|
||||
)
|
||||
->then(function (ResponseInterface $response) use ($proxyConnection) {
|
||||
if (! isset($response->buffer)) {
|
||||
$response->buffer = str($response);
|
||||
$response = $this->rewriteResponseHeaders($response);
|
||||
|
||||
$response->buffer = Message::toString($response);
|
||||
}
|
||||
|
||||
$this->sendChunkToServer($response->buffer, $proxyConnection);
|
||||
@@ -93,7 +105,7 @@ class HttpClient
|
||||
/* @var $body \React\Stream\ReadableStreamInterface */
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->logResponse(str($response));
|
||||
$this->logResponse(Message::toString($response));
|
||||
|
||||
$body->on('data', function ($chunk) use ($proxyConnection, $response) {
|
||||
$response->buffer .= $chunk;
|
||||
@@ -126,4 +138,25 @@ class HttpClient
|
||||
{
|
||||
return Request::fromString($data);
|
||||
}
|
||||
|
||||
protected function rewriteResponseHeaders(ResponseInterface $response)
|
||||
{
|
||||
if (! $response->hasHeader('Location')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$location = $response->getHeaderLine('Location');
|
||||
|
||||
if (! strstr($location, $this->connectionData->host)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$location = str_replace(
|
||||
$this->connectionData->host,
|
||||
$this->configuration->getUrl($this->connectionData->subdomain),
|
||||
$location
|
||||
);
|
||||
|
||||
return $response->withHeader('Location', $location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Client\Http\Modifiers;
|
||||
|
||||
use App\Client\Configuration;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Ratchet\Client\WebSocket;
|
||||
@@ -29,7 +29,7 @@ class CheckBasicAuthentication
|
||||
|
||||
if (is_null($username)) {
|
||||
$proxyConnection->send(
|
||||
str(new Response(401, [
|
||||
Message::toString(new Response(401, [
|
||||
'WWW-Authenticate' => 'Basic realm=Expose',
|
||||
], 'Unauthorized'))
|
||||
);
|
||||
@@ -89,7 +89,7 @@ class CheckBasicAuthentication
|
||||
protected function getCredentials()
|
||||
{
|
||||
try {
|
||||
$credentials = explode(':', $this->configuration->auth());
|
||||
$credentials = explode(':', $this->configuration->basicAuth());
|
||||
|
||||
return [
|
||||
$credentials[0] => $credentials[1],
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Client;
|
||||
use App\Client\Http\HttpClient;
|
||||
use function Ratchet\Client\connect;
|
||||
use Ratchet\Client\WebSocket;
|
||||
use Ratchet\RFC6455\Messaging\Frame;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Socket\Connector;
|
||||
|
||||
class ProxyManager
|
||||
{
|
||||
@@ -30,7 +32,7 @@ class ProxyManager
|
||||
], $this->loop)
|
||||
->then(function (WebSocket $proxyConnection) use ($clientId, $connectionData) {
|
||||
$proxyConnection->on('message', function ($message) use ($proxyConnection, $connectionData) {
|
||||
$this->performRequest($proxyConnection, $connectionData->request_id, (string) $message);
|
||||
$this->performRequest($proxyConnection, (string) $message, $connectionData);
|
||||
});
|
||||
|
||||
$proxyConnection->send(json_encode([
|
||||
@@ -43,8 +45,39 @@ class ProxyManager
|
||||
});
|
||||
}
|
||||
|
||||
protected function performRequest(WebSocket $proxyConnection, $requestId, string $requestData)
|
||||
public function createTcpProxy(string $clientId, $connectionData)
|
||||
{
|
||||
app(HttpClient::class)->performRequest((string) $requestData, $proxyConnection, $requestId);
|
||||
$protocol = $this->configuration->port() === 443 ? 'wss' : 'ws';
|
||||
|
||||
connect($protocol."://{$this->configuration->host()}:{$this->configuration->port()}/expose/control", [], [
|
||||
'X-Expose-Control' => 'enabled',
|
||||
], $this->loop)
|
||||
->then(function (WebSocket $proxyConnection) use ($clientId, $connectionData) {
|
||||
$connector = new Connector($this->loop);
|
||||
|
||||
$connector->connect('127.0.0.1:'.$connectionData->port)->then(function ($connection) use ($proxyConnection) {
|
||||
$connection->on('data', function ($data) use ($proxyConnection) {
|
||||
$binaryMsg = new Frame($data, true, Frame::OP_BINARY);
|
||||
$proxyConnection->send($binaryMsg);
|
||||
});
|
||||
|
||||
$proxyConnection->on('message', function ($message) use ($connection) {
|
||||
$connection->write($message);
|
||||
});
|
||||
});
|
||||
|
||||
$proxyConnection->send(json_encode([
|
||||
'event' => 'registerTcpProxy',
|
||||
'data' => [
|
||||
'tcp_request_id' => $connectionData->tcp_request_id ?? null,
|
||||
'client_id' => $clientId,
|
||||
],
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
protected function performRequest(WebSocket $proxyConnection, string $requestData, $connectionData)
|
||||
{
|
||||
app(HttpClient::class)->performRequest((string) $requestData, $proxyConnection, $connectionData);
|
||||
}
|
||||
}
|
||||
|
||||
22
app/Client/Support/ClearDomainNodeVisitor.php
Normal file
22
app/Client/Support/ClearDomainNodeVisitor.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class ClearDomainNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain') {
|
||||
$node->value = new ConstFetch(
|
||||
new Name('null')
|
||||
);
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
138
app/Client/Support/ConsoleSectionOutput.php
Normal file
138
app/Client/Support/ConsoleSectionOutput.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use Symfony\Component\Console\Output\StreamOutput;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
|
||||
/**
|
||||
* @author Pierre du Plessis <pdples@gmail.com>
|
||||
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
|
||||
*/
|
||||
class ConsoleSectionOutput extends StreamOutput
|
||||
{
|
||||
private $content = [];
|
||||
private $lines = 0;
|
||||
private $sections;
|
||||
private $terminal;
|
||||
|
||||
/**
|
||||
* @param resource $stream
|
||||
* @param \Symfony\Component\Console\Output\ConsoleSectionOutput[] $sections
|
||||
*/
|
||||
public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
|
||||
{
|
||||
parent::__construct($stream, $verbosity, $decorated, $formatter);
|
||||
array_unshift($sections, $this);
|
||||
$this->sections = &$sections;
|
||||
$this->terminal = new Terminal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears previous output for this section.
|
||||
*
|
||||
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
|
||||
*/
|
||||
public function clear(int $lines = null)
|
||||
{
|
||||
if (empty($this->content) || ! $this->isDecorated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($lines) {
|
||||
array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
|
||||
} else {
|
||||
$lines = $this->lines;
|
||||
$this->content = [];
|
||||
}
|
||||
|
||||
$this->lines -= $lines;
|
||||
|
||||
parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the previous output with a new message.
|
||||
*
|
||||
* @param array|string $message
|
||||
*/
|
||||
public function overwrite($message)
|
||||
{
|
||||
$this->clear();
|
||||
$this->writeln($message);
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return implode('', $this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addContent(string $input)
|
||||
{
|
||||
foreach (explode(\PHP_EOL, $input) as $lineContent) {
|
||||
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
|
||||
$this->content[] = $lineContent;
|
||||
$this->content[] = \PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doWrite(string $message, bool $newline)
|
||||
{
|
||||
if (! $this->isDecorated()) {
|
||||
parent::doWrite($message, $newline);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$erasedContent = $this->popStreamContentUntilCurrentSection();
|
||||
|
||||
$this->addContent($message);
|
||||
|
||||
parent::doWrite($message, true);
|
||||
parent::doWrite($erasedContent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
|
||||
* current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
|
||||
*/
|
||||
private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
|
||||
{
|
||||
$numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
|
||||
$erasedContent = [];
|
||||
|
||||
foreach ($this->sections as $section) {
|
||||
if ($section === $this) {
|
||||
break;
|
||||
}
|
||||
|
||||
$numberOfLinesToClear += $section->lines;
|
||||
$erasedContent[] = $section->getContent();
|
||||
}
|
||||
|
||||
if ($numberOfLinesToClear > 0) {
|
||||
// move cursor up n lines
|
||||
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
|
||||
// erase to end of screen
|
||||
parent::doWrite("\x1b[0J", false);
|
||||
}
|
||||
|
||||
return implode('', array_reverse($erasedContent));
|
||||
}
|
||||
|
||||
private function getDisplayLength(string $text): int
|
||||
{
|
||||
$cleanedText = Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
|
||||
$cleanedText = preg_replace('/]8;;(.*)]8;;/m', '', $cleanedText);
|
||||
|
||||
return Helper::width($cleanedText);
|
||||
}
|
||||
}
|
||||
27
app/Client/Support/DefaultDomainNodeVisitor.php
Normal file
27
app/Client/Support/DefaultDomainNodeVisitor.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class DefaultDomainNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/** @var string */
|
||||
protected $domain;
|
||||
|
||||
public function __construct(string $domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain') {
|
||||
$node->value = new String_($this->domain);
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
app/Client/Support/DefaultServerNodeVisitor.php
Normal file
27
app/Client/Support/DefaultServerNodeVisitor.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class DefaultServerNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/** @var string */
|
||||
protected $server;
|
||||
|
||||
public function __construct(string $server)
|
||||
{
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_server') {
|
||||
$node->value = new String_($this->server);
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/Client/Support/InsertDefaultDomainNodeVisitor.php
Normal file
28
app/Client/Support/InsertDefaultDomainNodeVisitor.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class InsertDefaultDomainNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
|
||||
$defaultDomainNode = new Node\Expr\ArrayItem(
|
||||
new ConstFetch(
|
||||
new Name('null')
|
||||
),
|
||||
new Node\Scalar\String_('default_domain')
|
||||
);
|
||||
|
||||
return [
|
||||
$node,
|
||||
$defaultDomainNode,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/Client/Support/InsertDefaultServerNodeVisitor.php
Normal file
28
app/Client/Support/InsertDefaultServerNodeVisitor.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\ConstFetch;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class InsertDefaultServerNodeVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
|
||||
$defaultServerNode = new Node\Expr\ArrayItem(
|
||||
new ConstFetch(
|
||||
new Name('null')
|
||||
),
|
||||
new Node\Scalar\String_('default_server')
|
||||
);
|
||||
|
||||
return [
|
||||
$node,
|
||||
$defaultServerNode,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Client\Support;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class TokenNodeVisitor extends NodeVisitorAbstract
|
||||
@@ -18,7 +19,7 @@ class TokenNodeVisitor extends NodeVisitorAbstract
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'auth_token') {
|
||||
$node->value->value = $this->token;
|
||||
$node->value = new String_($this->token);
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
81
app/Commands/ClearDefaultDomainCommand.php
Normal file
81
app/Commands/ClearDefaultDomainCommand.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Support\ClearDomainNodeVisitor;
|
||||
use App\Client\Support\InsertDefaultDomainNodeVisitor;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpParser\Lexer\Emulative;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\CloningVisitor;
|
||||
use PhpParser\Parser\Php7;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
|
||||
class ClearDefaultDomainCommand extends Command
|
||||
{
|
||||
protected $signature = 'default-domain:clear';
|
||||
|
||||
protected $description = 'Clear the default domain to use with Expose.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Clearing the default Expose domain.');
|
||||
|
||||
$configFile = implode(DIRECTORY_SEPARATOR, [
|
||||
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
|
||||
'.expose',
|
||||
'config.php',
|
||||
]);
|
||||
|
||||
if (! file_exists($configFile)) {
|
||||
@mkdir(dirname($configFile), 0777, true);
|
||||
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'));
|
||||
} else {
|
||||
$updatedConfigFile = $this->modifyConfigurationFile($configFile);
|
||||
}
|
||||
|
||||
file_put_contents($configFile, $updatedConfigFile);
|
||||
}
|
||||
|
||||
protected function modifyConfigurationFile(string $configFile)
|
||||
{
|
||||
$lexer = new Emulative([
|
||||
'usedAttributes' => [
|
||||
'comments',
|
||||
'startLine', 'endLine',
|
||||
'startTokenPos', 'endTokenPos',
|
||||
],
|
||||
]);
|
||||
$parser = new Php7($lexer);
|
||||
|
||||
$oldStmts = $parser->parse(file_get_contents($configFile));
|
||||
$oldTokens = $lexer->getTokens();
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new CloningVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($oldStmts);
|
||||
|
||||
$nodeFinder = new NodeFinder;
|
||||
|
||||
$defaultDomainNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
|
||||
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain';
|
||||
});
|
||||
|
||||
if (is_null($defaultDomainNode)) {
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new InsertDefaultDomainNodeVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
}
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new ClearDomainNodeVisitor());
|
||||
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
|
||||
$prettyPrinter = new Standard();
|
||||
|
||||
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,35 @@
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Server\Factory;
|
||||
use InvalidArgumentException;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class ServeCommand extends Command
|
||||
{
|
||||
protected $signature = 'serve {hostname=localhost} {host=0.0.0.0} {--validateAuthTokens} {--port=8080}';
|
||||
protected $signature = 'serve {hostname=localhost} {host=0.0.0.0} {--validateAuthTokens} {--port=8080} {--config=}';
|
||||
|
||||
protected $description = 'Start the expose server';
|
||||
|
||||
protected function loadConfiguration(string $configFile)
|
||||
{
|
||||
$configFile = realpath($configFile);
|
||||
|
||||
throw_if(! file_exists($configFile), new InvalidArgumentException("Invalid config file {$configFile}"));
|
||||
|
||||
$localConfig = require $configFile;
|
||||
config()->set('expose', $localConfig);
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
/** @var LoopInterface $loop */
|
||||
$loop = app(LoopInterface::class);
|
||||
|
||||
if ($this->option('config')) {
|
||||
$this->loadConfiguration($this->option('config'));
|
||||
}
|
||||
|
||||
$loop->futureTick(function () {
|
||||
$this->info('Expose server running on port '.$this->option('port').'.');
|
||||
});
|
||||
|
||||
119
app/Commands/ServerAwareCommand.php
Normal file
119
app/Commands/ServerAwareCommand.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Exceptions\InvalidServerProvided;
|
||||
use App\Logger\CliRequestLogger;
|
||||
use Illuminate\Console\Parser;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
abstract class ServerAwareCommand extends Command
|
||||
{
|
||||
const DEFAULT_HOSTNAME = 'bitinflow.dev';
|
||||
const DEFAULT_PORT = 443;
|
||||
const DEFAULT_SERVER_ENDPOINT = 'https://expose.dev/api/servers';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$inheritedSignature = '{--server=} {--server-host=} {--server-port=}';
|
||||
|
||||
$this->getDefinition()->addOptions(Parser::parse($inheritedSignature)[2]);
|
||||
|
||||
$this->configureConnectionLogger();
|
||||
}
|
||||
|
||||
protected function configureConnectionLogger()
|
||||
{
|
||||
app()->singleton(CliRequestLogger::class, function () {
|
||||
return new CliRequestLogger(new ConsoleOutput());
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getServerHost()
|
||||
{
|
||||
if ($this->option('server-host')) {
|
||||
return $this->option('server-host');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find the server in the servers array.
|
||||
* If no array exists at all (when upgrading from v1),
|
||||
* always return bitinflow.dev.
|
||||
*/
|
||||
if (config('expose.servers') === null) {
|
||||
return static::DEFAULT_HOSTNAME;
|
||||
}
|
||||
|
||||
$server = $this->option('server') ?? config('expose.default_server');
|
||||
$host = config('expose.servers.'.$server.'.host');
|
||||
|
||||
if (! is_null($host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
return $this->lookupRemoteServerHost($server);
|
||||
}
|
||||
|
||||
protected function getServerPort()
|
||||
{
|
||||
if ($this->option('server-port')) {
|
||||
return $this->option('server-port');
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find the server in the servers array.
|
||||
* If no array exists at all (when upgrading from v1),
|
||||
* always return bitinflow.dev.
|
||||
*/
|
||||
if (config('expose.servers') === null) {
|
||||
return static::DEFAULT_PORT;
|
||||
}
|
||||
|
||||
$server = $this->option('server') ?? config('expose.default_server');
|
||||
$host = config('expose.servers.'.$server.'.port');
|
||||
|
||||
if (! is_null($host)) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
return $this->lookupRemoteServerPort($server);
|
||||
}
|
||||
|
||||
protected function lookupRemoteServers()
|
||||
{
|
||||
try {
|
||||
return Http::withOptions([
|
||||
'verify' => false,
|
||||
])->get(config('expose.server_endpoint', static::DEFAULT_SERVER_ENDPOINT))->json();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function lookupRemoteServerHost($server)
|
||||
{
|
||||
$servers = $this->lookupRemoteServers();
|
||||
$host = Arr::get($servers, $server.'.host');
|
||||
|
||||
throw_if(is_null($host), new InvalidServerProvided($server));
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
protected function lookupRemoteServerPort($server)
|
||||
{
|
||||
$servers = $this->lookupRemoteServers();
|
||||
$port = Arr::get($servers, $server.'.port');
|
||||
|
||||
throw_if(is_null($port), new InvalidServerProvided($server));
|
||||
|
||||
return $port;
|
||||
}
|
||||
}
|
||||
43
app/Commands/ServerListCommand.php
Normal file
43
app/Commands/ServerListCommand.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
|
||||
class ServerListCommand extends Command
|
||||
{
|
||||
const DEFAULT_SERVER_ENDPOINT = 'https://expose.dev/api/servers';
|
||||
|
||||
protected $signature = 'servers';
|
||||
|
||||
protected $description = 'Set or retrieve the default server to use with Expose.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$servers = collect($this->lookupRemoteServers())->map(function ($server) {
|
||||
return [
|
||||
'key' => $server['key'],
|
||||
'region' => $server['region'],
|
||||
'plan' => Str::ucfirst($server['plan']),
|
||||
];
|
||||
});
|
||||
|
||||
$this->info('You can connect to a specific server with the --server=key option or set this server as default with the default-server command.');
|
||||
$this->info('');
|
||||
|
||||
$this->table(['Key', 'Region', 'Type'], $servers);
|
||||
}
|
||||
|
||||
protected function lookupRemoteServers()
|
||||
{
|
||||
try {
|
||||
return Http::withOptions([
|
||||
'verify' => false,
|
||||
])->get(config('expose.server_endpoint', static::DEFAULT_SERVER_ENDPOINT))->json();
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
98
app/Commands/SetDefaultDomainCommand.php
Normal file
98
app/Commands/SetDefaultDomainCommand.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Support\DefaultDomainNodeVisitor;
|
||||
use App\Client\Support\DefaultServerNodeVisitor;
|
||||
use App\Client\Support\InsertDefaultDomainNodeVisitor;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpParser\Lexer\Emulative;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\CloningVisitor;
|
||||
use PhpParser\Parser\Php7;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
|
||||
class SetDefaultDomainCommand extends Command
|
||||
{
|
||||
protected $signature = 'default-domain {domain?} {--server=}';
|
||||
|
||||
protected $description = 'Set or retrieve the default domain to use with Expose.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$domain = $this->argument('domain');
|
||||
$server = $this->option('server');
|
||||
if (! is_null($domain)) {
|
||||
$this->info('Setting the Expose default domain to "'.$domain.'"');
|
||||
|
||||
$configFile = implode(DIRECTORY_SEPARATOR, [
|
||||
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
|
||||
'.expose',
|
||||
'config.php',
|
||||
]);
|
||||
|
||||
if (! file_exists($configFile)) {
|
||||
@mkdir(dirname($configFile), 0777, true);
|
||||
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'), $domain, $server);
|
||||
} else {
|
||||
$updatedConfigFile = $this->modifyConfigurationFile($configFile, $domain, $server);
|
||||
}
|
||||
|
||||
file_put_contents($configFile, $updatedConfigFile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($domain = config('expose.default_domain'))) {
|
||||
$this->info('There is no default domain specified.');
|
||||
} else {
|
||||
$this->info('Current default domain: '.$domain);
|
||||
}
|
||||
}
|
||||
|
||||
protected function modifyConfigurationFile(string $configFile, string $domain, ?string $server)
|
||||
{
|
||||
$lexer = new Emulative([
|
||||
'usedAttributes' => [
|
||||
'comments',
|
||||
'startLine', 'endLine',
|
||||
'startTokenPos', 'endTokenPos',
|
||||
],
|
||||
]);
|
||||
$parser = new Php7($lexer);
|
||||
|
||||
$oldStmts = $parser->parse(file_get_contents($configFile));
|
||||
$oldTokens = $lexer->getTokens();
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new CloningVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($oldStmts);
|
||||
|
||||
$nodeFinder = new NodeFinder;
|
||||
|
||||
$defaultDomainNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
|
||||
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_domain';
|
||||
});
|
||||
|
||||
if (is_null($defaultDomainNode)) {
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new InsertDefaultDomainNodeVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
}
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new DefaultDomainNodeVisitor($domain));
|
||||
|
||||
if (! is_null($server)) {
|
||||
$nodeTraverser->addVisitor(new DefaultServerNodeVisitor($server));
|
||||
}
|
||||
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
|
||||
$prettyPrinter = new Standard();
|
||||
|
||||
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
|
||||
}
|
||||
}
|
||||
92
app/Commands/SetDefaultServerCommand.php
Normal file
92
app/Commands/SetDefaultServerCommand.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Support\DefaultServerNodeVisitor;
|
||||
use App\Client\Support\InsertDefaultServerNodeVisitor;
|
||||
use Illuminate\Console\Command;
|
||||
use PhpParser\Lexer\Emulative;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeFinder;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\NodeVisitor\CloningVisitor;
|
||||
use PhpParser\Parser\Php7;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
|
||||
class SetDefaultServerCommand extends Command
|
||||
{
|
||||
protected $signature = 'default-server {server?}';
|
||||
|
||||
protected $description = 'Set or retrieve the default server to use with Expose.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$server = $this->argument('server');
|
||||
if (! is_null($server)) {
|
||||
$this->info('Setting the Expose default server to "'.$server.'"');
|
||||
|
||||
$configFile = implode(DIRECTORY_SEPARATOR, [
|
||||
$_SERVER['HOME'] ?? $_SERVER['USERPROFILE'],
|
||||
'.expose',
|
||||
'config.php',
|
||||
]);
|
||||
|
||||
if (! file_exists($configFile)) {
|
||||
@mkdir(dirname($configFile), 0777, true);
|
||||
$updatedConfigFile = $this->modifyConfigurationFile(base_path('config/expose.php'), $server);
|
||||
} else {
|
||||
$updatedConfigFile = $this->modifyConfigurationFile($configFile, $server);
|
||||
}
|
||||
|
||||
file_put_contents($configFile, $updatedConfigFile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($server = config('expose.default_server'))) {
|
||||
$this->info('There is no default server specified.');
|
||||
} else {
|
||||
$this->info('Current default server: '.$server);
|
||||
}
|
||||
}
|
||||
|
||||
protected function modifyConfigurationFile(string $configFile, string $server)
|
||||
{
|
||||
$lexer = new Emulative([
|
||||
'usedAttributes' => [
|
||||
'comments',
|
||||
'startLine', 'endLine',
|
||||
'startTokenPos', 'endTokenPos',
|
||||
],
|
||||
]);
|
||||
$parser = new Php7($lexer);
|
||||
|
||||
$oldStmts = $parser->parse(file_get_contents($configFile));
|
||||
$oldTokens = $lexer->getTokens();
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new CloningVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($oldStmts);
|
||||
|
||||
$nodeFinder = new NodeFinder;
|
||||
|
||||
$defaultServerNode = $nodeFinder->findFirst($newStmts, function (Node $node) {
|
||||
return $node instanceof Node\Expr\ArrayItem && $node->key && $node->key->value === 'default_server';
|
||||
});
|
||||
|
||||
if (is_null($defaultServerNode)) {
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new InsertDefaultServerNodeVisitor());
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
}
|
||||
|
||||
$nodeTraverser = new NodeTraverser;
|
||||
$nodeTraverser->addVisitor(new DefaultServerNodeVisitor($server));
|
||||
|
||||
$newStmts = $nodeTraverser->traverse($newStmts);
|
||||
|
||||
$prettyPrinter = new Standard();
|
||||
|
||||
return $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,63 @@
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Factory;
|
||||
use App\Logger\CliRequestLogger;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ShareCommand extends Command
|
||||
class ShareCommand extends ServerAwareCommand
|
||||
{
|
||||
protected $signature = 'share {host} {--subdomain=} {--auth=}';
|
||||
protected $signature = 'share {host} {--subdomain=} {--auth=} {--basicAuth=} {--dns=} {--domain=}';
|
||||
|
||||
protected $description = 'Share a local url with a remote expose server';
|
||||
|
||||
protected function configureConnectionLogger()
|
||||
{
|
||||
app()->bind(CliRequestLogger::class, function () {
|
||||
return new CliRequestLogger(new ConsoleOutput());
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->configureConnectionLogger();
|
||||
$auth = $this->option('auth') ?? config('expose.auth_token', '');
|
||||
$this->info('Using auth token: '.$auth, OutputInterface::VERBOSITY_DEBUG);
|
||||
|
||||
if (strstr($this->argument('host'), 'host.docker.internal')) {
|
||||
config(['expose.dns' => true]);
|
||||
}
|
||||
|
||||
if ($this->option('dns') !== null) {
|
||||
config(['expose.dns' => empty($this->option('dns')) ? true : $this->option('dns')]);
|
||||
}
|
||||
|
||||
$domain = config('expose.default_domain');
|
||||
|
||||
if (! is_null($this->option('server'))) {
|
||||
$domain = null;
|
||||
}
|
||||
|
||||
if (! is_null($this->option('domain'))) {
|
||||
$domain = $this->option('domain');
|
||||
}
|
||||
|
||||
if (! is_null($this->option('subdomain'))) {
|
||||
$subdomains = explode(',', $this->option('subdomain'));
|
||||
$this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE);
|
||||
} else {
|
||||
$host = Str::beforeLast($this->argument('host'), '.');
|
||||
$host = str_replace('https://', '', $host);
|
||||
$host = str_replace('http://', '', $host);
|
||||
$host = Str::beforeLast($host, ':');
|
||||
$subdomains = [Str::slug($host)];
|
||||
$this->info('Trying to use custom domain: '.$subdomains[0].PHP_EOL, OutputInterface::VERBOSITY_VERBOSE);
|
||||
}
|
||||
|
||||
(new Factory())
|
||||
->setLoop(app(LoopInterface::class))
|
||||
->setHost(config('expose.host', 'localhost'))
|
||||
->setPort(config('expose.port', 8080))
|
||||
->setAuth($this->option('auth'))
|
||||
->setHost($this->getServerHost())
|
||||
->setPort($this->getServerPort())
|
||||
->setAuth($auth)
|
||||
->setBasicAuth($this->option('basicAuth'))
|
||||
->createClient()
|
||||
->share($this->argument('host'), explode(',', $this->option('subdomain')))
|
||||
->share(
|
||||
$this->argument('host'),
|
||||
$subdomains,
|
||||
$domain
|
||||
)
|
||||
->createHttpServer()
|
||||
->run();
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ namespace App\Commands;
|
||||
|
||||
class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
{
|
||||
protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=}';
|
||||
protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--basicAuth=} {--dns=} {--domain=}';
|
||||
|
||||
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,17 +56,22 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('.', '-', basename($projectPath));
|
||||
return basename($projectPath);
|
||||
}
|
||||
|
||||
protected function prepareSharedHost($host): string
|
||||
protected function detectProtocol($host): string
|
||||
{
|
||||
$certificateFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Certificates'.DIRECTORY_SEPARATOR.$host.'.crt';
|
||||
|
||||
if (file_exists($certificateFile)) {
|
||||
return 'https://'.$host;
|
||||
return 'https://';
|
||||
}
|
||||
|
||||
return $host;
|
||||
return config('expose.default_https', false) ? 'https://' : 'http://';
|
||||
}
|
||||
|
||||
protected function prepareSharedHost($host): string
|
||||
{
|
||||
return $this->detectProtocol($host).$host;
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Commands/SharePortCommand.php
Normal file
28
app/Commands/SharePortCommand.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Factory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class SharePortCommand extends ServerAwareCommand
|
||||
{
|
||||
protected $signature = 'share-port {port} {--auth=}';
|
||||
|
||||
protected $description = 'Share a local port with a remote expose server';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$auth = $this->option('auth') ?? config('expose.auth_token', '');
|
||||
|
||||
(new Factory())
|
||||
->setLoop(app(LoopInterface::class))
|
||||
->setHost($this->getServerHost())
|
||||
->setPort($this->getServerPort())
|
||||
->setAuth($auth)
|
||||
->createClient()
|
||||
->sharePort($this->argument('port'))
|
||||
->createHttpServer()
|
||||
->run();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class StoreAuthenticationTokenCommand extends Command
|
||||
{
|
||||
protected $signature = 'token {token?}';
|
||||
|
||||
protected $description = 'Set or retrieve the authentication token to use with expose.';
|
||||
protected $description = 'Set or retrieve the authentication token to use with Expose.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
||||
@@ -8,7 +8,9 @@ use Ratchet\ConnectionInterface;
|
||||
|
||||
interface ConnectionManager
|
||||
{
|
||||
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection;
|
||||
public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection;
|
||||
|
||||
public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection;
|
||||
|
||||
public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength);
|
||||
|
||||
@@ -18,11 +20,17 @@ interface ConnectionManager
|
||||
|
||||
public function removeControlConnection($connection);
|
||||
|
||||
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection;
|
||||
public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection;
|
||||
|
||||
public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
|
||||
|
||||
public function getConnections(): array;
|
||||
|
||||
public function getConnectionsForAuthToken(string $authToken): array;
|
||||
|
||||
public function getTcpConnectionsForAuthToken(string $authToken): array;
|
||||
|
||||
public function findControlConnectionsForIp(string $ip): array;
|
||||
|
||||
public function findControlConnectionsForAuthToken(string $token): array;
|
||||
}
|
||||
|
||||
24
app/Contracts/DomainRepository.php
Normal file
24
app/Contracts/DomainRepository.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface DomainRepository
|
||||
{
|
||||
public function getDomains(): PromiseInterface;
|
||||
|
||||
public function getDomainById($id): PromiseInterface;
|
||||
|
||||
public function getDomainByName(string $name): PromiseInterface;
|
||||
|
||||
public function getDomainsByUserId($id): PromiseInterface;
|
||||
|
||||
public function getDomainsByUserIdAndName($id, $name): PromiseInterface;
|
||||
|
||||
public function deleteDomainForUserId($userId, $domainId): PromiseInterface;
|
||||
|
||||
public function storeDomain(array $data): PromiseInterface;
|
||||
|
||||
public function updateDomain($id, array $data): PromiseInterface;
|
||||
}
|
||||
14
app/Contracts/LoggerRepository.php
Normal file
14
app/Contracts/LoggerRepository.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface LoggerRepository
|
||||
{
|
||||
public function logSubdomain($authToken, $subdomain);
|
||||
|
||||
public function getLogs(): PromiseInterface;
|
||||
|
||||
public function getLogsBySubdomain($subdomain): PromiseInterface;
|
||||
}
|
||||
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;
|
||||
}
|
||||
26
app/Contracts/SubdomainRepository.php
Normal file
26
app/Contracts/SubdomainRepository.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface SubdomainRepository
|
||||
{
|
||||
public function getSubdomains(): PromiseInterface;
|
||||
|
||||
public function getSubdomainById($id): PromiseInterface;
|
||||
|
||||
public function getSubdomainByName(string $name): PromiseInterface;
|
||||
|
||||
public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface;
|
||||
|
||||
public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface;
|
||||
|
||||
public function getSubdomainsByUserId($id): PromiseInterface;
|
||||
|
||||
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface;
|
||||
|
||||
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface;
|
||||
|
||||
public function storeSubdomain(array $data): PromiseInterface;
|
||||
}
|
||||
@@ -10,11 +10,15 @@ interface UserRepository
|
||||
|
||||
public function getUserById($id): PromiseInterface;
|
||||
|
||||
public function paginateUsers(int $perPage, int $currentPage): PromiseInterface;
|
||||
public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface;
|
||||
|
||||
public function getUserByToken(string $authToken): PromiseInterface;
|
||||
|
||||
public function storeUser(array $data): PromiseInterface;
|
||||
|
||||
public function deleteUser($id): PromiseInterface;
|
||||
|
||||
public function getUsersByTokens(array $authTokens): PromiseInterface;
|
||||
|
||||
public function updateLastSharedAt($id): PromiseInterface;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use Twig\Loader\ArrayLoader;
|
||||
|
||||
trait LoadsViews
|
||||
{
|
||||
protected function getView(ConnectionInterface $connection, string $view, array $data = [])
|
||||
protected function getView(?ConnectionInterface $connection, string $view, array $data = [])
|
||||
{
|
||||
$templatePath = implode(DIRECTORY_SEPARATOR, explode('.', $view));
|
||||
|
||||
@@ -23,7 +23,10 @@ trait LoadsViews
|
||||
$data = array_merge($data, [
|
||||
'request' => $connection->laravelRequest ?? null,
|
||||
]);
|
||||
|
||||
return stream_for($twig->render('template', $data));
|
||||
try {
|
||||
return stream_for($twig->render('template', $data));
|
||||
} catch (\Throwable $e) {
|
||||
var_dump($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,60 @@
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use App\Client\Support\ConsoleSectionOutput;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
|
||||
class CliRequestLogger extends Logger
|
||||
{
|
||||
/** @var Table */
|
||||
protected $table;
|
||||
|
||||
/** @var Collection */
|
||||
protected $requests;
|
||||
|
||||
/** @var \Symfony\Component\Console\Output\ConsoleSectionOutput */
|
||||
protected $section;
|
||||
|
||||
protected $verbColors = [
|
||||
'GET' => 'blue',
|
||||
'HEAD' => '#6C7280',
|
||||
'OPTIONS' => '#6C7280',
|
||||
'POST' => 'yellow',
|
||||
'PUT' => 'yellow',
|
||||
'PATCH' => 'yellow',
|
||||
'DELETE' => 'red',
|
||||
];
|
||||
|
||||
protected $consoleSectionOutputs = [];
|
||||
|
||||
/**
|
||||
* The current terminal width.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $terminalWidth;
|
||||
|
||||
/**
|
||||
* Computes the terminal width.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getTerminalWidth()
|
||||
{
|
||||
if ($this->terminalWidth == null) {
|
||||
$this->terminalWidth = (new Terminal)->getWidth();
|
||||
|
||||
$this->terminalWidth = $this->terminalWidth >= 30
|
||||
? $this->terminalWidth
|
||||
: 30;
|
||||
}
|
||||
|
||||
return $this->terminalWidth;
|
||||
}
|
||||
|
||||
public function __construct(ConsoleOutputInterface $consoleOutput)
|
||||
{
|
||||
parent::__construct($consoleOutput);
|
||||
|
||||
$this->section = $this->output->section();
|
||||
|
||||
$this->table = new Table($this->section);
|
||||
$this->table->setHeaders(['Method', 'URI', 'Response', 'Time', 'Duration']);
|
||||
$this->section = new ConsoleSectionOutput($this->output->getStream(), $this->consoleSectionOutputs, $this->output->getVerbosity(), $this->output->isDecorated(), $this->output->getFormatter());
|
||||
|
||||
$this->requests = new Collection();
|
||||
}
|
||||
@@ -37,8 +68,28 @@ class CliRequestLogger extends Logger
|
||||
return $this->output;
|
||||
}
|
||||
|
||||
protected function getRequestColor(?LoggedRequest $request)
|
||||
{
|
||||
$statusCode = optional($request->getResponse())->getStatusCode();
|
||||
$color = 'white';
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
$color = 'green';
|
||||
} elseif ($statusCode >= 300 && $statusCode < 400) {
|
||||
$color = 'blue';
|
||||
} elseif ($statusCode >= 400 && $statusCode < 500) {
|
||||
$color = 'yellow';
|
||||
} elseif ($statusCode >= 500) {
|
||||
$color = 'red';
|
||||
}
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
public function logRequest(LoggedRequest $loggedRequest)
|
||||
{
|
||||
$dashboardUrl = 'http://127.0.0.1:'.config('expose.dashboard_port');
|
||||
|
||||
if ($this->requests->has($loggedRequest->id())) {
|
||||
$this->requests[$loggedRequest->id()] = $loggedRequest;
|
||||
} else {
|
||||
@@ -46,18 +97,55 @@ class CliRequestLogger extends Logger
|
||||
}
|
||||
$this->requests = $this->requests->slice(0, config('expose.max_logged_requests', 10));
|
||||
|
||||
$this->section->clear();
|
||||
$terminalWidth = $this->getTerminalWidth();
|
||||
|
||||
$this->table->setRows($this->requests->map(function (LoggedRequest $loggedRequest) {
|
||||
$requests = $this->requests->map(function (LoggedRequest $loggedRequest) {
|
||||
return [
|
||||
$loggedRequest->getRequest()->getMethod(),
|
||||
$loggedRequest->getRequest()->getUri(),
|
||||
optional($loggedRequest->getResponse())->getStatusCode().' '.optional($loggedRequest->getResponse())->getReasonPhrase(),
|
||||
$loggedRequest->getStartTime()->toDateTimeString(),
|
||||
$loggedRequest->getDuration().'ms',
|
||||
'method' => $loggedRequest->getRequest()->getMethod(),
|
||||
'url' => $loggedRequest->getRequest()->getUri(),
|
||||
'duration' => $loggedRequest->getDuration(),
|
||||
'time' => $loggedRequest->getStartTime()->isToday() ? $loggedRequest->getStartTime()->toTimeString() : $loggedRequest->getStartTime()->toDateTimeString(),
|
||||
'color' => $this->getRequestColor($loggedRequest),
|
||||
'status' => optional($loggedRequest->getResponse())->getStatusCode(),
|
||||
];
|
||||
})->toArray());
|
||||
});
|
||||
|
||||
$this->table->render();
|
||||
$maxMethod = mb_strlen($requests->max('method'));
|
||||
$maxDuration = mb_strlen($requests->max('duration'));
|
||||
|
||||
$output = $requests->map(function ($loggedRequest) use ($terminalWidth, $maxMethod, $maxDuration) {
|
||||
$method = $loggedRequest['method'];
|
||||
$spaces = str_repeat(' ', max($maxMethod + 2 - mb_strlen($method), 0));
|
||||
$url = $loggedRequest['url'];
|
||||
$duration = $loggedRequest['duration'];
|
||||
$time = $loggedRequest['time'];
|
||||
$durationSpaces = str_repeat(' ', max($maxDuration + 2 - mb_strlen($duration), 0));
|
||||
$color = $loggedRequest['color'];
|
||||
$status = $loggedRequest['status'];
|
||||
|
||||
$dots = str_repeat('.', max($terminalWidth - strlen($method.$spaces.$url.$time.$durationSpaces.$duration) - 16, 0));
|
||||
|
||||
if (empty($dots)) {
|
||||
$url = substr($url, 0, $terminalWidth - strlen($method.$spaces.$time.$durationSpaces.$duration) - 15 - 3).'...';
|
||||
} else {
|
||||
$dots .= ' ';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
' <fg=%s;options=bold>%s </> <fg=%s;options=bold>%s%s</> %s<fg=#6C7280> %s%s%s%s ms</>',
|
||||
$color,
|
||||
$status,
|
||||
$this->verbColors[$method] ?? 'default',
|
||||
$method,
|
||||
$spaces,
|
||||
$url,
|
||||
$dots,
|
||||
$time,
|
||||
$durationSpaces,
|
||||
$duration,
|
||||
);
|
||||
});
|
||||
|
||||
$this->section->overwrite($output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Logger;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use function GuzzleHttp\Psr7\parse_request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Laminas\Http\Header\GenericHeader;
|
||||
use Laminas\Http\Request;
|
||||
use Laminas\Http\Response;
|
||||
use Namshi\Cuzzle\Formatter\CurlFormatter;
|
||||
@@ -19,11 +21,8 @@ class LoggedRequest implements \JsonSerializable
|
||||
/** @var Request */
|
||||
protected $parsedRequest;
|
||||
|
||||
/** @var string */
|
||||
protected $rawResponse;
|
||||
|
||||
/** @var Response */
|
||||
protected $parsedResponse;
|
||||
/** @var LoggedResponse */
|
||||
protected $response;
|
||||
|
||||
/** @var string */
|
||||
protected $id;
|
||||
@@ -51,6 +50,7 @@ class LoggedRequest implements \JsonSerializable
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$data = [
|
||||
@@ -71,22 +71,8 @@ class LoggedRequest implements \JsonSerializable
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->parsedResponse) {
|
||||
$logBody = $this->shouldReturnBody();
|
||||
|
||||
try {
|
||||
$body = $logBody ? $this->parsedResponse->getBody() : '';
|
||||
} catch (\Exception $e) {
|
||||
$body = '';
|
||||
}
|
||||
|
||||
$data['response'] = [
|
||||
'raw' => $logBody ? $this->rawResponse : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
|
||||
'status' => $this->parsedResponse->getStatusCode(),
|
||||
'headers' => $this->parsedResponse->getHeaders()->toArray(),
|
||||
'reason' => $this->parsedResponse->getReasonPhrase(),
|
||||
'body' => $logBody ? $body : 'SKIPPED BY CONFIG OR BINARY RESPONSE',
|
||||
];
|
||||
if ($this->response) {
|
||||
$data['response'] = $this->response->toArray();
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -107,96 +93,6 @@ class LoggedRequest implements \JsonSerializable
|
||||
return preg_match('~[^\x20-\x7E\t\r\n]~', $string) > 0;
|
||||
}
|
||||
|
||||
protected function shouldReturnBody(): bool
|
||||
{
|
||||
if ($this->skipByStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipByContentType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipByExtension()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipBySize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
|
||||
$contentType = $header ? $header->getMediaType() : '';
|
||||
$patterns = [
|
||||
'application/json',
|
||||
'text/*',
|
||||
'*javascript*',
|
||||
];
|
||||
|
||||
return Str::is($patterns, $contentType);
|
||||
}
|
||||
|
||||
protected function skipByStatus(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.status'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.status'), $this->parsedResponse->getStatusCode());
|
||||
}
|
||||
|
||||
protected function skipByContentType(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.content_type'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$header = $this->parsedResponse->getHeaders()->get('Content-Type');
|
||||
$contentType = $header ? $header->getMediaType() : '';
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.content_type'), $contentType);
|
||||
}
|
||||
|
||||
protected function skipByExtension(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.extension'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.extension'), $this->parsedRequest->getUri()->getPath());
|
||||
}
|
||||
|
||||
protected function skipBySize(): bool
|
||||
{
|
||||
$configSize = $this->getConfigSize(config()->get('expose.skip_body_log.size', '1MB'));
|
||||
$contentLength = $this->parsedResponse->getHeaders()->get('Content-Length');
|
||||
|
||||
if (! $contentLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contentSize = $contentLength->getFieldValue() ?? 0;
|
||||
|
||||
return $contentSize > $configSize;
|
||||
}
|
||||
|
||||
protected function getConfigSize(string $size): int
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$number = substr($size, 0, -2);
|
||||
$suffix = strtoupper(substr($size, -2));
|
||||
|
||||
// B or no suffix
|
||||
if (is_numeric(substr($suffix, 0, 1))) {
|
||||
return preg_replace('/[^\d]/', '', $size);
|
||||
}
|
||||
|
||||
// if we have an error in the input, default to GB
|
||||
$exponent = array_flip($units)[$suffix] ?? 5;
|
||||
|
||||
return $number * (1024 ** $exponent);
|
||||
}
|
||||
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->parsedRequest;
|
||||
@@ -204,9 +100,7 @@ class LoggedRequest implements \JsonSerializable
|
||||
|
||||
public function setResponse(string $rawResponse, Response $response)
|
||||
{
|
||||
$this->parsedResponse = $response;
|
||||
|
||||
$this->rawResponse = $rawResponse;
|
||||
$this->response = new LoggedResponse($rawResponse, $response, $this->getRequest());
|
||||
|
||||
if (is_null($this->stopTime)) {
|
||||
$this->stopTime = now();
|
||||
@@ -223,9 +117,9 @@ class LoggedRequest implements \JsonSerializable
|
||||
return $this->rawRequest;
|
||||
}
|
||||
|
||||
public function getResponse(): ?Response
|
||||
public function getResponse(): ?LoggedResponse
|
||||
{
|
||||
return $this->parsedResponse;
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function getPostData()
|
||||
@@ -280,7 +174,7 @@ class LoggedRequest implements \JsonSerializable
|
||||
return $postData;
|
||||
}
|
||||
|
||||
protected function detectSubdomain()
|
||||
public function detectSubdomain()
|
||||
{
|
||||
return collect($this->parsedRequest->getHeaders()->toArray())
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
@@ -320,4 +214,23 @@ class LoggedRequest implements \JsonSerializable
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getUrl()
|
||||
{
|
||||
$request = Message::parseRequest($this->rawRequest);
|
||||
dd($request->getUri()->withFragment(''));
|
||||
}
|
||||
|
||||
public function refreshId()
|
||||
{
|
||||
$requestId = (string) Str::uuid();
|
||||
|
||||
$this->getRequest()->getHeaders()->removeHeader(
|
||||
$this->getRequest()->getHeader('x-expose-request-id')
|
||||
);
|
||||
|
||||
$this->getRequest()->getHeaders()->addHeader(new GenericHeader('x-expose-request-id', $requestId));
|
||||
|
||||
$this->id = $requestId;
|
||||
}
|
||||
}
|
||||
|
||||
160
app/Logger/LoggedResponse.php
Normal file
160
app/Logger/LoggedResponse.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Laminas\Http\Request;
|
||||
use Laminas\Http\Response;
|
||||
|
||||
class LoggedResponse
|
||||
{
|
||||
/** @var string */
|
||||
protected $rawResponse;
|
||||
|
||||
/** @var Response */
|
||||
protected $response;
|
||||
|
||||
/** @var Request */
|
||||
protected $request;
|
||||
|
||||
protected $reasonPhrase;
|
||||
protected $body;
|
||||
protected $statusCode;
|
||||
protected $headers;
|
||||
|
||||
public function __construct(string $rawResponse, Response $response, Request $request)
|
||||
{
|
||||
$this->rawResponse = $rawResponse;
|
||||
$this->response = $response;
|
||||
$this->request = $request;
|
||||
|
||||
if (! $this->shouldReturnBody()) {
|
||||
$this->rawResponse = 'SKIPPED BY CONFIG OR BINARY RESPONSE';
|
||||
$this->body = 'SKIPPED BY CONFIG OR BINARY RESPONSE';
|
||||
} else {
|
||||
try {
|
||||
$this->body = $response->getBody();
|
||||
} catch (\Exception $e) {
|
||||
$this->body = '';
|
||||
}
|
||||
}
|
||||
|
||||
$this->statusCode = $response->getStatusCode();
|
||||
$this->reasonPhrase = $response->getReasonPhrase();
|
||||
$this->headers = $response->getHeaders()->toArray();
|
||||
|
||||
$this->response = null;
|
||||
$this->request = null;
|
||||
}
|
||||
|
||||
protected function shouldReturnBody(): bool
|
||||
{
|
||||
if ($this->skipByStatus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipByContentType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipByExtension()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->skipBySize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$header = $this->response->getHeaders()->get('Content-Type');
|
||||
$contentType = $header ? $header->getMediaType() : '';
|
||||
$patterns = [
|
||||
'application/json',
|
||||
'text/*',
|
||||
'*javascript*',
|
||||
];
|
||||
|
||||
return Str::is($patterns, $contentType);
|
||||
}
|
||||
|
||||
protected function skipByStatus(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.status'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.status'), $this->response->getStatusCode());
|
||||
}
|
||||
|
||||
protected function skipByContentType(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.content_type'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$header = $this->response->getHeaders()->get('Content-Type');
|
||||
$contentType = $header ? $header->getMediaType() : '';
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.content_type'), $contentType);
|
||||
}
|
||||
|
||||
protected function skipByExtension(): bool
|
||||
{
|
||||
if (empty(config()->get('expose.skip_body_log.extension'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::is(config()->get('expose.skip_body_log.extension'), $this->request->getUri()->getPath());
|
||||
}
|
||||
|
||||
protected function skipBySize(): bool
|
||||
{
|
||||
$configSize = $this->getConfigSize(config()->get('expose.skip_body_log.size', '1MB'));
|
||||
$contentLength = $this->response->getHeaders()->get('Content-Length');
|
||||
|
||||
if (! $contentLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$contentSize = $contentLength->getFieldValue() ?? 0;
|
||||
|
||||
return $contentSize > $configSize;
|
||||
}
|
||||
|
||||
protected function getConfigSize(string $size): int
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$number = substr($size, 0, -2);
|
||||
$suffix = strtoupper(substr($size, -2));
|
||||
|
||||
// B or no suffix
|
||||
if (is_numeric(substr($suffix, 0, 1))) {
|
||||
return preg_replace('/[^\d]/', '', $size);
|
||||
}
|
||||
|
||||
// if we have an error in the input, default to GB
|
||||
$exponent = array_flip($units)[$suffix] ?? 5;
|
||||
|
||||
return $number * (1024 ** $exponent);
|
||||
}
|
||||
|
||||
public function getStatusCode()
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function getReasonPhrase()
|
||||
{
|
||||
return $this->reasonPhrase;
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'raw' => $this->rawResponse,
|
||||
'status' => $this->statusCode,
|
||||
'headers' => $this->headers,
|
||||
'reason' => $this->reasonPhrase,
|
||||
'body' => $this->body,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
UriFactory::registerScheme('capacitor', Uri::class);
|
||||
UriFactory::registerScheme('chrome-extension', Uri::class);
|
||||
}
|
||||
|
||||
@@ -37,6 +38,14 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
$builtInConfig = config('expose');
|
||||
|
||||
$keyServerVariable = 'EXPOSE_CONFIG_FILE';
|
||||
if (array_key_exists($keyServerVariable, $_SERVER) && is_string($_SERVER[$keyServerVariable]) && file_exists($_SERVER[$keyServerVariable])) {
|
||||
$localConfig = require $_SERVER[$keyServerVariable];
|
||||
config()->set('expose', array_merge($builtInConfig, $localConfig));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$localConfigFile = getcwd().DIRECTORY_SEPARATOR.'.expose.php';
|
||||
|
||||
if (file_exists($localConfigFile)) {
|
||||
|
||||
@@ -40,6 +40,7 @@ class Configuration implements \JsonSerializable
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return array_merge([
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use App\Contracts\ConnectionManager as ConnectionManagerContract;
|
||||
use App\Contracts\LoggerRepository;
|
||||
use App\Contracts\StatisticsCollector;
|
||||
use App\Contracts\SubdomainGenerator;
|
||||
use App\Http\QueryParameters;
|
||||
use App\Server\Exceptions\NoFreePortAvailable;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Socket\Server;
|
||||
|
||||
class ConnectionManager implements ConnectionManagerContract
|
||||
{
|
||||
@@ -22,10 +26,18 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
/** @var LoopInterface */
|
||||
protected $loop;
|
||||
|
||||
public function __construct(SubdomainGenerator $subdomainGenerator, LoopInterface $loop)
|
||||
/** @var StatisticsCollector */
|
||||
protected $statisticsCollector;
|
||||
|
||||
/** @var LoggerRepository */
|
||||
protected $logger;
|
||||
|
||||
public function __construct(SubdomainGenerator $subdomainGenerator, StatisticsCollector $statisticsCollector, LoggerRepository $logger, LoopInterface $loop)
|
||||
{
|
||||
$this->subdomainGenerator = $subdomainGenerator;
|
||||
$this->loop = $loop;
|
||||
$this->statisticsCollector = $statisticsCollector;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function limitConnectionLength(ControlConnection $connection, int $maximumConnectionLength)
|
||||
@@ -41,7 +53,7 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
});
|
||||
}
|
||||
|
||||
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection
|
||||
public function storeConnection(string $host, ?string $subdomain, ?string $serverHost, ConnectionInterface $connection): ControlConnection
|
||||
{
|
||||
$clientId = (string) uniqid();
|
||||
|
||||
@@ -52,14 +64,75 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
$host,
|
||||
$subdomain ?? $this->subdomainGenerator->generateSubdomain(),
|
||||
$clientId,
|
||||
$serverHost,
|
||||
$this->getAuthTokenFromConnection($connection)
|
||||
);
|
||||
|
||||
$this->connections[] = $storedConnection;
|
||||
|
||||
$this->statisticsCollector->siteShared($this->getAuthTokenFromConnection($connection));
|
||||
|
||||
$this->logger->logSubdomain($storedConnection->authToken, $storedConnection->subdomain);
|
||||
|
||||
$this->performConnectionCallback($storedConnection);
|
||||
|
||||
return $storedConnection;
|
||||
}
|
||||
|
||||
protected function performConnectionCallback(ControlConnection $connection)
|
||||
{
|
||||
$connectionCallback = config('expose.admin.connection_callback');
|
||||
|
||||
if ($connectionCallback !== null && class_exists($connectionCallback)) {
|
||||
app($connectionCallback)->handle($connection);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeTcpConnection(int $port, ConnectionInterface $connection): ControlConnection
|
||||
{
|
||||
$clientId = (string) uniqid();
|
||||
|
||||
$connection->client_id = $clientId;
|
||||
|
||||
$storedConnection = new TcpControlConnection(
|
||||
$connection,
|
||||
$port,
|
||||
$this->getSharedTcpServer(),
|
||||
$clientId,
|
||||
$this->getAuthTokenFromConnection($connection)
|
||||
);
|
||||
|
||||
$this->connections[] = $storedConnection;
|
||||
|
||||
$this->statisticsCollector->portShared($this->getAuthTokenFromConnection($connection));
|
||||
|
||||
return $storedConnection;
|
||||
}
|
||||
|
||||
protected function getSharedTcpServer(): Server
|
||||
{
|
||||
$portRange = config('expose.admin.tcp_port_range');
|
||||
|
||||
$port = $portRange['from'] ?? 50000;
|
||||
$maxPort = $portRange['to'] ?? 60000;
|
||||
|
||||
do {
|
||||
try {
|
||||
$portFound = true;
|
||||
$server = new Server('0.0.0.0:'.$port, $this->loop);
|
||||
} catch (\RuntimeException $exception) {
|
||||
$portFound = false;
|
||||
$port++;
|
||||
|
||||
if ($port > $maxPort) {
|
||||
throw new NoFreePortAvailable();
|
||||
}
|
||||
}
|
||||
} while (! $portFound);
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection
|
||||
{
|
||||
$this->httpConnections[$requestId] = new HttpConnection($httpConnection);
|
||||
@@ -82,16 +155,26 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
|
||||
if (isset($connection->client_id)) {
|
||||
$clientId = $connection->client_id;
|
||||
|
||||
$controlConnection = collect($this->connections)->first(function ($connection) use ($clientId) {
|
||||
return $connection->client_id == $clientId;
|
||||
});
|
||||
|
||||
if ($controlConnection instanceof TcpControlConnection) {
|
||||
$controlConnection->stop();
|
||||
$controlConnection = null;
|
||||
}
|
||||
|
||||
$this->connections = collect($this->connections)->reject(function ($connection) use ($clientId) {
|
||||
return $connection->client_id == $clientId;
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection
|
||||
public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection
|
||||
{
|
||||
return collect($this->connections)->last(function ($connection) use ($subdomain) {
|
||||
return $connection->subdomain == $subdomain;
|
||||
return collect($this->connections)->last(function ($connection) use ($subdomain, $serverHost) {
|
||||
return $connection->subdomain == $subdomain && $connection->serverHost === $serverHost;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,6 +185,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;
|
||||
@@ -118,9 +215,29 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
->filter(function ($connection) use ($authToken) {
|
||||
return $connection->authToken === $authToken;
|
||||
})
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === ControlConnection::class;
|
||||
})
|
||||
->map(function ($connection) {
|
||||
return $connection->toArray();
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getTcpConnectionsForAuthToken(string $authToken): array
|
||||
{
|
||||
return collect($this->connections)
|
||||
->filter(function ($connection) use ($authToken) {
|
||||
return $connection->authToken === $authToken;
|
||||
})
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === TcpControlConnection::class;
|
||||
})
|
||||
->map(function ($connection) {
|
||||
return $connection->toArray();
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use App\Http\QueryParameters;
|
||||
use Evenement\EventEmitterTrait;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -12,20 +13,24 @@ class ControlConnection
|
||||
/** @var ConnectionInterface */
|
||||
public $socket;
|
||||
public $host;
|
||||
public $serverHost;
|
||||
public $authToken;
|
||||
public $subdomain;
|
||||
public $client_id;
|
||||
public $client_version;
|
||||
public $proxies = [];
|
||||
protected $shared_at;
|
||||
|
||||
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $authToken = '')
|
||||
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $serverHost, string $authToken = '')
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->host = $host;
|
||||
$this->subdomain = $subdomain;
|
||||
$this->client_id = $clientId;
|
||||
$this->authToken = $authToken;
|
||||
$this->serverHost = $serverHost;
|
||||
$this->shared_at = now()->toDateTimeString();
|
||||
$this->client_version = QueryParameters::create($socket->httpRequest)->get('version');
|
||||
}
|
||||
|
||||
public function setMaximumConnectionLength(int $maximumConnectionLength)
|
||||
@@ -43,6 +48,8 @@ class ControlConnection
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'createProxy',
|
||||
'data' => [
|
||||
'host' => $this->host,
|
||||
'subdomain' => $this->subdomain,
|
||||
'request_id' => $requestId,
|
||||
'client_id' => $this->client_id,
|
||||
],
|
||||
@@ -57,8 +64,12 @@ class ControlConnection
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'type' => 'http',
|
||||
'host' => $this->host,
|
||||
'remote_address' => $this->socket->remoteAddress ?? null,
|
||||
'server_host' => $this->serverHost,
|
||||
'client_id' => $this->client_id,
|
||||
'client_version' => $this->client_version,
|
||||
'auth_token' => $this->authToken,
|
||||
'subdomain' => $this->subdomain,
|
||||
'shared_at' => $this->shared_at,
|
||||
|
||||
108
app/Server/Connections/TcpControlConnection.php
Normal file
108
app/Server/Connections/TcpControlConnection.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\RFC6455\Messaging\Frame;
|
||||
use React\Socket\Server;
|
||||
|
||||
class TcpControlConnection extends ControlConnection
|
||||
{
|
||||
public $proxy;
|
||||
public $proxyConnection;
|
||||
public $port;
|
||||
public $shared_port;
|
||||
public $shared_server;
|
||||
|
||||
public function __construct(ConnectionInterface $socket, int $port, Server $sharedServer, string $clientId, string $authToken = '')
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->client_id = $clientId;
|
||||
$this->shared_server = $sharedServer;
|
||||
$this->port = $port;
|
||||
$this->shared_at = now()->toDateTimeString();
|
||||
$this->shared_port = parse_url($sharedServer->getAddress(), PHP_URL_PORT);
|
||||
$this->authToken = $authToken;
|
||||
|
||||
$this->configureServer($sharedServer);
|
||||
}
|
||||
|
||||
public function setMaximumConnectionLength(int $maximumConnectionLength)
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'setMaximumConnectionLength',
|
||||
'data' => [
|
||||
'length' => $maximumConnectionLength,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function registerProxy($requestId)
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'createProxy',
|
||||
'data' => [
|
||||
'request_id' => $requestId,
|
||||
'client_id' => $this->client_id,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function registerTcpProxy($requestId)
|
||||
{
|
||||
$this->socket->send(json_encode([
|
||||
'event' => 'createTcpProxy',
|
||||
'data' => [
|
||||
'port' => $this->port,
|
||||
'tcp_request_id' => $requestId,
|
||||
'client_id' => $this->client_id,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->shared_server->close();
|
||||
$this->shared_server = null;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->socket->close();
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'type' => 'tcp',
|
||||
'port' => $this->port,
|
||||
'auth_token' => $this->authToken,
|
||||
'client_id' => $this->client_id,
|
||||
'shared_port' => $this->shared_port,
|
||||
'shared_at' => $this->shared_at,
|
||||
];
|
||||
}
|
||||
|
||||
protected function configureServer(Server $sharedServer)
|
||||
{
|
||||
$requestId = uniqid();
|
||||
|
||||
$sharedServer->on('connection', function (\React\Socket\ConnectionInterface $connection) use ($requestId) {
|
||||
$this->proxyConnection = $connection;
|
||||
|
||||
$this->once('tcp_proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($connection) {
|
||||
$this->proxy = $proxy;
|
||||
|
||||
$connection->on('data', function ($data) use ($proxy) {
|
||||
$binaryMsg = new Frame($data, true, Frame::OP_BINARY);
|
||||
$proxy->send($binaryMsg);
|
||||
});
|
||||
|
||||
$connection->resume();
|
||||
});
|
||||
|
||||
$connection->pause();
|
||||
$this->registerTcpProxy($requestId);
|
||||
});
|
||||
}
|
||||
}
|
||||
135
app/Server/DomainRepository/DatabaseDomainRepository.php
Normal file
135
app/Server/DomainRepository/DatabaseDomainRepository.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\DomainRepository;
|
||||
|
||||
use App\Contracts\DomainRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class DatabaseDomainRepository implements DomainRepository
|
||||
{
|
||||
/** @var DatabaseInterface */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function getDomains(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM domains ORDER by created_at DESC')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getDomainById($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM domains WHERE id = :id', ['id' => $id])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getDomainByName(string $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM domains WHERE domain = :name', ['name' => $name])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getDomainsByUserId($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM domains WHERE user_id = :user_id ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function storeDomain(array $data): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->getDomainByName($data['domain'])
|
||||
->then(function ($registeredDomain) use ($data, $deferred) {
|
||||
$this->database->query("
|
||||
INSERT INTO domains (user_id, domain, created_at)
|
||||
VALUES (:user_id, :domain, DATETIME('now'))
|
||||
", $data)
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$this->database->query('SELECT * FROM domains WHERE id = :id', ['id' => $result->insertId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getDomainsByUserIdAndName($id, $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM domains WHERE user_id = :user_id AND domain = :name ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
'name' => $name,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function deleteDomainForUserId($userId, $domainId): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('DELETE FROM domains WHERE id = :id AND user_id = :user_id', [
|
||||
'id' => $domainId,
|
||||
'user_id' => $userId,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function updateDomain($id, array $data): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
// TODO
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
7
app/Server/Exceptions/NoFreePortAvailable.php
Normal file
7
app/Server/Exceptions/NoFreePortAvailable.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Exceptions;
|
||||
|
||||
class NoFreePortAvailable extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -3,26 +3,46 @@
|
||||
namespace App\Server;
|
||||
|
||||
use App\Contracts\ConnectionManager as ConnectionManagerContract;
|
||||
use App\Contracts\DomainRepository;
|
||||
use App\Contracts\LoggerRepository;
|
||||
use App\Contracts\StatisticsCollector;
|
||||
use App\Contracts\StatisticsRepository;
|
||||
use App\Contracts\SubdomainGenerator;
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Http\RouteGenerator;
|
||||
use App\Http\Server as HttpServer;
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use App\Server\DomainRepository\DatabaseDomainRepository;
|
||||
use App\Server\Http\Controllers\Admin\DeleteSubdomainController;
|
||||
use App\Server\Http\Controllers\Admin\DeleteUsersController;
|
||||
use App\Server\Http\Controllers\Admin\DisconnectSiteController;
|
||||
use App\Server\Http\Controllers\Admin\DisconnectTcpConnectionController;
|
||||
use App\Server\Http\Controllers\Admin\GetLogsController;
|
||||
use App\Server\Http\Controllers\Admin\GetLogsForSubdomainController;
|
||||
use App\Server\Http\Controllers\Admin\GetSettingsController;
|
||||
use App\Server\Http\Controllers\Admin\GetSiteDetailsController;
|
||||
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;
|
||||
use App\Server\Http\Controllers\Admin\ListSitesController;
|
||||
use App\Server\Http\Controllers\Admin\ListTcpConnectionsController;
|
||||
use App\Server\Http\Controllers\Admin\ListUsersController;
|
||||
use App\Server\Http\Controllers\Admin\RedirectToUsersController;
|
||||
use App\Server\Http\Controllers\Admin\ShowSettingsController;
|
||||
use App\Server\Http\Controllers\Admin\StoreDomainController;
|
||||
use App\Server\Http\Controllers\Admin\StoreSettingsController;
|
||||
use App\Server\Http\Controllers\Admin\StoreSubdomainController;
|
||||
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\LoggerRepository\NullLogger;
|
||||
use App\Server\StatisticsCollector\DatabaseStatisticsCollector;
|
||||
use App\Server\StatisticsRepository\DatabaseStatisticsRepository;
|
||||
use App\Server\SubdomainRepository\DatabaseSubdomainRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Phar;
|
||||
use Ratchet\Server\IoServer;
|
||||
@@ -120,15 +140,32 @@ class Factory
|
||||
$this->router->get('/users', ListUsersController::class, $adminCondition);
|
||||
$this->router->get('/settings', ShowSettingsController::class, $adminCondition);
|
||||
$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);
|
||||
$this->router->post('/api/users', StoreUsersController::class, $adminCondition);
|
||||
$this->router->get('/api/users/{id}', GetUserDetailsController::class, $adminCondition);
|
||||
$this->router->delete('/api/users/{id}', DeleteUsersController::class, $adminCondition);
|
||||
|
||||
$this->router->get('/api/logs', GetLogsController::class, $adminCondition);
|
||||
$this->router->get('/api/logs/{subdomain}', GetLogsForSubdomainController::class, $adminCondition);
|
||||
|
||||
$this->router->post('/api/domains', StoreDomainController::class, $adminCondition);
|
||||
$this->router->delete('/api/domains/{domain}', DeleteSubdomainController::class, $adminCondition);
|
||||
|
||||
$this->router->post('/api/subdomains', StoreSubdomainController::class, $adminCondition);
|
||||
$this->router->delete('/api/subdomains/{subdomain}', DeleteSubdomainController::class, $adminCondition);
|
||||
|
||||
$this->router->get('/api/sites', GetSitesController::class, $adminCondition);
|
||||
$this->router->get('/api/sites/{site}', GetSiteDetailsController::class, $adminCondition);
|
||||
$this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition);
|
||||
|
||||
$this->router->get('/api/tcp', GetTcpConnectionsController::class, $adminCondition);
|
||||
$this->router->delete('/api/tcp/{id}', DisconnectTcpConnectionController::class, $adminCondition);
|
||||
}
|
||||
|
||||
protected function bindConfiguration()
|
||||
@@ -165,8 +202,12 @@ class Factory
|
||||
$this->bindConfiguration()
|
||||
->bindSubdomainGenerator()
|
||||
->bindUserRepository()
|
||||
->bindLoggerRepository()
|
||||
->bindSubdomainRepository()
|
||||
->bindDomainRepository()
|
||||
->bindDatabase()
|
||||
->ensureDatabaseIsInitialized()
|
||||
->registerStatisticsCollector()
|
||||
->bindConnectionManager()
|
||||
->addAdminRoutes();
|
||||
|
||||
@@ -201,6 +242,33 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindSubdomainRepository()
|
||||
{
|
||||
app()->singleton(SubdomainRepository::class, function () {
|
||||
return app(config('expose.admin.subdomain_repository', DatabaseSubdomainRepository::class));
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindLoggerRepository()
|
||||
{
|
||||
app()->singleton(LoggerRepository::class, function () {
|
||||
return app(config('expose.admin.logger_repository', NullLogger::class));
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindDomainRepository()
|
||||
{
|
||||
app()->singleton(DomainRepository::class, function () {
|
||||
return app(config('expose.admin.domain_repository', DatabaseDomainRepository::class));
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindDatabase()
|
||||
{
|
||||
app()->singleton(DatabaseInterface::class, function () {
|
||||
@@ -227,7 +295,8 @@ class Factory
|
||||
->files()
|
||||
->ignoreDotFiles(true)
|
||||
->in(database_path('migrations'))
|
||||
->name('*.sql');
|
||||
->name('*.sql')
|
||||
->sortByName();
|
||||
|
||||
/** @var SplFileInfo $migration */
|
||||
foreach ($migrations as $migration) {
|
||||
@@ -243,4 +312,27 @@ class Factory
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function registerStatisticsCollector()
|
||||
{
|
||||
if (config('expose.admin.statistics.enable_statistics', true) === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
@@ -14,7 +14,7 @@ abstract class AdminController extends Controller
|
||||
protected function shouldHandleRequest(Request $request, ConnectionInterface $httpConnection): bool
|
||||
{
|
||||
try {
|
||||
$authorization = Str::after($request->header('Authorization'), 'Basic ');
|
||||
$authorization = Str::after($request->header('Authorization', ''), 'Basic ');
|
||||
$authParts = explode(':', base64_decode($authorization), 2);
|
||||
[$user, $password] = $authParts;
|
||||
|
||||
@@ -24,9 +24,11 @@ abstract class AdminController extends Controller
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$httpConnection->send(str(new Response(401, [
|
||||
$httpConnection->send(Message::toString(new Response(401, [
|
||||
'WWW-Authenticate' => 'Basic realm="Expose"',
|
||||
])));
|
||||
|
||||
$httpConnection->close();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class DeleteSubdomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository->getUserByToken($request->get('auth_token', ''))
|
||||
->then(function ($user) use ($request, $httpConnection) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->subdomainRepository->deleteSubdomainForUserId($user['id'], $request->get('subdomain'))
|
||||
->then(function ($deleted) use ($httpConnection) {
|
||||
$httpConnection->send(respond_json(['deleted' => $deleted], 200));
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,11 @@ class DisconnectSiteController extends AdminController
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id'));
|
||||
if ($request->has('server_host')) {
|
||||
$connection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($request->get('id'), $request->get('server_host'));
|
||||
} else {
|
||||
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id'));
|
||||
}
|
||||
|
||||
if (! is_null($connection)) {
|
||||
$connection->close();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Configuration;
|
||||
use App\Server\Connections\TcpControlConnection;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class DisconnectTcpConnectionController extends AdminController
|
||||
{
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$connection = $this->connectionManager->findControlConnectionForClientId($request->get('id'));
|
||||
|
||||
if (! is_null($connection)) {
|
||||
$connection->close();
|
||||
|
||||
$this->connectionManager->removeControlConnection($connection);
|
||||
}
|
||||
|
||||
$httpConnection->send(respond_json([
|
||||
'tcp_connections' => collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === TcpControlConnection::class;
|
||||
})
|
||||
->values(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
37
app/Server/Http/Controllers/Admin/GetLogsController.php
Normal file
37
app/Server/Http/Controllers/Admin/GetLogsController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\LoggerRepository;
|
||||
use App\Server\Configuration;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetLogsController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
/** @var LoggerRepository */
|
||||
protected $logger;
|
||||
|
||||
public function __construct(LoggerRepository $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$subdomain = $request->get('subdomain');
|
||||
$this->logger->getLogs()
|
||||
->then(function ($logs) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['logs' => $logs])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\LoggerRepository;
|
||||
use App\Server\Configuration;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetLogsForSubdomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
/** @var LoggerRepository */
|
||||
protected $logger;
|
||||
|
||||
public function __construct(LoggerRepository $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$subdomain = $request->get('subdomain');
|
||||
$this->logger->getLogsBySubdomain($subdomain)
|
||||
->then(function ($logs) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['logs' => $logs])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Configuration;
|
||||
use App\Server\Connections\ControlConnection;
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetSiteDetailsController extends AdminController
|
||||
{
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$domain = $request->get('site');
|
||||
|
||||
$connectedSite = collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === ControlConnection::class;
|
||||
})
|
||||
->first(function (ControlConnection $site) use ($domain) {
|
||||
return "{$site->subdomain}.{$site->serverHost}" === $domain;
|
||||
});
|
||||
|
||||
if (is_null($connectedSite)) {
|
||||
$httpConnection->send(
|
||||
Message::toString(new Response(404))
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$httpConnection->send(
|
||||
respond_json($connectedSite->toArray())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,63 @@
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Server\Configuration;
|
||||
use App\Server\Connections\ControlConnection;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetSitesController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'sites' => collect($this->connectionManager->getConnections())->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
$authTokens = [];
|
||||
|
||||
$sites = collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === ControlConnection::class;
|
||||
})
|
||||
->map(function ($site, $siteId) use (&$authTokens) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
$authTokens[] = $site['auth_token'];
|
||||
|
||||
return $site;
|
||||
})->values();
|
||||
|
||||
$this->userRepository->getUsersByTokens($authTokens)
|
||||
->then(function ($users) use ($httpConnection, $sites) {
|
||||
$users = collect($users);
|
||||
$sites = collect($sites)->map(function ($site) use ($users) {
|
||||
$site['user'] = $users->firstWhere('auth_token', $site['auth_token']);
|
||||
|
||||
return $site;
|
||||
})->values(),
|
||||
])
|
||||
);
|
||||
})->toArray();
|
||||
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'sites' => $sites,
|
||||
])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\StatisticsRepository;
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Server\Configuration;
|
||||
use App\Server\Connections\TcpControlConnection;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetTcpConnectionsController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, Configuration $configuration, UserRepository $userRepository)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$authTokens = [];
|
||||
$connections = collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === TcpControlConnection::class;
|
||||
})
|
||||
->map(function ($site, $siteId) use (&$authTokens) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
$authTokens[] = $site['auth_token'];
|
||||
|
||||
return $site;
|
||||
})
|
||||
->values();
|
||||
|
||||
$this->userRepository->getUsersByTokens($authTokens)
|
||||
->then(function ($users) use ($httpConnection, $connections) {
|
||||
$users = collect($users);
|
||||
$connections = collect($connections)->map(function ($connection) use ($users) {
|
||||
$connection['user'] = $users->firstWhere('auth_token', $connection['auth_token']);
|
||||
|
||||
return $connection;
|
||||
})->toArray();
|
||||
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'tcp_connections' => $connections,
|
||||
])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
@@ -13,21 +14,46 @@ class GetUserDetailsController extends AdminController
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository)
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUserById($request->get('id'))
|
||||
->then(function ($user) use ($httpConnection) {
|
||||
$id = $request->get('id');
|
||||
|
||||
if (! is_numeric($id)) {
|
||||
$promise = $this->userRepository->getUserByToken($id);
|
||||
} else {
|
||||
$promise = $this->userRepository->getUserById($id);
|
||||
}
|
||||
|
||||
$promise->then(function ($user) use ($httpConnection) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(
|
||||
respond_json(['user' => $user])
|
||||
respond_json([], 404)
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
$this->subdomainRepository->getSubdomainsByUserId($user['id'])
|
||||
->then(function ($subdomains) use ($httpConnection, $user) {
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'user' => $user,
|
||||
'subdomains' => $subdomains,
|
||||
])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class GetUsersController extends AdminController
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->paginateUsers(20, (int) $request->get('page', 1))
|
||||
->paginateUsers($request->get('search', ''), (int) $request->get('perPage', 20), (int) $request->get('page', 1))
|
||||
->then(function ($paginated) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['paginated' => $paginated])
|
||||
|
||||
@@ -25,12 +25,6 @@ class ListSitesController extends AdminController
|
||||
$sites = $this->getView($httpConnection, 'server.sites.index', [
|
||||
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
|
||||
'configuration' => $this->configuration,
|
||||
'sites' => collect($this->connectionManager->getConnections())->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
|
||||
return $site;
|
||||
})->values(),
|
||||
]);
|
||||
|
||||
$httpConnection->send(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Configuration;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class ListTcpConnectionsController extends AdminController
|
||||
{
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$sites = $this->getView($httpConnection, 'server.tcp.index', [
|
||||
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
|
||||
'configuration' => $this->configuration,
|
||||
]);
|
||||
|
||||
$httpConnection->send(
|
||||
respond_html($sites)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class ListUsersController extends AdminController
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->paginateUsers(20, (int) $request->get('page', 1))
|
||||
->paginateUsers($request->get('search', ''), 20, (int) $request->get('page', 1))
|
||||
->then(function ($paginated) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_html($this->getView($httpConnection, 'server.users.index', ['paginated' => $paginated]))
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class RedirectToUsersController extends AdminController
|
||||
{
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$httpConnection->send(str(new Response(301, [
|
||||
$httpConnection->send(Message::toString(new Response(301, [
|
||||
'Location' => '/sites',
|
||||
])));
|
||||
}
|
||||
|
||||
72
app/Server/Http/Controllers/Admin/StoreDomainController.php
Normal file
72
app/Server/Http/Controllers/Admin/StoreDomainController.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\DomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class StoreDomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var DomainRepository */
|
||||
protected $domainRepository;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, DomainRepository $domainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->domainRepository = $domainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'domain' => 'required',
|
||||
], [
|
||||
'required' => 'The :attribute field is required.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userRepository
|
||||
->getUserByToken($request->get('auth_token', ''))
|
||||
->then(function ($user) use ($httpConnection, $request) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user['can_specify_domains'] === 0) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve custom domains.'], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$insertData = [
|
||||
'user_id' => $user['id'],
|
||||
'domain' => $request->get('domain'),
|
||||
];
|
||||
|
||||
$this->domainRepository
|
||||
->storeDomain($insertData)
|
||||
->then(function ($domain) use ($httpConnection) {
|
||||
$httpConnection->send(respond_json(['domain' => $domain], 200));
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,14 @@ class StoreSettingsController extends AdminController
|
||||
|
||||
config()->set('expose.admin.messages.message_of_the_day', Arr::get($messages, 'message_of_the_day'));
|
||||
|
||||
config()->set('expose.admin.messages.custom_subdomain_unauthorized', Arr::get($messages, 'custom_subdomain_unauthorized'));
|
||||
|
||||
config()->set('expose.admin.messages.no_free_tcp_port_available', Arr::get($messages, 'no_free_tcp_port_available'));
|
||||
|
||||
config()->set('expose.admin.messages.tcp_port_sharing_unauthorized', Arr::get($messages, 'tcp_port_sharing_unauthorized'));
|
||||
|
||||
config()->set('expose.admin.messages.tcp_port_sharing_disabled', Arr::get($messages, 'tcp_port_sharing_disabled'));
|
||||
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'configuration' => $this->configuration,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Server\Configuration;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class StoreSubdomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'subdomain' => 'required',
|
||||
], [
|
||||
'required' => 'The :attribute field is required.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userRepository
|
||||
->getUserByToken($request->get('auth_token', ''))
|
||||
->then(function ($user) use ($httpConnection, $request) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user['can_specify_subdomains'] === 0) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve subdomains.'], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($request->get('subdomain'), config('expose.admin.reserved_subdomains', []))) {
|
||||
$httpConnection->send(respond_json(['error' => 'The subdomain is already taken.'], 422));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$insertData = [
|
||||
'user_id' => $user['id'],
|
||||
'subdomain' => $request->get('subdomain'),
|
||||
'domain' => $request->get('domain', $this->configuration->hostname()),
|
||||
];
|
||||
|
||||
$this->subdomainRepository
|
||||
->storeSubdomain($insertData)
|
||||
->then(function ($subdomain) use ($httpConnection) {
|
||||
$httpConnection->send(respond_json(['subdomain' => $subdomain], 200));
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\UserRepository;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -38,7 +37,11 @@ class StoreUsersController extends AdminController
|
||||
|
||||
$insertData = [
|
||||
'name' => $request->get('name'),
|
||||
'auth_token' => (string) Str::uuid(),
|
||||
'auth_token' => $request->get('token', (string) Str::uuid()),
|
||||
'can_specify_subdomains' => (int) $request->get('can_specify_subdomains'),
|
||||
'can_specify_domains' => (int) $request->get('can_specify_domains'),
|
||||
'can_share_tcp_ports' => (int) $request->get('can_share_tcp_ports'),
|
||||
'max_connections' => (int) $request->get('max_connections'),
|
||||
];
|
||||
|
||||
$this->userRepository
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
namespace App\Server\Http\Controllers;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Contracts\DomainRepository;
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Http\QueryParameters;
|
||||
use App\Server\Configuration;
|
||||
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 function React\Promise\reject;
|
||||
use stdClass;
|
||||
|
||||
class ControlMessageController implements MessageComponentInterface
|
||||
@@ -19,10 +25,22 @@ class ControlMessageController implements MessageComponentInterface
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository)
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
/** @var DomainRepository */
|
||||
protected $domainRepository;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository, Configuration $configuration, DomainRepository $domainRepository)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
$this->domainRepository = $domainRepository;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +71,10 @@ class ControlMessageController implements MessageComponentInterface
|
||||
if (isset($connection->request_id)) {
|
||||
return $this->sendResponseToHttpConnection($connection->request_id, $msg);
|
||||
}
|
||||
if (isset($connection->tcp_request_id)) {
|
||||
$connectionInfo = $this->connectionManager->findControlConnectionForClientId($connection->tcp_client_id);
|
||||
$connectionInfo->proxyConnection->write($msg);
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = json_decode($msg);
|
||||
@@ -75,24 +97,48 @@ class ControlMessageController implements MessageComponentInterface
|
||||
|
||||
protected function authenticate(ConnectionInterface $connection, $data)
|
||||
{
|
||||
if (! isset($data->subdomain)) {
|
||||
$data->subdomain = null;
|
||||
}
|
||||
if (! isset($data->type)) {
|
||||
$data->type = 'http';
|
||||
}
|
||||
if (! isset($data->server_host) || is_null($data->server_host)) {
|
||||
$data->server_host = $this->configuration->hostname();
|
||||
}
|
||||
|
||||
$this->verifyAuthToken($connection)
|
||||
->then(function () use ($connection, $data) {
|
||||
if (! $this->hasValidSubdomain($connection, $data->subdomain)) {
|
||||
return;
|
||||
->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']));
|
||||
}
|
||||
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection);
|
||||
if ($maximumConnectionCount > 0 && $connectionCount + 1 > $maximumConnectionCount) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.maximum_connection_count'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
|
||||
reject(null);
|
||||
}
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.message_of_the_day'),
|
||||
'subdomain' => $connectionInfo->subdomain,
|
||||
'client_id' => $connectionInfo->client_id,
|
||||
],
|
||||
]));
|
||||
return $user;
|
||||
})
|
||||
->then(function ($user) use ($connection, $data) {
|
||||
if ($data->type === 'http') {
|
||||
$this->handleHttpConnection($connection, $data, $user);
|
||||
} elseif ($data->type === 'tcp') {
|
||||
$this->handleTcpConnection($connection, $data, $user);
|
||||
}
|
||||
}, function () use ($connection) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
@@ -104,6 +150,91 @@ class ControlMessageController implements MessageComponentInterface
|
||||
});
|
||||
}
|
||||
|
||||
protected function resolveConnectionMessage($connectionInfo, $user)
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$connectionMessageResolver = config('expose.admin.messages.resolve_connection_message')($connectionInfo, $user);
|
||||
|
||||
if ($connectionMessageResolver instanceof PromiseInterface) {
|
||||
$connectionMessageResolver->then(function ($connectionMessage) use ($connectionInfo, $deferred) {
|
||||
$connectionInfo->message = $connectionMessage;
|
||||
$deferred->resolve($connectionInfo);
|
||||
});
|
||||
} else {
|
||||
$connectionInfo->message = $connectionMessageResolver;
|
||||
|
||||
return \React\Promise\resolve($connectionInfo);
|
||||
}
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null)
|
||||
{
|
||||
$this->hasValidDomain($connection, $data->server_host, $user)
|
||||
->then(function () use ($connection, $data, $user) {
|
||||
return $this->hasValidSubdomain($connection, $data->subdomain, $user, $data->server_host);
|
||||
})
|
||||
->then(function ($subdomain) use ($data, $connection, $user) {
|
||||
if ($subdomain === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data->subdomain = $subdomain;
|
||||
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection);
|
||||
|
||||
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
|
||||
|
||||
return $this->resolveConnectionMessage($connectionInfo, $user);
|
||||
})
|
||||
->then(function ($connectionInfo) use ($connection, $user) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'data' => [
|
||||
'message' => $connectionInfo->message,
|
||||
'subdomain' => $connectionInfo->subdomain,
|
||||
'server_host' => $connectionInfo->serverHost,
|
||||
'user' => $user,
|
||||
'client_id' => $connectionInfo->client_id,
|
||||
],
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
protected function handleTcpConnection(ConnectionInterface $connection, $data, $user = null)
|
||||
{
|
||||
if (! $this->canShareTcpPorts($connection, $data, $user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$connectionInfo = $this->connectionManager->storeTcpConnection($data->port, $connection);
|
||||
} catch (NoFreePortAvailable $exception) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.no_free_tcp_port_available'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.resolve_connection_message')($connectionInfo, $user),
|
||||
'user' => $user,
|
||||
'port' => $connectionInfo->port,
|
||||
'shared_port' => $connectionInfo->shared_port,
|
||||
'client_id' => $connectionInfo->client_id,
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
protected function registerProxy(ConnectionInterface $connection, $data)
|
||||
{
|
||||
$connection->request_id = $data->request_id;
|
||||
@@ -115,6 +246,18 @@ class ControlMessageController implements MessageComponentInterface
|
||||
]);
|
||||
}
|
||||
|
||||
protected function registerTcpProxy(ConnectionInterface $connection, $data)
|
||||
{
|
||||
$connection->tcp_client_id = $data->client_id;
|
||||
$connection->tcp_request_id = $data->tcp_request_id;
|
||||
|
||||
$connectionInfo = $this->connectionManager->findControlConnectionForClientId($data->client_id);
|
||||
|
||||
$connectionInfo->emit('tcp_proxy_ready_'.$data->tcp_request_id, [
|
||||
$connection,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -139,31 +282,141 @@ 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain): bool
|
||||
protected function hasValidDomain(ConnectionInterface $connection, ?string $serverHost, ?array $user): PromiseInterface
|
||||
{
|
||||
if (! is_null($user) && $serverHost !== $this->configuration->hostname()) {
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->domainRepository
|
||||
->getDomainsByUserId($user['id'])
|
||||
->then(function ($domains) use ($connection, $deferred, $serverHost) {
|
||||
$userDomain = collect($domains)->first(function ($domain) use ($serverHost) {
|
||||
return strtolower($domain['domain']) === strtolower($serverHost);
|
||||
});
|
||||
|
||||
if (is_null($userDomain)) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.custom_domain_unauthorized').PHP_EOL,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
$deferred->reject(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$deferred->resolve(null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
} else {
|
||||
return \React\Promise\resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user, string $serverHost): PromiseInterface
|
||||
{
|
||||
/**
|
||||
* Check if the user can specify a custom subdomain in the first place.
|
||||
*/
|
||||
if (! is_null($user) && $user['can_specify_subdomains'] === 0 && ! is_null($subdomain)) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'error',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL,
|
||||
],
|
||||
]));
|
||||
|
||||
return \React\Promise\resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given subdomain is reserved for a different user.
|
||||
*/
|
||||
if (! is_null($subdomain)) {
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
|
||||
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) {
|
||||
$message = config('expose.admin.messages.subdomain_taken');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
return $this->subdomainRepository->getSubdomainsByNameAndDomain($subdomain, $serverHost)
|
||||
->then(function ($foundSubdomains) use ($connection, $subdomain, $user, $serverHost) {
|
||||
$ownSubdomain = collect($foundSubdomains)->first(function ($subdomain) use ($user) {
|
||||
return $subdomain['user_id'] === $user['id'];
|
||||
});
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
if (count($foundSubdomains) > 0 && ! is_null($user) && is_null($ownSubdomain)) {
|
||||
$message = config('expose.admin.messages.subdomain_reserved', '');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
|
||||
return false;
|
||||
}
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return \React\Promise\resolve(false);
|
||||
}
|
||||
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
|
||||
|
||||
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain') || in_array($subdomain, config('expose.admin.reserved_subdomains', []))) {
|
||||
$message = config('expose.admin.messages.subdomain_taken');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return \React\Promise\resolve(false);
|
||||
}
|
||||
|
||||
return \React\Promise\resolve($subdomain);
|
||||
});
|
||||
}
|
||||
|
||||
return \React\Promise\resolve($subdomain);
|
||||
}
|
||||
|
||||
protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user)
|
||||
{
|
||||
if (! config('expose.admin.allow_tcp_port_sharing', true)) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.tcp_port_sharing_disabled'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_null($user) && $user['can_share_tcp_ports'] === 0) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.tcp_port_sharing_unauthorized'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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,15 +28,20 @@ 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)
|
||||
{
|
||||
$subdomain = $this->detectSubdomain($request);
|
||||
$serverHost = $this->detectServerHost($request);
|
||||
|
||||
if (is_null($subdomain)) {
|
||||
$httpConnection->send(
|
||||
@@ -46,7 +52,7 @@ class TunnelMessageController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
|
||||
|
||||
if (is_null($controlConnection)) {
|
||||
$httpConnection->send(
|
||||
@@ -57,14 +63,23 @@ class TunnelMessageController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$this->statisticsCollector->incomingRequest();
|
||||
|
||||
$this->sendRequestToClient($request, $controlConnection, $httpConnection);
|
||||
}
|
||||
|
||||
protected function detectSubdomain(Request $request): ?string
|
||||
{
|
||||
$subdomain = Str::before($request->getHost(), '.'.$this->configuration->hostname());
|
||||
$serverHost = $this->detectServerHost($request);
|
||||
|
||||
return $subdomain === $request->getHost() ? null : $subdomain;
|
||||
$subdomain = Str::before($request->header('Host'), '.'.$serverHost);
|
||||
|
||||
return $subdomain === $request->header('Host') ? null : $subdomain;
|
||||
}
|
||||
|
||||
protected function detectServerHost(Request $request): ?string
|
||||
{
|
||||
return Str::before(Str::after($request->header('Host'), '.'), ':');
|
||||
}
|
||||
|
||||
protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection)
|
||||
@@ -75,7 +90,7 @@ class TunnelMessageController extends Controller
|
||||
|
||||
$httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId);
|
||||
|
||||
transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection , $requestId) {
|
||||
transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $requestId) {
|
||||
$controlConnection->once('proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($request) {
|
||||
// Convert the Laravel request into a PSR7 request
|
||||
$psr17Factory = new Psr17Factory();
|
||||
@@ -107,7 +122,7 @@ class TunnelMessageController extends Controller
|
||||
{
|
||||
$request::setTrustedProxies([$controlConnection->socket->remoteAddress, '127.0.0.1'], Request::HEADER_X_FORWARDED_ALL);
|
||||
|
||||
$host = $this->configuration->hostname();
|
||||
$host = $controlConnection->serverHost;
|
||||
|
||||
if (! $request->isSecure()) {
|
||||
$host .= ":{$this->configuration->port()}";
|
||||
|
||||
@@ -35,6 +35,7 @@ class Router implements HttpServerInterface
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface
|
||||
*/
|
||||
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null)
|
||||
|
||||
83
app/Server/LoggerRepository/DatabaseLogger.php
Normal file
83
app/Server/LoggerRepository/DatabaseLogger.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\LoggerRepository;
|
||||
|
||||
use App\Contracts\LoggerRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class DatabaseLogger implements LoggerRepository
|
||||
{
|
||||
/** @var DatabaseInterface */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function logSubdomain($authToken, $subdomain)
|
||||
{
|
||||
app(UserRepository::class)->getUserByToken($authToken)
|
||||
->then(function ($user) use ($subdomain) {
|
||||
$this->database->query("
|
||||
INSERT INTO logs (user_id, subdomain, created_at)
|
||||
VALUES (:user_id, :subdomain, DATETIME('now'))
|
||||
", [
|
||||
'user_id' => $user['id'],
|
||||
'subdomain' => $subdomain,
|
||||
])->then(function () {
|
||||
$this->cleanOldLogs();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function cleanOldLogs()
|
||||
{
|
||||
$this->database->query("DELETE FROM logs WHERE created_at < date('now', '-30 day')");
|
||||
}
|
||||
|
||||
public function getLogsBySubdomain($subdomain): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('
|
||||
SELECT
|
||||
logs.id AS log_id,
|
||||
logs.subdomain,
|
||||
users.*
|
||||
FROM logs
|
||||
INNER JOIN users
|
||||
ON users.id = logs.user_id
|
||||
WHERE logs.subdomain = :subdomain', ['subdomain' => $subdomain])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getLogs(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('
|
||||
SELECT
|
||||
logs.id AS log_id,
|
||||
logs.subdomain,
|
||||
users.*
|
||||
FROM logs
|
||||
INNER JOIN users
|
||||
ON users.id = logs.user_id')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
24
app/Server/LoggerRepository/NullLogger.php
Normal file
24
app/Server/LoggerRepository/NullLogger.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\LoggerRepository;
|
||||
|
||||
use App\Contracts\LoggerRepository;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class NullLogger implements LoggerRepository
|
||||
{
|
||||
public function logSubdomain($authToken, $subdomain)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
|
||||
public function getLogsBySubdomain($subdomain): PromiseInterface
|
||||
{
|
||||
return \React\Promise\resolve([]);
|
||||
}
|
||||
|
||||
public function getLogs(): PromiseInterface
|
||||
{
|
||||
return \React\Promise\resolve([]);
|
||||
}
|
||||
}
|
||||
106
app/Server/StatisticsCollector/DatabaseStatisticsCollector.php
Normal file
106
app/Server/StatisticsCollector/DatabaseStatisticsCollector.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\StatisticsCollector;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
155
app/Server/SubdomainRepository/DatabaseSubdomainRepository.php
Normal file
155
app/Server/SubdomainRepository/DatabaseSubdomainRepository.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\SubdomainRepository;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class DatabaseSubdomainRepository implements SubdomainRepository
|
||||
{
|
||||
/** @var DatabaseInterface */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function getSubdomains(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains ORDER by created_at DESC')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainById($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $id])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainByName(string $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE subdomain = :name', ['name' => $name])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainByNameAndDomain(string $name, string $domain): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE subdomain = :name AND domain = :domain', [
|
||||
'name' => $name,
|
||||
'domain' => $domain,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainsByNameAndDomain(string $name, string $domain): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE subdomain = :name AND domain = :domain', [
|
||||
'name' => $name,
|
||||
'domain' => $domain,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainsByUserId($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE user_id = :user_id ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function storeSubdomain(array $data): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query("
|
||||
INSERT INTO subdomains (user_id, subdomain, domain, created_at)
|
||||
VALUES (:user_id, :subdomain, :domain, DATETIME('now'))
|
||||
", $data)
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE user_id = :user_id AND subdomain = :name ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
'name' => $name,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('DELETE FROM subdomains WHERE (id = :id OR subdomain = :id) AND user_id = :user_id', [
|
||||
'id' => $subdomainId,
|
||||
'user_id' => $userId,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
42
app/Server/Support/RetrieveWelcomeMessageFromApi.php
Normal file
42
app/Server/Support/RetrieveWelcomeMessageFromApi.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Support;
|
||||
|
||||
use App\Server\Connections\ControlConnection;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class RetrieveWelcomeMessageFromApi
|
||||
{
|
||||
/** @var Browser */
|
||||
protected $browser;
|
||||
|
||||
/** @var string */
|
||||
protected $url;
|
||||
|
||||
public function __construct(Browser $browser)
|
||||
{
|
||||
$this->browser = $browser;
|
||||
|
||||
$this->url = config('expose.admin.welcome_message_api_url');
|
||||
}
|
||||
|
||||
public function forUser(ControlConnection $connectionInfo, $user)
|
||||
{
|
||||
return $this->browser
|
||||
->post($this->url, [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
], json_encode([
|
||||
'user' => $user,
|
||||
'connectionInfo' => $connectionInfo->toArray(),
|
||||
]))
|
||||
->then(function (ResponseInterface $response) {
|
||||
$result = json_decode($response->getBody());
|
||||
|
||||
return $result->message ?? '';
|
||||
}, function (Exception $e) {
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -36,34 +36,52 @@ class DatabaseUserRepository implements UserRepository
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function paginateUsers(int $perPage, int $currentPage): PromiseInterface
|
||||
public function paginateUsers(string $searchQuery, int $perPage, int $currentPage): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM users ORDER by created_at DESC LIMIT :limit OFFSET :offset', [
|
||||
'limit' => $perPage + 1,
|
||||
'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred, $perPage, $currentPage) {
|
||||
if (count($result->rows) == $perPage + 1) {
|
||||
array_pop($result->rows);
|
||||
$nextPage = $currentPage + 1;
|
||||
}
|
||||
->query('SELECT COUNT(*) AS count FROM users')
|
||||
->then(function (Result $result) use ($searchQuery, $deferred, $perPage, $currentPage) {
|
||||
$totalUsers = $result->rows[0]['count'];
|
||||
|
||||
$users = collect($result->rows)->map(function ($user) {
|
||||
return $this->getUserDetails($user);
|
||||
})->toArray();
|
||||
$query = 'SELECT * FROM users ';
|
||||
|
||||
$paginated = [
|
||||
'users' => $users,
|
||||
'current_page' => $currentPage,
|
||||
'per_page' => $perPage,
|
||||
'next_page' => $nextPage ?? null,
|
||||
'previous_page' => $currentPage > 1 ? $currentPage - 1 : null,
|
||||
$bindings = [
|
||||
'limit' => $perPage + 1,
|
||||
'offset' => $currentPage < 2 ? 0 : ($currentPage - 1) * $perPage,
|
||||
];
|
||||
|
||||
$deferred->resolve($paginated);
|
||||
if ($searchQuery !== '') {
|
||||
$query .= "WHERE name LIKE '%".$searchQuery."%' ";
|
||||
$bindings['search'] = $searchQuery;
|
||||
}
|
||||
|
||||
$query .= ' ORDER by created_at DESC LIMIT :limit OFFSET :offset';
|
||||
|
||||
$this->database
|
||||
->query($query, $bindings)
|
||||
->then(function (Result $result) use ($deferred, $perPage, $currentPage, $totalUsers) {
|
||||
if (count($result->rows) == $perPage + 1) {
|
||||
array_pop($result->rows);
|
||||
$nextPage = $currentPage + 1;
|
||||
}
|
||||
|
||||
$users = collect($result->rows)->map(function ($user) {
|
||||
return $this->getUserDetails($user);
|
||||
})->toArray();
|
||||
|
||||
$paginated = [
|
||||
'total' => $totalUsers,
|
||||
'users' => $users,
|
||||
'current_page' => $currentPage,
|
||||
'per_page' => $perPage,
|
||||
'next_page' => $nextPage ?? null,
|
||||
'previous_page' => $currentPage > 1 ? $currentPage - 1 : null,
|
||||
];
|
||||
|
||||
$deferred->resolve($paginated);
|
||||
});
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
@@ -72,6 +90,7 @@ class DatabaseUserRepository implements UserRepository
|
||||
protected function getUserDetails(array $user)
|
||||
{
|
||||
$user['sites'] = $user['auth_token'] !== '' ? $this->connectionManager->getConnectionsForAuthToken($user['auth_token']) : [];
|
||||
$user['tcp_connections'] = $user['auth_token'] !== '' ? $this->connectionManager->getTcpConnectionsForAuthToken($user['auth_token']) : [];
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -95,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();
|
||||
@@ -102,7 +134,13 @@ class DatabaseUserRepository implements UserRepository
|
||||
$this->database
|
||||
->query('SELECT * FROM users WHERE auth_token = :token', ['token' => $authToken])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
$user = $result->rows[0] ?? null;
|
||||
|
||||
if (! is_null($user)) {
|
||||
$user = $this->getUserDetails($user);
|
||||
}
|
||||
|
||||
$deferred->resolve($user);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
@@ -112,15 +150,38 @@ class DatabaseUserRepository implements UserRepository
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query("
|
||||
INSERT INTO users (name, auth_token, created_at)
|
||||
VALUES (:name, :auth_token, DATETIME('now'))
|
||||
$this->getUserByToken($data['auth_token'])
|
||||
->then(function ($existingUser) use ($data, $deferred) {
|
||||
if (is_null($existingUser)) {
|
||||
$this->database->query("
|
||||
INSERT INTO users (name, auth_token, can_specify_subdomains, can_specify_domains, can_share_tcp_ports, max_connections, created_at)
|
||||
VALUES (:name, :auth_token, :can_specify_subdomains, :can_specify_domains, :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])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $result->insertId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
$this->database->query('
|
||||
UPDATE users
|
||||
SET
|
||||
name = :name,
|
||||
can_specify_subdomains = :can_specify_subdomains,
|
||||
can_specify_domains = :can_specify_domains,
|
||||
can_share_tcp_ports = :can_share_tcp_ports,
|
||||
max_connections = :max_connections
|
||||
WHERE
|
||||
auth_token = :auth_token
|
||||
', $data)
|
||||
->then(function (Result $result) use ($existingUser, $deferred) {
|
||||
$this->database->query('SELECT * FROM users WHERE id = :id', ['id' => $existingUser['id']])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
@@ -130,11 +191,31 @@ class DatabaseUserRepository implements UserRepository
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('DELETE FROM users WHERE id = :id', ['id' => $id])
|
||||
$this->database->query('DELETE FROM users WHERE id = :id OR auth_token = :id', ['id' => $id])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getUsersByTokens(array $authTokens): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$authTokenString = collect($authTokens)->map(function ($token) {
|
||||
return '"'.$token.'"';
|
||||
})->join(',');
|
||||
|
||||
$this->database->query('SELECT * FROM users WHERE auth_token IN ('.$authTokenString.')')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$users = collect($result->rows)->map(function ($user) {
|
||||
return $this->getUserDetails($user);
|
||||
})->toArray();
|
||||
|
||||
$deferred->resolve($users);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
use GuzzleHttp\Psr7\Message;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
|
||||
function respond_json($responseData, int $statusCode = 200)
|
||||
{
|
||||
return str(new Response(
|
||||
return Message::toString(new Response(
|
||||
$statusCode,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode($responseData, JSON_INVALID_UTF8_IGNORE)
|
||||
@@ -14,7 +14,7 @@ function respond_json($responseData, int $statusCode = 200)
|
||||
|
||||
function respond_html(string $html, int $statusCode = 200)
|
||||
{
|
||||
return str(new Response(
|
||||
return Message::toString(new Response(
|
||||
$statusCode,
|
||||
['Content-Type' => 'text/html'],
|
||||
$html
|
||||
|
||||
2
builds/.gitignore
vendored
2
builds/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
!.gitignore
|
||||
*
|
||||
BIN
builds/expose
BIN
builds/expose
Binary file not shown.
@@ -1,51 +1,56 @@
|
||||
{
|
||||
"name": "beyondcode/expose",
|
||||
"name": "bitinflow/expose",
|
||||
"type": "project",
|
||||
"description": "Expose",
|
||||
"description": "Create public URLs for local sites through any firewall and VPN.",
|
||||
"keywords": [
|
||||
"expose",
|
||||
"tunnel",
|
||||
"ngrok"
|
||||
],
|
||||
"homepage": "https://sharedwithexpose.com",
|
||||
"homepage": "https://bitinflow.dev",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "René Preuß",
|
||||
"email": "rene@bitinflow.com"
|
||||
},
|
||||
{
|
||||
"name": "Marcel Pociot",
|
||||
"email": "marcel@beyondco.de"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.3.0",
|
||||
"php": "^8.0",
|
||||
"ext-json": "*",
|
||||
"padraic/phar-updater": "^1.0.6"
|
||||
"laravel-zero/phar-updater": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"cboden/ratchet": "^0.4.2",
|
||||
"clue/block-react": "^1.3",
|
||||
"clue/buzz-react": "^2.7",
|
||||
"cboden/ratchet": "^0.4.3",
|
||||
"clue/block-react": "^1.4",
|
||||
"clue/buzz-react": "^2.9",
|
||||
"clue/reactphp-sqlite": "dev-modular-worker-for-phar-support",
|
||||
"guzzlehttp/guzzle": "^6.5",
|
||||
"guzzlehttp/psr7": "dev-master as 1.6.1",
|
||||
"illuminate/http": "5.8.* || ^6.0 || ^7.0",
|
||||
"illuminate/pipeline": "^7.6",
|
||||
"illuminate/validation": "^7.7",
|
||||
"laminas/laminas-http": "^2.11",
|
||||
"laravel-zero/framework": "^7.0",
|
||||
"mockery/mockery": "^1.3",
|
||||
"namshi/cuzzle": "^2.0",
|
||||
"nikic/php-parser": "^4.4",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"phpunit/phpunit": "^8.5",
|
||||
"ratchet/pawl": "^0.3.4",
|
||||
"react/http": "^0.8.6",
|
||||
"react/socket": "dev-master as 1.1",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"guzzlehttp/psr7": "^1.7",
|
||||
"illuminate/log": "^8.0",
|
||||
"illuminate/http": "5.8.* || ^6.0 || ^7.0 || ^8.0",
|
||||
"illuminate/pipeline": "^7.6 || ^8.0",
|
||||
"illuminate/validation": "^7.7 || ^8.0",
|
||||
"laminas/laminas-http": "^2.13",
|
||||
"laravel-zero/framework": "^8.2",
|
||||
"mockery/mockery": "^1.4.2",
|
||||
"octoper/cuzzle": "^3.1",
|
||||
"nikic/php-parser": "^v4.10",
|
||||
"nyholm/psr7": "^1.3",
|
||||
"phpunit/phpunit": "^9.4.3",
|
||||
"ratchet/pawl": "^0.3.5",
|
||||
"react/http": "^1.1.0",
|
||||
"react/socket": "^1.6",
|
||||
"react/stream": "^1.1.1",
|
||||
"riverline/multipart-parser": "^2.0",
|
||||
"symfony/expression-language": "^5.0",
|
||||
"symfony/http-kernel": "^4.0 || ^5.0",
|
||||
"symfony/expression-language": "^5.2",
|
||||
"symfony/http-kernel": "^4.0 || ^5.2",
|
||||
"symfony/psr-http-message-bridge": "^2.0",
|
||||
"twig/twig": "^3.0"
|
||||
"twig/twig": "^3.1"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
||||
8460
composer.lock
generated
8460
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => '1.3.0',
|
||||
'version' => '2.2.2',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -59,4 +59,6 @@ return [
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
],
|
||||
|
||||
'locale' => 'en',
|
||||
|
||||
];
|
||||
|
||||
@@ -4,30 +4,58 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Host
|
||||
| Servers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The expose server to connect to. By default, expose is using the free
|
||||
| sharedwithexpose.com server, offered by Beyond Code. You will need a free
|
||||
| Beyond Code account in order to authenticate with the server.
|
||||
| Feel free to host your own server and change this value.
|
||||
| The available Expose servers that your client can connect to.
|
||||
| When sharing sites or TCP ports, you can specify the server
|
||||
| that should be used using the `--server=` option.
|
||||
|
|
||||
*/
|
||||
'host' => 'sharedwithexpose.com',
|
||||
'servers' => [
|
||||
'free' => [
|
||||
'host' => 'bitinflow.dev',
|
||||
'port' => 443,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Port
|
||||
| Server Endpoint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The port that expose will try to connect to. If you want to bypass
|
||||
| firewalls and have proper SSL encrypted tunnels, make sure to use
|
||||
| port 443 and use a reverse proxy for Expose.
|
||||
| When you specify a server that does not exist in above static array,
|
||||
| Expose will perform a GET request to this URL and tries to retrieve
|
||||
| a JSON payload that looks like the configurations servers array.
|
||||
|
|
||||
| The free default server is already running on port 443.
|
||||
| Expose then tries to load the configuration for the given server
|
||||
| if available.
|
||||
|
|
||||
*/
|
||||
'port' => 443,
|
||||
'server_endpoint' => 'https://bitinflow.dev/api/servers',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Server
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The default server from the servers array,
|
||||
| or the servers endpoint above.
|
||||
|
|
||||
*/
|
||||
'default_server' => 'free',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DNS
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The DNS server to use when resolving the shared URLs.
|
||||
| When Expose is running from within Docker containers, you should set this to
|
||||
| `true` to fall-back to the system default DNS servers.
|
||||
|
|
||||
*/
|
||||
'dns' => '127.0.0.1',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -43,6 +71,20 @@ return [
|
||||
*/
|
||||
'auth_token' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The custom domain to use when sharing sites with Expose.
|
||||
| You can register your own custom domain using Expose Pro
|
||||
| Learn more at: https://expose.dev/get-pro
|
||||
|
|
||||
| > expose default-domain YOUR-CUSTOM-WHITELABEL-DOMAIN
|
||||
|
|
||||
*/
|
||||
'default_domain' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default TLD
|
||||
@@ -55,6 +97,18 @@ return [
|
||||
*/
|
||||
'default_tld' => 'test',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default HTTPS
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Whether to use HTTPS as a default when sharing your local sites. Expose
|
||||
| will try to look up the protocol if you are using Laravel Valet
|
||||
| automatically. Otherwise you can specify it here manually.
|
||||
|
|
||||
*/
|
||||
'default_https' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Logged Requests
|
||||
@@ -151,6 +205,37 @@ return [
|
||||
*/
|
||||
'validate_auth_tokens' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| TCP Port Sharing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Control if you want to allow users to share TCP ports with your Expose
|
||||
| server. You can add fine-grained control per authentication token,
|
||||
| but if you want to disable TCP port sharing in general, set this
|
||||
| value to false.
|
||||
|
|
||||
*/
|
||||
'allow_tcp_port_sharing' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| TCP Port Range
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Expose allows you to also share TCP ports, for example when sharing your
|
||||
| local SSH server with the public. This setting allows you to define the
|
||||
| port range that Expose will use to assign new ports to the users.
|
||||
|
|
||||
| Note: Do not use port ranges below 1024, as it might require root
|
||||
| privileges to assign these ports.
|
||||
|
|
||||
*/
|
||||
'tcp_port_range' => [
|
||||
'from' => 50000,
|
||||
'to' => 60000,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum connection length
|
||||
@@ -164,6 +249,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
|
||||
@@ -176,6 +276,17 @@ return [
|
||||
*/
|
||||
'subdomain' => 'expose',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reserved Subdomain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify any subdomains that you don't want to be able to register
|
||||
| on your expose server.
|
||||
|
|
||||
*/
|
||||
'reserved_subdomains' => [],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Subdomain Generator
|
||||
@@ -188,6 +299,25 @@ return [
|
||||
*/
|
||||
'subdomain_generator' => \App\Server\SubdomainGenerator\RandomSubdomainGenerator::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Connection Callback
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is a callback method that will be called when a new connection is
|
||||
| established.
|
||||
| The \App\Client\Callbacks\WebHookConnectionCallback::class is included out of the box.
|
||||
|
|
||||
*/
|
||||
'connection_callback' => null,
|
||||
|
||||
'connection_callbacks' => [
|
||||
'webhook' => [
|
||||
'url' => null,
|
||||
'secret' => null,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Users
|
||||
@@ -214,6 +344,10 @@ return [
|
||||
*/
|
||||
'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class,
|
||||
|
||||
'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class,
|
||||
|
||||
'logger_repository' => \App\Server\LoggerRepository\NullLogger::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Messages
|
||||
@@ -225,11 +359,35 @@ return [
|
||||
|
|
||||
*/
|
||||
'messages' => [
|
||||
'resolve_connection_message' => function ($connectionInfo, $user) {
|
||||
return config('expose.admin.messages.message_of_the_day');
|
||||
},
|
||||
|
||||
'message_of_the_day' => 'Thank you for using expose.',
|
||||
|
||||
'invalid_auth_token' => 'Authentication failed. Please check your authentication token and try again.',
|
||||
|
||||
'subdomain_taken' => 'The chosen subdomain :subdomain is already taken. Please choose a different subdomain.',
|
||||
|
||||
'subdomain_reserved' => 'The chosen subdomain :subdomain is not available. Please choose a different subdomain.',
|
||||
|
||||
'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro. Assigning a random subdomain instead.',
|
||||
|
||||
'custom_domain_unauthorized' => 'You are not allowed to use this custom domain.',
|
||||
|
||||
'tcp_port_sharing_unauthorized' => 'You are not allowed to share TCP ports. Please upgrade to Expose Pro.',
|
||||
|
||||
'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.',
|
||||
|
||||
'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,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
113
config/logging.php
Normal file
113
config/logging.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that gets used when writing
|
||||
| messages to the logs. The name specified in this option should match
|
||||
| one of the channels defined in the "channels" configuration array.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Out of
|
||||
| the box, Laravel uses the Monolog PHP logging library. This gives
|
||||
| you a variety of powerful log handlers / formatters to utilize.
|
||||
|
|
||||
| Available Drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog",
|
||||
| "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['stderr'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => 'Laravel Log',
|
||||
'emoji' => ':boom:',
|
||||
'level' => 'critical',
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => 'debug',
|
||||
'handler' => SyslogUdpHandler::class,
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
],
|
||||
|
||||
'deprecations' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD can_specify_subdomains BOOLEAN DEFAULT 1;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD can_share_tcp_ports BOOLEAN DEFAULT 1;
|
||||
7
database/migrations/04_create_subdomains_table.sql
Normal file
7
database/migrations/04_create_subdomains_table.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS subdomains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
subdomain STRING NOT NULL,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD max_connections INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -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
|
||||
)
|
||||
10
database/migrations/08_add_domain_to_subdomains_table.sql
Normal file
10
database/migrations/08_add_domain_to_subdomains_table.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE users ADD can_specify_domains BOOLEAN DEFAULT 1;
|
||||
ALTER TABLE subdomains ADD domain STRING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
domain STRING NOT NULL,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user