From Kromonos, 8 Months ago, written in PHP.
Embed
  1. <?php
  2.  
  3. namespace kromonos\firewall;
  4.  
  5. use kromonos\browser\Browser;
  6.  
  7. class KrautFlaer
  8. {
  9.     private $cacheFolder = null;
  10.     private $grav        = false;
  11.     private $config      = null;
  12.     private $cachetime   = 2;
  13.     private $ip          = '127.0.0.1';
  14.     private $user_agent  = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/85.0';
  15.     private $referer     = 'https://example.com';
  16.     private $noLogIP     = [
  17.     ];
  18.     private $header      = [
  19.         'Accept: text/xml,application/xml,application/xhtml+xml,'
  20.             . 'text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
  21.         'Cache-Control: max-age=0',
  22.         'Connection: keep-alive',
  23.         'Keep-Alive: 300',
  24.         'Accept-Charset: utf-8;q=0.7,*;q=0.7',
  25.         'Accept-Language: en-us,en;q=0.5',
  26.         'Pragma: '
  27.     ];
  28.     const BASE_API_URL = 'https://api.cloudflare.com/client/v4/ips';
  29.     const NOTICE       = 1;
  30.     const WARNING      = 2;
  31.     const ALERT        = 3;
  32.     const ERROR        = 4;
  33.     const DEBUG        = 5;
  34.  
  35.  
  36.     public function __construct($grav)
  37.     {
  38.         if ($grav) {
  39.             $this->grav = $grav;
  40.             $this->config = $grav['config'];
  41.         }
  42.  
  43.         if (!class_exists('\kromonos\browser\Browser')) {
  44.             include 'browser.php';
  45.         }
  46.  
  47.         $this->browser = new Browser();
  48.         $this->setIp();
  49.     }
  50.  
  51.     /**
  52.      * Return real ip address
  53.      *
  54.      * @return string
  55.      */
  56.     public function realIP()
  57.     {
  58.         $ip = $_SERVER['REMOTE_ADDR'];
  59.         if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])
  60.             && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
  61.             foreach ($matches[0] as $xip) {
  62.                 if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
  63.                     $ip = $xip;
  64.                     break;
  65.                 }
  66.             }
  67.         } elseif (isset($_SERVER['HTTP_CLIENT_IP']) && $this->validateIP($_SERVER['HTTP_CLIENT_IP'])) {
  68.             $ip = $_SERVER['HTTP_CLIENT_IP'];
  69.         } elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP']) && $this->validateIP($_SERVER['HTTP_CF_CONNECTING_IP'])) {
  70.             $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
  71.         } elseif (isset($_SERVER['HTTP_X_REAL_IP']) && $this->validateIP($_SERVER['HTTP_X_REAL_IP'])) {
  72.             $ip = $_SERVER['HTTP_X_REAL_IP'];
  73.         }
  74.  
  75.         if (isset($_GET['ip'])
  76.             && filter_var($_GET['ip'], FILTER_VALIDATE_IP)
  77.             && $this->grav['config']->get('system.debugger.enabled')) {
  78.             $ip = $_GET['ip'];
  79.         }
  80.  
  81.         return $this->ip = $ip;
  82.     }
  83.  
  84.     /**
  85.      * Set a log directory for doLog (when not using grav)
  86.      *
  87.      * @param string $directory
  88.      */
  89.     public function setLogDirectory(string $directory)
  90.     {
  91.         if (!file_exists($directory) && !is_dir($directory)) {
  92.             mkdir($directory, 0775);
  93.         }
  94.  
  95.         $this->logDirectory = rtrim($directory, '/');
  96.     }
  97.  
  98.     /**
  99.      * Set the logfile (when not using grav)
  100.      * Directory will be added automatically
  101.      *
  102.      * @param string $filename
  103.      */
  104.     public function setLogFile(string $filename)
  105.     {
  106.         $this->logFile = $this->logDirectory . '/' . $filename;
  107.     }
  108.  
  109.     /**
  110.      * Get log filename
  111.      *
  112.      * @return string
  113.      */
  114.     public function getLogFile(): string
  115.     {
  116.         return $this->logFile;
  117.     }
  118.  
  119.     /**
  120.      * Get translation from language.yml
  121.      *
  122.      * @param string $tag The tag to look for. Automatically prefixed with PLUGIN_FIREWALL
  123.      * @param mixed $args Arguments for sprintf replaces
  124.      *
  125.      * @return string
  126.      */
  127.     private function getTranslation($tag, ...$args): string
  128.     {
  129.         if ($this->grav === false) {
  130.             if (isset($this->translation[$tag])) {
  131.                 $tag = $this->translation[$tag];
  132.             }
  133.  
  134.             return sprintf($tag, ...$args);
  135.         }
  136.  
  137.         if (count($args) == 0) {
  138.             $args = [];
  139.         }
  140.  
  141.         return sprintf($this->grav['language']->translate([ 'PLUGIN_FIREWALL.' . $tag ], ['en'], true), ...$args);
  142.     }
  143.  
  144.     /**
  145.      * Do logging for Grav
  146.      *
  147.      * @param string $format  Useable like sprintf
  148.      * @param string $type    Default is set lo 'debug', can be notice, warning, error, alert or debug
  149.      * @param mixed  $args    Arguments for sprints
  150.      *
  151.      * @return string formatted message
  152.      */
  153.     public function doLog($format, $type = 5, ...$args): string
  154.     {
  155.         if (strtoupper($_SERVER['REQUEST_METHOD']) == 'HEAD' || $this->noLog()) {
  156.             return '';
  157.         }
  158.  
  159.         $message = count($args) > 0 ? sprintf($format, ...$args) : $format;
  160.         $message = '[' . $this->ip . '] [clfl] ' . $message;
  161.  
  162.         if ($this->grav === false) {
  163.             if ($this->logFile !== null) {
  164.                 $mType = is_int($type) ? $this->types[$type] : strtoupper($type);
  165.                 $doLog = sprintf(
  166.                     '[%s] %s %s',
  167.                     date('Y-m-d H:i:s'),
  168.                     $mType,
  169.                     $message . PHP_EOL
  170.                 );
  171.                 file_put_contents($this->logFile, $doLog, FILE_APPEND | LOCK_EX);
  172.             }
  173.  
  174.             return $message;
  175.         }
  176.  
  177.         $loglevel = $this->config->get('plugins.firewall.loglevel');
  178.         switch ($type) {
  179.             case static::NOTICE:
  180.                 $this->grav['log']->notice($message);
  181.                 break;
  182.  
  183.             case static::WARNING:
  184.                 $this->grav['log']->warning($message);
  185.                 break;
  186.  
  187.             case static::ERROR:
  188.                 $this->grav['log']->error($message);
  189.                 break;
  190.  
  191.             case static::ALERT:
  192.                 $this->grav['log']->alert($message);
  193.                 break;
  194.  
  195.             case static::DEBUG:
  196.             default:
  197.                 if (strtolower($loglevel) == 'debug') {
  198.                     $this->grav['log']->debug($message);
  199.                 }
  200.                 break;
  201.         }
  202.  
  203.         return $message;
  204.     }
  205.  
  206.     /**
  207.      * Check if a given ip is in a network
  208.      *
  209.      * @param string $ip    IP to check
  210.      * @param string $range IP/CIDR netmask
  211.      *
  212.      * @return boolean true if the ip is in this range / false if not.
  213.      */
  214.     public function ipInRange($ip, $range)
  215.     {
  216.         $this->doLog(
  217.             'Check if %s is in range of %s',
  218.             static::DEBUG,
  219.             $ip,
  220.             $range
  221.         );
  222.  
  223.         if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
  224.             && filter_var($range, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  225.             return $this->ip6InRange($ip, $range);
  226.         }
  227.  
  228.         if (strpos($range, '/') == false) {
  229.             $range .= '/32';
  230.         }
  231.         // $range is in IP/CIDR format eg 127.0.0.1/24
  232.         list($range, $netmask) = explode('/', $range, 2);
  233.         $range_decimal         = ip2long($range);
  234.         $ip_decimal            = ip2long($ip);
  235.         $wildcard_decimal      = pow(2, (32 - trim($netmask))) - 1;
  236.         $netmask_decimal       = ~$wildcard_decimal;
  237.  
  238.         return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
  239.     }
  240.  
  241.     /**
  242.      * Check if a given ip is in a network
  243.      *
  244.      * @param string $ip    IP to check in IPV6 format
  245.      * @param string $range IP/CIDR netmask
  246.      *
  247.      * @return boolean true if the ip is in this range / false if not.
  248.      */
  249.     public function ip6InRange($ip, $range)
  250.     {
  251.         $ip = inet_pton($ip);
  252.         $binaryip = $this->inetToBits($ip);
  253.  
  254.         if (strpos($range, '/') == false) {
  255.             $range .= '/128';
  256.         }
  257.  
  258.         list($net, $maskbits) = explode('/', $range);
  259.         $net                  = inet_pton($net);
  260.         $binarynet            = $this->inetToBits($net);
  261.  
  262.         $ip_net_bits = substr($binaryip, 0, $maskbits);
  263.         $net_bits    = substr($binarynet, 0, $maskbits);
  264.  
  265.         return ($ip_net_bits == $net_bits);
  266.     }
  267.  
  268.     /**
  269.      * Convert inet to bits
  270.      *
  271.      * @param string $inet inet address
  272.      *
  273.      * @return string $binaryip
  274.      */
  275.     public function inetToBits($inet)
  276.     {
  277.         $unpacked = unpack('A16', $inet);
  278.         $unpacked = str_split($unpacked[1]);
  279.         $binaryip = '';
  280.         foreach ($unpacked as $char) {
  281.             $binaryip .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
  282.         }
  283.  
  284.         return $binaryip;
  285.     }
  286.  
  287.     /**
  288.      * Validate an IP address
  289.      *
  290.      * @param string $ip The IP to check
  291.      *
  292.      * @return boolean
  293.      */
  294.     public function validateIP($ip)
  295.     {
  296.         return inet_pton($ip) !== false;
  297.     }
  298.  
  299.     /**
  300.      * Set the cache folder
  301.      */
  302.     public function setCacheFolder($value = false)
  303.     {
  304.         $this->cacheFolder = $value;
  305.     }
  306.  
  307.     /**
  308.      * Set the IP to check
  309.      */
  310.     public function setIp($value = false)
  311.     {
  312.         $this->ip = $value;
  313.     }
  314.  
  315.     /**
  316.      * Merge given array with defaults
  317.      *
  318.      * @param array $noLogs
  319.      * @return array
  320.      */
  321.     public function setNoLogIP(array $noLogs): array
  322.     {
  323.         return $this->noLogIP = array_unique(
  324.             array_merge(
  325.                 $this->noLogIP,
  326.                 $noLogs
  327.             )
  328.         );
  329.     }
  330.  
  331.     /**
  332.      * Do checks for logging or not
  333.      *
  334.      * @return bool
  335.      */
  336.     public function noLog($asn = false): bool
  337.     {
  338.         if (in_array($this->ip, $this->noLogIP)) {
  339.             return true;
  340.         }
  341.  
  342.         $path_parts = pathinfo($_SERVER['REQUEST_URI']);
  343.         if (isset($path_parts['extension']) && strtolower($path_parts['extension']) == 'rss') {
  344.             return true;
  345.         }
  346.  
  347.         return false;
  348.     }
  349.  
  350.     /**
  351.      * Fetch IP information from Cloudflare API
  352.      *
  353.      * @return array|false
  354.      */
  355.     public function getDataFromCF()
  356.     {
  357.         $cache_filename = $cache_file = '';
  358.         if ($this->cacheFolder != null) {
  359.             $cache_filename = 'cloudflare_ips.serial';
  360.             $cache_file     = $this->cacheFolder . '/' . $cache_filename;
  361.             $cachetime      = $this->cachetime * 86400;
  362.  
  363.             if ($this->grav !== false) {
  364.                 $cachetime = (int)$this->grav['config']->get(
  365.                     'plugins.firewall.proxycheck.cachetime'
  366.                 ) * 86400;
  367.             }
  368.  
  369.             if (file_exists($cache_file) && (filemtime($cache_file) + $cachetime) > time()) {
  370.                 $this->cached = true;
  371.                 $this->result = unserialize(file_get_contents($cache_file));
  372.                 $this->doLog(
  373.                     'Cache expires: %s (%ds).',
  374.                     static::DEBUG,
  375.                     date('Y-m-d H:i:s', filemtime($cache_file) + $cachetime),
  376.                     (filemtime($cache_file) + $cachetime) - time()
  377.                 );
  378.                 return $this->result;
  379.             }
  380.         }
  381.  
  382.         return $this->doCurl($cache_file, $cache_filename);
  383.     }
  384.  
  385.     /**
  386.      * Do the curl
  387.      *
  388.      * @param string $cache_file
  389.      * @param string $cache_filename
  390.      *
  391.      * @return array
  392.      */
  393.     public function doCurl($cache_file, $cache_filename = '')
  394.     {
  395.         $options = [
  396.             CURLOPT_URL            => static::BASE_API_URL,
  397.             CURLOPT_RETURNTRANSFER => 1,
  398.             CURLOPT_CONNECTTIMEOUT => 5,
  399.             CURLOPT_FOLLOWLOCATION => 1,
  400.             CURLOPT_USERAGENT      => $this->user_agent,
  401.             CURLOPT_HTTPHEADER     => $this->header,
  402.             CURLOPT_HEADER         => false,
  403.             CURLOPT_AUTOREFERER    => true,
  404.             CURLOPT_REFERER        => $this->referer,
  405.             CURLOPT_ENCODING       => 'gzip,deflate',
  406.         ];
  407.  
  408.         $curl = curl_init();
  409.         curl_setopt_array($curl, $options);
  410.         $result       = curl_exec($curl);
  411.         $data         = json_decode($result, true);
  412.         $data['curl'] = curl_getinfo($curl);
  413.         curl_close($curl);
  414.  
  415.         if ((isset($data['success']) && $data['success'] == true) && $this->cacheFolder != null) {
  416.             $logMsg = 'Wrote new cache file %s';
  417.             if (file_exists($cache_file)) {
  418.                 $logMsg = 'Refreshed cache file %s';
  419.             }
  420.  
  421.             $this->doLog($logMsg, static::DEBUG, $cache_filename);
  422.             file_put_contents($cache_file, serialize($data), LOCK_EX);
  423.             return $data;
  424.         }
  425.  
  426.         $logMsg = sprintf('Failed fetching data for %s from cloudflare', $this->ip);
  427.         $this->doLog($logMsg, static::ERROR);
  428.         return json_encode([
  429.             'status'  => 'error',
  430.             'message' => $logMsg
  431.         ]);
  432.     }
  433.  
  434.     public function isCloudflare()
  435.     {
  436.         $cfData = $this->getDataFromCF();
  437.  
  438.         if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
  439.             $ipAdresses = $cfData['result']['ipv4_cidrs'];
  440.         } elseif (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  441.             $ipAdresses = $cfData['result']['ipv6_cidrs'];
  442.         }
  443.  
  444.         $this->doLog(
  445.             'Cloudflare check for %s',
  446.             static::DEBUG,
  447.             $this->ip
  448.         );
  449.  
  450.         foreach ($ipAdresses as $value) {
  451.             $buffer = trim($value);
  452.             if (strlen(trim($buffer)) > 0
  453.                 && $this->ipInRange($this->ip, $buffer)
  454.             ) {
  455.                 return [
  456.                     'reason'      => 'Blocked due cloudflare match',
  457.                     'message'     => $this->getTranslation('CLOUDFLARE_BLOCK', $this->ip),
  458.                     'cloudflared' => true,
  459.                     'blocked'     => true
  460.                 ];
  461.                 break;
  462.             }
  463.         }
  464.  
  465.         return [
  466.             'reason'      => '',
  467.             'message'     => '',
  468.             'cloudflared' => false,
  469.             'blocked'     => false
  470.         ];
  471.     }
  472. }
  473.  

A PHP Error was encountered

Severity: Core Warning

Message: Module 'zip' already loaded

Filename: Unknown

Line Number: 0

Backtrace: