From 191ba390103d04af277b5f9f56453e21b0327d42 Mon Sep 17 00:00:00 2001 From: "neelkanth.kaushik" Date: Tue, 29 Sep 2020 20:52:42 +0530 Subject: [PATCH] First Commit with complete code --- LICENSE.md | 21 ++ README.md | 280 ++++++++++++++++++ composer.json | 32 ++ config/surveillance.php | 27 ++ .../create_surveillance_logs_table.php.stub | 39 +++ ...reate_surveillance_managers_table.php.stub | 39 +++ resources/lang/en/surveillance.php | 19 ++ src/Console/Commands/SurveillanceBlock.php | 69 +++++ src/Console/Commands/SurveillanceDisable.php | 72 +++++ src/Console/Commands/SurveillanceEnable.php | 69 +++++ .../Commands/SurveillanceRemoveRecord.php | 77 +++++ src/Console/Commands/SurveillanceUnblock.php | 72 +++++ src/Exceptions/SurveillanceRecordNotFound.php | 20 ++ src/Facades/Surveillance.php | 13 + src/Helpers/ConsoleHelper.php | 61 ++++ .../Middleware/SurveillanceMiddleware.php | 53 ++++ .../SurveillanceLogRepository.php | 92 ++++++ .../SurveillanceManagerRepository.php | 234 +++++++++++++++ src/Interfaces/SurveillanceLogsInterface.php | 22 ++ .../SurveillanceManagerInterface.php | 75 +++++ src/Models/SurveillanceLog.php | 9 + src/Models/SurveillanceManager.php | 21 ++ src/Providers/SurveillanceServiceProvider.php | 68 +++++ src/Services/Surveillance.php | 128 ++++++++ src/Traits/ValidateCommand.php | 32 ++ 25 files changed, 1644 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/surveillance.php create mode 100644 database/migrations/create_surveillance_logs_table.php.stub create mode 100644 database/migrations/create_surveillance_managers_table.php.stub create mode 100644 resources/lang/en/surveillance.php create mode 100644 src/Console/Commands/SurveillanceBlock.php create mode 100644 src/Console/Commands/SurveillanceDisable.php create mode 100644 src/Console/Commands/SurveillanceEnable.php create mode 100644 src/Console/Commands/SurveillanceRemoveRecord.php create mode 100644 src/Console/Commands/SurveillanceUnblock.php create mode 100644 src/Exceptions/SurveillanceRecordNotFound.php create mode 100644 src/Facades/Surveillance.php create mode 100644 src/Helpers/ConsoleHelper.php create mode 100644 src/Http/Middleware/SurveillanceMiddleware.php create mode 100644 src/Implementations/SurveillanceLogRepository.php create mode 100644 src/Implementations/SurveillanceManagerRepository.php create mode 100644 src/Interfaces/SurveillanceLogsInterface.php create mode 100644 src/Interfaces/SurveillanceManagerInterface.php create mode 100644 src/Models/SurveillanceLog.php create mode 100644 src/Models/SurveillanceManager.php create mode 100644 src/Providers/SurveillanceServiceProvider.php create mode 100644 src/Services/Surveillance.php create mode 100644 src/Traits/ValidateCommand.php diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fe1759a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Neelkanth Kaushik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f496ddd --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +![Laravel Surveillance Logo](https://github.com/neelkanthk/repo_logos/blob/master/surveillance_small.png?raw=true) + +# Surveillance + +A Laravel package to put malicious users, IP addresses and anonymous browser fingerprints under surveillance, write surveillance logs and block malicious ones from accessing the app. + +#### NOTE: This package does not provide a client side library for browser fingerprinting. [FingerprintJS Open Source](https://github.com/fingerprintjs/fingerprintjs) is a good library to use for client side browser fingerprinting. + +__This package provides__: + +_1. A middleware to be used on routes._ + +_2. A command line interface to enable/disable surveillance and block/unblock access._ + +_3. A fluent API to programmatically enable/disable surveillance, block/unblock access and log the requests at runtime._ + +_4. By default the package used MySQL database as storage but the package can be extended to use virtually any storage technology._ + +### Minimum Requirements + +#### 1. Laravel 6.0 +#### 2. PHP 7.2 + +## Installation + +#### 1. Install the package via composer: + +```bash +composer require neelkanthk/laravel-surveillance +``` + +#### 2.1. Publish the migration files: +```bash +php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="migrations" +``` + +#### 2.2. Publish language files: +```bash +php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="lang" +``` + +#### 3. Run the migrations +```bash +php artisan migrate +``` + +#### 4. After migrations have been run two tables will be created in the database namely `surveillance_managers` and `surveillance_logs` + +#### 5. You can publish the config file with: +```bash +php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="config" +``` + +This is the contents of the file that will be published at `config/surveillance.php`: + + +```php +return [ + + /* + * The name of the header to be used for browser fingerprint + */ + "fingerprint-header-key" => "fingerprint", + + /* + * This class is responsible enabling, disabling, blocking and unblocking. + * To override the default functionality extend the below class and provide its name here. + */ + "manager-repository" => 'Neelkanth\Laravel\Surveillance\Implementations\SurveillanceManagerRepository', + + /* + * This class is responsible for logging the surveillance enabled requests + * To override the default functionality extend the below class and provide its name here. + */ + "log-repository" => 'Neelkanth\Laravel\Surveillance\Implementations\SurveillanceLogRepository', + + /* + * The types which are allowed currently. + * DO NOT MODIFY THESE + */ + "allowed-types" => ["userid", "ip", "fingerprint"] +]; +``` + +## CLI Usage + +#### Enable surveillance for an IP Address +```bash +php artisan surveillance:enable ip 192.1.2.4 +``` + +#### Disable surveillance for an IP Address +```bash +php artisan surveillance:disable ip 192.1.2.4 +``` + +#### Enable surveillance for a User ID +```bash +php artisan surveillance:enable userid 1234 +``` + +#### Disable surveillance for a User ID +```bash +php artisan surveillance:disable userid 1234 +``` + +#### Enable surveillance for Browser Fingerprint +```bash +php artisan surveillance:enable fingerprint hjP0tLyIUy7SXaSY6gyb +``` + +#### Disable surveillance for Browser Fingerprint +```bash +php artisan surveillance:disable fingerprint hjP0tLyIUy7SXaSY6gyb +``` + +#### Block an IP Address +```bash +php artisan surveillance:block ip 192.1.2.4 +``` + +#### UnBlock an IP Address +```bash +php artisan surveillance:unblock ip 192.1.2.4 +``` + +#### Block a User ID +```bash +php artisan surveillance:block userid 1234 +``` + +#### UnBlock a User ID +```bash +php artisan surveillance:unblock userid 1234 +``` + +#### Block a Browser Fingerprint +```bash +php artisan surveillance:block fingerprint hjP0tLyIUy7SXaSY6gyb +``` + +#### UnBlock a Browser Fingerprint +```bash +php artisan surveillance:unblock fingerprint hjP0tLyIUy7SXaSY6gyb +``` + +#### Remove a Surveillance record from Database +```bash +php artisan surveillance:remove ip 192.5.4.3 +``` + +## Middleware Usage + +#### You can use the 'surveillance' middleware on any route or route group just like any other middleware. + +```php +Route::middleware(["surveillance"])->get('/', function () { + +}); +``` + +## Programmatic Usage + +#### Enable Surveillance + +```php +use Neelkanth\Laravel\Surveillance\Services\Surveillance; +Surveillance::manager()->type("ip")->value("192.5.4.1")->enableSurveillance(); +``` + +#### Block Access + +```php +use Neelkanth\Laravel\Surveillance\Services\Surveillance; +Surveillance::manager()->type("userid")->value(2121)->blockAccess(); +``` + +#### Logging a Request (Works when surveillance in enabled on User ID, IP Address or Browser Fingerprint) + +```php +use Neelkanth\Laravel\Surveillance\Services\Surveillance; +Surveillance::logger()->writeLog(); +``` + +## Allowed Types + +#### Currently only userid, ip and fingerprint types are allowed. + +## Customizing and Overriding the defaults + +### To override the default surveillance management funtionality + +#### Step 1: Extend the `SurveillanceManagerRepository` Class and override all of its methods + +```php +//Example repository to use MongoDB instead of MySQL +namespace App; + +use Neelkanth\Laravel\Surveillance\Implementations\SurveillanceManagerRepository; +use Illuminate\Support\Carbon; + +class SurveillanceManagerMongoDbRepository extends SurveillanceManagerRepository +{ + public function enableSurveillance() + { + $surveillance = $this->getRecord(); + if (is_null($surveillance)) { + $surveillance["type"] = $this->getType(); + $surveillance["value"] = $this->getValue(); + } + $surveillance["surveillance_enabled"] = 1; + $surveillance["surveillance_enabled_at"] = Carbon::now()->toDateTimeString(); + $collection = (new \MongoDB\Client)->surveillance->manager; + $insertOneResult = $collection->insertOne($surveillance); + return $insertOneResult; + } +} +``` + +#### Step 2: Provide the custom class in the `config/surveillance.php` file's `manager-repository` key + +```php +/* + * This class is responsible enabling, disabling, blocking and unblocking. + * To override the default functionality extend the below class and provide its name here. + */ +"manager-repository" => 'App\SurveillanceManagerMongoDbRepository', +``` + +### To override the default logging funtionality + +#### Step 1: Extend the `SurveillanceLogRepository` Class and override all of its methods + +```php + +//Example repository to write Logs in MongoDB instead of MySQL +namespace App; + +use Neelkanth\Laravel\Surveillance\Implementations\SurveillanceLogRepository; + +class SurveillanceLogMongoDbRepository extends SurveillanceLogRepository +{ + public function writeLog($dataToLog = null) + { + if (!is_null($dataToLog)) { + $this->setLogToWrite($dataToLog); + } + $log = $this->getLogToWrite(); + if (!empty($log) && is_array($log)) { + $collection = (new \MongoDB\Client)->surveillance->logs; + $insertOneResult = $collection->insertOne($log); + } + } +} +``` + +#### Step 2: Provide the custom class in the `config/surveillance.php` file's `log-repository` key + +```php +/* + * This class is responsible for logging the surveillance enabled requests + * To override the default functionality extend the below class and provide its name here. +*/ +"log-repository" => 'App\SurveillanceLogMongoDbRepository', +``` + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +## Security +If you discover any security-related issues, please email me.neelkanth@gmail.com instead of using the issue tracker. + +## Credits + +- [Neelkanth Kaushik](https://github.com/neelkanthk) +- [All Contributors](../../contributors) +- [CCTV Icon](https://pixabay.com/vectors/image-sign-warning-icon-cctv-3042333) + +## License +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..280a19e --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "neelkanthk/laravel-surveillance", + "description": "Put users, IP addresses and anonymous browser fingerprints on surveillance and block malicious ones.", + "keywords": ["laravel", "laravel-package", "laravel-security", "php", "fingerprintjs", "fingeprintjs2", "access-control"], + "type": "laravel-package", + "license": "MIT", + "authors": [ + { + "name": "Neelkanth Kaushik", + "email": "me.neelkanth@gmail.com" + } + ], + "require": { + "php": "^7.2", + "laravel/framework": "^6.0" + }, + "autoload": { + "psr-4": { + "Neelkanth\\Laravel\\Surveillance\\": "src" + } + }, + "extra": { + "laravel": { + "providers": [ + "Neelkanth\\Laravel\\Surveillance\\Providers\\SurveillanceServiceProvider" + ], + "aliases": { + "Surveillance": "Neelkanth\\Laravel\\Surveillance\\Facades\\Surveillance" + } + } + } +} diff --git a/config/surveillance.php b/config/surveillance.php new file mode 100644 index 0000000..178ac5f --- /dev/null +++ b/config/surveillance.php @@ -0,0 +1,27 @@ + "fingerprint", + + /* + * This class is responsible enabling, disabling, blocking and unblocking. + * To override the default functionality extend the below class and provide its name here. + */ + "manager-repository" => 'Neelkanth\Laravel\Surveillance\Implementations\SurveillanceManagerRepository', + + /* + * This class is responsible for logging the surveillance enabled requests + * To override the default functionality extend the below class and provide its name here. + */ + "log-repository" => 'Neelkanth\Laravel\Surveillance\Implementations\SurveillanceLogRepository', + + /* + * The types which are allowed currently. + * DO NOT MODIFY THESE + */ + "allowed-types" => ["userid", "ip", "fingerprint"] +]; diff --git a/database/migrations/create_surveillance_logs_table.php.stub b/database/migrations/create_surveillance_logs_table.php.stub new file mode 100644 index 0000000..b3460f0 --- /dev/null +++ b/database/migrations/create_surveillance_logs_table.php.stub @@ -0,0 +1,39 @@ +increments('id'); + $table->string("fingerprint")->nullable(); + $table->string("userid")->nullable(); + $table->string("ip")->nullable(); + $table->text("url")->nullable(); + $table->text("user_agent")->nullable(); + $table->text("cookies")->nullable(); + $table->text("session")->nullable(); + $table->text("files")->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('surveillance_logs'); + } +} diff --git a/database/migrations/create_surveillance_managers_table.php.stub b/database/migrations/create_surveillance_managers_table.php.stub new file mode 100644 index 0000000..2c9891d --- /dev/null +++ b/database/migrations/create_surveillance_managers_table.php.stub @@ -0,0 +1,39 @@ +increments('id'); + $table->string("type"); + $table->string("value"); + $table->unsignedTinyInteger("surveillance_enabled")->nullable(); + $table->unsignedTinyInteger("access_blocked")->nullable(); + $table->timestamp("surveillance_enabled_at")->nullable(); + $table->timestamp("surveillance_disabled_at")->nullable(); + $table->timestamp("access_blocked_at")->nullable(); + $table->timestamp("access_unblocked_at")->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('surveillance_managers'); + } +} diff --git a/resources/lang/en/surveillance.php b/resources/lang/en/surveillance.php new file mode 100644 index 0000000..1b02db6 --- /dev/null +++ b/resources/lang/en/surveillance.php @@ -0,0 +1,19 @@ + [ + 'enabled' => "\nSurveillance has been enabled.\n", + 'already-enabled' => "\nSurveillance is already enabled.\n", + 'disabled' => "\nSurveillance has been disabled.\n", + 'already-enabled' => "\nSurveillance is already disabled.\n", + 'blocked' => "\nAccess has been blocked.\n", + 'already-blocked' => "\nAccess is already blocked.\n", + 'unblocked' => "\nAccess has been unblocked.\n", + 'already-unblocked' => "\nAccess is already unblocked.\n", + 'record-removed' => "\nSurveillance record(s) removed.\n", + "record-not-found" => "\nNo existing record(s) found for type: [:type] and value: [:value].\n", + "confirm" => "Are you sure?", + "console-validation-failed" => "\nValidation failed.\n", + "console-allowed-types" => "Invalid 'type'. Only following types are allowed: [ :types ]" + ] +]; diff --git a/src/Console/Commands/SurveillanceBlock.php b/src/Console/Commands/SurveillanceBlock.php new file mode 100644 index 0000000..1375e5e --- /dev/null +++ b/src/Console/Commands/SurveillanceBlock.php @@ -0,0 +1,69 @@ +surveillance = $binding; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + try { + $this->validateArguments($this->arguments()); + $type = $this->argument("type"); + $value = $this->argument("value"); + $surveillance = $this->surveillance->setType($type)->setValue($value); + if ($surveillance->isAccessBlocked()) { + $this->info(trans("surveillance.messages.already-blocked")); + } else { + $surveillance->blockAccess(); + if ($surveillance->isAccessBlocked()) { + $this->info(trans("surveillance.messages.blocked")); + } + } + $output = ConsoleHelper::formatConsoleTableOutput($surveillance->getRecord()); + if (!empty($output["rows"])) { + $this->table($output["header"], $output["rows"]); + } + } catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } +} diff --git a/src/Console/Commands/SurveillanceDisable.php b/src/Console/Commands/SurveillanceDisable.php new file mode 100644 index 0000000..e7914c9 --- /dev/null +++ b/src/Console/Commands/SurveillanceDisable.php @@ -0,0 +1,72 @@ +surveillance = $binding; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + try { + $this->validateArguments($this->arguments()); + $type = $this->argument("type"); + $value = $this->argument("value"); + $surveillance = $this->surveillance->setType($type)->setValue($value); + if (is_null($surveillance->getRecord())) { + $this->error(trans("surveillance.messages.record-not-found", ["type" => $type, "value" => $value])); + exit(1); + } else if (!$surveillance->isSurveillanceEnabled()) { + $this->info(trans("surveillance.messages.already-disabled")); + } else { + $surveillance->disableSurveillance(); + if (!$surveillance->isSurveillanceEnabled()) { + $this->info(trans("surveillance.messages.disabled")); + } + } + $output = ConsoleHelper::formatConsoleTableOutput($surveillance->getRecord()); + if (!empty($output["rows"])) { + $this->table($output["header"], $output["rows"]); + } + } catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } +} diff --git a/src/Console/Commands/SurveillanceEnable.php b/src/Console/Commands/SurveillanceEnable.php new file mode 100644 index 0000000..e50d772 --- /dev/null +++ b/src/Console/Commands/SurveillanceEnable.php @@ -0,0 +1,69 @@ +surveillance = $binding; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + try { + $this->validateArguments($this->arguments()); + $type = $this->argument("type"); + $value = $this->argument("value"); + $surveillance = $this->surveillance->setType($type)->setValue($value); + if ($surveillance->isSurveillanceEnabled()) { + $this->info(trans("surveillance.messages.already-enable")); + } else { + $surveillance->enableSurveillance(); + if ($surveillance->isSurveillanceEnabled()) { + $this->info(trans("surveillance.messages.enabled")); + } + } + $output = ConsoleHelper::formatConsoleTableOutput($surveillance->getRecord()); + if (!empty($output["rows"])) { + $this->table($output["header"], $output["rows"]); + } + } catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } +} diff --git a/src/Console/Commands/SurveillanceRemoveRecord.php b/src/Console/Commands/SurveillanceRemoveRecord.php new file mode 100644 index 0000000..0613557 --- /dev/null +++ b/src/Console/Commands/SurveillanceRemoveRecord.php @@ -0,0 +1,77 @@ +surveillance = $binding; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + try { + $this->validateArguments($this->arguments()); + $type = $this->argument("type"); + $value = $this->argument("value"); + $surveillance = $this->surveillance->setType($type)->setValue($value); + + $records = $surveillance->getRecords(); + if ($records->count() > 0) { + $output = ConsoleHelper::formatConsoleTableOutput($surveillance->getRecords()); + if (!empty($output["rows"])) { + $this->table($output["header"], $output["rows"]); + } + + $confirmation = $this->confirm(trans("surveillance.messages.confirm")); + + if ($confirmation) { + $removeStatus = $surveillance->removeRecord(); + } + if ($removeStatus) { + $this->info(trans("surveillance.messages.record-removed")); + } + } else { + $this->error(trans("surveillance.messages.record-not-found", ["type" => $type, "value" => $value])); + exit(1); + } + } catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } +} diff --git a/src/Console/Commands/SurveillanceUnblock.php b/src/Console/Commands/SurveillanceUnblock.php new file mode 100644 index 0000000..791e1c1 --- /dev/null +++ b/src/Console/Commands/SurveillanceUnblock.php @@ -0,0 +1,72 @@ +surveillance = $binding; + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle() + { + try { + $this->validateArguments($this->arguments()); + $type = $this->argument("type"); + $value = $this->argument("value"); + $surveillance = $this->surveillance->setType($type)->setValue($value); + if (is_null($surveillance->getRecord())) { + $this->error(trans("surveillance.messages.record-not-found", ["type" => $type, "value" => $value])); + exit(1); + } else if (!$surveillance->isAccessBlocked()) { + $this->info(trans("surveillance.messages.already-unblocked")); + } else { + $surveillance->unblockAccess(); + if (!$surveillance->isAccessBlocked()) { + $this->info(trans("surveillance.messages.unblocked")); + } + } + $output = ConsoleHelper::formatConsoleTableOutput($surveillance->getRecord()); + if (!empty($output["rows"])) { + $this->table($output["header"], $output["rows"]); + } + } catch (Exception $ex) { + $this->error($ex->getMessage()); + } + } +} diff --git a/src/Exceptions/SurveillanceRecordNotFound.php b/src/Exceptions/SurveillanceRecordNotFound.php new file mode 100644 index 0000000..ff84940 --- /dev/null +++ b/src/Exceptions/SurveillanceRecordNotFound.php @@ -0,0 +1,20 @@ +message = 'No record found.'; + $this->code = 1404; + parent::__construct($this->message, $this->code); + } + + public function __toString() + { + return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; + } +} diff --git a/src/Facades/Surveillance.php b/src/Facades/Surveillance.php new file mode 100644 index 0000000..45f477c --- /dev/null +++ b/src/Facades/Surveillance.php @@ -0,0 +1,13 @@ + $header, + "rows" => $rows + ]; + } + + /** + * Console Table Output Transformer + * + * @param SurveillanceManager $model + * @return void + */ + public static function transform(SurveillanceManager $model) + { + $row = new stdClass(); + $row->id = $model->id; + $row->type = $model->type; + $row->value = $model->value; + $row->surveillance_enabled = ($model->surveillance_enabled == 1) ? "Yes" : "No"; + $row->access_blocked = ($model->access_blocked == 1) ? "Yes" : "No"; + $row->surveillance_enabled_at = is_null($model->surveillance_enabled_at) ? "-" : $model->surveillance_enabled_at->toDateTimeString(); + $row->surveillance_disabled_at = is_null($model->surveillance_disabled_at) ? "-" : $model->surveillance_disabled_at->toDateTimeString(); + $row->access_blocked_at = is_null($model->access_blocked_at) ? "-" : $model->access_blocked_at->toDateTimeString(); + $row->access_unblocked_at = is_null($model->access_unblocked_at) ? "-" : $model->access_unblocked_at->toDateTimeString(); + $row->created_at = is_null($model->created_at) ? "-" : $model->created_at->toDateTimeString(); + $row->updated_at = is_null($model->updated_at) ? "-" : $model->updated_at->toDateTimeString(); + return (array) $row; + } +} diff --git a/src/Http/Middleware/SurveillanceMiddleware.php b/src/Http/Middleware/SurveillanceMiddleware.php new file mode 100644 index 0000000..046dfe3 --- /dev/null +++ b/src/Http/Middleware/SurveillanceMiddleware.php @@ -0,0 +1,53 @@ +surveillanceManager = $manager; + $this->surveillanceLog = $log; + } + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + //Fingerprint + $fingerprintHeaderKey = config("surveillance.fingerprint-header-key"); + $fingerprint = $request->header($fingerprintHeaderKey); + //User id + $userId = Auth::id(); + //IP Address + $ipAddress = $request->ip(); + //Check if any of the above is blocked + $isAccessBlocked = $this->surveillanceManager->isAccessBlocked($userId, $ipAddress, $fingerprint); + if ($isAccessBlocked) { + abort(403); + } else { + //Check if surveillance is enabled on any of the above + $isSurveillanceEnabled = $this->surveillanceManager->isSurveillanceEnabled($userId, $ipAddress, $fingerprint); + + if ($isSurveillanceEnabled) { + //Log the request + $this->surveillanceLog->makeLogFromRequest($request)->writeLog(); + } + } + return $next($request); + } +} diff --git a/src/Implementations/SurveillanceLogRepository.php b/src/Implementations/SurveillanceLogRepository.php new file mode 100644 index 0000000..367689e --- /dev/null +++ b/src/Implementations/SurveillanceLogRepository.php @@ -0,0 +1,92 @@ +log = $log; + return $this; + } + + /** + * Get the log to write + * + * @return void + */ + public function getLogToWrite() + { + return $this->log; + } + + /** + * Retrieve the data from Request for logging + * + * @param Request $request + * @return void + */ + public function makeLogFromRequest(Request $request) + { + $fingerprintHeaderKey = config("surveillance.fingerprint-header-key"); + $cookies = $request->cookies->all(); + $files = $request->allFiles(); + $session = $request->session()->all(); + $this->log = [ + "fingerprint" => $request->header($fingerprintHeaderKey), + "userid" => Auth::id(), + "ip" => $request->ip(), + "url" => $request->fullUrl(), + "user_agent" => $request->userAgent(), + "cookies" => !empty($cookies) ? json_encode($cookies) : null, + "files" => !empty($files) ? json_encode($files) : null, + "session" => !empty($session) ? json_encode($session) : null + ]; + return $this; + } + + /** + * Write the log in the database + * + * @param [array] $dataToLog + * @return SurveillanceLog + */ + public function writeLog($dataToLog = null) + { + if (!is_null($dataToLog)) { + $this->setLogToWrite($dataToLog); + } + $log = $this->getLogToWrite(); + if (!empty($log) && is_array($log)) { + $surveillanceLog = new SurveillanceLog(); + $surveillanceLog->fingerprint = $log["fingerprint"]; + $surveillanceLog->userid = $log["userid"]; + $surveillanceLog->ip = $log["ip"]; + $surveillanceLog->url = $log["url"]; + $surveillanceLog->user_agent = $log["user_agent"]; + $surveillanceLog->cookies = $log["cookies"]; + $surveillanceLog->files = $log["files"]; + $surveillanceLog->session = $log["session"]; + $surveillanceLog->save(); + return $surveillanceLog; + } + } +} diff --git a/src/Implementations/SurveillanceManagerRepository.php b/src/Implementations/SurveillanceManagerRepository.php new file mode 100644 index 0000000..248e162 --- /dev/null +++ b/src/Implementations/SurveillanceManagerRepository.php @@ -0,0 +1,234 @@ +type = $type; + return $this; + } + + /** + * Set the surveillace value + * + * @param [int/string] $value + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * Get the surveillance type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Get the surveillance value + * + * @return string/int + */ + public function getValue() + { + return $this->value; + } + + /** + * Enable surveillance logging for a type and value + * + * @return SurveillanceManager + */ + public function enableSurveillance() + { + $surveillance = $this->getRecord(); + if (is_null($surveillance)) { + $surveillance = new SurveillanceManager(); + $surveillance->type = $this->getType(); + $surveillance->value = $this->getValue(); + } + $surveillance->surveillance_enabled = 1; + $surveillance->surveillance_enabled_at = Carbon::now()->toDateTimeString(); + $surveillance->save(); + return $surveillance; + } + + /** + * Disable surveillance logging for a type and value + * + * @return SurveillanceManager + */ + public function disableSurveillance() + { + $surveillance = $this->getRecord(); + if (is_null($surveillance)) { + throw new SurveillanceRecordNotFound(); + } else { + $surveillance->surveillance_enabled = 0; + $surveillance->surveillance_disabled_at = Carbon::now()->toDateTimeString(); + $surveillance->save(); + return $surveillance; + } + } + + /** + * Block access for a type and value + * + * @return SurveillanceManager + */ + public function blockAccess() + { + $surveillance = $this->getRecord(); + if (is_null($surveillance)) { + $surveillance = new SurveillanceManager(); + $surveillance->type = $this->getType(); + $surveillance->value = $this->getValue(); + } + $surveillance->access_blocked = 1; + $surveillance->access_blocked_at = Carbon::now()->toDateTimeString(); + $surveillance->save(); + return $surveillance; + } + + /** + * Unblock access for a type and value + * + * @return SurveillanceManager + */ + public function unblockAccess() + { + $surveillance = $this->getRecord(); + if (is_null($surveillance)) { + throw new SurveillanceRecordNotFound(); + } else { + $surveillance->access_blocked = 0; + $surveillance->access_unblocked_at = Carbon::now()->toDateTimeString(); + $surveillance->save(); + return $surveillance; + } + } + + /** + * Delete the surveillance manager record from database + * + * @return int + */ + public function removeRecord() + { + return SurveillanceManager::where("type", $this->getType()) + ->where("value", $this->getValue()) + ->delete(); + } + + /** + * Get a single surveillance manager record from database for a type and value + * + * @return SurveillanceManager + */ + public function getRecord() + { + return SurveillanceManager::where("type", $this->getType()) + ->where("value", $this->getValue())->first(); + } + + /** + * Get a multiple surveillance manager record from database for a type and value + * + * @return Collection + */ + public function getRecords() + { + return SurveillanceManager::where("type", $this->getType()) + ->where("value", $this->getValue())->get(); + } + + /** + * Checks if access is blocked or not + * + * @param [int] $userId + * @param [string] $ipAddress + * @param [string] $fingerprint + * @return boolean + */ + public function isAccessBlocked($userId = null, $ipAddress = null, $fingerprint = null) + { + $exists = false; + if (!is_null($userId) || !is_null($ipAddress) || !is_null($fingerprint)) { + $exists = SurveillanceManager::where(function ($query) use ($fingerprint) { + if (!is_null($fingerprint)) { + $query->where('type', 'fingerprint')->where('value', $fingerprint)->where("access_blocked", 1); + } + })->orWhere(function ($query) use ($userId) { + if (!is_null($userId)) { + $query->where('type', 'userid')->where('value', $userId)->where("access_blocked", 1); + } + })->orWhere(function ($query) use ($ipAddress) { + if (!is_null($ipAddress)) { + $query->where('type', 'ip')->where('value', $ipAddress)->where("access_blocked", 1); + } + })->exists(); + } else { + $exists = SurveillanceManager::where("type", $this->getType()) + ->where("value", $this->getValue()) + ->where("access_blocked", 1)->exists(); + } + return $exists; + } + + /** + * Checks if surveillance is enabled ot not + * + * @param [int] $userId + * @param [string] $ipAddress + * @param [string] $fingerprint + * @return boolean + */ + public function isSurveillanceEnabled($userId = null, $ipAddress = null, $fingerprint = null) + { + $exists = false; + + if (!is_null($userId) || !is_null($ipAddress) || !is_null($fingerprint)) { + $exists = SurveillanceManager::where(function ($query) use ($fingerprint) { + if (!is_null($fingerprint)) { + $query->where('type', 'fingerprint')->where('value', $fingerprint)->where("surveillance_enabled", 1); + } + })->orWhere(function ($query) use ($userId) { + if (!is_null($userId)) { + $query->where('type', 'userid')->where('value', $userId)->where("surveillance_enabled", 1); + } + })->orWhere(function ($query) use ($ipAddress) { + if (!is_null($ipAddress)) { + $query->where('type', 'ip')->where('value', $ipAddress)->where("surveillance_enabled", 1); + } + })->exists(); + } else { + $exists = SurveillanceManager::where("type", $this->getType()) + ->where("value", $this->getValue()) + ->where("surveillance_enabled", 1)->exists(); + } + return $exists; + } +} diff --git a/src/Interfaces/SurveillanceLogsInterface.php b/src/Interfaces/SurveillanceLogsInterface.php new file mode 100644 index 0000000..c9f05ed --- /dev/null +++ b/src/Interfaces/SurveillanceLogsInterface.php @@ -0,0 +1,22 @@ +mergeConfigFrom( + realpath(__DIR__ . '/../../config/surveillance.php'), + 'surveillance' + ); + //Register Facade + $this->app->bind('surveillance', function ($app) { + return new Surveillance(); + }); + + $managerRepository = config("surveillance.manager-repository"); + $this->app->bind( + 'Neelkanth\Laravel\Surveillance\Interfaces\SurveillanceManagerInterface', + $managerRepository + ); + + $logRepository = config("surveillance.log-repository"); + $this->app->bind( + 'Neelkanth\Laravel\Surveillance\Interfaces\SurveillanceLogInterface', + $logRepository + ); + } + + public function boot() + { + if ($this->app->runningInConsole()) { + $this->commands([ + "Neelkanth\Laravel\Surveillance\Console\Commands\SurveillanceEnable", + "Neelkanth\Laravel\Surveillance\Console\Commands\SurveillanceDisable", + "Neelkanth\Laravel\Surveillance\Console\Commands\SurveillanceBlock", + "Neelkanth\Laravel\Surveillance\Console\Commands\SurveillanceUnblock", + "Neelkanth\Laravel\Surveillance\Console\Commands\SurveillanceRemoveRecord" + ]); + + //php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="migrations" + $this->publishes([ + realpath(__DIR__ . '/../../database/migrations/create_surveillance_managers_table.php.stub') => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_surveillance_managers_table.php'), + realpath(__DIR__ . '/../../database/migrations/create_surveillance_logs_table.php.stub') => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_surveillance_logs_table.php') + ], 'migrations'); + + //php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="config" + $this->publishes([ + realpath(__DIR__ . '/../../config/surveillance.php') => config_path('surveillance.php'), + ], 'config'); + + //php artisan vendor:publish --provider="Neelkanth\Laravel\Surveillance\Providers\SurveillanceServiceProvider" --tag="lang" + $this->publishes([ + realpath(__DIR__ . '/../../resources/lang/en/surveillance.php') => resource_path('lang/en/surveillance.php'), + ], 'lang'); + } + + //Register Middleware + $router = $this->app->make(Router::class); + $router->aliasMiddleware('surveillance', SurveillanceMiddleware::class); + } +} diff --git a/src/Services/Surveillance.php b/src/Services/Surveillance.php new file mode 100644 index 0000000..ae98b44 --- /dev/null +++ b/src/Services/Surveillance.php @@ -0,0 +1,128 @@ +surveillanceManager(); + } + + public static function logger() + { + return (new static())->surveillanceLogger(); + } + + /** + * Set the surveillance type + * + * @param [string] $type + * @return $this + */ + public function type($type) + { + $this->type = $type; + return $this; + } + + /** + * Set the surveillance value + * + * @param [string] $value + * @return $this + */ + public function value($value) + { + $this->value = $value; + return $this; + } + + /** + * Resolve SurveillanceManagerInterface instance + * + * @return $this + */ + public function surveillanceManager() + { + $this->surveillanceManager = app()->make('Neelkanth\Laravel\Surveillance\Interfaces\SurveillanceManagerInterface'); + return $this; + } + + /** + * Resolve SurveillanceLogInterface instance + * + * @return $this + */ + public function surveillanceLogger() + { + $this->surveillanceLogger = app()->make('Neelkanth\Laravel\Surveillance\Interfaces\SurveillanceLogInterface'); + return $this; + } + + /** + * Enable surveillance logging for a type and value + * + * @return SurveillanceManager + */ + public function enableSurveillance() + { + return $this->surveillanceManager->setType($this->type)->setValue($this->value)->enableSurveillance(); + } + + /** + * Disable surveillance logging for a type and value + * + * @return SurveillanceManager + */ + public function disableSurveillance() + { + return $this->surveillanceManager->setType($this->type)->setValue($this->value)->disableSurveillance(); + } + + /** + * Block access for a type and value + * + * @return SurveillanceManager + */ + public function blockAccess() + { + return $this->surveillanceManager->setType($this->type)->setValue($this->value)->blockAccess(); + } + + /** + * Unblock access for a type and value + * + * @return SurveillanceManager + */ + public function unblockAccess() + { + return $this->surveillanceManager->setType($this->type)->setValue($this->value)->unblockAccess(); + } + + /** + * Delete the surveillance manager record from database + * + * @return int + */ + public function removeRecord() + { + return $this->surveillanceManager->setType($this->type)->setValue($this->value)->removeRecord(); + } + + /** + * Write Log + * + * @return void + */ + public function writeLog() + { + return $this->surveillanceLogger->makeLogFromRequest(request())->writeLog(); + } +} diff --git a/src/Traits/ValidateCommand.php b/src/Traits/ValidateCommand.php new file mode 100644 index 0000000..f1c6055 --- /dev/null +++ b/src/Traits/ValidateCommand.php @@ -0,0 +1,32 @@ + $args["type"]], + ['type' => [Rule::in($allowedTypes)]], + [trans("surveillance.messages.console-allowed-types", ["types" => implode(", ", $allowedTypes)])] + ); + if ($validator->fails()) { + $this->info(trans("surveillance.messages.console-validation-failed")); + foreach ($validator->errors()->all() as $error) { + $this->error($error . "\n"); + } + exit(1); + } + } +}