This commit is contained in:
Marcel Pociot
2020-05-01 20:52:12 +02:00
parent ea394c8e54
commit c717683634
16 changed files with 145 additions and 96 deletions

View File

@@ -3,15 +3,16 @@
namespace App\Contracts; namespace App\Contracts;
use App\Server\Connections\ControlConnection; use App\Server\Connections\ControlConnection;
use App\Server\Connections\HttpConnection;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
interface ConnectionManager interface ConnectionManager
{ {
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection; public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection;
public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): ConnectionInterface; public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection;
public function getHttpConnectionForRequestId(string $requestId): ?ConnectionInterface; public function getHttpConnectionForRequestId(string $requestId): ?HttpConnection;
public function removeControlConnection($connection); public function removeControlConnection($connection);

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Contracts;
use App\Server\Connections\HttpConnection;
use Illuminate\Http\Request;
interface RequestModifier
{
public function handle(Request $request, HttpConnection $httpConnection): ?Request;
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Http\Request;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface; use Ratchet\Http\HttpServerInterface;
use React\Promise\PromiseInterface;
use function GuzzleHttp\Psr7\parse_request; use function GuzzleHttp\Psr7\parse_request;
abstract class Controller implements HttpServerInterface abstract class Controller implements HttpServerInterface

View File

@@ -65,7 +65,7 @@ class LoggedRequest implements \JsonSerializable
'headers' => $this->parsedRequest->getHeaders()->toArray(), 'headers' => $this->parsedRequest->getHeaders()->toArray(),
'body' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->parsedRequest->getContent(), 'body' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->parsedRequest->getContent(),
'query' => $this->parsedRequest->getQuery()->toArray(), 'query' => $this->parsedRequest->getQuery()->toArray(),
'post' => $this->getPost(), 'post' => $this->getPostData(),
'curl' => '', //(new CurlFormatter())->format(parse_request($this->rawRequest)), 'curl' => '', //(new CurlFormatter())->format(parse_request($this->rawRequest)),
'additional_data' => $this->additionalData, 'additional_data' => $this->additionalData,
], ],
@@ -142,7 +142,7 @@ class LoggedRequest implements \JsonSerializable
return $this->parsedResponse; return $this->parsedResponse;
} }
protected function getPost() public function getPostData()
{ {
$postData = []; $postData = [];
@@ -151,7 +151,7 @@ class LoggedRequest implements \JsonSerializable
switch ($contentType) { switch ($contentType) {
case 'application/x-www-form-urlencoded': case 'application/x-www-form-urlencoded':
parse_str($this->parsedRequest->getContent(), $postData); parse_str($this->parsedRequest->getContent(), $postData);
$postData = collect($postData)->map(function ($key, $value) { $postData = collect($postData)->map(function ($value, $key) {
return [ return [
'name' => $key, 'name' => $key,
'value' => $value, 'value' => $value,
@@ -159,12 +159,12 @@ class LoggedRequest implements \JsonSerializable
})->toArray(); })->toArray();
break; break;
case 'application/json': case 'application/json':
$postData = collect(json_decode($this->parsedRequest->getContent(), true))->map(function ($key, $value) { $postData = collect(json_decode($this->parsedRequest->getContent(), true))->map(function ($value, $key) {
return [ return [
'name' => $key, 'name' => $key,
'value' => $value, 'value' => $value,
]; ];
})->toArray(); })->values()->toArray();
break; break;
default: default:

View File

@@ -35,14 +35,14 @@ class ConnectionManager implements ConnectionManagerContract
return $storedConnection; return $storedConnection;
} }
public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): ConnectionInterface public function storeHttpConnection(ConnectionInterface $httpConnection, $requestId): HttpConnection
{ {
$this->httpConnections[$requestId] = $httpConnection; $this->httpConnections[$requestId] = new HttpConnection($httpConnection);
return $httpConnection; return $this->httpConnections[$requestId];
} }
public function getHttpConnectionForRequestId(string $requestId): ?ConnectionInterface public function getHttpConnectionForRequestId(string $requestId): ?HttpConnection
{ {
return $this->httpConnections[$requestId] ?? null; return $this->httpConnections[$requestId] ?? null;
} }

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Server\Connections;
use Evenement\EventEmitterTrait;
use Ratchet\ConnectionInterface;
class HttpConnection
{
use EventEmitterTrait;
/** @var ConnectionInterface */
public $socket;
public function __construct(ConnectionInterface $socket)
{
$this->socket = $socket;
}
public function send($data)
{
$this->emit('data', [$data]);
$this->socket->send($data);
}
public function close()
{
$this->emit('close');
$this->socket->close();
}
}

View File

@@ -9,12 +9,10 @@ use App\Server\Connections\ConnectionManager;
use App\Server\Http\Controllers\Admin\DeleteUsersController; use App\Server\Http\Controllers\Admin\DeleteUsersController;
use App\Server\Http\Controllers\Admin\ListSitesController; use App\Server\Http\Controllers\Admin\ListSitesController;
use App\Server\Http\Controllers\Admin\ListUsersController; use App\Server\Http\Controllers\Admin\ListUsersController;
use App\Server\Http\Controllers\Admin\LoginController;
use App\Server\Http\Controllers\Admin\RedirectToUsersController; use App\Server\Http\Controllers\Admin\RedirectToUsersController;
use App\Server\Http\Controllers\Admin\SaveSettingsController; use App\Server\Http\Controllers\Admin\SaveSettingsController;
use App\Server\Http\Controllers\Admin\ShowSettingsController; use App\Server\Http\Controllers\Admin\ShowSettingsController;
use App\Server\Http\Controllers\Admin\StoreUsersController; use App\Server\Http\Controllers\Admin\StoreUsersController;
use App\Server\Http\Controllers\Admin\VerifyLoginController;
use App\Server\Http\Controllers\ControlMessageController; use App\Server\Http\Controllers\ControlMessageController;
use App\Server\Http\Controllers\TunnelMessageController; use App\Server\Http\Controllers\TunnelMessageController;
use App\Http\RouteGenerator; use App\Http\RouteGenerator;

View File

@@ -51,7 +51,7 @@ class ControlMessageController implements MessageComponentInterface
function onMessage(ConnectionInterface $connection, $msg) function onMessage(ConnectionInterface $connection, $msg)
{ {
if (isset($connection->request_id)) { if (isset($connection->request_id)) {
return $this->sendRequestToHttpConnection($connection->request_id, $msg); return $this->sendResponseToHttpConnection($connection->request_id, $msg);
} }
try { try {
@@ -66,10 +66,11 @@ class ControlMessageController implements MessageComponentInterface
} }
} }
protected function sendRequestToHttpConnection(string $requestId, $request) protected function sendResponseToHttpConnection(string $requestId, $response)
{ {
$httpConnection = $this->connectionManager->getHttpConnectionForRequestId($requestId); $httpConnection = $this->connectionManager->getHttpConnectionForRequestId($requestId);
$httpConnection->send($request);
$httpConnection->send($response);
} }
protected function authenticate(ConnectionInterface $connection, $data) protected function authenticate(ConnectionInterface $connection, $data)

View File

@@ -6,6 +6,7 @@ use App\Contracts\ConnectionManager;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Server\Configuration; use App\Server\Configuration;
use App\Server\Connections\ControlConnection; use App\Server\Connections\ControlConnection;
use App\Server\Connections\HttpConnection;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline; use Illuminate\Pipeline\Pipeline;
@@ -13,6 +14,7 @@ use Illuminate\Support\Str;
use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Factory\Psr17Factory;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\Frame; use Ratchet\RFC6455\Messaging\Frame;
use React\Promise\Deferred;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use function GuzzleHttp\Psr7\str; use function GuzzleHttp\Psr7\str;
@@ -22,13 +24,11 @@ class TunnelMessageController extends Controller
protected $connectionManager; protected $connectionManager;
/** @var Configuration */ /** @var Configuration */
private $configuration; protected $configuration;
protected $keepConnectionOpen = true; protected $keepConnectionOpen = true;
protected $middleware = [ protected $modifiers = [];
];
public function __construct(ConnectionManager $connectionManager, Configuration $configuration) public function __construct(ConnectionManager $connectionManager, Configuration $configuration)
{ {
@@ -62,26 +62,38 @@ class TunnelMessageController extends Controller
protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection) protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection)
{ {
(new Pipeline(app())) $request = $this->prepareRequest($request, $controlConnection);
->send($this->prepareRequest($request, $controlConnection))
->through($this->middleware)
->then(function ($request) use ($controlConnection, $httpConnection) {
$requestId = $request->header('X-Expose-Request-ID');
$this->connectionManager->storeHttpConnection($httpConnection, $requestId); $requestId = $request->header('X-Expose-Request-ID');
$controlConnection->once('proxy_ready_' . $requestId, function (ConnectionInterface $proxy) use ($request) { $httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId);
// Convert the Laravel request into a PSR7 request
$psr17Factory = new Psr17Factory();
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$request = $psrHttpFactory->createRequest($request);
$binaryMsg = new Frame(str($request), true, Frame::OP_BINARY); transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $httpConnection, $requestId) {
$proxy->send($binaryMsg); $controlConnection->once('proxy_ready_' . $requestId, function (ConnectionInterface $proxy) use ($request) {
}); // Convert the Laravel request into a PSR7 request
$psr17Factory = new Psr17Factory();
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$request = $psrHttpFactory->createRequest($request);
$controlConnection->registerProxy($requestId); $binaryMsg = new Frame(str($request), true, Frame::OP_BINARY);
$proxy->send($binaryMsg);
}); });
$controlConnection->registerProxy($requestId);
});
}
protected function passRequestThroughModifiers(Request $request, HttpConnection $httpConnection): ?Request
{
foreach ($this->modifiers as $modifier) {
$request = app($modifier)->handle($request, $httpConnection);
if (is_null($request)) {
break;
}
}
return $request;
} }
protected function prepareRequest(Request $request, ControlConnection $controlConnection): Request protected function prepareRequest(Request $request, ControlConnection $controlConnection): Request

