<?php

namespace BitFire;

use BitFire\Block;
use BitFire\Config as CFG;
use const BitFire\DS;

if (!defined("\BitFire\WAF_ROOT")) {
    die("ip_blocking must not be called directly");
}

/**
 * Fast client IP resolver.
 *
 * @param array $server           typically $_SERVER
 * @param bool  $trust_proxy      trust proxy headers only if REMOTE_ADDR is trusted
 * @param array $trusted_proxies  list of IPs or CIDRs, e.g. ['203.0.113.10','10.0.0.0/8','2001:db8::/32']
 * @param bool  $allow_private    allow RFC1918, loopback, and link-local as client IPs
 * @return ?string                validated IP or null
 */
function client_ip_fast(array $server,
                        bool $allow_private = false) : ?string
{
    $remote = $server['REMOTE_ADDR'] ?? null;

    $valid = ip_is_valid($remote, $allow_private) ? $remote : null;

    // prefer REMOTE_ADDR if it's not a local address
    if ($valid) {
        return $remote;
    }

    // quick single-IP headers used by CDNs / proxies
    $single_headers = [
        'HTTP_CF_CONNECTING_IP',   // Cloudflare
        'HTTP_TRUE_CLIENT_IP',     // Akamai
        'HTTP_X_REAL_IP',          // Nginx, etc.
        'HTTP_X_CLIENT_IP',
        'HTTP_X_CLUSTER_CLIENT_IP',
        'HTTP_FASTLY_CLIENT_IP',
        'HTTP_FLY_CLIENT_IP',
    ];
    foreach ($single_headers as $h) {
        if (!empty($server[$h])) {
            $ip = normalize_ip_token($server[$h]);
            if (ip_is_valid($ip, $allow_private)) {
                return $ip;
            }
        }
    }

    // X-Forwarded-For, pick the left-most non-proxy candidate
    if (!empty($server['HTTP_X_FORWARDED_FOR'])) {
        $parts = explode(',', $server['HTTP_X_FORWARDED_FOR']);
        foreach ($parts as $p) {
            $ip = normalize_ip_token($p);
            if (!ip_is_valid($ip, $allow_private)) { continue; }
            return $ip;
        }
    }

    // fallback
    return ip_is_valid($remote, $allow_private) ? $remote : null;
}

/* -------- helpers, intentionally small and fast -------- */

function ip_is_valid(?string $ip, bool $allow_private) : bool {
    if (!$ip) return false;
    $flags = $allow_private ? 0 : (FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    return filter_var($ip, FILTER_VALIDATE_IP, $flags) !== false;
}

function normalize_ip_token(string $t) : string {
    $t = trim($t, " \t\"'[]");       // strip quotes and brackets
    $semi = strpos($t, ';');         // ignore any params
    if ($semi !== false) $t = substr($t, 0, $semi);
    return trim($t);
}

function do_ip_block() {
    $ip = client_ip_fast($_SERVER);
    $myself = $_SERVER["SERVER_ADDR"] ?? "127.0.0.1";

    // never block mysql
    if ($ip != '' && $ip != '127.0.0.1' && $ip != $myself) {
        $block_file = \BitFire\BLOCK_DIR . DS . $ip;
        if (file_exists($block_file)) {
            // ip is still blocked
            $m = filemtime($block_file);
            $t = time();
            if ($m > $t) {
                http_response_code(429);
                header('Retry-After: ' . ($m - $t));
                $file = __DIR__ . DIRECTORY_SEPARATOR . "blocks" . DIRECTORY_SEPARATOR . "block_log.txt";


                $w = file_put_contents($file,
                    "$ip: {$_SERVER['HTTP_USER_AGENT']} => {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\n",
                    FILE_APPEND);
                die("<html><body><h1>Too Many Requests ($w) / Malicious Requests</h1><p>Your IP address is temporarily blocked.</p></body></html>\n");
            }
            // ip block has expired
            else {
                // whitelisted ips are never blocked
                if (file_get_contents($block_file) != 'allow') {
                    @unlink($block_file);
                }
            }
        }
    }
}

do_ip_block();
