1
|
|
|
<?php |
2
|
|
|
namespace Azine\GeoBlockingBundle\EventListener; |
3
|
|
|
|
4
|
|
|
use Symfony\Component\DependencyInjection\Container; |
5
|
|
|
|
6
|
|
|
use Psr\Log\LoggerInterface; |
7
|
|
|
|
8
|
|
|
use Symfony\Component\HttpFoundation\Response; |
9
|
|
|
use Symfony\Component\Security\Core\User\UserInterface; |
10
|
|
|
|
11
|
|
|
use Symfony\Component\HttpKernel\HttpKernelInterface; |
12
|
|
|
|
13
|
|
|
use Symfony\Component\HttpKernel\Event\GetResponseEvent; |
14
|
|
|
|
15
|
|
|
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; |
16
|
|
|
|
17
|
|
|
use Azine\GeoBlockingBundle\Adapter\GeoIpLookupAdapterInterface; |
18
|
|
|
|
19
|
|
|
class GeoBlockingKernelRequestListener |
20
|
|
|
{ |
21
|
|
|
private $configParams; |
22
|
|
|
private $lookUpAdapter; |
23
|
|
|
private $templating; |
24
|
|
|
private $logger; |
25
|
|
|
private $container; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @param EngineInterface $templating |
29
|
|
|
* @param GeoIpLookupAdapterInterface $lookupAdapter |
30
|
|
|
* @param LoggerInterface $logger |
31
|
|
|
* @param Container $container |
32
|
|
|
* @param array $parameters |
33
|
|
|
*/ |
34
|
24 |
|
public function __construct(EngineInterface $templating, GeoIpLookupAdapterInterface $lookupAdapter, LoggerInterface $logger, Container $container, array $parameters) |
35
|
|
|
{ |
36
|
24 |
|
$this->configParams = $parameters; |
37
|
24 |
|
$this->lookUpAdapter = $lookupAdapter; |
38
|
24 |
|
$this->templating = $templating; |
39
|
24 |
|
$this->logger = $logger; |
40
|
24 |
|
$this->container = $container; |
41
|
24 |
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param GetResponseEvent $event |
45
|
|
|
*/ |
46
|
24 |
|
public function onKernelRequest(GetResponseEvent $event) |
47
|
|
|
{ |
48
|
|
|
// ignore sub-requests |
49
|
24 |
|
if ($event->getRequestType() == HttpKernelInterface::SUB_REQUEST) { |
50
|
1 |
|
return; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
// check if the blocking is enabled at all |
54
|
23 |
|
if (!$this->configParams['enabled']) { |
55
|
1 |
|
$this->logger->info("azine_geoblocking_bundle: blocking not enabled"); |
56
|
|
|
|
57
|
1 |
|
return; |
58
|
|
|
} |
59
|
|
|
|
60
|
22 |
|
$request = $event->getRequest(); |
61
|
|
|
// check if blocking authenticated users is enabled |
62
|
22 |
|
$authenticated = $this->container->get('security.token_storage')->getToken()->getUser() instanceof UserInterface; |
63
|
22 |
|
if ($this->configParams['blockAnonOnly'] && $authenticated) { |
64
|
1 |
|
$this->logger->info("azine_geoblocking_bundle: allowed logged-in user"); |
65
|
|
|
|
66
|
1 |
|
return; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
// allow access it the "geoblocking_allow_cookie" is set to true |
70
|
21 |
|
$alloweByCookie = $this->configParams['allow_by_cookie']; |
71
|
|
|
|
72
|
21 |
|
if($alloweByCookie && $request->cookies->get($this->configParams['allow_by_cookie_name'], false)){ |
73
|
1 |
|
return; |
74
|
|
|
} |
75
|
|
|
|
76
|
20 |
|
$visitorAddress = $request->getClientIp(); |
77
|
|
|
|
78
|
|
|
// check if the visitors IP is a private IP => the request comes from the same subnet as the server or the server it self. |
79
|
20 |
|
if ($this->configParams['allowPrivateIPs']) { |
80
|
20 |
|
$patternForPrivateIPs = "#(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)#"; |
81
|
20 |
|
if (preg_match($patternForPrivateIPs, $visitorAddress) == 1) { |
82
|
2 |
|
$this->logger->info("azine_geoblocking_bundle: allowed private network"); |
83
|
|
|
|
84
|
2 |
|
return; |
85
|
|
|
} |
86
|
18 |
|
} |
87
|
|
|
|
88
|
|
|
// check if the route is allowed from the current visitors country via whitelists |
89
|
18 |
|
$routeName = $request->get('_route'); |
90
|
18 |
|
$allowedByRouteWhiteList = array_search($routeName, $this->configParams['routeWhitelist'], true) === false; |
91
|
18 |
|
if (!$allowedByRouteWhiteList) { |
92
|
1 |
|
$this->logger->info("azine_geoblocking_bundle: allowed by routeWhiteList"); |
93
|
|
|
|
94
|
1 |
|
return; |
95
|
|
|
} |
96
|
|
|
|
97
|
17 |
|
$country = $this->lookUpAdapter->getCountry($visitorAddress); |
98
|
17 |
|
$allowedByCountryWhiteList = array_search($country, $this->configParams['countryWhitelist'], true) === false; |
99
|
17 |
|
if (!$allowedByCountryWhiteList) { |
100
|
2 |
|
$this->logger->info("azine_geoblocking_bundle: allowed by countryWhiteList"); |
101
|
|
|
|
102
|
2 |
|
return; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
// check if the vistitor is a whitelisted IP |
106
|
15 |
|
if ($this->isAllowedByIpWhiteListConfig($visitorAddress)) { |
107
|
2 |
|
$this->logger->info("azine_geoblocking_bundle: allowed by ipWhiteList"); |
108
|
|
|
|
109
|
2 |
|
return; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
// check if the visitor is allowed because it's a search-engine crawler of google or msn |
113
|
13 |
|
if ($this->isAllowedBecauseIpIsSearchEngingeCrawler($visitorAddress)) { |
114
|
2 |
|
$this->logger->info("azine_geoblocking_bundle: allowed by searchEngineConfig"); |
115
|
|
|
|
116
|
2 |
|
return; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
|
120
|
|
|
// until here everything that is allowed has been filtered out. |
121
|
11 |
|
$useRouteBL = array_key_exists('routeBlacklist', $this->configParams) && !empty($this->configParams['routeBlacklist']); |
122
|
11 |
|
$useCountryBL = array_key_exists('countryBlacklist', $this->configParams) && !empty($this->configParams['countryBlacklist']); |
123
|
|
|
|
124
|
|
|
// if neither of the blackLists should be used, deny all remaining requests |
125
|
11 |
|
if (!$useRouteBL && !$useCountryBL) { |
126
|
7 |
|
$this->logger->warning("azine_geoblocking_bundle: no blackLists defined and the request (Route: $routeName, Country: $country, IP: $visitorAddress) was not allowed by any of the whiteList/positive filters."); |
127
|
7 |
|
$this->blockAccess($event, $country); |
128
|
|
|
|
129
|
7 |
|
return; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
// check if one of the blacklists denies access |
133
|
4 |
View Code Duplication |
if ($useRouteBL) { |
|
|
|
|
134
|
2 |
|
if (array_search($routeName, $this->configParams['routeBlacklist'], true) !== false) { |
135
|
1 |
|
$this->logger->warning("azine_geoblocking_bundle: blocked by routeBL.\n".print_r($this->configParams['routeBlacklist'], true)); |
136
|
1 |
|
$this->blockAccess($event, $country); |
137
|
1 |
|
} |
138
|
2 |
|
} |
139
|
|
|
|
140
|
4 |
View Code Duplication |
if ($useCountryBL) { |
|
|
|
|
141
|
3 |
|
if (array_search($country, $this->configParams['countryBlacklist'], true) !== false) { |
142
|
2 |
|
$this->logger->warning("azine_geoblocking_bundle: blocked by countryBL\n".print_r($this->configParams['countryBlacklist'], true)); |
143
|
2 |
|
$this->blockAccess($event, $country); |
144
|
2 |
|
} |
145
|
3 |
|
} |
146
|
|
|
|
147
|
|
|
// one or both blacklists were defined to be used, but the request was not filtered out => allow the request |
148
|
4 |
|
$this->logger->info("azine_geoblocking_bundle: allowed, no denial-rule triggered"); |
149
|
|
|
|
150
|
4 |
|
return; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @param GetResponseEvent $event |
155
|
|
|
* @param string $country |
156
|
|
|
*/ |
157
|
10 |
|
private function blockAccess(GetResponseEvent $event, $country) |
158
|
|
|
{ |
159
|
|
|
// render the "sorry you are not allowed here"-page |
160
|
10 |
|
$parameters = array('loginRoute' => $this->configParams['loginRoute'], 'country' => $country); |
161
|
10 |
|
$event->setResponse($this->templating->renderResponse($this->configParams['blockedPageView'], $parameters, new Response('', Response::HTTP_FORBIDDEN))); |
162
|
10 |
|
$event->stopPropagation(); |
163
|
|
|
|
164
|
10 |
|
if ($this->configParams['logBlockedRequests']) { |
165
|
1 |
|
$request = $event->getRequest(); |
166
|
1 |
|
$routeName = $request->get('_route'); |
167
|
1 |
|
$ip = $request->getClientIp(); |
168
|
1 |
|
$uagent = $_SERVER['HTTP_USER_AGENT']; |
169
|
1 |
|
$hostName = gethostbyaddr($ip); |
170
|
1 |
|
$this->logger->warning("azine_geoblocking_bundle: Route $routeName was blocked for a user from $country (IP: $ip , HostName $hostName, UAgent: '$uagent'"); |
171
|
1 |
|
} |
172
|
10 |
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* @param string $ip |
176
|
|
|
* @return boolean |
177
|
|
|
*/ |
178
|
15 |
|
private function isAllowedByIpWhiteListConfig($ip) |
179
|
|
|
{ |
180
|
15 |
|
if ($this->configParams['ip_whitelist']) { |
181
|
3 |
|
foreach ($this->configParams['ip_whitelist'] as $pattern) { |
182
|
3 |
|
if ($ip == $pattern || @preg_match($pattern, $ip) === 1) { |
183
|
2 |
|
return true; |
184
|
|
|
} |
185
|
1 |
|
} |
186
|
1 |
|
} |
187
|
|
|
|
188
|
13 |
|
return false; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @param string $ip |
193
|
|
|
* @return boolean |
194
|
|
|
*/ |
195
|
13 |
|
private function isAllowedBecauseIpIsSearchEngingeCrawler($ip) |
196
|
|
|
{ |
197
|
13 |
|
if ($this->configParams['allow_search_bots']) { |
198
|
|
|
|
199
|
|
|
// resolve host name |
200
|
3 |
|
$hostName = gethostbyaddr($ip); |
201
|
|
|
// reverse resolve IP |
202
|
3 |
|
$reverseIP = gethostbyname($hostName); |
203
|
|
|
|
204
|
|
|
|
205
|
|
|
// chekc if the hostname matches any of the search-engine names. |
206
|
3 |
|
$searchEngineDomains = $this->configParams['search_bot_domains']; |
207
|
|
|
|
208
|
3 |
|
$isSearchEngineDomain = false; |
209
|
|
|
|
210
|
3 |
|
foreach ($searchEngineDomains as $domain) { |
211
|
|
|
|
212
|
|
|
// if the hostname ends with any of the search-engine-domain names |
213
|
3 |
|
if (substr($hostName, - strlen($domain)) === $domain) { |
214
|
|
|
|
215
|
|
|
// set variable to true and stop the loop |
216
|
2 |
|
$isSearchEngineDomain = true; |
217
|
2 |
|
break; |
218
|
|
|
} |
219
|
3 |
|
} |
220
|
|
|
|
221
|
|
|
// if the IP and reverse resolved IP match and the ip belongs to a search-engine-domain |
222
|
3 |
|
if ($ip == $reverseIP && $isSearchEngineDomain) { |
223
|
|
|
// allow the ip |
224
|
2 |
|
return true; |
225
|
|
|
} |
226
|
1 |
|
} |
227
|
|
|
|
228
|
11 |
|
return false; |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
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.