<?php

// Simple firewall to protect DDOS attack
// It's better to use
// https://github.com/terrylinooo/shieldon
// https://github.com/antonioribeiro/firewall
// https://packagist.org/packages/middlewares/firewall


namespace Hura8\System;


use Jaybizzle\CrawlerDetect\CrawlerDetect;

class Firewall
{

    const MAX_CONCURRENT_VISIT_ALLOW = 5;
    const WHITE_LIST_IP = ROOT_DIR . "/config/system/white_list_ip.txt";
    const BAN_IP_LIST   = ROOT_DIR . "/config/build/ban_ip_list.txt";
    const TEMPORARY_BAN_DURATION = 60; //seconds

    /* @var $instance Firewall */
    protected static $instance;
    protected $log_dir = ROOT_DIR . '/cache/firewall/log/';
    protected $ban_dir = ROOT_DIR . '/cache/firewall/ban/';
    private $user_ip = '';
    protected $white_list = [
        // google bot ips
        // bing bots
        // others..
    ];

    protected function __construct() {
        // todo: use Redis for performance instead of directory
        /*$this->log_dir = ROOT_DIR . '/cache/firewall/log/';
        $this->ban_dir = ROOT_DIR . '/cache/firewall/ban/';
        if(!file_exists($this->log_dir)) {
            mkdir($this->log_dir, 0755, true);
        }
        if(!file_exists($this->ban_dir)) {
            mkdir($this->ban_dir, 0755, true);
        }*/
        $this->user_ip = getIpAddress();

        if(file_exists(self::WHITE_LIST_IP)) {
            $this->white_list = array_filter(explode("\n", file_get_contents(self::WHITE_LIST_IP)));
        }

    }


    public static function getInstance() {
        if(!static::$instance) {
            static::$instance = new self();
        }

        return static::$instance;
    }

    // check if current user agent is an crawler
    public function isCrawler() : bool {
        $objCrawlerDetect = new CrawlerDetect();
        //echo $objCrawlerDetect->getUserAgent();
        // https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
        return $objCrawlerDetect->isCrawler();
    }

    public function monitor() {
        // todo:
        return false;

        $total_concurrent_visit = $this->logIp($this->user_ip);
        if($total_concurrent_visit > self::MAX_CONCURRENT_VISIT_ALLOW) {
            // ban if not in whitelist
            if(!$this->isWhiteListed()) {
                $this->banTemporary($this->user_ip, self::TEMPORARY_BAN_DURATION);
            }

            die("System overload");
        }

        return true;
    }

    public function stopIfBanned() {
        if($this->isIPBanned()) {
            die("Perhaps, this website is not for you.");
        }

        return false;
    }

    protected function isWhiteListed() {
        $filter  = new IPFilter($this->white_list);
        return $filter->check($this->user_ip);
    }

    protected function isIPBanned() {

        // check whitelist
        if($this->isWhiteListed()) {
            return false;
        }

        // check temporary ban
        /*$file_id        = $this->ban_dir . $this->user_ip.'.data';
        if(file_exists($file_id)) {
            $lift_time = file_get_contents($file_id);
            // lift_time is not over yet
            if($lift_time > time()) {
                return true;
            }
        }*/

        // permanent ban
        $config_file = static::BAN_IP_LIST;
        if(@file_exists( $config_file )) {
            $list_ips       = @file_get_contents( $config_file );
            $list_ip_arr    = array_filter(explode("\n", $list_ips));
            $filter         = new IPFilter($list_ip_arr);

            return $filter->check($this->user_ip);
        }

        return false;
    }

    // ban an IP temporarily
    protected function banTemporary($ip, $time) {
        $file_id        = $this->ban_dir . $ip.'.data';
        @file_put_contents($file_id, time() + $time);
    }



    // only allow 1 of these formats:
    // - Full IP: 127.0.0.1
    // - Wildcard: 172.0.0.*
    // - Mask: 125.0.0.1/24
    // - Range: 125.0.0.1-125.0.0.9
    public static function validateIP($ip) {
        // - Full IP: 127.0.0.1
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
            return true;
        }

        // - Wildcard: 172.0.0.*
        if (strpos($ip, "*") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.(([0-9]+)|\*)\.(([0-9]+)|\*)$/i", $ip)) {
            return true;
        }

        // - Mask: 125.0.0.1/24
        if (strpos($ip, "/") !== false && preg_match("/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)\/([0-9]+)$/i", $ip)) {
            return true;
        }

        // - Range: 125.0.0.1-125.0.0.9
        if (strpos($ip, "-") !== false ) {
            list($begin, $end) = explode('-', $ip);
            return filter_var($begin, FILTER_VALIDATE_IP) &&  filter_var($end, FILTER_VALIDATE_IP);
        }

        return false;
    }


    private function logIp($ip) {
        $file_id        = $this->log_dir . $ip.'.data';
        $time_track     = CURRENT_TIME - CURRENT_TIME % 5; // track within 5 seconds,
        $data = [];
        if(file_exists($file_id)) {
            $data = \json_decode(file_get_contents($file_id), true);
        }

        if(isset($data[$time_track])) {
            $data[$time_track] = $data[$time_track] + 1;
        }else{
            $data[$time_track] = 1;
        }

        // only log for current time to prevent large $data's size
        // our purpose is only to detect if this IP is brutally forceing the system (too many visits per time)

        @file_put_contents($file_id, \json_encode($data));

        return $data[$time_track];
    }

}
