Skip to content

Commit

Permalink
Block by country middleware and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mchev committed Jan 7, 2024
1 parent 7a23141 commit ccac907
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 38 deletions.
75 changes: 72 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,39 @@ Banhammer for Laravel offers a very simple way to ban any Model by ID and by IP.

Banned models can have an expiration date and will be automatically unbanned using the Scheduler.

## Table of Contents
1. [Introduction](#banhammer-a-model-and-ip-ban-package-for-laravel)
- [Badges](#badges)
2. [Version Compatibility](#version-compatibility)
3. [Installation](#installation)
- [Composer Dependencies](#composer-dependencies)
4. [Upgrading To 2.0 from 1.x](#upgrading-to-20-from-1x)
- [Composer Dependencies](#composer-dependencies-1)
- [Configuration Changes](#configuration-changes)
5. [Usage](#usage)
- [Making a Model Bannable](#to-make-a-model-bannable-add-the-mchevbanhammertraitsbannable-trait-to-the-model)
- [Ban / Unban](#ban--unban)
- [IP](#ip)
- [Metas](#metas)
- [Blocking Access from Specific Countries](#blocking-access-from-specific-countries)
- [Middleware](#middleware)
- [Scheduler](#scheduler)
- [Events](#events)
- [Miscellaneous](#misc)
6. [Testing](#testing)
7. [Changelog](#changelog)
8. [Roadmap / Todo](#roadmap--todo)
9. [Contributing](#contributing)
10. [Security Vulnerabilities](#security-vulnerabilities)
11. [Credits](#credits)
12. [License](#license)

## Version Compatibility

Laravel | Banhammer
:---------------------|:----------
^9.0 | 1.x.x
^10.0 | 1.x.x
^9.0 | 1.x, 2.x
^10.0 | 1.x, 2.x

## Installation

Expand All @@ -36,7 +63,49 @@ You can publish the config file with:
php artisan vendor:publish --tag="banhammer-config"
```

It is possible to define the table name and the fallback_url in the `config/banhammer.php` file.
It is possible to define the table name and the fallback_url in the `config/ban.php` file.

## Upgrading To 2.0 from 1.x

### Composer Dependencies

To upgrade to Banhammer version 2.0, please follow these steps:

1. Update the package version in your application's composer.json file:

```json
"require": {
"mchev/banhammer": "^2.0"
}
```

Run the following command in your terminal:

```bash
composer update mchev/banhammer
```

### Configuration Changes

1. Check the config/ban.php file and make the following corrections:
```diff
- 'message' => 'You have been banned.',
+ 'messages' => [
+ 'user' => 'Your account has been banned.',
+ 'ip' => 'Access from your IP address is restricted.',
+ 'country' => 'Access from your country is restricted.',
+ ],
+
+ 'block_by_country' => env('BANHAMMER_BLOCK_BY_COUNTRY', false),
+
+ 'blocked_countries' => [], // Examples: ['US', 'CA', 'GB']
```

- Update the 'message' key to 'messages' and provide specific messages for user, IP, and country bans.
- Introduce the 'block_by_country' configuration option to enable or disable country-based blocking.
- Configure the list of 'blocked_countries' to specify the countries from which access is restricted.

These changes ensure compatibility with Banhammer version 2.0 and allow for more granular control over ban messages and country-based restrictions. After making these adjustments, your application should seamlessly migrate to the latest version of Banhammer.

## Usage

Expand Down
15 changes: 14 additions & 1 deletion config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@
|
*/

'blocked_countries' => [], // Examples: ['US', 'CA', 'GB']
'blocked_countries' => [], // Examples: ['US', 'CA', 'GB', 'FR', 'ES', 'DE']

/*
|--------------------------------------------------------------------------
| Cache Duration for IP Geolocation
|--------------------------------------------------------------------------
|
| This configuration option determines the duration, in minutes, for which
| the IP geolocation data will be stored in the cache. This helps prevent
| excessive requests and enables the middleware to efficiently determine
| whether to block a request based on the user's country.
|
*/
'cache_duration' => 120, // Duration in minutes

];
52 changes: 26 additions & 26 deletions src/Middleware/BlockByCountry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Mchev\Banhammer\Exceptions\BanhammerException;
use Mchev\Banhammer\Services\IpApiService;

class BlockByCountry
{
protected IpApiService $ipApiService;

public function __construct(IpApiService $ipApiService)
public function __construct(protected IpApiService $ipApiService)
{
$this->ipApiService = $ipApiService;
//
}

public function handle(Request $request, Closure $next)
Expand All @@ -25,37 +24,38 @@ public function handle(Request $request, Closure $next)
$ip = $request->ip();

if (! is_null($ip)) {
try {
// Get geolocation data using the IpApiService
$geolocationData = $this->ipApiService->getGeolocationData($ip);

if ($geolocationData['status'] === 'fail') {
Log::notice('Banhammer country check failure: '.$message, [
$cacheKey = 'banhammer_'.$ip;
$cachedResult = Cache::get($cacheKey);

if ($cachedResult === null) {
try {
$geolocationData = $this->ipApiService->getGeolocationData($ip);

if ($geolocationData['status'] === 'fail') {
Log::notice('Banhammer country check failure: '.$geolocationData['message'], [
'ip' => $ip,
]);
Cache::put($cacheKey, 'allowed', now()->addMinutes(config('ban.cache_duration')));
} else {
if (in_array($geolocationData['countryCode'], $blockedCountries)) {
Cache::put($cacheKey, 'blocked', now()->addMinutes(config('ban.cache_duration')));
throw new BanhammerException(config('ban.messages.country'));
}
}

} catch (\Exception $e) {
Log::debug('Banhammer Exception: '.$e->getMessage(), [
'ip' => $ip,
'country' => $geolocationData['countryCode'] ?? null,
]);
}

if ($this->isCountryBlocked($geolocationData, $blockedCountries)) {
throw new BanhammerException(config('ban.messages.country'));
}
} catch (\Exception $e) {
Log::debug('Banhammer Exception: '.$e->getMessage(), [
'ip' => $ip,
'country' => $geolocationData['countryCode'] ?? null,
]);

// Rethrow the exception to ensure the ban is enforced
} elseif ($cachedResult === 'blocked') {
throw new BanhammerException(config('ban.messages.country'));
}
}
}

return $next($request);
}

protected function isCountryBlocked(array $geolocationData, array $blockedCountries): bool
{
return isset($geolocationData['countryCode']) &&
$this->ipApiService->isCountryBlocked($geolocationData['countryCode'], $blockedCountries);
}
}
8 changes: 0 additions & 8 deletions src/Services/IpApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,4 @@ public function getGeolocationData(string $ip): ?array
return null;
}
}

/**
* Check if the country code is in the list of blocked countries.
*/
public function isCountryBlocked(string $countryCode, array $blockedCountries): bool
{
return in_array($countryCode, $blockedCountries);
}
}
147 changes: 147 additions & 0 deletions tests/Unit/BlockByCountryMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace Mchev\Banhammer\Tests\Unit;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Mchev\Banhammer\Exceptions\BanhammerException;
use Mchev\Banhammer\Middleware\BlockByCountry;
use Mchev\Banhammer\Services\IpApiService;
use Mockery;
use Mchev\Banhammer\Tests\TestCase;

class BlockByCountryMiddlewareTest extends TestCase
{
/** @var BlockByCountry */
private $middleware;

/** @var IpApiService|\Mockery\MockInterface */
private $ipApiService;

protected function setUp(): void
{
parent::setUp();

// Create a mock for IpApiService
$this->ipApiService = Mockery::mock(IpApiService::class);

// Create an instance of the middleware with the mock service
$this->middleware = new BlockByCountry($this->ipApiService);
}

protected function tearDown(): void
{
parent::tearDown();

// Close Mockery expectations
Mockery::close();
}

/** @test */
public function it_allows_request_from_non_blocked_country()
{
// Given
$ip = '100.42.30.255'; // IP from a country not in the blocked list

// Setting configuration using Config facade
config(['ban.block_by_country' => true]);
config(['ban.blocked_countries' => ['FR', 'US']]);

// Set up the mock behavior
$this->ipApiService
->shouldReceive('getGeolocationData')
->andReturn(['status' => 'success', 'countryCode' => 'CA']);

// Create a dummy request
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => $ip]);

// When
$response = $this->middleware->handle($request, function ($req) {
return response()->json(['status' => 'success']);
});

// Then
$this->assertEquals('success', json_decode($response->getContent(), true)['status']);
}

/** @test */
public function it_blocks_request_from_blocked_country()
{
// Given
$ip = '100.42.30.255'; // IP from a blocked country

// Setting configuration using Config facade
config(['ban.block_by_country' => true]);
config(['ban.blocked_countries' => ['FR', 'US']]);

// Set up the mock behavior
$this->ipApiService
->shouldReceive('getGeolocationData')
->andReturn(['status' => 'success', 'countryCode' => 'US']);

// Create a dummy request
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => $ip]);

// When
$this->expectException(BanhammerException::class);
$this->middleware->handle($request, function ($req) {
// Dummy callback, as we are not actually calling the next middleware or endpoint
});
}

/** @test */
public function it_allows_request_when_country_check_fails()
{
// Given
$ip = '100.42.30.255'; // IP from a country not in the blocked list

// Setting configuration using Config facade
config(['ban.block_by_country' => true]);
config(['ban.blocked_countries' => ['FR', 'US']]);

// Set up the mock behavior
$this->ipApiService
->shouldReceive('getGeolocationData')
->andReturn(['status' => 'fail', 'message' => 'Failed to get geolocation']);

// Create a dummy request
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => $ip]);

// When
$response = $this->middleware->handle($request, function ($req) {
return response()->json(['status' => 'success']);
});

// Then
$this->assertEquals('success', json_decode($response->getContent(), true)['status']);
}

/** @test */
public function it_allows_request_when_cache_is_present()
{
// Given
$ip = '100.42.30.255'; // IP from a country not in the blocked list

// Setting configuration using Config facade
config(['ban.block_by_country' => true]);
config(['ban.blocked_countries' => ['FR', 'US']]);

// Set up the mock behavior
$this->ipApiService
->shouldReceive('getGeolocationData')
->andReturn(['status' => 'success', 'countryCode' => 'CA']);

// Set up cache
Cache::put('banhammer_'.$ip, 'blocked', now()->addMinutes(config('ban.cache_duration')));

// Create a dummy request
$request = Request::create('/', 'GET', [], [], [], ['REMOTE_ADDR' => $ip]);

// When
$this->expectException(BanhammerException::class);
$this->middleware->handle($request, function ($req) {
// Dummy callback, as we are not actually calling the next middleware or endpoint
});
}
}

0 comments on commit ccac907

Please sign in to comment.