mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
Allow users to reserve subdomains (#131)
This commit is contained in:
@@ -197,6 +197,14 @@ class Client
|
||||
|
||||
protected function attachCommonConnectionListeners(ControlConnection $connection, Deferred $deferred)
|
||||
{
|
||||
$connection->on('info', function ($data) {
|
||||
$this->logger->info($data->message);
|
||||
});
|
||||
|
||||
$connection->on('error', function ($data) {
|
||||
$this->logger->error($data->message);
|
||||
});
|
||||
|
||||
$connection->on('authenticationFailed', function ($data) use ($deferred) {
|
||||
$this->logger->error($data->message);
|
||||
|
||||
|
||||
22
app/Contracts/SubdomainRepository.php
Normal file
22
app/Contracts/SubdomainRepository.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
interface SubdomainRepository
|
||||
{
|
||||
public function getSubdomains(): PromiseInterface;
|
||||
|
||||
public function getSubdomainById($id): PromiseInterface;
|
||||
|
||||
public function getSubdomainByName(string $name): PromiseInterface;
|
||||
|
||||
public function getSubdomainsByUserId($id): PromiseInterface;
|
||||
|
||||
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface;
|
||||
|
||||
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface;
|
||||
|
||||
public function storeSubdomain(array $data): PromiseInterface;
|
||||
}
|
||||
@@ -4,10 +4,12 @@ namespace App\Server;
|
||||
|
||||
use App\Contracts\ConnectionManager as ConnectionManagerContract;
|
||||
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\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;
|
||||
@@ -22,6 +24,7 @@ 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\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;
|
||||
@@ -130,6 +133,8 @@ class Factory
|
||||
$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->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->delete('/api/sites/{id}', DisconnectSiteController::class, $adminCondition);
|
||||
@@ -171,6 +176,7 @@ class Factory
|
||||
$this->bindConfiguration()
|
||||
->bindSubdomainGenerator()
|
||||
->bindUserRepository()
|
||||
->bindSubdomainRepository()
|
||||
->bindDatabase()
|
||||
->ensureDatabaseIsInitialized()
|
||||
->bindConnectionManager()
|
||||
@@ -207,6 +213,15 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindSubdomainRepository()
|
||||
{
|
||||
app()->singleton(SubdomainRepository::class, function () {
|
||||
return app(config('expose.admin.subdomain_repository'));
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function bindDatabase()
|
||||
{
|
||||
app()->singleton(DatabaseInterface::class, function () {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class DeleteSubdomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository->getUserByToken($request->get('auth_token', ''))
|
||||
->then(function ($user) use ($request, $httpConnection) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->subdomainRepository->deleteSubdomainForUserId($user['id'], $request->get('subdomain'))
|
||||
->then(function ($deleted) use ($httpConnection) {
|
||||
$httpConnection->send(respond_json(['deleted' => $deleted], 200));
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Ratchet\ConnectionInterface;
|
||||
@@ -13,21 +14,31 @@ class GetUserDetailsController extends AdminController
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository)
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$this->userRepository
|
||||
->getUserById($request->get('id'))
|
||||
->then(function ($user) use ($httpConnection) {
|
||||
$httpConnection->send(
|
||||
respond_json(['user' => $user])
|
||||
);
|
||||
->then(function ($user) use ($httpConnection, $request) {
|
||||
$this->subdomainRepository->getSubdomainsByUserId($request->get('id'))
|
||||
->then(function ($subdomains) use ($httpConnection, $user) {
|
||||
$httpConnection->send(
|
||||
respond_json([
|
||||
'user' => $user,
|
||||
'subdomains' => $subdomains,
|
||||
])
|
||||
);
|
||||
|
||||
$httpConnection->close();
|
||||
$httpConnection->close();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Http\Controllers\Admin;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class StoreSubdomainController extends AdminController
|
||||
{
|
||||
protected $keepConnectionOpen = true;
|
||||
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
public function handle(Request $request, ConnectionInterface $httpConnection)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'subdomain' => 'required',
|
||||
], [
|
||||
'required' => 'The :attribute field is required.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userRepository->getUserByToken($request->get('auth_token', ''))
|
||||
->then(function ($user) use ($httpConnection, $request) {
|
||||
if (is_null($user)) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user does not exist'], 404));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user['can_specify_subdomains'] === 0) {
|
||||
$httpConnection->send(respond_json(['error' => 'The user is not allowed to reserve subdomains.'], 401));
|
||||
$httpConnection->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$insertData = [
|
||||
'user_id' => $user['id'],
|
||||
'subdomain' => $request->get('subdomain'),
|
||||
];
|
||||
|
||||
$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,6 +3,7 @@
|
||||
namespace App\Server\Http\Controllers;
|
||||
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use App\Contracts\UserRepository;
|
||||
use App\Http\QueryParameters;
|
||||
use App\Server\Exceptions\NoFreePortAvailable;
|
||||
@@ -20,10 +21,14 @@ class ControlMessageController implements MessageComponentInterface
|
||||
/** @var UserRepository */
|
||||
protected $userRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository)
|
||||
/** @var SubdomainRepository */
|
||||
protected $subdomainRepository;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager, UserRepository $userRepository, SubdomainRepository $subdomainRepository)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->subdomainRepository = $subdomainRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,22 +105,26 @@ class ControlMessageController implements MessageComponentInterface
|
||||
|
||||
protected function handleHttpConnection(ConnectionInterface $connection, $data, $user = null)
|
||||
{
|
||||
if (! $this->hasValidSubdomain($connection, $data->subdomain, $user)) {
|
||||
return;
|
||||
}
|
||||
$this->hasValidSubdomain($connection, $data->subdomain, $user)->then(function ($subdomain) use ($data, $connection) {
|
||||
if ($subdomain === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection);
|
||||
$data->subdomain = $subdomain;
|
||||
|
||||
$this->connectionManager->limitConnectionLength($connectionInfo, config('expose.admin.maximum_connection_length'));
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection);
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.message_of_the_day'),
|
||||
'subdomain' => $connectionInfo->subdomain,
|
||||
'client_id' => $connectionInfo->client_id,
|
||||
],
|
||||
]));
|
||||
$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,
|
||||
],
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
protected function handleTcpConnection(ConnectionInterface $connection, $data, $user = null)
|
||||
@@ -203,39 +212,65 @@ class ControlMessageController implements MessageComponentInterface
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): bool
|
||||
protected function hasValidSubdomain(ConnectionInterface $connection, ?string $subdomain, ?array $user): 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' => 'subdomainTaken',
|
||||
'event' => 'info',
|
||||
'data' => [
|
||||
'message' => config('expose.admin.messages.custom_subdomain_unauthorized'),
|
||||
'message' => config('expose.admin.messages.custom_subdomain_unauthorized').PHP_EOL,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return false;
|
||||
return \React\Promise\resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given subdomain is reserved for a different user.
|
||||
*/
|
||||
if (! is_null($subdomain)) {
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
|
||||
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) {
|
||||
$message = config('expose.admin.messages.subdomain_taken');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
return $this->subdomainRepository->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');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return false;
|
||||
}
|
||||
return \React\Promise\resolve(false);
|
||||
}
|
||||
|
||||
$controlConnection = $this->connectionManager->findControlConnectionForSubdomain($subdomain);
|
||||
|
||||
if (! is_null($controlConnection) || $subdomain === config('expose.admin.subdomain')) {
|
||||
$message = config('expose.admin.messages.subdomain_taken');
|
||||
$message = str_replace(':subdomain', $subdomain, $message);
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'subdomainTaken',
|
||||
'data' => [
|
||||
'message' => $message,
|
||||
],
|
||||
]));
|
||||
$connection->close();
|
||||
|
||||
return \React\Promise\resolve(false);
|
||||
}
|
||||
|
||||
return \React\Promise\resolve($subdomain);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
return \React\Promise\resolve($subdomain);
|
||||
}
|
||||
|
||||
protected function canShareTcpPorts(ConnectionInterface $connection, $data, $user)
|
||||
|
||||
132
app/Server/SubdomainRepository/DatabaseSubdomainRepository.php
Normal file
132
app/Server/SubdomainRepository/DatabaseSubdomainRepository.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\SubdomainRepository;
|
||||
|
||||
use App\Contracts\SubdomainRepository;
|
||||
use Clue\React\SQLite\DatabaseInterface;
|
||||
use Clue\React\SQLite\Result;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class DatabaseSubdomainRepository implements SubdomainRepository
|
||||
{
|
||||
/** @var DatabaseInterface */
|
||||
protected $database;
|
||||
|
||||
public function __construct(DatabaseInterface $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
public function getSubdomains(): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains ORDER by created_at DESC')
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainById($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $id])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainByName(string $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE subdomain = :name', ['name' => $name])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0] ?? null);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainsByUserId($id): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE user_id = :user_id ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function storeSubdomain(array $data): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->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)
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$this->database->query('SELECT * FROM subdomains WHERE id = :id', ['id' => $result->insertId])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function getSubdomainsByUserIdAndName($id, $name): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database
|
||||
->query('SELECT * FROM subdomains WHERE user_id = :user_id AND subdomain = :name ORDER by created_at DESC', [
|
||||
'user_id' => $id,
|
||||
'name' => $name,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result->rows);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function deleteSubdomainForUserId($userId, $subdomainId): PromiseInterface
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->database->query('DELETE FROM subdomains WHERE id = :id AND user_id = :user_id', [
|
||||
'id' => $subdomainId,
|
||||
'user_id' => $userId,
|
||||
])
|
||||
->then(function (Result $result) use ($deferred) {
|
||||
$deferred->resolve($result);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
@@ -232,6 +232,8 @@ return [
|
||||
*/
|
||||
'user_repository' => \App\Server\UserRepository\DatabaseUserRepository::class,
|
||||
|
||||
'subdomain_repository' => \App\Server\SubdomainRepository\DatabaseSubdomainRepository::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Messages
|
||||
@@ -249,7 +251,7 @@ return [
|
||||
|
||||
'subdomain_taken' => 'The chosen subdomain :subdomain is already taken. Please choose a different subdomain.',
|
||||
|
||||
'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro.',
|
||||
'custom_subdomain_unauthorized' => 'You are not allowed to specify custom subdomains. Please upgrade to Expose Pro. Assigning a random subdomain instead.',
|
||||
|
||||
'no_free_tcp_port_available' => 'There are no free TCP ports available on this server. Please try again later.',
|
||||
],
|
||||
|
||||
7
database/migrations/04_create_subdomains_table.sql
Normal file
7
database/migrations/04_create_subdomains_table.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS subdomains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
subdomain STRING NOT NULL,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Feature\Server;
|
||||
use App\Contracts\ConnectionManager;
|
||||
use App\Server\Factory;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Clue\React\Buzz\Message\ResponseException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Nyholm\Psr7\Request;
|
||||
use Ratchet\Server\IoConnection;
|
||||
@@ -65,10 +66,10 @@ class ApiTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_user_details()
|
||||
public function it_does_not_allow_subdomain_reservation_for_users_without_the_right_flag()
|
||||
{
|
||||
/** @var Response $response */
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/users', [
|
||||
$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',
|
||||
@@ -76,6 +77,72 @@ class ApiTest extends TestCase
|
||||
'name' => 'Marcel',
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->expectException(ResponseException::class);
|
||||
$this->expectExceptionMessage('HTTP status code 401');
|
||||
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'auth_token' => $user->auth_token,
|
||||
'subdomain' => 'reserved',
|
||||
])));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_subdomain_reservation_for_users_with_the_right_flag()
|
||||
{
|
||||
/** @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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'auth_token' => $user->auth_token,
|
||||
'subdomain' => 'reserved',
|
||||
])));
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_user_details()
|
||||
{
|
||||
/** @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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'auth_token' => $user->auth_token,
|
||||
'subdomain' => 'reserved',
|
||||
])));
|
||||
|
||||
/** @var Response $response */
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/1', [
|
||||
'Host' => 'expose.localhost',
|
||||
@@ -85,10 +152,117 @@ class ApiTest extends TestCase
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$user = $body->user;
|
||||
$subdomains = $body->subdomains;
|
||||
|
||||
$this->assertSame('Marcel', $user->name);
|
||||
$this->assertSame([], $user->sites);
|
||||
$this->assertSame([], $user->tcp_connections);
|
||||
|
||||
$this->assertCount(1, $subdomains);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_delete_subdomains()
|
||||
{
|
||||
/** @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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'subdomain' => 'reserved',
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$this->await($this->browser->delete('http://127.0.0.1:8080/api/subdomains/1', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
/** @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());
|
||||
$subdomains = $body->subdomains;
|
||||
|
||||
$this->assertCount(0, $subdomains);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_not_reserve_an_already_reserved_subdomain()
|
||||
{
|
||||
/** @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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'subdomain' => 'reserved',
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$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' => 'Sebastian',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->expectException(ResponseException::class);
|
||||
$this->expectExceptionMessage('HTTP status code 422');
|
||||
|
||||
$this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'subdomain' => 'reserved',
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users/2', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
|
||||
$body = json_decode($response->getBody()->getContents());
|
||||
$subdomains = $body->subdomains;
|
||||
|
||||
$this->assertCount(0, $subdomains);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
@@ -235,8 +235,6 @@ class TunnelTest extends TestCase
|
||||
'can_specify_subdomains' => 0,
|
||||
])));
|
||||
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->createTestHttpServer();
|
||||
@@ -248,7 +246,92 @@ class TunnelTest extends TestCase
|
||||
$client = $this->createClient();
|
||||
$response = $this->await($client->connectToServer('127.0.0.1:8085', 'tunnel', $user->auth_token));
|
||||
|
||||
$this->assertSame('tunnel', $response->subdomain);
|
||||
$this->assertNotSame('tunnel', $response->subdomain);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_rejects_users_that_want_to_use_a_reserved_subdomain()
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'subdomain' => 'reserved',
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$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' => 'Test-User',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$this->createTestHttpServer();
|
||||
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
/**
|
||||
* We create an expose client that connects to our server and shares
|
||||
* the created test HTTP server.
|
||||
*/
|
||||
$client = $this->createClient();
|
||||
$response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', $user->auth_token));
|
||||
|
||||
$this->assertSame('reserved', $response->subdomain);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_users_to_use_their_own_reserved_subdomains()
|
||||
{
|
||||
$this->app['config']['expose.admin.validate_auth_tokens'] = true;
|
||||
|
||||
$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',
|
||||
'can_specify_subdomains' => 1,
|
||||
])));
|
||||
|
||||
$user = json_decode($response->getBody()->getContents())->user;
|
||||
|
||||
$response = $this->await($this->browser->post('http://127.0.0.1:8080/api/subdomains', [
|
||||
'Host' => 'expose.localhost',
|
||||
'Authorization' => base64_encode('username:secret'),
|
||||
'Content-Type' => 'application/json',
|
||||
], json_encode([
|
||||
'subdomain' => 'reserved',
|
||||
'auth_token' => $user->auth_token,
|
||||
])));
|
||||
|
||||
$this->createTestHttpServer();
|
||||
/**
|
||||
* We create an expose client that connects to our server and shares
|
||||
* the created test HTTP server.
|
||||
*/
|
||||
$client = $this->createClient();
|
||||
$response = $this->await($client->connectToServer('127.0.0.1:8085', 'reserved', $user->auth_token));
|
||||
|
||||
$this->assertSame('reserved', $response->subdomain);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
Reference in New Issue
Block a user