mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-13 21:45:55 +00:00
wip
This commit is contained in:
@@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user