geoip_shop_manager.py aktualisiert

This commit is contained in:
2025-12-17 10:56:01 +01:00
parent c21d62a27a
commit 7b681aab10

View File

@@ -8,6 +8,7 @@ Supports two modes:
- geoip: GeoIP blocking (only allowed regions can access) - geoip: GeoIP blocking (only allowed regions can access)
- bot: Rate-limit bots by bot-type, shop remains globally accessible - bot: Rate-limit bots by bot-type, shop remains globally accessible
v4.2.0: Korrekte Ownership für erstellte Dateien (nicht mehr root:root)
v4.1.0: IP-basierte Bot-Erkennung (für Bots die sich tarnen) v4.1.0: IP-basierte Bot-Erkennung (für Bots die sich tarnen)
v4.0.0: Bot-Rate-Limiting nach Bot-Typ (nicht IP), CrowdSec entfernt v4.0.0: Bot-Rate-Limiting nach Bot-Typ (nicht IP), CrowdSec entfernt
""" """
@@ -23,6 +24,7 @@ import socket
import ipaddress import ipaddress
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from collections import Counter
# ANSI Color Codes # ANSI Color Codes
COLOR_GREEN = "\033[92m" COLOR_GREEN = "\033[92m"
@@ -520,6 +522,70 @@ GENERIC_BOT_PATTERNS = [
] ]
# =============================================================================
# OWNERSHIP HELPER FUNCTIONS
# =============================================================================
def get_most_common_owner(httpdocs_path):
"""
Ermittelt die häufigste uid:gid-Kombination im httpdocs-Verzeichnis.
Gibt (uid, gid) zurück oder (None, None) wenn nicht ermittelbar.
"""
if not os.path.isdir(httpdocs_path):
return None, None
owner_counts = Counter()
try:
for entry in os.listdir(httpdocs_path):
entry_path = os.path.join(httpdocs_path, entry)
try:
stat_info = os.stat(entry_path)
owner_counts[(stat_info.st_uid, stat_info.st_gid)] += 1
except (OSError, IOError):
continue
except (OSError, IOError):
return None, None
if not owner_counts:
return None, None
# Häufigste Kombination zurückgeben
most_common = owner_counts.most_common(1)[0][0]
return most_common
def set_owner(path, uid, gid, recursive=False):
"""
Setzt Owner und Gruppe für eine Datei oder einen Ordner.
Optional rekursiv für Verzeichnisse.
"""
if uid is None or gid is None:
return
try:
os.chown(path, uid, gid)
if recursive and os.path.isdir(path):
for root, dirs, files in os.walk(path):
for d in dirs:
try:
os.chown(os.path.join(root, d), uid, gid)
except (OSError, IOError):
pass
for f in files:
try:
os.chown(os.path.join(root, f), uid, gid)
except (OSError, IOError):
pass
except (OSError, IOError):
pass
# =============================================================================
# EXISTING HELPER FUNCTIONS
# =============================================================================
def ip_in_cidr(ip_str, cidr_str): def ip_in_cidr(ip_str, cidr_str):
"""Prüft ob eine IP in einem CIDR-Netz liegt.""" """Prüft ob eine IP in einem CIDR-Netz liegt."""
try: try:
@@ -624,7 +690,7 @@ def generate_php_bot_ip_ranges():
# CACHE VALIDATION # CACHE VALIDATION
# ============================================================================= # =============================================================================
def generate_and_validate_cache(httpdocs_path, geo_region): def generate_and_validate_cache(httpdocs_path, geo_region, uid=None, gid=None):
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"]
@@ -662,6 +728,9 @@ if (count($ranges) >= {min_expected}) {{
result = subprocess.run(['php', temp_php], capture_output=True, text=True, timeout=120) result = subprocess.run(['php', temp_php], capture_output=True, text=True, timeout=120)
output = result.stdout.strip() output = result.stdout.strip()
if output.startswith('OK:'): if output.startswith('OK:'):
# Cache-Datei wurde erstellt, Ownership setzen
if os.path.isfile(cache_file):
set_owner(cache_file, uid, gid)
return True, int(output.split(':')[1]), None return True, int(output.split(':')[1]), None
elif output.startswith('FAIL:'): elif output.startswith('FAIL:'):
return False, int(output.split(':')[1]), f"Nur {output.split(':')[1]} Ranges (min. {min_expected} erwartet)" return False, int(output.split(':')[1]), f"Nur {output.split(':')[1]} Ranges (min. {min_expected} erwartet)"
@@ -1227,6 +1296,18 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
print(f"❌ index.php nicht gefunden") print(f"❌ index.php nicht gefunden")
return False return False
# Ermittle den häufigsten Owner im httpdocs-Verzeichnis
uid, gid = get_most_common_owner(httpdocs)
if not silent and uid is not None:
import pwd
import grp
try:
user_name = pwd.getpwuid(uid).pw_name
group_name = grp.getgrgid(gid).gr_name
print(f"\n👤 Ownership: {user_name}:{group_name} (uid={uid}, gid={gid})")
except (KeyError, ImportError):
print(f"\n👤 Ownership: uid={uid}, gid={gid}")
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 bot_mode: if bot_mode:
@@ -1242,6 +1323,7 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
print("\n[1/3] Aktiviere PHP-Blocking...") print("\n[1/3] Aktiviere PHP-Blocking...")
shutil.copy2(index_php, backup_php) shutil.copy2(index_php, backup_php)
set_owner(backup_php, uid, gid)
with open(index_php, 'r', encoding='utf-8') as f: with open(index_php, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
@@ -1260,6 +1342,7 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
lines.insert(insert_line, require_statement) lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f: with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines)) f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72) expiry = datetime.now() + timedelta(hours=72)
@@ -1271,6 +1354,8 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
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)
# Set ownership for ratelimit directories
set_owner(ratelimit_path, uid, gid, recursive=True)
if rate_limit is None: if rate_limit is None:
rate_limit = DEFAULT_RATE_LIMIT rate_limit = DEFAULT_RATE_LIMIT
@@ -1304,6 +1389,8 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
with open(blocking_file, 'w', encoding='utf-8') as f: with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content) f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if not silent: if not silent:
print(" ✅ PHP-Blocking aktiviert") print(" ✅ PHP-Blocking aktiviert")
@@ -1316,7 +1403,7 @@ def activate_blocking(shop, silent=False, mode="geoip", geo_region="dach", rate_
if not silent: if not silent:
print(f"\n[2/3] Generiere IP-Range-Cache ({len(region_info['countries'])} Länder)...") print(f"\n[2/3] Generiere IP-Range-Cache ({len(region_info['countries'])} Länder)...")
success, range_count, error = generate_and_validate_cache(httpdocs, geo_region) success, range_count, error = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
if success: if success:
if not silent: if not silent:
@@ -1470,7 +1557,11 @@ def activate_all_shops():
print(f" ⚠️ Übersprungen") print(f" ⚠️ Übersprungen")
continue continue
# Ermittle Owner für diesen Shop
uid, gid = get_most_common_owner(httpdocs)
shutil.copy2(index_php, backup_php) shutil.copy2(index_php, backup_php)
set_owner(backup_php, uid, gid)
with open(index_php, 'r', encoding='utf-8') as f: with open(index_php, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
@@ -1488,6 +1579,7 @@ def activate_all_shops():
lines.insert(insert_line, require_statement) lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f: with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines)) f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72) expiry = datetime.now() + timedelta(hours=72)
@@ -1497,6 +1589,7 @@ def activate_all_shops():
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)
set_owner(ratelimit_path, uid, gid, recursive=True)
geoip_content = BOT_SCRIPT_TEMPLATE.format( geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
@@ -1524,6 +1617,7 @@ def activate_all_shops():
with open(blocking_file, 'w', encoding='utf-8') as f: with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content) f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if bot_mode: if bot_mode:
add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration) add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration)
@@ -1531,7 +1625,7 @@ def activate_all_shops():
print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)") print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)")
else: else:
print(f" ⏳ Cache generieren...") print(f" ⏳ Cache generieren...")
cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region) cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
add_shop_to_active(shop, mode, geo_region) add_shop_to_active(shop, mode, geo_region)
print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)") print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)")
@@ -1620,7 +1714,11 @@ def activate_direct_shops_only():
print(f" ⚠️ Übersprungen") print(f" ⚠️ Übersprungen")
continue continue
# Ermittle Owner für diesen Shop
uid, gid = get_most_common_owner(httpdocs)
shutil.copy2(index_php, backup_php) shutil.copy2(index_php, backup_php)
set_owner(backup_php, uid, gid)
with open(index_php, 'r', encoding='utf-8') as f: with open(index_php, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
@@ -1638,6 +1736,7 @@ def activate_direct_shops_only():
lines.insert(insert_line, require_statement) lines.insert(insert_line, require_statement)
with open(index_php, 'w', encoding='utf-8') as f: with open(index_php, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines)) f.write('\n'.join(lines))
set_owner(index_php, uid, gid)
expiry = datetime.now() + timedelta(hours=72) expiry = datetime.now() + timedelta(hours=72)
@@ -1647,6 +1746,7 @@ def activate_direct_shops_only():
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)
set_owner(ratelimit_path, uid, gid, recursive=True)
geoip_content = BOT_SCRIPT_TEMPLATE.format( geoip_content = BOT_SCRIPT_TEMPLATE.format(
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'), expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
@@ -1674,6 +1774,7 @@ def activate_direct_shops_only():
with open(blocking_file, 'w', encoding='utf-8') as f: with open(blocking_file, 'w', encoding='utf-8') as f:
f.write(geoip_content) f.write(geoip_content)
set_owner(blocking_file, uid, gid)
if bot_mode: if bot_mode:
add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration) add_shop_to_active(shop, mode, geo_region, rate_limit, ban_duration)
@@ -1681,7 +1782,7 @@ def activate_direct_shops_only():
print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)") print(f" ✅ Aktiviert ({len(BOT_PATTERNS)} Patterns, {total_ip_ranges} IP-Ranges, {rate_limit} req/min)")
else: else:
print(f" ⏳ Cache generieren...") print(f" ⏳ Cache generieren...")
cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region) cache_ok, count, _ = generate_and_validate_cache(httpdocs, geo_region, uid, gid)
add_shop_to_active(shop, mode, geo_region) add_shop_to_active(shop, mode, geo_region)
print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)") print(f" ✅ Aktiviert ({count:,} Ranges)" if cache_ok else f" ⚠️ Aktiviert (Cache-Warnung)")
@@ -2056,11 +2157,12 @@ def main():
total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values()) total_ip_ranges = sum(len(ranges) for ranges in BOT_IP_RANGES.values())
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(" GeoIP Shop Blocker Manager v4.1.0") print(" GeoIP Shop Blocker Manager v4.2.0")
print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting") print(" 🇩🇪🇦🇹🇨🇭 DACH | 🇪🇺 Eurozone+GB | 🤖 Bot-Rate-Limiting")
print(" 🛡️ Mit Cache-Validierung und Fail-Open") print(" 🛡️ Mit Cache-Validierung und Fail-Open")
print(" 🚦 Rate-Limiting nach BOT-TYP (nicht IP)") print(" 🚦 Rate-Limiting nach BOT-TYP (nicht IP)")
print(f" 🌐 IP-basierte Erkennung: {total_ip_ranges} Ranges für {len(BOT_IP_RANGES)} Bot(s)") print(f" 🌐 IP-basierte Erkennung: {total_ip_ranges} Ranges für {len(BOT_IP_RANGES)} Bot(s)")
print(" 👤 Korrekte Ownership für erstellte Dateien")
print("=" * 60) print("=" * 60)
while True: while True: