1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\App\Middleware; |
4
|
|
|
|
5
|
|
|
// From PSR-7 |
6
|
|
|
use Psr\Http\Message\RequestInterface; |
7
|
|
|
use Psr\Http\Message\ResponseInterface; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* The IP middleware can restrict access to certain routes to certain IP. |
11
|
|
|
*/ |
12
|
|
|
class IpMiddleware |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* @var array|null |
16
|
|
|
*/ |
17
|
|
|
private $blacklist; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var array|null |
21
|
|
|
*/ |
22
|
|
|
private $whitelist; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var string|null |
26
|
|
|
*/ |
27
|
|
|
private $blacklistedRedirect; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var string|null |
31
|
|
|
*/ |
32
|
|
|
private $notWhitelistedRedirect; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var boolean |
36
|
|
|
*/ |
37
|
|
|
private $failOnInvalidIp; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @param array $data Constructor dependencies and options. |
41
|
|
|
*/ |
42
|
|
|
public function __construct(array $data) |
43
|
|
|
{ |
44
|
|
|
$data = array_merge($this->defaults(), $data); |
45
|
|
|
|
46
|
|
|
$this->blacklist = $data['blacklist']; |
47
|
|
|
$this->whitelist = $data['whitelist']; |
48
|
|
|
|
49
|
|
|
$this->blacklistedRedirect = $data['blacklisted_redirect']; |
50
|
|
|
$this->notWhitelistedRedirect = $data['not_whitelisted_redirect']; |
51
|
|
|
|
52
|
|
|
$this->failOnInvalidIp = $data['fail_on_invalid_ip']; |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Default middleware options. |
57
|
|
|
* |
58
|
|
|
* @return array |
59
|
|
|
*/ |
60
|
|
|
public function defaults() |
61
|
|
|
{ |
62
|
|
|
return [ |
63
|
|
|
'blacklist' => null, |
64
|
|
|
'whitelist' => null, |
65
|
|
|
|
66
|
|
|
'blacklisted_redirect' => null, |
67
|
|
|
'not_whitelisted_redirect' => null, |
68
|
|
|
|
69
|
|
|
'fail_on_invalid_ip' => false |
70
|
|
|
]; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Load a route content from path's cache. |
75
|
|
|
* |
76
|
|
|
* This method is as dumb / simple as possible. |
77
|
|
|
* It does not rely on any sort of settings / configuration. |
78
|
|
|
* Simply: if the cache for the route exists, it will be used to display the page. |
79
|
|
|
* The `$next` callback will not be called, therefore stopping the middleware stack. |
80
|
|
|
* |
81
|
|
|
* To generate the cache used in this middleware, @see \Charcoal\App\Middleware\CacheGeneratorMiddleware. |
82
|
|
|
* |
83
|
|
|
* @param RequestInterface $request The PSR-7 HTTP request. |
84
|
|
|
* @param ResponseInterface $response The PSR-7 HTTP response. |
85
|
|
|
* @param callable $next The next middleware callable in the stack. |
86
|
|
|
* @return ResponseInterface |
87
|
|
|
*/ |
88
|
|
|
public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next) |
89
|
|
|
{ |
90
|
|
|
$ip = $this->getClientIp($request); |
91
|
|
|
if (!$ip) { |
92
|
|
|
if ((!empty($this->blacklist) || !empty($this->whitelist)) && $this->failOnInvalidIp === true) { |
93
|
|
|
return $response->withStatus(403); |
94
|
|
|
} else { |
95
|
|
|
return $next($request, $response); |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
// Check blacklist. |
100
|
|
View Code Duplication |
if ($this->isIpBlacklisted($ip) === true) { |
|
|
|
|
101
|
|
|
if ($this->blacklistedRedirect) { |
|
|
|
|
102
|
|
|
return $response |
103
|
|
|
->withStatus(302) |
104
|
|
|
->withHeader('Location', $this->blacklistedRedirect); |
105
|
|
|
} else { |
106
|
|
|
// IP explicitely blacklisted: forbidden |
107
|
|
|
return $response->withStatus(403); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
// Check whitelist. |
112
|
|
View Code Duplication |
if ($this->isIpWhitelisted($ip) === false) { |
|
|
|
|
113
|
|
|
if ($this->notWhitelistedRedirect) { |
|
|
|
|
114
|
|
|
return $response |
115
|
|
|
->withStatus(302) |
116
|
|
|
->withHeader('Location', $this->blacklistedRedirect); |
117
|
|
|
} else { |
118
|
|
|
// IP not whistelisted: forbidden |
119
|
|
|
return $response->withStatus(403); |
120
|
|
|
} |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
// If here, not blacklisted or not-whitelisted; continue as normal. |
124
|
|
|
return $next($request, $response); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Check wether a certain IP is explicitely blacklisted. |
129
|
|
|
* |
130
|
|
|
* If the blacklist is null or empty, then nothing is ever blacklisted (return false). |
131
|
|
|
* |
132
|
|
|
* Note: this method only performs an exact string match on IP address, no IP masking / range features. |
133
|
|
|
* |
134
|
|
|
* @param string $ip The IP address to check against the blacklist. |
135
|
|
|
* @return boolean |
136
|
|
|
*/ |
137
|
|
|
private function isIpBlacklisted($ip) |
138
|
|
|
{ |
139
|
|
|
if (empty($this->blacklist)) { |
140
|
|
|
return false; |
141
|
|
|
} |
142
|
|
|
return $this->isIpInRange($ip, $this->blacklist); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Check wether a certain IP is explicitely whitelisted. |
147
|
|
|
* |
148
|
|
|
* If the whitelist is null or empty, then all IPs are whitelisted (return true). |
149
|
|
|
* |
150
|
|
|
* Note; This method only performs an exact string match on IP address, no IP masking / range features. |
151
|
|
|
* |
152
|
|
|
* @param string $ip The IP address to check against the whitelist. |
153
|
|
|
* @return boolean |
154
|
|
|
*/ |
155
|
|
|
private function isIpWhitelisted($ip) |
156
|
|
|
{ |
157
|
|
|
if (empty($this->whitelist)) { |
158
|
|
|
return true; |
159
|
|
|
} |
160
|
|
|
return $this->isIpInRange($ip, $this->whitelist); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* @param string $ip The IP to check. |
166
|
|
|
* @param string[] $cidrs The array of IPs/CIDRs to validate against. "/32" netmask is assumed. |
167
|
|
|
* @return boolean |
168
|
|
|
*/ |
169
|
|
|
private function isIpInRange($ip, array $cidrs) |
170
|
|
|
{ |
171
|
|
|
foreach ($cidrs as $range) { |
172
|
|
|
if (strpos($range, '/') === false) { |
173
|
|
|
$range .= '/32'; |
174
|
|
|
} |
175
|
|
|
// $range is in IP/CIDR format eg 127.0.0.1/24 |
176
|
|
|
list($subnet, $netmask) = explode('/', $range, 2); |
177
|
|
|
$netmask = ~(pow(2, (32 - $netmask)) - 1); |
178
|
|
|
if ((ip2long($ip) & $netmask) == (ip2long($subnet) & $netmask)) { |
179
|
|
|
return true; |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
// Nothing matched |
183
|
|
|
return false; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* @param RequestInterface $request The PSR-7 HTTP request. |
188
|
|
|
* @return string |
189
|
|
|
*/ |
190
|
|
|
private function getClientIp(RequestInterface $request) |
|
|
|
|
191
|
|
|
{ |
192
|
|
|
if (isset($_SERVER['REMOTE_ADDR'])) { |
193
|
|
|
return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP); |
194
|
|
|
} |
195
|
|
|
return ''; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.