Source Code
The entire project, readable in your browser. No hidden JavaScript, no sneaky beacons. If we claimed we don't track you, this is where you check.
|
Pages/
Feeds/
Backend/
Includes/
Stylesheet/
JavaScript/
data/counter.txt and data/guestbook.txt hold visitor-supplied content
(a number and signed messages), so they are not shown here.
|
1 <?php 2 /** 3 * api/scan.php 4 * ----------------------------------------------------------------------------- 5 * The one and only backend endpoint. Returns JSON describing everything the 6 * SERVER can see about this request: 7 * 8 * - the visitor's IP address 9 * - every HTTP request header 10 * - connection / protocol / TLS facts 11 * - Cloudflare + proxy header detection 12 * - server software / environment facts 13 * - an outbound IP-intelligence lookup: country, city, timezone, lat/lon, 14 * ISP, ASN, organization, currency, continent, and VPN/proxy/hosting/tor 15 * flags (best-effort, using the free providers listed in config.php) 16 * 17 * IMPORTANT: This endpoint STORES NOTHING. No database, no log file, no cookie. 18 * It reads the request, optionally asks a geo provider about the IP, and returns 19 * the result straight to the same visitor who made the request. Nothing persists 20 * after the response is sent. 21 * ----------------------------------------------------------------------------- 22 */ 23 24 require_once __DIR__ . '/../includes/config.php'; 25 require_once __DIR__ . '/../includes/functions.php'; 26 27 // This is an API: emit JSON, allow same-origin XHR, and never cache. 28 if (!headers_sent()) { 29 header('Content-Type: application/json; charset=UTF-8'); 30 header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 31 header('Pragma: no-cache'); 32 header('X-Content-Type-Options: nosniff'); 33 // Same-origin only; there is nothing to gain from CORS here. 34 } 35 36 // ----------------------------------------------------------------------------- 37 // 1. Core request facts 38 // ----------------------------------------------------------------------------- 39 $ip = client_ip(); 40 $headers = collect_request_headers(); 41 $isHttps = is_https(); 42 $protocol = http_protocol(); 43 44 // ----------------------------------------------------------------------------- 45 // 2. Cloudflare / proxy / forwarding header detection 46 // ----------------------------------------------------------------------------- 47 $proxyHeaderKeys = array( 48 'Via', 'Forwarded', 'X-Forwarded-For', 'X-Forwarded-Host', 'X-Forwarded-Proto', 49 'X-Real-IP', 'X-Client-IP', 'X-Cluster-Client-IP', 'Client-IP', 'True-Client-IP', 50 'Proxy-Connection', 'X-Proxy-ID', 'X-Forwarded', 'Forwarded-For', 51 ); 52 $cloudflareHeaderKeys = array( 53 'CF-Connecting-IP', 'CF-IPCountry', 'CF-RAY', 'CF-Visitor', 'CF-Worker', 54 'CF-Request-ID', 'CDN-Loop', 55 ); 56 57 $detectedProxyHeaders = array(); 58 foreach ($proxyHeaderKeys as $k) { 59 if (isset($headers[$k])) { 60 $detectedProxyHeaders[$k] = $headers[$k]; 61 } 62 } 63 64 $detectedCloudflare = array(); 65 foreach ($cloudflareHeaderKeys as $k) { 66 if (isset($headers[$k])) { 67 $detectedCloudflare[$k] = $headers[$k]; 68 } 69 } 70 $behindCloudflare = !empty($detectedCloudflare); 71 $behindProxy = !empty($detectedProxyHeaders) || $behindCloudflare; 72 73 // ----------------------------------------------------------------------------- 74 // 3. Server / environment facts 75 // ----------------------------------------------------------------------------- 76 $server = array( 77 'software' => $_SERVER['SERVER_SOFTWARE'] ?? '(hidden)', 78 'name' => $_SERVER['SERVER_NAME'] ?? '(unknown)', 79 'addr' => $_SERVER['SERVER_ADDR'] ?? '(unknown)', 80 'port' => $_SERVER['SERVER_PORT'] ?? '(unknown)', 81 'protocol' => $protocol, 82 'php_version' => PHP_VERSION, 83 'php_sapi' => PHP_SAPI, 84 'https' => $isHttps, 85 'request_time' => date('c'), 86 'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET', 87 'remote_port' => $_SERVER['REMOTE_PORT'] ?? '(unknown)', 88 'host' => $_SERVER['HTTP_HOST'] ?? '(unknown)', 89 ); 90 91 // ----------------------------------------------------------------------------- 92 // 4. Convenience-parsed request facts (pulled from headers for the UI) 93 // ----------------------------------------------------------------------------- 94 $request = array( 95 'ip' => $ip, 96 'user_agent' => $headers['User-Agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? ''), 97 'accept' => $headers['Accept'] ?? '', 98 'accept_language' => $headers['Accept-Language'] ?? '', 99 'accept_encoding' => $headers['Accept-Encoding'] ?? '', 100 'dnt' => $headers['DNT'] ?? ($headers['Dnt'] ?? '(not sent)'), 101 'referer' => $headers['Referer'] ?? '(none)', 102 'connection' => $headers['Connection'] ?? '', 103 'sec_ch_ua' => $headers['Sec-Ch-UA'] ?? ($headers['Sec-CH-UA'] ?? ''), 104 'sec_ch_ua_platform' => $headers['Sec-Ch-UA-Platform'] ?? '', 105 'sec_ch_ua_mobile' => $headers['Sec-Ch-UA-Mobile'] ?? '', 106 'sec_fetch_site' => $headers['Sec-Fetch-Site'] ?? '', 107 'sec_fetch_mode' => $headers['Sec-Fetch-Mode'] ?? '', 108 'upgrade_insecure'=> $headers['Upgrade-Insecure-Requests'] ?? '', 109 'is_https' => $isHttps, 110 'protocol' => $protocol, 111 ); 112 113 // ----------------------------------------------------------------------------- 114 // 5. Outbound IP intelligence lookup (best-effort; storing nothing) 115 // ----------------------------------------------------------------------------- 116 $geo = ip_intelligence($ip); 117 118 // ----------------------------------------------------------------------------- 119 // 6. Assemble and emit. No storage, no logging -- just echo and exit. 120 // ----------------------------------------------------------------------------- 121 $out = array( 122 'ok' => true, 123 'stored' => false, // we promise: nothing is persisted 124 'generated' => date('c'), 125 'ip' => $ip, 126 'request' => $request, 127 'headers' => $headers, 128 'server' => $server, 129 'network' => array( 130 'behind_proxy' => $behindProxy, 131 'behind_cloudflare' => $behindCloudflare, 132 'proxy_headers' => $detectedProxyHeaders, 133 'cloudflare_headers'=> $detectedCloudflare, 134 ), 135 'geo' => $geo, 136 ); 137 138 echo json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 139 exit; 140 141 142 // ============================================================================= 143 // HELPERS (kept local to the endpoint) 144 // ============================================================================= 145 146 /** 147 * Resolve an IP to a normalized intelligence array using the ordered providers 148 * from config.php. Tries each until one answers. Returns a consistent shape 149 * regardless of which provider replied. Never throws; on total failure returns 150 * a structure with 'ok' => false so the client can degrade gracefully. 151 */ 152 function ip_intelligence(string $ip): array 153 { 154 $empty = array( 155 'ok' => false, 156 'provider' => null, 157 'ip' => $ip, 158 'is_private' => is_private_ip($ip), 159 'continent' => null, 160 'country' => null, 161 'country_code'=> null, 162 'region' => null, 163 'city' => null, 164 'zip' => null, 165 'lat' => null, 166 'lon' => null, 167 'timezone' => null, 168 'currency' => null, 169 'isp' => null, 170 'org' => null, 171 'asn' => null, 172 'as_name' => null, 173 'reverse' => null, 174 'mobile' => null, 175 'proxy' => null, 176 'hosting' => null, 177 'vpn' => null, 178 'tor' => null, 179 'note' => null, 180 ); 181 182 // Don't bother looking up private / reserved addresses. 183 if ($empty['is_private']) { 184 $empty['note'] = 'Private or reserved IP -- geolocation not applicable.'; 185 return $empty; 186 } 187 if (!IP_LOOKUP_ENABLED) { 188 $empty['note'] = 'IP lookup disabled by site configuration.'; 189 return $empty; 190 } 191 192 $providers = isset($GLOBALS['IP_LOOKUP_PROVIDERS']) ? $GLOBALS['IP_LOOKUP_PROVIDERS'] : array(); 193 194 foreach ($providers as $p) { 195 $url = str_replace('{ip}', rawurlencode($ip), $p['url']); 196 $body = http_get($url, IP_LOOKUP_TIMEOUT); 197 if ($body === null || $body === '') { 198 continue; 199 } 200 $json = json_decode($body, true); 201 if (!is_array($json)) { 202 continue; 203 } 204 $parsed = null; 205 switch ($p['parser']) { 206 case 'ipapi': $parsed = parse_ipapi($json); break; 207 case 'ipwhois': $parsed = parse_ipwhois($json); break; 208 case 'ipapico': $parsed = parse_ipapico($json); break; 209 } 210 if ($parsed !== null) { 211 $parsed['ip'] = $ip; 212 $parsed['is_private'] = false; 213 $parsed['ok'] = true; 214 $parsed['provider'] = $p['name']; 215 // Fill any missing keys from the empty template for a stable shape. 216 return array_merge($empty, array('ok' => true, 'provider' => $p['name']), $parsed); 217 } 218 } 219 220 $empty['note'] = 'All geolocation providers were unreachable or rate-limited.'; 221 return $empty; 222 } 223 224 /** 225 * Parse an ip-api.com response into the normalized shape. 226 */ 227 function parse_ipapi(array $j): ?array 228 { 229 if (!isset($j['status']) || $j['status'] !== 'success') { 230 return null; 231 } 232 $asn = null; $asname = null; 233 if (!empty($j['as'])) { 234 // Format: "AS15169 Google LLC" 235 if (preg_match('/^AS(\d+)\s*(.*)$/i', $j['as'], $m)) { 236 $asn = 'AS' . $m[1]; 237 $asname = trim($m[2]) !== '' ? trim($m[2]) : ($j['asname'] ?? null); 238 } else { 239 $asn = $j['as']; 240 } 241 } 242 return array( 243 'continent' => $j['continent'] ?? null, 244 'country' => $j['country'] ?? null, 245 'country_code' => $j['countryCode'] ?? null, 246 'region' => $j['regionName'] ?? null, 247 'city' => $j['city'] ?? null, 248 'zip' => $j['zip'] ?? null, 249 'lat' => isset($j['lat']) ? (float)$j['lat'] : null, 250 'lon' => isset($j['lon']) ? (float)$j['lon'] : null, 251 'timezone' => $j['timezone'] ?? null, 252 'currency' => $j['currency'] ?? null, 253 'isp' => $j['isp'] ?? null, 254 'org' => $j['org'] ?? null, 255 'asn' => $asn, 256 'as_name' => $asname ?? ($j['asname'] ?? null), 257 'reverse' => $j['reverse'] ?? null, 258 'mobile' => isset($j['mobile']) ? (bool)$j['mobile'] : null, 259 'proxy' => isset($j['proxy']) ? (bool)$j['proxy'] : null, 260 'hosting' => isset($j['hosting']) ? (bool)$j['hosting'] : null, 261 'vpn' => isset($j['proxy']) ? (bool)$j['proxy'] : null, // ip-api folds VPN into proxy 262 'tor' => null, 263 ); 264 } 265 266 /** 267 * Parse an ipwho.is response into the normalized shape. 268 */ 269 function parse_ipwhois(array $j): ?array 270 { 271 if (isset($j['success']) && $j['success'] === false) { 272 return null; 273 } 274 $conn = isset($j['connection']) && is_array($j['connection']) ? $j['connection'] : array(); 275 $tz = isset($j['timezone']) && is_array($j['timezone']) ? $j['timezone'] : array(); 276 $cur = isset($j['currency']) && is_array($j['currency']) ? $j['currency'] : array(); 277 return array( 278 'continent' => $j['continent'] ?? null, 279 'country' => $j['country'] ?? null, 280 'country_code' => $j['country_code'] ?? null, 281 'region' => $j['region'] ?? null, 282 'city' => $j['city'] ?? null, 283 'zip' => $j['postal'] ?? null, 284 'lat' => isset($j['latitude']) ? (float)$j['latitude'] : null, 285 'lon' => isset($j['longitude']) ? (float)$j['longitude'] : null, 286 'timezone' => $tz['id'] ?? null, 287 'currency' => $cur['code'] ?? null, 288 'isp' => $conn['isp'] ?? null, 289 'org' => $conn['org'] ?? null, 290 'asn' => isset($conn['asn']) ? ('AS' . $conn['asn']) : null, 291 'as_name' => $conn['org'] ?? null, 292 'reverse' => null, 293 'mobile' => null, 294 'proxy' => null, 295 'hosting' => null, 296 'vpn' => null, 297 'tor' => null, 298 ); 299 } 300 301 /** 302 * Parse an ipapi.co response into the normalized shape. 303 */ 304 function parse_ipapico(array $j): ?array 305 { 306 if (!empty($j['error'])) { 307 return null; 308 } 309 return array( 310 'continent' => $j['continent_code'] ?? null, 311 'country' => $j['country_name'] ?? null, 312 'country_code' => $j['country_code'] ?? null, 313 'region' => $j['region'] ?? null, 314 'city' => $j['city'] ?? null, 315 'zip' => $j['postal'] ?? null, 316 'lat' => isset($j['latitude']) ? (float)$j['latitude'] : null, 317 'lon' => isset($j['longitude']) ? (float)$j['longitude'] : null, 318 'timezone' => $j['timezone'] ?? null, 319 'currency' => $j['currency'] ?? null, 320 'isp' => $j['org'] ?? null, 321 'org' => $j['org'] ?? null, 322 'asn' => $j['asn'] ?? null, 323 'as_name' => $j['org'] ?? null, 324 'reverse' => null, 325 'mobile' => null, 326 'proxy' => null, 327 'hosting' => null, 328 'vpn' => null, 329 'tor' => null, 330 ); 331 } 332 333 /** 334 * Minimal HTTP GET. Uses cURL when available, falls back to file_get_contents 335 * with a stream context. Returns the body string, or null on any failure. 336 */ 337 function http_get(string $url, int $timeout): ?string 338 { 339 // Prefer cURL. 340 if (function_exists('curl_init')) { 341 $ch = curl_init(); 342 curl_setopt_array($ch, array( 343 CURLOPT_URL => $url, 344 CURLOPT_RETURNTRANSFER => true, 345 CURLOPT_TIMEOUT => $timeout, 346 CURLOPT_CONNECTTIMEOUT => $timeout, 347 CURLOPT_FOLLOWLOCATION => true, 348 CURLOPT_MAXREDIRS => 3, 349 CURLOPT_SSL_VERIFYPEER => true, 350 CURLOPT_SSL_VERIFYHOST => 2, 351 CURLOPT_USERAGENT => 'WhatCanILearn/1.0 (+privacy demo; no logging)', 352 CURLOPT_HTTPHEADER => array('Accept: application/json'), 353 )); 354 $body = curl_exec($ch); 355 $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 356 curl_close($ch); 357 if ($body !== false && $code >= 200 && $code < 400) { 358 return (string)$body; 359 } 360 // fall through to try the stream method 361 } 362 363 // Fallback: allow_url_fopen. 364 if (ini_get('allow_url_fopen')) { 365 $ctx = stream_context_create(array( 366 'http' => array( 367 'method' => 'GET', 368 'timeout' => $timeout, 369 'header' => "Accept: application/json\r\n" 370 . "User-Agent: WhatCanILearn/1.0 (+privacy demo; no logging)\r\n", 371 ), 372 'https' => array( 373 'method' => 'GET', 374 'timeout' => $timeout, 375 'header' => "Accept: application/json\r\n" 376 . "User-Agent: WhatCanILearn/1.0 (+privacy demo; no logging)\r\n", 377 ), 378 'ssl' => array( 379 'verify_peer' => true, 380 'verify_peer_name' => true, 381 ), 382 )); 383 $body = @file_get_contents($url, false, $ctx); 384 if ($body !== false) { 385 return (string)$body; 386 } 387 } 388 389 return null; 390 } 391 392 /** 393 * True if an IP is private, loopback, link-local, or otherwise reserved. 394 */ 395 function is_private_ip(string $ip): bool 396 { 397 if (!filter_var($ip, FILTER_VALIDATE_IP)) { 398 return true; // treat garbage as "not lookupable" 399 } 400 return !filter_var( 401 $ip, 402 FILTER_VALIDATE_IP, 403 FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE 404 ); 405 } 406 You are reading the real file being executed on the server. What you see is what runs. |