mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 13:35:54 +00:00
wip
This commit is contained in:
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
* text=auto
|
||||
/.github export-ignore
|
||||
.styleci.yml export-ignore
|
||||
.scrutinizer.yml export-ignore
|
||||
BACKERS.md export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/vendor
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
.phpunit.result.cache
|
||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
<p align="center">
|
||||
<img title="Laravel Zero" height="100" src="https://raw.githubusercontent.com/laravel-zero/docs/master/images/logo/laravel-zero-readme.png" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel-zero/framework/actions"><img src="https://img.shields.io/github/workflow/status/laravel-zero/framework/Continuous%20Integration.svg" alt="Build Status"></img></a>
|
||||
<a href="https://scrutinizer-ci.com/g/laravel-zero/framework"><img src="https://img.shields.io/scrutinizer/g/laravel-zero/framework.svg" alt="Quality Score"></img></a>
|
||||
<a href="https://packagist.org/packages/laravel-zero/framework"><img src="https://poser.pugx.org/laravel-zero/framework/d/total.svg" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel-zero/framework"><img src="https://poser.pugx.org/laravel-zero/framework/v/stable.svg" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel-zero/framework"><img src="https://poser.pugx.org/laravel-zero/framework/license.svg" alt="License"></a>
|
||||
</p>
|
||||
|
||||
<h4> <center>This is a <bold>community project</bold> and not an official Laravel one </center></h4>
|
||||
|
||||
Laravel Zero was created by, and is maintained by [Nuno Maduro](https://github.com/nunomaduro), and is a micro-framework that provides an elegant starting point for your console application. It is an **unofficial** and customized version of Laravel optimized for building command-line applications.
|
||||
|
||||
- Built on top of the [Laravel](https://laravel.com) components.
|
||||
- Optional installation of Laravel [Eloquent](https://laravel-zero.com/docs/database/), Laravel [Logging](https://laravel-zero.com/docs/logging/) and many others.
|
||||
- Supports interactive [menus](https://laravel-zero.com/docs/build-interactive-menus/) and [desktop notifications](https://laravel-zero.com/docs/send-desktop-notifications/) on Linux, Windows & MacOS.
|
||||
- Ships with a [Scheduler](https://laravel-zero.com/docs/task-scheduling/) and a [Standalone Compiler](https://laravel-zero.com/docs/build-a-standalone-application/).
|
||||
- Integration with [Collision](https://github.com/nunomaduro/collision) - Beautiful error reporting
|
||||
|
||||
------
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit [laravel-zero.com](https://laravel-zero.com/).
|
||||
|
||||
## Support the development
|
||||
**Do you like this project? Support it by donating**
|
||||
|
||||
- PayPal: [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L)
|
||||
- Patreon: [Donate](https://www.patreon.com/nunomaduro)
|
||||
|
||||
## License
|
||||
|
||||
Laravel Zero is an open-source software licensed under the [MIT license](https://github.com/laravel-zero/laravel-zero/blob/stable/LICENSE.md).
|
||||
39
app/Client/Client.php
Normal file
39
app/Client/Client.php
Normal 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
74
app/Client/Connection.php
Normal 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
101
app/Client/Factory.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
84
app/Client/ProxyManager.php
Normal file
84
app/Client/ProxyManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
89
app/Client/TunnelConnection.php
Normal file
89
app/Client/TunnelConnection.php
Normal 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);
|
||||
}
|
||||
}
|
||||
0
app/Commands/.gitkeep
Normal file
0
app/Commands/.gitkeep
Normal file
21
app/Commands/ServeCommand.php
Normal file
21
app/Commands/ServeCommand.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Server\Factory;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
|
||||
class ServeCommand extends Command
|
||||
{
|
||||
protected $signature = 'serve';
|
||||
|
||||
protected $description = 'Start the shaft server';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
(new Factory())
|
||||
->createServer()
|
||||
->run();
|
||||
}
|
||||
}
|
||||
24
app/Commands/ShareCommand.php
Normal file
24
app/Commands/ShareCommand.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Factory;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class ShareCommand extends Command
|
||||
{
|
||||
protected $signature = 'share {host} {--subdomain=}';
|
||||
|
||||
protected $description = 'Share a local url with a remote shaft server';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
(new Factory())
|
||||
->setLoop(app(LoopInterface::class))
|
||||
->createClient($this->argument('host'), explode(',', $this->option('subdomain')))
|
||||
->createHttpServer()
|
||||
->run();
|
||||
}
|
||||
}
|
||||
32
app/HttpServer/App.php
Normal file
32
app/HttpServer/App.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer;
|
||||
|
||||
use Ratchet\Http\Router;
|
||||
use Ratchet\Server\IoServer;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Socket\Server as Reactor;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
class App extends \Ratchet\App
|
||||
{
|
||||
public function __construct($httpHost, $port, $address, LoopInterface $loop)
|
||||
{
|
||||
$this->httpHost = $httpHost;
|
||||
$this->port = $port;
|
||||
|
||||
$socket = new Reactor($address.':'.$port, $loop);
|
||||
|
||||
$this->routes = new RouteCollection;
|
||||
|
||||
$urlMatcher = new UrlMatcher($this->routes, new RequestContext);
|
||||
|
||||
$router = new Router($urlMatcher);
|
||||
|
||||
$httpServer = new HttpServer($router);
|
||||
|
||||
$this->_server = new IoServer($httpServer, $socket, $loop);
|
||||
}
|
||||
}
|
||||
22
app/HttpServer/Controllers/Controller.php
Normal file
22
app/HttpServer/Controllers/Controller.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer\Controllers;
|
||||
|
||||
use Exception;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\Http\HttpServerInterface;
|
||||
|
||||
abstract class Controller implements HttpServerInterface
|
||||
{
|
||||
public function onClose(ConnectionInterface $connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $connection, Exception $e)
|
||||
{
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $from, $msg)
|
||||
{
|
||||
}
|
||||
}
|
||||
24
app/HttpServer/Controllers/DashboardController.php
Normal file
24
app/HttpServer/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer\Controllers;
|
||||
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
||||
{
|
||||
$connection->send(
|
||||
str(new Response(
|
||||
200,
|
||||
['Content-Type' => 'text/html'],
|
||||
file_get_contents(base_path('resources/views/index.html'))
|
||||
))
|
||||
);
|
||||
|
||||
$connection->close();
|
||||
}
|
||||
}
|
||||
28
app/HttpServer/Controllers/LogController.php
Normal file
28
app/HttpServer/Controllers/LogController.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer\Controllers;
|
||||
|
||||
use App\Logger\RequestLogger;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class LogController extends Controller
|
||||
{
|
||||
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
||||
{
|
||||
/** @var RequestLogger $logger */
|
||||
$logger = app(RequestLogger::class);
|
||||
|
||||
$connection->send(
|
||||
str(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode($logger->getData(), JSON_INVALID_UTF8_IGNORE)
|
||||
))
|
||||
);
|
||||
|
||||
$connection->close();
|
||||
}
|
||||
}
|
||||
35
app/HttpServer/Controllers/ReplayLogController.php
Normal file
35
app/HttpServer/Controllers/ReplayLogController.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer\Controllers;
|
||||
|
||||
use App\Client\TunnelConnection;
|
||||
use App\HttpServer\QueryParameters;
|
||||
use App\Logger\RequestLogger;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class ReplayLogController extends Controller
|
||||
{
|
||||
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
||||
{
|
||||
/** @var RequestLogger $logger */
|
||||
$logger = app(RequestLogger::class);
|
||||
$requestData = $logger->findLoggedRequest(QueryParameters::create($request)->get('log'))->getRequestData();
|
||||
|
||||
/** @var TunnelConnection $tunnel */
|
||||
$tunnel = app(TunnelConnection::class);
|
||||
$tunnel->performRequest($requestData);
|
||||
|
||||
$connection->send(
|
||||
str(new Response(
|
||||
200,
|
||||
['Content-Type' => 'application/json'],
|
||||
''
|
||||
))
|
||||
);
|
||||
|
||||
$connection->close();
|
||||
}
|
||||
}
|
||||
62
app/HttpServer/Controllers/StoreLogController.php
Normal file
62
app/HttpServer/Controllers/StoreLogController.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer\Controllers;
|
||||
|
||||
use Exception;
|
||||
use App\WebSockets\Socket;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use function GuzzleHttp\Psr7\str;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class StoreLogController extends Controller
|
||||
{
|
||||
|
||||
public function onOpen(ConnectionInterface $connection, RequestInterface $request = null)
|
||||
{
|
||||
$connection->contentLength = $this->findContentLength($request->getHeaders());
|
||||
|
||||
$connection->requestBuffer = (string) $request->getBody();
|
||||
|
||||
$this->checkContentLength($connection);
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $from, $msg)
|
||||
{
|
||||
$from->requestBuffer .= $msg;
|
||||
|
||||
$this->checkContentLength($from);
|
||||
}
|
||||
|
||||
protected function findContentLength(array $headers): int
|
||||
{
|
||||
return Collection::make($headers)->first(function ($values, $header) {
|
||||
return strtolower($header) === 'content-length';
|
||||
})[0] ?? 0;
|
||||
}
|
||||
|
||||
protected function checkContentLength(ConnectionInterface $connection)
|
||||
{
|
||||
if (strlen($connection->requestBuffer) === $connection->contentLength) {
|
||||
try {
|
||||
/*
|
||||
* This is the post payload from our PHPUnit tests.
|
||||
* Send it to the connected connections.
|
||||
*/
|
||||
foreach (Socket::$connections as $webSocketConnection) {
|
||||
$webSocketConnection->send($connection->requestBuffer);
|
||||
}
|
||||
|
||||
$connection->send(str(new Response(200)));
|
||||
} catch (Exception $e) {
|
||||
$connection->send(str(new Response(500, [], $e->getMessage())));
|
||||
}
|
||||
|
||||
$connection->close();
|
||||
|
||||
unset($connection->requestBuffer);
|
||||
unset($connection->contentLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/HttpServer/HttpServer.php
Normal file
15
app/HttpServer/HttpServer.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer;
|
||||
|
||||
use Ratchet\Http\HttpServerInterface;
|
||||
|
||||
class HttpServer extends \Ratchet\Http\HttpServer
|
||||
{
|
||||
public function __construct(HttpServerInterface $component)
|
||||
{
|
||||
parent::__construct($component);
|
||||
|
||||
$this->_reqParser->maxSize = 15242880;
|
||||
}
|
||||
}
|
||||
35
app/HttpServer/QueryParameters.php
Normal file
35
app/HttpServer/QueryParameters.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\HttpServer;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
class QueryParameters
|
||||
{
|
||||
/** @var \Psr\Http\Message\RequestInterface */
|
||||
protected $request;
|
||||
|
||||
public static function create(RequestInterface $request)
|
||||
{
|
||||
return new static($request);
|
||||
}
|
||||
|
||||
public function __construct(RequestInterface $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
$queryParameters = [];
|
||||
|
||||
parse_str($this->request->getUri()->getQuery(), $queryParameters);
|
||||
|
||||
return $queryParameters;
|
||||
}
|
||||
|
||||
public function get(string $name): string
|
||||
{
|
||||
return $this->all()[$name] ?? '';
|
||||
}
|
||||
}
|
||||
177
app/Logger/LoggedRequest.php
Normal file
177
app/Logger/LoggedRequest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Laminas\Http\Request;
|
||||
use Laminas\Http\Response;
|
||||
use Riverline\MultiPartParser\StreamedPart;
|
||||
|
||||
class LoggedRequest implements \JsonSerializable
|
||||
{
|
||||
/** @var string */
|
||||
protected $rawRequest;
|
||||
|
||||
/** @var Request */
|
||||
protected $parsedRequest;
|
||||
|
||||
/** @var string */
|
||||
protected $rawResponse;
|
||||
|
||||
/** @var Response */
|
||||
protected $parsedResponse;
|
||||
|
||||
/** @var string */
|
||||
protected $id;
|
||||
|
||||
/** @var Carbon */
|
||||
protected $startTime;
|
||||
|
||||
/** @var Carbon */
|
||||
protected $stopTime;
|
||||
|
||||
/** @var string */
|
||||
protected $subdomain;
|
||||
|
||||
public function __construct(string $rawRequest, Request $parsedRequest)
|
||||
{
|
||||
$this->id = (string)Str::uuid();
|
||||
$this->startTime = now();
|
||||
$this->rawRequest = $rawRequest;
|
||||
$this->parsedRequest = $parsedRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$data = [
|
||||
'id' => $this->id,
|
||||
'performed_at' => $this->startTime->toDateTimeString(),
|
||||
'duration' => $this->startTime->diffInMilliseconds($this->stopTime, false),
|
||||
'subdomain' => $this->detectSubdomain(),
|
||||
'request' => [
|
||||
'raw' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->rawRequest,
|
||||
'method' => $this->parsedRequest->getMethod(),
|
||||
'uri' => $this->parsedRequest->getUri()->getPath(),
|
||||
'headers' => $this->parsedRequest->getHeaders()->toArray(),
|
||||
'body' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->parsedRequest->getContent(),
|
||||
'query' => $this->parsedRequest->getQuery()->toArray(),
|
||||
'post' => $this->getPost(),
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->parsedResponse) {
|
||||
$data['response'] = [
|
||||
'raw' => $this->shouldReturnBody() ? $this->rawResponse : 'BINARY',
|
||||
'status' => $this->parsedResponse->getStatusCode(),
|
||||
'headers' => $this->parsedResponse->getHeaders()->toArray(),
|
||||
'reason' => $this->parsedResponse->getReasonPhrase(),
|
||||
'body' => $this->shouldReturnBody() ? $this->parsedResponse->getBody() : 'BINARY',
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function isBinary(string $string): bool
|
||||
{
|
||||
return preg_match('~[^\x20-\x7E\t\r\n]~', $string) > 0;
|
||||
}
|
||||
|
||||
protected function shouldReturnBody()
|
||||
{
|
||||
$contentType = Arr::get($this->parsedResponse->getHeaders()->toArray(), 'Content-Type');
|
||||
|
||||
return $contentType === 'application/json' || Str::is('text/*', $contentType) || Str::is('*javascript*', $contentType);
|
||||
}
|
||||
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->parsedRequest;
|
||||
}
|
||||
|
||||
public function setResponse(string $rawResponse, Response $response)
|
||||
{
|
||||
$this->parsedResponse = $response;
|
||||
|
||||
$this->rawResponse = $rawResponse;
|
||||
|
||||
$this->stopTime = now();
|
||||
}
|
||||
|
||||
public function id()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRequestData()
|
||||
{
|
||||
return $this->rawRequest;
|
||||
}
|
||||
|
||||
protected function getResponseBody()
|
||||
{
|
||||
return \Laminas\Http\Response::fromString($this->rawResponse)->getBody();
|
||||
}
|
||||
|
||||
protected function getPost()
|
||||
{
|
||||
$postData = [];
|
||||
|
||||
$contentType = Arr::get($this->parsedRequest->getHeaders()->toArray(), 'Content-Type');
|
||||
|
||||
switch ($contentType) {
|
||||
case 'application/x-www-form-urlencoded':
|
||||
parse_str($this->parsedRequest->getContent(), $postData);
|
||||
$postData = collect($postData)->map(function ($key, $value) {
|
||||
return [
|
||||
'name' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
})->toArray();
|
||||
break;
|
||||
case 'application/json':
|
||||
$postData = collect(json_decode($this->parsedRequest->getContent(), true))->map(function ($key, $value) {
|
||||
return [
|
||||
'name' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
break;
|
||||
default:
|
||||
$stream = fopen('php://temp', 'rw');
|
||||
fwrite($stream, $this->rawRequest);
|
||||
rewind($stream);
|
||||
|
||||
try {
|
||||
$document = new StreamedPart($stream);
|
||||
if ($document->isMultiPart()) {
|
||||
$postData = collect($document->getParts())->map(function (StreamedPart $part) {
|
||||
return [
|
||||
'name' => $part->getName(),
|
||||
'value' => $part->isFile() ? null : $part->getBody(),
|
||||
'is_file' => $part->isFile(),
|
||||
'filename' => $part->isFile() ? $part->getFileName() : null,
|
||||
'mime_type' => $part->isFile() ? $part->getMimeType() : null,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
//
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $postData;
|
||||
}
|
||||
|
||||
protected function detectSubdomain()
|
||||
{
|
||||
return Arr::get($this->parsedRequest->getHeaders()->toArray(), 'X-Original-Host');
|
||||
}
|
||||
}
|
||||
65
app/Logger/Logger.php
Normal file
65
app/Logger/Logger.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Logger
|
||||
{
|
||||
/** @var \Symfony\Component\Console\Output\OutputInterface */
|
||||
protected $consoleOutput;
|
||||
|
||||
/** @var bool */
|
||||
protected $enabled = false;
|
||||
|
||||
/** @var bool */
|
||||
protected $verbose = false;
|
||||
|
||||
public function __construct(OutputInterface $consoleOutput)
|
||||
{
|
||||
$this->consoleOutput = $consoleOutput;
|
||||
}
|
||||
|
||||
public function enable($enabled = true)
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function verbose($verbose = false)
|
||||
{
|
||||
$this->verbose = $verbose;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function info(string $message)
|
||||
{
|
||||
$this->line($message, 'info');
|
||||
}
|
||||
|
||||
protected function warn(string $message)
|
||||
{
|
||||
if (! $this->consoleOutput->getFormatter()->hasStyle('warning')) {
|
||||
$style = new OutputFormatterStyle('yellow');
|
||||
|
||||
$this->consoleOutput->getFormatter()->setStyle('warning', $style);
|
||||
}
|
||||
|
||||
$this->line($message, 'warning');
|
||||
}
|
||||
|
||||
protected function error(string $message)
|
||||
{
|
||||
$this->line($message, 'error');
|
||||
}
|
||||
|
||||
protected function line(string $message, string $style)
|
||||
{
|
||||
$styled = $style ? "<$style>$message</$style>" : $message;
|
||||
|
||||
$this->consoleOutput->writeln($styled);
|
||||
}
|
||||
}
|
||||
64
app/Logger/RequestLogger.php
Normal file
64
app/Logger/RequestLogger.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use Clue\React\Buzz\Browser;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Laminas\Http\Request;
|
||||
use Laminas\Http\Response;
|
||||
use function GuzzleHttp\Psr7\stream_for;
|
||||
|
||||
class RequestLogger
|
||||
{
|
||||
protected $requests = [];
|
||||
protected $responses = [];
|
||||
|
||||
public function __construct(Browser $browser)
|
||||
{
|
||||
$this->client = $browser;
|
||||
}
|
||||
|
||||
public function findLoggedRequest(string $id): ?LoggedRequest
|
||||
{
|
||||
return collect($this->requests)->first(function (LoggedRequest $loggedRequest) use ($id) {
|
||||
return $loggedRequest->id() === $id;
|
||||
});
|
||||
}
|
||||
|
||||
public function logRequest(string $rawRequest, Request $request)
|
||||
{
|
||||
array_unshift($this->requests, new LoggedRequest($rawRequest, $request));
|
||||
|
||||
$this->requests = array_slice($this->requests, 0, 10);
|
||||
|
||||
$this->pushLogs();
|
||||
}
|
||||
|
||||
public function logResponse(Request $request, string $rawResponse, Response $response)
|
||||
{
|
||||
$loggedRequest = collect($this->requests)->first(function (LoggedRequest $loggedRequest) use ($request) {
|
||||
return $loggedRequest->getRequest() === $request;
|
||||
});
|
||||
if ($loggedRequest) {
|
||||
$loggedRequest->setResponse($rawResponse, $response);
|
||||
|
||||
$this->pushLogs();
|
||||
}
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->requests;
|
||||
}
|
||||
|
||||
protected function pushLogs()
|
||||
{
|
||||
$this
|
||||
->client
|
||||
->post(
|
||||
'http://127.0.0.1:4040/logs',
|
||||
['Content-Type' => 'application/json'],
|
||||
json_encode($this->getData(), JSON_INVALID_UTF8_IGNORE)
|
||||
);
|
||||
}
|
||||
}
|
||||
29
app/Providers/AppServiceProvider.php
Normal file
29
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Logger\RequestLogger;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use React\EventLoop\Factory as LoopFactory;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(LoopInterface::class, function () {
|
||||
return LoopFactory::create();
|
||||
});
|
||||
|
||||
$this->app->singleton(RequestLogger::class, function () {
|
||||
$browser = new Browser(app(LoopInterface::class));
|
||||
return new RequestLogger($browser);
|
||||
});
|
||||
}
|
||||
}
|
||||
48
app/Server/Connections/Connection.php
Normal file
48
app/Server/Connections/Connection.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class Connection
|
||||
{
|
||||
/** @var IoConnection */
|
||||
public $socket;
|
||||
public $host;
|
||||
public $subdomain;
|
||||
public $client_id;
|
||||
public $proxies = [];
|
||||
|
||||
public function __construct(IoConnection $socket, string $host, string $subdomain, string $clientId)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->host = $host;
|
||||
$this->subdomain = $subdomain;
|
||||
$this->client_id = $clientId;
|
||||
}
|
||||
|
||||
public function setProxy(ConnectionInterface $proxy)
|
||||
{
|
||||
$this->proxies[] = $proxy;
|
||||
}
|
||||
|
||||
public function getProxy(): ?ConnectionInterface
|
||||
{
|
||||
return array_pop($this->proxies);
|
||||
}
|
||||
|
||||
public function rewriteHostInformation($serverHost, $port, string $data)
|
||||
{
|
||||
$appName = config('app.name');
|
||||
$appVersion = config('app.version');
|
||||
|
||||
return str_replace(
|
||||
"Host: {$this->subdomain}.{$serverHost}:{$port}\r\n",
|
||||
"Host: {$this->host}\r\n" .
|
||||
"X-Tunnel-By: {$appName} {$appVersion}\r\n" .
|
||||
"X-Original-Host: {$this->subdomain}.{$serverHost}:{$port}\r\n",
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
||||
60
app/Server/Connections/ConnectionManager.php
Normal file
60
app/Server/Connections/ConnectionManager.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class ConnectionManager
|
||||
{
|
||||
/** @var array */
|
||||
protected $connections = [];
|
||||
protected $host;
|
||||
protected $port;
|
||||
|
||||
public function __construct($host, $port)
|
||||
{
|
||||
$this->host = $host;
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
public function storeConnection(string $host, ?string $subdomain, IoConnection $connection)
|
||||
{
|
||||
$clientId = (string)uniqid();
|
||||
|
||||
$storedConnection = new Connection($connection, $host, $subdomain ?? $this->generateSubdomain(), $clientId);
|
||||
|
||||
$this->connections[] = $storedConnection;
|
||||
|
||||
return $storedConnection;
|
||||
}
|
||||
|
||||
public function findConnectionForSubdomain($subdomain): ?Connection
|
||||
{
|
||||
return collect($this->connections)->last(function ($connection) use ($subdomain) {
|
||||
return $connection->subdomain == $subdomain;
|
||||
});
|
||||
}
|
||||
|
||||
public function findConnectionForClientId(string $clientId): ?Connection
|
||||
{
|
||||
return collect($this->connections)->last(function ($connection) use ($clientId) {
|
||||
return $connection->client_id == $clientId;
|
||||
});
|
||||
}
|
||||
|
||||
protected function generateSubdomain(): string
|
||||
{
|
||||
return strtolower(Str::random(10));
|
||||
}
|
||||
|
||||
public function host()
|
||||
{
|
||||
return $this->host === '127.0.0.1' ? 'localhost' : $this->host;
|
||||
}
|
||||
|
||||
public function port()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
}
|
||||
45
app/Server/Connections/IoConnection.php
Normal file
45
app/Server/Connections/IoConnection.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Connections;
|
||||
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\Socket\ConnectionInterface as ReactConn;
|
||||
|
||||
class IoConnection implements ConnectionInterface {
|
||||
/**
|
||||
* @var \React\Socket\ConnectionInterface
|
||||
*/
|
||||
protected $conn;
|
||||
|
||||
|
||||
/**
|
||||
* @param \React\Socket\ConnectionInterface $conn
|
||||
*/
|
||||
public function __construct(ReactConn $conn) {
|
||||
$this->conn = $conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReactConn
|
||||
*/
|
||||
public function getConnection(): ReactConn
|
||||
{
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function send($data) {
|
||||
$this->conn->write($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close() {
|
||||
$this->conn->end();
|
||||
}
|
||||
}
|
||||
58
app/Server/Factory.php
Normal file
58
app/Server/Factory.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server;
|
||||
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use React\Socket\Server;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\EventLoop\Factory as LoopFactory;
|
||||
|
||||
class Factory
|
||||
{
|
||||
/** @var string */
|
||||
protected $host = '127.0.0.1';
|
||||
|
||||
/** @var int */
|
||||
protected $port = 8080;
|
||||
|
||||
/** @var \React\EventLoop\LoopInterface */
|
||||
protected $loop;
|
||||
|
||||
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 createServer()
|
||||
{
|
||||
$socket = new Server("{$this->host}:{$this->port}", $this->loop);
|
||||
|
||||
$connectionManager = new ConnectionManager($this->host, $this->port);
|
||||
|
||||
$app = new Shaft($connectionManager);
|
||||
|
||||
return new IoServer($app, $socket, $this->loop);
|
||||
}
|
||||
|
||||
}
|
||||
32
app/Server/IoServer.php
Normal file
32
app/Server/IoServer.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server;
|
||||
|
||||
|
||||
use App\Server\Connections\IoConnection;
|
||||
|
||||
class IoServer extends \Ratchet\Server\IoServer
|
||||
{
|
||||
public function handleConnect($conn) {
|
||||
$conn->decor = new IoConnection($conn);
|
||||
$conn->decor->resourceId = (int)$conn->stream;
|
||||
|
||||
$uri = $conn->getRemoteAddress();
|
||||
$conn->decor->remoteAddress = trim(
|
||||
parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_HOST),
|
||||
'[]'
|
||||
);
|
||||
|
||||
$this->app->onOpen($conn->decor);
|
||||
|
||||
$conn->on('data', function ($data) use ($conn) {
|
||||
$this->handleData($data, $conn);
|
||||
});
|
||||
$conn->on('close', function () use ($conn) {
|
||||
$this->handleEnd($conn);
|
||||
});
|
||||
$conn->on('error', function (\Exception $e) use ($conn) {
|
||||
$this->handleError($e, $conn);
|
||||
});
|
||||
}
|
||||
}
|
||||
60
app/Server/Messages/ControlMessage.php
Normal file
60
app/Server/Messages/ControlMessage.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Messages;
|
||||
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use stdClass;
|
||||
|
||||
class ControlMessage implements Message
|
||||
{
|
||||
/** \stdClass */
|
||||
protected $payload;
|
||||
|
||||
/** @var \Ratchet\ConnectionInterface */
|
||||
protected $connection;
|
||||
|
||||
/** @var ConnectionManager */
|
||||
protected $connectionManager;
|
||||
|
||||
public function __construct($payload, ConnectionInterface $connection, ConnectionManager $connectionManager)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
|
||||
$this->connection = $connection;
|
||||
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function respond()
|
||||
{
|
||||
$eventName = $this->payload->event;
|
||||
|
||||
if (method_exists($this, $eventName)) {
|
||||
call_user_func([$this, $eventName], $this->connection, $this->payload->data ?? new stdClass());
|
||||
}
|
||||
}
|
||||
|
||||
protected function authenticate(ConnectionInterface $connection, $data)
|
||||
{
|
||||
$connectionInfo = $this->connectionManager->storeConnection($data->host, $data->subdomain, $connection);
|
||||
|
||||
$connection->send(json_encode([
|
||||
'event' => 'authenticated',
|
||||
'subdomain' => $connectionInfo->subdomain,
|
||||
'client_id' => $connectionInfo->client_id
|
||||
]));
|
||||
}
|
||||
|
||||
protected function registerProxy(ConnectionInterface $connection, $data)
|
||||
{
|
||||
$connectionInfo = $this->connectionManager->findConnectionForClientId($data->client_id);
|
||||
|
||||
$connectionInfo->socket->getConnection()->emit('proxy_ready_'.$data->request_id, [
|
||||
$connection,
|
||||
]);
|
||||
|
||||
$connectionInfo->setProxy($connection);
|
||||
}
|
||||
}
|
||||
8
app/Server/Messages/Message.php
Normal file
8
app/Server/Messages/Message.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Messages;
|
||||
|
||||
interface Message
|
||||
{
|
||||
public function respond();
|
||||
}
|
||||
18
app/Server/Messages/MessageFactory.php
Normal file
18
app/Server/Messages/MessageFactory.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Messages;
|
||||
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class MessageFactory
|
||||
{
|
||||
public static function createForMessage(string $message, ConnectionInterface $connection, ConnectionManager $connectionManager)
|
||||
{
|
||||
$payload = json_decode($message);
|
||||
|
||||
return json_last_error() === JSON_ERROR_NONE
|
||||
? new ControlMessage($payload, $connection, $connectionManager)
|
||||
: new TunnelMessage($message, $connection, $connectionManager);
|
||||
}
|
||||
}
|
||||
96
app/Server/Messages/TunnelMessage.php
Normal file
96
app/Server/Messages/TunnelMessage.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server\Messages;
|
||||
|
||||
use App\Server\Connections\Connection;
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use App\Server\Connections\IoConnection;
|
||||
use BFunky\HttpParser\HttpRequestParser;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\Stream\Util;
|
||||
use function GuzzleHttp\Psr7\parse_request;
|
||||
|
||||
class TunnelMessage implements Message
|
||||
{
|
||||
/** string */
|
||||
protected $payload;
|
||||
|
||||
/** @var \Ratchet\ConnectionInterface */
|
||||
protected $connection;
|
||||
|
||||
/** @var ConnectionManager */
|
||||
private $connectionManager;
|
||||
|
||||
public function __construct($payload, ConnectionInterface $connection, ConnectionManager $connectionManager)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
|
||||
$this->connection = $connection;
|
||||
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function respond()
|
||||
{
|
||||
$clientConnection = $this->connectionManager->findConnectionForSubdomain($this->detectSubdomain());
|
||||
|
||||
if (is_null($clientConnection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->hasBufferedAllData()) {
|
||||
$this->copyDataToClient($clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getContentLength(): ?int
|
||||
{
|
||||
$request = parse_request($this->connection->buffer);
|
||||
|
||||
return Arr::first($request->getHeader('Content-Length'));
|
||||
}
|
||||
|
||||
protected function detectSubdomain(): ?string
|
||||
{
|
||||
$subdomain = '';
|
||||
|
||||
$headers = collect(explode("\r\n", $this->connection->buffer))->map(function ($header) use (&$subdomain) {
|
||||
$headerData = explode(':', $header);
|
||||
if ($headerData[0] === 'Host') {
|
||||
$domainParts = explode('.', $headerData[1]);
|
||||
$subdomain = trim($domainParts[0]);
|
||||
}
|
||||
});
|
||||
|
||||
return $subdomain;
|
||||
}
|
||||
|
||||
private function copyDataToClient(Connection $clientConnection)
|
||||
{
|
||||
$data = $clientConnection->rewriteHostInformation($this->connectionManager->host(), $this->connectionManager->port(), $this->connection->buffer);
|
||||
|
||||
$requestId = uniqid();
|
||||
|
||||
// Ask client to create a new proxy
|
||||
$clientConnection->socket->send(json_encode([
|
||||
'event' => 'createProxy',
|
||||
'request_id' => $requestId,
|
||||
'client_id' => $clientConnection->client_id,
|
||||
]) . "||");
|
||||
|
||||
$clientConnection->socket->getConnection()->once('proxy_ready_' . $requestId, function (IoConnection $proxy) use ($data, $requestId) {
|
||||
Util::pipe($proxy->getConnection(), $this->connection->getConnection());
|
||||
|
||||
$proxy->send($data);
|
||||
});
|
||||
|
||||
unset($this->connection->buffer);
|
||||
}
|
||||
|
||||
protected function hasBufferedAllData()
|
||||
{
|
||||
return is_null($this->getContentLength()) || strlen(Str::after($this->connection->buffer, "\r\n\r\n")) === $this->getContentLength();
|
||||
}
|
||||
}
|
||||
54
app/Server/Shaft.php
Normal file
54
app/Server/Shaft.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Server;
|
||||
|
||||
use App\Server\Connections\ConnectionManager;
|
||||
use App\Server\Messages\ControlMessage;
|
||||
use App\Server\Messages\MessageFactory;
|
||||
use App\Server\Messages\TunnelMessage;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use function GuzzleHttp\Psr7\parse_request;
|
||||
|
||||
class Shaft implements MessageComponentInterface
|
||||
{
|
||||
protected $connectionManager;
|
||||
|
||||
public function __construct(ConnectionManager $connectionManager)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function onOpen(ConnectionInterface $conn)
|
||||
{
|
||||
// TODO: Implement onOpen() method.
|
||||
}
|
||||
|
||||
public function onClose(ConnectionInterface $conn)
|
||||
{
|
||||
dump("close connection");
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $conn, \Exception $e)
|
||||
{
|
||||
// TODO: Implement onError() method.
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $connection, $message)
|
||||
{
|
||||
$payload = json_decode($message);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$message = new ControlMessage($payload, $connection, $this->connectionManager);
|
||||
$message->respond();
|
||||
} else {
|
||||
if (! isset($connection->buffer)) {
|
||||
$connection->buffer = '';
|
||||
}
|
||||
$connection->buffer .= $message;
|
||||
|
||||
$message = new TunnelMessage($connection->buffer, $connection, $this->connectionManager);
|
||||
$message->respond();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
app/WebSockets/Socket.php
Normal file
29
app/WebSockets/Socket.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\WebSockets;
|
||||
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\RFC6455\Messaging\MessageInterface;
|
||||
use Ratchet\WebSocket\MessageComponentInterface;
|
||||
|
||||
class Socket implements MessageComponentInterface
|
||||
{
|
||||
public static $connections = [];
|
||||
|
||||
public function onOpen(ConnectionInterface $connection)
|
||||
{
|
||||
self::$connections[] = $connection;
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $from, MessageInterface $msg)
|
||||
{
|
||||
}
|
||||
|
||||
public function onClose(ConnectionInterface $connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $connection, \Exception $e)
|
||||
{
|
||||
}
|
||||
}
|
||||
50
bootstrap/app.php
Executable file
50
bootstrap/app.php
Executable file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The first thing we will do is create a new Laravel application instance
|
||||
| which serves as the "glue" for all the components of Laravel, and is
|
||||
| the IoC container for the system binding all of the various parts.
|
||||
|
|
||||
*/
|
||||
|
||||
$app = new LaravelZero\Framework\Application(
|
||||
dirname(__DIR__)
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bind Important Interfaces
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, we need to bind some important interfaces into the container so
|
||||
| we will be able to resolve them when needed. The kernels serve the
|
||||
| incoming requests to this application from both the web and CLI.
|
||||
|
|
||||
*/
|
||||
|
||||
$app->singleton(
|
||||
Illuminate\Contracts\Console\Kernel::class,
|
||||
LaravelZero\Framework\Kernel::class
|
||||
);
|
||||
|
||||
$app->singleton(
|
||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
||||
Illuminate\Foundation\Exceptions\Handler::class
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Return The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This script returns the application instance. The instance is given to
|
||||
| the calling script so we can separate the building of the instances
|
||||
| from the actual running of the application and sending responses.
|
||||
|
|
||||
*/
|
||||
|
||||
return $app;
|
||||
18
box.json
Normal file
18
box.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"chmod": "0755",
|
||||
"directories": [
|
||||
"app",
|
||||
"bootstrap",
|
||||
"config",
|
||||
"vendor"
|
||||
],
|
||||
"files": [
|
||||
"composer.json"
|
||||
],
|
||||
"exclude-composer-files": false,
|
||||
"compression": "GZ",
|
||||
"compactors": [
|
||||
"KevinGH\\Box\\Compactor\\Php",
|
||||
"KevinGH\\Box\\Compactor\\Json"
|
||||
]
|
||||
}
|
||||
57
composer.json
Normal file
57
composer.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "laravel-zero/laravel-zero",
|
||||
"description": "The Laravel Zero Framework.",
|
||||
"keywords": ["framework", "laravel", "laravel zero", "console", "cli"],
|
||||
"homepage": "https://laravel-zero.com",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel-zero/laravel-zero/issues",
|
||||
"source": "https://github.com/laravel-zero/laravel-zero"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"ext-json": "*",
|
||||
"bfunky/http-parser": "^2.2",
|
||||
"cboden/ratchet": "^0.4.2",
|
||||
"clue/buzz-react": "^2.7",
|
||||
"guzzlehttp/guzzle": "^6.5",
|
||||
"laminas/laminas-http": "^2.11",
|
||||
"laravel-zero/framework": "^7.0",
|
||||
"react/socket": "^1.4",
|
||||
"riverline/multipart-parser": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.3.1",
|
||||
"phpunit/phpunit": "^8.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
"scripts": {
|
||||
"post-create-project-cmd": [
|
||||
"@php application app:rename"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"bin": ["phunnel"]
|
||||
}
|
||||
5851
composer.lock
generated
Normal file
5851
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
config/app.php
Normal file
60
config/app.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application. This value is used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| any other location as required by the application or its packages.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => 'Phunnel',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "version" your application is currently running
|
||||
| in. You may want to follow the "Semantic Versioning" - Given a version
|
||||
| number MAJOR.MINOR.PATCH when an update happens: https://semver.org.
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => app('git.version'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. This can be overridden using
|
||||
| the global command line "--env" option when calling commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => 'development',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The service providers listed here will be automatically loaded on the
|
||||
| request to your application. Feel free to add your own services to
|
||||
| this array to grant expanded functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
],
|
||||
|
||||
];
|
||||
80
config/commands.php
Normal file
80
config/commands.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Command
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel Zero will always run the command specified below when no command name is
|
||||
| provided. Consider update the default command for single command applications.
|
||||
| You cannot pass arguments to the default command because they are ignored.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => NunoMaduro\LaravelConsoleSummary\SummaryCommand::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commands Paths
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "paths" that should be loaded by the console's
|
||||
| kernel. Foreach "path" present on the array provided below the kernel
|
||||
| will extract all "Illuminate\Console\Command" based class commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => [app_path('Commands')],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Added Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may want to include a single command class without having to load an
|
||||
| entire folder. Here you can specify which commands should be added to
|
||||
| your list of commands. The console's kernel will try to load them.
|
||||
|
|
||||
*/
|
||||
|
||||
'add' => [
|
||||
// ..
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Hidden Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Your application commands will always be visible on the application list
|
||||
| of commands. But you can still make them "hidden" specifying an array
|
||||
| of commands below. All "hidden" commands can still be run/executed.
|
||||
|
|
||||
*/
|
||||
|
||||
'hidden' => [
|
||||
NunoMaduro\LaravelConsoleSummary\SummaryCommand::class,
|
||||
Symfony\Component\Console\Command\HelpCommand::class,
|
||||
Illuminate\Console\Scheduling\ScheduleRunCommand::class,
|
||||
Illuminate\Console\Scheduling\ScheduleFinishCommand::class,
|
||||
Illuminate\Foundation\Console\VendorPublishCommand::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Removed Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Do you have a service provider that loads a list of commands that
|
||||
| you don't need? No problem. Laravel Zero allows you to specify
|
||||
| below a list of commands that you don't to see in your app.
|
||||
|
|
||||
*/
|
||||
|
||||
'remove' => [
|
||||
// ..
|
||||
],
|
||||
|
||||
];
|
||||
24
phpunit.xml.dist
Normal file
24
phpunit.xml.dist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false">
|
||||
<testsuites>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">./tests/Unit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">./app</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
||||
53
phunnel
Executable file
53
phunnel
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composer provides a convenient, automatically generated class loader
|
||||
| for our application. We just need to utilize it! We'll require it
|
||||
| into the script here so that we do not have to worry about the
|
||||
| loading of any our classes "manually". Feels great to relax.
|
||||
|
|
||||
*/
|
||||
|
||||
$autoloader = require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php';
|
||||
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Run The Artisan Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When we run the console application, the current CLI command will be
|
||||
| executed in this console and the response sent back to a terminal
|
||||
| or another output device for the developers. Here goes nothing!
|
||||
|
|
||||
*/
|
||||
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
|
||||
$status = $kernel->handle(
|
||||
$input = new Symfony\Component\Console\Input\ArgvInput,
|
||||
new Symfony\Component\Console\Output\ConsoleOutput
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Shutdown The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Once Artisan has finished running, we will fire off the shutdown events
|
||||
| so that any final work may be done by the application before we shut
|
||||
| down the process. This is the last thing to happen to the request.
|
||||
|
|
||||
*/
|
||||
|
||||
$kernel->terminate($input, $status);
|
||||
|
||||
exit($status);
|
||||
243
resources/views/index.html
Normal file
243
resources/views/index.html
Normal file
@@ -0,0 +1,243 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tailwindcss/ui@latest/dist/tailwind-ui.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js?skin=sunburst"></script>
|
||||
<script>
|
||||
!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a});
|
||||
</script>
|
||||
<style>
|
||||
.even\:bg-gray-50:nth-child(even) {
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="p-5 flex flex-row">
|
||||
<div class="w-1/3 flex flex-col mr-5">
|
||||
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
URL
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Response
|
||||
</th>
|
||||
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Duration
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
<tr v-for="log in logs"
|
||||
:class="{'bg-gray-100': currentLog === log}"
|
||||
@click="setLog(log)">
|
||||
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 font-medium text-gray-900">
|
||||
<p>
|
||||
{{ log.request.method }}
|
||||
{{ log.request.uri }}
|
||||
</p>
|
||||
<span class="text-xs">{{ log.subdomain }}</span>
|
||||
</td>
|
||||
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
<div v-if="log.response">
|
||||
{{ log.response.status }} - {{ log.response.reason }}
|
||||
</div>
|
||||
<div v-else>
|
||||
...
|
||||
</div>
|
||||
</td>
|
||||
<td class="cursor.pointer px-6 py-4 whitespace-no-wrap border-b border-gray-200 text-sm leading-5 text-gray-500">
|
||||
<div v-if="log.response">
|
||||
{{ log.duration }}ms
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-2/3 ml-5">
|
||||
<div v-if="currentLog" class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 flex">
|
||||
{{ currentLog.request.method }} {{ currentLog.request.uri }}
|
||||
<div class="flex-grow"></div>
|
||||
<span class="inline-flex rounded-md shadow-sm">
|
||||
<button @click.prevent="replay(currentLog)"
|
||||
type="button" class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 text-xs leading-4 font-medium rounded text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:text-gray-800 active:bg-gray-50 transition ease-in-out duration-150">
|
||||
Replay
|
||||
</button>
|
||||
</span>
|
||||
</h3>
|
||||
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||
Status code: {{ currentLog.response?.status}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="hidden sm:block">
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex">
|
||||
<a href="#"
|
||||
@click.prevent="setView('request')"
|
||||
:class="{
|
||||
'border-indigo-500 text-indigo-600 text-indigo-600 focus:text-indigo-800 focus:border-indigo-700': view === 'request',
|
||||
'border-border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300': view === 'response',
|
||||
}"
|
||||
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm leading-5 focus:outline-none">
|
||||
Request
|
||||
</a>
|
||||
<a href="#"
|
||||
@click.prevent="setView('response')"
|
||||
:class="{
|
||||
'border-indigo-500 text-indigo-600': view === 'response',
|
||||
'border-border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300': view === 'request',
|
||||
}"
|
||||
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm leading-5 focus:outline-none">
|
||||
Response
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="view === 'request'">
|
||||
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-900">
|
||||
Query Parameters
|
||||
</dt>
|
||||
</div>
|
||||
<div v-for="(value, name) in currentLog.request.query"
|
||||
:key="'query_' + 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">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||
{{ name }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
{{ value }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-900">
|
||||
Post Parameters
|
||||
</dt>
|
||||
</div>
|
||||
<div v-for="(parameter, name) in currentLog.request.post"
|
||||
:key="'post_' + 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">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||
{{ parameter.name }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
<span v-if="parameter.is_file">File: {{ parameter.filename }} ({{ parameter.mime_type }})</span>
|
||||
<span v-else>{{ parameter.value }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-900">
|
||||
Headers
|
||||
</dt>
|
||||
</div>
|
||||
<div v-for="(value, header) in currentLog.request.headers"
|
||||
:key="header"
|
||||
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">
|
||||
{{ header }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
{{ value }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<pre class="p-6 prettyprint">{{ currentLog.request.body }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="view === 'response'">
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm leading-5 font-medium text-gray-900">
|
||||
Headers
|
||||
</dt>
|
||||
</div>
|
||||
<div v-for="(value, header) in currentLog.response.headers"
|
||||
:key="header"
|
||||
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">
|
||||
{{ header }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||
{{ value }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<pre class="p-6 prettyprint">{{ currentLog.response.body }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#app',
|
||||
|
||||
data: {
|
||||
currentLog: null,
|
||||
view: 'request',
|
||||
logs: [],
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLog: function(log) {
|
||||
this.currentLog = log;
|
||||
|
||||
this.$nextTick(function(){
|
||||
PR.prettyPrint();
|
||||
});
|
||||
},
|
||||
setView: function(view) {
|
||||
this.view = view;
|
||||
|
||||
this.$nextTick(function(){
|
||||
PR.prettyPrint();
|
||||
});
|
||||
},
|
||||
replay: function(log) {
|
||||
fetch('/replay/'+log.id);
|
||||
},
|
||||
connect: function() {
|
||||
let conn = new ReconnectingWebSocket(`ws://${window.location.hostname}:${window.location.port}/socket`);
|
||||
|
||||
conn.onmessage = (e) => {
|
||||
this.logs = JSON.parse(e.data);
|
||||
};
|
||||
},
|
||||
loadLogs: function(log) {
|
||||
fetch('/logs')
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.logs = data;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.connect();
|
||||
|
||||
this.loadLogs();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
tests/CreatesApplication.php
Executable file
22
tests/CreatesApplication.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
trait CreatesApplication
|
||||
{
|
||||
/**
|
||||
* Creates the application.
|
||||
*
|
||||
* @return \Illuminate\Foundation\Application
|
||||
*/
|
||||
public function createApplication()
|
||||
{
|
||||
$app = require __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->make(Kernel::class)->bootstrap();
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
20
tests/Feature/InspiringCommandTest.php
Executable file
20
tests/Feature/InspiringCommandTest.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class InspiringCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testInspiringCommand()
|
||||
{
|
||||
$this->artisan('inspiring')
|
||||
->expectsOutput('Simplicity is the ultimate sophistication.')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
}
|
||||
10
tests/TestCase.php
Executable file
10
tests/TestCase.php
Executable file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use LaravelZero\Framework\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
use CreatesApplication;
|
||||
}
|
||||
18
tests/Unit/ExampleTest.php
Normal file
18
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testBasicTest()
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user