Skip to content

Commit d63a1bd

Browse files
authored
Merge pull request #5 from dsone/laravel-11
Laravel 11
2 parents 57530b3 + de961ca commit d63a1bd

File tree

5 files changed

+124
-57
lines changed

5 files changed

+124
-57
lines changed

CHANGELOG.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
# CHANGELOG
22

3+
## 0.5 - 2024-12-07
4+
5+
- Raise minimum Laravel version to 11
6+
- Raise minimum PHP version to 8.3
7+
- Fix `block_timeout` not being used
8+
- Fix filter not working correctly with sequential array
9+
- Allow callbacks to be callables, or static/instance methods on classes
10+
311
## 0.4 - 2023-02-15
412

5-
- Raise minimum Laravel version to 10
13+
- Raise minimum Laravel version to 10
614

715
## 0.3.1 - 2022-02-20
816

9-
- Update for Laravel 9 compatibility
17+
- Update for Laravel 9 compatibility
1018

1119
## 0.3 - 2021-05-08
1220

13-
- [ch445] Add option for IP hashing
14-
- [ch441] Add filter option for user Agents
15-
- [ch441] Change `$type` parameter in callbacks to `$criteriaBits`
16-
- Change namespace from `Dsone\ExceptionHandler` to `Dsone\Strainex`
21+
- [ch445] Add option for IP hashing
22+
- [ch441] Add filter option for user Agents
23+
- [ch441] Change `$type` parameter in callbacks to `$criteriaBits`
24+
- Change namespace from `Dsone\ExceptionHandler` to `Dsone\Strainex`
1725

1826
## 0.2 - 2021-03-06
1927

20-
- Supporting map table instead of sequential array for SEO spam referer list
21-
- Support for `exit` instead of returning response codes
22-
- Adding PHP 7.3 as dependency
23-
- Formatting/expanding README
28+
- Supporting map table instead of sequential array for SEO spam referer list
29+
- Support for `exit` instead of returning response codes
30+
- Adding PHP 7.3 as dependency
31+
- Formatting/expanding README
2432

2533
## 0.1 - 2021-03-06
2634

27-
- Initial version
35+
- Initial version

README.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,59 @@ There's an absurd amount of noise for the yet bazillionth time scan of a technol
55
Strainex might be of service to you.
66

77
### Beware
8+
89
_Strainex is not meant to make your website more secure._
910

1011
If you want to make your website more secure than this is not the package you're looking for.
1112
In such cases a honeypot setup, fail2ban, plain DENY/ALLOW in your .htaccess or similar tools might be better suited for your use case.
1213

1314
## What Strainex does: reducing noise
15+
1416
Strainex is an attempt at reducing noise in your (custom) logging for when errors occur. By filtering out common SEO spam and scan attempts that end in a 404 most of the times in an easy way.
1517
It's customizable, so you can add arbitrary URL requests to "block" further requests from that one entity visiting you on a daily basis.
1618
Blocking here means two things:
19+
1720
1. Preventing any more exception handlers to run, which in turn would trigger your custom logging/handling (basically "filtering")
1821
2. Optionally prevent any further requests from the same IP for a configurable time (proper "blocking")
19-
22+
2023
With Strainex you can keep those logs of yours a bit more clean by reducing the noise easily by 90% that way.
2124

2225
### How it works
26+
2327
When an entity requests a URL from your Laravel website, like `example.com/wp-admin`, it usually returns a 404 in a Laravel context. Within Laravel, a 404 is an HttpException. It is thrown somewhere in your application for a not found route.
24-
28+
2529
The default Exception handler in `app\Exceptions\Handler` is invoked at last (unless re-configured with other handlers), doing whatever you have configured in there for such cases. The entity gets to see the 404 only (or any other triggered exception code) and goes ahead to request the next URL like `example.com/vendor/phpunit`, rinse and repeat. Filling up your logs with noise.
26-
30+
2731
Strainex does not change the normal behaviour of how Laravel handles these exceptions. Instead, it adds onto that by wrapping itself around these exceptions (404 and other HttpExceptions) via a Decorator Pattern, checks if certain configured routes were accessed, specific referer or user agents are detected and aborts itself before invoking any other exception handlers.
28-
32+
2933
If blocking is enabled, Strainex saves the IP in a Redis instance. The next request from that IP is checked within the boot process of Laravel. If the request is from a known entity, Strainex aborts the boot process. Strainex returns a configurable response code (default 500 for blocked, 503 for filtered) or simply exits. Keeping your logs clean(er) and your app from bootstrapping any further.
3034

