geoip_shop_manager.py aktualisiert
This commit is contained in:
@@ -8,9 +8,9 @@ Supports two geo regions:
|
|||||||
Supports three modes:
|
Supports three modes:
|
||||||
- php+crowdsec: GeoIP blocking with CrowdSec integration
|
- php+crowdsec: GeoIP blocking with CrowdSec integration
|
||||||
- php-only: GeoIP blocking without CrowdSec
|
- php-only: GeoIP blocking without CrowdSec
|
||||||
- bot-only: Block only bots, shop remains globally accessible
|
- bot-only: Rate-limit bots, shop remains globally accessible
|
||||||
|
|
||||||
v3.4.2: Fixed directory permissions for rate-limit (777 for PHP access)
|
v3.4.3: Fixed rate-limit logic - bots under limit are allowed through
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -56,8 +56,8 @@ DEFAULT_BAN_DURATION = 5 # Minuten
|
|||||||
|
|
||||||
# Minimum expected IP ranges per region (for validation)
|
# Minimum expected IP ranges per region (for validation)
|
||||||
MIN_RANGES = {
|
MIN_RANGES = {
|
||||||
"dach": 1000, # DE+AT+CH should have at least 1000 ranges
|
"dach": 1000,
|
||||||
"eurozone": 5000 # 22 countries should have at least 5000 ranges
|
"eurozone": 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -84,7 +84,7 @@ GEO_REGIONS = {
|
|||||||
"none": {
|
"none": {
|
||||||
"name": "Bot-Only",
|
"name": "Bot-Only",
|
||||||
"countries": [],
|
"countries": [],
|
||||||
"description": "Nur Bot-Blocking, weltweit erreichbar",
|
"description": "Nur Bot-Rate-Limiting, weltweit erreichbar",
|
||||||
"icon": "🤖",
|
"icon": "🤖",
|
||||||
"short": "BOT"
|
"short": "BOT"
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,6 @@ def generate_php_countries_array(geo_region):
|
|||||||
|
|
||||||
|
|
||||||
def generate_php_bot_patterns():
|
def generate_php_bot_patterns():
|
||||||
"""Generate PHP array of bot patterns"""
|
|
||||||
patterns = []
|
patterns = []
|
||||||
for bot_name, pattern in BOT_PATTERNS.items():
|
for bot_name, pattern in BOT_PATTERNS.items():
|
||||||
escaped_pattern = pattern.replace("'", "\\'")
|
escaped_pattern = pattern.replace("'", "\\'")
|
||||||
@@ -166,7 +165,6 @@ def generate_php_bot_patterns():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def generate_and_validate_cache(httpdocs_path, geo_region):
|
def generate_and_validate_cache(httpdocs_path, geo_region):
|
||||||
"""Generate the IP ranges cache and validate it. Returns (success, range_count, error_message)"""
|
|
||||||
cache_file = os.path.join(httpdocs_path, CACHE_FILE)
|
cache_file = os.path.join(httpdocs_path, CACHE_FILE)
|
||||||
region_info = get_geo_region_info(geo_region)
|
region_info = get_geo_region_info(geo_region)
|
||||||
countries = region_info["countries"]
|
countries = region_info["countries"]
|
||||||
@@ -218,7 +216,6 @@ if (count($ranges) >= {min_expected}) {{
|
|||||||
|
|
||||||
|
|
||||||
def validate_existing_cache(httpdocs_path, geo_region):
|
def validate_existing_cache(httpdocs_path, geo_region):
|
||||||
"""Validate existing cache. Returns (valid, range_count, error_message)"""
|
|
||||||
cache_file = os.path.join(httpdocs_path, CACHE_FILE)
|
cache_file = os.path.join(httpdocs_path, CACHE_FILE)
|
||||||
min_expected = MIN_RANGES.get(geo_region, 1000)
|
min_expected = MIN_RANGES.get(geo_region, 1000)
|
||||||
|
|
||||||
@@ -244,7 +241,7 @@ else {{ echo "OK:" . count($data); }}
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PHP TEMPLATES WITH FAIL-OPEN AND RATE-LIMITING
|
# PHP TEMPLATES - GEOIP
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
GEOIP_SCRIPT_TEMPLATE = '''<?php
|
GEOIP_SCRIPT_TEMPLATE = '''<?php
|
||||||
@@ -417,12 +414,18 @@ if (!$is_allowed) {{
|
|||||||
}}
|
}}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHP TEMPLATES - BOT RATE-LIMITING (FIXED: Bots under limit ALLOWED through)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
BOT_ONLY_SCRIPT_TEMPLATE = '''<?php
|
BOT_ONLY_SCRIPT_TEMPLATE = '''<?php
|
||||||
/**
|
/**
|
||||||
* Bot Blocking Script with Rate-Limiting - Worldwide Access
|
* Bot Rate-Limiting Script - Worldwide Access
|
||||||
* Valid until: {expiry_date}
|
* Valid until: {expiry_date}
|
||||||
* Blocks known bots/crawlers, allows all human traffic globally
|
* Rate-limits known bots/crawlers, allows all human traffic globally
|
||||||
* Rate-Limit: {rate_limit} req/min, Ban: {ban_duration} min
|
* IMPORTANT: Bots UNDER the limit are ALLOWED through!
|
||||||
|
* Only blocked when exceeding limit.
|
||||||
|
* Rate-Limit: {rate_limit} req/min, Ban: {ban_duration_min} min
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$expiry_date = strtotime('{expiry_timestamp}');
|
$expiry_date = strtotime('{expiry_timestamp}');
|
||||||
@@ -447,8 +450,8 @@ $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|||||||
$visitor_hash = md5($visitor_ip . '|' . $user_agent);
|
$visitor_hash = md5($visitor_ip . '|' . $user_agent);
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
if (!is_dir($bans_dir)) @mkdir($bans_dir, 0755, true);
|
if (!is_dir($bans_dir)) @mkdir($bans_dir, 0777, true);
|
||||||
if (!is_dir($counts_dir)) @mkdir($counts_dir, 0755, true);
|
if (!is_dir($counts_dir)) @mkdir($counts_dir, 0777, true);
|
||||||
|
|
||||||
// === STEP 1: Check if visitor is banned ===
|
// === STEP 1: Check if visitor is banned ===
|
||||||
$ban_file = "$bans_dir/$visitor_hash.ban";
|
$ban_file = "$bans_dir/$visitor_hash.ban";
|
||||||
@@ -479,10 +482,10 @@ foreach ($bot_patterns as $bot_name => $pattern) {{
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Not a bot - allow through
|
// Not a bot - allow through without any rate limiting
|
||||||
if ($detected_bot === null) return;
|
if ($detected_bot === null) return;
|
||||||
|
|
||||||
// === STEP 3: Rate-Limit Check ===
|
// === STEP 3: Rate-Limit Check for detected bot ===
|
||||||
$count_file = "$counts_dir/$visitor_hash.count";
|
$count_file = "$counts_dir/$visitor_hash.count";
|
||||||
$current_time = time();
|
$current_time = time();
|
||||||
$count = 1;
|
$count = 1;
|
||||||
@@ -519,54 +522,52 @@ if (file_exists($count_file)) {{
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
// === STEP 4: Check if limit exceeded ===
|
// === STEP 4: Check if limit exceeded ===
|
||||||
$is_banned = false;
|
|
||||||
if ($count > $rate_limit) {{
|
if ($count > $rate_limit) {{
|
||||||
// Create ban
|
// Create ban
|
||||||
$ban_until = $current_time + $ban_duration;
|
$ban_until = $current_time + $ban_duration;
|
||||||
@file_put_contents($ban_file, $ban_until, LOCK_EX);
|
@file_put_contents($ban_file, $ban_until, LOCK_EX);
|
||||||
$is_banned = true;
|
|
||||||
}}
|
|
||||||
|
|
||||||
// === STEP 5: Log and block ===
|
// Log the ban
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
||||||
|
|
||||||
if ($is_banned) {{
|
|
||||||
$ban_minutes = $ban_duration / 60;
|
$ban_minutes = $ban_duration / 60;
|
||||||
@file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX);
|
@file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX);
|
||||||
}} else {{
|
@file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX);
|
||||||
@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX);
|
|
||||||
|
// Block this request
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
header('Retry-After: ' . $ban_duration);
|
||||||
|
exit;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@file_put_contents($crowdsec_queue, "$timestamp|$visitor_ip|{shop_name}\\n", FILE_APPEND | LOCK_EX);
|
// === STEP 5: Under limit - log and ALLOW through ===
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX);
|
||||||
|
|
||||||
// === STEP 6: Probabilistic cleanup ===
|
// === STEP 6: Probabilistic cleanup ===
|
||||||
if (rand(1, $cleanup_probability) === 1) {{
|
if (rand(1, $cleanup_probability) === 1) {{
|
||||||
$now = time();
|
$now = time();
|
||||||
// Clean expired bans
|
|
||||||
foreach (glob("$bans_dir/*.ban") as $f) {{
|
foreach (glob("$bans_dir/*.ban") as $f) {{
|
||||||
$ban_time = (int)@file_get_contents($f);
|
$ban_time = (int)@file_get_contents($f);
|
||||||
if ($now > $ban_time) @unlink($f);
|
if ($now > $ban_time) @unlink($f);
|
||||||
}}
|
}}
|
||||||
// Clean old count files (older than 2x window)
|
|
||||||
foreach (glob("$counts_dir/*.count") as $f) {{
|
foreach (glob("$counts_dir/*.count") as $f) {{
|
||||||
if ($now - filemtime($f) > $window_size * 2) @unlink($f);
|
if ($now - filemtime($f) > $window_size * 2) @unlink($f);
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
header('HTTP/1.1 403 Forbidden');
|
// Bot is under rate limit - ALLOW through (no exit, no 403)
|
||||||
if ($is_banned) {{
|
return;
|
||||||
header('Retry-After: ' . $ban_duration);
|
|
||||||
}}
|
|
||||||
exit;
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = '''<?php
|
BOT_ONLY_SCRIPT_TEMPLATE_NO_CROWDSEC = '''<?php
|
||||||
/**
|
/**
|
||||||
* Bot Blocking Script with Rate-Limiting - Worldwide Access (No CrowdSec)
|
* Bot Rate-Limiting Script - Worldwide Access (No CrowdSec)
|
||||||
* Valid until: {expiry_date}
|
* Valid until: {expiry_date}
|
||||||
* Blocks known bots/crawlers, allows all human traffic globally
|
* Rate-limits known bots/crawlers, allows all human traffic globally
|
||||||
* Rate-Limit: {rate_limit} req/min, Ban: {ban_duration} min
|
* IMPORTANT: Bots UNDER the limit are ALLOWED through!
|
||||||
|
* Only blocked when exceeding limit.
|
||||||
|
* Rate-Limit: {rate_limit} req/min, Ban: {ban_duration_min} min
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$expiry_date = strtotime('{expiry_timestamp}');
|
$expiry_date = strtotime('{expiry_timestamp}');
|
||||||
@@ -590,20 +591,18 @@ $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|||||||
$visitor_hash = md5($visitor_ip . '|' . $user_agent);
|
$visitor_hash = md5($visitor_ip . '|' . $user_agent);
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
if (!is_dir($bans_dir)) @mkdir($bans_dir, 0755, true);
|
if (!is_dir($bans_dir)) @mkdir($bans_dir, 0777, true);
|
||||||
if (!is_dir($counts_dir)) @mkdir($counts_dir, 0755, true);
|
if (!is_dir($counts_dir)) @mkdir($counts_dir, 0777, true);
|
||||||
|
|
||||||
// === STEP 1: Check if visitor is banned ===
|
// === STEP 1: Check if visitor is banned ===
|
||||||
$ban_file = "$bans_dir/$visitor_hash.ban";
|
$ban_file = "$bans_dir/$visitor_hash.ban";
|
||||||
if (file_exists($ban_file)) {{
|
if (file_exists($ban_file)) {{
|
||||||
$ban_until = (int)@file_get_contents($ban_file);
|
$ban_until = (int)@file_get_contents($ban_file);
|
||||||
if (time() < $ban_until) {{
|
if (time() < $ban_until) {{
|
||||||
// Still banned - immediate 403
|
|
||||||
header('HTTP/1.1 403 Forbidden');
|
header('HTTP/1.1 403 Forbidden');
|
||||||
header('Retry-After: ' . ($ban_until - time()));
|
header('Retry-After: ' . ($ban_until - time()));
|
||||||
exit;
|
exit;
|
||||||
}}
|
}}
|
||||||
// Ban expired - remove file
|
|
||||||
@unlink($ban_file);
|
@unlink($ban_file);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -642,7 +641,6 @@ if (file_exists($count_file)) {{
|
|||||||
$count = (int)$parts[1];
|
$count = (int)$parts[1];
|
||||||
|
|
||||||
if ($current_time - $window_start > $window_size) {{
|
if ($current_time - $window_start > $window_size) {{
|
||||||
// New window
|
|
||||||
$window_start = $current_time;
|
$window_start = $current_time;
|
||||||
$count = 1;
|
$count = 1;
|
||||||
}} else {{
|
}} else {{
|
||||||
@@ -650,7 +648,6 @@ if (file_exists($count_file)) {{
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
ftruncate($fp, 0);
|
ftruncate($fp, 0);
|
||||||
rewind($fp);
|
rewind($fp);
|
||||||
fwrite($fp, "$window_start|$count");
|
fwrite($fp, "$window_start|$count");
|
||||||
@@ -662,44 +659,38 @@ if (file_exists($count_file)) {{
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
// === STEP 4: Check if limit exceeded ===
|
// === STEP 4: Check if limit exceeded ===
|
||||||
$is_banned = false;
|
|
||||||
if ($count > $rate_limit) {{
|
if ($count > $rate_limit) {{
|
||||||
// Create ban
|
|
||||||
$ban_until = $current_time + $ban_duration;
|
$ban_until = $current_time + $ban_duration;
|
||||||
@file_put_contents($ban_file, $ban_until, LOCK_EX);
|
@file_put_contents($ban_file, $ban_until, LOCK_EX);
|
||||||
$is_banned = true;
|
|
||||||
}}
|
|
||||||
|
|
||||||
// === STEP 5: Log and block ===
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
|
||||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
||||||
|
|
||||||
if ($is_banned) {{
|
|
||||||
$ban_minutes = $ban_duration / 60;
|
$ban_minutes = $ban_duration / 60;
|
||||||
@file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX);
|
@file_put_contents($log_file, "[$timestamp] BANNED: $detected_bot | IP: $visitor_ip | Exceeded $rate_limit req/min | Ban: {{$ban_minutes}}m | UA: $user_agent\\n", FILE_APPEND | LOCK_EX);
|
||||||
}} else {{
|
|
||||||
@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX);
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
header('Retry-After: ' . $ban_duration);
|
||||||
|
exit;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// === STEP 5: Under limit - log and ALLOW through ===
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
@file_put_contents($log_file, "[$timestamp] BOT: $detected_bot | IP: $visitor_ip | Count: $count/$rate_limit | URI: $uri\\n", FILE_APPEND | LOCK_EX);
|
||||||
|
|
||||||
// === STEP 6: Probabilistic cleanup ===
|
// === STEP 6: Probabilistic cleanup ===
|
||||||
if (rand(1, $cleanup_probability) === 1) {{
|
if (rand(1, $cleanup_probability) === 1) {{
|
||||||
$now = time();
|
$now = time();
|
||||||
// Clean expired bans
|
|
||||||
foreach (glob("$bans_dir/*.ban") as $f) {{
|
foreach (glob("$bans_dir/*.ban") as $f) {{
|
||||||
$ban_time = (int)@file_get_contents($f);
|
$ban_time = (int)@file_get_contents($f);
|
||||||
if ($now > $ban_time) @unlink($f);
|
if ($now > $ban_time) @unlink($f);
|
||||||
}}
|
}}
|
||||||
// Clean old count files (older than 2x window)
|
|
||||||
foreach (glob("$counts_dir/*.count") as $f) {{
|
foreach (glob("$counts_dir/*.count") as $f) {{
|
||||||
if ($now - filemtime($f) > $window_size * 2) @unlink($f);
|
if ($now - filemtime($f) > $window_size * 2) @unlink($f);
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
header('HTTP/1.1 403 Forbidden');
|
// Bot is under rate limit - ALLOW through (no exit, no 403)
|
||||||
if ($is_banned) {{
|
return;
|
||||||
header('Retry-After: ' . $ban_duration);
|
|
||||||
}}
|
|
||||||
exit;
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -731,7 +722,7 @@ def add_to_crowdsec(ip, shop):
|
|||||||
result = subprocess.run(['cscli', 'decisions', 'add', '--ip', ip, '--duration', '72h', '--type', 'ban', '--reason', f'GeoIP: blocked by {shop}'], capture_output=True, text=True, timeout=10)
|
result = subprocess.run(['cscli', 'decisions', 'add', '--ip', ip, '--duration', '72h', '--type', 'ban', '--reason', f'GeoIP: blocked by {shop}'], capture_output=True, text=True, timeout=10)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
PROCESSED_IPS[ip] = now
|
PROCESSED_IPS[ip] = now
|
||||||
log(f"✅ Added {ip} to CrowdSec (from {shop})")
|
log(f"Added {ip} to CrowdSec (from {shop})")
|
||||||
return True
|
return True
|
||||||
except: pass
|
except: pass
|
||||||
return False
|
return False
|
||||||
@@ -752,12 +743,11 @@ def process_queue_file(shop_path, shop):
|
|||||||
return processed
|
return processed
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
log("🚀 GeoIP CrowdSec Watcher started")
|
log("GeoIP CrowdSec Watcher started")
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
active_shops = get_active_shops()
|
active_shops = get_active_shops()
|
||||||
for shop, info in active_shops.items():
|
for shop, info in active_shops.items():
|
||||||
# Only process shops with CrowdSec enabled
|
|
||||||
mode = info.get('mode', 'php+crowdsec')
|
mode = info.get('mode', 'php+crowdsec')
|
||||||
if mode in ['php+crowdsec', 'bot+crowdsec']:
|
if mode in ['php+crowdsec', 'bot+crowdsec']:
|
||||||
shop_path = os.path.join(VHOSTS_DIR, shop)
|
shop_path = os.path.join(VHOSTS_DIR, shop)
|
||||||
@@ -875,7 +865,6 @@ def get_shop_geo_region(shop):
|
|||||||
|
|
||||||
|
|
||||||
def get_shop_rate_limit_config(shop):
|
def get_shop_rate_limit_config(shop):
|
||||||
"""Get rate limit configuration for a shop"""
|
|
||||||
if not os.path.isfile(ACTIVE_SHOPS_FILE):
|
if not os.path.isfile(ACTIVE_SHOPS_FILE):
|
||||||
return None, None
|
return None, None
|
||||||
try:
|
try:
|
||||||
@@ -965,11 +954,6 @@ def get_active_shops():
|
|||||||
return active
|
return active
|
||||||
|
|
||||||
|
|
||||||
def get_crowdsec_modes():
|
|
||||||
"""Return list of modes that use CrowdSec"""
|
|
||||||
return ['php+crowdsec', 'bot+crowdsec']
|
|
||||||
|
|
||||||
|
|
||||||
def select_geo_region():
|
def select_geo_region():
|
||||||
print(f"\n🌍 Wähle die Geo-Region:")
|
print(f"\n🌍 Wähle die Geo-Region:")
|
||||||
print(f" [1] {GEO_REGIONS['dach']['icon']} DACH - {GEO_REGIONS['dach']['description']}")
|
print(f" [1] {GEO_REGIONS['dach']['icon']} DACH - {GEO_REGIONS['dach']['description']}")
|
||||||
@@ -981,8 +965,8 @@ def select_mode():
|
|||||||
print(f"\n🔧 Wähle den Blocking-Modus:")
|
print(f"\n🔧 Wähle den Blocking-Modus:")
|
||||||
print(f" [1] 🌍 GeoIP + CrowdSec (IPs werden an CrowdSec gemeldet)")
|
print(f" [1] 🌍 GeoIP + CrowdSec (IPs werden an CrowdSec gemeldet)")
|
||||||
print(f" [2] 🌍 Nur GeoIP (keine CrowdSec-Synchronisation)")
|
print(f" [2] 🌍 Nur GeoIP (keine CrowdSec-Synchronisation)")
|
||||||
print(f" [3] 🤖 Nur Bot-Blocking (weltweit erreichbar, mit CrowdSec)")
|
print(f" [3] 🤖 Bot-Rate-Limiting (weltweit erreichbar, mit CrowdSec)")
|
||||||
print(f" [4] 🤖 Nur Bot-Blocking (weltweit erreichbar, ohne CrowdSec)")
|
print(f" [4] 🤖 Bot-Rate-Limiting (weltweit erreichbar, ohne CrowdSec)")
|
||||||
|
|
||||||
choice = input(f"\nModus wählen [1/2/3/4]: ").strip()
|
choice = input(f"\nModus wählen [1/2/3/4]: ").strip()
|
||||||
if choice == "2":
|
if choice == "2":
|
||||||
@@ -995,10 +979,8 @@ def select_mode():
|
|||||||
|
|
||||||
|
|
||||||
def select_rate_limit():
|
def select_rate_limit():
|
||||||
"""Ask user for rate-limit configuration. Returns (rate_limit, ban_duration_seconds)"""
|
|
||||||
print(f"\n🚦 Rate-Limit Konfiguration:")
|
print(f"\n🚦 Rate-Limit Konfiguration:")
|
||||||
|
|
||||||
# Rate limit
|
|
||||||
rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip()
|
rate_input = input(f" Requests pro Minute bevor Ban [{DEFAULT_RATE_LIMIT}]: ").strip()
|
||||||
try:
|
try:
|
||||||
rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT
|
rate_limit = int(rate_input) if rate_input else DEFAULT_RATE_LIMIT
|
||||||
@@ -1007,7 +989,6 @@ def select_rate_limit():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
rate_limit = DEFAULT_RATE_LIMIT
|
rate_limit = DEFAULT_RATE_LIMIT
|
||||||
|
|
||||||
# Ban duration
|
|
||||||
ban_input = input(f" Ban-Dauer in Minuten [{DEFAULT_BAN_DURATION}]: ").strip()
|
ban_input = input(f" Ban-Dauer in Minuten [{DEFAULT_BAN_DURATION}]: ").strip()
|
||||||
try:
|
try:
|
||||||
ban_minutes = int(ban_input) if ban_input else DEFAULT_BAN_DURATION
|
ban_minutes = int(ban_input) if ban_input else DEFAULT_BAN_DURATION
|
||||||
@@ -1025,7 +1006,6 @@ def select_rate_limit():
|
|||||||
|
|
||||||
|
|
||||||
def get_mode_icon(mode):
|
def get_mode_icon(mode):
|
||||||
"""Return icon for mode"""
|
|
||||||
icons = {
|
icons = {
|
||||||
'php+crowdsec': '🛡️',
|
'php+crowdsec': '🛡️',
|
||||||
'php-only': '📝',
|
'php-only': '📝',
|
||||||
@@ -1036,28 +1016,24 @@ def get_mode_icon(mode):
|
|||||||
|
|
||||||
|
|
||||||
def get_mode_description(mode):
|
def get_mode_description(mode):
|
||||||
"""Return description for mode"""
|
|
||||||
descriptions = {
|
descriptions = {
|
||||||
'php+crowdsec': 'GeoIP + CrowdSec',
|
'php+crowdsec': 'GeoIP + CrowdSec',
|
||||||
'php-only': 'Nur GeoIP',
|
'php-only': 'Nur GeoIP',
|
||||||
'bot+crowdsec': 'Bot-Block + CrowdSec',
|
'bot+crowdsec': 'Bot-Rate-Limit + CrowdSec',
|
||||||
'bot-only': 'Nur Bot-Block'
|
'bot-only': 'Nur Bot-Rate-Limit'
|
||||||
}
|
}
|
||||||
return descriptions.get(mode, mode)
|
return descriptions.get(mode, mode)
|
||||||
|
|
||||||
|
|
||||||
def is_bot_only_mode(mode):
|
def is_bot_only_mode(mode):
|
||||||
"""Check if mode is bot-only"""
|
|
||||||
return mode in ['bot-only', 'bot+crowdsec']
|
return mode in ['bot-only', 'bot+crowdsec']
|
||||||
|
|
||||||
|
|
||||||
def uses_crowdsec(mode):
|
def uses_crowdsec(mode):
|
||||||
"""Check if mode uses CrowdSec"""
|
|
||||||
return mode in ['php+crowdsec', 'bot+crowdsec']
|
return mode in ['php+crowdsec', 'bot+crowdsec']
|
||||||
|
|
||||||
|
|
||||||
def get_direct_shops(available_shops):
|
def get_direct_shops(available_shops):
|
||||||
"""Return list of shops that are NOT behind Link11 (direct exposure)"""
|
|
||||||
direct_shops = []
|
direct_shops = []
|
||||||
for shop in available_shops:
|
for shop in available_shops:
|
||||||
link11_info = check_link11(shop)
|
link11_info = check_link11(shop)
|
||||||
@@ -1067,7 +1043,6 @@ def get_direct_shops(available_shops):
|
|||||||
|
|
||||||
|
|
||||||
def get_link11_shops(available_shops):
|
def get_link11_shops(available_shops):
|
||||||
"""Return list of shops that ARE behind Link11"""
|
|
||||||
link11_shops = []
|
link11_shops = []
|
||||||
for shop in available_shops:
|
for shop in available_shops:
|
||||||
link11_info = check_link11(shop)
|
link11_info = check_link11(shop)
|
||||||
@@ -1110,7 +1085,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach"
|
|||||||
if not silent:
|
if not silent:
|
||||||
print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} für: {shop}")
|
print(f"\n🔧 Aktiviere {region_info['icon']} {region_info['name']} für: {shop}")
|
||||||
if is_bot_mode:
|
if is_bot_mode:
|
||||||
print(f" Modus: Nur Bot-Blocking (weltweit erreichbar)")
|
print(f" Modus: Bot-Rate-Limiting (weltweit erreichbar)")
|
||||||
if rate_limit and ban_duration:
|
if rate_limit and ban_duration:
|
||||||
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
else:
|
else:
|
||||||
@@ -1158,19 +1133,17 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach"
|
|||||||
|
|
||||||
# Select appropriate template
|
# Select appropriate template
|
||||||
if is_bot_mode:
|
if is_bot_mode:
|
||||||
# Create rate-limit directories with open permissions (PHP runs as different user)
|
# Create rate-limit directories with open permissions
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
||||||
# Ensure parent dir also has correct permissions
|
|
||||||
os.chmod(ratelimit_path, 0o777)
|
os.chmod(ratelimit_path, 0o777)
|
||||||
os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777)
|
os.chmod(os.path.join(ratelimit_path, 'bans'), 0o777)
|
||||||
os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777)
|
os.chmod(os.path.join(ratelimit_path, 'counts'), 0o777)
|
||||||
|
|
||||||
# Use defaults if not specified
|
|
||||||
if rate_limit is None:
|
if rate_limit is None:
|
||||||
rate_limit = DEFAULT_RATE_LIMIT
|
rate_limit = DEFAULT_RATE_LIMIT
|
||||||
if ban_duration is None:
|
if ban_duration is None:
|
||||||
ban_duration = DEFAULT_BAN_DURATION * 60 # Convert to seconds
|
ban_duration = DEFAULT_BAN_DURATION * 60
|
||||||
|
|
||||||
if uses_crowdsec(mode):
|
if uses_crowdsec(mode):
|
||||||
template = BOT_ONLY_SCRIPT_TEMPLATE
|
template = BOT_ONLY_SCRIPT_TEMPLATE
|
||||||
@@ -1186,7 +1159,8 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach"
|
|||||||
shop_name=shop,
|
shop_name=shop,
|
||||||
bot_patterns=generate_php_bot_patterns(),
|
bot_patterns=generate_php_bot_patterns(),
|
||||||
rate_limit=rate_limit,
|
rate_limit=rate_limit,
|
||||||
ban_duration=ban_duration
|
ban_duration=ban_duration,
|
||||||
|
ban_duration_min=ban_duration // 60
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
countries_array = generate_php_countries_array(geo_region)
|
countries_array = generate_php_countries_array(geo_region)
|
||||||
@@ -1248,6 +1222,7 @@ def activate_blocking(shop, silent=False, mode="php+crowdsec", geo_region="dach"
|
|||||||
else:
|
else:
|
||||||
print(f" 🤖 {len(BOT_PATTERNS)} Bot-Patterns aktiv")
|
print(f" 🤖 {len(BOT_PATTERNS)} Bot-Patterns aktiv")
|
||||||
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
|
print(f" ℹ️ Bots unter dem Limit werden durchgelassen")
|
||||||
print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}")
|
print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
@@ -1268,7 +1243,6 @@ def deactivate_blocking(shop, silent=False):
|
|||||||
print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} für: {shop}")
|
print(f"\n🔧 Deaktiviere {region_info['icon']} {region_info['name']} für: {shop}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
# Restore backup
|
|
||||||
if not silent:
|
if not silent:
|
||||||
print("\n[1/5] PHP-Blocking entfernen...")
|
print("\n[1/5] PHP-Blocking entfernen...")
|
||||||
|
|
||||||
@@ -1282,12 +1256,10 @@ def deactivate_blocking(shop, silent=False):
|
|||||||
with open(index_php, 'w') as f:
|
with open(index_php, 'w') as f:
|
||||||
f.write('\n'.join(lines))
|
f.write('\n'.join(lines))
|
||||||
|
|
||||||
# Remove files
|
|
||||||
for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE, CROWDSEC_QUEUE_FILE]]:
|
for f in [os.path.join(httpdocs, x) for x in [BLOCKING_FILE, CACHE_FILE, LOG_FILE, CROWDSEC_QUEUE_FILE]]:
|
||||||
if os.path.isfile(f):
|
if os.path.isfile(f):
|
||||||
os.remove(f)
|
os.remove(f)
|
||||||
|
|
||||||
# Remove rate-limit directory
|
|
||||||
if not silent:
|
if not silent:
|
||||||
print("\n[2/5] Rate-Limit Daten entfernen...")
|
print("\n[2/5] Rate-Limit Daten entfernen...")
|
||||||
if os.path.isdir(ratelimit_path):
|
if os.path.isdir(ratelimit_path):
|
||||||
@@ -1355,6 +1327,7 @@ def activate_all_shops():
|
|||||||
print(f" Region: {region_info['icon']} {region_info['name']}")
|
print(f" Region: {region_info['icon']} {region_info['name']}")
|
||||||
else:
|
else:
|
||||||
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
|
print(f" ℹ️ Bots unter dem Limit werden durchgelassen")
|
||||||
|
|
||||||
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
|
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
|
||||||
print("\n❌ Abgebrochen")
|
print("\n❌ Abgebrochen")
|
||||||
@@ -1403,7 +1376,6 @@ def activate_all_shops():
|
|||||||
expiry = datetime.now() + timedelta(hours=72)
|
expiry = datetime.now() + timedelta(hours=72)
|
||||||
|
|
||||||
if is_bot_mode:
|
if is_bot_mode:
|
||||||
# Create rate-limit directories with open permissions (PHP runs as different user)
|
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
||||||
os.chmod(ratelimit_path, 0o777)
|
os.chmod(ratelimit_path, 0o777)
|
||||||
@@ -1424,7 +1396,8 @@ def activate_all_shops():
|
|||||||
shop_name=shop,
|
shop_name=shop,
|
||||||
bot_patterns=generate_php_bot_patterns(),
|
bot_patterns=generate_php_bot_patterns(),
|
||||||
rate_limit=rate_limit,
|
rate_limit=rate_limit,
|
||||||
ban_duration=ban_duration
|
ban_duration=ban_duration,
|
||||||
|
ban_duration_min=ban_duration // 60
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY
|
template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY
|
||||||
@@ -1461,27 +1434,22 @@ def activate_all_shops():
|
|||||||
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
|
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
|
||||||
else:
|
else:
|
||||||
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
|
print(f" ℹ️ Bots unter dem Limit werden durchgelassen")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
|
|
||||||
def activate_direct_shops_only():
|
def activate_direct_shops_only():
|
||||||
"""Activate blocking only for shops that are NOT behind Link11 (direct exposure)"""
|
|
||||||
available_shops = [s for s in get_available_shops() if s not in get_active_shops()]
|
available_shops = [s for s in get_available_shops() if s not in get_active_shops()]
|
||||||
|
|
||||||
if not available_shops:
|
if not available_shops:
|
||||||
print("\n⚠️ Keine Shops zum Aktivieren verfügbar")
|
print("\n⚠️ Keine Shops zum Aktivieren verfügbar")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Filter: only shops NOT behind Link11
|
|
||||||
direct_shops = get_direct_shops(available_shops)
|
direct_shops = get_direct_shops(available_shops)
|
||||||
link11_shops = get_link11_shops(available_shops)
|
link11_shops = get_link11_shops(available_shops)
|
||||||
|
|
||||||
if not direct_shops:
|
if not direct_shops:
|
||||||
print("\n⚠️ Keine direkten Shops gefunden (alle sind hinter Link11)")
|
print("\n⚠️ Keine direkten Shops gefunden (alle sind hinter Link11)")
|
||||||
if link11_shops:
|
|
||||||
print(f"\n📋 {len(link11_shops)} Shop(s) hinter Link11 (werden übersprungen):")
|
|
||||||
for shop in link11_shops:
|
|
||||||
print(f" {COLOR_GREEN}• {shop} [Link11]{COLOR_RESET}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"\n{'=' * 60}")
|
print(f"\n{'=' * 60}")
|
||||||
@@ -1516,8 +1484,7 @@ def activate_direct_shops_only():
|
|||||||
print(f" Region: {region_info['icon']} {region_info['name']}")
|
print(f" Region: {region_info['icon']} {region_info['name']}")
|
||||||
else:
|
else:
|
||||||
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
print(f" Aktiviert: {len(direct_shops)} direkte Shop(s)")
|
print(f" ℹ️ Bots unter dem Limit werden durchgelassen")
|
||||||
print(f" Übersprungen: {len(link11_shops)} Link11-Shop(s)")
|
|
||||||
|
|
||||||
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
|
if input(f"\nFortfahren? (ja/nein): ").strip().lower() not in ['ja', 'j', 'yes', 'y']:
|
||||||
print("\n❌ Abgebrochen")
|
print("\n❌ Abgebrochen")
|
||||||
@@ -1566,7 +1533,6 @@ def activate_direct_shops_only():
|
|||||||
expiry = datetime.now() + timedelta(hours=72)
|
expiry = datetime.now() + timedelta(hours=72)
|
||||||
|
|
||||||
if is_bot_mode:
|
if is_bot_mode:
|
||||||
# Create rate-limit directories with open permissions (PHP runs as different user)
|
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'bans'), mode=0o777, exist_ok=True)
|
||||||
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
os.makedirs(os.path.join(ratelimit_path, 'counts'), mode=0o777, exist_ok=True)
|
||||||
os.chmod(ratelimit_path, 0o777)
|
os.chmod(ratelimit_path, 0o777)
|
||||||
@@ -1587,7 +1553,8 @@ def activate_direct_shops_only():
|
|||||||
shop_name=shop,
|
shop_name=shop,
|
||||||
bot_patterns=generate_php_bot_patterns(),
|
bot_patterns=generate_php_bot_patterns(),
|
||||||
rate_limit=rate_limit,
|
rate_limit=rate_limit,
|
||||||
ban_duration=ban_duration
|
ban_duration=ban_duration,
|
||||||
|
ban_duration_min=ban_duration // 60
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY
|
template = GEOIP_SCRIPT_TEMPLATE if uses_crowdsec(mode) else GEOIP_SCRIPT_TEMPLATE_PHP_ONLY
|
||||||
@@ -1625,6 +1592,7 @@ def activate_direct_shops_only():
|
|||||||
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
|
print(f" 🛡️ Fail-Open bei Cache-Fehlern aktiv")
|
||||||
else:
|
else:
|
||||||
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f" 🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
|
print(f" ℹ️ Bots unter dem Limit werden durchgelassen")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
|
|
||||||
@@ -1663,7 +1631,6 @@ def deactivate_all_shops():
|
|||||||
if os.path.isfile(f):
|
if os.path.isfile(f):
|
||||||
os.remove(f)
|
os.remove(f)
|
||||||
|
|
||||||
# Remove rate-limit directory
|
|
||||||
if os.path.isdir(ratelimit_path):
|
if os.path.isdir(ratelimit_path):
|
||||||
shutil.rmtree(ratelimit_path)
|
shutil.rmtree(ratelimit_path)
|
||||||
|
|
||||||
@@ -1684,11 +1651,8 @@ def get_shop_log_stats(shop):
|
|||||||
ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR)
|
ratelimit_path = os.path.join(httpdocs, RATELIMIT_DIR)
|
||||||
php_blocks = 0
|
php_blocks = 0
|
||||||
ips = {}
|
ips = {}
|
||||||
bots = {} # Track bot statistics
|
bots = {}
|
||||||
bans = 0 # Track rate-limit bans
|
bans = 0
|
||||||
|
|
||||||
shop_mode = get_shop_mode(shop)
|
|
||||||
is_bot_mode = is_bot_only_mode(shop_mode)
|
|
||||||
|
|
||||||
if os.path.isfile(log_file):
|
if os.path.isfile(log_file):
|
||||||
with open(log_file, 'r') as f:
|
with open(log_file, 'r') as f:
|
||||||
@@ -1697,14 +1661,12 @@ def get_shop_log_stats(shop):
|
|||||||
ip, ua = None, 'Unknown'
|
ip, ua = None, 'Unknown'
|
||||||
detected_bot = None
|
detected_bot = None
|
||||||
|
|
||||||
# Check if this is a ban line
|
|
||||||
if 'BANNED: ' in line:
|
if 'BANNED: ' in line:
|
||||||
bans += 1
|
bans += 1
|
||||||
try:
|
try:
|
||||||
detected_bot = line.split('BANNED: ')[1].split(' |')[0].strip()
|
detected_bot = line.split('BANNED: ')[1].split(' |')[0].strip()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
# Parse bot-only format: BOT: botname | IP: ...
|
|
||||||
elif 'BOT: ' in line:
|
elif 'BOT: ' in line:
|
||||||
try:
|
try:
|
||||||
detected_bot = line.split('BOT: ')[1].split(' |')[0].strip()
|
detected_bot = line.split('BOT: ')[1].split(' |')[0].strip()
|
||||||
@@ -1726,14 +1688,11 @@ def get_shop_log_stats(shop):
|
|||||||
if ip not in ips:
|
if ip not in ips:
|
||||||
ips[ip] = {'count': 0, 'ua': ua, 'bot': None}
|
ips[ip] = {'count': 0, 'ua': ua, 'bot': None}
|
||||||
ips[ip]['count'] += 1
|
ips[ip]['count'] += 1
|
||||||
# Store bot name if detected (from BOT: or BANNED: in log)
|
|
||||||
if detected_bot and not ips[ip]['bot']:
|
if detected_bot and not ips[ip]['bot']:
|
||||||
ips[ip]['bot'] = detected_bot
|
ips[ip]['bot'] = detected_bot
|
||||||
# Fallback: try to detect from UA if we have one
|
|
||||||
if ua != 'Unknown' and ips[ip]['ua'] == 'Unknown':
|
if ua != 'Unknown' and ips[ip]['ua'] == 'Unknown':
|
||||||
ips[ip]['ua'] = ua
|
ips[ip]['ua'] = ua
|
||||||
|
|
||||||
# Track bot statistics
|
|
||||||
if detected_bot:
|
if detected_bot:
|
||||||
bots[detected_bot] = bots.get(detected_bot, 0) + 1
|
bots[detected_bot] = bots.get(detected_bot, 0) + 1
|
||||||
elif ua and ua != 'Unknown':
|
elif ua and ua != 'Unknown':
|
||||||
@@ -1741,7 +1700,6 @@ def get_shop_log_stats(shop):
|
|||||||
if bot_name != 'Unbekannt':
|
if bot_name != 'Unbekannt':
|
||||||
bots[bot_name] = bots.get(bot_name, 0) + 1
|
bots[bot_name] = bots.get(bot_name, 0) + 1
|
||||||
|
|
||||||
# Count active bans from file system
|
|
||||||
active_bans = 0
|
active_bans = 0
|
||||||
bans_dir = os.path.join(ratelimit_path, 'bans')
|
bans_dir = os.path.join(ratelimit_path, 'bans')
|
||||||
if os.path.isdir(bans_dir):
|
if os.path.isdir(bans_dir):
|
||||||
@@ -1779,7 +1737,7 @@ def show_logs(shop):
|
|||||||
print(f"📊 {shop} | {region_info['icon']} {region_info['name']} {get_mode_icon(shop_mode)}")
|
print(f"📊 {shop} | {region_info['icon']} {region_info['name']} {get_mode_icon(shop_mode)}")
|
||||||
print(f"{'═' * 70}")
|
print(f"{'═' * 70}")
|
||||||
print(f"⏱️ Laufzeit: {format_duration(runtime)}")
|
print(f"⏱️ Laufzeit: {format_duration(runtime)}")
|
||||||
print(f"📈 Blocks: {blocks} ({req_min:.1f} req/min)")
|
print(f"📈 Log-Einträge: {blocks} ({req_min:.1f} req/min)")
|
||||||
|
|
||||||
if not is_bot_mode:
|
if not is_bot_mode:
|
||||||
valid, count, err = validate_existing_cache(httpdocs, shop_geo)
|
valid, count, err = validate_existing_cache(httpdocs, shop_geo)
|
||||||
@@ -1791,7 +1749,6 @@ def show_logs(shop):
|
|||||||
print(f"🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
print(f"🚦 Rate-Limit: {rate_limit} req/min, Ban: {ban_duration // 60} min")
|
||||||
print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} aktiv")
|
print(f"🚫 Bans: {total_bans} ausgelöst, {active_bans} aktiv")
|
||||||
|
|
||||||
# Show bot statistics for bot-only mode
|
|
||||||
if bots:
|
if bots:
|
||||||
print(f"\n🤖 Bot-Statistik:")
|
print(f"\n🤖 Bot-Statistik:")
|
||||||
for bot_name, count in sorted(bots.items(), key=lambda x: x[1], reverse=True)[:10]:
|
for bot_name, count in sorted(bots.items(), key=lambda x: x[1], reverse=True)[:10]:
|
||||||
@@ -1799,7 +1756,7 @@ def show_logs(shop):
|
|||||||
print(f" {bot_name}: {count}x {bar}")
|
print(f" {bot_name}: {count}x {bar}")
|
||||||
|
|
||||||
if os.path.isfile(log_file):
|
if os.path.isfile(log_file):
|
||||||
print(f"\n📝 Letzte 30 Blocks:")
|
print(f"\n📝 Letzte 30 Log-Einträge:")
|
||||||
with open(log_file, 'r') as f:
|
with open(log_file, 'r') as f:
|
||||||
for line in f.readlines()[-30:]:
|
for line in f.readlines()[-30:]:
|
||||||
print(line.rstrip())
|
print(line.rstrip())
|
||||||
@@ -1807,13 +1764,11 @@ def show_logs(shop):
|
|||||||
if ips:
|
if ips:
|
||||||
print(f"\n🔥 Top 10 IPs:")
|
print(f"\n🔥 Top 10 IPs:")
|
||||||
for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]:
|
for ip, data in sorted(ips.items(), key=lambda x: x[1]['count'], reverse=True)[:10]:
|
||||||
# Use stored bot name first, fallback to UA detection
|
|
||||||
bot = data.get('bot') or detect_bot(data['ua'])
|
bot = data.get('bot') or detect_bot(data['ua'])
|
||||||
print(f" {ip} ({bot}): {data['count']}x")
|
print(f" {ip} ({bot}): {data['count']}x")
|
||||||
|
|
||||||
|
|
||||||
def show_all_logs():
|
def show_all_logs():
|
||||||
"""Show combined logs for all active shops"""
|
|
||||||
active_shops = get_active_shops()
|
active_shops = get_active_shops()
|
||||||
|
|
||||||
if not active_shops:
|
if not active_shops:
|
||||||
@@ -1828,19 +1783,17 @@ def show_all_logs():
|
|||||||
total_php_blocks = 0
|
total_php_blocks = 0
|
||||||
total_bans = 0
|
total_bans = 0
|
||||||
total_active_bans = 0
|
total_active_bans = 0
|
||||||
shop_php_stats = {} # shop -> {'blocks': N, 'runtime_minutes': float, 'req_min': float, 'ips': {}, 'bots': {}}
|
shop_php_stats = {}
|
||||||
all_ips = {} # ip -> {'count': N, 'ua': user_agent, 'shops': {shop: count}}
|
all_ips = {}
|
||||||
all_bots = {} # bot_name -> count
|
all_bots = {}
|
||||||
total_minutes = 0
|
total_minutes = 0
|
||||||
|
|
||||||
# Collect PHP stats
|
|
||||||
for shop in active_shops:
|
for shop in active_shops:
|
||||||
blocks, ips, bots, activation_time, bans, active_bans = get_shop_log_stats(shop)
|
blocks, ips, bots, activation_time, bans, active_bans = get_shop_log_stats(shop)
|
||||||
total_php_blocks += blocks
|
total_php_blocks += blocks
|
||||||
total_bans += bans
|
total_bans += bans
|
||||||
total_active_bans += active_bans
|
total_active_bans += active_bans
|
||||||
|
|
||||||
# Calculate runtime and req/min
|
|
||||||
if activation_time:
|
if activation_time:
|
||||||
runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60
|
runtime_minutes = (datetime.now() - activation_time).total_seconds() / 60
|
||||||
req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0
|
req_min = blocks / runtime_minutes if runtime_minutes > 0 else 0
|
||||||
@@ -1868,17 +1821,14 @@ def show_all_logs():
|
|||||||
all_ips[ip]['shops'][shop] = data['count']
|
all_ips[ip]['shops'][shop] = data['count']
|
||||||
if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown':
|
if data['ua'] != 'Unknown' and all_ips[ip]['ua'] == 'Unknown':
|
||||||
all_ips[ip]['ua'] = data['ua']
|
all_ips[ip]['ua'] = data['ua']
|
||||||
# Update bot name if we have one
|
|
||||||
if data.get('bot') and not all_ips[ip].get('bot'):
|
if data.get('bot') and not all_ips[ip].get('bot'):
|
||||||
all_ips[ip]['bot'] = data.get('bot')
|
all_ips[ip]['bot'] = data.get('bot')
|
||||||
|
|
||||||
for bot_name, count in bots.items():
|
for bot_name, count in bots.items():
|
||||||
all_bots[bot_name] = all_bots.get(bot_name, 0) + count
|
all_bots[bot_name] = all_bots.get(bot_name, 0) + count
|
||||||
|
|
||||||
# Calculate total req/min
|
|
||||||
total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0
|
total_req_min = total_php_blocks / total_minutes if total_minutes > 0 else 0
|
||||||
|
|
||||||
# Get CrowdSec stats
|
|
||||||
crowdsec_stats = {}
|
crowdsec_stats = {}
|
||||||
if check_crowdsec():
|
if check_crowdsec():
|
||||||
code, stdout, _ = run_command("cscli decisions list -o raw --limit 0")
|
code, stdout, _ = run_command("cscli decisions list -o raw --limit 0")
|
||||||
@@ -1890,8 +1840,7 @@ def show_all_logs():
|
|||||||
break
|
break
|
||||||
total_crowdsec = sum(crowdsec_stats.values())
|
total_crowdsec = sum(crowdsec_stats.values())
|
||||||
|
|
||||||
# Display PHP blocks with req/min and top IP per shop
|
print(f"\n📝 Log-Einträge gesamt: {total_php_blocks} (⌀ {total_req_min:.1f} req/min, Laufzeit: {format_duration(total_minutes)})")
|
||||||
print(f"\n📝 Blocks gesamt: {total_php_blocks} (⌀ {total_req_min:.1f} req/min, Laufzeit: {format_duration(total_minutes)})")
|
|
||||||
if shop_php_stats:
|
if shop_php_stats:
|
||||||
for shop in sorted(shop_php_stats.keys()):
|
for shop in sorted(shop_php_stats.keys()):
|
||||||
stats = shop_php_stats[shop]
|
stats = shop_php_stats[shop]
|
||||||
@@ -1901,13 +1850,11 @@ def show_all_logs():
|
|||||||
bar = "█" * min(int(req_min * 2), 20) if req_min > 0 else ""
|
bar = "█" * min(int(req_min * 2), 20) if req_min > 0 else ""
|
||||||
runtime_str = format_duration(runtime) if runtime > 0 else "?"
|
runtime_str = format_duration(runtime) if runtime > 0 else "?"
|
||||||
|
|
||||||
# Get geo region icon and mode icon
|
|
||||||
geo_region = get_shop_geo_region(shop)
|
geo_region = get_shop_geo_region(shop)
|
||||||
region_info = get_geo_region_info(geo_region)
|
region_info = get_geo_region_info(geo_region)
|
||||||
geo_icon = region_info['icon']
|
geo_icon = region_info['icon']
|
||||||
mode_icon = get_mode_icon(get_shop_mode(shop))
|
mode_icon = get_mode_icon(get_shop_mode(shop))
|
||||||
|
|
||||||
# Color shop name based on Link11 status
|
|
||||||
link11_info = check_link11(shop)
|
link11_info = check_link11(shop)
|
||||||
if link11_info['is_link11']:
|
if link11_info['is_link11']:
|
||||||
shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}"
|
shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}"
|
||||||
@@ -1916,14 +1863,12 @@ def show_all_logs():
|
|||||||
|
|
||||||
print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} ({req_min:.1f} req/min, seit {runtime_str}) {bar}")
|
print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} ({req_min:.1f} req/min, seit {runtime_str}) {bar}")
|
||||||
|
|
||||||
# Show top IP for this shop
|
|
||||||
shop_ips = stats['ips']
|
shop_ips = stats['ips']
|
||||||
if shop_ips and count > 0:
|
if shop_ips and count > 0:
|
||||||
top_ip = max(shop_ips.items(), key=lambda x: x[1]['count'])
|
top_ip = max(shop_ips.items(), key=lambda x: x[1]['count'])
|
||||||
top_ip_addr = top_ip[0]
|
top_ip_addr = top_ip[0]
|
||||||
top_ip_count = top_ip[1]['count']
|
top_ip_count = top_ip[1]['count']
|
||||||
top_ip_ua = top_ip[1]['ua']
|
top_ip_ua = top_ip[1]['ua']
|
||||||
# Use stored bot name first, fallback to UA detection
|
|
||||||
top_ip_bot = top_ip[1].get('bot') or detect_bot(top_ip_ua)
|
top_ip_bot = top_ip[1].get('bot') or detect_bot(top_ip_ua)
|
||||||
top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0
|
top_ip_req_min = top_ip_count / runtime if runtime > 0 else 0
|
||||||
|
|
||||||
@@ -1934,7 +1879,6 @@ def show_all_logs():
|
|||||||
|
|
||||||
print(f" │ └─➤ Top: {top_ip_addr} ({display_name}) - {top_ip_count}x, {top_ip_req_min:.1f} req/min")
|
print(f" │ └─➤ Top: {top_ip_addr} ({display_name}) - {top_ip_count}x, {top_ip_req_min:.1f} req/min")
|
||||||
|
|
||||||
# Display Rate-Limit Bans
|
|
||||||
if total_bans > 0 or total_active_bans > 0:
|
if total_bans > 0 or total_active_bans > 0:
|
||||||
print(f"\n🚫 Rate-Limit Bans: {total_bans} ausgelöst, {total_active_bans} aktiv")
|
print(f"\n🚫 Rate-Limit Bans: {total_bans} ausgelöst, {total_active_bans} aktiv")
|
||||||
for shop in sorted(shop_php_stats.keys()):
|
for shop in sorted(shop_php_stats.keys()):
|
||||||
@@ -1946,60 +1890,44 @@ def show_all_logs():
|
|||||||
else:
|
else:
|
||||||
shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}"
|
shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}"
|
||||||
|
|
||||||
geo_region = get_shop_geo_region(shop)
|
|
||||||
region_info = get_geo_region_info(geo_region)
|
|
||||||
geo_icon = region_info['icon']
|
|
||||||
mode_icon = get_mode_icon(get_shop_mode(shop))
|
|
||||||
|
|
||||||
bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else ""
|
bar = "█" * min(stats['bans'] // 2, 20) if stats['bans'] > 0 else ""
|
||||||
print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}")
|
print(f" ├─ {shop_colored}: {stats['bans']} bans ({stats['active_bans']} aktiv) {bar}")
|
||||||
|
|
||||||
# Display CrowdSec bans
|
|
||||||
print(f"\n🛡️ CrowdSec-Bans gesamt: {total_crowdsec}")
|
print(f"\n🛡️ CrowdSec-Bans gesamt: {total_crowdsec}")
|
||||||
if crowdsec_stats:
|
if crowdsec_stats:
|
||||||
for shop in sorted(crowdsec_stats.keys()):
|
for shop in sorted(crowdsec_stats.keys()):
|
||||||
count = crowdsec_stats[shop]
|
count = crowdsec_stats[shop]
|
||||||
bar = "█" * min(count // 10, 20) if count > 0 else ""
|
bar = "█" * min(count // 10, 20) if count > 0 else ""
|
||||||
|
|
||||||
geo_region = get_shop_geo_region(shop)
|
|
||||||
region_info = get_geo_region_info(geo_region)
|
|
||||||
geo_icon = region_info['icon']
|
|
||||||
mode_icon = get_mode_icon(get_shop_mode(shop))
|
|
||||||
|
|
||||||
link11_info = check_link11(shop)
|
link11_info = check_link11(shop)
|
||||||
if link11_info['is_link11']:
|
if link11_info['is_link11']:
|
||||||
shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}"
|
shop_colored = f"{COLOR_GREEN}{shop}{COLOR_RESET}"
|
||||||
else:
|
else:
|
||||||
shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}"
|
shop_colored = f"{COLOR_RED}{shop}{COLOR_RESET}"
|
||||||
|
|
||||||
print(f" ├─ {shop_colored} {geo_icon} {mode_icon}: {count} {bar}")
|
print(f" ├─ {shop_colored}: {count} {bar}")
|
||||||
elif check_crowdsec():
|
elif check_crowdsec():
|
||||||
print(" └─ Keine aktiven Bans")
|
print(" └─ Keine aktiven Bans")
|
||||||
else:
|
else:
|
||||||
print(" └─ CrowdSec nicht verfügbar")
|
print(" └─ CrowdSec nicht verfügbar")
|
||||||
|
|
||||||
# Display bot statistics
|
|
||||||
if all_bots:
|
if all_bots:
|
||||||
print(f"\n🤖 Bot-Statistik (alle Shops):")
|
print(f"\n🤖 Bot-Statistik (alle Shops):")
|
||||||
for bot_name, count in sorted(all_bots.items(), key=lambda x: x[1], reverse=True)[:15]:
|
for bot_name, count in sorted(all_bots.items(), key=lambda x: x[1], reverse=True)[:15]:
|
||||||
bar = "█" * min(count // 5, 20) if count > 0 else "█"
|
bar = "█" * min(count // 5, 20) if count > 0 else "█"
|
||||||
print(f" {bot_name}: {count}x {bar}")
|
print(f" {bot_name}: {count}x {bar}")
|
||||||
|
|
||||||
# Top 50 blocked IPs with bot detection, req/min, and top shop
|
|
||||||
if all_ips:
|
if all_ips:
|
||||||
print(f"\n🔥 Top 50 blockierte IPs (alle Shops):")
|
print(f"\n🔥 Top 50 IPs (alle Shops):")
|
||||||
sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:50]
|
sorted_ips = sorted(all_ips.items(), key=lambda x: x[1]['count'], reverse=True)[:50]
|
||||||
for ip, data in sorted_ips:
|
for ip, data in sorted_ips:
|
||||||
count = data['count']
|
count = data['count']
|
||||||
ua = data['ua']
|
ua = data['ua']
|
||||||
# Use stored bot name first, fallback to UA detection
|
|
||||||
bot_name = data.get('bot') or detect_bot(ua)
|
bot_name = data.get('bot') or detect_bot(ua)
|
||||||
shops_data = data['shops']
|
shops_data = data['shops']
|
||||||
|
|
||||||
# Calculate req/min for this IP
|
|
||||||
ip_req_min = count / total_minutes if total_minutes > 0 else 0
|
ip_req_min = count / total_minutes if total_minutes > 0 else 0
|
||||||
|
|
||||||
# Find top shop for this IP
|
|
||||||
if shops_data:
|
if shops_data:
|
||||||
top_shop = max(shops_data.items(), key=lambda x: x[1])
|
top_shop = max(shops_data.items(), key=lambda x: x[1])
|
||||||
top_shop_name = top_shop[0]
|
top_shop_name = top_shop[0]
|
||||||
@@ -2034,10 +1962,10 @@ def show_all_logs():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print(" GeoIP Shop Blocker Manager v3.4.2")
|
print(" GeoIP Shop Blocker Manager v3.4.3")
|
||||||
print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Only")
|
print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting")
|
||||||
print(" 🛡️ Mit Cache-Validierung und Fail-Open")
|
print(" 🛡️ Mit Cache-Validierung und Fail-Open")
|
||||||
print(" 🚦 Mit File-basiertem Rate-Limiting")
|
print(" 🚦 Bots unter Rate-Limit werden durchgelassen")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec")
|
print(f" {'✅' if check_crowdsec() else '⚠️ '} CrowdSec")
|
||||||
@@ -2146,7 +2074,7 @@ def main():
|
|||||||
rate_limit, ban_duration = get_shop_rate_limit_config(shop)
|
rate_limit, ban_duration = get_shop_rate_limit_config(shop)
|
||||||
rl_str = f", {rate_limit} req/min" if rate_limit else ""
|
rl_str = f", {rate_limit} req/min" if rate_limit else ""
|
||||||
ban_str = f", {active_bans} aktive Bans" if active_bans > 0 else ""
|
ban_str = f", {active_bans} aktive Bans" if active_bans > 0 else ""
|
||||||
print(f" {blocks} blocks, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns{rl_str}{ban_str}")
|
print(f" {blocks} log entries, {format_duration(runtime)}, {len(BOT_PATTERNS)} Bot-Patterns{rl_str}{ban_str}")
|
||||||
else:
|
else:
|
||||||
valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop))
|
valid, count, _ = validate_existing_cache(httpdocs, get_shop_geo_region(shop))
|
||||||
cache_str = f"✅{count:,}" if valid else "⚠️"
|
cache_str = f"✅{count:,}" if valid else "⚠️"
|
||||||
|
|||||||
Reference in New Issue
Block a user