This commit is contained in:
Marcel Pociot
2020-04-17 16:13:15 +02:00
parent 2778d5a489
commit 60727ac86e
5 changed files with 127 additions and 33 deletions

View File

@@ -3,12 +3,15 @@
namespace App\Client; namespace App\Client;
use App\Logger\RequestLogger; use App\Logger\RequestLogger;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Laminas\Http\Request; use Laminas\Http\Request;
use Laminas\Http\Response; use Laminas\Http\Response;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\Socket\ConnectionInterface; use React\Socket\ConnectionInterface;
use React\Socket\Connector; use React\Socket\Connector;
use React\Stream\Util; use React\Stream\Util;
use function GuzzleHttp\Psr7\str;
class TunnelConnection class TunnelConnection
{ {
@@ -17,6 +20,8 @@ class TunnelConnection
/** @var RequestLogger */ /** @var RequestLogger */
protected $logger; protected $logger;
/** @var Request */
protected $request; protected $request;
public function __construct(LoopInterface $loop, RequestLogger $logger) public function __construct(LoopInterface $loop, RequestLogger $logger)
@@ -25,6 +30,11 @@ class TunnelConnection
$this->logger = $logger; $this->logger = $logger;
} }
protected function requiresAuthentication(): bool
{
return !empty($this->getCredentials());
}
public function performRequest($requestData, ConnectionInterface $proxyConnection = null) public function performRequest($requestData, ConnectionInterface $proxyConnection = null)
{ {
$this->request = $this->parseRequest($requestData); $this->request = $this->parseRequest($requestData);
@@ -33,8 +43,17 @@ class TunnelConnection
dump($this->request->getMethod() . ' ' . $this->request->getUri()->getPath()); dump($this->request->getMethod() . ' ' . $this->request->getUri()->getPath());
if (! is_null($proxyConnection)) { if ($this->requiresAuthentication() && !is_null($proxyConnection)) {
$proxyConnection->pause(); $username = $this->getAuthorizationUsername();
if (is_null($username)) {
$proxyConnection->write(
str(new \GuzzleHttp\Psr7\Response(401, [
'WWW-Authenticate' => 'Basic realm=Expose'
], 'Unauthorized'))
);
$proxyConnection->end();
return;
}
} }
(new Connector($this->loop)) (new Connector($this->loop))
@@ -47,32 +66,38 @@ class TunnelConnection
$connection->httpBuffer .= $data; $connection->httpBuffer .= $data;
try { $response = $this->parseResponse($connection->httpBuffer);
$response = $this->parseResponse($connection->httpBuffer);
if (! is_null($response) && $this->hasBufferedAllData($connection)) {
$this->logger->logResponse($this->request, $connection->httpBuffer, $response); $this->logger->logResponse($this->request, $connection->httpBuffer, $response);
if (! is_null($proxyConnection)) {
$proxyConnection->write($connection->httpBuffer);
}
unset($proxyConnection->buffer);
unset($connection->httpBuffer); unset($connection->httpBuffer);
} catch (\Throwable $e) {
//
} }
}); });
if (! is_null($proxyConnection)) {
Util::pipe($connection, $proxyConnection, ['end' => true]);
}
$connection->write($requestData); $connection->write($requestData);
if (! is_null($proxyConnection)) {
$proxyConnection->resume();
unset($proxyConnection->buffer);
}
}); });
} }
protected function getContentLength($connection): ?int
{
$response = $this->parseResponse($connection->httpBuffer);
return Arr::get($response->getHeaders()->toArray(), 'Content-Length');
}
protected function hasBufferedAllData($connection)
{
return is_null($this->getContentLength($connection)) || strlen(Str::after($connection->httpBuffer, "\r\n\r\n")) === $this->getContentLength($connection);
}
protected function parseResponse(string $response) protected function parseResponse(string $response)
{ {
try { try {
@@ -82,8 +107,60 @@ class TunnelConnection
} }
} }
protected function parseRequest($data) protected function parseRequest($data): Request
{ {
return Request::fromString($data); return Request::fromString($data);
} }
protected function getCredentials()
{
try {
$credentials = explode(':', $GLOBALS['expose.auth']);
return [
$credentials[0] => $credentials[1],
];
} catch (\Exception $e) {
return [];
}
}
protected function getAuthorizationUsername(): ?string
{
$authorization = $this->parseAuthorizationHeader(Arr::get($this->request->getHeaders()->toArray(), 'Authorization', ''));
$credentials = $this->getCredentials();
if (empty($authorization)) {
return null;
}
if (!array_key_exists($authorization['username'], $credentials)) {
return null;
}
if ($credentials[$authorization['username']] !== $authorization['password']) {
return null;
}
return $authorization['username'];
}
protected function parseAuthorizationHeader(string $header)
{
if (strpos($header, 'Basic') !== 0) {
return null;
}
$header = base64_decode(substr($header, 6));
if ($header === false) {
return null;
}
$header = explode(':', $header, 2);
return [
'username' => $header[0],
'password' => isset($header[1]) ? $header[1] : null,
];
}
} }

