mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-14 14:05:54 +00:00
Add fileserver support
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Client;
|
||||
|
||||
use App\Client\Fileserver\Fileserver;
|
||||
use App\Client\Http\Controllers\AttachDataToLogController;
|
||||
use App\Client\Http\Controllers\ClearLogsController;
|
||||
use App\Client\Http\Controllers\CreateTunnelController;
|
||||
@@ -33,6 +34,9 @@ class Factory
|
||||
/** @var App */
|
||||
protected $app;
|
||||
|
||||
/** @var Fileserver */
|
||||
protected $fileserver;
|
||||
|
||||
/** @var RouteGenerator */
|
||||
protected $router;
|
||||
|
||||
@@ -116,6 +120,15 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function shareFolder(string $folder, string $name, $subdomain = null)
|
||||
{
|
||||
$host = $this->createFileServer($folder, $name);
|
||||
|
||||
$this->share($host, $subdomain);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function addRoutes()
|
||||
{
|
||||
$this->router->get('/', DashboardController::class);
|
||||
@@ -134,18 +147,18 @@ class Factory
|
||||
}
|
||||
}
|
||||
|
||||
protected function detectNextFreeDashboardPort($port = 4040): int
|
||||
protected function detectNextAvailablePort($startPort = 4040): int
|
||||
{
|
||||
while (is_resource(@fsockopen('127.0.0.1', $port))) {
|
||||
$port++;
|
||||
while (is_resource(@fsockopen('127.0.0.1', $startPort))) {
|
||||
$startPort++;
|
||||
}
|
||||
|
||||
return $port;
|
||||
return $startPort;
|
||||
}
|
||||
|
||||
public function createHttpServer()
|
||||
{
|
||||
$dashboardPort = $this->detectNextFreeDashboardPort();
|
||||
$dashboardPort = $this->detectNextAvailablePort();
|
||||
|
||||
config()->set('expose.dashboard_port', $dashboardPort);
|
||||
|
||||
@@ -156,11 +169,25 @@ class Factory
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function createFileServer(string $folder, string $name)
|
||||
{
|
||||
$port = $this->detectNextAvailablePort(8090);
|
||||
|
||||
$this->fileserver = new Fileserver($folder, $name, $port, '0.0.0.0', $this->loop);
|
||||
|
||||
return "127.0.0.1:{$port}";
|
||||
}
|
||||
|
||||
public function getApp(): App
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
public function getFileserver(): Fileserver
|
||||
{
|
||||
return $this->fileserver;
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->loop->run();
|
||||
|
||||
137
app/Client/Fileserver/ConnectionHandler.php
Normal file
137
app/Client/Fileserver/ConnectionHandler.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Fileserver;
|
||||
|
||||
use App\Http\Controllers\Concerns\LoadsViews;
|
||||
use App\Http\QueryParameters;
|
||||
use GuzzleHttp\Psr7\ServerRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Http\Response;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\Glob;
|
||||
use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class ConnectionHandler
|
||||
{
|
||||
use LoadsViews;
|
||||
|
||||
/** @var string */
|
||||
protected $rootFolder;
|
||||
|
||||
/** @var string */
|
||||
protected $name;
|
||||
|
||||
/** @var LoopInterface */
|
||||
protected $loop;
|
||||
|
||||
public function __construct(string $rootFolder, string $name, LoopInterface $loop)
|
||||
{
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->name = $name;
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request)
|
||||
{
|
||||
$request = $this->createLaravelRequest($request);
|
||||
$targetPath = realpath($this->rootFolder . DIRECTORY_SEPARATOR . $request->path());
|
||||
|
||||
if (! $this->isValidTarget($targetPath)) {
|
||||
return new Response(404);
|
||||
}
|
||||
|
||||
if (is_dir($targetPath)) {
|
||||
// Directory listing
|
||||
$directoryContent = Finder::create()
|
||||
->depth(0)
|
||||
->sort(function ($a, $b) {
|
||||
return strcmp(strtolower($a->getRealpath()), strtolower($b->getRealpath()));
|
||||
})
|
||||
->in($targetPath);
|
||||
|
||||
if ($this->name !== '') {
|
||||
$directoryContent->name($this->name);
|
||||
}
|
||||
|
||||
$parentPath = explode('/', $request->path());
|
||||
array_pop($parentPath);
|
||||
$parentPath = implode('/', $parentPath);
|
||||
|
||||
return new Response(
|
||||
200,
|
||||
['Content-Type' => 'text/html'],
|
||||
$this->getView(null, 'client.fileserver', [
|
||||
'currentPath' => $request->path(),
|
||||
'parentPath' => $parentPath,
|
||||
'directory' => $targetPath,
|
||||
'directoryContent' => $directoryContent
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (is_file($targetPath)) {
|
||||
return new Response(
|
||||
200,
|
||||
['Content-Type' => mime_content_type($targetPath)],
|
||||
new ReadableResourceStream(fopen($targetPath, 'r'), $this->loop)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function isValidTarget(string $targetPath): bool
|
||||
{
|
||||
if (! file_exists($targetPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->name !== '') {
|
||||
$filter = new class(basename($targetPath), [$this->name]) extends FilenameFilterIterator {
|
||||
protected $filename;
|
||||
|
||||
public function __construct(string $filename, array $matchPatterns)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
|
||||
foreach ($matchPatterns as $pattern) {
|
||||
$this->matchRegexps[] = $this->toRegex($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
public function accept()
|
||||
{
|
||||
return $this->isAccepted($this->filename);
|
||||
}
|
||||
};
|
||||
return $filter->accept();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function createLaravelRequest(ServerRequestInterface $request): Request
|
||||
{
|
||||
try {
|
||||
parse_str($request->getBody(), $bodyParameters);
|
||||
} catch (\Throwable $e) {
|
||||
$bodyParameters = [];
|
||||
}
|
||||
|
||||
$serverRequest = (new ServerRequest(
|
||||
$request->getMethod(),
|
||||
$request->getUri(),
|
||||
$request->getHeaders(),
|
||||
$request->getBody(),
|
||||
$request->getProtocolVersion(),
|
||||
))
|
||||
->withQueryParams(QueryParameters::create($request)->all())
|
||||
->withParsedBody($bodyParameters);
|
||||
|
||||
return Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest));
|
||||
}
|
||||
}
|
||||
31
app/Client/Fileserver/Fileserver.php
Normal file
31
app/Client/Fileserver/Fileserver.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Fileserver;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Http\Response;
|
||||
use React\Http\Server;
|
||||
use React\Socket\Server as SocketServer;
|
||||
|
||||
class Fileserver
|
||||
{
|
||||
/** @var SocketServer */
|
||||
protected $socket;
|
||||
|
||||
public function __construct($rootFolder, $name, $port, $address, LoopInterface $loop)
|
||||
{
|
||||
$server = new Server(function (ServerRequestInterface $request) use ($rootFolder, $name, $loop) {
|
||||
return (new ConnectionHandler($rootFolder, $name, $loop))->handle($request);
|
||||
});
|
||||
|
||||
$this->socket = new SocketServer("{$address}:{$port}", $loop);
|
||||
|
||||
$server->listen($this->socket);
|
||||
}
|
||||
|
||||
public function getSocket(): SocketServer
|
||||
{
|
||||
return $this->socket;
|
||||
}
|
||||
}
|
||||
48
app/Commands/ShareFilesCommand.php
Normal file
48
app/Commands/ShareFilesCommand.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Client\Factory;
|
||||
use App\Logger\CliRequestLogger;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
class ShareFilesCommand extends Command
|
||||
{
|
||||
protected $signature = 'share-files {folder=.} {--name=} {--subdomain=} {--auth=}';
|
||||
|
||||
protected $description = 'Share a local folder with a remote expose server';
|
||||
|
||||
protected function configureConnectionLogger()
|
||||
{
|
||||
app()->bind(CliRequestLogger::class, function () {
|
||||
return new CliRequestLogger(new ConsoleOutput());
|
||||
});
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if ( !is_dir($this->argument('folder'))) {
|
||||
throw new \InvalidArgumentException('The folder '.$this->argument('folder').' does not exist.');
|
||||
}
|
||||
|
||||
$this->configureConnectionLogger();
|
||||
|
||||
(new Factory())
|
||||
->setLoop(app(LoopInterface::class))
|
||||
->setHost(config('expose.host', 'localhost'))
|
||||
->setPort(config('expose.port', 8080))
|
||||
->setAuth($this->option('auth'))
|
||||
->createClient()
|
||||
->shareFolder(
|
||||
$this->argument('folder'),
|
||||
$this->option('name') ?? '',
|
||||
explode(',', $this->option('subdomain'))
|
||||
)
|
||||
->createHttpServer()
|
||||
->run();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use Twig\Loader\ArrayLoader;
|
||||
|
||||
trait LoadsViews
|
||||
{
|
||||
protected function getView(ConnectionInterface $connection, string $view, array $data = [])
|
||||
protected function getView(?ConnectionInterface $connection, string $view, array $data = [])
|
||||
{
|
||||
$templatePath = implode(DIRECTORY_SEPARATOR, explode('.', $view));
|
||||
|
||||
@@ -23,7 +23,10 @@ trait LoadsViews
|
||||
$data = array_merge($data, [
|
||||
'request' => $connection->laravelRequest ?? null,
|
||||
]);
|
||||
|
||||
try {
|
||||
return stream_for($twig->render('template', $data));
|
||||
} catch (\Throwable $e) {
|
||||
var_dump($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user