mirror of
https://github.com/bitinflow/expose.git
synced 2026-03-14 14:05:54 +00:00
Compare commits
5 Commits
fix-http-p
...
share-file
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548c29772a | ||
|
|
844a3cd15a | ||
|
|
e773dfa689 | ||
|
|
c56f05c030 | ||
|
|
ce945e1326 |
@@ -21,4 +21,3 @@ ENV password=password
|
||||
ENV exposeConfigPath=/src/config/expose.php
|
||||
|
||||
CMD sed -i "s|username|${username}|g" ${exposeConfigPath} && sed -i "s|password|${password}|g" ${exposeConfigPath} && php expose serve ${domain} --port ${port} --validateAuthTokens
|
||||
ENTRYPOINT ["/src/expose"]
|
||||
|
||||
@@ -45,13 +45,13 @@ class Client
|
||||
$sharedUrl = $this->prepareSharedUrl($sharedUrl);
|
||||
|
||||
foreach ($subdomains as $subdomain) {
|
||||
$this->connectToServer($sharedUrl, $subdomain, $this->configuration->auth());
|
||||
$this->connectToServer($sharedUrl, $subdomain, config('expose.auth_token'));
|
||||
}
|
||||
}
|
||||
|
||||
public function sharePort(int $port)
|
||||
{
|
||||
$this->connectToServerAndShareTcp($port, $this->configuration->auth());
|
||||
$this->connectToServerAndShareTcp($port, config('expose.auth_token'));
|
||||
}
|
||||
|
||||
protected function prepareSharedUrl(string $sharedUrl): string
|
||||
@@ -60,11 +60,16 @@ class Client
|
||||
return $sharedUrl;
|
||||
}
|
||||
|
||||
$host = Arr::get($parsedUrl, 'host', Arr::get($parsedUrl, 'path', 'localhost'));
|
||||
$scheme = Arr::get($parsedUrl, 'scheme', 'http');
|
||||
$port = Arr::get($parsedUrl, 'port', $scheme === 'https' ? 443 : 80);
|
||||
$url = Arr::get($parsedUrl, 'host', Arr::get($parsedUrl, 'path'));
|
||||
|
||||
return sprintf('%s://%s:%s', $scheme, $host, $port);
|
||||
if (Arr::get($parsedUrl, 'scheme') === 'https') {
|
||||
$url .= ':443';
|
||||
}
|
||||
if (! is_null($port = Arr::get($parsedUrl, 'port'))) {
|
||||
$url .= ":{$port}";
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function connectToServer(string $sharedUrl, $subdomain, $authToken = ''): PromiseInterface
|
||||
|
||||
@@ -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();
|
||||
|
||||
135
app/Client/Fileserver/ConnectionHandler.php
Normal file
135
app/Client/Fileserver/ConnectionHandler.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?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 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\Iterator\FilenameFilterIterator;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
30
app/Client/Fileserver/Fileserver.php
Normal file
30
app/Client/Fileserver/Fileserver.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Client\Fileserver;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,10 @@ use function GuzzleHttp\Psr7\str;
|
||||
use Laminas\Http\Request;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Ratchet\Client\WebSocket;
|
||||
use Ratchet\RFC6455\Messaging\Frame;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
|
||||
class HttpClient
|
||||
{
|
||||
@@ -76,6 +74,7 @@ class HttpClient
|
||||
protected function createConnector(): Connector
|
||||
{
|
||||
return new Connector($this->loop, [
|
||||
'dns' => '127.0.0.1',
|
||||
'tls' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
@@ -86,17 +85,22 @@ class HttpClient
|
||||
protected function sendRequestToApplication(RequestInterface $request, $proxyConnection = null)
|
||||
{
|
||||
(new Browser($this->loop, $this->createConnector()))
|
||||
->withFollowRedirects(false)
|
||||
->withRejectErrorResponse(false)
|
||||
->requestStreaming($request->getMethod(), $this->getExposeUri($request), $request->getHeaders(), $request->getBody())
|
||||
->withOptions([
|
||||
'followRedirects' => false,
|
||||
'obeySuccessCode' => false,
|
||||
'streaming' => true,
|
||||
])
|
||||
->send($request)
|
||||
->then(function (ResponseInterface $response) use ($proxyConnection) {
|
||||
if (! isset($response->buffer)) {
|
||||
$response = $this->rewriteResponseHeaders($response);
|
||||
|
||||
$response->buffer = str($response);
|
||||
}
|
||||
|
||||
$this->sendChunkToServer($response->buffer, $proxyConnection);
|
||||
|
||||
/* @var $body ReadableStreamInterface */
|
||||
/* @var $body \React\Stream\ReadableStreamInterface */
|
||||
$body = $response->getBody();
|
||||
|
||||
$this->logResponse(str($response));
|
||||
@@ -133,14 +137,24 @@ class HttpClient
|
||||
return Request::fromString($data);
|
||||
}
|
||||
|
||||
private function getExposeUri(RequestInterface $request): UriInterface
|
||||
protected function rewriteResponseHeaders(ResponseInterface $response)
|
||||
{
|
||||
$exposeProto = $request->getHeader('x-expose-proto')[0];
|
||||
$exposeHost = explode(':', $request->getHeader('x-expose-host')[0]);
|
||||
if (! $response->hasHeader('Location')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $request->getUri()
|
||||
->withScheme($exposeProto)
|
||||
->withHost($exposeHost[0])
|
||||
->withPort($exposeHost[1]);
|
||||
$location = $response->getHeaderLine('Location');
|
||||
|
||||
if (! strstr($location, $this->connectionData->host)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$location = str_replace(
|
||||
$this->connectionData->host,
|
||||
$this->configuration->getUrl($this->connectionData->subdomain),
|
||||
$location
|
||||
);
|
||||
|
||||
return $response->withHeader('Location', $location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
class ShareCommand extends Command
|
||||
{
|
||||
protected $signature = 'share {host} {--subdomain=} {--auth=} {--server-host=} {--server-port=}';
|
||||
protected $signature = 'share {host} {--subdomain=} {--auth=}';
|
||||
|
||||
protected $description = 'Share a local url with a remote expose server';
|
||||
|
||||
@@ -27,15 +27,11 @@ class ShareCommand extends Command
|
||||
{
|
||||
$this->configureConnectionLogger();
|
||||
|
||||
$serverHost = $this->option('server-host') ?? config('expose.host', 'localhost');
|
||||
$serverPort = $this->option('server-port') ?? config('expose.port', 8080);
|
||||
$auth = $this->option('auth') ?? config('expose.auth_token', '');
|
||||
|
||||
(new Factory())
|
||||
->setLoop(app(LoopInterface::class))
|
||||
->setHost($serverHost)
|
||||
->setPort($serverPort)
|
||||
->setAuth($auth)
|
||||
->setHost(config('expose.host', 'localhost'))
|
||||
->setPort(config('expose.port', 8080))
|
||||
->setAuth($this->option('auth'))
|
||||
->createClient()
|
||||
->share($this->argument('host'), explode(',', $this->option('subdomain')))
|
||||
->createHttpServer()
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Commands;
|
||||
|
||||
class ShareCurrentWorkingDirectoryCommand extends ShareCommand
|
||||
{
|
||||
protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=} {--server-host=} {--server-port=}';
|
||||
protected $signature = 'share-cwd {host?} {--subdomain=} {--auth=}';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +37,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
$builtInConfig = config('expose');
|
||||
|
||||
$keyServerVariable = 'EXPOSE_CONFIG_FILE';
|
||||
if (array_key_exists($keyServerVariable, $_SERVER) && is_string($_SERVER[$keyServerVariable]) && file_exists($_SERVER[$keyServerVariable])) {
|
||||
$localConfig = require $_SERVER[$keyServerVariable];
|
||||
config()->set('expose', array_merge($builtInConfig, $localConfig));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$localConfigFile = getcwd().DIRECTORY_SEPARATOR.'.expose.php';
|
||||
|
||||
if (file_exists($localConfigFile)) {
|
||||
|
||||
@@ -45,13 +45,15 @@ class ConnectionManager implements ConnectionManagerContract
|
||||
|
||||
public function storeConnection(string $host, ?string $subdomain, ConnectionInterface $connection): ControlConnection
|
||||
{
|
||||
$connection->client_id = sha1(uniqid('', true));
|
||||
$clientId = (string) uniqid();
|
||||
|
||||
$connection->client_id = $clientId;
|
||||
|
||||
$storedConnection = new ControlConnection(
|
||||
$connection,
|
||||
$host,
|
||||
$subdomain ?? $this->subdomainGenerator->generateSubdomain(),
|
||||
$connection->client_id,
|
||||
$clientId,
|
||||
$this->getAuthTokenFromConnection($connection)
|
||||
);
|
||||
|
||||
|
||||
@@ -113,13 +113,9 @@ class TunnelMessageController extends Controller
|
||||
$host .= ":{$this->configuration->port()}";
|
||||
}
|
||||
|
||||
$exposeUrl = parse_url($controlConnection->host);
|
||||
|
||||
$request->headers->set('Host', "{$controlConnection->subdomain}.{$host}");
|
||||
$request->headers->set('Host', $controlConnection->host);
|
||||
$request->headers->set('X-Forwarded-Proto', $request->isSecure() ? 'https' : 'http');
|
||||
$request->headers->set('X-Expose-Request-ID', sha1(uniqid('', true)));
|
||||
$request->headers->set('X-Expose-Host', sprintf('%s:%s', $exposeUrl['host'], $exposeUrl['port']));
|
||||
$request->headers->set('X-Expose-Proto', $exposeUrl['scheme']);
|
||||
$request->headers->set('X-Expose-Request-ID', uniqid());
|
||||
$request->headers->set('Upgrade-Insecure-Requests', 1);
|
||||
$request->headers->set('X-Exposed-By', config('app.name').' '.config('app.version'));
|
||||
$request->headers->set('X-Original-Host', "{$controlConnection->subdomain}.{$host}");
|
||||
|
||||
@@ -17,12 +17,6 @@ The configuration file will be written to your home directory inside a `.expose`
|
||||
|
||||
`~/.expose/config.php`
|
||||
|
||||
You can also provide a custom location of the config file by providing the full path as a server variable.
|
||||
|
||||
```bash
|
||||
EXPOSE_CONFIG_FILE="~/my-custom-config.php" expose share
|
||||
```
|
||||
|
||||
And the default content of the configuration file is this:
|
||||
|
||||
```php
|
||||
|
||||
93
resources/views/client/fileserver.twig
Normal file
93
resources/views/client/fileserver.twig
Normal file
@@ -0,0 +1,93 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Expose Fileserver</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.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/npm/clipboard@2/dist/clipboard.min.js"></script>
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.0/build/styles/github.min.css">
|
||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.0/build/highlight.min.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="">
|
||||
<div class="relative bg-indigo-600" style="marign-left: -1px">
|
||||
<div class="max-w-screen-xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
|
||||
<div class="pr-16 sm:text-center sm:px-16">
|
||||
<p class="font-medium text-white flex justify-center">
|
||||
<span class="inline-block font-mono">{{ directory }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% macro bytesToSize(bytes) %}
|
||||
{% set kilobyte = 1024 %}
|
||||
{% set megabyte = kilobyte * 1024 %}
|
||||
{% set gigabyte = megabyte * 1024 %}
|
||||
{% set terabyte = gigabyte * 1024 %}
|
||||
|
||||
{% if bytes < kilobyte %}
|
||||
{{ bytes ~ ' B' }}
|
||||
{% elseif bytes < megabyte %}
|
||||
{{ (bytes / kilobyte)|number_format(2, '.') ~ ' KiB' }}
|
||||
{% elseif bytes < gigabyte %}
|
||||
{{ (bytes / megabyte)|number_format(2, '.') ~ ' MiB' }}
|
||||
{% elseif bytes < terabyte %}
|
||||
{{ (bytes / gigabyte)|number_format(2, '.') ~ ' GiB' }}
|
||||
{% else %}
|
||||
{{ (bytes / terabyte)|number_format(2, '.') ~ ' TiB' }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
<div class="flex flex-col px-6 py-4">
|
||||
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date Modified
|
||||
</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
|
||||
Size
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white">
|
||||
{% if currentPath != '/' %}
|
||||
<tr class="border-b">
|
||||
<td colspan="3" class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-mono text-gray-900">
|
||||
<a href="/{{ parentPath }}" class="text-indigo-600 font-bold hover:text-indigo-900">Back</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for item in directoryContent %}
|
||||
<tr class="{% if loop.index % 2 == 0 %} bg-gray-50 {% endif %}">
|
||||
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-mono text-gray-900">
|
||||
{% if currentPath != '/' %}
|
||||
<a href="/{{ currentPath }}/{{ item.getFilename() }}" class="text-indigo-600 hover:text-indigo-900">{{ item.filename }}</a>
|
||||
{% else %}
|
||||
<a href="/{{ item.getFilename() }}" class="text-indigo-600 hover:text-indigo-900">{{ item.filename }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||
{{ item.getMTime() | date("m/d/Y H:i:s") }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
|
||||
{% if item.isDir() %}
|
||||
-
|
||||
{% else %}
|
||||
{{ _self.bytesToSize(item.getSize()) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
111
tests/Feature/Client/FileserverTest.php
Executable file
111
tests/Feature/Client/FileserverTest.php
Executable file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Client;
|
||||
|
||||
use App\Client\Configuration;
|
||||
use App\Client\Factory;
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Clue\React\Buzz\Message\ResponseException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Tests\Feature\TestCase;
|
||||
|
||||
class FileserverTest extends TestCase
|
||||
{
|
||||
/** @var Browser */
|
||||
protected $browser;
|
||||
|
||||
/** @var Factory */
|
||||
protected $clientFactory;
|
||||
|
||||
/** @var string */
|
||||
protected $fileserverUrl;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->browser = new Browser($this->loop);
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
$this->clientFactory->getFileserver()->getSocket()->close();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function accessing_the_fileserver_works()
|
||||
{
|
||||
$this->shareFolder(__DIR__);
|
||||
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->await($this->browser->get('http://'.$this->fileserverUrl));
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function accessing_invalid_files_returns_404()
|
||||
{
|
||||
$this->shareFolder(__DIR__);
|
||||
|
||||
$this->expectException(ResponseException::class);
|
||||
$this->expectExceptionMessage(404);
|
||||
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->await($this->browser->get('http://'.$this->fileserverUrl.'/invalid-file'));
|
||||
|
||||
$this->assertSame(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_return_filtered_responses()
|
||||
{
|
||||
$this->shareFolder(__DIR__.'/../../fixtures', '*.md');
|
||||
|
||||
$this->expectException(ResponseException::class);
|
||||
$this->expectExceptionMessage(404);
|
||||
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->await($this->browser->get('http://'.$this->fileserverUrl.'/test.txt'));
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('test-file'.PHP_EOL, $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_return_file_responses()
|
||||
{
|
||||
$this->shareFolder(__DIR__.'/../../fixtures');
|
||||
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->await($this->browser->get('http://'.$this->fileserverUrl.'/test.txt'));
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('test-file'.PHP_EOL, $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_return_file_responses_for_valid_filtered_files()
|
||||
{
|
||||
$this->shareFolder(__DIR__.'/../../fixtures', '*.txt');
|
||||
|
||||
/** @var ResponseInterface $response */
|
||||
$response = $this->await($this->browser->get('http://'.$this->fileserverUrl.'/test.txt'));
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSame('test-file'.PHP_EOL, $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
protected function shareFolder(string $folder, string $name = '')
|
||||
{
|
||||
app()->singleton(Configuration::class, function ($app) {
|
||||
return new Configuration('localhost', '8080', false);
|
||||
});
|
||||
|
||||
$factory = (new Factory())->setLoop($this->loop);
|
||||
$this->fileserverUrl = $factory->createFileServer($folder, $name);
|
||||
$this->clientFactory = $factory;
|
||||
}
|
||||
}
|
||||
1
tests/fixtures/test.md
vendored
Normal file
1
tests/fixtures/test.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Markdown
|
||||
1
tests/fixtures/test.txt
vendored
Normal file
1
tests/fixtures/test.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
test-file
|
||||
Reference in New Issue
Block a user