diff --git a/.gitignore b/.gitignore index 20d9dbd..ac9fe87 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /.vscode /.vagrant .phpunit.result.cache +expose.php +database/expose.db diff --git a/app/Client/Client.php b/app/Client/Client.php index 3339db7..fc42be7 100644 --- a/app/Client/Client.php +++ b/app/Client/Client.php @@ -3,6 +3,7 @@ namespace App\Client; use App\Client\Connections\ControlConnection; +use App\Logger\CliRequestLogger; use Ratchet\Client\WebSocket; use React\EventLoop\LoopInterface; use function Ratchet\Client\connect; @@ -15,16 +16,22 @@ class Client /** @var Configuration */ protected $configuration; + /** @var CliRequestLogger */ + protected $logger; + public static $subdomains = []; - public function __construct(LoopInterface $loop, Configuration $configuration) + public function __construct(LoopInterface $loop, Configuration $configuration, CliRequestLogger $logger) { $this->loop = $loop; $this->configuration = $configuration; + $this->logger = $logger; } public function share(string $sharedUrl, array $subdomains = []) { + $this->logger->info("Sharing http://{$sharedUrl}"); + foreach ($subdomains as $subdomain) { $this->connectToServer($sharedUrl, $subdomain); } @@ -32,7 +39,7 @@ class Client protected function connectToServer(string $sharedUrl, $subdomain) { - connect("ws://{$this->configuration->host()}:{$this->configuration->port()}/__expose_control__", [], [ + connect("ws://{$this->configuration->host()}:{$this->configuration->port()}/__expose_control__?authToken={$this->configuration->authToken()}", [], [ 'X-Expose-Control' => 'enabled', ], $this->loop) ->then(function (WebSocket $clientConnection) use ($sharedUrl, $subdomain) { @@ -40,8 +47,14 @@ class Client $connection->authenticate($sharedUrl, $subdomain); + $connection->on('authenticationFailed', function ($data) { + $this->logger->error("Authentication failed. Please check your authentication token and try again."); + exit(1); + }); + $connection->on('authenticated', function ($data) { - dump("Connected to http://$data->subdomain.{$this->configuration->host()}:{$this->configuration->port()}"); + $this->logger->info("Connected to http://$data->subdomain.{$this->configuration->host()}:{$this->configuration->port()}"); + static::$subdomains[] = "$data->subdomain.{$this->configuration->host()}:{$this->configuration->port()}"; }); diff --git a/app/Client/Configuration.php b/app/Client/Configuration.php index 423fdc0..22af727 100644 --- a/app/Client/Configuration.php +++ b/app/Client/Configuration.php @@ -13,13 +13,18 @@ class Configuration /** @var string|null */ protected $auth; - public function __construct(string $host, int $port, ?string $auth = null) + /** @var string|null */ + protected $authToken; + + public function __construct(string $host, int $port, ?string $auth = null, string $authToken = null) { $this->host = $host; $this->port = $port; $this->auth = $auth; + + $this->authToken = $authToken; } public function host(): string @@ -36,4 +41,9 @@ class Configuration { return $this->port; } + + public function authToken() + { + return $this->authToken; + } } diff --git a/app/Client/Connections/ControlConnection.php b/app/Client/Connections/ControlConnection.php index 457064b..9adf715 100644 --- a/app/Client/Connections/ControlConnection.php +++ b/app/Client/Connections/ControlConnection.php @@ -34,9 +34,8 @@ class ControlConnection $this->socket->on('message', function (Message $message) { $decodedEntry = json_decode($message); + $this->emit($decodedEntry->event ?? '', [$decodedEntry]); if (method_exists($this, $decodedEntry->event ?? '')) { - $this->emit($decodedEntry->event, [$decodedEntry]); - call_user_func([$this, $decodedEntry->event], $decodedEntry); } }); diff --git a/app/Client/Factory.php b/app/Client/Factory.php index 7e4d8ea..11e14a0 100644 --- a/app/Client/Factory.php +++ b/app/Client/Factory.php @@ -27,6 +27,9 @@ class Factory /** @var string */ protected $auth = ''; + /** @var string */ + protected $authToken = ''; + /** @var \React\EventLoop\LoopInterface */ protected $loop; @@ -52,13 +55,20 @@ class Factory return $this; } - public function setAuth(string $auth) + public function setAuth(?string $auth) { $this->auth = $auth; return $this; } + public function setAuthToken(?string $authToken) + { + $this->authToken = $authToken; + + return $this; + } + public function setLoop(LoopInterface $loop) { $this->loop = $loop; @@ -69,7 +79,7 @@ class Factory protected function bindConfiguration() { app()->singleton(Configuration::class, function ($app) { - return new Configuration($this->host, $this->port, $this->auth); + return new Configuration($this->host, $this->port, $this->auth, $this->authToken); }); } diff --git a/app/Client/Http/HttpClient.php b/app/Client/Http/HttpClient.php index 6e70803..0f6b922 100644 --- a/app/Client/Http/HttpClient.php +++ b/app/Client/Http/HttpClient.php @@ -47,8 +47,6 @@ class HttpClient $request = $this->passRequestThroughModifiers(parse_request($requestData), $proxyConnection); - dump($this->request->getMethod() . ' ' . $this->request->getUri()->getPath()); - transform($request, function ($request) use ($proxyConnection) { $this->sendRequestToApplication($request, $proxyConnection); }); diff --git a/app/Commands/ServeCommand.php b/app/Commands/ServeCommand.php index f4d63fa..15c897b 100644 --- a/app/Commands/ServeCommand.php +++ b/app/Commands/ServeCommand.php @@ -8,7 +8,7 @@ use React\EventLoop\LoopInterface; class ServeCommand extends Command { - protected $signature = 'serve {host=0.0.0.0} {hostname=localhost}'; + protected $signature = 'serve {host=0.0.0.0} {hostname=localhost} {--validateAuthTokens}'; protected $description = 'Start the shaft server'; @@ -18,6 +18,7 @@ class ServeCommand extends Command ->setLoop(app(LoopInterface::class)) ->setHost($this->argument('host')) ->setHostname($this->argument('hostname')) + ->validateAuthTokens($this->option('validateAuthTokens')) ->createServer() ->run(); } diff --git a/app/Commands/ShareCommand.php b/app/Commands/ShareCommand.php index d549229..ac94b85 100644 --- a/app/Commands/ShareCommand.php +++ b/app/Commands/ShareCommand.php @@ -3,23 +3,37 @@ namespace App\Commands; use App\Client\Factory; +use App\Logger\CliRequestLogger; use Illuminate\Console\Scheduling\Schedule; use LaravelZero\Framework\Commands\Command; use React\EventLoop\LoopInterface; +use Symfony\Component\Console\Output\ConsoleOutput; class ShareCommand extends Command { - protected $signature = 'share {host} {--subdomain=} {--auth=}'; + protected $signature = 'share {host} {--subdomain=} {--auth=} {--token=}'; protected $description = 'Share a local url with a remote shaft server'; + protected function configureConnectionLogger() + { + app()->bind(CliRequestLogger::class, function () { + return new CliRequestLogger(new ConsoleOutput()); + }); + + return $this; + } + public function handle() { + $this->configureConnectionLogger(); + (new Factory()) ->setLoop(app(LoopInterface::class)) // ->setHost('beyond.sh') // TODO: Read from (local/global) config file // ->setPort(8080) // TODO: Read from (local/global) config file ->setAuth($this->option('auth')) + ->setAuthToken($this->option('token')) ->createClient($this->argument('host'), explode(',', $this->option('subdomain'))) ->createHttpServer() ->run(); diff --git a/app/Commands/ShareCurrentWorkingDirectoryCommand.php b/app/Commands/ShareCurrentWorkingDirectoryCommand.php new file mode 100644 index 0000000..6a6b4d9 --- /dev/null +++ b/app/Commands/ShareCurrentWorkingDirectoryCommand.php @@ -0,0 +1,17 @@ +input->setArgument('host', basename(getcwd()).'.test'); + + parent::handle(); + } +} diff --git a/app/HttpServer/Controllers/PostController.php b/app/HttpServer/Controllers/PostController.php index 196f1e0..c17c4ac 100644 --- a/app/HttpServer/Controllers/PostController.php +++ b/app/HttpServer/Controllers/PostController.php @@ -68,13 +68,21 @@ abstract class PostController extends Controller protected function createLaravelRequest(ConnectionInterface $connection): Request { + try { + parse_str($connection->requestBuffer, $bodyParameters); + } catch (\Throwable $e) { + $bodyParameters = []; + } + $serverRequest = (new ServerRequest( $connection->request->getMethod(), $connection->request->getUri(), $connection->request->getHeaders(), $connection->requestBuffer, $connection->request->getProtocolVersion() - ))->withQueryParams(QueryParameters::create($connection->request)->all()); + )) + ->withQueryParams(QueryParameters::create($connection->request)->all()) + ->withParsedBody($bodyParameters); return Request::createFromBase((new HttpFoundationFactory)->createRequest($serverRequest)); } diff --git a/app/Logger/CliRequestLogger.php b/app/Logger/CliRequestLogger.php new file mode 100644 index 0000000..2cce701 --- /dev/null +++ b/app/Logger/CliRequestLogger.php @@ -0,0 +1,55 @@ +section = $this->output->section(); + + $this->table = new Table($this->section); + $this->table->setHeaders(['Method', 'URI', 'Response', 'Duration']); + + $this->requests = new Collection(); + } + + public function logRequest(LoggedRequest $loggedRequest) + { + if ($this->requests->has($loggedRequest->id())) { + $this->requests[$loggedRequest->id()] = $loggedRequest; + } else { + $this->requests->prepend($loggedRequest, $loggedRequest->id()); + } + $this->requests = $this->requests->slice(0, 10); + + $this->section->clear(); + + $this->table->setRows($this->requests->map(function (LoggedRequest $loggedRequest) { + return [ + $loggedRequest->getRequest()->getMethod(), + $loggedRequest->getRequest()->getUri(), + optional($loggedRequest->getResponse())->getStatusCode() . ' ' . optional($loggedRequest->getResponse())->getReasonPhrase(), + $loggedRequest->getDuration().'ms' + ]; + })->toArray()); + + $this->table->render(); + } +} diff --git a/app/Logger/LoggedRequest.php b/app/Logger/LoggedRequest.php index 82687f2..408d46f 100644 --- a/app/Logger/LoggedRequest.php +++ b/app/Logger/LoggedRequest.php @@ -56,7 +56,7 @@ class LoggedRequest implements \JsonSerializable $data = [ 'id' => $this->id, 'performed_at' => $this->startTime->toDateTimeString(), - 'duration' => $this->startTime->diffInMilliseconds($this->stopTime, false), + 'duration' => $this->getDuration(), 'subdomain' => $this->detectSubdomain(), 'request' => [ 'raw' => $this->isBinary($this->rawRequest) ? 'BINARY' : $this->rawRequest, @@ -132,6 +132,11 @@ class LoggedRequest implements \JsonSerializable return $this->rawRequest; } + public function getResponse() + { + return $this->parsedResponse; + } + protected function getResponseBody() { return \Laminas\Http\Response::fromString($this->rawResponse)->getBody(); @@ -198,4 +203,9 @@ class LoggedRequest implements \JsonSerializable { return Arr::get($this->parsedRequest->getHeaders()->toArray(), 'X-Expose-Request-ID', (string)Str::uuid()); } + + public function getDuration() + { + return $this->startTime->diffInMilliseconds($this->stopTime, false); + } } diff --git a/app/Logger/Logger.php b/app/Logger/Logger.php index 2258131..2ccaa2c 100644 --- a/app/Logger/Logger.php +++ b/app/Logger/Logger.php @@ -2,64 +2,21 @@ namespace App\Logger; +use Illuminate\Console\Concerns\InteractsWithIO; +use Illuminate\Console\OutputStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; class Logger { - /** @var \Symfony\Component\Console\Output\OutputInterface */ - protected $consoleOutput; + use InteractsWithIO; - /** @var bool */ - protected $enabled = false; + /** @var ConsoleOutputInterface */ + protected $output; - /** @var bool */ - protected $verbose = false; - - public function __construct(OutputInterface $consoleOutput) + public function __construct(ConsoleOutputInterface $consoleOutput) { - $this->consoleOutput = $consoleOutput; - } - - public function enable($enabled = true) - { - $this->enabled = $enabled; - - return $this; - } - - public function verbose($verbose = false) - { - $this->verbose = $verbose; - - return $this; - } - - protected function info(string $message) - { - $this->line($message, 'info'); - } - - protected function warn(string $message) - { - if (! $this->consoleOutput->getFormatter()->hasStyle('warning')) { - $style = new OutputFormatterStyle('yellow'); - - $this->consoleOutput->getFormatter()->setStyle('warning', $style); - } - - $this->line($message, 'warning'); - } - - protected function error(string $message) - { - $this->line($message, 'error'); - } - - protected function line(string $message, string $style) - { - $styled = $style ? "<$style>$message$style>" : $message; - - $this->consoleOutput->writeln($styled); + $this->output = $consoleOutput; } } diff --git a/app/Logger/RequestLogger.php b/app/Logger/RequestLogger.php index 9f9cdc2..60e6ac8 100644 --- a/app/Logger/RequestLogger.php +++ b/app/Logger/RequestLogger.php @@ -10,12 +10,19 @@ use function GuzzleHttp\Psr7\stream_for; class RequestLogger { + /** @var array */ protected $requests = []; + + /** @var array */ protected $responses = []; - public function __construct(Browser $browser) + /** @var CliRequestLogger */ + protected $cliRequestLogger; + + public function __construct(Browser $browser, CliRequestLogger $cliRequestLogger) { $this->client = $browser; + $this->cliRequestLogger = $cliRequestLogger; } public function findLoggedRequest(string $id): ?LoggedRequest @@ -27,10 +34,14 @@ class RequestLogger public function logRequest(string $rawRequest, Request $request) { - array_unshift($this->requests, new LoggedRequest($rawRequest, $request)); + $loggedRequest = new LoggedRequest($rawRequest, $request); + + array_unshift($this->requests, $loggedRequest); $this->requests = array_slice($this->requests, 0, 10); + $this->cliRequestLogger->logRequest($loggedRequest); + $this->pushLogs(); } @@ -42,6 +53,8 @@ class RequestLogger if ($loggedRequest) { $loggedRequest->setResponse($rawResponse, Response::fromString($rawResponse)); + $this->cliRequestLogger->logRequest($loggedRequest); + $this->pushLogs(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 92466c4..0b299bf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Logger\CliRequestLogger; use App\Logger\RequestLogger; use Clue\React\Buzz\Browser; use Illuminate\Support\ServiceProvider; @@ -21,10 +22,8 @@ class AppServiceProvider extends ServiceProvider return LoopFactory::create(); }); - $this->app->singleton(RequestLogger::class, function () { - $browser = new Browser(app(LoopInterface::class)); - - return new RequestLogger($browser); + $this->app->singleton(RequestLogger::class, function ($app) { + return new RequestLogger($app->make(Browser::class), $app->make(CliRequestLogger::class)); }); } } diff --git a/app/Server/Factory.php b/app/Server/Factory.php index 9f679c8..c7241b5 100644 --- a/app/Server/Factory.php +++ b/app/Server/Factory.php @@ -6,15 +6,21 @@ use App\Contracts\ConnectionManager as ConnectionManagerContract; use App\Contracts\SubdomainGenerator; use App\HttpServer\HttpServer; use App\Server\Connections\ConnectionManager; +use App\Server\Http\Controllers\Admin\DeleteUsersController; +use App\Server\Http\Controllers\Admin\ListUsersController; +use App\Server\Http\Controllers\Admin\StoreUsersController; use App\Server\Http\Controllers\ControlMessageController; use App\Server\Http\Controllers\TunnelMessageController; use App\Server\SubdomainGenerator\RandomSubdomainGenerator; +use Clue\React\SQLite\DatabaseInterface; use Ratchet\Http\Router; use Ratchet\Server\IoServer; use Ratchet\WebSocket\WsServer; use React\Socket\Server; use React\EventLoop\LoopInterface; use React\EventLoop\Factory as LoopFactory; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; @@ -34,9 +40,13 @@ class Factory /** @var \React\EventLoop\LoopInterface */ protected $loop; + /** @var RouteCollection */ + protected $routes; + public function __construct() { $this->loop = LoopFactory::create(); + $this->routes = new RouteCollection(); } public function setHost(string $host) @@ -67,25 +77,42 @@ class Factory return $this; } - protected function getRoutes(): RouteCollection + protected function addExposeRoutes() { - $routes = new RouteCollection(); - - $routes->add('control', + $this->routes->add('control', new Route('/__expose_control__', [ '_controller' => new WsServer(app(ControlMessageController::class)) ], [], [], null, [], [] ) ); - $routes->add('tunnel', + $this->routes->add('tunnel', new Route('/{__catchall__}', [ '_controller' => app(TunnelMessageController::class), ], [ '__catchall__' => '.*' ])); + } - return $routes; + protected function addAdminRoutes() + { + $this->routes->add('admin.users.index', + new Route('/expose/users', [ + '_controller' => app(ListUsersController::class), + ], [], [], null, [], ['GET']) + ); + + $this->routes->add('admin.users.store', + new Route('/expose/users', [ + '_controller' => app(StoreUsersController::class), + ], [], [], null, [], ['POST']) + ); + + $this->routes->add('admin.users.delete', + new Route('/expose/users/delete/{id}', [ + '_controller' => app(DeleteUsersController::class), + ], [], [], null, [], ['DELETE']) + ); } protected function bindConfiguration() @@ -117,9 +144,17 @@ class Factory $this->bindSubdomainGenerator(); + $this->bindDatabase(); + + $this->ensureDatabaseIsInitialized(); + $this->bindConnectionManager(); - $urlMatcher = new UrlMatcher($this->getRoutes(), new RequestContext); + $this->addAdminRoutes(); + + $this->addExposeRoutes(); + + $urlMatcher = new UrlMatcher($this->routes, new RequestContext); $router = new Router($urlMatcher); @@ -128,4 +163,36 @@ class Factory return new IoServer($http, $socket, $this->loop); } + protected function bindDatabase() + { + app()->singleton(DatabaseInterface::class, function() { + $factory = new \Clue\React\SQLite\Factory($this->loop); + return $factory->openLazy(base_path('database/expose.db')); + }); + } + + protected function ensureDatabaseIsInitialized() + { + /** @var DatabaseInterface $db */ + $db = app(DatabaseInterface::class); + + $migrations = (new Finder()) + ->files() + ->ignoreDotFiles(true) + ->in(database_path('migrations')) + ->name('*.sql'); + + /** @var SplFileInfo $migration */ + foreach ($migrations as $migration) { + $db->exec($migration->getContents()); + } + } + + public function validateAuthTokens(bool $validate) + { + dump($validate); + + return $this; + } + } diff --git a/app/Server/Http/Controllers/Admin/DeleteUsersController.php b/app/Server/Http/Controllers/Admin/DeleteUsersController.php new file mode 100644 index 0000000..3d43f10 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/DeleteUsersController.php @@ -0,0 +1,38 @@ +database = $database; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $this->database->query("DELETE FROM users WHERE id = :id", ['id' => $request->id]) + ->then(function (Result $result) use ($httpConnection) { + $httpConnection->send(respond_json(['deleted' => true], 200)); + $httpConnection->close(); + }); + } +} diff --git a/app/Server/Http/Controllers/Admin/ListUsersController.php b/app/Server/Http/Controllers/Admin/ListUsersController.php new file mode 100644 index 0000000..ec73f2a --- /dev/null +++ b/app/Server/Http/Controllers/Admin/ListUsersController.php @@ -0,0 +1,53 @@ +database = $database; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $this->database->query('SELECT * FROM users ORDER by created_at DESC')->then(function (Result $result) use ($httpConnection) { + $httpConnection->send( + respond_html($this->getView(['users' => $result->rows])) + ); + + $httpConnection->close(); + }, function (\Exception $exception) use ($httpConnection) { + $httpConnection->send(respond_html('Something went wrong: '.$exception->getMessage(), 500)); + + $httpConnection->close(); + }); + } + + protected function getView(array $data) + { + $twig = new Environment( + new ArrayLoader([ + 'template' => file_get_contents(base_path('resources/views/admin/users/index.twig')), + ]) + ); + + return stream_for($twig->render('template', $data)); + } +} diff --git a/app/Server/Http/Controllers/Admin/StoreUsersController.php b/app/Server/Http/Controllers/Admin/StoreUsersController.php new file mode 100644 index 0000000..7a22da3 --- /dev/null +++ b/app/Server/Http/Controllers/Admin/StoreUsersController.php @@ -0,0 +1,62 @@ +database = $database; + } + + public function handle(Request $request, ConnectionInterface $httpConnection) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required', + ], [ + 'required' => 'The :attribute field is required.', + ]); + + if ($validator->fails()) { + $httpConnection->send(respond_json(['errors' => $validator->getMessageBag()], 401)); + $httpConnection->close(); + + return; + } + + $insertData = [ + 'name' => $request->get('name'), + 'auth_token' => (string)Str::uuid() + ]; + + $this->database->query(" + INSERT INTO users (name, auth_token, created_at) + VALUES (:name, :auth_token, DATETIME('now')) + ", $insertData) + ->then(function (Result $result) use ($httpConnection) { + $this->database->query("SELECT * FROM users WHERE id = :id", ['id' => $result->insertId]) + ->then(function (Result $result) use ($httpConnection) { + $httpConnection->send(respond_json(['user' => $result->rows[0]], 200)); + $httpConnection->close(); + }); + }); + } +} diff --git a/app/Server/Http/Controllers/ControlMessageController.php b/app/Server/Http/Controllers/ControlMessageController.php index eab1a59..8a2a428 100644 --- a/app/Server/Http/Controllers/ControlMessageController.php +++ b/app/Server/Http/Controllers/ControlMessageController.php @@ -3,6 +3,9 @@ namespace App\Server\Http\Controllers; use App\Contracts\ConnectionManager; +use App\HttpServer\QueryParameters; +use Clue\React\SQLite\DatabaseInterface; +use Clue\React\SQLite\Result; use stdClass; use Ratchet\ConnectionInterface; use Ratchet\MessageComponentInterface; @@ -13,9 +16,21 @@ class ControlMessageController implements MessageComponentInterface /** @var ConnectionManager */ protected $connectionManager; - public function __construct(ConnectionManager $connectionManager) + /** @var DatabaseInterface */ + protected $database; + + public function __construct(ConnectionManager $connectionManager, DatabaseInterface $database) { $this->connectionManager = $connectionManager; + $this->database = $database; + } + + /** + * @inheritDoc + */ + function onOpen(ConnectionInterface $connection) + { + $this->verifyAuthToken($connection); } /** @@ -80,14 +95,6 @@ class ControlMessageController implements MessageComponentInterface ]); } - /** - * @inheritDoc - */ - function onOpen(ConnectionInterface $conn) - { - // - } - /** * @inheritDoc */ @@ -95,4 +102,21 @@ class ControlMessageController implements MessageComponentInterface { // } + + protected function verifyAuthToken(ConnectionInterface $connection) + { + $authToken = QueryParameters::create($connection->httpRequest)->get('authToken'); + + $this->database + ->query("SELECT * FROM users WHERE auth_token = :token", ['token' => $authToken]) + ->then(function (Result $result) use ($connection) { + if (count($result->rows) === 0) { + $connection->send(json_encode([ + 'event' => 'authenticationFailed', + 'data' => [] + ])); + $connection->close(); + } + }); + } } diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..c9227d7 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,27 @@ + 'application/json'], + json_encode($responseData) + )); +} + +function respond_html(string $html, int $statusCode = 200) +{ + return str(new Response( + $statusCode, + ['Content-Type' => 'text/html'], + $html + )); +} diff --git a/composer.json b/composer.json index b4a5013..5c23455 100644 --- a/composer.json +++ b/composer.json @@ -21,10 +21,12 @@ "bfunky/http-parser": "^2.2", "cboden/ratchet": "^0.4.2", "clue/buzz-react": "^2.7", + "clue/reactphp-sqlite": "^1.0", "guzzlehttp/guzzle": "^6.5", "guzzlehttp/psr7": "dev-master as 1.6.1", "illuminate/http": "5.8.*|^6.0|^7.0", "illuminate/pipeline": "^7.6", + "illuminate/validation": "^7.7", "laminas/laminas-http": "^2.11", "laravel-zero/framework": "^7.0", "namshi/cuzzle": "^2.0", @@ -34,7 +36,8 @@ "riverline/multipart-parser": "^2.0", "symfony/expression-language": "^5.0", "symfony/http-kernel": "^4.0|^5.0", - "symfony/psr-http-message-bridge": "^2.0" + "symfony/psr-http-message-bridge": "^2.0", + "twig/twig": "^3.0" }, "require-dev": { "mockery/mockery": "^1.3.1", @@ -43,7 +46,10 @@ "autoload": { "psr-4": { "App\\": "app/" - } + }, + "files": [ + "app/helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index c17db95..aaeade2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fdb4509b6b8b8d83aeaef0e26caa1837", + "content-hash": "eb16eacae0b922bc590c0093325a423f", "packages": [ { "name": "bfunky/http-parser", @@ -167,6 +167,108 @@ ], "time": "2020-02-26T12:05:32+00:00" }, + { + "name": "clue/ndjson-react", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "767ec9543945802b5766fab0da4520bf20626f66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/767ec9543945802b5766fab0da4520bf20626f66", + "reference": "767ec9543945802b5766fab0da4520bf20626f66", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7 || ^0.6" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.0 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "time": "2020-02-04T11:48:52+00:00" + }, + { + "name": "clue/reactphp-sqlite", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-sqlite.git", + "reference": "8c7b89db764129275e22dc883b95c1e8c726332d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-sqlite/zipball/8c7b89db764129275e22dc883b95c1e8c726332d", + "reference": "8c7b89db764129275e22dc883b95c1e8c726332d", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "ext-sqlite3": "*", + "php": ">=5.4", + "react/child-process": "^0.6", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", + "react/promise": "^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.0 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\SQLite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "Async SQLite database, lightweight non-blocking process wrapper around file-based database extension (ext-sqlite3), built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-sqlite", + "keywords": [ + "async", + "database", + "non-blocking", + "reactphp", + "sqlite" + ], + "time": "2019-05-17T11:02:20+00:00" + }, { "name": "container-interop/container-interop", "version": "1.2.0", @@ -266,6 +368,68 @@ ], "time": "2019-10-30T19:59:35+00:00" }, + { + "name": "doctrine/lexer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "time": "2019-10-30T14:39:59+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v2.3.0", @@ -320,6 +484,64 @@ ], "time": "2019-03-31T00:38:28+00:00" }, + { + "name": "egulias/email-validator", + "version": "2.1.17", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ade6887fd9bd74177769645ab5c474824f8a418a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a", + "reference": "ade6887fd9bd74177769645ab5c474824f8a418a", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.10" + }, + "require-dev": { + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2020-02-13T22:36:52+00:00" + }, { "name": "evenement/evenement", "version": "v3.0.1", @@ -1260,6 +1482,105 @@ "homepage": "https://laravel.com", "time": "2020-04-16T17:12:54+00:00" }, + { + "name": "illuminate/translation", + "version": "v7.7.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/translation.git", + "reference": "74c6c0c15efc2a3e1a7e8b893dcbe68007467ea6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/translation/zipball/74c6c0c15efc2a3e1a7e8b893dcbe68007467ea6", + "reference": "74c6c0c15efc2a3e1a7e8b893dcbe68007467ea6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/contracts": "^7.0", + "illuminate/filesystem": "^7.0", + "illuminate/support": "^7.0", + "php": "^7.2.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Translation package.", + "homepage": "https://laravel.com", + "time": "2020-04-15T20:57:47+00:00" + }, + { + "name": "illuminate/validation", + "version": "v7.7.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/validation.git", + "reference": "99377aec3b5a2c2184d99de3dd7c8fb675e5a4ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/validation/zipball/99377aec3b5a2c2184d99de3dd7c8fb675e5a4ef", + "reference": "99377aec3b5a2c2184d99de3dd7c8fb675e5a4ef", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10", + "ext-json": "*", + "illuminate/container": "^7.0", + "illuminate/contracts": "^7.0", + "illuminate/support": "^7.0", + "illuminate/translation": "^7.0", + "php": "^7.2.5", + "symfony/http-foundation": "^5.0", + "symfony/mime": "^5.0" + }, + "suggest": { + "illuminate/database": "Required to use the database presence verifier (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Validation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Validation package.", + "homepage": "https://laravel.com", + "time": "2020-04-19T19:55:49+00:00" + }, { "name": "jolicode/jolinotif", "version": "v2.1.0", @@ -3101,6 +3422,49 @@ ], "time": "2019-07-11T13:45:28+00:00" }, + { + "name": "react/child-process", + "version": "v0.6.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", + "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/stream": "^1.0 || ^0.7.6" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", + "react/socket": "^1.0", + "sebastian/environment": "^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "time": "2019-02-15T13:48:16+00:00" + }, { "name": "react/dns", "version": "v1.2.0", @@ -5107,6 +5471,68 @@ ], "time": "2020-03-27T16:56:45+00:00" }, + { + "name": "twig/twig", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3b88ccd180a6b61ebb517aea3b1a8906762a1dc2", + "reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "time": "2020-02-11T15:33:47+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v4.1.4", diff --git a/config/app.php b/config/app.php index 10c90fa..362189c 100644 --- a/config/app.php +++ b/config/app.php @@ -55,6 +55,8 @@ return [ 'providers' => [ App\Providers\AppServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, ], ]; diff --git a/config/commands.php b/config/commands.php index 838b65f..ec54067 100644 --- a/config/commands.php +++ b/config/commands.php @@ -13,7 +13,7 @@ return [ | */ - 'default' => NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, + 'default' => \App\Commands\ShareCurrentWorkingDirectoryCommand::class, /* |-------------------------------------------------------------------------- @@ -60,6 +60,7 @@ return [ Illuminate\Console\Scheduling\ScheduleRunCommand::class, Illuminate\Console\Scheduling\ScheduleFinishCommand::class, Illuminate\Foundation\Console\VendorPublishCommand::class, + \App\Commands\ShareCurrentWorkingDirectoryCommand::class, ], /* diff --git a/database/migrations/01_create_users_table.sql b/database/migrations/01_create_users_table.sql new file mode 100644 index 0000000..307abb5 --- /dev/null +++ b/database/migrations/01_create_users_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name STRING NOT NULL, + auth_token STRING, + created_at DATETIME, + updated_at DATETIME +) diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..50cfad9 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,12 @@ +{ + "verbose": false, + "ignore": [ + ".git", + ".idea" + ], + "execMap": { + "php": "php" + }, + "restartable": "r", + "ext": "php" +} diff --git a/resources/views/admin/users/index.twig b/resources/views/admin/users/index.twig new file mode 100644 index 0000000..9776d52 --- /dev/null +++ b/resources/views/admin/users/index.twig @@ -0,0 +1,238 @@ + +
+ + + + + + + +