From 4b9e117f1ac1c550ccfb22b7715bc33c6c6aaf2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Preu=C3=9F?= Date: Thu, 22 Jul 2021 17:28:09 +0200 Subject: [PATCH] Improve lock file --- .gitignore | 3 +- README.md | 12 ++++- app/Bunny/Filesystem/CompareOptions.php | 4 +- app/Bunny/Filesystem/EdgeStorageCache.php | 18 ++++--- app/Bunny/Filesystem/FileCompare.php | 6 +-- app/Bunny/Lock/Exceptions/LockException.php | 13 +++++ app/Bunny/Lock/Lock.php | 57 +++++++++++++++++++++ app/Commands/DeployCommand.php | 10 ++-- 8 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 app/Bunny/Lock/Exceptions/LockException.php create mode 100644 app/Bunny/Lock/Lock.php diff --git a/.gitignore b/.gitignore index 50fb19d..d402b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ .phpunit.result.cache .env *.bk +bunny-cli.lock +bunny-cli.lock.bk /dist* -*.sha256 diff --git a/README.md b/README.md index 065ef77..177f8b4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ With Bunny CDN's Storage Edge, your web applications benefit from replicated sto Bunny CLI currently only comes with a `deploy` command. With this command, you can easily synconizise your `dist` folder with your edge storage. -> All files in the edge storage that are **not** in your local `dist` directory will be deleted. +> **IMPORTANT**: All files in the edge storage that are **not** in your local `dist` directory will be deleted. ```plain ➜ $ bunny deploy @@ -43,6 +43,16 @@ We offer you a [GitHub Action for Bunny CLI](https://github.com/marketplace/acti args: deploy --dir=dist ``` +## Secure your `.well-known/bunny-cli.lock` file + +Bunny CLI generates a lock file, which by default is located at `.well-known/bunny-cli.lock`. This file locks the files of your project to a known state. To prevent this from being publicly accessible it is recommended to create a new edge rule in your pull zone. You can use the following example as a template: + +Action: `Block Request` +Condition Matching: `Match Any` +Condition: If `Request URL` `Match Any` `*/.well-known/bunny-cli.lock` + +Now the file should no longer be accessible. It can take a few minutes until your Edge Rule is active. + ## Frequently Asked Questions ### Q: Is this a zero-downtime deployment? diff --git a/app/Bunny/Filesystem/CompareOptions.php b/app/Bunny/Filesystem/CompareOptions.php index 6abf001..35b7376 100644 --- a/app/Bunny/Filesystem/CompareOptions.php +++ b/app/Bunny/Filesystem/CompareOptions.php @@ -7,8 +7,8 @@ namespace App\Bunny\Filesystem; class CompareOptions { const START = 'start'; - const NO_SHA256_CACHE = 'no_sha256_cache'; + const NO_SHA256_VERIFICATION = 'no_sha256_verification'; const NO_SHA256_GENERATION = 'no_sha256_generation'; - const SHA256_NAME = 'sha256_name'; + const LOCK_FILE = 'lock_file'; const DRY_RUN = 'dry-run'; } diff --git a/app/Bunny/Filesystem/EdgeStorageCache.php b/app/Bunny/Filesystem/EdgeStorageCache.php index ddf42e2..6623730 100644 --- a/app/Bunny/Filesystem/EdgeStorageCache.php +++ b/app/Bunny/Filesystem/EdgeStorageCache.php @@ -4,12 +4,14 @@ namespace App\Bunny\Filesystem; use App\Bunny\Filesystem\Exceptions\FileNotFoundException; use App\Bunny\Filesystem\Exceptions\FilesystemException; +use App\Bunny\Lock\Exceptions\LockException; +use App\Bunny\Lock\Lock; use Psr\Http\Message\ResponseInterface; class EdgeStorageCache { private EdgeStorage $edgeStorage; - private string $filename = '.well-known/bunny.sha256'; + private string $filename = Lock::DEFAULT_FILENAME; public function __construct(EdgeStorage $edgeStorage) { @@ -47,18 +49,22 @@ class EdgeStorageCache return $response->getStatusCode() === 200; } + /** + * @throws FileNotFoundException + * @throws LockException + */ private function extract(string $contents): array { - if (!$array = json_decode($contents, true)) { - throw new FileNotFoundException('Cannot parse cache file.'); - } + $lock = Lock::parse($contents, $this->filename); - return array_map(fn(array $x) => EdgeFile::fromArray($x), $array); + return array_map(fn(array $x) => EdgeFile::fromArray($x), $lock->getFiles()); } private function hydrate(array $files, string $search = '', string $replace = ''): string { - return json_encode(array_map(fn(LocalFile $x) => $x->toArray($search, $replace), $files), JSON_PRETTY_PRINT); + return Lock::fromFiles( + array_map(fn(LocalFile $x) => $x->toArray($search, $replace), $files) + )->toString(); } public function setFilename(string $filename) diff --git a/app/Bunny/Filesystem/FileCompare.php b/app/Bunny/Filesystem/FileCompare.php index 00576c9..84d8e96 100644 --- a/app/Bunny/Filesystem/FileCompare.php +++ b/app/Bunny/Filesystem/FileCompare.php @@ -168,9 +168,9 @@ class FileCompare */ private function getEdgeFiles(array $options, string $edge, int $expectedMax): array { - $this->edgeStorage->getStorageCache()->setFilename($options[CompareOptions::SHA256_NAME]); + $this->edgeStorage->getStorageCache()->setFilename($options[CompareOptions::LOCK_FILE]); - if ($options[CompareOptions::NO_SHA256_CACHE]) { + if ($options[CompareOptions::NO_SHA256_VERIFICATION]) { return $this->getAllFilesRecursive($expectedMax, $edge); } @@ -180,7 +180,7 @@ class FileCompare } catch (FileNotFoundException $exception) { $this->command->warn(sprintf( '⚠ Cannot fetch %s from storage due "%s". Using recursive fallback...', - $options[CompareOptions::SHA256_NAME], + $options[CompareOptions::LOCK_FILE], $exception->getMessage() )); return $this->getAllFilesRecursive($expectedMax, $edge); diff --git a/app/Bunny/Lock/Exceptions/LockException.php b/app/Bunny/Lock/Exceptions/LockException.php new file mode 100644 index 0000000..4c6e3bb --- /dev/null +++ b/app/Bunny/Lock/Exceptions/LockException.php @@ -0,0 +1,13 @@ +contents['version'] = 1; + $this->contents['_readme'] = [ + 'This file locks the files of your project to a known state', + 'Read more about it at https://github.com/own3d/bunny-cli/wiki', + 'This file is @generated automatically' + ]; + $this->contents['files'] = $contents['files'] ?? []; + } + + public static function parse(string $contents, string $filename = self::DEFAULT_FILENAME): self + { + if (!$array = json_decode($contents, true)) { + throw new FileNotFoundException(sprintf('Cannot decode %s file.', $filename)); + } + + if (!isset($array['version']) || $array['version'] !== 1) { + throw LockException::fromInvalidVersion($array['version'] ?? 'undefined'); + } + + return new self($array); + } + + public static function fromFiles(array $files): self + { + return new self(['files' => $files]); + } + + public function getFiles(): array + { + return $this->contents['files']; + } + + public function toArray(): array + { + return $this->contents; + } + + public function toString(): string + { + return json_encode($this->toArray(), JSON_PRETTY_PRINT); + } +} diff --git a/app/Commands/DeployCommand.php b/app/Commands/DeployCommand.php index 9e1dd8a..f49a20d 100644 --- a/app/Commands/DeployCommand.php +++ b/app/Commands/DeployCommand.php @@ -19,9 +19,9 @@ class DeployCommand extends Command */ protected $signature = 'deploy {--dir=dist : Root directory to upload} - {--no-sha256-cache : Skips .well-known/bunny.sha256 and queries the storage endpoints recursively instead} - {--no-sha256-generation : Skips .well-known/bunny.sha256 generation} - {--sha256-name=.well-known/bunny.sha256 : Change filename of .well-known/bunny.sha256} + {--no-sha256-verification : Skips checksum verification from bunny-cli.lock and polls the storage api recursively instead} + {--no-sha256-generation : Skips checksum generation for bunny-cli.lock} + {--lock-file=.well-known/bunny-cli.lock : Changes the location and filename of .well-known/bunny-cli.lock} {--dry-run : Outputs the operations but will not execute anything}'; /** @@ -62,9 +62,9 @@ class DeployCommand extends Command try { $fileCompare->compare($localPath, $edgePath, [ CompareOptions::START => $start, - CompareOptions::NO_SHA256_CACHE => $this->option('no-sha256-cache'), + CompareOptions::NO_SHA256_VERIFICATION => $this->option('no-sha256-verification'), CompareOptions::NO_SHA256_GENERATION => $this->option('no-sha256-generation'), - CompareOptions::SHA256_NAME => $this->option('sha256-name'), + CompareOptions::LOCK_FILE => $this->option('lock-file'), CompareOptions::DRY_RUN => $this->option('dry-run'), ]); } catch (FilesystemException $exception) {