This commit is contained in:
Marcel Pociot
2020-04-14 21:19:23 +02:00
commit 2b03398f40
48 changed files with 8099 additions and 0 deletions

39
app/Client/Client.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Client;
use React\EventLoop\LoopInterface;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
class Client
{
/** @var LoopInterface */
protected $loop;
protected $host;
protected $port;
public function __construct(LoopInterface $loop, $host, $port)
{
$this->loop = $loop;
$this->host = $host;
$this->port = $port;
}
public function share($sharedUrl, array $subdomains = [])
{
foreach ($subdomains as $subdomain) {
$connector = new Connector($this->loop);
$connector->connect("{$this->host}:{$this->port}")
->then(function (ConnectionInterface $clientConnection) use ($sharedUrl, $subdomain) {
$connection = Connection::create($clientConnection, new ProxyManager($this->host, $this->port, $this->loop));
$connection->authenticate($sharedUrl, $subdomain);
$clientConnection->on('authenticated', function ($data) {
dump("Connected to http://$data->subdomain.{$this->host}:{$this->port}");
});
});
}
}
}

74
app/Client/Connection.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
namespace App\Client;
use Throwable;
use React\Socket\ConnectionInterface;
class Connection
{
/** @var ConnectionInterface */
protected $socket;
/** @var ProxyManager */
protected $proxyManager;
public static function create(ConnectionInterface $socketConnection, ProxyManager $proxyManager)
{
return new static($socketConnection, $proxyManager);
}
public function __construct(ConnectionInterface $socketConnection, ProxyManager $proxyManager)
{
$this->socket = $socketConnection;
$this->proxyManager = $proxyManager;
$this->socket->on('data', function ($data) {
$jsonStrings = explode("||", $data);
$decodedEntries = [];
foreach ($jsonStrings as $jsonString) {
try {
$decodedJsonObject = json_decode($jsonString);
if (is_object($decodedJsonObject)) {
$decodedEntries[] = $decodedJsonObject;
}
} catch (Throwable $e) {
// Ignore payload
}
}
foreach ($decodedEntries as $decodedEntry) {
if (method_exists($this, $decodedEntry->event ?? '')) {
$this->socket->emit($decodedEntry->event, [$decodedEntry]);
call_user_func([$this, $decodedEntry->event], $decodedEntry);
}
}
});
}
public function authenticated($data)
{
$this->socket->_id = $data->client_id;
$this->createProxy($data);
}
public function createProxy($data)
{
$this->proxyManager->createProxy($this->socket, $data);
}
public function authenticate(string $sharedHost, string $subdomain)
{
$this->socket->write(json_encode([
'event' => 'authenticate',
'data' => [
'host' => $sharedHost,
'subdomain' => empty($subdomain) ? null : $subdomain,
],
]));
}
}