View File

@@ -28,6 +28,7 @@
"require-dev": { "require-dev": {
"bfunky/http-parser": "^2.2", "bfunky/http-parser": "^2.2",
"cboden/ratchet": "^0.4.2", "cboden/ratchet": "^0.4.2",
"clue/block-react": "^1.3",
"clue/buzz-react": "^2.7", "clue/buzz-react": "^2.7",
"clue/reactphp-sqlite": "^1.0", "clue/reactphp-sqlite": "^1.0",
"guzzlehttp/guzzle": "^6.5", "guzzlehttp/guzzle": "^6.5",

62
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "801c0ab8f694ff370f2eefea409a7fcc", "content-hash": "710c897a2bbd37c60950d540b46ba1b5",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@@ -2561,21 +2561,22 @@
}, },
{ {
"name": "nesbot/carbon", "name": "nesbot/carbon",
"version": "2.32.2", "version": "2.33.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/briannesbitt/Carbon.git", "url": "https://github.com/briannesbitt/Carbon.git",
"reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc" "reference": "4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b",
"reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", "reference": "4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"php": "^7.1.8 || ^8.0", "php": "^7.1.8 || ^8.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^3.4 || ^4.0 || ^5.0" "symfony/translation": "^3.4 || ^4.0 || ^5.0"
}, },
"require-dev": { "require-dev": {
@@ -2628,7 +2629,7 @@
"datetime", "datetime",
"time" "time"
], ],
"time": "2020-03-31T13:43:19+00:00" "time": "2020-04-20T15:05:43+00:00"
}, },
{ {
"name": "nunomaduro/collision", "name": "nunomaduro/collision",
@@ -5536,55 +5537,6 @@
"homepage": "https://github.com/sebastianbergmann/version", "homepage": "https://github.com/sebastianbergmann/version",
"time": "2016-10-03T07:35:21+00:00" "time": "2016-10-03T07:35:21+00:00"
}, },
{
"name": "seregazhuk/react-promise-testing",
"version": "0.4.0",
"source": {
"type": "git",
"url": "https://github.com/seregazhuk/php-react-promise-testing.git",
"reference": "a56481a6feae2cb51f91d554af41a66802a01a37"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/seregazhuk/php-react-promise-testing/zipball/a56481a6feae2cb51f91d554af41a66802a01a37",
"reference": "a56481a6feae2cb51f91d554af41a66802a01a37",
"shasum": ""
},
"require": {
"clue/block-react": "^1.3",
"php": ">=7.2",
"phpunit/phpunit": "^8.2",
"react/promise": "^2.7"
},
"require-dev": {
"codeclimate/php-test-reporter": "^0.4.4"
},
"type": "library",
"autoload": {
"psr-4": {
"seregazhuk\\React\\PromiseTesting\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sergey Zhuk",
"email": "seregazhuk88@gmail.com"
}
],
"description": "PHPUnit-based library for testing ReactPHP promises",
"homepage": "https://github.com/seregazhuk/php-react-promise-testing",
"keywords": [
"promise",
"reactphp",
"test",
"testing"
],
"time": "2020-03-09T12:02:56+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v5.0.8", "version": "v5.0.8",

View File

@@ -1,8 +1,8 @@
<?php <?php
return [ return [
'host' => 'expose.dev', 'host' => 'localhost',
'port' => 443, 'port' => 8080,
'auth_token' => '', 'auth_token' => '',
'admin' => [ 'admin' => [

View File

@@ -238,8 +238,8 @@
Post Parameters Post Parameters
</dt> </dt>
</div> </div>
<div v-for="(parameter, name) in currentLog.request.post" <div v-for="parameter in currentLog.request.post"
:key="'post_' + name" :key="'post_' + parameter.name"
class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> class="even:bg-gray-50 odd:bg-gray-50 px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500"> <dt class="text-sm leading-5 font-medium text-gray-500">
@{ parameter.name } @{ parameter.name }

View File

@@ -27,6 +27,9 @@ class AdminTest extends TestCase
parent::setUp(); parent::setUp();
$this->browser = new Browser($this->loop); $this->browser = new Browser($this->loop);
$this->browser = $this->browser->withOptions([
'followRedirects' => false,
]);
$this->startServer(); $this->startServer();
} }
@@ -54,11 +57,11 @@ class AdminTest extends TestCase
public function it_accepts_valid_credentials() public function it_accepts_valid_credentials()
{ {
/** @var ResponseInterface $response */ /** @var ResponseInterface $response */
$response = $this->await($this->browser->get('http://127.0.0.1:8080', [ $response = $this->await($this->browser->get('http://127.0.0.1:8080/', [
'Host' => 'expose.localhost', 'Host' => 'expose.localhost',
'Authorization' => base64_encode("username:secret"), 'Authorization' => base64_encode("username:secret"),
])); ]));
$this->assertSame(200, $response->getStatusCode()); $this->assertSame(301, $response->getStatusCode());
} }
/** @test */ /** @test */
@@ -116,6 +119,7 @@ class AdminTest extends TestCase
protected function startServer() protected function startServer()
{ {
$this->app['config']['expose.admin.subdomain'] = 'expose';
$this->app['config']['expose.admin.database'] = ':memory:'; $this->app['config']['expose.admin.database'] = ':memory:';
$this->app['config']['expose.admin.users'] = [ $this->app['config']['expose.admin.users'] = [

View File

@@ -33,6 +33,13 @@ class TunnelTest extends TestCase
$this->startServer(); $this->startServer();
} }
public function tearDown(): void
{
$this->serverFactory->getSocket()->close();
parent::tearDown();
}
/** @test */ /** @test */
public function it_returns_404_for_non_existing_clients() public function it_returns_404_for_non_existing_clients()
{ {

View File

@@ -4,7 +4,9 @@ namespace Tests\Unit;
use App\Logger\LoggedRequest; use App\Logger\LoggedRequest;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Laminas\Http\Request as LaminasRequest; use Laminas\Http\Request as LaminasRequest;
use Laminas\Http\Response as LaminasResponse;
use Tests\TestCase; use Tests\TestCase;
use function GuzzleHttp\Psr7\str; use function GuzzleHttp\Psr7\str;
@@ -22,6 +24,33 @@ class LoggedRequestTest extends TestCase
$this->assertSame('example-request', $loggedRequest->id()); $this->assertSame('example-request', $loggedRequest->id());
} }
/** @test */
public function it_returns_post_data_for_json_payloads()
{
$postData = [
'name' => 'Marcel',
'project' => 'expose'
];
$rawRequest = str(new Request(200, '/expose', [
'Content-Type' => 'application/json'
], json_encode($postData)));
$parsedRequest = LaminasRequest::fromString($rawRequest);
$loggedRequest = new LoggedRequest($rawRequest, $parsedRequest);
$this->assertSame([
[
'name' => 'name',
'value' => 'Marcel'
],
[
'name' => 'project',
'value' => 'expose'
]
], $loggedRequest->getPostData());
}
/** @test */ /** @test */
public function it_returns_the_raw_request() public function it_returns_the_raw_request()
{ {