mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 21:45:55 +00:00
Compare commits
61 Commits
analysis-l
...
associate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a83349e6b9 | ||
|
|
9363e97d81 | ||
|
|
47b2350631 | ||
|
|
13f184a955 | ||
|
|
55a456d5e1 | ||
|
|
f9084c3c31 | ||
|
|
730b8457a6 | ||
|
|
188e1efe57 | ||
|
|
eaf04a8eae | ||
|
|
41e6e674e0 | ||
|
|
3d76b49fea | ||
|
|
1d5169af07 | ||
|
|
0945b1e66b | ||
|
|
a2bdf518ab | ||
|
|
0216948d18 | ||
|
|
9e31b020b6 | ||
|
|
bf0025979e | ||
|
|
dda3cbbae5 | ||
|
|
6a07859078 | ||
|
|
8db13e70af | ||
|
|
dfe889692b | ||
|
|
e5b2aada2f | ||
|
|
076da2c0de | ||
|
|
6410c7eb5e | ||
|
|
0d9413dfdf | ||
|
|
87a4115c14 | ||
|
|
096a2b2a70 | ||
|
|
6d6306b3b2 | ||
|
|
611a4c617c | ||
|
|
54bd95c66c | ||
|
|
3cb254e1f5 | ||
|
|
e960ffb825 | ||
|
|
459135f286 | ||
|
|
0efb42f989 | ||
|
|
6f04a0dfb6 | ||
|
|
91f169460e | ||
|
|
f8a6b45af7 | ||
|
|
b48dba1413 | ||
|
|
8bcc7613d9 | ||
|
|
9158887a60 | ||
|
|
b3f2edd18c | ||
|
|
b4379ddf6d | ||
|
|
70a9666f37 | ||
|
|
0b9f860138 | ||
|
|
dae1851e1d | ||
|
|
70d275bb1c | ||
|
|
8b8c6c8e2e | ||
|
|
c5b89e1179 | ||
|
|
18d67abc3f | ||
|
|
38efb0b879 | ||
|
|
04c881a875 | ||
|
|
d98eabe36e | ||
|
|
262a1eac4a | ||
|
|
979bacb928 | ||
|
|
732e0aeb3e | ||
|
|
2c0c544eeb | ||
|
|
528d5d74e0 | ||
|
|
68200aedc4 | ||
|
|
8628a7e1b6 | ||
|
|
5df98c4b91 | ||
|
|
78fbef90cd |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
tests
|
||||
.git
|
||||
4
.env-example
Normal file
4
.env-example
Normal file
@@ -0,0 +1,4 @@
|
||||
PORT=8080
|
||||
DOMAIN=example.com
|
||||
ADMIN_USERNAME=username
|
||||
ADMIN_PASSWORD=password
|
||||
17
.gitattributes
vendored
17
.gitattributes
vendored
@@ -1,7 +1,14 @@
|
||||
* text=auto
|
||||
/.github export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
/.github export-ignore
|
||||
/tests export-ignore
|
||||
/docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.styleci.yml export-ignore
|
||||
.scrutinizer.yml export-ignore
|
||||
BACKERS.md export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
BACKERS.md export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
nodemod.json export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
|
||||
19
.github/workflows/docker-publish.yml
vendored
Normal file
19
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Publish Docker image
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: beyondcodegmbh/expose-server
|
||||
tag_with_ref: true
|
||||
tags: latest
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@
|
||||
expose.php
|
||||
database/expose.db
|
||||
.expose.php
|
||||
.env
|
||||
|
||||
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## 1.3.0 (2020-07-01)
|
||||
* Feature: Add pagination to admin user interface
|
||||
* Feature: Add request time to CLI output
|
||||
* Feature: Add `X-Forwarded-Host` header
|
||||
* Fix: Fix remaining time calculation
|
||||
* Fix: Don't use underscores for automatic subdomain generation
|
||||
|
||||
## 1.1.0 (2020-06-18)
|
||||
* Feature: Allow overriding the subdomain when using `expose` without specifying `expose share` explicitly
|
||||
* Show badges in the local dashboard for 3xx response statuses
|
||||
* Fix: Updated minimum PHP dependency
|
||||
* Fix: Added support for detecting the Windows user home path
|
||||
* Fix: Use minified VueJS versions
|
||||
* Various spelling fixes
|
||||
|
||||
## 1.0.1 (2020-06-17)
|
||||
* Fixes an issue when setting the auth token
|
||||
|
||||
## 1.0.0 (2020-06-17)
|
||||
* Initial release
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM php:7.4-cli
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y git libzip-dev zip
|
||||
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
# Get latest Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
COPY . /src
|
||||
WORKDIR /src
|
||||
|
||||
# install the dependencies
|
||||
RUN composer install -o --prefer-dist && chmod a+x expose
|
||||
|
||||
ENV port=8080
|
||||
ENV domain=localhost
|
||||
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
|
||||
@@ -84,7 +84,7 @@ class Client
|
||||
|
||||
$connection->authenticate($sharedUrl, $subdomain);
|
||||
|
||||
$clientConnection->on('close', function () use ($deferred, $sharedUrl, $subdomain, $authToken) {
|
||||
$clientConnection->on('close', function () use ($sharedUrl, $subdomain, $authToken) {
|
||||
$this->logger->error('Connection to server closed.');
|
||||
|
||||
$this->retryConnectionOrExit($sharedUrl, $subdomain, $authToken);
|
||||
@@ -108,10 +108,11 @@ class Client
|
||||
$this->loop->addPeriodicTimer(1, function () use ($data, $timeoutSection) {
|
||||
$this->timeConnected++;
|
||||
|
||||
$carbon = Carbon::createFromFormat('s', str_pad($data->length * 60 - $this->timeConnected, 2, 0, STR_PAD_LEFT));
|
||||
$secondsRemaining = $data->length * 60 - $this->timeConnected;
|
||||
$remaining = Carbon::now()->diff(Carbon::now()->addSeconds($secondsRemaining));
|
||||
|
||||
$timeoutSection->clear();
|
||||
$timeoutSection->writeln('Remaining time: '.$carbon->format('H:i:s'));
|
||||
$timeoutSection->writeln('Remaining time: '.$remaining->format('%H:%I:%S'));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->input->setArgument('host', basename(getcwd()).'.'.$this->detectTld());
|
||||
$subdomain = $this->detectName();
|
||||
$host = $this->prepareSharedHost($subdomain.'.'.$this->detectTld());
|
||||
|
||||
if (! $this->hasOption('subdomain')) {
|
||||
$subdomain = str_replace('.', '_', basename(getcwd()));
|
||||
$this->input->setArgument('host', $host);
|
||||
|
||||
if (! $this->option('subdomain')) {
|
||||
$this->input->setOption('subdomain', $subdomain);
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
|
||||
protected function detectTld(): string
|
||||
{
|
||||
$valetConfigFile = $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'].DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'config.json';
|
||||
$valetConfigFile = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'config.json';
|
||||
|
||||
if (file_exists($valetConfigFile)) {
|
||||
$valetConfig = json_decode(file_get_contents($valetConfigFile));
|
||||
@@ -30,4 +32,41 @@ class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
|
||||
return config('expose.default_tld', 'test');
|
||||
}
|
||||
|
||||
protected function detectName(): string
|
||||
{
|
||||
$projectPath = getcwd();
|
||||
$valetSitesPath = ($_SERVER['HOME'] ?? $_SERVER['USERPROFILE']).DIRECTORY_SEPARATOR.'.config'.DIRECTORY_SEPARATOR.'valet'.DIRECTORY_SEPARATOR.'Sites';
|
||||
|
||||
if (is_dir($valetSitesPath)) {
|
||||
$site = collect(scandir($valetSitesPath))
|
||||
->skip(2)
|
||||
->map(function ($site) use ($valetSitesPath) {
|
||||
return $valetSitesPath.DIRECTORY_SEPARATOR.$site;
|
||||
})->mapWithKeys(function ($site) {
|
||||
return [$site => readlink($site)];
|
||||
})->filter(function ($sourcePath) use ($projectPath) {
|
||||
return $sourcePath === $projectPath;
|
||||
})
|
||||
->keys()
|
||||
->first();
|
||||
|
||||
if ($site) {
|
||||
$projectPath = $site;
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('.', '-', basename($projectPath));
|
||||
}
|
||||
|
||||
protected function prepareSharedHost($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 $host;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,6 @@ interface ConnectionManager
|
||||
public function findControlConnectionForClientId(string $clientId): ?ControlConnection;
|
||||
|
||||
public function getConnections(): array;
|
||||
|
||||
public function getConnectionsForAuthToken(string $authToken): array;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ interface UserRepository
|
||||
|
||||
public function getUserById($id): PromiseInterface;
|
||||
|
||||
public function paginateUsers(int $perPage, int $currentPage): PromiseInterface;
|
||||
|
||||
public function getUserByToken(string $authToken): PromiseInterface;
|
||||
|
||||
public function storeUser(array $data): PromiseInterface;
|
||||
|
||||
@@ -24,7 +24,7 @@ class CliRequestLogger extends Logger
|
||||
$this->section = $this->output->section();
|
||||
|
||||
$this->table = new Table($this->section);
|
||||
$this->table->setHeaders(['Method', 'URI', 'Response', 'Duration']);
|
||||
$this->table->setHeaders(['Method', 'URI', 'Response', 'Time', 'Duration']);
|
||||
|
||||
$this->requests = new Collection();
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class CliRequestLogger extends Logger
|
||||
$loggedRequest->getRequest()->getMethod(),
|
||||
$loggedRequest->getRequest()->getUri(),
|
||||
optional($loggedRequest->getResponse())->getStatusCode().' '.optional($loggedRequest->getResponse())->getReasonPhrase(),
|
||||
$loggedRequest->getStartTime()->toDateTimeString(),
|
||||
$loggedRequest->getDuration().'ms',
|
||||
];
|
||||
})->toArray());
|
||||
|
||||
@@ -296,6 +296,11 @@ class LoggedRequest implements \JsonSerializable
|
||||
})->get('x-expose-request-id', (string) Str::uuid());
|
||||
}
|
||||
|
||||
public function getStartTime()
|
||||
{
|
||||
return $this->startTime;
|
||||
}
|
||||
|
||||
public function getDuration()
|
||||
{
|
||||
return $this->startTime->diffInMilliseconds($this->stopTime, false);
|
||||
@@ -303,6 +308,12 @@ class LoggedRequest implements \JsonSerializable
|
||||
|
||||
protected function getRequestAsCurl(): string
|
||||
{
|
||||
$maxRequestLength = 256000;
|
||||
|
||||
if (strlen($this->rawRequest) > $maxRequestLength) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return (new CurlFormatter())->format(parse_request($this->rawRequest));
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Logger\CliRequestLogger;
|
||||
use App\Logger\RequestLogger;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laminas\Uri\Uri;
|
||||
use Laminas\Uri\UriFactory;
|
||||
use React\EventLoop\Factory as LoopFactory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
@@ -13,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
UriFactory::registerScheme('chrome-extension', Uri::class);
|
||||
}
|
||||
|
||||
public function register()
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Server\Connections;
|
||||
|
||||
use App\Contracts\ConnectionManager as ConnectionManagerContract;
|
||||
use App\Contracts\SubdomainGenerator;
|
||||
use App\Http\QueryParameters;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
@@ -46,7 +47,13 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
|
||||
$connection->client_id = $clientId;
|
||||
|
||||
$storedConnection = new ControlConnection($connection, $host, $subdomain ?? $this->subdomainGenerator->generateSubdomain(), $clientId);
|
||||
$storedConnection = new ControlConnection(
|
||||
$connection,
|
||||
$host,
|
||||
$subdomain ?? $this->subdomainGenerator->generateSubdomain(),
|
||||
$clientId,
|
||||
$this->getAuthTokenFromConnection($connection)
|
||||
);
|
||||
|
||||
$this->connections[] = $storedConnection;
|
||||
|
||||
@@ -99,4 +106,21 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
{
|
||||
return $this->connections;
|
||||
}
|
||||
|
||||
protected function getAuthTokenFromConnection(ConnectionInterface $connection): string
|
||||
{
|
||||
return QueryParameters::create($connection->httpRequest)->get('authToken');
|
||||
}
|
||||
|
||||
public function getConnectionsForAuthToken(string $authToken): array
|
||||
{
|
||||
return collect($this->connections)
|
||||
->filter(function ($connection) use ($authToken) {
|
||||
return $connection->authToken === $authToken;
|
||||
})
|
||||
->map(function ($connection) {
|
||||
return $connection->toArray();
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,19 @@ class ControlConnection
|
||||
/** @var ConnectionInterface */
|
||||
public $socket;
|
||||
public $host;
|
||||
public $authToken;
|
||||
public $subdomain;
|
||||
public $client_id;
|
||||
public $proxies = [];
|
||||
protected $shared_at;
|
||||
|
||||
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId)
|
||||
public function __construct(ConnectionInterface $socket, string $host, string $subdomain, string $clientId, string $authToken = '')
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->host = $host;
|
||||
$this->subdomain = $subdomain;
|
||||
$this->client_id = $clientId;
|
||||
$this->authToken = $authToken;
|
||||
$this->shared_at = now()->toDateTimeString();
|
||||
}
|
||||
|
||||
@@ -57,6 +59,7 @@ class ControlConnection
|
||||
return [
|
||||
'host' => $this->host,
|
||||
'client_id' => $this->client_id,
|
||||
'auth_token' => $this->authToken,
|
||||
'subdomain' => $this->subdomain,
|
||||
'shared_at' => $this->shared_at,
|
||||
];
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Server\Http\Controllers\Admin\DeleteUsersController;
|
||||
use App\Server\Http\Controllers\Admin\DisconnectSiteController;
|
||||
use App\Server\Http\Controllers\Admin\GetSettingsController;
|
||||
use App\Server\Http\Controllers\Admin\GetSitesController;
|
||||
use App\Server\Http\Controllers\Admin\GetUserDetailsController;
|
||||
use App\Server\Http\Controllers\Admin\GetUsersController;
|
||||
use App\Server\Http\Controllers\Admin\ListSitesController;
|
||||
use App\Server\Http\Controllers\Admin\ListUsersController;
|
||||
@@ -124,6 +125,7 @@ class Factory
|
||||
$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/sites', GetSitesController::class, $adminCondition);
|
||||
$this->router->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class GetUserDetailsController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUserById($request->get('id'))
|
||||
->then(function ($user) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['user' => $user])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,10 @@ class GetUsersController extends AdminController
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUsers()
|
||||
->then(function ($users) use ($httpConnection) {
|
||||
->paginateUsers(20, (int) $request->get('page', 1))
|
||||
->then(function ($paginated) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['users' => $users])
|
||||
respond_json(['paginated' => $paginated])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
|
||||
@@ -25,7 +25,12 @@ class ListSitesController extends AdminController
|
||||
$sites = $this->getView($httpConnection, 'server.sites.index', [
|
||||
'scheme' => $this->configuration->port() === 443 ? 'https' : 'http',
|
||||
'configuration' => $this->configuration,
|
||||
'sites' => $this->connectionManager->getConnections(),
|
||||
'sites' => collect($this->connectionManager->getConnections())->map(function ($site, $siteId) {
|
||||
$site = $site->toArray();
|
||||
$site['id'] = $siteId;
|
||||
|
||||
return $site;
|
||||
})->values(),
|
||||
]);
|
||||
|
||||
$httpConnection->send(
|
||||
|
||||
@@ -21,10 +21,10 @@ class ListUsersController extends AdminController
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUsers()
|
||||
->then(function ($users) use ($httpConnection) {
|
||||
->paginateUsers(20, (int) $request->get('page', 1))
|
||||
->then(function ($paginated) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_html($this->getView($httpConnection, 'server.users.index', ['users' => $users]))
|
||||
respond_html($this->getView($httpConnection, 'server.users.index', ['paginated' => $paginated]))
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Http\QueryParameters;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\WebSocket\MessageComponentInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\FulfilledPromise;
|
||||
use React\Promise\PromiseInterface;
|
||||
use stdClass;
|
||||
|
||||
@@ -127,7 +126,7 @@ class ControlMessageController implements MessageComponentInterface
|
||||
protected function verifyAuthToken(ConnectionInterface $connection): PromiseInterface
|
||||
{
|
||||
if (config('expose.admin.validate_auth_tokens') !== true) {
|
||||
return new FulfilledPromise();
|
||||
return \React\Promise\resolve(null);
|
||||
}
|
||||
|
||||
$deferred = new Deferred();
|
||||
@@ -136,7 +135,7 @@ class ControlMessageController implements MessageComponentInterface
|
||||
|
||||
$this->userRepository
|
||||
->getUserByToken($authToken)
|
||||
->then(function ($user) use ($connection, $deferred) {
|
||||
->then(function ($user) use ($deferred) {
|
||||
if (is_null($user)) {
|
||||
$deferred->reject();
|
||||
} else {
|
||||
|
||||
@@ -75,7 +75,7 @@ class TunnelMessageController extends Controller
|
||||
|
||||
$httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId);
|
||||
|
||||
transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $httpConnection, $requestId) {
|
||||
transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection , $requestId) {
|
||||
$controlConnection->once('proxy_ready_'.$requestId, function (ConnectionInterface $proxy) use ($request) {
|
||||
// Convert the Laravel request into a PSR7 request
|
||||
$psr17Factory = new Psr17Factory();
|
||||
@@ -119,6 +119,7 @@ class TunnelMessageController extends Controller
|
||||
$request->headers->set('Upgrade-Insecure-Requests', 1);
|
||||
$request->headers->set('X-Exposed-By', config('app.name').' '.config('app.version'));
|
||||
$request->headers->set('X-Original-Host', "{$controlConnection->subdomain}.{$host}");
|
||||
$request->headers->set('X-Forwarded-Host', "{$controlConnection->subdomain}.{$host}");
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Server\UserRepository;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Contracts\UserRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
@@ -13,9 +14,13 @@ class DatabaseUserRepository implements UserRepository
|
||||
/** @var DatabaseInterface */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseInterface $database)
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
public function __construct(DatabaseInterface $database, ConnectionManager $connectionManager)
|
||||
{
|
||||
$this->database = $database;
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function getUsers(): PromiseInterface
|
||||
@@ -31,6 +36,46 @@ class DatabaseUserRepository implements UserRepository
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function paginateUsers(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;
|
||||
}
|
||||
|
||||
$users = collect($result->rows)->map(function ($user) {
|
||||
return $this->getUserDetails($user);
|
||||
})->toArray();
|
||||
|
||||
$paginated = [
|
||||
'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();
|
||||
}
|
||||
|
||||
protected function getUserDetails(array $user)
|
||||
{
|
||||
$user['sites'] = $user['auth_token'] !== '' ? $this->connectionManager->getConnectionsForAuthToken($user['auth_token']) : [];
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getUserById($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
@@ -38,7 +83,13 @@ class DatabaseUserRepository implements UserRepository
|
||||
$this->database
|
||||
->query('SELECT * FROM users WHERE id = :id', ['id' => $id])
|
||||
->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();
|
||||
|
||||
BIN
builds/expose
BIN
builds/expose
Binary file not shown.
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "beyondcode/expose",
|
||||
"description": "Expose",
|
||||
"keywords": ["expose", "tunnel", "ngrok"],
|
||||
"homepage": "https://sharedwithexpose.com",
|
||||
"type": "project",
|
||||
"description": "Expose",
|
||||
"keywords": [
|
||||
"expose",
|
||||
"tunnel",
|
||||
"ngrok"
|
||||
],
|
||||
"homepage": "https://sharedwithexpose.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
@@ -11,44 +15,43 @@
|
||||
"email": "marcel@beyondco.de"
|
||||
}
|
||||
],
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/seankndy/reactphp-sqlite"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.3.0",
|
||||
"ext-json": "*",
|
||||
"padraic/phar-updater": "^1.0.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"nikic/php-parser": "^4.4",
|
||||
"cboden/ratchet": "^0.4.2",
|
||||
"clue/block-react": "^1.3",
|
||||
"clue/buzz-react": "^2.7",
|
||||
"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/http": "5.8.* || ^6.0 || ^7.0",
|
||||
"illuminate/pipeline": "^7.6",
|
||||
"illuminate/validation": "^7.7",
|
||||
"laminas/laminas-http": "^2.11",
|
||||
"laravel-zero/framework": "^7.0",
|
||||
"mockery/mockery": "^1.3",
|
||||
"namshi/cuzzle": "^2.0",
|
||||
"nikic/php-parser": "^4.4",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"phpunit/phpunit": "^8.5",
|
||||
"ratchet/pawl": "^0.3.4",
|
||||
"react/http": "^0.8.6",
|
||||
"react/socket": "dev-master as 1.1",
|
||||
"react/stream": "^1.1.1",
|
||||
"react/socket": "^1.4",
|
||||
"riverline/multipart-parser": "^2.0",
|
||||
"symfony/expression-language": "^5.0",
|
||||
"symfony/http-kernel": "^4.0|^5.0",
|
||||
"symfony/http-kernel": "^4.0 || ^5.0",
|
||||
"symfony/psr-http-message-bridge": "^2.0",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
@@ -62,17 +65,20 @@
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/seankndy/reactphp-sqlite"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"bin": [
|
||||
"builds/expose"
|
||||
],
|
||||
"scripts": {
|
||||
"post-create-project-cmd": [
|
||||
"@php application app:rename"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"bin": ["builds/expose"]
|
||||
}
|
||||
}
|
||||
|
||||
710
composer.lock
generated
710
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => app('git.version'),
|
||||
'version' => '1.3.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The expose server to connect to. By default, expose is using the free
|
||||
| expose.dev server, offered by Beyond Code. You will need a 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.
|
||||
|
|
||||
@@ -78,7 +78,7 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Allowed Memory
|
||||
| Skip Response Logging
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes, some responses don't need to be logged. Some are too big,
|
||||
|
||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
expose:
|
||||
image: beyondcodegmbh/expose-server:latest
|
||||
ports:
|
||||
- 127.0.0.1:8080:${PORT}
|
||||
environment:
|
||||
port: ${PORT}
|
||||
domain: ${DOMAIN}
|
||||
username: ${ADMIN_USERNAME}
|
||||
password: ${ADMIN_PASSWORD}
|
||||
restart: always
|
||||
volumes:
|
||||
- ./database/expose.db:/root/.expose
|
||||
@@ -16,7 +16,7 @@ The result looks like this:
|
||||
```json
|
||||
{
|
||||
"configuration":{
|
||||
"hostname": "expose.dev",
|
||||
"hostname": "sharedwithexpose.com",
|
||||
"port": 8080,
|
||||
"database": "/home/forge/expose/database/expose.db",
|
||||
"validate_auth_tokens": false,
|
||||
|
||||
@@ -28,7 +28,7 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The expose server to connect to. By default, expose is using the free
|
||||
| expose.dev server, offered by Beyond Code. You will need a 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.
|
||||
|
|
||||
@@ -98,7 +98,7 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Allowed Memory
|
||||
| Skip Response Logging
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes, some responses don't need to be logged. Some are too big,
|
||||
|
||||
@@ -41,6 +41,9 @@ expose share http://192.168.2.100
|
||||
|
||||
# Will share access to http://my-local-site.dev using a randomly generated subdomain
|
||||
expose share my-local-site.dev
|
||||
|
||||
# Will share access to https://my-local-site.dev using a randomly generated subdomain (note the https)
|
||||
expose share https://my-local-site.dev
|
||||
```
|
||||
|
||||
## Share a local site with a given subdomain
|
||||
|
||||
@@ -15,11 +15,11 @@ cd ~/Sites/my-awesome-project/
|
||||
expose
|
||||
```
|
||||
|
||||
This will connect to the provided server at expose.dev and give you a tunnel that you can immediately start using.
|
||||
This will connect to the provided server at sharedwithexpose.com and give you a tunnel that you can immediately start using.
|
||||
|
||||
To learn more about how you can share your local sites, check out the [sharing local sites](/docs/expose/client/sharing) documentation.
|
||||
|
||||
## Using the provided server at expose.dev
|
||||
## Using the provided server at sharedwithexpose.com
|
||||
|
||||
A big advantage of Expose over other alternatives such as ngrok, is the ability to host your own server. To make sharing your sites as easy as possible, we provide and host a custom expose server on our own - so getting started with expose is a breeze.
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ order: 2
|
||||
|
||||
Once your Expose server is running, you can only access it over the port that you configure when the server gets started.
|
||||
|
||||
If you want to enable SSL support, you will need to use a proxy service - like Nginx, HAProxy or Caddy - to handle the SSL configurations and proxy all non-SSL requests to your expose server.
|
||||
If you want to enable SSL support, you will need to use a proxy service - like Nginx, HAProxy, Apache2 or Caddy - to handle the SSL configurations and proxy all non-SSL requests to your expose server.
|
||||
|
||||
## Nginx configuration
|
||||
|
||||
A basic Nginx configuration would look like this, but you might want to tweak the SSL parameters to your liking.
|
||||
|
||||
@@ -32,8 +34,55 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Apache2 configuration
|
||||
|
||||
A basic Apache configuration would look like this, but you might want to tweak the SSL parameters to your liking.
|
||||
|
||||
```
|
||||
Listen 80
|
||||
Listen 443
|
||||
|
||||
<IfModule mod_ssl.c>
|
||||
<VirtualHost *:443>
|
||||
ServerName expose.domain.tld
|
||||
ServerAlias *.expose.domain.tld
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||
|
||||
ServerAdmin admin@domain.tld
|
||||
|
||||
ProxyPass "/" "http://localhost:8080/"
|
||||
ProxyPassReverse "/" "http://localhost:8080/"
|
||||
ProxyPreserveHost On
|
||||
|
||||
|
||||
# Needed for websocket support
|
||||
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC,OR]
|
||||
RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
|
||||
RewriteRule .* ws://127.0.0.1:8080%{REQUEST_URI} [P,QSA,L]
|
||||
|
||||
<Proxy http://localhost:8080>
|
||||
|
||||
Require all granted
|
||||
|
||||
Options none
|
||||
</Proxy>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/expose.domain.tld-error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/expose.domain.tld-access.log combined
|
||||
|
||||
SSLCertificateFile /etc/letsencrypt/live/expose.domain.tld-0001/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/expose.domain.tld-0001/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
</VirtualHost>
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
@@ -98,7 +98,7 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The expose server to connect to. By default, expose is using the free
|
||||
| expose.dev server, offered by Beyond Code. You will need a 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.
|
||||
|
|
||||
@@ -122,4 +122,21 @@ return [
|
||||
// ...
|
||||
```
|
||||
|
||||
## Running With Docker
|
||||
|
||||
To run Expose with docker use the included `docker-compose.yaml`. Copy `.env-example` to `.env` and update the configuration.
|
||||
|
||||
```
|
||||
PORT=8080
|
||||
DOMAIN=example.com
|
||||
ADMIN_USERNAME=username
|
||||
ADMIN_PASSWORD=password
|
||||
```
|
||||
|
||||
After updating the environment variables you can start the server:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Now that your basic expose server is running, let's take a look at how you can add SSL support.
|
||||
|
||||
@@ -75,6 +75,27 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex items-center justify-end text-gray-900 p-4" v-if="paginated.current_page > 0">
|
||||
<button
|
||||
:disabled="paginated.previous_page == null"
|
||||
v-on:click="getUsers(paginated.previous_page)"
|
||||
type="button"
|
||||
:class="(paginated.previous_page == null ? 'opacity-50 cursor-not-allowed' : '') + ' mr-3 py-2 px-4 border border-gray-300 rounded-md text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out'"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<button
|
||||
:disabled="paginated.next_page == null"
|
||||
v-on:click="getUsers(paginated.next_page)"
|
||||
type="button"
|
||||
:class="(paginated.next_page == null ? 'opacity-50 cursor-not-allowed' : '') + ' py-2 px-4 border border-gray-300 rounded-md text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition duration-150 ease-in-out'"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center text-gray-900 p-4" v-else>
|
||||
<span class="text-xl">The expose server does not have any users yet.</span>
|
||||
</div>
|
||||
@@ -94,10 +115,25 @@
|
||||
name: '',
|
||||
errors: {},
|
||||
},
|
||||
users: {{ users|json_encode|raw }}
|
||||
paginated: {{ paginated|json_encode|raw }}
|
||||
},
|
||||
|
||||
computed: {
|
||||
users : function() {
|
||||
return this.paginated.users;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getUsers(page) {
|
||||
fetch('/api/users?page=' + page)
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
}).then((data) => {
|
||||
this.paginated = data.paginated;
|
||||
});
|
||||
},
|
||||
|
||||
deleteUser(user) {
|
||||
fetch('/api/users/' + user.id, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -8,6 +8,7 @@ use Clue\React\Buzz\Browser;
|
||||
use Clue\React\Buzz\Message\ResponseException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Support\Str;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Ratchet\Server\IoConnection;
|
||||
use Tests\Feature\TestCase;
|
||||
@@ -149,6 +150,8 @@ class AdminTest extends TestCase
|
||||
$connectionManager = app(ConnectionManager::class);
|
||||
|
||||
$connection = \Mockery::mock(IoConnection::class);
|
||||
$connection->httpRequest = new Request('GET', '/?authToken=some-token');
|
||||
|
||||
$connectionManager->storeConnection('some-host.text', 'fixed-subdomain', $connection);
|
||||
|
||||
/** @var Response $response */
|
||||
|
||||
201
tests/Feature/Server/ApiTest.php
Normal file
201
tests/Feature/Server/ApiTest.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Server;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Factory;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Ratchet\Server\IoConnection;
|
||||
use Tests\Feature\TestCase;
|
||||
|
||||
class ApiTest extends TestCase
|
||||
{
|
||||
/** @var Browser */
|
||||
protected $browser;
|
||||
|
||||
/** @var Factory */
|
||||
protected $serverFactory;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->browser = new Browser($this->loop);
|
||||
$this->browser = $this->browser->withOptions([
|
||||
'followRedirects' => false,
|
||||
]);
|
||||
|
||||
$this->startServer();
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
$this->serverFactory->getSocket()->close();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_list_all_registered_users()
|
||||
{
|
||||
/** @var Response $response */
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'name' => 'Marcel',
|
||||
])));
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$users = $body->paginated->users;
|
||||
|
||||
$this->assertCount(1, $users);
|
||||
$this->assertSame('Marcel', $users[0]->name);
|
||||
$this->assertSame([], $users[0]->sites);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_user_details()
|
||||
{
|
||||
/** @var Response $response */
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'name' => 'Marcel',
|
||||
])));
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/1', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$user = $body->user;
|
||||
|
||||
$this->assertSame('Marcel', $user->name);
|
||||
$this->assertSame([], $user->sites);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_list_all_currently_connected_sites_from_all_users()
|
||||
{
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'name' => 'Marcel',
|
||||
])));
|
||||
|
||||
$createdUser = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
/** @var ConnectionManager $connectionManager */
|
||||
$connectionManager = app(ConnectionManager::class);
|
||||
|
||||
$connection = \Mockery::mock(IoConnection::class);
|
||||
$connection->httpRequest = new Request('GET', '/?authToken='.$createdUser->auth_token);
|
||||
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
|
||||
|
||||
$connection = \Mockery::mock(IoConnection::class);
|
||||
$connection->httpRequest = new Request('GET', '/?authToken=some-other-token');
|
||||
$connectionManager->storeConnection('some-different-host.test', 'different-subdomain', $connection);
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$users = $body->paginated->users;
|
||||
|
||||
$this->assertCount(1, $users[0]->sites);
|
||||
$this->assertSame('some-host.test', $users[0]->sites[0]->host);
|
||||
$this->assertSame('fixed-subdomain', $users[0]->sites[0]->subdomain);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_list_all_currently_connected_sites()
|
||||
{
|
||||
/** @var ConnectionManager $connectionManager */
|
||||
$connectionManager = app(ConnectionManager::class);
|
||||
|
||||
$connection = \Mockery::mock(IoConnection::class);
|
||||
$connection->httpRequest = new Request('GET', '/?authToken=some-token');
|
||||
|
||||
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$sites = $body->sites;
|
||||
|
||||
$this->assertCount(1, $sites);
|
||||
$this->assertSame('some-host.test', $sites[0]->host);
|
||||
$this->assertSame('some-token', $sites[0]->auth_token);
|
||||
$this->assertSame('fixed-subdomain', $sites[0]->subdomain);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_list_all_currently_connected_sites_without_auth_tokens()
|
||||
{
|
||||
/** @var ConnectionManager $connectionManager */
|
||||
$connectionManager = app(ConnectionManager::class);
|
||||
|
||||
$connection = \Mockery::mock(IoConnection::class);
|
||||
$connection->httpRequest = new Request('GET', '/');
|
||||
|
||||
$connectionManager->storeConnection('some-host.test', 'fixed-subdomain', $connection);
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/sites', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$sites = $body->sites;
|
||||
|
||||
$this->assertCount(1, $sites);
|
||||
$this->assertSame('some-host.test', $sites[0]->host);
|
||||
$this->assertSame('', $sites[0]->auth_token);
|
||||
$this->assertSame('fixed-subdomain', $sites[0]->subdomain);
|
||||
}
|
||||
|
||||
protected function startServer()
|
||||
{
|
||||
$this->app['config']['expose.admin.subdomain'] = 'expose';
|
||||
$this->app['config']['expose.admin.database'] = ':memory:';
|
||||
|
||||
$this->app['config']['expose.admin.users'] = [
|
||||
'username' => 'secret',
|
||||
];
|
||||
|
||||
$this->serverFactory = new Factory();
|
||||
|
||||
$this->serverFactory->setLoop($this->loop)
|
||||
->createServer();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,19 @@ class LoggedRequestTest extends TestCase
|
||||
$this->assertSame('example-request', $loggedRequest->id());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_retrieves_the_request_for_chrome_extensions()
|
||||
{
|
||||
$rawRequest = str(new Request(200, '/expose', [
|
||||
'Origin' => 'chrome-extension://expose',
|
||||
'X-Expose-Request-ID' => 'example-request',
|
||||
]));
|
||||
$parsedRequest = LaminasRequest::fromString($rawRequest);
|
||||
|
||||
$loggedRequest = new LoggedRequest($rawRequest, $parsedRequest);
|
||||
$this->assertSame('example-request', $loggedRequest->id());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_post_data_for_json_payloads()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user