first commit
This commit is contained in:
124
README.md
Normal file
124
README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# BotLimiter - Smart Traffic Control & Firewall for PrestaShop
|
||||||
|
|
||||||
|
* **Version:** 1.0.0
|
||||||
|
* **Author:** Panariga
|
||||||
|
* **Compatibility:** PrestaShop 1.7 / 8.x / 9.x
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**BotLimiter** is a high-performance firewall designed specifically to stop "Faceted Search Scrapers" and aggressive bots that overload PrestaShop servers by iterating through filter combinations (e.g., `?q=Category-Food` or `?order=price.asc`).
|
||||||
|
|
||||||
|
Unlike standard CAPTCHAs that annoy users, BotLimiter uses a **"Cookie Trap"** mechanism. It detects heavy requests (filters/sorting) and seamlessly verifies the visitor using a lightweight JavaScript redirection flow. Real users pass instantly; dumb bots get stuck; aggressive scrapers are logged for **Fail2Ban**.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
1. **Request Interception:** Hooks into `actionFrontControllerInitBefore` to stop processing before the heavy Database/Theme loading starts.
|
||||||
|
2. **HEAD Request Block:** Instantly drops `HEAD` requests on filtered pages (a common bot tactic to check for 200 OK without downloading content).
|
||||||
|
3. **The "Trap" logic:**
|
||||||
|
* Redirects visitors without a valid session to a lightweight HTML/JS verification page.
|
||||||
|
* Generates an encrypted token based on the user's IP.
|
||||||
|
* Validates the token upon auto-submission.
|
||||||
|
4. **Extensible Rules:** Built on a generic `RuleInterface` architecture.
|
||||||
|
5. **Fail2Ban Integration:** Writes offending IPs to a dedicated log file compatible with server-side banning tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Upload the `botlimiter` folder to your `/modules/` directory.
|
||||||
|
2. Go to **Back Office > Modules > Module Manager**.
|
||||||
|
3. Find **Bot Limiter** and click **Install**.
|
||||||
|
4. Clear your PrestaShop cache (`Advanced Parameters > Performance`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The module uses the following flow:
|
||||||
|
|
||||||
|
1. **Hook:** `actionFrontControllerInitBefore`
|
||||||
|
2. **Rule Engine:** Checks if the request is "Heavy" (contains `q=` or `order=` parameters).
|
||||||
|
3. **Allowlist:** Passes `Googlebot`, `Bingbot`, and legitimate users with a verified session cookie.
|
||||||
|
4. **Verification:**
|
||||||
|
* If unverified, redirects to `module-botlimiter-verify`.
|
||||||
|
* This controller renders a minimal template (no heavy footer/header).
|
||||||
|
* Javascript automatically appends an encrypted token and redirects back to the original URL.
|
||||||
|
5. **Validation:** The module decrypts the token. If it matches the IP, a session cookie is set, and the user can browse freely.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fail2Ban Configuration (Virtualmin / Universal)
|
||||||
|
|
||||||
|
The module writes logs to:
|
||||||
|
`/your_shop_root/var/logs/botlimiter_ban.log`
|
||||||
|
|
||||||
|
The format is:
|
||||||
|
`[YYYY-MM-DD HH:MM:SS] [IP:192.168.1.1] [REASON:HEAD_REQUEST_SPAM]`
|
||||||
|
|
||||||
|
### 1. Create the Filter
|
||||||
|
|
||||||
|
Create a new file `/etc/fail2ban/filter.d/prestashop-bot.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Definition]
|
||||||
|
# Regex to match the custom log format from BotLimiter
|
||||||
|
failregex = ^\[.*\] \[IP:<HOST>\] \[REASON:.*\]$
|
||||||
|
|
||||||
|
# Ignore nothing
|
||||||
|
ignoreregex =
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the Jail (Virtualmin Universal Rule)
|
||||||
|
|
||||||
|
We need a rule that scans **all** virtual servers on your machine. Virtualmin typically stores web roots in `/home/USERNAME/public_html` or `/home/USERNAME/domains/DOMAIN/public_html`.
|
||||||
|
|
||||||
|
Add this to your `/etc/fail2ban/jail.local` (or `jail.conf`):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[prestashop-bot]
|
||||||
|
enabled = true
|
||||||
|
port = http,https
|
||||||
|
filter = prestashop-bot
|
||||||
|
# Multi-path configuration to scan all Virtualmin domains
|
||||||
|
logpath = /home/*/public_html/var/logs/botlimiter_ban.log
|
||||||
|
/home/*/domains/*/public_html/var/logs/botlimiter_ban.log
|
||||||
|
|
||||||
|
# OPTIONS:
|
||||||
|
# maxretry: Number of times a bot can hit the trap before ban
|
||||||
|
maxretry = 5
|
||||||
|
|
||||||
|
# findtime: Time window (seconds) to count the retries (10 minutes)
|
||||||
|
findtime = 600
|
||||||
|
|
||||||
|
# bantime: How long to ban them (1 hour)
|
||||||
|
bantime = 3600
|
||||||
|
|
||||||
|
# Backend: standard "auto" or "pyinotify" works best for wildcards
|
||||||
|
backend = auto
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart Fail2Ban
|
||||||
|
|
||||||
|
```bash
|
||||||
|
service fail2ban restart
|
||||||
|
# OR
|
||||||
|
systemctl restart fail2ban
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verify It Works
|
||||||
|
|
||||||
|
To check if Fail2Ban is correctly monitoring the files across your Virtualmin domains:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fail2ban-client status prestashop-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see a list of "File list" showing the log paths it found across your home directories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
* **Users complain about loops:** Ensure JavaScript is enabled. The verification controller relies on JS to append the token.
|
||||||
|
* **Whitelisting:** If you have a specific monitoring service (like UptimeRobot), ensure you add their User-Agent to `classes/rules/FilterTrapRule.php`.
|
||||||
|
```
|
||||||
56
botlimiter.php
Normal file
56
botlimiter.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* BotLimiter - Smart Traffic Control
|
||||||
|
* @author Panariga
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('_PS_VERSION_')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__FILE__) . '/classes/RuleManager.php';
|
||||||
|
require_once dirname(__FILE__) . '/classes/BotLogger.php';
|
||||||
|
|
||||||
|
class BotLimiter extends Module
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->name = 'botlimiter';
|
||||||
|
$this->tab = 'administration';
|
||||||
|
$this->version = '1.0.0';
|
||||||
|
$this->author = 'Panariga';
|
||||||
|
$this->need_instance = 0;
|
||||||
|
$this->bootstrap = true;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->displayName = $this->l('Bot Limiter & Firewall');
|
||||||
|
$this->description = $this->l('Intelligent protection against faceted search scrapers.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
return parent::install() &&
|
||||||
|
$this->registerHook('actionFrontControllerInitBefore');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hookActionFrontControllerInitBefore($params)
|
||||||
|
{
|
||||||
|
// 1. Skip if we are currently ON the verification page to avoid loops
|
||||||
|
if ($this->context->controller instanceof BotLimiterVerifyModuleFrontController) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Initialize Manager
|
||||||
|
$manager = new RuleManager($this->context);
|
||||||
|
|
||||||
|
// 3. Register Rules (Add more here in future)
|
||||||
|
$manager->addRule(new HeadRequestRule());
|
||||||
|
$manager->addRule(new FilterTrapRule());
|
||||||
|
|
||||||
|
// 4. Execute Rules
|
||||||
|
// If a rule returns FALSE, it means traffic is blocked or redirected.
|
||||||
|
// The rule is responsible for the redirect/exit.
|
||||||
|
$manager->process();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
classes/BotLogger.php
Normal file
15
classes/BotLogger.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class BotLogger
|
||||||
|
{
|
||||||
|
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/botlimiter_ban.log';
|
||||||
|
|
||||||
|
public static function logBan($ip, $reason)
|
||||||
|
{
|
||||||
|
$date = date('Y-m-d H:i:s');
|
||||||
|
$message = sprintf("[%s] [IP:%s] [REASON:%s]" . PHP_EOL, $date, $ip, $reason);
|
||||||
|
|
||||||
|
// Append to log file
|
||||||
|
file_put_contents(self::LOG_FILE, $message, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
classes/RuleManager.php
Normal file
27
classes/RuleManager.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(__FILE__) . '/rules/RuleInterface.php';
|
||||||
|
require_once dirname(__FILE__) . '/rules/HeadRequestRule.php';
|
||||||
|
require_once dirname(__FILE__) . '/rules/FilterTrapRule.php';
|
||||||
|
|
||||||
|
class RuleManager
|
||||||
|
{
|
||||||
|
private $rules = [];
|
||||||
|
private $context;
|
||||||
|
|
||||||
|
public function __construct($context)
|
||||||
|
{
|
||||||
|
$this->context = $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRule(RuleInterface $rule)
|
||||||
|
{
|
||||||
|
$this->rules[] = $rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process()
|
||||||
|
{
|
||||||
|
foreach ($this->rules as $rule) {
|
||||||
|
$rule->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
classes/rules/FilterTrapRule.php
Normal file
69
classes/rules/FilterTrapRule.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
use PrestaShop\PrestaShop\Core\Crypto\PhpEncryption;
|
||||||
|
|
||||||
|
class FilterTrapRule implements RuleInterface
|
||||||
|
{
|
||||||
|
public function execute()
|
||||||
|
{
|
||||||
|
// 1. Only analyze heavy requests (filters/sorting)
|
||||||
|
if (!Tools::getIsset('q') && !Tools::getIsset('order')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = Context::getContext();
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
|
|
||||||
|
// 2. Allow whitelisted Bots (Google/Bing)
|
||||||
|
// We trust them not to spam. If they do, use robots.txt.
|
||||||
|
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : '';
|
||||||
|
if (strpos($ua, 'googlebot') !== false || strpos($ua, 'bingbot') !== false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check if user is already verified in this session
|
||||||
|
if (isset($context->cookie->bot_verified) && $context->cookie->bot_verified == 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check for Incoming Token (Returning from Verify Controller)
|
||||||
|
$incoming_token = Tools::getValue('bot_token');
|
||||||
|
if ($incoming_token) {
|
||||||
|
try {
|
||||||
|
// Verify Token
|
||||||
|
$encryption = new PhpEncryption(_NEW_COOKIE_KEY_);
|
||||||
|
$decrypted_ip = $encryption->decrypt($incoming_token);
|
||||||
|
|
||||||
|
if ($decrypted_ip === $ip) {
|
||||||
|
// SUCCESS: User ran JS, posted back, and IP matches.
|
||||||
|
$context->cookie->bot_verified = 1;
|
||||||
|
$context->cookie->write(); // Force save
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Token mismatch (stolen URL?)
|
||||||
|
BotLogger::logBan($ip, 'INVALID_TOKEN');
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
die('Security Token Mismatch');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Garbage token
|
||||||
|
BotLogger::logBan($ip, 'GARBAGE_TOKEN');
|
||||||
|
die('Access Denied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. If we are here: Heavy request + No Cookie + No Token.
|
||||||
|
// Redirect to the Trap (Verify Controller)
|
||||||
|
|
||||||
|
$current_url = $_SERVER['REQUEST_URI'];
|
||||||
|
// Remove existing bot_token if present to avoid loops
|
||||||
|
$current_url = preg_replace('/([?&])bot_token=[^&]+(&|$)/', '$1', $current_url);
|
||||||
|
|
||||||
|
// Encode target URL safely
|
||||||
|
$target = urlencode($current_url);
|
||||||
|
|
||||||
|
$link = $context->link->getModuleLink('botlimiter', 'verify', ['return_url' => $target]);
|
||||||
|
|
||||||
|
Tools::redirect($link);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
classes/rules/HeadRequestRule.php
Normal file
18
classes/rules/HeadRequestRule.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class HeadRequestRule implements RuleInterface
|
||||||
|
{
|
||||||
|
public function execute()
|
||||||
|
{
|
||||||
|
// Detect HEAD request with Filter parameters
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'HEAD' && (Tools::getIsset('q') || Tools::getIsset('order'))) {
|
||||||
|
|
||||||
|
// Log for Fail2Ban
|
||||||
|
BotLogger::logBan($_SERVER['REMOTE_ADDR'], 'HEAD_REQUEST_SPAM');
|
||||||
|
|
||||||
|
header('HTTP/1.1 405 Method Not Allowed');
|
||||||
|
die('Method Not Allowed');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
classes/rules/RuleInterface.php
Normal file
8
classes/rules/RuleInterface.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
interface RuleInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return bool|void Returns true if passed, calls exit/redirect if failed
|
||||||
|
*/
|
||||||
|
public function execute();
|
||||||
|
}
|
||||||
30
controllers/front/verify.php
Normal file
30
controllers/front/verify.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
use PrestaShop\PrestaShop\Core\Crypto\PhpEncryption;
|
||||||
|
|
||||||
|
class BotLimiterVerifyModuleFrontController extends ModuleFrontController
|
||||||
|
{
|
||||||
|
public function initContent()
|
||||||
|
{
|
||||||
|
parent::initContent(); // This initializes the Standard PS Cookie
|
||||||
|
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$return_url = urldecode(Tools::getValue('return_url'));
|
||||||
|
|
||||||
|
// Sanity check on return URL to prevent open redirect vulnerabilities
|
||||||
|
if (strpos($return_url, '/') !== 0) {
|
||||||
|
$return_url = Context::getContext()->shop->getBaseURL(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Encrypted Token
|
||||||
|
// Using IP ensures the token cannot be generated on one machine and used on another
|
||||||
|
$encryption = new PhpEncryption(_NEW_COOKIE_KEY_);
|
||||||
|
$token = $encryption->encrypt($ip);
|
||||||
|
|
||||||
|
$this->context->smarty->assign([
|
||||||
|
'return_url' => $return_url,
|
||||||
|
'bot_token' => $token,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->setTemplate('module:botlimiter/views/templates/front/verify.tpl');
|
||||||
|
}
|
||||||
|
}
|
||||||
65
views/templates/front/verify.tpl
Normal file
65
views/templates/front/verify.tpl
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{extends file='layouts/layout-content-only.tpl'}
|
||||||
|
|
||||||
|
{block name='content'}
|
||||||
|
<div class="bot-protection-container">
|
||||||
|
<div class="bot-protection-box">
|
||||||
|
<div class="icon-shield">🛡️</div>
|
||||||
|
|
||||||
|
<h3>{l s='Security Check' mod='botlimiter'}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{l s='We are checking your browser to ensure you are not a bot.' mod='botlimiter'}
|
||||||
|
<br>
|
||||||
|
{l s='This helps us keep prices low and the server fast for real humans.' mod='botlimiter'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form id="bot-form" action="{$return_url|escape:'html':'UTF-8'}" method="GET">
|
||||||
|
<button type="submit" class="btn btn-primary" id="manual-btn" style="display:none;">
|
||||||
|
{l s='I am human (Click to continue)' mod='botlimiter'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="loader">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>{l s='Verifying...' mod='botlimiter'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { background-color: #f4f4f4; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||||
|
.bot-protection-box { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; max-width: 400px; width: 100%; }
|
||||||
|
.icon-shield { font-size: 48px; margin-bottom: 20px; }
|
||||||
|
.btn-primary { background: #2fb5d2; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 20px; }
|
||||||
|
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #2fb5d2; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; }
|
||||||
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var targetUrl = "{$return_url|escape:'javascript':'UTF-8'}";
|
||||||
|
var separator = targetUrl.indexOf('?') !== -1 ? '&' : '?';
|
||||||
|
var finalUrl = targetUrl + separator + "bot_token=" + "{$bot_token|escape:'javascript':'UTF-8'}";
|
||||||
|
|
||||||
|
// 1. Auto-redirect for normal users (JS enabled)
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = finalUrl;
|
||||||
|
}, 800);
|
||||||
|
|
||||||
|
// 2. Show manual button after 3 seconds (in case redirect fails or human wants control)
|
||||||
|
setTimeout(function() {
|
||||||
|
document.getElementById('loader').style.display = 'none';
|
||||||
|
var btn = document.getElementById('manual-btn');
|
||||||
|
btn.style.display = 'inline-block';
|
||||||
|
// Update form action to include token for manual click
|
||||||
|
document.getElementById('bot-form').action = finalUrl;
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<style>#loader { display: none; }</style>
|
||||||
|
<div style="color: #d63031; border: 1px solid #d63031; padding: 10px; margin-top: 20px; border-radius: 4px;">
|
||||||
|
<strong>{l s='JavaScript is required.' mod='botlimiter'}</strong><br>
|
||||||
|
{l s='Please enable JavaScript or turn off your VPN/AdBlocker to access the site filters.' mod='botlimiter'}
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
{/block}
|
||||||
Reference in New Issue
Block a user