Init
This commit is contained in:
23 files changed
+1216
No files matched your search
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Contracts\Http\Kernel;
|
||||
use Mattitja\BadIpBlocker\Middleware\CheckBadIps;
|
||||
|
||||
it('registers the middleware globally without any setup', function () {
|
||||
expect(app(Kernel::class)->hasMiddleware(CheckBadIps::class))->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not register the middleware twice when the app also adds it', function () {
|
||||
app(Kernel::class)->pushMiddleware(CheckBadIps::class);
|
||||
|
||||
$count = collect(app(Kernel::class)->getGlobalMiddleware())
|
||||
->filter(fn ($middleware) => $middleware === CheckBadIps::class)
|
||||
->count();
|
||||
|
||||
expect($count)->toBe(1);
|
||||
});
|
||||
|
||||
it('schedules a daily prune', function () {
|
||||
$event = collect(app(Schedule::class)->events())
|
||||
->first(fn ($event) => str_contains($event->command ?? '', 'bad-ip-blocker:prune'));
|
||||
|
||||
expect($event)->not->toBeNull()
|
||||
->and($event->expression)->toBe('0 0 * * *');
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Mattitja\BadIpBlocker\Facades\BadIpBlocker;
|
||||
|
||||
it('adds an ip', function () {
|
||||
$this->artisan('bad-ip-blocker:add', ['ip' => '203.0.113.10', '--reason' => 'bot', '--ttl' => 60])
|
||||
->assertSuccessful();
|
||||
|
||||
expect(BadIpBlocker::all()['203.0.113.10'])
|
||||
->reason->toBe('bot')
|
||||
->expires_at->not->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an invalid ip or ttl', function () {
|
||||
$this->artisan('bad-ip-blocker:add', ['ip' => 'nope'])->assertFailed();
|
||||
$this->artisan('bad-ip-blocker:add', ['ip' => '203.0.113.10', '--ttl' => 'abc'])->assertFailed();
|
||||
|
||||
expect(BadIpBlocker::all())->toBe([]);
|
||||
});
|
||||
|
||||
it('removes an ip', function () {
|
||||
BadIpBlocker::block('203.0.113.10');
|
||||
|
||||
$this->artisan('bad-ip-blocker:remove', ['ip' => '203.0.113.10'])->assertSuccessful();
|
||||
$this->artisan('bad-ip-blocker:remove', ['ip' => '203.0.113.10'])->assertFailed();
|
||||
});
|
||||
|
||||
it('lists ips', function () {
|
||||
$this->artisan('bad-ip-blocker:list')->expectsOutputToContain('No blocked')->assertSuccessful();
|
||||
|
||||
BadIpBlocker::block('203.0.113.10', 'bot');
|
||||
$blockedAt = BadIpBlocker::all()['203.0.113.10']['blocked_at'];
|
||||
|
||||
$this->artisan('bad-ip-blocker:list')
|
||||
->expectsTable(['IP', 'Reason', 'Blocked at', 'Expires at'], [['203.0.113.10', 'bot', $blockedAt, 'never']])
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
it('prunes expired blocks', function () {
|
||||
$this->artisan('bad-ip-blocker:prune')->assertSuccessful();
|
||||
});
|
||||
|
||||
it('clears all blocks after confirmation', function () {
|
||||
BadIpBlocker::block('203.0.113.10');
|
||||
|
||||
$this->artisan('bad-ip-blocker:clear')
|
||||
->expectsConfirmation('Remove all blocked IP addresses?', 'no')
|
||||
->assertFailed();
|
||||
expect(BadIpBlocker::all())->not->toBe([]);
|
||||
|
||||
$this->artisan('bad-ip-blocker:clear', ['--force' => true])->assertSuccessful();
|
||||
expect(BadIpBlocker::all())->toBe([]);
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Mattitja\BadIpBlocker\Facades\BadIpBlocker;
|
||||
|
||||
it('lets normal visitors through', function () {
|
||||
$this->getFrom('/', '203.0.113.10')->assertOk()->assertSee('ok');
|
||||
});
|
||||
|
||||
it('blocks ips on the list', function () {
|
||||
BadIpBlocker::block('203.0.113.10');
|
||||
|
||||
$this->getFrom('/', '203.0.113.10')->assertStatus(418)->assertSee('Blocked.');
|
||||
$this->getFrom('/', '192.0.2.30')->assertOk();
|
||||
});
|
||||
|
||||
it('uses the configured response', function () {
|
||||
config(['bad-ip-blocker.response' => ['status' => 403, 'message' => 'Nope']]);
|
||||
BadIpBlocker::block('203.0.113.10');
|
||||
|
||||
$this->getFrom('/', '203.0.113.10')->assertForbidden()->assertSee('Nope');
|
||||
});
|
||||
|
||||
it('does nothing when disabled', function () {
|
||||
config(['bad-ip-blocker.enabled' => false]);
|
||||
BadIpBlocker::block('203.0.113.10');
|
||||
|
||||
$this->getFrom('/', '203.0.113.10')->assertOk();
|
||||
});
|
||||
|
||||
it('never blocks whitelisted ranges, including private proxy addresses', function () {
|
||||
BadIpBlocker::block('192.168.1.10');
|
||||
|
||||
$this->getFrom('/', '192.168.1.10')->assertOk();
|
||||
$this->getFrom('/.well-known/security.txt', '192.168.1.20')->assertNotFound();
|
||||
|
||||
expect(BadIpBlocker::isBlocked('192.168.1.20'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('auto-blocks clients probing for a missing security.txt', function (string $path) {
|
||||
$this->getFrom($path, '198.51.100.20')->assertStatus(418);
|
||||
|
||||
expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeTrue()
|
||||
->and(BadIpBlocker::all()['198.51.100.20']['reason'])->toBe('Auto: '.$path);
|
||||
|
||||
$this->getFrom('/', '198.51.100.20')->assertStatus(418);
|
||||
})->with(['/.well-known/security.txt', '/security.txt', '/.env', '/wp-login.php']);
|
||||
|
||||
it('does not auto-block when the path exists in the app', function () {
|
||||
Route::get('/.well-known/security.txt', fn () => 'Contact: mailto:security@example.com');
|
||||
|
||||
$this->getFrom('/.well-known/security.txt', '198.51.100.20')->assertOk();
|
||||
|
||||
expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not auto-block ordinary 404s', function () {
|
||||
foreach (['/about', '/products', '/missing-page'] as $path) {
|
||||
$this->getFrom($path, '203.0.113.10')->assertNotFound();
|
||||
}
|
||||
|
||||
expect(BadIpBlocker::isBlocked('203.0.113.10'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('respects max_hits', function () {
|
||||
config(['bad-ip-blocker.auto_block.max_hits' => 2]);
|
||||
|
||||
$this->getFrom('/.well-known/security.txt', '198.51.100.20')->assertNotFound();
|
||||
expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse();
|
||||
|
||||
$this->getFrom('/security.txt', '198.51.100.20')->assertStatus(418);
|
||||
expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('expires auto-blocks after 7 days by default', function () {
|
||||
CarbonImmutable::setTestNow('2026-11-11 20:00:00');
|
||||
|
||||
$this->getFrom('/security.txt', '198.51.100.20');
|
||||
|
||||
expect(BadIpBlocker::all()['198.51.100.20']['expires_at'])->toBe('2026-11-18T20:00:00+00:00');
|
||||
|
||||
CarbonImmutable::setTestNow();
|
||||
});
|
||||
|
||||
it('can make auto-blocks permanent', function () {
|
||||
config(['bad-ip-blocker.auto_block.ttl' => null]);
|
||||
|
||||
$this->getFrom('/security.txt', '198.51.100.20');
|
||||
|
||||
expect(BadIpBlocker::all()['198.51.100.20']['expires_at'])->toBeNull();
|
||||
});
|
||||
|
||||
it('can disable auto-blocking', function () {
|
||||
config(['bad-ip-blocker.auto_block.enabled' => false]);
|
||||
|
||||
$this->getFrom('/security.txt', '198.51.100.20')->assertNotFound();
|
||||
|
||||
expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse();
|
||||
});
|
||||
Reference in new issue
Block a user