<?php
namespace kromonos\firewall;
use kromonos\browser\Browser;
class KrautFlaer
{
private $cacheFolder = null;
private $grav = false;
private $config = null;
private $cachetime = 2;
private $ip = '127.0.0.1';
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/85.0';
private $referer = 'https://example.com';
private $noLogIP = [
];
private $header = [
'Accept: text/xml,application/xml,application/xhtml+xml,'
. 'text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
'Cache-Control: max-age=0',
'Connection: keep-alive',
'Keep-Alive: 300',
'Accept-Charset: utf-8;q=0.7,*;q=0.7',
'Accept-Language: en-us,en;q=0.5',
'Pragma: '
];
const BASE_API_URL = 'https://api.cloudflare.com/client/v4/ips';
const NOTICE = 1;
const WARNING = 2;
const ALERT = 3;
const ERROR = 4;
const DEBUG = 5;
public function __construct($grav)
{
if ($grav) {
$this->grav = $grav;
$this->config = $grav['config'];
}
include 'browser.php';
}
$this->browser = new Browser();
$this->setIp();
}
/**
* Return real ip address
*
* @return string
*/
public function realIP()
{
$ip = $_SERVER['REMOTE_ADDR'];
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])
&& preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
foreach ($matches[0] as $xip) {
if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) {
$ip = $xip;
break;
}
}
} elseif (isset($_SERVER['HTTP_CLIENT_IP']) && $this->validateIP($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP']) && $this->validateIP($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (isset($_SERVER['HTTP_X_REAL_IP']) && $this->validateIP($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
&& $this->grav['config']->get('system.debugger.enabled')) {
$ip = $_GET['ip'];
}
return $this->ip = $ip;
}
/**
* Set a log directory for doLog (when not using grav)
*
* @param string $directory
*/
public function setLogDirectory(string $directory)
{
}
$this->logDirectory = rtrim($directory, '/');
}
/**
* Set the logfile (when not using grav)
* Directory will be added automatically
*
* @param string $filename
*/
public function setLogFile(string $filename)
{
$this->logFile = $this->logDirectory . '/' . $filename;
}
/**
* Get log filename
*
* @return string
*/
public function getLogFile(): string
{
return $this->logFile;
}
/**
* Get translation from language.yml
*
* @param string $tag The tag to look for. Automatically prefixed with PLUGIN_FIREWALL
* @param mixed $args Arguments for sprintf replaces
*
* @return string
*/
private function getTranslation($tag, ...$args): string
{
if ($this->grav === false) {
if (isset($this->translation[$tag])) {
$tag = $this->translation[$tag];
}
}
$args = [];
}
return sprintf($this->grav['language']->translate([ 'PLUGIN_FIREWALL.' . $tag ], ['en'], true), ...$args);
}
/**
* Do logging for Grav
*
* @param string $format Useable like sprintf
* @param string $type Default is set lo 'debug', can be notice, warning, error, alert or debug
* @param mixed $args Arguments for sprints
*
* @return string formatted message
*/
public function doLog($format, $type = 5, ...$args): string
{
if (strtoupper($_SERVER['REQUEST_METHOD']) == 'HEAD' || $this->noLog()) {
return '';
}
$message = count($args) > 0 ?
sprintf($format, ...$args) : $format;
$message = '[' . $this->ip . '] [clfl] ' . $message;
if ($this->grav === false) {
if ($this->logFile !== null) {
'[%s] %s %s',
$mType,
$message . PHP_EOL
);
}
return $message;
}
$loglevel = $this->config->get('plugins.firewall.loglevel');
switch ($type) {
case static::NOTICE:
$this->grav['log']->notice($message);
break;
case static::WARNING:
$this->grav['log']->warning($message);
break;
case static::ERROR:
$this->grav['log']->error($message);
break;
case static::ALERT:
$this->grav['log']->alert($message);
break;
case static::DEBUG:
default:
$this->grav['log']->debug($message);
}
break;
}
return $message;
}
/**
* Check if a given ip is in a network
*
* @param string $ip IP to check
* @param string $range IP/CIDR netmask
*
* @return boolean true if the ip is in this range / false if not.
*/
public function ipInRange($ip, $range)
{
$this->doLog(
'Check if %s is in range of %s',
static::DEBUG,
$ip,
$range
);
if (filter_var($ip, FILTER_VALIDATE_IP
, FILTER_FLAG_IPV6
)
&& filter_var($range, FILTER_VALIDATE_IP
, FILTER_FLAG_IPV6
)) {
return $this->ip6InRange($ip, $range);
}
if (strpos($range, '/') == false) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
$wildcard_decimal = pow(2, (32 - trim($netmask))) - 1;
$netmask_decimal = ~$wildcard_decimal;
return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
}
/**
* Check if a given ip is in a network
*
* @param string $ip IP to check in IPV6 format
* @param string $range IP/CIDR netmask
*
* @return boolean true if the ip is in this range / false if not.
*/
public function ip6InRange($ip, $range)
{
$ip = inet_pton($ip);
$binaryip = $this->inetToBits($ip);
if (strpos($range, '/') == false) {
$range .= '/128';
}
$net = inet_pton($net);
$binarynet = $this->inetToBits($net);
$ip_net_bits = substr($binaryip, 0, $maskbits);
$net_bits = substr($binarynet, 0, $maskbits);
return ($ip_net_bits == $net_bits);
}
/**
* Convert inet to bits
*
* @param string $inet inet address
*
* @return string $binaryip
*/
public function inetToBits($inet)
{
$unpacked = unpack('A16', $inet);
$binaryip = '';
foreach ($unpacked as $char) {
}
return $binaryip;
}
/**
* Validate an IP address
*
* @param string $ip The IP to check
*
* @return boolean
*/
public function validateIP($ip)
{
return inet_pton($ip) !== false;
}
/**
* Set the cache folder
*/
public function setCacheFolder($value = false)
{
$this->cacheFolder = $value;
}
/**
* Set the IP to check
*/
public function setIp($value = false)
{
$this->ip = $value;
}
/**
* Merge given array with defaults
*
* @param array $noLogs
* @return array
*/
{
$this->noLogIP,
$noLogs
)
);
}
/**
* Do checks for logging or not
*
* @return bool
*/
public function noLog($asn = false): bool
{
if (in_array($this->ip, $this->noLogIP)) {
return true;
}
$path_parts = pathinfo($_SERVER['REQUEST_URI']);
if (isset($path_parts['extension']) && strtolower($path_parts['extension']) == 'rss') {
return true;
}
return false;
}
/**
* Fetch IP information from Cloudflare API
*
* @return array|false
*/
public function getDataFromCF()
{
$cache_filename = $cache_file = '';
if ($this->cacheFolder != null) {
$cache_filename = 'cloudflare_ips.serial';
$cache_file = $this->cacheFolder . '/' . $cache_filename;
$cachetime = $this->cachetime * 86400;
if ($this->grav !== false) {
$cachetime = (int)$this->grav['config']->get(
'plugins.firewall.proxycheck.cachetime'
) * 86400;
}
$this->cached = true;
$this->doLog(
'Cache expires: %s (%ds).',
static::DEBUG,
);
return $this->result;
}
}
return $this->doCurl($cache_file, $cache_filename);
}
/**
* Do the curl
*
* @param string $cache_file
* @param string $cache_filename
*
* @return array
*/
public function doCurl($cache_file, $cache_filename = '')
{
$options = [
CURLOPT_URL => static::BASE_API_URL,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_USERAGENT => $this->user_agent,
CURLOPT_HTTPHEADER
=> $this->header,
CURLOPT_HEADER => false,
CURLOPT_AUTOREFERER => true,
CURLOPT_REFERER => $this->referer,
CURLOPT_ENCODING => 'gzip,deflate',
];
if ((isset($data['success']) && $data['success'] == true) && $this->cacheFolder != null) {
$logMsg = 'Wrote new cache file %s';
$logMsg = 'Refreshed cache file %s';
}
$this->doLog($logMsg, static::DEBUG, $cache_filename);
return $data;
}
$logMsg = sprintf('Failed fetching data for %s from cloudflare', $this->ip);
$this->doLog($logMsg, static::ERROR);
'status' => 'error',
'message' => $logMsg
]);
}
public function isCloudflare()
{
$cfData = $this->getDataFromCF();
if (filter_var($this->ip, FILTER_VALIDATE_IP
, FILTER_FLAG_IPV4
)) {
$ipAdresses = $cfData['result']['ipv4_cidrs'];
} elseif (filter_var($this->ip, FILTER_VALIDATE_IP
, FILTER_FLAG_IPV6
)) {
$ipAdresses = $cfData['result']['ipv6_cidrs'];
}
$this->doLog(
'Cloudflare check for %s',
static::DEBUG,
$this->ip
);
foreach ($ipAdresses as $value) {
&& $this->ipInRange($this->ip, $buffer)
) {
return [
'reason' => 'Blocked due cloudflare match',
'message' => $this->getTranslation('CLOUDFLARE_BLOCK', $this->ip),
'cloudflared' => true,
'blocked' => true
];
break;
}
}
return [
'reason' => '',
'message' => '',
'cloudflared' => false,
'blocked' => false
];
}
}