PluginProbe
The Innovative Form Builder – IvyForms / 1.4.1
The Innovative Form Builder – IvyForms v1.4.1
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Security / Common / IpDetectionService.php

IpDetectionService.php in The Innovative Form Builder – IvyForms 1.4.1, at backend/src/Services/Security/Common/IpDetectionService.php

324 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Services\Security\Common;
9
10 /**
11 * Enhanced IP Detection Service
12 *
13 * Provides comprehensive IP address detection with support for various proxy
14 * configurations, CDN services (Cloudflare), and security filtering options.
15 *
16 * @package IvyForms\Services\Security
17 * @SuppressWarnings(PHPMD)
18 */
19 class IpDetectionService
20 {
21 /**
22 * IP source priority order for detection
23 */
24 private const IP_SOURCES = [
25 'HTTP_CF_CONNECTING_IP', // Cloudflare connecting IP (highest priority if valid)
26 'HTTP_CLIENT_IP', // Shared internet IP
27 'HTTP_X_REAL_IP', // Real IP header (Nginx proxy)
28 'HTTP_X_FORWARDED_FOR', // Proxy forwarded IP
29 'HTTP_X_FORWARDED', // Alternative proxy header
30 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster client IP
31 'HTTP_FORWARDED_FOR', // Another proxy variant
32 'HTTP_FORWARDED', // Standard forwarded header
33 'REMOTE_ADDR' // Direct connection IP (fallback)
34 ];
35
36 /**
37 * Cloudflare IP ranges for validation
38 */
39 private const CLOUDFLARE_IP_RANGES = [
40 '199.27.128.0/21',
41 '173.245.48.0/20',
42 '103.21.244.0/22',
43 '103.22.200.0/22',
44 '103.31.4.0/22',
45 '141.101.64.0/18',
46 '108.162.192.0/18',
47 '190.93.240.0/20',
48 '188.114.96.0/20',
49 '197.234.240.0/22',
50 '198.41.128.0/17',
51 '162.158.0.0/15',
52 '104.16.0.0/12',
53 '172.64.0.0/13',
54 '131.0.72.0/22'
55 ];
56
57 /**
58 * Required Cloudflare headers for validation
59 */
60 private const CLOUDFLARE_HEADERS = [
61 'HTTP_CF_CONNECTING_IP',
62 'HTTP_CF_IPCOUNTRY',
63 'HTTP_CF_RAY',
64 'HTTP_CF_VISITOR'
65 ];
66
67 /**
68 * Get user IP address with enhanced detection
69 *
70 * @param bool $allowPrivate Whether to allow private IP addresses (default: false)
71 * @param bool $validateCloudflare Whether to validate Cloudflare IPs (default: true)
72 * @return string The detected IP address or empty string if none found
73 */
74 public static function getUserIpAddress(bool $allowPrivate = false, bool $validateCloudflare = true): string
75 {
76 // Try Cloudflare IP first if detected and validation is enabled
77 if ($validateCloudflare) {
78 $cloudflareIp = self::getCloudflareIp();
79 if ($cloudflareIp !== null) {
80 return $cloudflareIp;
81 }
82 }
83
84 // Check all IP sources in priority order
85 foreach (self::IP_SOURCES as $source) {
86 $ip = self::getServerVariable($source);
87
88 if (empty($ip)) {
89 continue;
90 }
91
92 // Handle comma-separated IPs (common in X-Forwarded-For)
93 if (strpos($ip, ',') !== false) {
94 $ips = array_map('trim', explode(',', $ip));
95 foreach ($ips as $candidateIp) {
96 $validIp = self::validateAndCleanIp($candidateIp, $allowPrivate);
97 if ($validIp !== '') {
98 return $validIp;
99 }
100 }
101 } else {
102 $validIp = self::validateAndCleanIp($ip, $allowPrivate);
103 if ($validIp !== '') {
104 return $validIp;
105 }
106 }
107 }
108
109 return '';
110 }
111
112 /**
113 * Get the most reliable IP address (prefers direct connection)
114 *
115 * @param bool $allowPrivate Whether to allow private IP addresses
116 * @return string The most reliable IP address
117 */
118 public static function getReliableIpAddress(bool $allowPrivate = false): string
119 {
120 // First try direct connection (most reliable)
121 $directIp = self::getServerVariable('REMOTE_ADDR');
122 if (!empty($directIp)) {
123 $validIp = self::validateAndCleanIp($directIp, $allowPrivate);
124 if ($validIp !== '') {
125 return $validIp;
126 }
127 }
128
129 // Fallback to full detection
130 return self::getUserIpAddress($allowPrivate, true);
131 }
132
133 /**
134 * Check if the current request is from Cloudflare
135 *
136 * @return bool True if request appears to be from Cloudflare
137 */
138 public static function isCloudflareRequest(): bool
139 {
140 return self::isCloudflare();
141 }
142
143 /**
144 * Get detailed IP information for debugging
145 *
146 * @return array<string, mixed> Array containing all IP detection details
147 */
148 public static function getIpDebugInfo(): array
149 {
150 $debugInfo = [
151 'detected_ip' => self::getUserIpAddress(),
152 'reliable_ip' => self::getReliableIpAddress(),
153 'is_cloudflare' => self::isCloudflare(),
154 'cloudflare_ip' => self::getCloudflareIp(),
155 'sources' => []
156 ];
157
158 foreach (self::IP_SOURCES as $source) {
159 $value = self::getServerVariable($source);
160 if (!empty($value)) {
161 $debugInfo['sources'][$source] = $value;
162 }
163 }
164
165 return $debugInfo;
166 }
167
168 /**
169 * Get Cloudflare IP if request is from Cloudflare
170 *
171 * @return string|null The Cloudflare connecting IP or null if not valid
172 */
173 private static function getCloudflareIp(): ?string
174 {
175 if (!self::isCloudflare()) {
176 return null;
177 }
178
179 $cfIP = self::getServerVariable('HTTP_CF_CONNECTING_IP');
180 if (empty($cfIP)) {
181 return null;
182 }
183
184 // Sanitize and validate the IP
185 $sanitizedIp = sanitize_text_field(wp_unslash($cfIP));
186 return filter_var($sanitizedIp, FILTER_VALIDATE_IP) ? $sanitizedIp : null;
187 }
188
189 /**
190 * Check if the request is from Cloudflare
191 *
192 * @return bool True if request is from Cloudflare
193 */
194 private static function isCloudflare(): bool
195 {
196 // Check required Cloudflare headers first
197 foreach (self::CLOUDFLARE_HEADERS as $header) {
198 if (!isset($_SERVER[$header])) {
199 return false;
200 }
201 }
202
203 // Get the source IP for validation
204 $sourceIp = self::getSourceIpForValidation();
205 if (empty($sourceIp)) {
206 return false;
207 }
208
209 // Validate if the source IP is from Cloudflare's ranges
210 return self::isCloudflareIp($sourceIp);
211 }
212
213 /**
214 * Get source IP for Cloudflare validation
215 *
216 * @return string The source IP address
217 */
218 private static function getSourceIpForValidation(): string
219 {
220 $sources = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
221
222 foreach ($sources as $source) {
223 $ip = self::getServerVariable($source);
224 if (!empty($ip)) {
225 return sanitize_text_field(wp_unslash($ip));
226 }
227 }
228
229 return '';
230 }
231
232 /**
233 * Validate if IP is from Cloudflare's IP ranges
234 *
235 * @param string $ip The IP address to validate
236 * @return bool True if IP is from Cloudflare
237 */
238 private static function isCloudflareIp(string $ip): bool
239 {
240 foreach (self::CLOUDFLARE_IP_RANGES as $range) {
241 if (self::ipInRange($ip, $range)) {
242 return true;
243 }
244 }
245
246 return false;
247 }
248
249 /**
250 * Validate and clean an IP address
251 *
252 * @param string $ip The IP address to validate
253 * @param bool $allowPrivate Whether to allow private IP addresses
254 * @return string The cleaned IP address or empty string if invalid
255 */
256 private static function validateAndCleanIp(string $ip, bool $allowPrivate): string
257 {
258 // Remove any surrounding whitespace
259 $ip = trim($ip);
260
261 // Remove port number if present (e.g., "192.168.1.1:8080")
262 if (strpos($ip, ':') !== false && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
263 $ip = explode(':', $ip)[0];
264 }
265
266 // Validate IP format
267 if (!filter_var($ip, FILTER_VALIDATE_IP)) {
268 return '';
269 }
270
271 // Check for private IP addresses if not allowed
272 if (!$allowPrivate && self::isPrivateIp($ip)) {
273 return '';
274 }
275
276 return $ip;
277 }
278
279 /**
280 * Check if an IP address is private/local
281 *
282 * @param string $ip The IP address to check
283 * @return bool True if the IP is private
284 */
285 private static function isPrivateIp(string $ip): bool
286 {
287 // Use filter_var for private IP detection
288 return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
289 }
290
291 /**
292 * Check if an IP address is within a given CIDR range
293 *
294 * @param string $ip The IP address to check
295 * @param string $range The IP range in CIDR notation
296 * @return bool True if the IP is in the range
297 */
298 private static function ipInRange(string $ip, string $range): bool
299 {
300 if (strpos($range, '/') === false) {
301 $range .= '/32';
302 }
303
304 [$subnet, $netmask] = explode('/', $range, 2);
305 $rangeDecimal = ip2long($subnet);
306 $ipDecimal = ip2long($ip);
307 $wildcardDecimal = pow(2, (32 - (int)$netmask)) - 1;
308 $netmaskDecimal = ~$wildcardDecimal;
309
310 return ($ipDecimal & $netmaskDecimal) === ($rangeDecimal & $netmaskDecimal);
311 }
312
313 /**
314 * Safely get server variable
315 *
316 * @param string $key The server variable key
317 * @return string The server variable value or empty string
318 */
319 private static function getServerVariable(string $key): string
320 {
321 return $_SERVER[$key] ?? '';
322 }
323 }
324