mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 21:45:55 +00:00
Compare commits
210 Commits
custom-hos
...
master
| 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 | ||
|
|
548c29772a | ||
|
|
844a3cd15a | ||
|
|
e773dfa689 | ||
|
|
c56f05c030 | ||
|
|
ce945e1326 |
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,5 +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
|
||||
ENTRYPOINT ["/src/expose"]
|
||||
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,18 +44,23 @@ 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, config('expose.auth_token'));
|
||||
$this->connectToServerAndShareTcp($port, $this->configuration->auth());
|
||||
}
|
||||
|
||||
protected function prepareSharedUrl(string $sharedUrl): string
|
||||
@@ -72,28 +81,30 @@ 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(function () use ($sharedUrl, $subdomain, $authToken) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, $authToken);
|
||||
$this->retryConnectionOrExit(function () use ($sharedUrl, $subdomain, $serverHost, $authToken) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, $serverHost, $authToken);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,19 +118,22 @@ class Client
|
||||
|
||||
$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);
|
||||
});
|
||||
@@ -146,8 +160,9 @@ class Client
|
||||
$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}", [], [
|
||||
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) {
|
||||
@@ -171,9 +186,9 @@ class Client
|
||||
$host = $this->configuration->host();
|
||||
|
||||
$this->logger->info($data->message);
|
||||
$this->logger->info("Local-Port:\t\t{$port}");
|
||||
$this->logger->info("Shared-Port:\t\t{$data->shared_port}");
|
||||
$this->logger->info("Expose-URL:\t\ttcp://{$host}:{$data->shared_port}.");
|
||||
$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);
|
||||
@@ -201,6 +216,10 @@ class Client
|
||||
$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);
|
||||
});
|
||||
@@ -231,7 +250,9 @@ class Client
|
||||
$deferred->reject();
|
||||
|
||||
$this->loop->futureTick(function () {
|
||||
exit(1);
|
||||
if ($this->shouldExit) {
|
||||
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,11 +35,26 @@ 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);
|
||||
@@ -40,7 +63,7 @@ class Configuration
|
||||
public function getUrl(string $subdomain): string
|
||||
{
|
||||
$httpProtocol = $this->port() === 443 ? 'https' : 'http';
|
||||
$host = $this->host();
|
||||
$host = $this->serverHost();
|
||||
|
||||
if ($httpProtocol !== 'https') {
|
||||
$host .= ":{$this->port()}";
|
||||
|
||||
@@ -57,13 +57,14 @@ class ControlConnection
|
||||
$this->proxyManager->createTcpProxy($this->clientId, $data);
|
||||
}
|
||||
|
||||
public function authenticate(string $sharedHost, string $subdomain)
|
||||
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,
|
||||
],
|
||||
]));
|
||||
|
||||
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,9 @@ 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;
|
||||
}
|
||||
@@ -116,6 +130,15 @@ class Factory
|
||||
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;
|
||||
}
|
||||
|
||||
protected function addRoutes()
|
||||
{
|
||||
$this->router->get('/', DashboardController::class);
|
||||
@@ -127,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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ 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;
|
||||
@@ -74,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,
|
||||
@@ -85,17 +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)) {
|
||||
if (! isset($response->buffer)) {
|
||||
$response = $this->rewriteResponseHeaders($response);
|
||||
|
||||
$response->buffer = str($response);
|
||||
$response->buffer = Message::toString($response);
|
||||
}
|
||||
|
||||
$this->sendChunkToServer($response->buffer, $proxyConnection);
|
||||
@@ -103,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;
|
||||
@@ -139,13 +141,13 @@ class HttpClient
|
||||
|
||||
protected function rewriteResponseHeaders(ResponseInterface $response)
|
||||
{
|
||||
if (!$response->hasHeader('Location')) {
|
||||
if (! $response->hasHeader('Location')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$location = $response->getHeaderLine('Location');
|
||||
|
||||
if (!strstr($location, $this->connectionData->host)) {
|
||||
if (! strstr($location, $this->connectionData->host)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,23 @@
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Factory;
|
||||
use App\Logger\CliRequestLogger;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
class SharePortCommand extends Command
|
||||
class SharePortCommand extends ServerAwareCommand
|
||||
{
|
||||
protected $signature = 'share-port {port} {--auth=}';
|
||||
|
||||
protected $description = 'Share a local port 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', '');
|
||||
|
||||
(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)
|
||||
->createClient()
|
||||
->sharePort($this->argument('port'))
|
||||
->createHttpServer()
|
||||
|
||||
@@ -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,7 @@ 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;
|
||||
|
||||
@@ -20,7 +20,7 @@ interface ConnectionManager
|
||||
|
||||
public function removeControlConnection($connection);
|
||||
|
||||
public function findControlConnectionForSubdomain($subdomain): ?ControlConnection;
|
||||
public function findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost): ?ControlConnection;
|
||||
|
||||
public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
|
||||
|
||||
@@ -29,4 +29,8 @@ interface ConnectionManager
|
||||
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;
|
||||
}
|
||||
@@ -12,6 +12,10 @@ interface SubdomainRepository
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
@@ -48,6 +50,7 @@ class LoggedRequest implements \JsonSerializable
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$data = [
|
||||
@@ -171,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) {
|
||||
@@ -211,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +3,8 @@
|
||||
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;
|
||||
@@ -24,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)
|
||||
@@ -43,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();
|
||||
|
||||
@@ -54,14 +64,30 @@ 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();
|
||||
@@ -78,6 +104,8 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
|
||||
$this->connections[] = $storedConnection;
|
||||
|
||||
$this->statisticsCollector->portShared($this->getAuthTokenFromConnection($connection));
|
||||
|
||||
return $storedConnection;
|
||||
}
|
||||
|
||||
@@ -143,10 +171,10 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,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;
|
||||
|
||||
@@ -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)
|
||||
@@ -61,7 +66,10 @@ class ControlConnection
|
||||
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,
|
||||
|
||||
@@ -76,6 +76,7 @@ class TcpControlConnection extends ControlConnection
|
||||
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,
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,27 @@
|
||||
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;
|
||||
@@ -23,12 +32,17 @@ 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;
|
||||
@@ -128,16 +142,28 @@ class Factory
|
||||
$this->router->get('/sites', ListSitesController::class, $adminCondition);
|
||||
$this->router->get('/tcp', ListTcpConnectionsController::class, $adminCondition);
|
||||
|
||||
$this->router->get('/api/statistics', GetStatisticsController::class, $adminCondition);
|
||||
$this->router->get('/api/settings', GetSettingsController::class, $adminCondition);
|
||||
$this->router->post('/api/settings', StoreSettingsController::class, $adminCondition);
|
||||
|
||||
$this->router->get('/api/users', GetUsersController::class, $adminCondition);
|
||||
$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->delete('/api/users/{id}', DeleteUsersController::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);
|
||||
}
|
||||
@@ -176,9 +202,12 @@ class Factory
|
||||
$this->bindConfiguration()
|
||||
->bindSubdomainGenerator()
|
||||
->bindUserRepository()
|
||||
->bindLoggerRepository()
|
||||
->bindSubdomainRepository()
|
||||
->bindDomainRepository()
|
||||
->bindDatabase()
|
||||
->ensureDatabaseIsInitialized()
|
||||
->registerStatisticsCollector()
|
||||
->bindConnectionManager()
|
||||
->addAdminRoutes();
|
||||
|
||||
@@ -216,7 +245,25 @@ class Factory
|
||||
protected function bindSubdomainRepository()
|
||||
{
|
||||
app()->singleton(SubdomainRepository::class, function () {
|
||||
return app(config('expose.admin.subdomain_repository'));
|
||||
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;
|
||||
@@ -248,7 +295,8 @@ class Factory
|
||||
->files()
|
||||
->ignoreDotFiles(true)
|
||||
->in(database_path('migrations'))
|
||||
->name('*.sql');
|
||||
->name('*.sql')
|
||||
->sortByName();
|
||||
|
||||
/** @var SplFileInfo $migration */
|
||||
foreach ($migrations as $migration) {
|
||||
@@ -264,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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
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,6 +3,7 @@
|
||||
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;
|
||||
@@ -10,31 +11,55 @@ 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())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === ControlConnection::class;
|
||||
})
|
||||
->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
$authTokens = [];
|
||||
|
||||
return $site;
|
||||
})->values(),
|
||||
])
|
||||
);
|
||||
$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;
|
||||
})->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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
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;
|
||||
@@ -10,32 +11,55 @@ use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetTcpConnectionsController 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([
|
||||
'tcp_connections' => collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === TcpControlConnection::class;
|
||||
})
|
||||
->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
$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(),
|
||||
])
|
||||
);
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,25 @@ class GetUserDetailsController extends AdminController
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUserById($request->get('id'))
|
||||
->then(function ($user) use ($httpConnection, $request) {
|
||||
$this->subdomainRepository->getSubdomainsByUserId($request->get('id'))
|
||||
$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([], 404)
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
$this->subdomainRepository->getSubdomainsByUserId($user['id'])
|
||||
->then(function ($subdomains) use ($httpConnection, $user) {
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
@@ -39,6 +54,6 @@ class GetUserDetailsController extends AdminController
|
||||
|
||||
$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])
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Configuration;
|
||||
use App\Server\Connections\ControlConnection;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
@@ -26,17 +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())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === ControlConnection::class;
|
||||
})
|
||||
->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
|
||||
return $site;
|
||||
})
|
||||
->values(),
|
||||
]);
|
||||
|
||||
$httpConnection->send(
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
@@ -26,17 +25,6 @@ class ListTcpConnectionsController extends AdminController
|
||||
$sites = $this->getView($httpConnection, 'server.tcp.index', [
|
||||
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
|
||||
'configuration' => $this->configuration,
|
||||
'connections' => collect($this->connectionManager->getConnections())
|
||||
->filter(function ($connection) {
|
||||
return get_class($connection) === TcpControlConnection::class;
|
||||
})
|
||||
->map(function ($connection, $connectionId) {
|
||||
$connection = $connection->toArray();
|
||||
$connection['id'] = $connectionId;
|
||||
|
||||
return $connection;
|
||||
})
|
||||
->values(),
|
||||
]);
|
||||
|
||||
$httpConnection->send(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -18,10 +19,14 @@ class StoreSubdomainController extends AdminController
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
/** @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)
|
||||
@@ -39,7 +44,8 @@ class StoreSubdomainController extends AdminController
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userRepository->getUserByToken($request->get('auth_token', ''))
|
||||
$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));
|
||||
@@ -55,20 +61,22 @@ class StoreSubdomainController extends AdminController
|
||||
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) {
|
||||
if (is_null($subdomain)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The subdomain is already taken.'], 422));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
$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,9 +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,14 +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
|
||||
@@ -24,11 +28,19 @@ class ControlMessageController implements MessageComponentInterface
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +97,42 @@ 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 ($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']));
|
||||
}
|
||||
|
||||
if ($maximumConnectionCount > 0 && $connectionCount + 1 > $maximumConnectionCount) {
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticationFailed',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.maximum_connection_count'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
reject(null);
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->then(function ($user) use ($connection, $data) {
|
||||
if ($data->type === 'http') {
|
||||
$this->handleHttpConnection($connection, $data, $user);
|
||||
@@ -103,28 +150,57 @@ 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->hasValidSubdomain($connection, $data->subdomain, $user)->then(function ($subdomain) use ($data, $connection) {
|
||||
if ($subdomain === false) {
|
||||
return;
|
||||
}
|
||||
$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;
|
||||
$data->subdomain = $subdomain;
|
||||
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection);
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $data->server_host, $connection);
|
||||
|
||||
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
|
||||
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
|
||||
|
||||
$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 $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)
|
||||
@@ -150,7 +226,8 @@ class ControlMessageController implements MessageComponentInterface
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.message_of_the_day'),
|
||||
'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,
|
||||
@@ -205,21 +282,60 @@ 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, ?array $user): PromiseInterface
|
||||
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' => 'info',
|
||||
'event' => 'error',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL,
|
||||
],
|
||||
@@ -232,10 +348,14 @@ class ControlMessageController implements MessageComponentInterface
|
||||
* Check if the given subdomain is reserved for a different user.
|
||||
*/
|
||||
if (! is_null($subdomain)) {
|
||||
return $this->subdomainRepository->getSubdomainByName($subdomain)
|
||||
->then(function ($foundSubdomain) use ($connection, $subdomain, $user) {
|
||||
if (! is_null($foundSubdomain) && ! is_null($user) && $foundSubdomain['user_id'] !== $user['id']) {
|
||||
$message = config('expose.admin.messages.subdomain_reserved');
|
||||
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'];
|
||||
});
|
||||
|
||||
if (count($foundSubdomains) > 0 && ! is_null($user) && is_null($ownSubdomain)) {
|
||||
$message = config('expose.admin.messages.subdomain_reserved', '');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
|
||||
$connection->send(json_encode([
|
||||
@@ -249,9 +369,9 @@ class ControlMessageController implements MessageComponentInterface
|
||||
return \React\Promise\resolve(false);
|
||||
}
|
||||
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost);
|
||||
|
||||
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) {
|
||||
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);
|
||||
|
||||
@@ -275,11 +395,23 @@ class ControlMessageController implements MessageComponentInterface
|
||||
|
||||
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.custom_subdomain_unauthorized'),
|
||||
'message' => config('expose.admin.messages.tcp_port_sharing_unauthorized'),
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,38 @@ class DatabaseSubdomainRepository implements SubdomainRepository
|
||||
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();
|
||||
@@ -76,23 +108,14 @@ class DatabaseSubdomainRepository implements SubdomainRepository
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->getSubdomainByName($data['subdomain'])
|
||||
->then(function ($registeredSubdomain) use ($data, $deferred) {
|
||||
if (! is_null($registeredSubdomain)) {
|
||||
$deferred->resolve(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->query("
|
||||
INSERT INTO subdomains (user_id, subdomain, created_at)
|
||||
VALUES (:user_id, :subdomain, DATETIME('now'))
|
||||
", $data)
|
||||
$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) {
|
||||
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,7 +142,7 @@ class DatabaseSubdomainRepository implements SubdomainRepository
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('DELETE FROM subdomains WHERE id = :id AND user_id = :user_id', [
|
||||
$this->database->query('DELETE FROM subdomains WHERE (id = :id OR subdomain = :id) AND user_id = :user_id', [
|
||||
'id' => $subdomainId,
|
||||
'user_id' => $userId,
|
||||
])
|
||||
|
||||
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();
|
||||
@@ -96,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();
|
||||
@@ -103,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();
|
||||
@@ -113,15 +150,38 @@ class DatabaseUserRepository implements UserRepository
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query("
|
||||
INSERT INTO users (name, auth_token, can_specify_subdomains, can_share_tcp_ports, created_at)
|
||||
VALUES (:name, :auth_token, :can_specify_subdomains, :can_share_tcp_ports, 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();
|
||||
@@ -131,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,50 +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",
|
||||
"ext-json": "*"
|
||||
"php": "^8.0",
|
||||
"ext-json": "*",
|
||||
"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",
|
||||
"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,
|
||||
|
||||
7893
composer.lock
generated
7893
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,19 @@ 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
|
||||
@@ -182,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
|
||||
@@ -194,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
|
||||
@@ -206,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
|
||||
@@ -234,6 +346,8 @@ return [
|
||||
|
||||
'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class,
|
||||
|
||||
'logger_repository' => \App\Server\LoggerRepository\NullLogger::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Messages
|
||||
@@ -245,15 +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 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
|
||||
)
|
||||
6
database/migrations/09_create_logs_table.sql
Normal file
6
database/migrations/09_create_logs_table.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
subdomain STRING NOT NULL,
|
||||
created_at DATETIME
|
||||
)
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
expose:
|
||||
image: beyondcodegmbh/expose-server:latest
|
||||
ports:
|
||||
- 127.0.0.1:8080:${PORT}
|
||||
- 8080:${PORT}
|
||||
environment:
|
||||
port: ${PORT}
|
||||
domain: ${DOMAIN}
|
||||
|
||||
9
docker-entrypoint.sh
Normal file
9
docker-entrypoint.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
sed -i "s|username|${username}|g" ${exposeConfigPath} && sed -i "s|password|${password}|g" ${exposeConfigPath}
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
exec /src/expose serve ${domain} --port ${port} --validateAuthTokens
|
||||
else
|
||||
exec /src/expose "$@"
|
||||
fi
|
||||
@@ -16,7 +16,7 @@ The result looks like this:
|
||||
```json
|
||||
{
|
||||
"configuration":{
|
||||
"hostname": "sharedwithexpose.com",
|
||||
"hostname": "bitinflow.dev",
|
||||
"port": 8080,
|
||||
"database": "/home/forge/expose/database/expose.db",
|
||||
"validate_auth_tokens": false,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Basic Authentication
|
||||
order: 2
|
||||
order: 4
|
||||
---
|
||||
|
||||
# Sharing sites with basic authentication
|
||||
|
||||
Expose allows you to share your local sites with custom basic authentication credentials.
|
||||
|
||||
This can be useful, if you have a static subdomain that you share with someone else, for example a client, and you want to provide some additional security to it. Before someone can access your shared site, they need to provide the correct credentials.
|
||||
This is useful, if you have a static subdomain that you share with someone else, for example a client, and you want to provide some additional security to it. Before someone can access your shared site, they need to provide the correct credentials.
|
||||
|
||||
> **Warning**: You can not add basic authentication to a website that already uses basic authentication.
|
||||
|
||||
@@ -29,4 +29,4 @@ You can also use the basic authentication parameter in addition to a custom subd
|
||||
|
||||
```bash
|
||||
expose share my-site.test --subdomain=site --auth="admin:secret"
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Configuration
|
||||
order: 3
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Configuration
|
||||
@@ -17,6 +17,12 @@ The configuration file will be written to your home directory inside a `.expose`
|
||||
|
||||
`~/.expose/config.php`
|
||||
|
||||
You can also provide a custom location of the config file by providing the full path as a server variable.
|
||||
|
||||
```bash
|
||||
EXPOSE_CONFIG_FILE="~/my-custom-config.php" expose share
|
||||
```
|
||||
|
||||
And the default content of the configuration file is this:
|
||||
|
||||
```php
|
||||
@@ -28,12 +34,12 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 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
|
||||
| bitinflow.dev 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.
|
||||
|
|
||||
*/
|
||||
'host' => 'sharedwithexpose.com',
|
||||
'host' => 'bitinflow.dev',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
---
|
||||
title: Dashboard
|
||||
order: 5
|
||||
title: Local Dashboard
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Dashboard
|
||||
|
||||
Once you share a local site, expose will show you all incoming HTTP requests along with their status code and duration in your terminal:
|
||||
Once you share a local site, Expose shows you all incoming HTTP requests along with their status code and duration in your terminal:
|
||||
|
||||

|
||||
|
||||
While this is great to get a quick look of the incoming requests, you sometimes need more information than this.
|
||||
While this is great to get a quick look of the incoming requests, you often need more information than this.
|
||||
|
||||
Because of that, expose is also exposing a web based dashboard on port 4040.
|
||||
Because of that, Expose is also exposing a web based dashboard on port 4040.
|
||||
|
||||
Once you start sharing a site, expose will show you a QR code that you can scan with your mobile device, to easily browse your shared sites on your phone or tablet.
|
||||
Once you start sharing a site, Expose shows you a QR code that you can scan with your mobile device, to easily browse your shared sites on your phone or tablet.
|
||||
|
||||

|
||||
|
||||
Once a request comes in, you can see all incoming HTTP requests as they hit your local site in realtime.
|
||||
When you click on a specific request, you can see detailed information about the request and response.
|
||||
When a request comes in, you can see all incoming HTTP requests as they hit your local site in realtime.
|
||||
You can click on a specific request and see detailed information about the request and response. Expose provides several tools for developers to make webhook testing easier – the most powerful one is that you can replay requests without firing the webhook again. So if your previous process required to create multiple test orders to see how the paylods of your payment provider look like, Expose makes this a breeze by allowing you to replay these requests without creating more orders.
|
||||
|
||||

|
||||

|
||||
|
||||
44
docs/client/global-server-infrastructure.md
Normal file
44
docs/client/global-server-infrastructure.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Global Server Infrastructure
|
||||
order: 4
|
||||
---
|
||||
|
||||
# Global Server Infrastructure ::pro
|
||||
|
||||
[Expose Pro](/get-pro) allows you to choose between multiple Expose servers around the world, so that you can use an endpoint closest to you.
|
||||
|
||||
To get a list of all the available Expose servers, you can run `expose servers`
|
||||
|
||||
```
|
||||
$ expose servers
|
||||
|
||||
+------+---------------------------+------+
|
||||
| Key | Region | Type |
|
||||
+------+---------------------------+------+
|
||||
| eu-1 | EU (Frankfurt) | Pro |
|
||||
| us-1 | US (New York) | Pro |
|
||||
| us-2 | US (San Francisco) | Pro |
|
||||
| ap-1 | Asia Pacific (Singapore) | Pro |
|
||||
| in-1 | India (Bangalore) | Pro |
|
||||
| sa-1 | South America (São Paulo) | Pro |
|
||||
| au-1 | Australia (Sydney) | Pro |
|
||||
+------+---------------------------+------+
|
||||
```
|
||||
|
||||
## Changing servers
|
||||
|
||||
When you share a local URL, or a local TCP port, you can specify the Expose server region, using the `--server` command line option. Pass the server key as the option to connect to this specific server.
|
||||
|
||||
```bash
|
||||
expose share my-local-site.test --server=eu-1
|
||||
```
|
||||
|
||||
## Setting a default server
|
||||
|
||||
Most of the time you will want to always use the server location that is closest to you for all of your Expose commands. You can define the default server that Expose should use, by calling the `expose default-server` command:
|
||||
|
||||
```bash
|
||||
expose default-server us-2
|
||||
```
|
||||
|
||||
Now the next time that you will share a local URL or port, Expose is automatically going to connect to the `us-2` server for your.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user