3135
## Requirements
32-
* PHP >=7.3
33-
* Laravel >8.*
34-
* Redis _(optional)_
35-
36-
If you want to also block entities, you need Redis setup on your machine.
36+
37+
- PHP >=8.3
38+
- Laravel >=11.\*
39+
- Redis _(optional)_
40+
41+
If you want to also block entities, you need Redis setup on your machine.
3742
In that case any subsequent request of a previously blocked entity is being prevented by aborting early.
3843

3944
## Installation
4045

4146
1. Add this repository to your composer json:
42-
```
43-
"repositories": [
44-
{ "type": "vcs", "url": "https://github.com/dsone/strainex" }
45-
]
46-
```
47-
2. Install via
48-
`composer require dsone/strainex`
47+
```
48+
"repositories": [
49+
{ "type": "vcs", "url": "https://github.com/dsone/strainex" }
50+
]
51+
```
52+
2. Install via
53+
`composer require dsone/strainex`
4954
3. Publish config of `Dsone\Strainex\Providers\StrainexServiceProvider` via
50-
`php artisan vendor:publish`
55+
`php artisan vendor:publish`
5156
4. Read commentary in `config/strainex.php` and edit settings for the URL and referer to filter out
5257
5. Edit .env vars as you see fit or leave defaults as defined in the config file
5358
5459
## Credit
60+
5561
...where credit is due.
56-
62+
5763
The Decorator Pattern around exception handling in Laravel was adapted from [cerbero90/exception-handler](https://github.com/cerbero90/exception-handler).

composer.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@
3030
}
3131
},
3232
"require": {
33-
"php": ">=8.1",
34-
"predis/predis": "^1.1",
35-
"nesbot/carbon": "^2.31",
36-
"illuminate/redis": "^10.0",
37-
"illuminate/support": "^10"
33+
"php": "^8.3",
34+
"predis/predis": ">=1.1",
35+
"nesbot/carbon": ">=2.31",
36+
"illuminate/redis": ">=10.0",
37+
"illuminate/support": ">=10"
3838
},
3939
"require-dev": {
40-
"orchestra/testbench": "^6.0",
41-
"phpunit/phpunit": "^10.0",
42-
"mockery/mockery": "^1.4.4"
40+
"orchestra/testbench": ">=6.0",
41+
"phpunit/phpunit": "^11.3.6",
42+
"mockery/mockery": "^1.6"
4343
}
4444
}

src/Classes/StrainexDecorator.php

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Request;
66
use Throwable;
77
use Carbon\Carbon;
8+
use ReflectionMethod;
89
use Illuminate\Support\Facades\Redis;
910
use Illuminate\Contracts\Debug\ExceptionHandler;
1011

@@ -75,7 +76,7 @@ public function report(Throwable $e)
7576
* @return boolean True on match, false otherwise.
7677
*/
7778
private function isMatch(string $needle, Array $haystack) {
78-
if (isset($mapping[0])) { // sequential array
79+
if (isset($haystack[0])) { // sequential array
7980
return in_array($needle, $haystack);
8081
}
8182

@@ -138,38 +139,78 @@ private function filterEvent(Throwable $exception) {
138139
config('strainex.redis_string', 'strainex:ip:ban:') . $ip,
139140
serialize($data),
140141
'ex', // define 4th param as seconds
141-
(config('app.env', 'production') === 'local' ? 15 : 21600) // 15s in dev, 6h in non-dev
142+
(config('app.env', 'production') === 'local' ? 10 : config('strainex.block_timeout')) // 10s in dev, 6h in prod
142143
);
143144

144145
// Invoke optional callback
145146
$callback = config('strainex.callbacks.blocked', false);
146-
if ($callback && is_callable($callback)) {
147-
$callback($exception, $data, $criteriaBits);
148-
}
147+
$this->invokeCallback($callback, $exception, $data, $criteriaBits);
149148

150149
// Exit
151-
if (config('strainex.always_exit', false)) { exit(0); }
150+
if (config('strainex.always_exit', false)) {
151+
exit(0);
152+
}
152153
// Trigger new error, going back to $this->report, returning early because !!$strainex_abort
153154
abort(config('strainex.blocked_status'));
154155
}
155156
// only a filtered event without blocking
156157
} else {
157158
// Invoke optional callback
158159
$callback = config('strainex.callbacks.filtered', false);
159-
if ($callback && is_callable($callback)) {
160-
$callback($exception, $criteriaBits);
161-
}
160+
$this->invokeCallback($callback, $exception, $criteriaBits);
162161

163162
// Exit
164-
if (config('strainex.always_exit', false)) { exit(0); }
163+
if (config('strainex.always_exit', false)) {
164+
exit(0);
165+
}
165166
// Trigger new error, going back to $this->report, returning early because !!$strainex_abort
166167
abort(config('strainex.filtered_status'));
167168
}
168169
} else {
169170
// Invoke optional callback
170171
$callback = config('strainex.callbacks.passed', false);
171-
if ($callback && is_callable($callback)) {
172-
$callback($exception);
172+
$this->invokeCallback($callback, $exception);
173+
}
174+
}
175+
176+
/**
177+
* Invoke a callback, either a single callable or an array of class/method pairs.
178+
* Supports static and non-static methods as well as callable functions.
179+
*
180+
* @param mixed $callback Either a callable or an array of class/method pairs.
181+
* @param mixed ...$cbParams Parameters to pass to the callback.
182+
* @return void
183+
*/
184+
private function invokeCallback($callback, ...$cbParams) {
185+
if (!$callback) {
186+
return;
187+
}
188+
189+
if ($callback && is_callable($callback)) {
190+
$callback(...$cbParams);
191+
} else if (is_array($callback)) {
192+
foreach ($callback as $cb) {
193+
if (is_callable($cb)) {
194+
$cb(...$cbParams);
195+
continue;
196+
}
197+
if (!is_array($cb)) {
198+
continue;
199+
}
200+
list($class, $method) = $cb;
201+
202+
if (class_exists($class)) {
203+
if (method_exists($class, $method)) {
204+
$reflection = new ReflectionMethod($class, $method);
205+
206+
if ($reflection->isStatic()) {
207+
$class::$method(...$cbParams);
208+
} else {
209+
$instance = new $class();
210+
$instance->$method(...$cbParams);
211+
}
212+
}
213+
}
173214
}
174215
}
175216
}

