[ CRT: off ]
Visitors: 000003472 | Server: 2026-07-13 23:19:50 UTC | UTC: 2026-07-13 23:19:50 UTC
Best Viewed in Firefox | Powered by PHP 7.4.33 | Connection: HTTPS (secure) | No Cookies · No Tracking
*** Welcome to the last honest website on the internet *** +++ This site stores NOTHING about you -- everything is computed in YOUR browser +++ NEW! Live browser fingerprinting module added +++ NEW! Interactive terminal -- type help to begin +++ Best viewed in Firefox at 1024x768 or higher +++ Remember: if the product is free, usually YOU are the product -- but not here +++ Greetz to everyone still running an old CRT monitor <3

disk 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.

disk api/scan.php  (15.7 KB, 406 lines)
   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.