From c71768363440bfe05abda6deada7c99afef21b6c Mon Sep 17 00:00:00 2001 From: Marcel Pociot Date: Fri, 1 May 2020 20:52:12 +0200 Subject: [PATCH] wip --- app/Contracts/ConnectionManager.php | 5 +- app/Contracts/RequestModifier.php | 11 ++++ app/Http/Controllers/Controller.php | 1 + app/Logger/LoggedRequest.php | 10 +-- app/Server/Connections/ConnectionManager.php | 8 +-- app/Server/Connections/HttpConnection.php | 32 ++++++++++ app/Server/Factory.php | 2 - .../Controllers/ControlMessageController.php | 7 ++- .../Controllers/TunnelMessageController.php | 50 +++++++++------ composer.json | 1 + composer.lock | 62 +++---------------- config/expose.php | 4 +- resources/views/client/dashboard.twig | 4 +- tests/Feature/Server/AdminTest.php | 8 ++- tests/Feature/Server/TunnelTest.php | 7 +++ tests/Unit/LoggedRequestTest.php | 29 +++++++++ 16 files changed, 145 insertions(+), 96 deletions(-) create mode 100644 app/Contracts/RequestModifier.php create mode 100644 app/Server/Connections/HttpConnection.php diff --git a/app/Contracts/ConnectionManager.php b/app/Contracts/ConnectionManager.php index d6a7e61..e3064f0 100644 --- a/app/Contracts/ConnectionManager.php +++ b/app/Contracts/ConnectionManager.php @@ -3,15 +3,16 @@ namespace App\Contracts; use App\Server\Connections\ControlConnection; +use App\Server\Connections\HttpConnection; use Ratchet\ConnectionInterface; interface ConnectionManager { 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); diff --git a/app/Contracts/RequestModifier.php b/app/Contracts/RequestModifier.php new file mode 100644 index 0000000..ba65151 --- /dev/null +++ b/app/Contracts/RequestModifier.php @@ -0,0 +1,11 @@ + $this->parsedRequest->getHeaders()->toArray(), 'body' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->parsedRequest->getContent(), 'query' => $this->parsedRequest->getQuery()->toArray(), - 'post' => $this->getPost(), + 'post' => $this->getPostData(), 'curl' => '', //(new CurlFormatter())->format(parse_request($this->rawRequest)), 'additional_data' => $this->additionalData, ], @@ -142,7 +142,7 @@ class LoggedRequest implements \JsonSerializable return $this->parsedResponse; } - protected function getPost() + public function getPostData() { $postData = []; @@ -151,7 +151,7 @@ class LoggedRequest implements \JsonSerializable switch ($contentType) { case 'application/x-www-form-urlencoded': parse_str($this->parsedRequest->getContent(), $postData); - $postData = collect($postData)->map(function ($key, $value) { + $postData = collect($postData)->map(function ($value, $key) { return [ 'name' => $key, 'value' => $value, @@ -159,12 +159,12 @@ class LoggedRequest implements \JsonSerializable })->toArray(); break; 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 [ 'name' => $key, 'value' => $value, ]; - })->toArray(); + })->values()->toArray(); break; default: diff --git a/app/Server/Connections/ConnectionManager.php b/app/Server/Connections/ConnectionManager.php index 84bb254..d182d4b 100644 --- a/app/Server/Connections/ConnectionManager.php +++ b/app/Server/Connections/ConnectionManager.php @@ -35,14 +35,14 @@ class ConnectionManager implements ConnectionManagerContract 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; } diff --git a/app/Server/Connections/HttpConnection.php b/app/Server/Connections/HttpConnection.php new file mode 100644 index 0000000..96de039 --- /dev/null +++ b/app/Server/Connections/HttpConnection.php @@ -0,0 +1,32 @@ +socket = $socket; + } + + public function send($data) + { + $this->emit('data', [$data]); + $this->socket->send($data); + } + + public function close() + { + $this->emit('close'); + + $this->socket->close(); + } +} diff --git a/app/Server/Factory.php b/app/Server/Factory.php index b6c63e1..841e940 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -9,12 +9,10 @@ use App\Server\Connections\ConnectionManager; use App\Server\Http\Controllers\Admin\DeleteUsersController; use App\Server\Http\Controllers\Admin\ListSitesController; 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\SaveSettingsController; use App\Server\Http\Controllers\Admin\ShowSettingsController; 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\TunnelMessageController; use App\Http\RouteGenerator; diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index 141f02e..7557ad3 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -51,7 +51,7 @@ class ControlMessageController implements MessageComponentInterface function onMessage(ConnectionInterface $connection, $msg) { if (isset($connection->request_id)) { - return $this->sendRequestToHttpConnection($connection->request_id, $msg); + return $this->sendResponseToHttpConnection($connection->request_id, $msg); } 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->send($request); + + $httpConnection->send($response); } protected function authenticate(ConnectionInterface $connection, $data) diff --git a/app/Server/Http/Controllers/TunnelMessageController.php b/app/Server/Http/Controllers/TunnelMessageController.php index 65c33e0..85500c7 100644 --- a/app/Server/Http/Controllers/TunnelMessageController.php +++ b/app/Server/Http/Controllers/TunnelMessageController.php @@ -6,6 +6,7 @@ use App\Contracts\ConnectionManager; use App\Http\Controllers\Controller; use App\Server\Configuration; use App\Server\Connections\ControlConnection; +use App\Server\Connections\HttpConnection; use GuzzleHttp\Psr7\Response; use Illuminate\Http\Request; use Illuminate\Pipeline\Pipeline; @@ -13,6 +14,7 @@ use Illuminate\Support\Str; use Nyholm\Psr7\Factory\Psr17Factory; use Ratchet\ConnectionInterface; use Ratchet\RFC6455\Messaging\Frame; +use React\Promise\Deferred; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use function GuzzleHttp\Psr7\str; @@ -22,13 +24,11 @@ class TunnelMessageController extends Controller protected $connectionManager; /** @var Configuration */ - private $configuration; + protected $configuration; protected $keepConnectionOpen = true; - protected $middleware = [ - - ]; + protected $modifiers = []; public function __construct(ConnectionManager $connectionManager, Configuration $configuration) { @@ -62,26 +62,38 @@ class TunnelMessageController extends Controller protected function sendRequestToClient(Request $request, ControlConnection $controlConnection, ConnectionInterface $httpConnection) { - (new Pipeline(app())) - ->send($this->prepareRequest($request, $controlConnection)) - ->through($this->middleware) - ->then(function ($request) use ($controlConnection, $httpConnection) { - $requestId = $request->header('X-Expose-Request-ID'); + $request = $this->prepareRequest($request, $controlConnection); - $this->connectionManager->storeHttpConnection($httpConnection, $requestId); + $requestId = $request->header('X-Expose-Request-ID'); - $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); + $httpConnection = $this->connectionManager->storeHttpConnection($httpConnection, $requestId); - $binaryMsg = new Frame(str($request), true, Frame::OP_BINARY); - $proxy->send($binaryMsg); - }); + transform($this->passRequestThroughModifiers($request, $httpConnection), function (Request $request) use ($controlConnection, $httpConnection, $requestId) { + $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 diff --git a/composer.json b/composer.json index e71b2fa..c162753 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "require-dev": { "bfunky/http-parser": "^2.2", "cboden/ratchet": "^0.4.2", + "clue/block-react": "^1.3", "clue/buzz-react": "^2.7", "clue/reactphp-sqlite": "^1.0", "guzzlehttp/guzzle": "^6.5", diff --git a/composer.lock b/composer.lock index 7787032..07b6015 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "801c0ab8f694ff370f2eefea409a7fcc", + "content-hash": "710c897a2bbd37c60950d540b46ba1b5", "packages": [], "packages-dev": [ { @@ -2561,21 +2561,22 @@ }, { "name": "nesbot/carbon", - "version": "2.32.2", + "version": "2.33.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc" + "reference": "4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", - "reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b", + "reference": "4d93cb95a80d9ffbff4018fe58ae3b7dd7f4b99b", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", "symfony/translation": "^3.4 || ^4.0 || ^5.0" }, "require-dev": { @@ -2628,7 +2629,7 @@ "datetime", "time" ], - "time": "2020-03-31T13:43:19+00:00" + "time": "2020-04-20T15:05:43+00:00" }, { "name": "nunomaduro/collision", @@ -5536,55 +5537,6 @@ "homepage": "https://github.com/sebastianbergmann/version", "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", "version": "v5.0.8", diff --git a/config/expose.php b/config/expose.php index fd4a515..0b916ca 100644 --- a/config/expose.php +++ b/config/expose.php @@ -1,8 +1,8 @@ 'expose.dev', - 'port' => 443, + 'host' => 'localhost', + 'port' => 8080, 'auth_token' => '', 'admin' => [ diff --git a/resources/views/client/dashboard.twig b/resources/views/client/dashboard.twig index c32bcf5..f49b405 100644 --- a/resources/views/client/dashboard.twig +++ b/resources/views/client/dashboard.twig @@ -238,8 +238,8 @@ Post Parameters -
@{ parameter.name } diff --git a/tests/Feature/Server/AdminTest.php b/tests/Feature/Server/AdminTest.php index 346886e..3b6adcc 100644 --- a/tests/Feature/Server/AdminTest.php +++ b/tests/Feature/Server/AdminTest.php @@ -27,6 +27,9 @@ class AdminTest extends TestCase parent::setUp(); $this->browser = new Browser($this->loop); + $this->browser = $this->browser->withOptions([ + 'followRedirects' => false, + ]); $this->startServer(); } @@ -54,11 +57,11 @@ class AdminTest extends TestCase public function it_accepts_valid_credentials() { /** @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', 'Authorization' => base64_encode("username:secret"), ])); - $this->assertSame(200, $response->getStatusCode()); + $this->assertSame(301, $response->getStatusCode()); } /** @test */ @@ -116,6 +119,7 @@ class AdminTest extends TestCase protected function startServer() { + $this->app['config']['expose.admin.subdomain'] = 'expose'; $this->app['config']['expose.admin.database'] = ':memory:'; $this->app['config']['expose.admin.users'] = [ diff --git a/tests/Feature/Server/TunnelTest.php b/tests/Feature/Server/TunnelTest.php index 6b83871..e3d410b 100644 --- a/tests/Feature/Server/TunnelTest.php +++ b/tests/Feature/Server/TunnelTest.php @@ -33,6 +33,13 @@ class TunnelTest extends TestCase $this->startServer(); } + public function tearDown(): void + { + $this->serverFactory->getSocket()->close(); + + parent::tearDown(); + } + /** @test */ public function it_returns_404_for_non_existing_clients() { diff --git a/tests/Unit/LoggedRequestTest.php b/tests/Unit/LoggedRequestTest.php index c90d6c3..3e7b228 100644 --- a/tests/Unit/LoggedRequestTest.php +++ b/tests/Unit/LoggedRequestTest.php @@ -4,7 +4,9 @@ namespace Tests\Unit; use App\Logger\LoggedRequest; use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; use Laminas\Http\Request as LaminasRequest; +use Laminas\Http\Response as LaminasResponse; use Tests\TestCase; use function GuzzleHttp\Psr7\str; @@ -22,6 +24,33 @@ class LoggedRequestTest extends TestCase $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 */ public function it_returns_the_raw_request() {