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
+
+
+ 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);
+ }
+}