View File

@@ -3,7 +3,6 @@
namespace App\Commands; namespace App\Commands;
use App\Server\Factory; use App\Server\Factory;
use Illuminate\Console\Scheduling\Schedule;
use LaravelZero\Framework\Commands\Command; use LaravelZero\Framework\Commands\Command;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;

View File

@@ -9,16 +9,20 @@ use React\EventLoop\LoopInterface;
class ShareCommand extends Command class ShareCommand extends Command
{ {
protected $signature = 'share {host} {--subdomain=}'; protected $signature = 'share {host} {--subdomain=} {--auth=}';
protected $description = 'Share a local url with a remote shaft server'; protected $description = 'Share a local url with a remote shaft server';
public function handle() public function handle()
{ {
if ($this->option('auth')) {
$GLOBALS['expose.auth'] = $this->option('auth');
}
(new Factory()) (new Factory())
->setLoop(app(LoopInterface::class)) ->setLoop(app(LoopInterface::class))
->setHost('beyond.sh') // ->setHost('beyond.sh') // TODO: Read from (local/global) config file
->setPort(8080) // ->setPort(8080) // TODO: Read from (local/global) config file
->createClient($this->argument('host'), explode(',', $this->option('subdomain'))) ->createClient($this->argument('host'), explode(',', $this->option('subdomain')))
->createHttpServer() ->createHttpServer()
->run(); ->run();

View File

@@ -6,6 +6,7 @@ use App\Server\Connections\Connection;
use App\Server\Connections\ConnectionManager; use App\Server\Connections\ConnectionManager;
use App\Server\Connections\IoConnection; use App\Server\Connections\IoConnection;
use BFunky\HttpParser\HttpRequestParser; use BFunky\HttpParser\HttpRequestParser;
use GuzzleHttp\Psr7\Response;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
@@ -37,6 +38,9 @@ class TunnelMessage implements Message
$clientConnection = $this->connectionManager->findConnectionForSubdomain($this->detectSubdomain()); $clientConnection = $this->connectionManager->findConnectionForSubdomain($this->detectSubdomain());
if (is_null($clientConnection)) { if (is_null($clientConnection)) {
// $this->connection->send(\GuzzleHttp\Psr7\str(new Response(404, [], 'Not found')));
// $this->connection->close();
// dump("No clinet connection");
return; return;
} }

View File

@@ -100,13 +100,19 @@
<div class="p-5 flex flex-row"> <div class="p-5 flex flex-row">
<div class="w-1/3 flex flex-col mr-5"> <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="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<span class="inline-flex rounded-md shadow-sm mb-4"> <div class="flex mb-4">
<span class="h-8 inline-flex rounded-md shadow-sm">
<button @click.prevent="clearLogs" <button @click.prevent="clearLogs"
type="button" 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"> 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">
Clear Clear
</button> </button>
</span> </span>
<div class="ml-4 flex-grow relative rounded-md shadow-sm">
<input class="h-8 form-input block w-full sm:text-sm sm:leading-5" v-model="search" placeholder="Search" />
</div>
</div>
<div <div
class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200"> class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
<table class="min-w-full"> <table class="min-w-full">
@@ -124,7 +130,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="bg-white"> <tbody class="bg-white">
<tr v-for="log in logs" <tr v-for="log in filteredLogs"
:class="{'bg-gray-100': currentLog === log}" :class="{'bg-gray-100': currentLog === log}"
@click="setLog(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"> <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">
@@ -302,15 +308,7 @@
Response Response
</dt> </dt>
<div> <div>
<div class="sm:hidden"> <div>
<select class="form-select block w-full">
<option>My Account</option>
<option>Company</option>
<option selected>Team Members</option>
<option>Billing</option>
</select>
</div>
<div class="hidden sm:block">
<nav class="flex"> <nav class="flex">
<a href="#" <a href="#"
@click.prevent="setActiveTab('raw')" @click.prevent="setActiveTab('raw')"
@@ -345,12 +343,24 @@
el: '#app', el: '#app',
data: { data: {
search: '',
currentLog: null, currentLog: null,
view: 'request', view: 'request',
activeTab: 'raw', activeTab: 'raw',
logs: [], logs: [],
}, },
computed: {
filteredLogs: function() {
if (this.search === '') {
return this.logs;
}
return this.logs.filter(log => {
return log.request.uri.indexOf(this.search) !== -1;
});
},
},
methods: { methods: {
setActiveTab: function(tab) { setActiveTab: function(tab) {
this.activeTab = tab; this.activeTab = tab;