@@ -73,6 +92,9 @@
Custom Subdomains
|
+
+ TCP ports
+ |
Created At
|
@@ -95,6 +117,14 @@
Yes
+
+
+ No
+
+
+ Yes
+
+ |
@{ user.created_at }
|
@@ -144,6 +174,7 @@
userForm: {
name: '',
can_specify_subdomains: true,
+ can_share_tcp_ports: true,
errors: {},
},
paginated: {{ paginated|json_encode|raw }}
@@ -186,7 +217,8 @@
}).then((data) => {
if (data.user) {
this.userForm.name = '';
- this.userForm.can_specify_subdomains = 0;
+ this.userForm.can_specify_subdomains = true;
+ this.userForm.can_share_tcp_ports = true;
this.userForm.errors = {};
this.users.unshift(data.user);
}
diff --git a/tests/Feature/Server/ApiTest.php b/tests/Feature/Server/ApiTest.php
index ad53dc8..620a91e 100644
--- a/tests/Feature/Server/ApiTest.php
+++ b/tests/Feature/Server/ApiTest.php
@@ -88,6 +88,7 @@ class ApiTest extends TestCase
$this->assertSame('Marcel', $user->name);
$this->assertSame([], $user->sites);
+ $this->assertSame([], $user->tcp_connections);
}
/** @test */
@@ -115,6 +116,10 @@ class ApiTest extends TestCase
$connection->httpRequest = new Request('GET', '/?authToken=some-other-token');
$connectionManager->storeConnection('some-different-host.test', 'different-subdomain', $connection);
+ $connection = \Mockery::mock(IoConnection::class);
+ $connection->httpRequest = new Request('GET', '/?authToken='.$createdUser->auth_token);
+ $connectionManager->storeTcpConnection(2525, $connection);
+
/** @var Response $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080/api/users', [
'Host' => 'expose.localhost',
@@ -126,6 +131,7 @@ class ApiTest extends TestCase
$users = $body->paginated->users;
$this->assertCount(1, $users[0]->sites);
+ $this->assertCount(1, $users[0]->tcp_connections);
$this->assertSame('some-host.test', $users[0]->sites[0]->host);
$this->assertSame('fixed-subdomain', $users[0]->sites[0]->subdomain);
}
diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php
index cc612f6..73f543c 100644
--- a/tests/Feature/Server/TunnelTest.php
+++ b/tests/Feature/Server/TunnelTest.php
@@ -9,6 +9,7 @@ use Clue\React\Buzz\Message\ResponseException;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Server;
+use React\Socket\Connection;
use Tests\Feature\TestCase;
class TunnelTest extends TestCase
@@ -22,6 +23,9 @@ class TunnelTest extends TestCase
/** @var \React\Socket\Server */
protected $testHttpServer;
+ /** @var \React\Socket\Server */
+ protected $testTcpServer;
+
public function setUp(): void
{
parent::setUp();
@@ -42,6 +46,10 @@ class TunnelTest extends TestCase
$this->testHttpServer->close();
}
+ if (isset($this->testTcpServer)) {
+ $this->testTcpServer->close();
+ }
+
parent::tearDown();
}
@@ -81,6 +89,93 @@ class TunnelTest extends TestCase
$this->assertSame('Hello World!', $response->getBody()->getContents());
}
+ /** @test */
+ public function it_sends_incoming_requests_to_the_connected_client_via_tcp()
+ {
+ $this->createTestTcpServer();
+
+ $this->app['config']['expose.admin.validate_auth_tokens'] = false;
+
+ /**
+ * We create an expose client that connects to our server and shares
+ * the created test HTTP server.
+ */
+ $client = $this->createClient();
+ $response = $this->await($client->connectToServerAndShareTcp(8085));
+
+ /**
+ * Once the client is connected, we connect to the
+ * created tunnel.
+ */
+ $connector = new \React\Socket\Connector($this->loop);
+ $connection = $this->await($connector->connect('127.0.0.1:'.$response->shared_port));
+
+ $this->assertInstanceOf(Connection::class, $connection);
+ }
+
+ /** @test */
+ public function it_rejects_tcp_sharing_if_forbidden()
+ {
+ $this->createTestTcpServer();
+
+ $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_share_tcp_ports' => 0,
+ ])));
+
+ $user = json_decode($response->getBody()->getContents())->user;
+
+ $this->expectException(\UnexpectedValueException::class);
+
+ /**
+ * We create an expose client that connects to our server and shares
+ * the created test HTTP server.
+ */
+ $client = $this->createClient();
+ $this->await($client->connectToServerAndShareTcp(8085, $user->auth_token));
+ }
+
+ /** @test */
+ public function it_allows_tcp_sharing_if_enabled_for_user()
+ {
+ $this->createTestTcpServer();
+
+ $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_share_tcp_ports' => 1,
+ ])));
+
+ $user = json_decode($response->getBody()->getContents())->user;
+
+ /**
+ * We create an expose client that connects to our server and shares
+ * the created test HTTP server.
+ */
+ $client = $this->createClient();
+ $response = $this->await($client->connectToServerAndShareTcp(8085, $user->auth_token));
+
+ /**
+ * Once the client is connected, we connect to the
+ * created tunnel.
+ */
+ $connector = new \React\Socket\Connector($this->loop);
+ $connection = $this->await($connector->connect('127.0.0.1:'.$response->shared_port));
+
+ $this->assertInstanceOf(Connection::class, $connection);
+ }
+
/** @test */
public function it_rejects_clients_with_invalid_auth_tokens()
{
@@ -221,4 +316,15 @@ class TunnelTest extends TestCase
$this->testHttpServer = new \React\Socket\Server(8085, $this->loop);
$server->listen($this->testHttpServer);
}
+
+ protected function createTestTcpServer()
+ {
+ $this->testTcpServer = new \React\Socket\Server(8085, $this->loop);
+
+ $this->testTcpServer->on('connection', function (\React\Socket\ConnectionInterface $connection) {
+ $connection->write('Hello '.$connection->getRemoteAddress()."!\n");
+
+ $connection->pipe($connection);
+ });
+ }
}