From 53eaae33c50cbc95c1da5ab8d46582bf7c8bd7c3 Mon Sep 17 00:00:00 2001 From: "XANDAHQ\\Wai Yan Hein" Date: Fri, 15 Jan 2021 15:13:38 +0000 Subject: [PATCH] First commit --- .gitignore | 2 + .idea/modules.xml | 8 ++++ .idea/php.xml | 9 ++++ .idea/url-signer.iml | 13 +++++ .idea/workspace.xml | 62 ++++++++++++++++++++++++ composer.json | 19 ++++++++ readme.md | 23 +++++++++ src/URLSigner.php | 102 +++++++++++++++++++++++++++++++++++++++ tests/UrlSignerTest.php | 104 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 342 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/php.xml create mode 100644 .idea/url-signer.iml create mode 100644 .idea/workspace.xml create mode 100644 composer.json create mode 100644 readme.md create mode 100644 src/URLSigner.php create mode 100644 tests/UrlSignerTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6456ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +vendor \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..df1c035 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..a445b75 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/url-signer.iml b/.idea/url-signer.iml new file mode 100644 index 0000000..fce4448 --- /dev/null +++ b/.idea/url-signer.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..5bd4b9e --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,62 @@ + + + + + + + $PROJECT_DIR$/composer.json + + + + + + + + + + + + + + + + + + + + + + + + 1610723206984 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f449d38 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "waiyanhein/lumen-signed-url", + "description": "Package to generate temporary signd URL for Lumen framework", + "authors": [ + { + "name": "Wai Yan Hein", + "email": "iljimae.ic@gmail.com" + } + ], + "require": { + "php": ">=7.2.0", + "laravel/lumen-framework": ">=5.5.x-dev" + }, + "autoload": { + "psr-4": { + "Waiyanhein\\LumenSignedUrl\\": "src/" + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cf92ea0 --- /dev/null +++ b/readme.md @@ -0,0 +1,23 @@ +### Package to generate signed url for Lumen framework + +This package use Laravel file storage system to save the signed URLs, https://laravel.com/docs/8.x/filesystem. + +##### Installation +`composer require waiyanhein/lumen-signed-url` + +##### Generating temporary Signed URL +``` +$signedUrl = URLSigner::sign("http://testing.com", Carbon::now()->addMinutes(10)->format('Y-m-d H:i:s')); +``` +- Note: the date must be in `Y-m-d H:i:s` format. + +##### Signing URL with parameters +If your URL has parameters you can pass them as the third parameter as array. +``` +$signedUrl = URLSigner::sign("http://testing.com", Carbon::now()->addMinutes(10)->format('Y-m-d H:i:s'), [ 'first_name' = 'Wai', 'last_name' => 'Hein' ]); +``` + +##### Validating the Signed URL +``` +$isValid = URLSigner::validate($signedUrl); +``` diff --git a/src/URLSigner.php b/src/URLSigner.php new file mode 100644 index 0000000..8a459f3 --- /dev/null +++ b/src/URLSigner.php @@ -0,0 +1,102 @@ + 0) { + foreach ($params as $key => $value) { + $signedUrl = "{$signedUrl}&{$key}={$value}"; + } + } + $signedUrlData = [ + 'expire_at' => $expireAt, + 'signature' => $signature, + 'url' => $url, + 'signed_url' => $signedUrl, + 'params' => $params, + ]; + + $existingFileContent = ''; + if (Storage::exists(static::$FILENAME)) { + $existingFileContent = Storage::disk(static::$STORAGE_DISK)->get(static::$FILENAME); + } + $signedUrlDataList = []; + if ($existingFileContent) { + try { + $signedUrlDataList = json_decode($existingFileContent); + } catch (\Exception $e) { + + } + } + + $signedUrlDataList[] = $signedUrlData; + Storage::disk(static::$STORAGE_DISK)->put(static::$FILENAME, json_encode($signedUrlDataList)); + + return $signedUrl; + } + + public static function validate($signedUrl) + { + if (empty($signedUrl)) { + return false; + } + + $urlComponents = parse_url($signedUrl); + if (! isset($urlComponents['query'])) { + return false; + } + + $params = []; + parse_str($urlComponents['query'], $params); + + if (!(isset($params['expireAt']) && isset($params['signature']))) { + return false; + } + + try { + $expireAt = base64_decode($params['expireAt']); + $signature = $params['signature']; + $fileContent = Storage::disk(static::$STORAGE_DISK)->get(static::$FILENAME); + $signedUrlDataList = json_decode($fileContent); + if (! $signedUrlDataList) { + return false; + } + + $signedUrlDataList = collect($signedUrlDataList); + $signedUrl = $signedUrlDataList->where('signature', '=', $signature)->first(); + if (! $signedUrl) { + return false; + } + if ($signedUrl->expire_at != $expireAt) { + return false; + } + + return Carbon::createFromFormat('Y-m-d H:i:s', $expireAt)->gte(Carbon::now()); + } catch (\Exception $exception) { + return false; + } + } +} diff --git a/tests/UrlSignerTest.php b/tests/UrlSignerTest.php new file mode 100644 index 0000000..ccd78b8 --- /dev/null +++ b/tests/UrlSignerTest.php @@ -0,0 +1,104 @@ +delete(URLSigner::$FILENAME); + } + } + + public function testItCanGenerateSignedUrl() + { + $url = 'http://testing.com'; + $expireAt = Carbon::now()->addDays(5)->format('Y-m-d H:i:s'); + $signedUrl = URLSigner::sign($url, $expireAt); + + $this->assertNotEmpty($signedUrl); + //test the file content to ensure that the meta data is correct. + $fileContent = Storage::disk(URLSigner::$STORAGE_DISK)->get(URLSigner::$FILENAME); + $signedUrlList = json_decode($fileContent); + $this->assertEquals($expireAt, $signedUrlList[0]->expire_at); + $this->assertNotEmpty($signedUrlList[0]->signature); + $this->assertEquals($url, $signedUrlList[0]->url); + $encodedExpireAt = base64_encode($expireAt); + $this->assertEquals("{$url}?signature={$signedUrlList[0]->signature}&expireAt={$encodedExpireAt}", $signedUrlList[0]->signed_url); + } + + public function testItCanGenerateSignedUrlWithParams() + { + $url = 'http://testing.com'; + $expireAt = Carbon::now()->addDays(5)->format('Y-m-d H:i:s'); + $params = [ + 'first_name' => 'Wai', + 'last_name' => 'Hein', + ]; + $signedUrl = URLSigner::sign($url, $expireAt, $params); + + $this->assertNotEmpty($signedUrl); + //test the file content to ensure that the meta data is correct. + $fileContent = Storage::disk(URLSigner::$STORAGE_DISK)->get(URLSigner::$FILENAME); + $signedUrlList = json_decode($fileContent); + $encodedExpireAt = base64_encode($expireAt); + $this->assertEquals("{$url}?signature={$signedUrlList[0]->signature}&expireAt={$encodedExpireAt}&first_name=Wai&last_name=Hein", $signedUrlList[0]->signed_url); + $this->assertEquals('Wai', $signedUrlList[0]->params->first_name); + $this->assertEquals('Hein', $signedUrlList[0]->params->last_name); + } + + public function testSignedUrlValidationPasses() + { + $url = 'http://testing.com'; + $expireAt = Carbon::now()->addSeconds(10)->format('Y-m-d H:i:s'); + $params = [ + 'first_name' => 'Wai', + 'last_name' => 'Hein', + ]; + $signedUrl = URLSigner::sign($url, $expireAt, $params); + + $this->assertTrue(URLSigner::validate($signedUrl)); + } + + public function testSignedUrlValidationFails() + { + $url = 'http://testing.com'; + $expireAt = Carbon::now()->subSeconds(5)->format('Y-m-d H:i:s'); + $params = [ + 'first_name' => 'Wai', + 'last_name' => 'Hein', + ]; + $signedUrl = URLSigner::sign($url, $expireAt, $params); + + $this->assertFalse(URLSigner::validate($signedUrl)); + } + + public function testItSavesMultipleSignedUrlsInStorage() + { + $urls = [ + [ + 'url' => 'http://testing1.com', + 'expire_at' => Carbon::now()->addMinutes(5)->format('Y-m-d H:i:s'), + ], + [ + 'url' => 'http://testing2.com', + 'expire_at' => Carbon::now()->addMinutes(3)->format('Y-m-d H:i:s'), + ] + ]; + + $signedUrls = []; + foreach ($urls as $url) { + $signedUrls[] = URLSigner::sign($url['url'], $url['expire_at']); + } + + $fileContent = Storage::disk(URLSigner::$STORAGE_DISK)->get(URLSigner::$FILENAME); + $signedUrlList = json_decode($fileContent); + $this->assertEquals(2, count($signedUrlList)); + $this->assertEquals($signedUrls[0], $signedUrlList[0]->signed_url); + $this->assertEquals($signedUrls[1], $signedUrlList[1]->signed_url); + } +}