101
app/Client/Factory.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
namespace App\Client;
use App\HttpServer\App;
use App\HttpServer\Controllers\DashboardController;
use App\HttpServer\Controllers\LogController;
use App\HttpServer\Controllers\ReplayLogController;
use App\HttpServer\Controllers\StoreLogController;
use App\WebSockets\Socket;
use Ratchet\WebSocket\WsServer;
use React\EventLoop\LoopInterface;
use Symfony\Component\Routing\Route;
use React\EventLoop\Factory as LoopFactory;
class Factory
{
/** @var string */
protected $host = 'localhost';
/** @var int */
protected $port = 8080;
/** @var \React\EventLoop\LoopInterface */
protected $loop;
/** @var App */
protected $app;
public function __construct()
{
$this->loop = LoopFactory::create();
}
public function setHost(string $host)
{
$this->host = $host;
return $this;
}
public function setPort(int $port)
{
$this->port = $port;
return $this;
}
public function setLoop(LoopInterface $loop)
{
$this->loop = $loop;
return $this;
}
public function createClient($sharedUrl, $subdomain = null)
{
$client = new Client($this->loop, $this->host, $this->port);
$client->share($sharedUrl, $subdomain);
return $this;
}
protected function addRoutes()
{
$dashboardRoute = new Route('/', ['_controller' => new DashboardController()], [], [], null, [], ['GET']);
$logRoute = new Route('/logs', ['_controller' => new LogController()], [], [], null, [], ['GET']);
$storeLogRoute = new Route('/logs', ['_controller' => new StoreLogController()], [], [], null, [], ['POST']);
$replayLogRoute = new Route('/replay/{log}', ['_controller' => new ReplayLogController()], [], [], null, [], ['GET']);
$this->app->route('/socket', new WsServer(new Socket()), ['*']);
$this->app->routes->add('dashboard', $dashboardRoute);
$this->app->routes->add('logs', $logRoute);
$this->app->routes->add('storeLogs', $storeLogRoute);
$this->app->routes->add('replayLog', $replayLogRoute);
}
public function createHttpServer()
{
$this->loop->futureTick(function () {
$dashboardUrl = 'http://127.0.0.1:4040/';
echo('Started Dashboard on port 4040'. PHP_EOL);
echo('If the dashboard does not automatically open, visit: '.$dashboardUrl . PHP_EOL);
});
$this->app = new App('127.0.0.1', 4040, '0.0.0.0', $this->loop);
$this->addRoutes();
return $this;
}
public function run()
{
$this->loop->run();
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Client;
use App\Logger\RequestLogger;
use BFunky\HttpParser\HttpRequestParser;
use BFunky\HttpParser\HttpResponseParser;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Stream\ThroughStream;
use React\Stream\Util;
use React\Stream\WritableResourceStream;
use GuzzleHttp\Psr7 as gPsr;
use function GuzzleHttp\Psr7\parse_request;
class ProxyManager
{
private $host;
private $port;
private $loop;
public function __construct($host, $port, $loop)
{
$this->host = $host;
$this->port = $port;
$this->loop = $loop;
}
public function createProxy(ConnectionInterface $clientConnection, $connectionData)
{
$connector = new Connector($this->loop);
$connector->connect("{$this->host}:{$this->port}")->then(function (ConnectionInterface $proxyConnection) use ($clientConnection, $connector, $connectionData) {
$proxyConnection->write(json_encode([
'event' => 'registerProxy',
'data' => [
'request_id' => $connectionData->request_id ?? null,
'client_id' => $clientConnection->_id,
],
]));
$proxyConnection->on('data', function ($data) use (&$proxyData, $proxyConnection, $connector) {
if (!isset($proxyConnection->buffer)) {
$proxyConnection->buffer = '';
}
$proxyConnection->buffer .= $data;
if ($this->hasBufferedAllData($proxyConnection)) {
$tunnel = app(TunnelConnection::class);
$tunnel->performRequest($proxyConnection->buffer, $proxyConnection);
}
});
});
}
private function parseResponse(string $response)
{
try {
return gPsr\parse_response($response);
} catch (\Throwable $e) {
return null;
}
}
private function parseRequest($data)
{
return gPsr\parse_request($data);
}
protected function getContentLength($proxyConnection): ?int
{
$request = parse_request($proxyConnection->buffer);
return Arr::first($request->getHeader('Content-Length'));
}
protected function hasBufferedAllData($proxyConnection)
{
return is_null($this->getContentLength($proxyConnection)) || strlen(Str::after($proxyConnection->buffer, "\r\n\r\n")) >= $this->getContentLength($proxyConnection);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Client;
use App\Logger\RequestLogger;
use Laminas\Http\Request;
use Laminas\Http\Response;
use React\EventLoop\LoopInterface;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Stream\Util;
class TunnelConnection
{
/** @var LoopInterface */
protected $loop;
/** @var RequestLogger */
protected $logger;
protected $request;
public function __construct(LoopInterface $loop, RequestLogger $logger)
{
$this->loop = $loop;
$this->logger = $logger;
}
public function performRequest($requestData, ConnectionInterface $proxyConnection = null)
{
$this->request = $this->parseRequest($requestData);
$this->logger->logRequest($requestData, $this->request);
dump($this->request->getMethod() . ' ' . $this->request->getUri()->getPath());
if (! is_null($proxyConnection)) {
$proxyConnection->pause();
}
(new Connector($this->loop))
->connect("localhost:80")
->then(function (ConnectionInterface $connection) use ($requestData, $proxyConnection) {
$connection->on('data', function ($data) use (&$chunks, &$contentLength, $connection, $proxyConnection) {
if (!isset($connection->httpBuffer)) {
$connection->httpBuffer = '';
}
$connection->httpBuffer .= $data;
try {
$response = $this->parseResponse($connection->httpBuffer);
$this->logger->logResponse($this->request, $connection->httpBuffer, $response);
unset($connection->httpBuffer);
} catch (\Throwable $e) {
//
}
});
if (! is_null($proxyConnection)) {
Util::pipe($connection, $proxyConnection, ['end' => true]);
}
$connection->write($requestData);
if (! is_null($proxyConnection)) {
$proxyConnection->resume();
unset($proxyConnection->buffer);
}
});
}
protected function parseResponse(string $response)
{
try {
return Response::fromString($response);
} catch (\Throwable $e) {
return null;
}
}
protected function parseRequest($data)
{
return Request::fromString($data);
}
}