src/config/strainex.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333
* Map: [ 'A' => 1, 'B' => 1, 'C' => 1, 'D' => 1, 'E' => 1 ] - a lot better for large lists
3434
*/
3535
'referer' => [
36-
36+
3737
],
3838
/**
3939
* Similar to the referers but for the User-Agent strings.
4040
* userAgents are checked in lowercase, hence use lowercase here.
4141
*/
4242
'userAgents' => [
43-
43+
4444
],
4545
/**
4646
* RegExp usable partial URL strings.
@@ -90,16 +90,24 @@
9090
/**
9191
* Optional callables that are invoked at different states.
9292
*
93-
* Can be a class with a _static_ `method` in the form of
93+
* Can be a class with a method in the form of
9494
* [ MyExceptionEvent:class, 'method' ].
95+
* `method` can be static or non-static.
96+
* Examples:
97+
* ```php
98+
* 'blocked' => [ [ App\Utils\StrainexCallbacks::class, 'blocked' ] ]
99+
* 'blocked' => function() { dd('blocked'); }
100+
* 'blocked' => [ function() { dd('blocked') }, [ App\Utils\StrainexCallbacks::class, 'blocked' ] ]
101+
* ```
95102
*
96-
* Beware that when a second request for an IP blocked entity comes along,
103+
* Beware that when a second request for an IP-blocked-entity comes along,
97104
* the invoked callback might not be able to use all of Laravel's features,
98105
* since Strainex tries to leave the boostrapping asap.
99106
*/
100107
'callbacks' => [
101108
/**
102-
* A request was blocked, only invokable if block_requests is true.
109+
* A request was blocked, only invokable if `block_requests` is true.
110+
*
103111
* The checks to block are in this order: SEO (bit 1), userAgent (bit 2), URL (bit 4).
104112
* $criteriaBits will be a bitmask of what matched the criteria to block the entity.
105113
* An entity provoking a 404 with a userAgent match will therefore have the bit (2 | 4) = 6.
@@ -111,13 +119,17 @@
111119
*/
112120
'blocked' => false,
113121
/**
114-
* Almost the same as blocked, just that this is only invokable if block_requests is false.
122+
* The replacement callback for when `block_requests` is false.
123+
* If you do not want to block requests directly and automatically, you can use this callback.
124+
*
115125
* The callable gets only the original $exception and $criteriaBits as parameter.
116126
*/
117127
'filtered' => false,
118128
/**
119-
* The negated case of filtered and blocked basically.
120-
* Most useful for debugging purposes.
129+
* Valid requests that are not blocked/filtered but raise an exception anyway.
130+
*
131+
* Most useful for debugging purposes only or to count errors raised by specific IPs.
132+
*
121133
* Does not rely on the block_requests setting.
122134
*/
123135
'passed' => false

0 commit comments

Comments
 (0)