diff --git a/app/Client/TunnelConnection.php b/app/Client/TunnelConnection.php index 2541e55..beee5c9 100644 --- a/app/Client/TunnelConnection.php +++ b/app/Client/TunnelConnection.php @@ -3,12 +3,15 @@ namespace App\Client; use App\Logger\RequestLogger; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Laminas\Http\Request; use Laminas\Http\Response; use React\EventLoop\LoopInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Stream\Util; +use function GuzzleHttp\Psr7\str; class TunnelConnection { @@ -17,6 +20,8 @@ class TunnelConnection /** @var RequestLogger */ protected $logger; + + /** @var Request */ protected $request; public function __construct(LoopInterface $loop, RequestLogger $logger) @@ -25,6 +30,11 @@ class TunnelConnection $this->logger = $logger; } + protected function requiresAuthentication(): bool + { + return !empty($this->getCredentials()); + } + public function performRequest($requestData, ConnectionInterface $proxyConnection = null) { $this->request = $this->parseRequest($requestData); @@ -33,8 +43,17 @@ class TunnelConnection dump($this->request->getMethod() . ' ' . $this->request->getUri()->getPath()); - if (! is_null($proxyConnection)) { - $proxyConnection->pause(); + if ($this->requiresAuthentication() && !is_null($proxyConnection)) { + $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)) @@ -47,32 +66,38 @@ class TunnelConnection $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); + if (! is_null($proxyConnection)) { + $proxyConnection->write($connection->httpBuffer); + } + + unset($proxyConnection->buffer); + 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 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) { try { @@ -82,8 +107,60 @@ class TunnelConnection } } - protected function parseRequest($data) + protected function parseRequest($data): Request { 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, + ]; + } } diff --git a/app/Commands/ServeCommand.php b/app/Commands/ServeCommand.php index a9cf40f..f4d63fa 100644 --- a/app/Commands/ServeCommand.php +++ b/app/Commands/ServeCommand.php @@ -3,7 +3,6 @@ namespace App\Commands; use App\Server\Factory; -use Illuminate\Console\Scheduling\Schedule; use LaravelZero\Framework\Commands\Command; use React\EventLoop\LoopInterface; diff --git a/app/Commands/ShareCommand.php b/app/Commands/ShareCommand.php index a9b7b40..80bc690 100644 --- a/app/Commands/ShareCommand.php +++ b/app/Commands/ShareCommand.php @@ -9,16 +9,20 @@ use React\EventLoop\LoopInterface; 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'; public function handle() { + if ($this->option('auth')) { + $GLOBALS['expose.auth'] = $this->option('auth'); + } + (new Factory()) ->setLoop(app(LoopInterface::class)) - ->setHost('beyond.sh') - ->setPort(8080) +// ->setHost('beyond.sh') // TODO: Read from (local/global) config file +// ->setPort(8080) // TODO: Read from (local/global) config file ->createClient($this->argument('host'), explode(',', $this->option('subdomain'))) ->createHttpServer() ->run(); diff --git a/app/Server/Messages/TunnelMessage.php b/app/Server/Messages/TunnelMessage.php index c176cea..d22cdb5 100644 --- a/app/Server/Messages/TunnelMessage.php +++ b/app/Server/Messages/TunnelMessage.php @@ -6,6 +6,7 @@ use App\Server\Connections\Connection; use App\Server\Connections\ConnectionManager; use App\Server\Connections\IoConnection; use BFunky\HttpParser\HttpRequestParser; +use GuzzleHttp\Psr7\Response; use Illuminate\Support\Arr; use Illuminate\Support\Str; use Ratchet\ConnectionInterface; @@ -37,6 +38,9 @@ class TunnelMessage implements Message $clientConnection = $this->connectionManager->findConnectionForSubdomain($this->detectSubdomain()); if (is_null($clientConnection)) { +// $this->connection->send(\GuzzleHttp\Psr7\str(new Response(404, [], 'Not found'))); +// $this->connection->close(); +// dump("No clinet connection"); return; } diff --git a/resources/views/index.html b/resources/views/index.html index e6953c8..892d790 100644 --- a/resources/views/index.html +++ b/resources/views/index.html @@ -100,13 +100,19 @@
- +
+ + +
+ +
+
@@ -124,7 +130,7 @@ -
@@ -302,15 +308,7 @@ Response