├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── changelog.md ├── composer.json ├── docs ├── attack.png └── playground.png ├── phpunit.xml ├── readme.md ├── src ├── Events │ └── AttackDetected.php ├── Exceptions │ └── ConfigurationOptionNotAvailable.php ├── Filters │ ├── Blacklist.php │ └── Whitelist.php ├── Firewall.php ├── Listeners │ └── NotifyAdmins.php ├── Middleware │ ├── BlockAttacks.php │ ├── FilterMiddleware.php │ ├── FirewallBlacklist.php │ ├── FirewallWhitelist.php │ └── Middleware.php ├── Notifications │ ├── Channels │ │ ├── BaseChannel.php │ │ ├── Contract.php │ │ ├── Mail.php │ │ └── Slack.php │ └── Notification.php ├── Repositories │ ├── Cache │ │ └── Cache.php │ ├── Countries.php │ ├── DataRepository.php │ ├── IpList.php │ └── Message.php ├── Support │ ├── AttackBlocker.php │ ├── IpAddress.php │ ├── Responder.php │ └── ServiceInstances.php ├── Vendor │ └── Laravel │ │ ├── Artisan │ │ ├── AddToList.php │ │ ├── Base.php │ │ ├── Blacklist.php │ │ ├── Clear.php │ │ ├── Flush.php │ │ ├── Remove.php │ │ ├── Report.php │ │ ├── UpdateGeoIp.php │ │ └── Whitelist.php │ │ ├── Facade.php │ │ ├── Models │ │ ├── Firewall.php │ │ └── User.php │ │ └── ServiceProvider.php ├── config │ ├── .gitkeep │ └── config.php └── migrations │ └── 2014_02_01_311070_create_firewall_table.php └── tests ├── ArtisanTest.php ├── AttackBlockerTest.php ├── CacheTest.php ├── FilterTest.php ├── FirewallArrayTest.php ├── FirewallDatabaseTest.php ├── FirewallTestCase.php ├── GeoIpTest.php ├── MiddlewareTest.php ├── ModelTest.php ├── ReadIpsFromFilesTest.php ├── ServiceDisabledTest.php ├── TestCase.php ├── bootstrap.php └── files └── iplist.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | tests/geoipdb/* 6 | reports/* 7 | coverage/* 8 | tests/databases/* 9 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: 7.2 5 | tests: 6 | override: 7 | - 8 | command: 'vendor/bin/phpunit --coverage-clover=/tmp/coverage-clover.xml' 9 | coverage: 10 | file: '/tmp/coverage-clover.xml' 11 | format: 'clover' 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: vendor/bin/phpunit 2 | 3 | language: php 4 | 5 | php: 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | - 7.4 10 | - 8.0 11 | - 8.1 12 | # - nightly 13 | 14 | sudo: false 15 | 16 | cache: 17 | directories: 18 | - $HOME/.composer/cache 19 | 20 | before_script: 21 | - curl -s http://getcomposer.org/installer | php 22 | - php composer.phar update --dev 23 | - travis_retry composer self-update 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Antonio Carlos Ribeiro 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [2.3.0] - 2019-09-11 5 | ### Added 6 | - Laravel 6.0 support 7 | - Support to identify IP behind Cloudflare 8 | - Artisan command firewall:cache:clear 9 | - Ability to set the log stack. New key in config: `'log_stack' => ['single']` 10 | 11 | ### Fixed 12 | - Response in notification-only mode 13 | - Migrations being execute when not in database mode 14 | - Exception when trying to remove from database an ip inside a country 15 | 16 | ## [2.2.1] - 2018-07-31 17 | ### Fixed 18 | - Fix whitelisted IP being blocked by AttackBlocker 19 | 20 | ## [2.0.2] - 2017-08-26 21 | ### Fixed 22 | - Minor fixes 23 | 24 | ## [2.0.1] - 2017-08-21 25 | ### Fixed 26 | - Minor fixes 27 | 28 | ## [2.0.0] - 2017-08-21 29 | ### Added 30 | - Attack blocker 31 | 32 | ## [1.1.0] - 2017-08-20 33 | ### Added 34 | - GeoIp2 database file updater artisan command 35 | 36 | ## [1.0.9] - 2017-08-13 37 | ### Added 38 | - Laravel 5.5 autodiscovery 39 | - Snippets to readme 40 | ### Changed 41 | - Laravel 5.* install docs 42 | ### Fix 43 | - Readme things 44 | - Markdown headings 45 | 46 | ## [1.0.8] - 2017-01-31 47 | ### Updated 48 | - To Laravel 5.4 49 | 50 | ## [1.0.7] - 2016-12-25 51 | ### Fixed 52 | - Lexical problem 53 | 54 | ## [1.0.6] - 2016-12-24 55 | ### Updated 56 | - Support package 57 | 58 | ## [1.0.5] - 2016-12-24 59 | ### Fixed 60 | - Output on firewall:clear command 61 | - Whitelisting problem 62 | 63 | ## [1.0.4] - 2016-12-24 64 | ### Added 65 | - Ability to black/whitelist IPs for the session 66 | 67 | ## [1.0.3] - 2016-05-14 68 | ### Fixed 69 | - Remove forgotten brace 70 | 71 | ## [1.0.2] - 2016-05-13 72 | ### Fixed 73 | - Bug on PHP 7.0.2 74 | - Deprecated Table helper (Symfony) 75 | 76 | ## [1.0.1] - 2016-04-18 77 | ### Changed 78 | - Upgrade support package 79 | ### Fixed 80 | - Middleware problem 81 | 82 | ## [1.0.0] - 2015-12-24 83 | ### Added 84 | - Move to Middleware (BC) 85 | 86 | ## [1.0.0] - 2015-12-24 87 | ### Added 88 | - Move to Middleware (BC) 89 | 90 | ## [0.5.4] - 2015-12-01 91 | ### Fixed 92 | - Config publishing docs 93 | - Minor fixes 94 | 95 | ## [0.5.3] - 2015-03 96 | ### Added 97 | - Add support for GeoIP 98 | 99 | ## [0.5.2] - 2015-03 100 | ### Fixed 101 | - Fix incorrectly using array as object 102 | 103 | ## [0.5.1] - 2014-02 104 | ### Fixed 105 | - Minor fixes and improvements 106 | 107 | ## [0.5.0] - 2014-02-18 108 | ### Added 109 | - Laravel 5 compatibility. 110 | - Allow black and white lists to be stored in arrays, files or file of files, instead of a database table. 111 | - Allow redirect_non_whitelisted_to to be a named route or url (to). 112 | - Option to disable database lookup, disabled by default. 113 | - Change log, according to http://keepachangelog.com/. 114 | - Add cache for the list of IP addresses. 115 | 116 | ### Changed 117 | - Removed unnecessary configuration options. 118 | - Range search now defaults to enabled. 119 | 120 | ## [0.2.0] - 2014-07-08 121 | ### Fixed 122 | - Migrator fixed 123 | - Rename migrator 124 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pragmarx/firewall", 3 | "description": "A Laravel IP whitelisting and blacklisting", 4 | "keywords": [ 5 | "firewall", 6 | "whitelist", 7 | "blacklist", 8 | "laravel" 9 | ], 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Antonio Carlos Ribeiro", 14 | "email": "acr@antoniocarlosribeiro.com", 15 | "role": "Creator" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.6", 20 | "illuminate/support": ">=5.3", 21 | "pragmarx/support": ">=0.8.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "~7|~8|~9", 25 | "orchestra/testbench": "3.8.*|4.*|5.*|6.*|7.*|8.*", 26 | "geoip2/geoip2": "~2.0" 27 | }, 28 | "suggest": { 29 | "geoip/geoip": "~1.14", 30 | "geoip2/geoip2": "~2.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "PragmaRX\\Firewall\\": "src/", 35 | "PragmaRX\\Firewall\\Tests\\": "tests/" 36 | } 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "0.5.x-dev" 41 | }, 42 | "laravel": { 43 | "providers": [ 44 | "PragmaRX\\Firewall\\Vendor\\Laravel\\ServiceProvider" 45 | ], 46 | "aliases": { 47 | "Firewall": "PragmaRX\\Firewall\\Vendor\\Laravel\\Facade" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/firewall/6b3575a7b5977e223620d8281423e5175c875ea7/docs/attack.png -------------------------------------------------------------------------------- /docs/playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/firewall/6b3575a7b5977e223620d8281423e5175c875ea7/docs/playground.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Firewall 2.2 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/pragmarx/firewall.svg?style=flat-square)](https://packagist.org/packages/pragmarx/firewall) [![License](https://img.shields.io/badge/license-BSD_3_Clause-brightgreen.svg?style=flat-square)](LICENSE) [![Downloads](https://img.shields.io/packagist/dt/pragmarx/firewall.svg?style=flat-square)](https://packagist.org/packages/pragmarx/firewall) [![Code Quality](https://img.shields.io/scrutinizer/g/antonioribeiro/firewall.svg?style=flat-square)](https://scrutinizer-ci.com/g/antonioribeiro/firewall/?branch=master) [![Build](https://img.shields.io/scrutinizer/build/g/antonioribeiro/firewall.svg?style=flat-square)](https://scrutinizer-ci.com/g/antonioribeiro/firewall/?branch=master) [![Coverage](https://img.shields.io/scrutinizer/coverage/g/antonioribeiro/firewall.svg?style=flat-square)](https://scrutinizer-ci.com/g/antonioribeiro/firewall/?branch=master) [![StyleCI](https://styleci.io/repos/15290899/shield)](https://styleci.io/repos/15290899) 4 | 5 | ## Purpose 6 | 7 | This a "soft-firewall" package. Its purpose is to help people prevent unauthorized access to routes **by IP address**. It is able to keep track of IPs, countries and hosts (dynamic ip), and redirect non-authorized users to, for instance, a "Coming Soon" page, while letting whitelisted IPs to have access to the entire site. It is now also able to detect and block attacks (too many requests) from single IPs or whole countries. 8 | 9 | This package can prevent some headaches and help you block some access to your apps, but cannot replace firewalls and appliances, for attacks at the network level, you'll still need a real firewall. 10 | 11 | ## Features 12 | 13 | * Control access to routes and groups via black and white lists. 14 | * Detect and block attacks to your application, from IP addresses or countries. 15 | * Send Slack notifications in attack events. 16 | * Allow whitelisted to access the whole site and send everyone else to a "coming soon page". 17 | * Redirect blacklisted users to some other page. 18 | * Use database or arrays to store IP lists. 19 | * Whitelist your development machine using a dynamic DNS host name. 20 | * Done using middleware, so you can protect/unprotect groups of routes. 21 | * All features are available for hosts, IP addresses, ranges of IP addresses and whole countries. 22 | * Super fast, less than 10ms increase in each request. 23 | * Highly configurable. 24 | 25 | ## Concepts 26 | 27 | ### Blacklist 28 | 29 | All IP addresses in those lists will no be able to access routes filtered by the blacklist filter. 30 | 31 | ### Whitelist 32 | 33 | Those IP addresses, ranges or countries can 34 | 35 | - Access blacklisted routes even if they are in a range of blacklisted IP addresses. 36 | - Access 'allow whitelisted' filtered routes. 37 | - If a route is filtered by the 'allow whitelisted' filter and the IP is not whitelisted, the request will be redirected to an alternative url or route name. 38 | 39 | ## Attack Detection 40 | 41 | ![attack](docs/attack.png) 42 | 43 | Firewall is able to detect simple attacks to your page, by counting requests from the same IP or country. Just enable it on your `config/firewall.php` and, to receive notifications, configure the Slack service in `config/services.php`: 44 | 45 | ```php 46 | 'slack' => [ 47 | 'webhook_url' => env('SLACK_WEBHOOK_URL'), 48 | ], 49 | ``` 50 | 51 | and add the route notification method to your user model: 52 | 53 | ```php 54 | /** 55 | * Route notifications for the Slack channel. 56 | * 57 | * @return string 58 | */ 59 | public function routeNotificationForSlack() 60 | { 61 | return config('services.slack.webhook_url'); 62 | } 63 | ``` 64 | 65 | ## IPs lists 66 | 67 | IPs (white and black) lists can be stored in array, files and database. Initially database access to lists is disabled, so, to test your Firewall configuration you can publish the config file and edit the `blacklist` or `whitelist` arrays: 68 | 69 | ```php 70 | 'blacklist' => array( 71 | '127.0.0.1', 72 | '192.168.17.0/24' 73 | '127.0.0.1/255.255.255.255' 74 | '10.0.0.1-10.0.0.255' 75 | '172.17.*.*' 76 | 'country:br' 77 | '/usr/bin/firewall/blacklisted.txt', 78 | ), 79 | ``` 80 | 81 | The file (for instance `/usr/bin/firewall/blacklisted.txt`) must contain one IP, range or file name per line, and, yes, it will search for files recursively, so you can have a file of files if you need: 82 | 83 | ``` 84 | 127.0.0.2 85 | 10.0.0.0-10.0.0.100 86 | /tmp/blacklist.txt 87 | ``` 88 | 89 | ## Redirecting non-whitelisted IP addresses 90 | 91 | Non-whitelisted IP addresses can be blocked or redirected. To configure redirection you'll have to publish the `config.php` file and configure: 92 | 93 | ```php 94 | 'redirect_non_whitelisted_to' => 'coming/soon', 95 | ``` 96 | 97 | ## Artisan Commands 98 | 99 | You have access to the following commands: 100 | 101 | #### Global 102 | 103 | ``` 104 | firewall:cache:clear Clear the firewall cache. 105 | firewall:list List all IP address, white and blacklisted. 106 | firewall:updategeoip Update the GeoIP database. 107 | ``` 108 | 109 | #### When database is enabled 110 | 111 | ``` 112 | firewall:blacklist Add an IP address to blacklist. 113 | firewall:clear Remove all ip addresses from white and black lists. 114 | firewall:remove Remove an IP address from white or black list. 115 | firewall:whitelist Add an IP address to whitelist. 116 | ``` 117 | 118 | Those are results from `firewall:list`: 119 | 120 | ``` 121 | +--------------+-----------+-----------+ 122 | | IP Address | Whitelist | Blacklist | 123 | +--------------+-----------+-----------+ 124 | | 10.17.12.7 | | X | 125 | | 10.17.12.100 | X | | 126 | | 10.17.12.101 | X | | 127 | | 10.17.12.102 | X | | 128 | | 10.17.12.200 | | X | 129 | +--------------+-----------+-----------+ 130 | ``` 131 | 132 | ``` 133 | +-----------------------+-----------+-----------+ 134 | | IP Address | Whitelist | Blacklist | 135 | +-----------------------+-----------+-----------+ 136 | | 172.0.0.0-172.0.0.255 | | X | 137 | | country:br | | X | 138 | | host:mypc.myname.com | X | | 139 | +-----------------------+-----------+-----------+ 140 | ``` 141 | 142 | ## Facade 143 | 144 | You can also use the `Firewall Facade` to manage the lists: 145 | 146 | ```php 147 | $whitelisted = Firewall::isWhitelisted('10.17.12.1'); 148 | $blacklisted = Firewall::isBlacklisted('10.0.0.3'); 149 | 150 | Firewall::whitelist('192.168.1.1'); 151 | Firewall::blacklist('10.17.12.1', true); /// true = force in case IP is whitelisted 152 | Firewall::blacklist('127.0.0.0-127.0.0.255'); 153 | Firewall::blacklist('200.212.331.0/28'); 154 | Firewall::blacklist('country:br'); 155 | 156 | if (Firewall::whichList($ip) !== false) // returns false, 'whitelist' or 'blacklist' 157 | { 158 | Firewall::remove($ip); 159 | } 160 | ``` 161 | 162 | Return a blocking access response: 163 | 164 | ```php 165 | return Firewall::blockAccess(); 166 | ``` 167 | 168 | Suspicious events will be (if you wish) logged, so `tail` it: 169 | 170 | ``` 171 | php artisan tail 172 | ``` 173 | 174 | ## Blocking Whole Countries 175 | 176 | You can block a country by, instead of an ip address, pass `country:<2-letter ISO code>`. So, to block all Brazil's IP addresses, you do: 177 | 178 | ``` 179 | php artisan firewall:blacklist country:br 180 | ``` 181 | 182 | You will have to add this requirement to your `composer.json` file: 183 | 184 | ``` 185 | "geoip/geoip": "~1.14" 186 | ``` 187 | 188 | or 189 | 190 | ``` 191 | "geoip2/geoip2": "~2.0" 192 | ``` 193 | 194 | You need to enable country search on your firewall.php config file: 195 | 196 | ```php 197 | 'enable_country_search' => true, 198 | ``` 199 | 200 | And you can schedule this command to update your cities GeoIp database regularly: 201 | 202 | ``` 203 | php artisan firewall:updategeoip 204 | ``` 205 | 206 | You can find those codes here: [isocodes](http://www.spoonfork.org/isocodes.html) 207 | 208 | ## Session Blocking 209 | 210 | You can block users from accessing some pages only for the current session, by using those methods: 211 | 212 | ```php 213 | Firewall::whitelistOnSession($ip); 214 | Firewall::blacklistOnSession($ip); 215 | Firewall::removeFromSession($ip); 216 | ``` 217 | 218 | ## Playground & Bootstrap App 219 | 220 | Click [here](http://pragmarx.com/firewall) to see it working and in case you need a help figuring out things, try [this repository](https://github.com/antonioribeiro/pragmarx.com). 221 | 222 | ![playground](docs/playground.png) 223 | 224 | ## Installation 225 | 226 | ### Compatible with 227 | 228 | - Laravel 4+ (version 1.*) 229 | - Laravel 5.0, 5.1, 5.2 and 5.3 (version 1.*) 230 | - Laravel 5.4, 5.5, 5.6 and 5.7 (version 2.*) 231 | 232 | ### Installing 233 | 234 | Require the Firewall package using [Composer](https://getcomposer.org/doc/01-basic-usage.md): 235 | 236 | ``` 237 | composer require pragmarx/firewall 238 | ``` 239 | 240 | * Laravel 5.5 and up 241 | 242 | You don't have to do anything else, this package uses Package Auto-Discovery's feature, and should be available as soon as you install it via Composer. 243 | 244 | * Laravel 5.4 and below 245 | 246 | Add the Service Provider and the Facade to your app/config/app.php: 247 | 248 | ```php 249 | PragmaRX\Firewall\Vendor\Laravel\ServiceProvider::class, 250 | ``` 251 | 252 | ```php 253 | 'Firewall' => PragmaRX\Firewall\Vendor\Laravel\Facade::class, 254 | ``` 255 | 256 | Add middlewares to your app/Http/Kernel.php 257 | 258 | ```php 259 | protected $routeMiddleware = [ 260 | ... 261 | 'fw-only-whitelisted' => \PragmaRX\Firewall\Middleware\FirewallWhitelist::class, 262 | 'fw-block-blacklisted' => \PragmaRX\Firewall\Middleware\FirewallBlacklist::class, 263 | 'fw-block-attacks' => \PragmaRX\Firewall\Middleware\BlockAttacks::class, 264 | ]; 265 | ``` 266 | 267 | or 268 | 269 | ```php 270 | protected $middlewareGroups = [ 271 | 'web' => [ 272 | ... 273 | ], 274 | 275 | 'api' => [ 276 | ... 277 | ], 278 | 279 | 'firewall' => [ 280 | \PragmaRX\Firewall\Middleware\FirewallBlacklist::class, 281 | \PragmaRX\Firewall\Middleware\BlockAttacks::class, 282 | ], 283 | ]; 284 | ``` 285 | 286 | Then you can use them in your routes: 287 | 288 | ```php 289 | Route::group(['middleware' => 'fw-block-blacklisted'], function () 290 | { 291 | Route::get('/', 'HomeController@index'); 292 | }); 293 | ``` 294 | 295 | Or you could use both. In the following example the allow group will give free access to the 'coming soon' page and block or just redirect non-whitelisted IP addresses to another, while still blocking access to the blacklisted ones. 296 | 297 | ```php 298 | Route::group(['middleware' => 'fw-block-blacklisted'], function () 299 | { 300 | Route::get('coming/soon', function() 301 | { 302 | return "We are about to launch, please come back in a few days."; 303 | }); 304 | 305 | Route::group(['middleware' => 'fw-only-whitelisted'], function () 306 | { 307 | Route::get('/', 'HomeController@index'); 308 | }); 309 | }); 310 | ``` 311 | 312 | **Note:** You can add other middleware you have already created to the new groups by simply 313 | adding it to the `fw-allow-wl` or `fw-block-bl` middleware group. 314 | 315 | Migrate your database 316 | 317 | ``` 318 | php artisan migrate 319 | ``` 320 | 321 | **Warning:** If you already have a Firewall package installed and migrated, you need to update your migration name, in the `migrations` table, to `2014_02_01_311070_create_firewall_table`, otherwise the migrate command will fail tell you the table already exists. 322 | 323 | To publish the configuration file you'll have to: 324 | 325 | **Laravel 4** 326 | 327 | ``` 328 | php artisan config:publish pragmarx/firewall 329 | ``` 330 | 331 | **Laravel 5** 332 | 333 | ``` 334 | php artisan vendor:publish --provider="PragmaRX\Firewall\Vendor\Laravel\ServiceProvider" 335 | ``` 336 | 337 | ## TODO 338 | 339 | - Tests, tests, tests. 340 | 341 | ## Author 342 | 343 | [Antonio Carlos Ribeiro](http://twitter.com/iantonioribeiro) 344 | 345 | ## License 346 | 347 | Firewall is licensed under the BSD 3-Clause License - see the `LICENSE` file for details 348 | 349 | ## Contributing 350 | 351 | Pull requests and issues are more than welcome. 352 | -------------------------------------------------------------------------------- /src/Events/AttackDetected.php: -------------------------------------------------------------------------------- 1 | record = $record; 25 | 26 | $this->channel = $channel; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigurationOptionNotAvailable.php: -------------------------------------------------------------------------------- 1 | make('firewall'); 15 | 16 | if ($firewall->isBlacklisted($ipAddress = $firewall->getIp())) { 17 | $firewall->log('[blocked] IP blacklisted: '.$ipAddress); 18 | 19 | return $firewall->blockAccess(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Filters/Whitelist.php: -------------------------------------------------------------------------------- 1 | make('firewall'); 15 | 16 | if (!$firewall->isWhitelisted()) { 17 | $response = (new Responder())->respond( 18 | $this->config()->get('responses.whitelist') 19 | ); 20 | 21 | if (!is_null($this->config()->get('responses.whitelist.redirect_to'))) { 22 | $action = 'redirected'; 23 | } else { 24 | $action = 'blocked'; 25 | } 26 | 27 | $message = sprintf('[%s] IP not whitelisted: %s', $action, $firewall->getIp()); 28 | 29 | $firewall->log($message); 30 | 31 | return $response; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Firewall.php: -------------------------------------------------------------------------------- 1 | config = $config; 71 | 72 | $this->dataRepository = $dataRepository; 73 | 74 | $this->request = $request; 75 | 76 | $this->attackBlocker = $attackBlocker; 77 | 78 | $this->messageRepository = $messageRepository; 79 | 80 | $this->setIp(null); 81 | } 82 | 83 | /** 84 | * Get all IP addresses. 85 | * 86 | * @return \Illuminate\Support\Collection 87 | */ 88 | public function all() 89 | { 90 | return $this->dataRepository->all(); 91 | } 92 | 93 | /** 94 | * Get all IP addresses by country. 95 | * 96 | * @param $country 97 | * 98 | * @return \Illuminate\Support\Collection 99 | */ 100 | public function allByCountry($country) 101 | { 102 | return $this->dataRepository->allByCountry($country); 103 | } 104 | 105 | /** 106 | * Blacklist an IP address. 107 | * 108 | * @param $ip 109 | * @param bool $force 110 | * 111 | * @return bool 112 | */ 113 | public function blacklist($ip, $force = false) 114 | { 115 | return $this->dataRepository->addToList(false, $ip, $force); 116 | } 117 | 118 | /** 119 | * Create a blocked access response. 120 | * 121 | * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null 122 | */ 123 | public function blockAccess() 124 | { 125 | return (new Responder())->respond( 126 | $this->config->get('responses.blacklist') 127 | ); 128 | } 129 | 130 | /** 131 | * Clear firewall table. 132 | * 133 | * @return mixed 134 | */ 135 | public function clear() 136 | { 137 | return $this->dataRepository->clear(); 138 | } 139 | 140 | /** 141 | * Find an IP address. 142 | * 143 | * @param string $ip 144 | * 145 | * @return mixed 146 | */ 147 | public function find($ip) 148 | { 149 | return $this->dataRepository->find($ip); 150 | } 151 | 152 | /** 153 | * Get the IP address. 154 | * 155 | * @param null $ip 156 | * 157 | * @return null|string 158 | */ 159 | public function getIp($ip = null) 160 | { 161 | return $ip ?: $this->ip; 162 | } 163 | 164 | /** 165 | * Get the messages. 166 | * 167 | * @return \Illuminate\Support\Collection 168 | */ 169 | public function getMessages() 170 | { 171 | return $this->messageRepository->getMessages(); 172 | } 173 | 174 | /** 175 | * Check if IP address is valid. 176 | * 177 | * @param $ip 178 | * 179 | * @return bool 180 | */ 181 | public function ipIsValid($ip) 182 | { 183 | return $this->dataRepository->ipIsValid($ip); 184 | } 185 | 186 | /** 187 | * Check if IP is blacklisted. 188 | * 189 | * @param null|string $ip 190 | * 191 | * @return bool 192 | */ 193 | public function isBlacklisted($ip = null) 194 | { 195 | $list = $this->whichList($ip); 196 | 197 | return !($list == 'whitelist') && 198 | $list == 'blacklist'; 199 | } 200 | 201 | /** 202 | * Check if IP address is whitelisted. 203 | * 204 | * @param null|string $ip 205 | * 206 | * @return bool 207 | */ 208 | public function isWhitelisted($ip = null) 209 | { 210 | return $this->whichList($ip) == 'whitelist'; 211 | } 212 | 213 | /** 214 | * Register messages in log. 215 | * 216 | * @param $message 217 | * 218 | * @return void 219 | */ 220 | public function log($message) 221 | { 222 | if ($this->config->get('enable_log')) { 223 | $this->getLogger()->info("FIREWALL: $message"); 224 | } 225 | } 226 | 227 | public function getLogger() 228 | { 229 | if ($stack = $this->config->get('log_stack')) { 230 | return app()->log->stack($stack); 231 | } 232 | 233 | return app()->log; 234 | } 235 | 236 | /** 237 | * Remove IP from all lists. 238 | * 239 | * @param $ip 240 | * 241 | * @return bool 242 | */ 243 | public function remove($ip) 244 | { 245 | return $this->dataRepository->remove($ip); 246 | } 247 | 248 | /** 249 | * Get the list of all IP addresses stored. 250 | * 251 | * @return mixed 252 | */ 253 | public function report() 254 | { 255 | return $this->dataRepository->all(); 256 | } 257 | 258 | /** 259 | * Set the current IP address. 260 | * 261 | * @param $ip 262 | */ 263 | public function setIp($ip) 264 | { 265 | if ($ip) { 266 | $this->ip = $ip; 267 | } elseif (!$this->ip) { 268 | if ($ip = $this->request->server('HTTP_CF_CONNECTING_IP')) { 269 | $this->ip = $ip; 270 | } elseif ($ip = $this->request->server->get('HTTP_X_FORWARDED_FOR')) { 271 | $this->ip = $ip; 272 | } elseif ($ip = $this->request->getClientIp()) { 273 | $this->ip = $ip; 274 | } 275 | } 276 | } 277 | 278 | /** 279 | * Check if a string is a valid country info. 280 | * 281 | * @param $country 282 | * 283 | * @return bool 284 | */ 285 | public function validCountry($country) 286 | { 287 | return $this->dataRepository->validCountry($country); 288 | } 289 | 290 | /** 291 | * Tell in which list (black/white) an IP address is. 292 | * 293 | * @param $ip 294 | * 295 | * @return bool|string 296 | */ 297 | public function whichList($ip) 298 | { 299 | return $this->dataRepository->whichList($this->getIp($ip)); 300 | } 301 | 302 | /** 303 | * Whitelist an IP address. 304 | * 305 | * @param $ip 306 | * @param bool $force 307 | * 308 | * @return bool 309 | */ 310 | public function whitelist($ip, $force = false) 311 | { 312 | return $this->dataRepository->addToList(true, $ip, $force); 313 | } 314 | 315 | /** 316 | * Update the GeoIp2 database. 317 | * 318 | * @return bool 319 | */ 320 | public function updateGeoIp() 321 | { 322 | $updater = new GeoIpUpdater(); 323 | 324 | $success = $updater->updateGeoIpFiles($this->config->get('geoip_database_path')); 325 | 326 | $this->messageRepository->addMessage($updater->getMessages()); 327 | 328 | return $success; 329 | } 330 | 331 | /** 332 | * Check if the application is receiving some sort of attack. 333 | * 334 | * @param null $ipAddress 335 | * 336 | * @return bool 337 | */ 338 | public function isBeingAttacked($ipAddress = null) 339 | { 340 | return $this->attackBlocker->isBeingAttacked($this->getIp($ipAddress)); 341 | } 342 | 343 | /** 344 | * Get a response to the attack. 345 | * 346 | * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null 347 | */ 348 | public function responseToAttack() 349 | { 350 | return $this->attackBlocker->responseToAttack(); 351 | } 352 | 353 | /** 354 | * Get country code from an IP address. 355 | * 356 | * @param $ip 357 | * 358 | * @return bool|string 359 | */ 360 | public function getCountryFromIp($ip) 361 | { 362 | return $this->dataRepository->getCountryFromIp($ip); 363 | } 364 | 365 | /** 366 | * Make a country info from a string. 367 | * 368 | * @param $country 369 | * 370 | * @return bool|string 371 | */ 372 | public function makeCountryFromString($country) 373 | { 374 | return $this->dataRepository->makeCountryFromString($country); 375 | } 376 | 377 | /** 378 | * Get the GeoIP instance. 379 | * 380 | * @return object 381 | */ 382 | public function getGeoIp() 383 | { 384 | return $this->dataRepository->getGeoIp(); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/Listeners/NotifyAdmins.php: -------------------------------------------------------------------------------- 1 | map(function ($item) { 20 | if (class_exists($class = config('firewall.notifications.users.model'))) { 21 | $model = app($class); 22 | 23 | $model->email = $item; 24 | 25 | return $model; 26 | } 27 | })->filter(); 28 | } 29 | 30 | /** 31 | * Handle the event. 32 | * 33 | * @param AttackDetected $event 34 | * 35 | * @return void 36 | */ 37 | public function handle(AttackDetected $event) 38 | { 39 | try { 40 | IlluminateNotification::send( 41 | $this->getNotifiableUsers(), 42 | new Notification($event->record, $event->channel) 43 | ); 44 | } catch (Exception $exception) { 45 | info((string) $exception); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Middleware/BlockAttacks.php: -------------------------------------------------------------------------------- 1 | enabled() && app('firewall')->isBeingAttacked()) { 20 | return app('firewall')->responseToAttack() ?: $next($request); 21 | } 22 | 23 | return $next($request); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Middleware/FilterMiddleware.php: -------------------------------------------------------------------------------- 1 | enabled()) { 25 | $filterResponse = $this->filter(); 26 | 27 | if ($filterResponse != null) { 28 | return $filterResponse; 29 | } 30 | } 31 | 32 | return $next($request); 33 | } 34 | 35 | /** 36 | * Filter. 37 | * 38 | * @return mixed 39 | */ 40 | abstract public function filter(); 41 | } 42 | -------------------------------------------------------------------------------- /src/Middleware/FirewallBlacklist.php: -------------------------------------------------------------------------------- 1 | blacklist = $blacklist; 14 | } 15 | 16 | /** 17 | * Filter Request. 18 | * 19 | * @return mixed 20 | */ 21 | public function filter() 22 | { 23 | return $this->blacklist->filter(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Middleware/FirewallWhitelist.php: -------------------------------------------------------------------------------- 1 | whitelist = $whitelist; 14 | } 15 | 16 | /** 17 | * Filter Request. 18 | * 19 | * @return mixed 20 | */ 21 | public function filter() 22 | { 23 | return $this->whitelist->filter(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Middleware/Middleware.php: -------------------------------------------------------------------------------- 1 | getActionMessage(), $domain, $this->makeMessage($item)); 24 | } 25 | 26 | /** 27 | * @param $item 28 | * 29 | * @return mixed 30 | */ 31 | protected function makeMessage($item) 32 | { 33 | $ip = "{$item['ipAddress']} - {$item['host']}"; 34 | 35 | if ($item['type'] == 'ip') { 36 | return "$ip"; 37 | } 38 | 39 | return "{$item['country_code']}-{$item['country_name']} ({$ip})"; 40 | } 41 | 42 | /** 43 | * Make a geolocation model for the item. 44 | * 45 | * @param $item 46 | * 47 | * @return array 48 | */ 49 | public function makeGeolocation($item) 50 | { 51 | return collect([ 52 | config('firewall.notifications.message.geolocation.field_latitude') => $item['geoIp']['latitude'], 53 | config('firewall.notifications.message.geolocation.field_longitude') => $item['geoIp']['longitude'], 54 | config('firewall.notifications.message.geolocation.field_country_code') => $item['geoIp']['country_code'], 55 | config('firewall.notifications.message.geolocation.field_country_name') => $item['geoIp']['country_name'], 56 | config('firewall.notifications.message.geolocation.field_city') => $item['geoIp']['city'], 57 | ])->filter()->toArray(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Notifications/Channels/Contract.php: -------------------------------------------------------------------------------- 1 | from( 22 | config('firewall.notifications.from.address'), 23 | config('firewall.notifications.from.name').' '. 24 | config('firewall.notifications.from.icon_emoji') 25 | ) 26 | ->subject(config('firewall.notifications.message.request_count.title')) 27 | ->line(sprintf( 28 | config('firewall.notifications.message.request_count.message'), 29 | $item['requestCount'], 30 | $item['firstRequestAt']->diffInSeconds(Carbon::now()), 31 | (string) $item['firstRequestAt'] 32 | )) 33 | ->line(config('firewall.notifications.message.uri.title').': '.$item['server']['REQUEST_URI']) 34 | ->line(config('firewall.notifications.message.user_agent.title').': '.$item['userAgent']) 35 | ->line(config('firewall.notifications.message.blacklisted.title').': '.$item['isBlacklisted'] ? 'YES' : 'NO'); 36 | 37 | $geo = $this->makeGeolocation($item); 38 | 39 | if ($item['geoIp']) { 40 | $message->line(config('firewall.notifications.message.geolocation.title')." - Latitude : {$geo['Latitude']}"); 41 | $message->line(config('firewall.notifications.message.geolocation.title')." - Longitude : {$geo['Longitude']}"); 42 | $message->line(config('firewall.notifications.message.geolocation.title')." - Country code : {$geo['Country code']}"); 43 | $message->line(config('firewall.notifications.message.geolocation.title')." - Country name : {$geo['Country name']}"); 44 | } 45 | 46 | return $message; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Notifications/Channels/Slack.php: -------------------------------------------------------------------------------- 1 | error() 22 | ->from( 23 | config('firewall.notifications.from.name'), 24 | config('firewall.notifications.from.icon_emoji') 25 | ) 26 | ->content($this->getMessage($item)) 27 | ->attachment(function ($attachment) use ($item) { 28 | $attachment->title(config('firewall.notifications.message.request_count.title')) 29 | ->content( 30 | sprintf( 31 | config('firewall.notifications.message.request_count.message'), 32 | $item['requestCount'], 33 | $item['firstRequestAt']->diffInSeconds(Carbon::now()), 34 | (string) $item['firstRequestAt'] 35 | ) 36 | ); 37 | }) 38 | ->attachment(function ($attachment) use ($item) { 39 | $attachment->title($title = config('firewall.notifications.message.uri.title')) 40 | ->content($item['server']['REQUEST_URI']); 41 | }) 42 | ->attachment(function ($attachment) use ($item) { 43 | $attachment->title(config('firewall.notifications.message.user_agent.title')) 44 | ->content($item['userAgent']); 45 | }) 46 | ->attachment(function ($attachment) use ($item) { 47 | $attachment->title(config('firewall.notifications.message.blacklisted.title')) 48 | ->content($item['isBlacklisted'] ? 'YES' : 'NO'); 49 | }); 50 | 51 | if ($item['geoIp']) { 52 | $message->attachment(function ($attachment) use ($item) { 53 | $attachment->title(config('firewall.notifications.message.geolocation.title')) 54 | ->fields($this->makeGeolocation($item)); 55 | }); 56 | } 57 | 58 | return $message; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Notifications/Notification.php: -------------------------------------------------------------------------------- 1 | item = $item; 30 | 31 | $this->channel = $channel; 32 | } 33 | 34 | /** 35 | * @param $name 36 | * 37 | * @return \Illuminate\Foundation\Application|mixed 38 | */ 39 | private function getSenderInstance($name) 40 | { 41 | $name = substr($name, 2); 42 | 43 | return app(config('firewall.notifications.channels.'.strtolower($name).'.sender')); 44 | } 45 | 46 | /** 47 | * Get the notification's delivery channels. 48 | * 49 | * @return array 50 | */ 51 | public function via() 52 | { 53 | return [$this->channel]; 54 | } 55 | 56 | /** 57 | * @param $name 58 | * @param $parameters 59 | * 60 | * @return mixed 61 | */ 62 | public function __call($name, $parameters) 63 | { 64 | $parameters[] = $this->item; 65 | 66 | return call_user_func_array( 67 | [ 68 | $this->getSenderInstance($name), 69 | 'send', 70 | ], 71 | $parameters 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Repositories/Cache/Cache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 19 | } 20 | 21 | /** 22 | * Cache remember. 23 | * 24 | * @param $model 25 | * 26 | * @return void 27 | */ 28 | public function remember($model) 29 | { 30 | if (($timeout = $this->expireTime()) > 0) { 31 | $this->put($model->ip_address, $model, $timeout); 32 | } 33 | } 34 | 35 | /** 36 | * Make a cache key. 37 | * 38 | * @param $key 39 | * 40 | * @return string 41 | */ 42 | public function key($key) 43 | { 44 | return sha1(static::CACHE_BASE_NAME."ip_address.$key"); 45 | } 46 | 47 | /** 48 | * Flush cache. 49 | */ 50 | public function flush() 51 | { 52 | $this->cache->flush(); 53 | } 54 | 55 | /** 56 | * Check if cache has key. 57 | * 58 | * @param $key 59 | * 60 | * @return bool 61 | */ 62 | public function has($key) 63 | { 64 | if ($this->enabled()) { 65 | return $this->cache->has($this->key($key)); 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /** 72 | * Get a value from the cache. 73 | * 74 | * @param $key 75 | * @param null $default 76 | * 77 | * @return mixed|null 78 | */ 79 | public function get($key, $default = null) 80 | { 81 | if ($this->enabled()) { 82 | return $this->cache->get($this->key($key), $default); 83 | } 84 | } 85 | 86 | /** 87 | * Remove an ip address from cache. 88 | * 89 | * @param $key 90 | * 91 | * @return void 92 | */ 93 | public function forget($key) 94 | { 95 | if ($this->enabled()) { 96 | $this->cache->forget($this->key($key)); 97 | } 98 | } 99 | 100 | /** 101 | * Store an item in the cache for a given number of minutes. 102 | * 103 | * @param string $key 104 | * @param mixed $value 105 | * @param int|null|bool $minutes 106 | * 107 | * @return void 108 | */ 109 | public function put($key, $value, $minutes = null) 110 | { 111 | if ($timeout = $this->enabled()) { 112 | $this->cache->put($this->key($key), $value, $minutes ?: $timeout); 113 | } 114 | } 115 | 116 | /** 117 | * Get cache expire time. 118 | * 119 | * @return int|bool 120 | */ 121 | public function expireTime() 122 | { 123 | return $this->config()->get('cache_expire_time'); 124 | } 125 | 126 | /** 127 | * Get enabled state. 128 | * 129 | * @return int|bool 130 | */ 131 | public function enabled() 132 | { 133 | return $this->config()->get('cache_expire_time') !== false && 134 | $this->expireTime() > 0; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Repositories/Countries.php: -------------------------------------------------------------------------------- 1 | 'Andorra, Principality of', 14 | 'ae' => 'United Arab Emirates', 15 | 'af' => 'Afghanistan, Islamic State of', 16 | 'ag' => 'Antigua and Barbuda', 17 | 'ai' => 'Anguilla', 18 | 'al' => 'Albania', 19 | 'am' => 'Armenia', 20 | 'an' => 'Netherlands Antilles', 21 | 'ao' => 'Angola', 22 | 'aq' => 'Antarctica', 23 | 'ar' => 'Argentina', 24 | 'arpa' => 'Old style Arpanet', 25 | 'as' => 'American Samoa', 26 | 'at' => 'Austria', 27 | 'au' => 'Australia', 28 | 'aw' => 'Aruba', 29 | 'az' => 'Azerbaidjan', 30 | 'ba' => 'Bosnia-Herzegovina', 31 | 'bb' => 'Barbados', 32 | 'bd' => 'Bangladesh', 33 | 'be' => 'Belgium', 34 | 'bf' => 'Burkina Faso', 35 | 'bg' => 'Bulgaria', 36 | 'bh' => 'Bahrain', 37 | 'bi' => 'Burundi', 38 | 'bj' => 'Benin', 39 | 'bm' => 'Bermuda', 40 | 'bn' => 'Brunei Darussalam', 41 | 'bo' => 'Bolivia', 42 | 'br' => 'Brazil', 43 | 'bs' => 'Bahamas', 44 | 'bt' => 'Bhutan', 45 | 'bv' => 'Bouvet Island', 46 | 'bw' => 'Botswana', 47 | 'by' => 'Belarus', 48 | 'bz' => 'Belize', 49 | 'ca' => 'Canada', 50 | 'cc' => 'Cocos (Keeling) Islands', 51 | 'cf' => 'Central African Republic', 52 | 'cd' => 'Congo, The Democratic Republic of the', 53 | 'cg' => 'Congo', 54 | 'ch' => 'Switzerland', 55 | 'ci' => 'Ivory Coast (Cote D\'Ivoire)', 56 | 'ck' => 'Cook Islands', 57 | 'cl' => 'Chile', 58 | 'cm' => 'Cameroon', 59 | 'cn' => 'China', 60 | 'co' => 'Colombia', 61 | 'com' => 'Commercial', 62 | 'cr' => 'Costa Rica', 63 | 'cs' => 'Former Czechoslovakia', 64 | 'cu' => 'Cuba', 65 | 'cv' => 'Cape Verde', 66 | 'cx' => 'Christmas Island', 67 | 'cy' => 'Cyprus', 68 | 'cz' => 'Czech Republic', 69 | 'de' => 'Germany', 70 | 'dj' => 'Djibouti', 71 | 'dk' => 'Denmark', 72 | 'dm' => 'Dominica', 73 | 'do' => 'Dominican Republic', 74 | 'dz' => 'Algeria', 75 | 'ec' => 'Ecuador', 76 | 'edu' => 'Educational', 77 | 'ee' => 'Estonia', 78 | 'eg' => 'Egypt', 79 | 'eh' => 'Western Sahara', 80 | 'er' => 'Eritrea', 81 | 'es' => 'Spain', 82 | 'et' => 'Ethiopia', 83 | 'fi' => 'Finland', 84 | 'fj' => 'Fiji', 85 | 'fk' => 'Falkland Islands', 86 | 'fm' => 'Micronesia', 87 | 'fo' => 'Faroe Islands', 88 | 'fr' => 'France', 89 | 'fx' => 'France (European Territory)', 90 | 'ga' => 'Gabon', 91 | 'gb' => 'Great Britain', 92 | 'gd' => 'Grenada', 93 | 'ge' => 'Georgia', 94 | 'gf' => 'French Guyana', 95 | 'gh' => 'Ghana', 96 | 'gi' => 'Gibraltar', 97 | 'gl' => 'Greenland', 98 | 'gm' => 'Gambia', 99 | 'gn' => 'Guinea', 100 | 'gov' => 'USA Government', 101 | 'gp' => 'Guadeloupe (French)', 102 | 'gq' => 'Equatorial Guinea', 103 | 'gr' => 'Greece', 104 | 'gs' => 'S. Georgia & S. Sandwich Isls.', 105 | 'gt' => 'Guatemala', 106 | 'gu' => 'Guam (USA)', 107 | 'gw' => 'Guinea Bissau', 108 | 'gy' => 'Guyana', 109 | 'hk' => 'Hong Kong', 110 | 'hm' => 'Heard and McDonald Islands', 111 | 'hn' => 'Honduras', 112 | 'hr' => 'Croatia', 113 | 'ht' => 'Haiti', 114 | 'hu' => 'Hungary', 115 | 'id' => 'Indonesia', 116 | 'ie' => 'Ireland', 117 | 'il' => 'Israel', 118 | 'in' => 'India', 119 | 'int' => 'International', 120 | 'io' => 'British Indian Ocean Territory', 121 | 'iq' => 'Iraq', 122 | 'ir' => 'Iran', 123 | 'is' => 'Iceland', 124 | 'it' => 'Italy', 125 | 'jm' => 'Jamaica', 126 | 'jo' => 'Jordan', 127 | 'jp' => 'Japan', 128 | 'ke' => 'Kenya', 129 | 'kg' => 'Kyrgyz Republic (Kyrgyzstan)', 130 | 'kh' => 'Cambodia, Kingdom of', 131 | 'ki' => 'Kiribati', 132 | 'km' => 'Comoros', 133 | 'kn' => 'Saint Kitts & Nevis Anguilla', 134 | 'kp' => 'North Korea', 135 | 'kr' => 'South Korea', 136 | 'kw' => 'Kuwait', 137 | 'ky' => 'Cayman Islands', 138 | 'kz' => 'Kazakhstan', 139 | 'la' => 'Laos', 140 | 'lb' => 'Lebanon', 141 | 'lc' => 'Saint Lucia', 142 | 'li' => 'Liechtenstein', 143 | 'lk' => 'Sri Lanka', 144 | 'lr' => 'Liberia', 145 | 'ls' => 'Lesotho', 146 | 'lt' => 'Lithuania', 147 | 'lu' => 'Luxembourg', 148 | 'lv' => 'Latvia', 149 | 'ly' => 'Libya', 150 | 'ma' => 'Morocco', 151 | 'mc' => 'Monaco', 152 | 'md' => 'Moldavia', 153 | 'mg' => 'Madagascar', 154 | 'mh' => 'Marshall Islands', 155 | 'mil' => 'USA Military', 156 | 'mk' => 'Macedonia', 157 | 'ml' => 'Mali', 158 | 'mm' => 'Myanmar', 159 | 'mn' => 'Mongolia', 160 | 'mo' => 'Macau', 161 | 'mp' => 'Northern Mariana Islands', 162 | 'mq' => 'Martinique (French)', 163 | 'mr' => 'Mauritania', 164 | 'ms' => 'Montserrat', 165 | 'mt' => 'Malta', 166 | 'mu' => 'Mauritius', 167 | 'mv' => 'Maldives', 168 | 'mw' => 'Malawi', 169 | 'mx' => 'Mexico', 170 | 'my' => 'Malaysia', 171 | 'mz' => 'Mozambique', 172 | 'na' => 'Namibia', 173 | 'nato' => 'NATO (this was purged in 1996 - see hq.nato.int)', 174 | 'nc' => 'New Caledonia (French)', 175 | 'ne' => 'Niger', 176 | 'net' => 'Network', 177 | 'nf' => 'Norfolk Island', 178 | 'ng' => 'Nigeria', 179 | 'ni' => 'Nicaragua', 180 | 'nl' => 'Netherlands', 181 | 'no' => 'Norway', 182 | 'np' => 'Nepal', 183 | 'nr' => 'Nauru', 184 | 'nt' => 'Neutral Zone', 185 | 'nu' => 'Niue', 186 | 'nz' => 'New Zealand', 187 | 'om' => 'Oman', 188 | 'org' => 'Non-Profit Making Organisations (sic)', 189 | 'pa' => 'Panama', 190 | 'pe' => 'Peru', 191 | 'pf' => 'Polynesia (French)', 192 | 'pg' => 'Papua New Guinea', 193 | 'ph' => 'Philippines', 194 | 'pk' => 'Pakistan', 195 | 'pl' => 'Poland', 196 | 'pm' => 'Saint Pierre and Miquelon', 197 | 'pn' => 'Pitcairn Island', 198 | 'pr' => 'Puerto Rico', 199 | 'pt' => 'Portugal', 200 | 'pw' => 'Palau', 201 | 'py' => 'Paraguay', 202 | 'qa' => 'Qatar', 203 | 're' => 'Reunion (French)', 204 | 'ro' => 'Romania', 205 | 'ru' => 'Russian Federation', 206 | 'rw' => 'Rwanda', 207 | 'sa' => 'Saudi Arabia', 208 | 'sb' => 'Solomon Islands', 209 | 'sc' => 'Seychelles', 210 | 'sd' => 'Sudan', 211 | 'se' => 'Sweden', 212 | 'sg' => 'Singapore', 213 | 'sh' => 'Saint Helena', 214 | 'si' => 'Slovenia', 215 | 'sj' => 'Svalbard and Jan Mayen Islands', 216 | 'sk' => 'Slovak Republic', 217 | 'sl' => 'Sierra Leone', 218 | 'sm' => 'San Marino', 219 | 'sn' => 'Senegal', 220 | 'so' => 'Somalia', 221 | 'sr' => 'Suriname', 222 | 'st' => 'Saint Tome (Sao Tome) and Principe', 223 | 'su' => 'Former USSR', 224 | 'sv' => 'El Salvador', 225 | 'sy' => 'Syria', 226 | 'sz' => 'Swaziland', 227 | 'tc' => 'Turks and Caicos Islands', 228 | 'td' => 'Chad', 229 | 'tf' => 'French Southern Territories', 230 | 'tg' => 'Togo', 231 | 'th' => 'Thailand', 232 | 'tj' => 'Tadjikistan', 233 | 'tk' => 'Tokelau', 234 | 'tm' => 'Turkmenistan', 235 | 'tn' => 'Tunisia', 236 | 'to' => 'Tonga', 237 | 'tp' => 'East Timor', 238 | 'tr' => 'Turkey', 239 | 'tt' => 'Trinidad and Tobago', 240 | 'tv' => 'Tuvalu', 241 | 'tw' => 'Taiwan', 242 | 'tz' => 'Tanzania', 243 | 'ua' => 'Ukraine', 244 | 'ug' => 'Uganda', 245 | 'uk' => 'United Kingdom', 246 | 'um' => 'USA Minor Outlying Islands', 247 | 'us' => 'United States', 248 | 'uy' => 'Uruguay', 249 | 'uz' => 'Uzbekistan', 250 | 'va' => 'Holy See (Vatican City State)', 251 | 'vc' => 'Saint Vincent & Grenadines', 252 | 've' => 'Venezuela', 253 | 'vg' => 'Virgin Islands (British)', 254 | 'vi' => 'Virgin Islands (USA)', 255 | 'vn' => 'Vietnam', 256 | 'vu' => 'Vanuatu', 257 | 'wf' => 'Wallis and Futuna Islands', 258 | 'ws' => 'Samoa', 259 | 'ye' => 'Yemen', 260 | 'yt' => 'Mayotte', 261 | 'yu' => 'Yugoslavia', 262 | 'za' => 'South Africa', 263 | 'zm' => 'Zambia', 264 | 'zr' => 'Zaire', 265 | 'zw' => 'Zimbabwe', 266 | ]; 267 | 268 | public function all() 269 | { 270 | return new Collection($this->all); 271 | } 272 | 273 | /** 274 | * Get the GeoIp instance. 275 | * 276 | * @return \PragmaRX\Support\GeoIp\GeoIp 277 | */ 278 | public function getGeoIp() 279 | { 280 | return $this->geoIp(); 281 | } 282 | 283 | public function isValid($cc) 284 | { 285 | $cc = strtolower(str_replace('country:', '', $cc)); 286 | 287 | return $this->all()->has($cc); 288 | } 289 | 290 | /** 291 | * Get country code from an IP address. 292 | * 293 | * @param $ip_address 294 | * 295 | * @return string|null 296 | */ 297 | public function getCountryFromIp($ip_address) 298 | { 299 | if ($geo = $this->geoIp()->searchAddr($ip_address)) { 300 | return strtolower($geo['country_code']); 301 | } 302 | } 303 | 304 | /** 305 | * Make a country info from a string. 306 | * 307 | * @param $country 308 | * 309 | * @return string 310 | */ 311 | public function makeCountryFromString($country) 312 | { 313 | if ($ips = $this->ipAddress()->isCidr($country)) { 314 | $country = $ips[0]; 315 | } 316 | 317 | if ($this->validCountry($country)) { 318 | return $country; 319 | } 320 | 321 | if ($this->dataRepository()->ipIsValid($country)) { 322 | $country = $this->getCountryFromIp($this->ipAddress()->hostToIp($country)); 323 | } 324 | 325 | return "country:{$country}"; 326 | } 327 | 328 | /** 329 | * Check if a string is a valid country info. 330 | * 331 | * @param $country 332 | * 333 | * @return bool 334 | */ 335 | public function validCountry($country) 336 | { 337 | $country = strtolower($country); 338 | 339 | if ($this->config()->get('enable_country_search')) { 340 | if (starts_with($country, 'country:') && $this->isValid($country)) { 341 | return true; 342 | } 343 | } 344 | 345 | return false; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Repositories/DataRepository.php: -------------------------------------------------------------------------------- 1 | countries()->validCountry($country); 26 | } 27 | 28 | /** 29 | * Get country code from an IP address. 30 | * 31 | * @param $ip_address 32 | * 33 | * @return string|null 34 | */ 35 | public function getCountryFromIp($ip_address) 36 | { 37 | return $this->countries()->getCountryFromIp($ip_address); 38 | } 39 | 40 | /** 41 | * Get all IP addresses by country. 42 | * 43 | * @param $country 44 | * 45 | * @return \Illuminate\Support\Collection 46 | */ 47 | public function allByCountry($country) 48 | { 49 | $country = $this->makeCountryFromString($country); 50 | 51 | return $this->ipList()->all()->filter(function ($item) use ($country) { 52 | return $item['ip_address'] == $country || 53 | $this->makeCountryFromString($this->getCountryFromIp($item['ip_address'])) == $country; 54 | }); 55 | } 56 | 57 | /** 58 | * Make a country info from a string. 59 | * 60 | * @param $country 61 | * 62 | * @return string 63 | */ 64 | public function makeCountryFromString($country) 65 | { 66 | return $this->countries()->makeCountryFromString($country); 67 | } 68 | 69 | /** 70 | * Clear all items from all lists. 71 | * 72 | * @return int 73 | */ 74 | public function clear() 75 | { 76 | $deleted = 0; 77 | 78 | foreach ($this->ipList()->all() as $ip) { 79 | if ($this->remove($ip['ip_address'])) { 80 | $deleted++; 81 | } 82 | } 83 | 84 | return $deleted; 85 | } 86 | 87 | /** 88 | * Get the GeoIP instance. 89 | * 90 | * @return \PragmaRX\Support\GeoIp\GeoIp 91 | */ 92 | public function getGeoIp() 93 | { 94 | return $this->countries()->getGeoIp(); 95 | } 96 | 97 | /** 98 | * Add an IP to black or whitelist. 99 | * 100 | * @param $whitelist 101 | * @param $ip 102 | * @param bool $force 103 | * 104 | * @return bool 105 | */ 106 | public function addToList($whitelist, $ip, $force = false) 107 | { 108 | return $this->ipList()->addToList($whitelist, $ip, $force); 109 | } 110 | 111 | /** 112 | * Get all IP addresses. 113 | * 114 | * @return \Illuminate\Support\Collection 115 | */ 116 | public function all() 117 | { 118 | return $this->ipList()->all(); 119 | } 120 | 121 | /** 122 | * Tell in which list (black/white) an IP address is. 123 | * 124 | * @param $ip_address 125 | * 126 | * @return null|string 127 | */ 128 | public function whichList($ip_address) 129 | { 130 | return $this->ipList()->whichList($ip_address); 131 | } 132 | 133 | /** 134 | * Check if IP address is valid. 135 | * 136 | * @param $ip 137 | * 138 | * @return bool 139 | */ 140 | public function ipIsValid($ip) 141 | { 142 | return $this->ipAddress()->isValid($ip); 143 | } 144 | 145 | /** 146 | * Find an IP address in the data source. 147 | * 148 | * @param string $ip 149 | * 150 | * @return mixed 151 | */ 152 | public function find($ip) 153 | { 154 | return $this->ipList()->find($ip); 155 | } 156 | 157 | /** 158 | * Remove IP from all lists. 159 | * 160 | * @param $ip 161 | * 162 | * @return bool 163 | */ 164 | public function remove($ip) 165 | { 166 | return $this->ipList()->remove($ip); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Repositories/IpList.php: -------------------------------------------------------------------------------- 1 | model = $model; 27 | } 28 | 29 | /** 30 | * Get a list of non database ip addresses. 31 | * 32 | * @return array 33 | */ 34 | private function getNonDatabaseIps() 35 | { 36 | return array_merge_recursive( 37 | array_map(function ($ip) { 38 | $ip['whitelisted'] = true; 39 | 40 | return $ip; 41 | }, $this->formatIpArray($this->config()->get('whitelist'))), 42 | array_map(function ($ip) { 43 | $ip['whitelisted'] = false; 44 | 45 | return $ip; 46 | }, $this->formatIpArray($this->config()->get('blacklist'))) 47 | ); 48 | } 49 | 50 | /** 51 | * Remove ip from all array lists. 52 | * 53 | * @param $ip 54 | * 55 | * @return bool 56 | */ 57 | private function removeFromArrayList($ip) 58 | { 59 | return $this->removeFromArrayListType('whitelist', $ip) || 60 | $this->removeFromArrayListType('blacklist', $ip); 61 | } 62 | 63 | /** 64 | * Remove the ip address from an array list. 65 | * 66 | * @param $type 67 | * @param $ip 68 | * 69 | * @return bool 70 | */ 71 | private function removeFromArrayListType($type, $ip) 72 | { 73 | if (($key = array_search($ip, $data = $this->config()->get($type))) !== false) { 74 | unset($data[$key]); 75 | 76 | $this->cache()->forget($ip); 77 | 78 | $this->config()->set($type, $data); 79 | 80 | $this->messages()->addMessage(sprintf('%s removed from %s', $ip, $type)); 81 | 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | /** 89 | * Remove ip from database. 90 | * 91 | * @param \Illuminate\Database\Eloquent\Model $ip 92 | * 93 | * @return bool 94 | */ 95 | private function removeFromDatabaseList($ip) 96 | { 97 | if ($ip = $this->find($ip)) { 98 | $ip->delete(); 99 | 100 | $this->cache()->forget($ip->ip_address); 101 | 102 | $this->messages()->addMessage(sprintf('%s removed from %s', $ip, $ip->whitelisted ? 'whitelist' : 'blacklist')); 103 | } 104 | } 105 | 106 | /** 107 | * Transform a list of ips to a list of models. 108 | * 109 | * @param $ipList 110 | * 111 | * @return \Illuminate\Support\Collection 112 | */ 113 | private function toModels($ipList) 114 | { 115 | $ips = []; 116 | 117 | foreach ($ipList as $ip) { 118 | $ips[] = $this->makeModel($ip); 119 | } 120 | 121 | return collect($ips); 122 | } 123 | 124 | /** 125 | * Make a model instance. 126 | * 127 | * @param $ip 128 | * 129 | * @return \Illuminate\Database\Eloquent\Model 130 | */ 131 | private function makeModel($ip) 132 | { 133 | return $this->model->newInstance($ip); 134 | } 135 | 136 | /** 137 | * Read a file contents. 138 | * 139 | * @param $file 140 | * 141 | * @return array 142 | */ 143 | private function readFile($file) 144 | { 145 | if ($this->fileSystem()->exists($file)) { 146 | $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 147 | 148 | return $this->makeArrayOfIps($lines); 149 | } 150 | 151 | return []; 152 | } 153 | 154 | /** 155 | * Format all ips in an array. 156 | * 157 | * @param $list 158 | * 159 | * @return array 160 | */ 161 | private function formatIpArray($list) 162 | { 163 | return array_map(function ($ip) { 164 | return ['ip_address' => $ip]; 165 | }, $this->makeArrayOfIps($list)); 166 | } 167 | 168 | /** 169 | * Make a list of arrays from all sort of things. 170 | * 171 | * @param $list 172 | * 173 | * @return array 174 | */ 175 | private function makeArrayOfIps($list) 176 | { 177 | $list = (array) $list ?: []; 178 | 179 | $ips = []; 180 | 181 | foreach ($list as $item) { 182 | $ips = array_merge($ips, $this->getIpsFromAnything($item)); 183 | } 184 | 185 | return $ips; 186 | } 187 | 188 | /** 189 | * Get a list of ips from anything. 190 | * 191 | * @param $item 192 | * 193 | * @return array 194 | */ 195 | private function getIpsFromAnything($item) 196 | { 197 | if (starts_with($item, 'country:')) { 198 | return [$item]; 199 | } 200 | 201 | $item = $this->ipAddress()->hostToIp($item); 202 | 203 | if ($this->ipAddress()->ipV4Valid($item)) { 204 | return [$item]; 205 | } 206 | 207 | return $this->readFile($item); 208 | } 209 | 210 | /** 211 | * Search for an ip in a list of ips. 212 | * 213 | * @param $ip 214 | * @param $ips 215 | * 216 | * @return null|\Illuminate\Database\Eloquent\Model 217 | */ 218 | private function ipArraySearch($ip, $ips) 219 | { 220 | foreach ($ips as $key => $value) { 221 | if ( 222 | (isset($value['ip_address']) && $value['ip_address'] == $ip) || 223 | (strval($key) == $ip) || 224 | ($value == $ip) 225 | ) { 226 | return $value; 227 | } 228 | } 229 | } 230 | 231 | /** 232 | * Get all IPs from database. 233 | * 234 | * @return \Illuminate\Support\Collection 235 | */ 236 | private function getAllFromDatabase() 237 | { 238 | if ($this->config()->get('use_database')) { 239 | return $this->model->all(); 240 | } else { 241 | return collect([]); 242 | } 243 | } 244 | 245 | /** 246 | * Merge IP lists. 247 | * 248 | * @param $database_ips 249 | * @param $config_ips 250 | * 251 | * @return \Illuminate\Support\Collection 252 | */ 253 | private function mergeLists($database_ips, $config_ips) 254 | { 255 | return collect($database_ips) 256 | ->merge(collect($config_ips)); 257 | } 258 | 259 | /** 260 | * Check if an IP address is in a secondary (black/white) list. 261 | * 262 | * @param $ip_address 263 | * 264 | * @return bool|array 265 | */ 266 | public function checkSecondaryLists($ip_address) 267 | { 268 | foreach ($this->all() as $range) { 269 | if ($this->ipAddress()->hostToIp($range->ip_address) == $ip_address || $this->ipAddress()->validRange($ip_address, $range)) { 270 | return $range; 271 | } 272 | } 273 | 274 | return false; 275 | } 276 | 277 | /** 278 | * Add an IP to black or whitelist. 279 | * 280 | * @param $whitelist 281 | * @param $ip 282 | * @param bool $force 283 | * 284 | * @return bool 285 | */ 286 | public function addToList($whitelist, $ip, $force = false) 287 | { 288 | $list = $whitelist 289 | ? 'whitelist' 290 | : 'blacklist'; 291 | 292 | if (!$this->ipAddress()->isValid($ip)) { 293 | return false; 294 | } 295 | 296 | $listed = $this->whichList($ip); 297 | 298 | if ($listed == $list) { 299 | $this->messages()->addMessage(sprintf('%s is already %s', $ip, $list.'ed')); 300 | 301 | return false; 302 | } else { 303 | if (empty($listed) || $force) { 304 | if (!empty($listed)) { 305 | $this->remove($ip); 306 | } 307 | 308 | $this->addToProperList($whitelist, $ip); 309 | 310 | $this->messages()->addMessage(sprintf('%s is now %s', $ip, $list.'ed')); 311 | 312 | return true; 313 | } 314 | } 315 | 316 | $this->messages()->addMessage(sprintf('%s is currently %sed', $ip, $listed)); 317 | 318 | return false; 319 | } 320 | 321 | /** 322 | * Tell in which list (black/white) an IP address is. 323 | * 324 | * @param $ip_address 325 | * 326 | * @return null|string 327 | */ 328 | public function whichList($ip_address) 329 | { 330 | if (!$ip_found = $this->find($ip_address)) { 331 | if (!$ip_found = $this->findByCountry($ip_address)) { 332 | if (!$ip_found = $this->checkSecondaryLists($ip_address)) { 333 | return; 334 | } 335 | } 336 | } 337 | 338 | return !is_null($ip_found) 339 | ? ($ip_found['whitelisted'] ? 'whitelist' : 'blacklist') 340 | : null; 341 | } 342 | 343 | /** 344 | * Remove IP from all lists. 345 | * 346 | * @param $ip 347 | * 348 | * @return bool 349 | */ 350 | public function remove($ip) 351 | { 352 | $listed = $this->whichList($ip); 353 | 354 | if (!empty($listed)) { 355 | $this->delete($ip); 356 | 357 | return true; 358 | } 359 | 360 | $this->messages()->addMessage(sprintf('%s is not listed', $ip)); 361 | 362 | return false; 363 | } 364 | 365 | /** 366 | * Add ip or range to array list. 367 | * 368 | * @param $whitelist 369 | * @param $ip 370 | * 371 | * @return array|mixed 372 | */ 373 | private function addToArrayList($whitelist, $ip) 374 | { 375 | $data = $this->config()->get($list = $whitelist ? 'whitelist' : 'blacklist'); 376 | 377 | $data[] = $ip; 378 | 379 | $this->config()->set($list, $data); 380 | 381 | return $data; 382 | } 383 | 384 | /** 385 | * Find an IP address in the data source. 386 | * 387 | * @param string $ip 388 | * 389 | * @return mixed 390 | */ 391 | public function find($ip) 392 | { 393 | if ($this->cache()->has($ip)) { 394 | return $this->cache()->get($ip); 395 | } 396 | 397 | if ($model = $this->findIp($ip)) { 398 | $this->cache()->remember($model); 399 | } 400 | 401 | return $model; 402 | } 403 | 404 | /** 405 | * Find an IP address by country. 406 | * 407 | * @param $country 408 | * 409 | * @return mixed 410 | */ 411 | public function findByCountry($country) 412 | { 413 | if ($this->config()->get('enable_country_search') && !is_null($country = $this->countries()->makeCountryFromString($country))) { 414 | return $this->find($country); 415 | } 416 | } 417 | 418 | /** 419 | * Find a Ip in the data source. 420 | * 421 | * @param string $ip 422 | * 423 | * @return void 424 | */ 425 | public function addToProperList($whitelist, $ip) 426 | { 427 | $this->config()->get('use_database') ? 428 | $this->addToDatabaseList($whitelist, $ip) : 429 | $this->addToArrayList($whitelist, $ip); 430 | } 431 | 432 | /** 433 | * Delete ip address. 434 | * 435 | * @param $ip 436 | * 437 | * @return bool|void 438 | */ 439 | public function delete($ip) 440 | { 441 | $this->config()->get('use_database') ? 442 | $this->removeFromDatabaseList($ip) : 443 | $this->removeFromArrayList($ip); 444 | } 445 | 446 | /** 447 | * Get all IP addresses. 448 | * 449 | * @return \Illuminate\Support\Collection 450 | */ 451 | public function all() 452 | { 453 | $cacheTime = $this->config()->get('ip_list_cache_expire_time'); 454 | 455 | if ($cacheTime > 0 && $list = $this->cache()->get(static::IP_ADDRESS_LIST_CACHE_NAME)) { 456 | return $list; 457 | } 458 | 459 | $list = $this->mergeLists( 460 | $this->getAllFromDatabase(), 461 | $this->toModels($this->getNonDatabaseIps()) 462 | ); 463 | 464 | if ($cacheTime > 0) { 465 | $this->cache()->put(static::IP_ADDRESS_LIST_CACHE_NAME, $list, $cacheTime); 466 | } 467 | 468 | return $list; 469 | } 470 | 471 | /** 472 | * Find ip address in all lists. 473 | * 474 | * @param $ip 475 | * 476 | * @return \Illuminate\Database\Eloquent\Model|null|static 477 | */ 478 | private function findIp($ip) 479 | { 480 | if ($model = $this->nonDatabaseFind($ip)) { 481 | return $model; 482 | } 483 | 484 | if ($this->config()->get('use_database')) { 485 | return $this->model->where('ip_address', $ip)->first(); 486 | } 487 | } 488 | 489 | /** 490 | * Find ip in non database lists. 491 | * 492 | * @param $ip 493 | * 494 | * @return \Illuminate\Database\Eloquent\Model 495 | */ 496 | private function nonDatabaseFind($ip) 497 | { 498 | $ips = $this->getNonDatabaseIps(); 499 | 500 | if ($ip = $this->ipArraySearch($ip, $ips)) { 501 | return $this->makeModel($ip); 502 | } 503 | } 504 | 505 | /** 506 | * Add ip or range to database. 507 | * 508 | * @param $whitelist 509 | * @param $ip 510 | * 511 | * @return \Illuminate\Database\Eloquent\Model 512 | */ 513 | private function addToDatabaseList($whitelist, $ip) 514 | { 515 | $this->model->unguard(); 516 | 517 | $model = $this->model->create([ 518 | 'ip_address' => $ip, 519 | 'whitelisted' => $whitelist, 520 | ]); 521 | 522 | $this->cache()->remember($model); 523 | 524 | return $model; 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/Repositories/Message.php: -------------------------------------------------------------------------------- 1 | messageList = collect(); 24 | } 25 | 26 | /** 27 | * Add a message to the messages list. 28 | * 29 | * @param $message 30 | * 31 | * @return void 32 | */ 33 | public function addMessage($message) 34 | { 35 | collect((array) $message)->each(function ($item) { 36 | collect($item)->flatten()->each(function ($flattened) { 37 | $this->messageList->push($flattened); 38 | }); 39 | }); 40 | } 41 | 42 | /** 43 | * Get the messages. 44 | * 45 | * @return \Illuminate\Support\Collection 46 | */ 47 | public function getMessages() 48 | { 49 | return $this->messageList; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Support/AttackBlocker.php: -------------------------------------------------------------------------------- 1 | null, 20 | 21 | 'country' => null, 22 | ]; 23 | 24 | /** 25 | * The ip address. 26 | * 27 | * @var string 28 | */ 29 | protected $ipAddress; 30 | 31 | /** 32 | * The cache key. 33 | * 34 | * @var string 35 | */ 36 | protected $key; 37 | 38 | /** 39 | * The max request count. 40 | * 41 | * @var int 42 | */ 43 | protected $maxRequestCount; 44 | 45 | /** 46 | * The max request count. 47 | * 48 | * @var int 49 | */ 50 | protected $maxSeconds; 51 | 52 | /** 53 | * The firewall instance. 54 | * 55 | * @var Firewall 56 | */ 57 | protected $firewall; 58 | 59 | /** 60 | * The country. 61 | * 62 | * @var string 63 | */ 64 | protected $country; 65 | 66 | /** 67 | * The enabled items. 68 | * 69 | * @var \Illuminate\Support\Collection 70 | */ 71 | protected $enabledItems; 72 | 73 | /** 74 | * Blacklist the IP address. 75 | * 76 | * @param $record 77 | * 78 | * @return bool 79 | */ 80 | protected function blacklist($record) 81 | { 82 | if ($record['isBlacklisted']) { 83 | return false; 84 | } 85 | 86 | $blacklistUnknown = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_unknown"); 87 | 88 | $blackWhitelisted = $this->config()->get("attack_blocker.action.{$record['type']}.blacklist_whitelisted"); 89 | 90 | if ($blacklistUnknown || $blackWhitelisted) { 91 | $record['isBlacklisted'] = true; 92 | 93 | $ipAddress = $record['type'] == 'country' ? 'country:'.$record['country_code'] : $record['ipAddress']; 94 | 95 | $this->firewall->blacklist($ipAddress, $blackWhitelisted); 96 | 97 | $this->save($record); 98 | 99 | return true; 100 | } 101 | 102 | return false; 103 | } 104 | 105 | /** 106 | * Check for expiration. 107 | * 108 | * @return void 109 | */ 110 | protected function checkExpiration() 111 | { 112 | $this->getEnabledItems()->each(function ($index, $type) { 113 | if (($this->now()->diffInSeconds($this->record[$type]['lastRequestAt'])) <= ($this->getMaxSecondsForType($type))) { 114 | return $this->record; 115 | } 116 | 117 | return $this->record[$type] = $this->getEmptyRecord($this->record[$type]['key'], $type); 118 | }); 119 | } 120 | 121 | /** 122 | * Get an empty record. 123 | * 124 | * @return array 125 | */ 126 | protected function getEmptyRecord($key, $type) 127 | { 128 | return $this->makeRecord($key, $type); 129 | } 130 | 131 | /** 132 | * Get enabled items. 133 | * 134 | * @return \Illuminate\Support\Collection 135 | */ 136 | private function getEnabledItems() 137 | { 138 | if (is_null($this->enabledItems)) { 139 | $this->loadConfig(); 140 | } 141 | 142 | return $this->enabledItems; 143 | } 144 | 145 | /** 146 | * Get a timestamp for the time the cache should expire. 147 | * 148 | * @param $type 149 | * 150 | * @return \Carbon\Carbon 151 | */ 152 | protected function getExpirationTimestamp($type) 153 | { 154 | return $this->now()->addSeconds($this->getMaxSecondsForType($type)); 155 | } 156 | 157 | /** 158 | * Search geo localization by ip. 159 | * 160 | * @param $ipAddress 161 | * 162 | * @return array|null 163 | */ 164 | protected function getGeo($ipAddress) 165 | { 166 | return $this->firewall->getGeoIp()->searchAddr($ipAddress); 167 | } 168 | 169 | /** 170 | * Get max request count from config. 171 | * 172 | * @param string $type 173 | * 174 | * @return int 175 | */ 176 | protected function getMaxRequestCountForType($type = 'ip') 177 | { 178 | return !is_null($this->maxRequestCount) 179 | ? $this->maxRequestCount 180 | : ($this->maxRequestCount = $this->config()->get("attack_blocker.allowed_frequency.{$type}.requests")); 181 | } 182 | 183 | /** 184 | * Get max seconds from config. 185 | * 186 | * @param $type 187 | * 188 | * @return int 189 | */ 190 | protected function getMaxSecondsForType($type) 191 | { 192 | return !is_null($this->maxSeconds) 193 | ? $this->maxSeconds 194 | : ($this->maxSeconds = $this->config()->get("attack_blocker.allowed_frequency.{$type}.seconds")); 195 | } 196 | 197 | /** 198 | * Get the response configuration. 199 | * 200 | * @return array 201 | */ 202 | protected function getResponseConfig() 203 | { 204 | return $this->config()->get('attack_blocker.response'); 205 | } 206 | 207 | /** 208 | * Increment request count. 209 | * 210 | * @return void 211 | */ 212 | protected function increment() 213 | { 214 | $this->getEnabledItems()->each(function ($index, $type) { 215 | $this->save($type, ['requestCount' => $this->record[$type]['requestCount'] + 1]); 216 | }); 217 | } 218 | 219 | /** 220 | * Check if this is an attack. 221 | * 222 | * @return bool 223 | */ 224 | protected function isAttack() 225 | { 226 | return $this->getEnabledItems()->filter(function ($index, $type) { 227 | if (!$this->isWhitelisted($type) && $this->record[$type]['requestCount'] > $this->getMaxRequestCountForType($type)) { 228 | $this->takeAction($this->record[$type]); 229 | 230 | return true; 231 | } 232 | })->count() > 0; 233 | } 234 | 235 | /** 236 | * Check for attacks. 237 | * 238 | * @param $ipAddress 239 | * 240 | * @return bool 241 | */ 242 | public function isBeingAttacked($ipAddress) 243 | { 244 | if (!$this->isEnabled()) { 245 | return false; 246 | } 247 | 248 | $this->loadRecord($ipAddress); 249 | 250 | return $this->isAttack(); 251 | } 252 | 253 | /** 254 | * Get enabled state. 255 | * 256 | * @return bool 257 | */ 258 | protected function isEnabled() 259 | { 260 | return count($this->getEnabledItems()) > 0; 261 | } 262 | 263 | /** 264 | * Is the current user whitelisted? 265 | * 266 | * @param $type 267 | * 268 | * @return bool 269 | */ 270 | private function isWhitelisted($type) 271 | { 272 | return $this->firewall->whichList($this->record[$type]['ipAddress']) == 'whitelist' && 273 | !$this->config()->get("attack_blocker.action.{$this->record[$type]['type']}.blacklist_whitelisted"); 274 | } 275 | 276 | /** 277 | * Load the configuration. 278 | * 279 | * @return void 280 | */ 281 | private function loadConfig() 282 | { 283 | $this->enabledItems = collect($this->config()->get('attack_blocker.enabled'))->filter(function ($item) { 284 | return $item === true; 285 | }); 286 | } 287 | 288 | /** 289 | * Load a record. 290 | * 291 | * @param $ipAddress 292 | * 293 | * @return void 294 | */ 295 | protected function loadRecord($ipAddress) 296 | { 297 | $this->ipAddress = $ipAddress; 298 | 299 | $this->loadRecordItems(); 300 | 301 | $this->checkExpiration(); 302 | 303 | $this->increment(); 304 | } 305 | 306 | /** 307 | * Load all record items. 308 | * 309 | * @return void 310 | */ 311 | protected function loadRecordItems() 312 | { 313 | $this->getEnabledItems()->each(function ($index, $type) { 314 | if (is_null($this->record[$type] = $this->cache()->get($key = $this->makeKeyForType($type, $this->ipAddress)))) { 315 | $this->record[$type] = $this->getEmptyRecord($key, $type); 316 | } 317 | }); 318 | } 319 | 320 | /** 321 | * Write to the log. 322 | * 323 | * @param $string 324 | * 325 | * @return void 326 | */ 327 | protected function log($string) 328 | { 329 | $this->firewall->log($string); 330 | } 331 | 332 | /** 333 | * Send attack the the log. 334 | * 335 | * @param $record 336 | * 337 | * @return void 338 | */ 339 | protected function logAttack($record) 340 | { 341 | $this->log("Attacker detected - IP: {$record['ipAddress']} - Request count: {$record['requestCount']}"); 342 | } 343 | 344 | /** 345 | * Get the current date time. 346 | * 347 | * @return Carbon 348 | */ 349 | private function now() 350 | { 351 | Carbon::setTestNow(); 352 | 353 | return Carbon::now(); 354 | } 355 | 356 | /** 357 | * Make a response. 358 | * 359 | * @return null|\Illuminate\Http\Response 360 | */ 361 | public function responseToAttack() 362 | { 363 | if ($this->isAttack()) { 364 | return (new Responder())->respond($this->getResponseConfig(), $this->record); 365 | } 366 | } 367 | 368 | /** 369 | * Make a hashed key. 370 | * 371 | * @param $field 372 | * 373 | * @return string 374 | */ 375 | public function makeHashedKey($field) 376 | { 377 | return hash( 378 | 'sha256', 379 | $this->config()->get('attack_blocker.cache_key_prefix').'-'.$field 380 | ); 381 | } 382 | 383 | /** 384 | * Make the cache key to record countries. 385 | * 386 | * @param $ipAddress 387 | * 388 | * @return string|null 389 | */ 390 | protected function makeKeyForType($type, $ipAddress) 391 | { 392 | if ($type == 'country') { 393 | $geo = $this->getGeo($ipAddress); 394 | 395 | if (is_null($geo)) { 396 | $this->log("No GeoIp info for {$ipAddress}, is it installed?"); 397 | } 398 | 399 | if (!is_null($geo) && $this->country = $geo['country_code']) { 400 | return $this->makeHashedKey($this->country); 401 | } 402 | 403 | unset($this->getEnabledItems()['country']); 404 | 405 | return; 406 | } 407 | 408 | return $this->makeHashedKey($this->ipAddress = $ipAddress); 409 | } 410 | 411 | /** 412 | * Make a record. 413 | * 414 | * @param $key 415 | * @param $type 416 | * 417 | * @return array 418 | */ 419 | protected function makeRecord($key, $type) 420 | { 421 | $geo = $this->getGeo($this->ipAddress); 422 | 423 | return [ 424 | 'type' => $type, 425 | 426 | 'key' => $key, 427 | 428 | 'ipAddress' => $this->ipAddress, 429 | 430 | 'requestCount' => 0, 431 | 432 | 'firstRequestAt' => $this->now(), 433 | 434 | 'lastRequestAt' => $this->now(), 435 | 436 | 'isBlacklisted' => false, 437 | 438 | 'wasNotified' => false, 439 | 440 | 'userAgent' => request()->server('HTTP_USER_AGENT'), 441 | 442 | 'server' => request()->server(), 443 | 444 | 'geoIp' => $geo, 445 | 446 | 'country_name' => $geo ? $geo['country_name'] : null, 447 | 448 | 'country_code' => $geo ? $geo['country_code'] : null, 449 | 450 | 'host' => gethostbyaddr($this->ipAddress), 451 | ]; 452 | } 453 | 454 | /** 455 | * Send notifications. 456 | * 457 | * @param $record 458 | * 459 | * @return void 460 | */ 461 | protected function notify($record) 462 | { 463 | if (!$record['wasNotified'] && $this->config()->get('notifications.enabled')) { 464 | $this->save($record['type'], ['wasNotified' => true]); 465 | 466 | collect($this->config()->get('notifications.channels'))->filter(function ($value, $channel) use ($record) { 467 | event(new AttackDetected($record, $channel)); 468 | }); 469 | } 470 | } 471 | 472 | /** 473 | * Renew first request timestamp, to keep the offender blocked. 474 | * 475 | * @param $record 476 | * 477 | * @return void 478 | */ 479 | protected function renew($record) 480 | { 481 | $this->save($record['type'], ['lastRequestAt' => $this->now()]); 482 | } 483 | 484 | /** 485 | * Set firewall. 486 | * 487 | * @param Firewall $firewall 488 | * 489 | * @return void 490 | */ 491 | public function setFirewall($firewall) 492 | { 493 | $this->firewall = $firewall; 494 | } 495 | 496 | /** 497 | * Store record on cache. 498 | * 499 | * @param $type 500 | * @param array $items 501 | * 502 | * @return array 503 | */ 504 | protected function save($type, $items = []) 505 | { 506 | if (is_array($type)) { 507 | $items = $type; 508 | 509 | $type = $type['type']; 510 | } 511 | 512 | $this->record[$type] = array_merge($this->record[$type], $items); 513 | 514 | $this->cache()->put($this->record[$type]['key'], $this->record[$type], $this->getExpirationTimestamp($type)); 515 | 516 | return $this->record[$type]; 517 | } 518 | 519 | /** 520 | * Take the necessary action to keep the offender blocked. 521 | * 522 | * @return void 523 | */ 524 | protected function takeAction($record) 525 | { 526 | $this->renew($record); 527 | 528 | $this->blacklist($record); 529 | 530 | $this->notify($record); 531 | 532 | $this->logAttack($record); 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /src/Support/IpAddress.php: -------------------------------------------------------------------------------- 1 | hostToIp($ip)) || 21 | $this->countries()->validCountry($ip)) { 22 | $this->messages()->addMessage(sprintf('%s is not a valid IP address', $ip)); 23 | } 24 | 25 | return $result; 26 | } 27 | 28 | /** 29 | * Get the ip address of a host. 30 | * 31 | * @param $ip 32 | * 33 | * @return string 34 | */ 35 | public function hostToIp($ip) 36 | { 37 | if (is_string($ip) && starts_with($ip, $string = 'host:')) { 38 | return gethostbyname(str_replace($string, '', $ip)); 39 | } 40 | 41 | return $ip; 42 | } 43 | 44 | /** 45 | * Check if IP is in a valid range. 46 | * 47 | * @param $ip_address 48 | * @param $range 49 | * 50 | * @return bool 51 | */ 52 | public function validRange($ip_address, $range) 53 | { 54 | return $this->config()->get('enable_range_search') && 55 | SupportIpAddress::ipV4Valid($range->ip_address) && 56 | SupportIpAddress::ipv4InRange($ip_address, $range->ip_address); 57 | } 58 | 59 | /** 60 | * Check if an ip v4 is valid. 61 | * 62 | * @param $item 63 | * 64 | * @return bool 65 | */ 66 | public function ipV4Valid($item) 67 | { 68 | if (realpath($item) !== false) { 69 | return false; 70 | } 71 | 72 | return SupportIpAddress::ipV4Valid($item); 73 | } 74 | 75 | /** 76 | * Check if a string is a CIDR. 77 | * 78 | * @param $country 79 | * 80 | * @return bool|array 81 | */ 82 | public function isCidr($country) 83 | { 84 | return SupportIpAddress::isCidr($country); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Support/Responder.php: -------------------------------------------------------------------------------- 1 | getFromInstanceCache($binding))) { 18 | $instance = app('firewall.'.$binding); 19 | } 20 | 21 | $this->setInstance($binding, $instance); 22 | 23 | return $instance; 24 | } 25 | 26 | /** 27 | * Get an instance from the instance memory cache. 28 | * 29 | * @param $binding 30 | * @param $instance 31 | */ 32 | public function getFromInstanceCache($binding) 33 | { 34 | if (isset($this->instances[$binding])) { 35 | return $this->instances[$binding]; 36 | } 37 | } 38 | 39 | /** 40 | * Set the instance. 41 | * 42 | * @param $binding 43 | * @param $instance 44 | */ 45 | public function setInstance($binding, $instance) 46 | { 47 | $this->instances[$binding] = $instance; 48 | } 49 | 50 | /** 51 | * Get the Countries instance. 52 | * 53 | * @return \PragmaRX\Firewall\Repositories\Countries 54 | */ 55 | public function countries() 56 | { 57 | return $this->getInstance('countries'); 58 | } 59 | 60 | /** 61 | * Get the IpList instance. 62 | * 63 | * @return \PragmaRX\Firewall\Repositories\IpList 64 | */ 65 | public function ipList() 66 | { 67 | return $this->getInstance('iplist'); 68 | } 69 | 70 | /** 71 | * Get the MessageRepository instance. 72 | * 73 | * @return \PragmaRX\Firewall\Repositories\Message 74 | */ 75 | public function messages() 76 | { 77 | return $this->getInstance('messages'); 78 | } 79 | 80 | /** 81 | * Get the Cache instance. 82 | * 83 | * @return \PragmaRX\Firewall\Repositories\Cache\Cache 84 | */ 85 | public function cache() 86 | { 87 | return $this->getInstance('cache'); 88 | } 89 | 90 | /** 91 | * Get the Config instance. 92 | * 93 | * @return \PragmaRX\Support\Config 94 | */ 95 | public function config() 96 | { 97 | return $this->getInstance('config'); 98 | } 99 | 100 | /** 101 | * Get the FileSystem instance. 102 | * 103 | * @return \PragmaRX\Support\Filesystem 104 | */ 105 | public function fileSystem() 106 | { 107 | return $this->getInstance('filesystem'); 108 | } 109 | 110 | /** 111 | * Get the GeoIp instance. 112 | * 113 | * @return \PragmaRX\Support\GeoIp\GeoIp 114 | */ 115 | public function geoIp() 116 | { 117 | return $this->getInstance('geoip'); 118 | } 119 | 120 | /** 121 | * Get the IpAddress instance. 122 | * 123 | * @return \PragmaRX\Firewall\Support\IpAddress 124 | */ 125 | public function ipAddress() 126 | { 127 | return $this->getInstance('ipaddress'); 128 | } 129 | 130 | /** 131 | * Get the DataRepository instance. 132 | * 133 | * @return \PragmaRX\Firewall\Repositories\DataRepository 134 | */ 135 | public function dataRepository() 136 | { 137 | return $this->getInstance('datarepository'); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/AddToList.php: -------------------------------------------------------------------------------- 1 | description = sprintf($this->description, $this->listName); 22 | } 23 | 24 | /** 25 | * Execute the console command. 26 | * 27 | * @return void 28 | */ 29 | public function fire() 30 | { 31 | $this->fireCommand($this->listName, [$this->argument('ip'), $this->option('force')]); 32 | } 33 | 34 | /** 35 | * Get the console command arguments. 36 | * 37 | * @return array 38 | */ 39 | protected function getArguments() 40 | { 41 | return [ 42 | ['ip', InputArgument::REQUIRED, 'The IP address to be added.'], 43 | ]; 44 | } 45 | 46 | /** 47 | * Get the console command options. 48 | * 49 | * @return array 50 | */ 51 | protected function getOptions() 52 | { 53 | return [ 54 | ['force', null, InputOption::VALUE_NONE, 'Remove IP before adding it to the list.'], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Base.php: -------------------------------------------------------------------------------- 1 | $type($message); 13 | } 14 | } 15 | 16 | /** 17 | * Handle the command. 18 | * 19 | * @return void 20 | */ 21 | public function handle() 22 | { 23 | $this->fire(); 24 | } 25 | 26 | /** 27 | * Fire the command. 28 | * 29 | * @return void 30 | */ 31 | public function fireCommand($method, $parameters) 32 | { 33 | $instance = app('firewall'); 34 | 35 | $type = call_user_func_array([$instance, $method], $parameters) 36 | ? 'info' 37 | : 'error'; 38 | 39 | $this->displayMessages($type, app('firewall')->getMessages()); 40 | } 41 | 42 | abstract public function fire(); 43 | } 44 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Blacklist.php: -------------------------------------------------------------------------------- 1 | option('force')) { 31 | $this->error('This command won\'t run unless you use --force.'); 32 | } else { 33 | if (app('firewall')->clear()) { 34 | $this->info('List cleared.'); 35 | } else { 36 | $this->info('There were no IP addresses to be deleted.'); 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Get the console command options. 43 | * 44 | * @return array 45 | */ 46 | protected function getOptions() 47 | { 48 | return [ 49 | ['force', null, InputOption::VALUE_NONE, 'Remove IP before adding it to the list.'], 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Flush.php: -------------------------------------------------------------------------------- 1 | clear(); 29 | 30 | $this->info('Cache cleared.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Remove.php: -------------------------------------------------------------------------------- 1 | fireCommand('remove', [$this->argument('ip')]); 31 | } 32 | 33 | /** 34 | * Get the console command arguments. 35 | * 36 | * @return array 37 | */ 38 | protected function getArguments() 39 | { 40 | return [ 41 | ['ip', InputArgument::REQUIRED, 'The IP address to be added.'], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Report.php: -------------------------------------------------------------------------------- 1 | output); 39 | 40 | $list = []; 41 | 42 | foreach (app('firewall')->report() as $ip) { 43 | $list[] = [ 44 | $ip['ip_address'], 45 | $ip['whitelisted'] == false 46 | ? '' 47 | : ' X ', 48 | $ip['whitelisted'] == false 49 | ? ' X ' 50 | : '', 51 | ]; 52 | } 53 | 54 | $table->setHeaders(['IP Address', 'Whitelist', 'Blacklist'])->setRows($list); 55 | 56 | $table->render(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/UpdateGeoIp.php: -------------------------------------------------------------------------------- 1 | updateGeoIp() 31 | ? 'info' 32 | : 'error'; 33 | 34 | $this->displayMessages($type, $firewall->getMessages()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/Artisan/Whitelist.php: -------------------------------------------------------------------------------- 1 | routeNotificationForSlack(); 16 | } 17 | 18 | return $this->routeNotificationForEmail(); 19 | } 20 | 21 | /** 22 | * Route notifications for the Email channel. 23 | * 24 | * @return string 25 | */ 26 | public function routeNotificationForEmail() 27 | { 28 | return $this->email; 29 | } 30 | 31 | /** 32 | * Route notifications for the Slack channel. 33 | * 34 | * @return string 35 | */ 36 | public function routeNotificationForSlack() 37 | { 38 | return config('services.slack.webhook_url'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Vendor/Laravel/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | getConfig('firewall_model')) { 54 | throw new ConfigurationOptionNotAvailable('Config option "firewall_model" is not available, please publish/check your configuration.'); 55 | } 56 | 57 | return new $firewallModel(); 58 | } 59 | 60 | /** 61 | * Get the root directory for this ServiceProvider. 62 | * 63 | * @return string 64 | */ 65 | public function getRootDirectory() 66 | { 67 | return __DIR__.'/../..'; 68 | } 69 | 70 | /** 71 | * Register the service provider. 72 | * 73 | * @return void 74 | */ 75 | public function register() 76 | { 77 | parent::register(); 78 | 79 | if (!$this->getConfig('enabled')) { 80 | return; 81 | } 82 | 83 | $this->registerFileSystem(); 84 | 85 | $this->registerCache(); 86 | 87 | $this->registerFirewall(); 88 | 89 | $this->registerDataRepository(); 90 | 91 | $this->registerMessageRepository(); 92 | 93 | $this->registerIpList(); 94 | 95 | $this->registerIpAddress(); 96 | 97 | $this->registerGeoIp(); 98 | 99 | $this->registerAttackBlocker(); 100 | 101 | $this->registerReportCommand(); 102 | 103 | $this->registerCountriesRepository(); 104 | 105 | if ($this->getConfig('use_database')) { 106 | $this->registerMigrations(); 107 | $this->registerWhitelistCommand(); 108 | $this->registerBlacklistCommand(); 109 | $this->registerRemoveCommand(); 110 | $this->registerClearCommand(); 111 | } 112 | 113 | $this->registerUpdateGeoIpCommand(); 114 | 115 | // $this->registerFlushCommand(); // TODO 116 | 117 | $this->registerMiddleware(); 118 | 119 | $this->registerEventListeners(); 120 | } 121 | 122 | /** 123 | * Register the attack blocker. 124 | */ 125 | private function registerAttackBlocker() 126 | { 127 | $this->app->singleton('firewall.attackBlocker', function () { 128 | return new AttackBlocker(); 129 | }); 130 | } 131 | 132 | /** 133 | * Register the countries repository. 134 | */ 135 | private function registerCountriesRepository() 136 | { 137 | $this->app->singleton('firewall.countries', function () { 138 | return new Countries(); 139 | }); 140 | } 141 | 142 | /** 143 | * Register the Blacklist Artisan command. 144 | * 145 | * @return void 146 | */ 147 | private function registerBlacklistCommand() 148 | { 149 | $this->app->singleton('firewall.blacklist.command', function () { 150 | return new BlacklistCommand(); 151 | }); 152 | 153 | $this->commands('firewall.blacklist.command'); 154 | } 155 | 156 | /** 157 | * Register the Cache driver used by Firewall. 158 | * 159 | * @return void 160 | */ 161 | private function registerCache() 162 | { 163 | $this->app->singleton('firewall.cache', function () { 164 | return new Cache(app('cache')); 165 | }); 166 | } 167 | 168 | /** 169 | * Register the List Artisan command. 170 | * 171 | * @return void 172 | */ 173 | private function registerClearCommand() 174 | { 175 | $this->app->singleton('firewall.clear.command', function () { 176 | return new ClearCommand(); 177 | }); 178 | 179 | $this->commands('firewall.clear.command'); 180 | } 181 | 182 | /** 183 | * Register the cache:clear Artisan command. 184 | * 185 | * @return void 186 | */ 187 | private function registerFlushCommand() 188 | { 189 | $this->app->singleton('firewall.flush.command', function () { 190 | return new Flush(); 191 | }); 192 | 193 | $this->commands('firewall.flush.command'); 194 | } 195 | 196 | /** 197 | * Register the Data Repository driver used by Firewall. 198 | * 199 | * @return void 200 | */ 201 | private function registerDataRepository() 202 | { 203 | $this->app->singleton('firewall.datarepository', function () { 204 | return new DataRepository(); 205 | }); 206 | } 207 | 208 | /** 209 | * Register event listeners. 210 | */ 211 | private function registerEventListeners() 212 | { 213 | Event::listen(AttackDetected::class, NotifyAdmins::class); 214 | } 215 | 216 | /** 217 | * Register the Filesystem driver used by Firewall. 218 | * 219 | * @return void 220 | */ 221 | private function registerFileSystem() 222 | { 223 | $this->app->singleton('firewall.filesystem', function () { 224 | return new Filesystem(); 225 | }); 226 | } 227 | 228 | /** 229 | * Takes all the components of Firewall and glues them 230 | * together to create Firewall. 231 | * 232 | * @return void 233 | */ 234 | private function registerFirewall() 235 | { 236 | $this->app->singleton('firewall', function ($app) { 237 | $app['firewall.loaded'] = true; 238 | 239 | $this->firewall = new Firewall( 240 | $app['firewall.config'], 241 | $app['firewall.datarepository'], 242 | $app['request'], 243 | $attackBlocker = $app['firewall.attackBlocker'], 244 | $app['firewall.messages'] 245 | ); 246 | 247 | $attackBlocker->setFirewall($this->firewall); 248 | 249 | return $this->firewall; 250 | }); 251 | } 252 | 253 | private function registerIpAddress() 254 | { 255 | $this->app->singleton('firewall.ipaddress', function () { 256 | return new IpAddress(); 257 | }); 258 | } 259 | 260 | /** 261 | * Register the ip list repository. 262 | */ 263 | private function registerIpList() 264 | { 265 | $this->app->singleton('firewall.iplist', function () { 266 | return new IpList($this->getFirewallModel()); 267 | }); 268 | } 269 | 270 | /** 271 | * Register the message repository. 272 | */ 273 | private function registerMessageRepository() 274 | { 275 | $this->app->singleton('firewall.messages', function () { 276 | return new Message(); 277 | }); 278 | } 279 | 280 | /** 281 | * Register blocking and unblocking Middleware. 282 | * 283 | * @return void 284 | */ 285 | private function registerMiddleware() 286 | { 287 | $this->app->singleton('firewall.middleware.blacklist', function () { 288 | return new FirewallBlacklist(new Blacklist()); 289 | }); 290 | 291 | $this->app->singleton('firewall.middleware.whitelist', function () { 292 | return new FirewallWhitelist(new Whitelist()); 293 | }); 294 | } 295 | 296 | private function registerMigrations() 297 | { 298 | $this->loadMigrationsFrom(__DIR__.'/../../migrations'); 299 | } 300 | 301 | private function registerGeoIp() 302 | { 303 | $this->app->singleton('firewall.geoip', function () { 304 | return new GeoIp($this->getConfig('geoip_database_path')); 305 | }); 306 | } 307 | 308 | /** 309 | * Register the List Artisan command. 310 | * 311 | * @return void 312 | */ 313 | private function registerRemoveCommand() 314 | { 315 | $this->app->singleton('firewall.remove.command', function () { 316 | return new RemoveCommand(); 317 | }); 318 | 319 | $this->commands('firewall.remove.command'); 320 | } 321 | 322 | /** 323 | * Register the List Artisan command. 324 | * 325 | * @return void 326 | */ 327 | private function registerReportCommand() 328 | { 329 | $this->app->singleton('firewall.list.command', function () { 330 | return new ReportCommand(); 331 | }); 332 | 333 | $this->commands('firewall.list.command'); 334 | } 335 | 336 | /** 337 | * Register the updategeoip command. 338 | */ 339 | private function registerUpdateGeoIpCommand() 340 | { 341 | $this->app->singleton('firewall.updategeoip.command', function () { 342 | return new UpdateGeoIpCommand(); 343 | }); 344 | 345 | $this->commands('firewall.updategeoip.command'); 346 | } 347 | 348 | /** 349 | * Register the Whitelist Artisan command. 350 | * 351 | * @return void 352 | */ 353 | private function registerWhitelistCommand() 354 | { 355 | $this->app->singleton('firewall.whitelist.command', function () { 356 | return new WhitelistCommand(); 357 | }); 358 | 359 | $this->commands('firewall.whitelist.command'); 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/firewall/6b3575a7b5977e223620d8281423e5175c875ea7/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | env('FIREWALL_ENABLED', true), 11 | 12 | /* 13 | * Whitelisted and blacklisted IP addresses, ranges, countries, files and/or files of files 14 | * 15 | * Examples of IP address, hosts, country codes and CIDRs 16 | * '127.0.0.1', 17 | * '192.168.17.0/24' 18 | * '127.0.0.1/255.255.255.255' 19 | * '10.0.0.1-10.0.0.255' 20 | * '172.17.*.*' 21 | * 'country:br' 22 | * 'host:google.com', 23 | * storage_path().DIRECTORY_SEPARATOR.'blacklisted.txt', // a file with IPs, one per line 24 | */ 25 | 26 | 'blacklist' => [ 27 | ], 28 | 29 | 'whitelist' => [ 30 | ], 31 | 32 | /* 33 | * Response action for blocked responses 34 | * 35 | */ 36 | 37 | 'responses' => [ 38 | 'blacklist' => [ 39 | 'code' => 403, // 200 = log && notify, but keep pages rendering 40 | 41 | 'message' => null, 42 | 43 | 'view' => null, 44 | 45 | 'redirect_to' => null, 46 | 47 | 'abort' => false, // return abort() instead of Response::make() - disabled by default 48 | ], 49 | 50 | 'whitelist' => [ 51 | 'code' => 403, // 200 = log && notify, but keep pages rendering 52 | 53 | 'message' => null, 54 | 55 | 'view' => null, 56 | 57 | 'redirect_to' => null, 58 | 59 | 'abort' => false, // return abort() instead of Response::make() - disabled by default 60 | ], 61 | ], 62 | 63 | /* 64 | * Do you wish to redirect non whitelisted accesses to an error page? 65 | * 66 | * You can use a route name (coming.soon) or url (/coming/soon); 67 | * 68 | */ 69 | 70 | 'redirect_non_whitelisted_to' => null, 71 | 72 | /* 73 | * How long should we keep IP addresses in cache? 74 | * 75 | * This is a general client IP addresses cache. When the user hits your system his/her IP address 76 | * is searched and cached for the desired time. Finding an IP address contained in a CIDR 77 | * range (172.17.0.0/24, for instance) can be a "slow", caching it improves performance. 78 | * 79 | */ 80 | 81 | 'cache_expire_time' => 60, // minutes 82 | 83 | /* 84 | * How long should we keep lists of IP addresses in cache? 85 | * 86 | * This is the list cache. Database lists can take some time to load and process, 87 | * caching it, if you are not making frequent changes to your lists, may improve firewall speed a lot. 88 | */ 89 | 90 | 'ip_list_cache_expire_time' => 0, // minutes - disabled by default 91 | 92 | /* 93 | * Send suspicious events to log? 94 | * 95 | */ 96 | 97 | 'enable_log' => true, 98 | 99 | 'log_stack' => null, 100 | 101 | /* 102 | * Search by range allow you to store ranges of addresses in 103 | * your black and whitelist: 104 | * 105 | * 192.168.17.0/24 or 106 | * 127.0.0.1/255.255.255.255 or 107 | * 10.0.0.1-10.0.0.255 or 108 | * 172.17.*.* 109 | * 110 | * Note that range searches may be slow and waste memory, this is why 111 | * it is disabled by default. 112 | * 113 | */ 114 | 115 | 'enable_range_search' => true, 116 | 117 | /* 118 | * Search by country range allow you to store country ids in your 119 | * your black and whitelist: 120 | * 121 | * php artisan firewall:whitelist country:us 122 | * php artisan firewall:blacklist country:cn 123 | * 124 | */ 125 | 126 | 'enable_country_search' => false, 127 | 128 | /* 129 | * Should Firewall use the database? 130 | */ 131 | 132 | 'use_database' => false, 133 | 134 | /* 135 | * Models 136 | * 137 | * When using the "eloquent" driver, we need to know which Eloquent models 138 | * should be used. 139 | * 140 | */ 141 | 142 | 'firewall_model' => 'PragmaRX\Firewall\Vendor\Laravel\Models\Firewall', 143 | 144 | /* 145 | * Session object binding in the IoC Container 146 | * 147 | * When blacklisting IPs for the current session, Firewall 148 | * will need to instantiate the session object. 149 | * 150 | */ 151 | 152 | 'session_binding' => 'session', 153 | 154 | /* 155 | * GeoIp2 database path. 156 | * 157 | * To get a fresh version of this file, use the command 158 | * 159 | * php artisan firewall:updategeoip 160 | * 161 | */ 162 | 163 | 'geoip_database_path' => __DIR__.'/geoip', //storage_path('geoip'), 164 | 165 | /* 166 | * Block suspicious attacks 167 | */ 168 | 169 | 'attack_blocker' => [ 170 | 171 | 'enabled' => [ 172 | 'ip' => true, 173 | 174 | 'country' => false, 175 | ], 176 | 177 | 'cache_key_prefix' => 'firewall-attack-blocker', 178 | 179 | 'allowed_frequency' => [ 180 | 181 | 'ip' => [ 182 | 'requests' => 50, 183 | 184 | 'seconds' => 1 * 60, // 1 minute 185 | ], 186 | 187 | 'country' => [ 188 | 'requests' => 3000, 189 | 190 | 'seconds' => 2 * 60, // 2 minutes 191 | ], 192 | 193 | ], 194 | 195 | 'action' => [ 196 | 197 | 'ip' => [ 198 | 'blacklist_unknown' => true, 199 | 200 | 'blacklist_whitelisted' => false, 201 | ], 202 | 203 | 'country' => [ 204 | 'blacklist_unknown' => false, 205 | 206 | 'blacklist_whitelisted' => false, 207 | ], 208 | 209 | ], 210 | 211 | 'response' => [ 212 | 'code' => 403, // 200 = log && notify, but keep pages rendering 213 | 214 | 'message' => null, 215 | 216 | 'view' => null, 217 | 218 | 'redirect_to' => null, 219 | 220 | 'abort' => false, // return abort() instead of Response::make() - disabled by default 221 | ], 222 | 223 | ], 224 | 225 | 'notifications' => [ 226 | 'enabled' => true, 227 | 228 | 'message' => [ 229 | 'title' => 'User agent', 230 | 231 | 'message' => "A possible attack on '%s' has been detected from %s", 232 | 233 | 'request_count' => [ 234 | 'title' => 'Request count', 235 | 236 | 'message' => 'Received %s requests in the last %s seconds. Timestamp of first request: %s', 237 | ], 238 | 239 | 'uri' => [ 240 | 'title' => 'First URI offended', 241 | ], 242 | 243 | 'blacklisted' => [ 244 | 'title' => 'Was it blacklisted?', 245 | ], 246 | 247 | 'user_agent' => [ 248 | 'title' => 'User agent', 249 | ], 250 | 251 | 'geolocation' => [ 252 | 'title' => 'Geolocation', 253 | 254 | 'field_latitude' => 'Latitude', 255 | 256 | 'field_longitude' => 'Longitude', 257 | 258 | 'field_country_code' => 'Country code', 259 | 260 | 'field_country_name' => 'Country name', 261 | 262 | 'field_city' => 'City', 263 | ], 264 | ], 265 | 266 | 'route' => '', 267 | 268 | 'from' => [ 269 | 'name' => 'Laravel Firewall', 270 | 271 | 'address' => 'firewall@mydomain.com', 272 | 273 | 'icon_emoji' => ':fire:', 274 | ], 275 | 276 | 'users' => [ 277 | 'model' => PragmaRX\Firewall\Vendor\Laravel\Models\User::class, 278 | 279 | 'emails' => [ 280 | 'admin@mydomain.com', 281 | ], 282 | ], 283 | 284 | 'channels' => [ 285 | 'slack' => [ 286 | 'enabled' => true, 287 | 'sender' => PragmaRX\Firewall\Notifications\Channels\Slack::class, 288 | ], 289 | 290 | 'mail' => [ 291 | 'enabled' => true, 292 | 'sender' => PragmaRX\Firewall\Notifications\Channels\Mail::class, 293 | ], 294 | ], 295 | ], 296 | ]; 297 | -------------------------------------------------------------------------------- /src/migrations/2014_02_01_311070_create_firewall_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->string('ip_address', 39)->unique()->index(); 20 | 21 | $table->boolean('whitelisted')->default(false); /// default is blacklist 22 | 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migration. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('firewall'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/ArtisanTest.php: -------------------------------------------------------------------------------- 1 | config('use_database', true); 13 | 14 | return parent::getPackageProviders($app); 15 | } 16 | 17 | public function testUpdateGeoip() 18 | { 19 | $this->assertEquals(0, Artisan::call('firewall:updategeoip')); 20 | } 21 | 22 | public function testBlacklist() 23 | { 24 | Artisan::call('firewall:blacklist', ['ip' => $ip = '127.0.0.1']); 25 | 26 | $this->assertTrue(Firewall::isBlacklisted($ip)); 27 | } 28 | 29 | public function testWhitelist() 30 | { 31 | Artisan::call('firewall:whitelist', ['ip' => $ip = '127.0.0.1']); 32 | 33 | $this->assertTrue(Firewall::isWhitelisted($ip)); 34 | } 35 | 36 | public function testRemove() 37 | { 38 | Artisan::call('firewall:whitelist', ['ip' => $ip1 = '127.0.0.1']); 39 | 40 | Artisan::call('firewall:blacklist', ['ip' => $ip2 = '127.0.0.2']); 41 | 42 | Artisan::call('firewall:remove', ['ip' => $ip1 = '127.0.0.1']); 43 | 44 | $this->assertFalse(Firewall::isWhitelisted($ip1)); 45 | 46 | $this->assertTrue(Firewall::isBlacklisted($ip2)); 47 | 48 | Artisan::call('firewall:clear', ['--force' => true]); 49 | 50 | $this->assertFalse(Firewall::isWhitelisted($ip2)); 51 | } 52 | 53 | public function testReport() 54 | { 55 | $this->assertEquals(0, Artisan::call('firewall:list')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/AttackBlockerTest.php: -------------------------------------------------------------------------------- 1 | config('notifications.enabled', true); 15 | 16 | $this->config('attack_blocker.allowed_frequency.ip.requests', 2); 17 | 18 | $this->config('attack_blocker.enabled.ip', true); 19 | 20 | $this->config('attack_blocker.enabled.country', true); 21 | 22 | $this->config('attack_blocker.allowed_frequency.country.requests', 3); 23 | 24 | config()->set('services.slack.webhook_url', '12345'); 25 | } 26 | 27 | public function test_attack() 28 | { 29 | $this->config('notifications.enabled', false); 30 | 31 | $this->config('attack_blocker.allowed_frequency.ip.requests', 2); 32 | 33 | $this->assertFalse(Firewall::isBeingAttacked('172.17.0.1')); 34 | $this->assertFalse(Firewall::isBeingAttacked('172.17.0.1')); 35 | 36 | $this->assertNull(Firewall::responseToAttack()); 37 | 38 | $this->assertTrue(Firewall::isBeingAttacked('172.17.0.1')); 39 | 40 | $this->assertInstanceOf(Response::class, Firewall::responseToAttack()); 41 | } 42 | 43 | public function test_send_notification_ip_attack() 44 | { 45 | $this->assertFalse(Firewall::isBeingAttacked('8.8.8.8')); 46 | $this->assertFalse(Firewall::isBeingAttacked('8.8.8.8')); 47 | $this->assertTrue(Firewall::isBeingAttacked('8.8.8.8')); 48 | } 49 | 50 | public function test_send_notification_country_attack() 51 | { 52 | Firewall::isBeingAttacked('8.8.8.1'); 53 | Firewall::isBeingAttacked('8.8.8.2'); 54 | Firewall::isBeingAttacked('8.8.8.3'); 55 | Firewall::isBeingAttacked('8.8.8.4'); 56 | Firewall::isBeingAttacked('8.8.8.5'); 57 | Firewall::isBeingAttacked('8.8.8.6'); 58 | Firewall::isBeingAttacked('8.8.8.7'); 59 | 60 | $this->assertTrue(Firewall::isBeingAttacked('8.8.8.8')); 61 | 62 | $this->assertFalse(Firewall::isBlacklisted('8.8.8.8')); 63 | } 64 | 65 | public function test_send_notification_country_block_attack_and_blacklist() 66 | { 67 | $this->config('attack_blocker.action.blacklist_unknown', true); 68 | 69 | $this->config('attack_blocker.action.blacklist_whitelisted', true); 70 | 71 | Firewall::isBeingAttacked('8.8.8.1'); 72 | Firewall::isBeingAttacked('8.8.8.2'); 73 | Firewall::isBeingAttacked('8.8.8.3'); 74 | Firewall::isBeingAttacked('8.8.8.4'); 75 | Firewall::isBeingAttacked('8.8.8.5'); 76 | Firewall::isBeingAttacked('8.8.8.6'); 77 | Firewall::isBeingAttacked('8.8.8.7'); 78 | 79 | $this->assertTrue(Firewall::isBeingAttacked('8.8.8.8')); 80 | 81 | $this->assertFalse(Firewall::isBlacklisted('country:us')); 82 | } 83 | 84 | public function test_send_notification_no_country_ip_attack() 85 | { 86 | Firewall::isBeingAttacked('127.0.0.1'); 87 | Firewall::isBeingAttacked('127.0.0.2'); 88 | Firewall::isBeingAttacked('127.0.0.3'); 89 | Firewall::isBeingAttacked('127.0.0.4'); 90 | Firewall::isBeingAttacked('127.0.0.5'); 91 | Firewall::isBeingAttacked('127.0.0.6'); 92 | Firewall::isBeingAttacked('127.0.0.7'); 93 | 94 | $this->assertFalse(Firewall::isBeingAttacked('127.0.0.8')); 95 | } 96 | 97 | public function test_blocker_disabled() 98 | { 99 | $this->config('attack_blocker.enabled.ip', false); 100 | 101 | $this->config('attack_blocker.enabled.country', false); 102 | 103 | Firewall::isBeingAttacked('8.8.8.1'); 104 | Firewall::isBeingAttacked('8.8.8.2'); 105 | Firewall::isBeingAttacked('8.8.8.3'); 106 | Firewall::isBeingAttacked('8.8.8.4'); 107 | Firewall::isBeingAttacked('8.8.8.5'); 108 | Firewall::isBeingAttacked('8.8.8.6'); 109 | Firewall::isBeingAttacked('8.8.8.7'); 110 | 111 | $this->assertFalse(Firewall::isBeingAttacked('8.8.8.8')); 112 | } 113 | 114 | public function test_expiration() 115 | { 116 | $this->config('attack_blocker.allowed_frequency.ip.requests', 2); 117 | $this->config('attack_blocker.allowed_frequency.ip.seconds', 2); 118 | 119 | $this->assertFalse(Firewall::isBeingAttacked('172.17.0.1')); 120 | $this->assertFalse(Firewall::isBeingAttacked('172.17.0.1')); 121 | 122 | $this->assertNull(Firewall::responseToAttack()); 123 | 124 | $this->assertTrue(Firewall::isBeingAttacked('172.17.0.1')); 125 | 126 | sleep(5); 127 | 128 | $this->assertFalse(Firewall::isBeingAttacked('172.17.0.1')); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/CacheTest.php: -------------------------------------------------------------------------------- 1 | cache = app('firewall.cache'); 23 | } 24 | 25 | public function testCacheHoldsCachedIp() 26 | { 27 | $this->firewall->blacklist($ip = '172.17.0.1'); 28 | 29 | $this->firewall->find($ip); 30 | 31 | $this->assertTrue($this->cache->has($ip)); 32 | } 33 | 34 | public function testCachePut() 35 | { 36 | $key = '1234'; 37 | 38 | foreach (range(1, 100) as $counter) { 39 | $this->cache->put($key, $this->cache->get($key, 0) + 1, 10); 40 | } 41 | 42 | $this->assertEquals(100, $this->cache->get($key)); 43 | } 44 | 45 | public function testDisabledCache() 46 | { 47 | $this->cache->put($key = '1234', $this->cache->get($key, 0) + 1, 10); 48 | $this->cache->put($key = '1234', $this->cache->get($key, 0) + 1, 10); 49 | 50 | $this->assertEquals(2, $this->cache->get($key)); 51 | 52 | $this->assertTrue($this->cache->has($key)); 53 | 54 | $this->config('cache_expire_time', false); 55 | 56 | $this->assertFalse($this->cache->has($key)); 57 | 58 | $this->assertNull($this->cache->get($key)); 59 | } 60 | 61 | public function testListCache() 62 | { 63 | $this->firewall->blacklist($ip = '172.17.0.1'); 64 | 65 | $this->assertTrue($this->firewall->isBlacklisted($ip)); 66 | 67 | $this->firewall->clear(); 68 | 69 | $this->assertFalse($this->firewall->isBlacklisted($ip)); 70 | 71 | $this->config('ip_list_cache_expire_time', 1); 72 | 73 | $this->firewall->blacklist($ip); 74 | 75 | $this->assertTrue($this->firewall->isBlacklisted($ip)); 76 | 77 | $this->firewall->clear(); 78 | 79 | $this->assertTrue($this->firewall->isBlacklisted($ip)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/FilterTest.php: -------------------------------------------------------------------------------- 1 | filter(); 27 | 28 | $this->assertNull($response); 29 | } 30 | 31 | public function testRedirectWhitelisted() 32 | { 33 | $this->config('responses.whitelist.redirect_to', 'http://whatever.com'); 34 | 35 | $response = (new Whitelist())->filter(); 36 | 37 | $this->assertInstanceOf(RedirectResponse::class, $response); 38 | } 39 | 40 | public function testRedirectWhitelistedToRouteName() 41 | { 42 | Route::get('/redirected', ['as' => 'redirected', function () { 43 | return 'whatever'; 44 | }]); 45 | 46 | $this->config('responses.whitelist.redirect_to', 'redirected'); 47 | 48 | $response = (new Whitelist())->filter(); 49 | 50 | $this->assertInstanceOf(RedirectResponse::class, $response); 51 | 52 | $this->assertTrue(str_contains($response->getContent(), '/redirected')); 53 | } 54 | 55 | public function testRedirectWhitelistedToView() 56 | { 57 | $this->expectException(InvalidArgumentException::class); 58 | 59 | $this->config('responses.whitelist.view', 'redirected'); 60 | 61 | $response = (new Whitelist())->filter(); 62 | } 63 | 64 | public function testWhitelistIgnoreListing() 65 | { 66 | $this->config('responses.whitelist.code', 200); 67 | 68 | $response = (new Whitelist())->filter(); 69 | 70 | $this->assertNull($response); 71 | } 72 | 73 | public function testRedirectWhitelistedToAbort() 74 | { 75 | $this->expectException(HttpException::class); 76 | 77 | $this->config('responses.whitelist.abort', true); 78 | 79 | $response = (new Whitelist())->filter(); 80 | } 81 | 82 | public function testNotWhitelisted() 83 | { 84 | $response = (new Whitelist())->filter(); 85 | 86 | $this->assertEquals(403, $response->getStatusCode()); 87 | } 88 | 89 | public function testBlacklist() 90 | { 91 | Firewall::blacklist('127.0.0.1'); 92 | 93 | $response = (new Blacklist())->filter(); 94 | 95 | $this->assertEquals(403, $response->getStatusCode()); 96 | } 97 | 98 | public function testNotBlacklisted() 99 | { 100 | $response = (new Blacklist())->filter(); 101 | 102 | $this->assertNull($response); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/FirewallArrayTest.php: -------------------------------------------------------------------------------- 1 | config('use_database', false); 19 | 20 | return [ 21 | FirewallServiceProvider::class, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/FirewallDatabaseTest.php: -------------------------------------------------------------------------------- 1 | config('use_database', true); 12 | 13 | return [ 14 | FirewallServiceProvider::class, 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/FirewallTestCase.php: -------------------------------------------------------------------------------- 1 | assertFalse(Firewall::blacklist('127.0.0.256')); 13 | } 14 | 15 | public function testFirewallIsInstantiable() 16 | { 17 | $false = Firewall::isBlackListed('impossible'); 18 | 19 | $this->assertFalse($false); 20 | } 21 | 22 | public function testDisableFirewall() 23 | { 24 | $this->config('enabled', false); 25 | 26 | Firewall::blacklist($ip = '172.17.0.100'); 27 | 28 | $this->assertTrue(Firewall::isBlackListed($ip)); 29 | } 30 | 31 | public function testCanBlacklistIps() 32 | { 33 | Firewall::blacklist($ip = '172.17.0.100'); 34 | 35 | $this->assertTrue(Firewall::isBlackListed($ip)); 36 | 37 | $this->assertFalse(Firewall::isWhitelisted($ip)); 38 | 39 | $this->assertFalse(Firewall::isBlackListed('172.17.0.101')); 40 | 41 | $this->assertFalse(Firewall::isWhitelisted('172.17.0.101')); 42 | 43 | $this->assertEquals(Firewall::whichList($ip), 'blacklist'); 44 | } 45 | 46 | public function testCanWhitelistIps() 47 | { 48 | Firewall::whitelist($ip = '172.17.0.101'); 49 | 50 | $this->assertFalse(Firewall::isBlackListed($ip)); 51 | 52 | $this->assertTrue(Firewall::isWhitelisted($ip)); 53 | 54 | $this->assertFalse(Firewall::isWhitelisted('172.17.0.102')); 55 | 56 | $this->assertFalse(Firewall::isBlacklisted('172.17.0.102')); 57 | 58 | $this->assertEquals(Firewall::whichList($ip), 'whitelist'); 59 | } 60 | 61 | public function testCanListCidrs() 62 | { 63 | Firewall::whitelist('172.17.0.0/24'); 64 | 65 | $this->assertTrue(Firewall::isWhitelisted($ip = '172.17.0.1')); 66 | 67 | $this->assertTrue(Firewall::isWhitelisted('172.17.0.100')); 68 | 69 | $this->assertTrue(Firewall::isWhitelisted('172.17.0.255')); 70 | 71 | $this->assertFalse(Firewall::isBlacklisted($ip)); 72 | } 73 | 74 | public function testRefusesWrongIpAddresses() 75 | { 76 | $false = Firewall::whitelist('172.17.0.256'); 77 | 78 | $this->assertFalse($false); 79 | 80 | $this->assertEquals(Firewall::getMessages()->toArray(), ['172.17.0.256 is not a valid IP address']); 81 | 82 | $this->assertFalse($false); 83 | } 84 | 85 | public function testForceToAList() 86 | { 87 | Firewall::whitelist($ip = '172.17.0.1'); 88 | 89 | $this->assertTrue(Firewall::isWhitelisted($ip)); 90 | 91 | Firewall::blacklist($ip = '172.17.0.1'); 92 | 93 | $this->assertTrue(Firewall::isWhitelisted($ip)); 94 | 95 | Firewall::blacklist($ip = '172.17.0.1', true); // force 96 | 97 | $this->assertFalse(Firewall::isWhitelisted($ip)); 98 | 99 | $this->assertTrue(Firewall::isBlacklisted($ip)); 100 | } 101 | 102 | public function testFindIp() 103 | { 104 | Firewall::whitelist($ip = '172.17.0.1'); 105 | 106 | $model = Firewall::find($ip); 107 | 108 | $this->assertInstanceOf(\PragmaRX\Firewall\Vendor\Laravel\Models\Firewall::class, $model); 109 | 110 | $this->assertNull(Firewall::find('impossible')); 111 | } 112 | 113 | public function testGetAllIps() 114 | { 115 | Firewall::whitelist('172.17.0.1'); 116 | Firewall::whitelist('172.17.0.2'); 117 | Firewall::whitelist('172.17.0.3'); 118 | 119 | $this->assertCount(3, Firewall::all()); 120 | 121 | Firewall::remove('172.17.0.3'); 122 | 123 | $this->assertCount(2, Firewall::all()); 124 | 125 | Firewall::clear('172.17.0.3'); 126 | 127 | $this->assertCount(0, Firewall::all()); 128 | } 129 | 130 | public function testBlockAccess() 131 | { 132 | $this->assertInstanceOf(Response::class, Firewall::blockAccess()); 133 | } 134 | 135 | public function testLog() 136 | { 137 | $this->assertNull(Firewall::log('whatever')); 138 | } 139 | 140 | public function testIpValidation() 141 | { 142 | $this->assertTrue(Firewall::ipIsValid('172.17.0.100')); 143 | 144 | $this->assertFalse(Firewall::ipIsValid('172.17.0.256')); 145 | } 146 | 147 | public function testReport() 148 | { 149 | Firewall::whitelist('172.17.0.1'); 150 | Firewall::blacklist('172.17.0.2'); 151 | 152 | $expected = [ 153 | [ 154 | 'ip_address' => '172.17.0.1', 155 | 'whitelisted' => 1, 156 | ], 157 | [ 158 | 'ip_address' => '172.17.0.2', 159 | 'whitelisted' => 0, 160 | ], 161 | ]; 162 | 163 | $report = Firewall::report()->map(function ($item, $key) { 164 | return collect($item->toArray())->only(['ip_address', 'whitelisted']); 165 | })->toArray(); 166 | 167 | $this->assertEquals($expected, $report); 168 | } 169 | 170 | public function testDoNotReinsertExistent() 171 | { 172 | Firewall::blacklist('172.17.0.1'); 173 | 174 | Firewall::blacklist('172.17.0.1'); 175 | 176 | $this->assertTrue(Firewall::isBlacklisted('172.17.0.1')); 177 | } 178 | 179 | public function testDoNotRemoveNonExistent() 180 | { 181 | Firewall::remove('172.17.0.1'); 182 | 183 | Firewall::blacklist('172.17.0.1'); 184 | 185 | Firewall::remove('172.17.0.1'); 186 | 187 | Firewall::remove('172.17.0.1'); 188 | 189 | $this->assertFalse(Firewall::isWhitelisted('172.17.0.1')); 190 | 191 | $this->assertFalse(Firewall::isBlacklisted('172.17.0.1')); 192 | } 193 | 194 | public function testSetip() 195 | { 196 | $this->assertEquals('127.0.0.1', Firewall::getIp()); 197 | 198 | Firewall::setIp($ip = '127.0.0.2'); 199 | 200 | $this->assertEquals($ip, Firewall::getIp()); 201 | 202 | Firewall::setIp($ip = '127.0.0.1'); 203 | 204 | Firewall::blacklist('127.0.0.1'); 205 | 206 | $this->assertTrue(Firewall::isBlacklisted()); 207 | } 208 | 209 | public function testListByHost() 210 | { 211 | Firewall::blacklist('host:corinna.antoniocarlosribeiro.com'); 212 | 213 | $this->assertTrue(Firewall::isBlacklisted('67.205.143.231')); 214 | } 215 | 216 | public function testWildcard() 217 | { 218 | Firewall::whitelist('172.17.*.*'); 219 | 220 | $this->assertTrue(Firewall::isWhitelisted($ip = '172.17.0.100')); 221 | 222 | $this->assertTrue(Firewall::isWhitelisted($ip = '172.17.1.101')); 223 | 224 | $this->assertTrue(Firewall::isWhitelisted($ip = '172.17.2.102')); 225 | 226 | $this->assertTrue(Firewall::isWhitelisted($ip = '172.17.255.255')); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /tests/GeoIpTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('us', Firewall::getCountryFromIp('8.8.8.8')); 21 | 22 | $this->assertEquals('br', Firewall::getCountryFromIp('200.222.0.24')); 23 | } 24 | 25 | public function testBlockPerCountry() 26 | { 27 | Firewall::blacklist('country:us'); 28 | 29 | $this->assertTrue(Firewall::isBlacklisted('8.8.8.8')); 30 | 31 | $this->assertFalse(Firewall::isWhitelisted('8.8.8.8')); 32 | } 33 | 34 | public function testMakeCountry() 35 | { 36 | $this->assertEquals('country:br', Firewall::makeCountryFromString('br')); 37 | 38 | $this->assertEquals('country:br', Firewall::makeCountryFromString('country:br')); 39 | 40 | $this->assertEquals('country:br', Firewall::makeCountryFromString('200.222.0.21')); 41 | } 42 | 43 | public function testCountryIpListing() 44 | { 45 | Firewall::blacklist('8.8.8.7'); 46 | Firewall::blacklist('8.8.8.8'); 47 | Firewall::blacklist('8.8.8.9'); 48 | 49 | Firewall::blacklist('200.222.0.21'); 50 | Firewall::blacklist('200.222.0.22'); 51 | 52 | $this->assertCount(2, Firewall::allByCountry('br')); 53 | 54 | $this->assertCount(3, Firewall::allByCountry('us')); 55 | } 56 | 57 | public function testCountryIsValid() 58 | { 59 | $this->assertTrue(Firewall::validCountry('country:us')); 60 | 61 | $this->assertTrue(Firewall::validCountry('country:br')); 62 | 63 | $this->assertFalse(Firewall::validCountry('country:xx')); 64 | } 65 | 66 | public function testCountryCidr() 67 | { 68 | Firewall::blacklist('country:us'); 69 | 70 | $this->assertTrue(Firewall::isBlacklisted('8.8.8.0/24')); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/MiddlewareTest.php: -------------------------------------------------------------------------------- 1 | request = new Request(); 28 | 29 | $this->blockAttacks = (new BlockAttacks()); 30 | 31 | $this->blacklist = (new FirewallBlacklist(new Blacklist())); 32 | 33 | $this->whitelist = (new FirewallWhitelist(new Whitelist())); 34 | 35 | $this->config('attack_blocker.enabled.ip', true); 36 | 37 | $this->config('attack_blocker.allowed_frequency.ip.requests', 2); 38 | } 39 | 40 | public function testBlacklist() 41 | { 42 | $this->blacklist->filter($this->request); 43 | 44 | $this->assertEquals('next', $this->blacklist->handle($this->request, $this->getNextClosure())); 45 | 46 | Firewall::blacklist('127.0.0.1'); 47 | 48 | $this->assertInstanceOf(Response::class, $this->blacklist->handle($this->request, $this->getNextClosure())); 49 | } 50 | 51 | public function testWhitelist() 52 | { 53 | $this->whitelist->filter($this->request); 54 | 55 | $this->assertInstanceOf(Response::class, $this->whitelist->handle($this->request, $this->getNextClosure())); 56 | 57 | Firewall::whitelist('127.0.0.1'); 58 | 59 | $this->assertEquals('next', $this->whitelist->handle($this->request, $this->getNextClosure())); 60 | } 61 | 62 | public function testBlockAttack() 63 | { 64 | $this->assertFalse(Firewall::isBeingAttacked('127.0.0.1')); 65 | 66 | $this->assertEquals('next', $this->blockAttacks->handle($this->request, $this->getNextClosure())); 67 | 68 | $this->assertTrue(Firewall::isBeingAttacked('127.0.0.1')); 69 | 70 | $this->assertInstanceOf(Response::class, $this->blockAttacks->handle($this->request, $this->getNextClosure())); 71 | } 72 | 73 | public function testRegister() 74 | { 75 | $this->assertInstanceOf(FirewallBlacklist::class, app('firewall.middleware.blacklist')); 76 | 77 | $this->assertInstanceOf(FirewallWhitelist::class, app('firewall.middleware.whitelist')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/ModelTest.php: -------------------------------------------------------------------------------- 1 | set('firewall.firewall_model', null); 13 | 14 | return parent::getPackageProviders($app); 15 | } 16 | 17 | public function testModelNotAvailable() 18 | { 19 | $this->expectException(ConfigurationOptionNotAvailable::class); 20 | 21 | Firewall::blacklist($ip = '127.0.0.1'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/ReadIpsFromFilesTest.php: -------------------------------------------------------------------------------- 1 | getFilename(), $lines); 30 | 31 | $this->config('blacklist', $this->getFilename()); 32 | } 33 | 34 | public function testReadFile() 35 | { 36 | Firewall::blacklist($this->getFilename()); 37 | 38 | $this->assertTrue(Firewall::isBlackListed('10.0.0.9')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/ServiceDisabledTest.php: -------------------------------------------------------------------------------- 1 | set('firewall.enabled', false); 13 | 14 | return [ 15 | FirewallServiceProvider::class, 16 | ]; 17 | } 18 | 19 | public function testFirewallIsDisabled() 20 | { 21 | $this->expectException(\Exception::class); 22 | 23 | Firewall::blacklist($ip = '172.17.0.100'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | config->set("firewall.{$key}", $value); 16 | } 17 | 18 | app()->config->get("firewall.{$key}"); 19 | } 20 | 21 | private function configureDatabase() 22 | { 23 | if (!file_exists($path = __DIR__.'/databases')) { 24 | mkdir($path); 25 | } 26 | 27 | touch($this->database = tempnam($path, 'database.sqlite.')); 28 | 29 | app()->config->set( 30 | 'database.connections.testbench', 31 | [ 32 | 'driver' => 'sqlite', 33 | 'database' => $this->database, 34 | 'prefix' => '', 35 | ] 36 | ); 37 | } 38 | 39 | private function deleteDatabase() 40 | { 41 | @unlink($this->database); 42 | } 43 | 44 | protected function setUp(): void 45 | { 46 | parent::setUp(); 47 | 48 | if (config('firewall.enabled')) { 49 | $this->firewall = app('firewall'); 50 | 51 | app('firewall.cache')->flush(); 52 | } 53 | 54 | $this->configureDatabase(); 55 | 56 | $this->artisan('migrate:refresh', ['--database' => 'testbench']); 57 | } 58 | 59 | protected function tearDown(): void 60 | { 61 | parent::tearDown(); 62 | 63 | $this->deleteDatabase(); 64 | } 65 | 66 | protected function getPackageProviders($app) 67 | { 68 | $app['config']->set('firewall.enabled', true); 69 | 70 | $app['config']->set('firewall.geoip_database_path', __DIR__.'/geoipdb'); 71 | 72 | $app['config']->set('firewall.enable_country_search', true); 73 | 74 | $app['config']->set('firewall.cache_expire_time', 10); 75 | 76 | return [ 77 | FirewallServiceProvider::class, 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |