geoip_shop_manager.py hinzugefügt
This commit is contained in:
771
geoip_shop_manager.py
Normal file
771
geoip_shop_manager.py
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
GeoIP Shop Blocker Manager - Final Fixed Version
|
||||||
|
2-Component System: PHP blocking + Python watcher (systemd service)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
VHOSTS_DIR = "/var/www/vhosts"
|
||||||
|
BACKUP_SUFFIX = ".geoip_backup"
|
||||||
|
BLOCKING_FILE = "geoip_blocking.php"
|
||||||
|
CACHE_FILE = "de_ip_ranges.cache"
|
||||||
|
LOG_FILE = "geoip_blocked.log"
|
||||||
|
CROWDSEC_QUEUE_FILE = "geoip_crowdsec_queue.log"
|
||||||
|
WATCHER_SCRIPT = "/usr/local/bin/geoip_crowdsec_watcher.py"
|
||||||
|
SYSTEMD_SERVICE = "/etc/systemd/system/geoip-crowdsec-watcher.service"
|
||||||
|
ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json"
|
||||||
|
|
||||||
|
# PHP GeoIP blocking script (no exec, just logging)
|
||||||
|
GEOIP_SCRIPT = '''<?php
|
||||||
|
/**
|
||||||
|
* GeoIP Blocking Script - Blocks all non-German IPs
|
||||||
|
* Logs blocked IPs for CrowdSec watcher to process
|
||||||
|
* Valid until: {expiry_date}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Auto-disable after 72 hours
|
||||||
|
$expiry_date = strtotime('{expiry_timestamp}');
|
||||||
|
if (time() > $expiry_date) {{
|
||||||
|
return; // Script expired, allow all traffic
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Get visitor IP
|
||||||
|
$visitor_ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
if (empty($visitor_ip)) {{
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Skip private IPs
|
||||||
|
if (filter_var($visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {{
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Files
|
||||||
|
$cache_file = __DIR__ . '/{cache_file}';
|
||||||
|
$cache_duration = 86400; // 24 hours
|
||||||
|
$log_file = __DIR__ . '/{log_file}';
|
||||||
|
$crowdsec_queue = __DIR__ . '/{crowdsec_queue}';
|
||||||
|
|
||||||
|
// Function to download German IP ranges
|
||||||
|
function download_de_ranges() {{
|
||||||
|
$ranges = [];
|
||||||
|
$url = 'https://www.ipdeny.com/ipblocks/data/aggregated/de-aggregated.zone';
|
||||||
|
$content = @file_get_contents($url);
|
||||||
|
|
||||||
|
if ($content !== false) {{
|
||||||
|
$lines = explode("\\n", trim($content));
|
||||||
|
foreach ($lines as $line) {{
|
||||||
|
$line = trim($line);
|
||||||
|
if (!empty($line) && strpos($line, '/') !== false) {{
|
||||||
|
$ranges[] = $line;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
return $ranges;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Function to check if IP is in CIDR range
|
||||||
|
function ip_in_range($ip, $cidr) {{
|
||||||
|
list($subnet, $mask) = explode('/', $cidr);
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$subnet_long = ip2long($subnet);
|
||||||
|
$mask_long = -1 << (32 - (int)$mask);
|
||||||
|
return ($ip_long & $mask_long) == ($subnet_long & $mask_long);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Load or download IP ranges
|
||||||
|
$de_ranges = [];
|
||||||
|
if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cache_duration) {{
|
||||||
|
$de_ranges = unserialize(file_get_contents($cache_file));
|
||||||
|
}} else {{
|
||||||
|
$de_ranges = download_de_ranges();
|
||||||
|
if (!empty($de_ranges)) {{
|
||||||
|
@file_put_contents($cache_file, serialize($de_ranges));
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Check if visitor IP is from Germany
|
||||||
|
$is_german = false;
|
||||||
|
foreach ($de_ranges as $range) {{
|
||||||
|
if (ip_in_range($visitor_ip, $range)) {{
|
||||||
|
$is_german = true;
|
||||||
|
break;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Block non-German IPs
|
||||||
|
if (!$is_german) {{
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
|
||||||
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
|
||||||
|
// Log for humans
|
||||||
|
$log_entry = "[$timestamp] IP: $visitor_ip | UA: $user_agent | URI: $request_uri\\n";
|
||||||
|
@file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);
|
||||||
|
|
||||||
|
// Queue for CrowdSec (simple format)
|
||||||
|
$queue_entry = "$timestamp|$visitor_ip|{shop_name}\\n";
|
||||||
|
@file_put_contents($crowdsec_queue, $queue_entry, FILE_APPEND | LOCK_EX);
|
||||||
|
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
exit;
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Python watcher script (runs as systemd service)
|
||||||
|
WATCHER_SCRIPT_CONTENT = '''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
GeoIP CrowdSec Watcher Service
|
||||||
|
Monitors queue files and adds blocked IPs to CrowdSec
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
VHOSTS_DIR = "/var/www/vhosts"
|
||||||
|
QUEUE_FILE = "geoip_crowdsec_queue.log"
|
||||||
|
ACTIVE_SHOPS_FILE = "/var/lib/crowdsec/geoip_active_shops.json"
|
||||||
|
PROCESSED_IPS = {} # In-memory cache to avoid re-adding same IP
|
||||||
|
CHECK_INTERVAL = 5 # Check every 5 seconds
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}", flush=True)
|
||||||
|
|
||||||
|
def get_active_shops():
|
||||||
|
"""Get list of shops with active GeoIP blocking"""
|
||||||
|
if not os.path.isfile(ACTIVE_SHOPS_FILE):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(ACTIVE_SHOPS_FILE, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def add_to_crowdsec(ip, shop):
|
||||||
|
"""Add IP to CrowdSec with 72h ban"""
|
||||||
|
# Check if already processed recently (within last hour)
|
||||||
|
now = time.time()
|
||||||
|
if ip in PROCESSED_IPS and (now - PROCESSED_IPS[ip]) < 3600:
|
||||||
|
return True
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
'cscli', 'decisions', 'add',
|
||||||
|
'--ip', ip,
|
||||||
|
'--duration', '72h',
|
||||||
|
'--type', 'ban',
|
||||||
|
'--reason', f'GeoIP: Non-DE IP blocked by {shop}'
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
PROCESSED_IPS[ip] = now
|
||||||
|
log(f"✅ Added {ip} to CrowdSec (from {shop})")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log(f"⚠️ Failed to add {ip}: {result.stderr.strip()}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log(f"❌ Error adding {ip}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_queue_file(shop_path, shop):
|
||||||
|
"""Process queue file for a shop"""
|
||||||
|
queue_file = os.path.join(shop_path, 'httpdocs', QUEUE_FILE)
|
||||||
|
|
||||||
|
if not os.path.isfile(queue_file):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read all lines
|
||||||
|
with open(queue_file, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Process each line
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = line.split('|')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
timestamp = parts[0]
|
||||||
|
ip = parts[1]
|
||||||
|
if add_to_crowdsec(ip, shop):
|
||||||
|
processed += 1
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Clear the file after processing
|
||||||
|
if processed > 0:
|
||||||
|
with open(queue_file, 'w') as f:
|
||||||
|
f.write('')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log(f"❌ Error processing {shop}: {e}")
|
||||||
|
|
||||||
|
return processed
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log("🚀 GeoIP CrowdSec Watcher started")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
|
||||||
|
if not active_shops:
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
total_processed = 0
|
||||||
|
|
||||||
|
for shop, info in active_shops.items():
|
||||||
|
shop_path = os.path.join(VHOSTS_DIR, shop)
|
||||||
|
if os.path.isdir(shop_path):
|
||||||
|
count = process_queue_file(shop_path, shop)
|
||||||
|
total_processed += count
|
||||||
|
|
||||||
|
if total_processed > 0:
|
||||||
|
log(f"📊 Processed {total_processed} IPs in this cycle")
|
||||||
|
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("👋 Shutting down...")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log(f"❌ Error in main loop: {e}")
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Systemd service file
|
||||||
|
SYSTEMD_SERVICE_CONTENT = '''[Unit]
|
||||||
|
Description=GeoIP CrowdSec Watcher Service
|
||||||
|
After=network.target crowdsec.service
|
||||||
|
Wants=crowdsec.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/bin/geoip_crowdsec_watcher.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, capture_output=True):
|
||||||
|
"""Run a shell command"""
|
||||||
|
try:
|
||||||
|
if capture_output:
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
||||||
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
else:
|
||||||
|
result = subprocess.run(cmd, shell=True, timeout=30)
|
||||||
|
return result.returncode, "", ""
|
||||||
|
except Exception as e:
|
||||||
|
return -1, "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def check_crowdsec():
|
||||||
|
"""Check if CrowdSec is running"""
|
||||||
|
code, stdout, stderr = run_command("systemctl is-active crowdsec")
|
||||||
|
return code == 0 and stdout.strip() == "active"
|
||||||
|
|
||||||
|
|
||||||
|
def install_watcher_service():
|
||||||
|
"""Install the watcher script and systemd service"""
|
||||||
|
print(" 📦 Installiere CrowdSec-Watcher-Service...")
|
||||||
|
|
||||||
|
# Create watcher script
|
||||||
|
with open(WATCHER_SCRIPT, 'w') as f:
|
||||||
|
f.write(WATCHER_SCRIPT_CONTENT)
|
||||||
|
os.chmod(WATCHER_SCRIPT, 0o755)
|
||||||
|
print(f" ✅ Watcher-Script erstellt: {WATCHER_SCRIPT}")
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
with open(SYSTEMD_SERVICE, 'w') as f:
|
||||||
|
f.write(SYSTEMD_SERVICE_CONTENT)
|
||||||
|
print(f" ✅ Systemd-Service erstellt: {SYSTEMD_SERVICE}")
|
||||||
|
|
||||||
|
# Reload systemd and start service
|
||||||
|
run_command("systemctl daemon-reload")
|
||||||
|
run_command("systemctl enable geoip-crowdsec-watcher.service")
|
||||||
|
run_command("systemctl start geoip-crowdsec-watcher.service")
|
||||||
|
|
||||||
|
# Check if started
|
||||||
|
time.sleep(1)
|
||||||
|
code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service")
|
||||||
|
if code == 0 and stdout.strip() == "active":
|
||||||
|
print(" ✅ Service gestartet und läuft")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ⚠️ Service konnte nicht gestartet werden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_watcher_service():
|
||||||
|
"""Uninstall the watcher script and systemd service"""
|
||||||
|
print(" 📦 Deinstalliere CrowdSec-Watcher-Service...")
|
||||||
|
|
||||||
|
# Stop and disable service
|
||||||
|
run_command("systemctl stop geoip-crowdsec-watcher.service")
|
||||||
|
run_command("systemctl disable geoip-crowdsec-watcher.service")
|
||||||
|
|
||||||
|
# Remove files
|
||||||
|
if os.path.isfile(SYSTEMD_SERVICE):
|
||||||
|
os.remove(SYSTEMD_SERVICE)
|
||||||
|
print(f" 🗑️ Service-Datei gelöscht")
|
||||||
|
|
||||||
|
if os.path.isfile(WATCHER_SCRIPT):
|
||||||
|
os.remove(WATCHER_SCRIPT)
|
||||||
|
print(f" 🗑️ Watcher-Script gelöscht")
|
||||||
|
|
||||||
|
run_command("systemctl daemon-reload")
|
||||||
|
print(" ✅ Service deinstalliert")
|
||||||
|
|
||||||
|
|
||||||
|
def add_shop_to_active(shop):
|
||||||
|
"""Add shop to active shops tracking"""
|
||||||
|
os.makedirs(os.path.dirname(ACTIVE_SHOPS_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
shops = {}
|
||||||
|
if os.path.isfile(ACTIVE_SHOPS_FILE):
|
||||||
|
with open(ACTIVE_SHOPS_FILE, 'r') as f:
|
||||||
|
shops = json.load(f)
|
||||||
|
|
||||||
|
shops[shop] = {
|
||||||
|
"activated": datetime.now().isoformat(),
|
||||||
|
"expiry": (datetime.now() + timedelta(hours=72)).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(ACTIVE_SHOPS_FILE, 'w') as f:
|
||||||
|
json.dump(shops, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_shop_from_active(shop):
|
||||||
|
"""Remove shop from active shops tracking"""
|
||||||
|
if not os.path.isfile(ACTIVE_SHOPS_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(ACTIVE_SHOPS_FILE, 'r') as f:
|
||||||
|
shops = json.load(f)
|
||||||
|
|
||||||
|
if shop in shops:
|
||||||
|
del shops[shop]
|
||||||
|
|
||||||
|
with open(ACTIVE_SHOPS_FILE, 'w') as f:
|
||||||
|
json.dump(shops, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_crowdsec_decisions(shop):
|
||||||
|
"""Remove CrowdSec decisions for a shop"""
|
||||||
|
if not check_crowdsec():
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" 🔍 Entferne CrowdSec-Decisions für {shop}...")
|
||||||
|
|
||||||
|
# Use the same reliable method as manual cleanup
|
||||||
|
# This uses xargs to properly handle all IPs
|
||||||
|
cleanup_cmd = f"cscli decisions list -o raw | grep '{shop}' | cut -d',' -f3 | cut -d':' -f2 | xargs -I {{}} cscli decisions delete --ip {{}}"
|
||||||
|
|
||||||
|
code, stdout, stderr = run_command(cleanup_cmd)
|
||||||
|
|
||||||
|
# Count how many were removed by checking the output
|
||||||
|
if stdout:
|
||||||
|
# Count "deleted" messages
|
||||||
|
removed = stdout.count("decision(s) deleted")
|
||||||
|
if removed > 0:
|
||||||
|
print(f" ✅ {removed} Decisions entfernt")
|
||||||
|
else:
|
||||||
|
print(" ℹ️ Keine Decisions gefunden")
|
||||||
|
else:
|
||||||
|
print(" ℹ️ Keine Decisions gefunden")
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_shops():
|
||||||
|
"""Get list of all shops"""
|
||||||
|
shops = []
|
||||||
|
if not os.path.exists(VHOSTS_DIR):
|
||||||
|
return shops
|
||||||
|
|
||||||
|
for entry in os.listdir(VHOSTS_DIR):
|
||||||
|
shop_path = os.path.join(VHOSTS_DIR, entry)
|
||||||
|
if os.path.isdir(shop_path) and entry not in ['chroot', 'system', 'default']:
|
||||||
|
httpdocs = os.path.join(shop_path, 'httpdocs')
|
||||||
|
if os.path.isdir(httpdocs):
|
||||||
|
index_php = os.path.join(httpdocs, 'index.php')
|
||||||
|
if os.path.isfile(index_php):
|
||||||
|
shops.append(entry)
|
||||||
|
|
||||||
|
return sorted(shops)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_shops():
|
||||||
|
"""Get list of shops with active blocking"""
|
||||||
|
active = []
|
||||||
|
shops = get_available_shops()
|
||||||
|
|
||||||
|
for shop in shops:
|
||||||
|
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
|
||||||
|
blocking_file = os.path.join(httpdocs, BLOCKING_FILE)
|
||||||
|
backup_file = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')
|
||||||
|
|
||||||
|
if os.path.isfile(blocking_file) or os.path.isfile(backup_file):
|
||||||
|
active.append(shop)
|
||||||
|
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
def activate_blocking(shop):
|
||||||
|
"""Activate GeoIP blocking"""
|
||||||
|
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
|
||||||
|
index_php = os.path.join(httpdocs, 'index.php')
|
||||||
|
backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')
|
||||||
|
blocking_file = os.path.join(httpdocs, BLOCKING_FILE)
|
||||||
|
|
||||||
|
if os.path.isfile(backup_php):
|
||||||
|
print(f"⚠️ GeoIP-Blocking bereits aktiv für {shop}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.isfile(index_php):
|
||||||
|
print(f"❌ index.php nicht gefunden")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\n🔧 Aktiviere Hybrid GeoIP-Blocking für: {shop}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Install watcher service if not exists
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
if not active_shops: # First shop
|
||||||
|
print("\n[1/3] Installiere CrowdSec-Watcher-Service...")
|
||||||
|
if check_crowdsec():
|
||||||
|
install_watcher_service()
|
||||||
|
else:
|
||||||
|
print(" ⚠️ CrowdSec nicht verfügbar - nur PHP-Blocking")
|
||||||
|
else:
|
||||||
|
print("\n[1/3] CrowdSec-Watcher-Service bereits aktiv")
|
||||||
|
|
||||||
|
# Step 2: PHP blocking
|
||||||
|
print("\n[2/3] Aktiviere PHP-Blocking...")
|
||||||
|
|
||||||
|
print(" 📋 Backup erstellen...")
|
||||||
|
shutil.copy2(index_php, backup_php)
|
||||||
|
|
||||||
|
with open(index_php, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lines = content.split('\n')
|
||||||
|
insert_line = 0
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if 'declare(strict_types' in line:
|
||||||
|
insert_line = i + 1
|
||||||
|
break
|
||||||
|
elif '<?php' in line and insert_line == 0:
|
||||||
|
insert_line = i + 1
|
||||||
|
|
||||||
|
require_statement = f"require_once __DIR__ . '/{BLOCKING_FILE}';"
|
||||||
|
|
||||||
|
if require_statement not in content:
|
||||||
|
lines.insert(insert_line, require_statement)
|
||||||
|
with open(index_php, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\n'.join(lines))
|
||||||
|
print(" ✏️ index.php modifiziert")
|
||||||
|
|
||||||
|
expiry = datetime.now() + timedelta(hours=72)
|
||||||
|
geoip_content = GEOIP_SCRIPT.format(
|
||||||
|
expiry_date=expiry.strftime('%Y-%m-%d %H:%M:%S CET'),
|
||||||
|
expiry_timestamp=expiry.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
cache_file=CACHE_FILE,
|
||||||
|
log_file=LOG_FILE,
|
||||||
|
crowdsec_queue=CROWDSEC_QUEUE_FILE,
|
||||||
|
shop_name=shop
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(blocking_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(geoip_content)
|
||||||
|
print(" 📝 geoip_blocking.php erstellt")
|
||||||
|
|
||||||
|
# Step 3: Register shop
|
||||||
|
print("\n[3/3] Registriere Shop...")
|
||||||
|
add_shop_to_active(shop)
|
||||||
|
print(" ✅ Shop registriert")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"✅ Hybrid GeoIP-Blocking aktiviert für: {shop}")
|
||||||
|
print(f" Gültig bis: {expiry.strftime('%Y-%m-%d %H:%M:%S CET')}")
|
||||||
|
print(f" PHP-Log: {os.path.join(httpdocs, LOG_FILE)}")
|
||||||
|
print(f" CrowdSec-Queue: {os.path.join(httpdocs, CROWDSEC_QUEUE_FILE)}")
|
||||||
|
print(f"\n 🔄 Der Watcher-Service synchronisiert blockierte IPs zu CrowdSec")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate_blocking(shop):
|
||||||
|
"""Deactivate GeoIP blocking"""
|
||||||
|
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
|
||||||
|
index_php = os.path.join(httpdocs, 'index.php')
|
||||||
|
backup_php = os.path.join(httpdocs, f'index.php{BACKUP_SUFFIX}')
|
||||||
|
blocking_file = os.path.join(httpdocs, BLOCKING_FILE)
|
||||||
|
cache_file = os.path.join(httpdocs, CACHE_FILE)
|
||||||
|
log_file = os.path.join(httpdocs, LOG_FILE)
|
||||||
|
queue_file = os.path.join(httpdocs, CROWDSEC_QUEUE_FILE)
|
||||||
|
|
||||||
|
print(f"\n🔧 Deaktiviere Hybrid GeoIP-Blocking für: {shop}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Remove PHP blocking
|
||||||
|
print("\n[1/4] PHP-Blocking entfernen...")
|
||||||
|
|
||||||
|
if os.path.isfile(backup_php):
|
||||||
|
shutil.move(backup_php, index_php)
|
||||||
|
print(" 📋 index.php wiederhergestellt")
|
||||||
|
else:
|
||||||
|
if os.path.isfile(index_php):
|
||||||
|
with open(index_php, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
lines = [line for line in content.split('\n') if BLOCKING_FILE not in line]
|
||||||
|
with open(index_php, 'w') as f:
|
||||||
|
f.write('\n'.join(lines))
|
||||||
|
|
||||||
|
for f in [blocking_file, cache_file, log_file, queue_file]:
|
||||||
|
if os.path.isfile(f):
|
||||||
|
os.remove(f)
|
||||||
|
print(" 🗑️ PHP-Dateien gelöscht")
|
||||||
|
|
||||||
|
# Step 2: Remove from tracking
|
||||||
|
print("\n[2/4] Deregistriere Shop...")
|
||||||
|
remove_shop_from_active(shop)
|
||||||
|
print(" ✅ Shop deregistriert")
|
||||||
|
|
||||||
|
# Step 3: Clean CrowdSec decisions
|
||||||
|
print("\n[3/4] CrowdSec-Decisions entfernen...")
|
||||||
|
if check_crowdsec():
|
||||||
|
cleanup_crowdsec_decisions(shop)
|
||||||
|
|
||||||
|
# Step 4: Uninstall service if last shop
|
||||||
|
print("\n[4/4] Prüfe Watcher-Service...")
|
||||||
|
remaining_shops = [s for s in get_active_shops() if s != shop]
|
||||||
|
if not remaining_shops:
|
||||||
|
print(" ℹ️ Keine aktiven Shops mehr - deinstalliere Service")
|
||||||
|
uninstall_watcher_service()
|
||||||
|
else:
|
||||||
|
print(f" ℹ️ Service bleibt aktiv ({len(remaining_shops)} Shop(s) noch aktiv)")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"✅ Hybrid GeoIP-Blocking deaktiviert für: {shop}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def show_logs(shop):
|
||||||
|
"""Show logs"""
|
||||||
|
httpdocs = os.path.join(VHOSTS_DIR, shop, 'httpdocs')
|
||||||
|
log_file = os.path.join(httpdocs, LOG_FILE)
|
||||||
|
|
||||||
|
if os.path.isfile(log_file):
|
||||||
|
print(f"\n📊 PHP-Blocks für {shop}:")
|
||||||
|
print("=" * 80)
|
||||||
|
with open(log_file, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for line in lines[-50:]:
|
||||||
|
print(line.rstrip())
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"Gesamt: {len(lines)}")
|
||||||
|
else:
|
||||||
|
print(f"ℹ️ Keine Logs für {shop}")
|
||||||
|
|
||||||
|
if check_crowdsec():
|
||||||
|
print(f"\n📊 CrowdSec Decisions für {shop}:")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# Use raw output (CSV format)
|
||||||
|
# Format: id,source,ip,reason,action,country,as,events_count,expiration,simulated,alert_id
|
||||||
|
code, stdout, _ = run_command("cscli decisions list -o raw")
|
||||||
|
if code == 0 and stdout:
|
||||||
|
lines = stdout.strip().split('\n')
|
||||||
|
shop_decisions = []
|
||||||
|
|
||||||
|
for line in lines[1:]: # Skip header
|
||||||
|
if shop in line:
|
||||||
|
shop_decisions.append(line)
|
||||||
|
|
||||||
|
if shop_decisions:
|
||||||
|
print(f"Aktive Bans: {len(shop_decisions)}")
|
||||||
|
print("\nLetzte 20 Bans:")
|
||||||
|
for line in shop_decisions[:20]:
|
||||||
|
parts = line.split(',')
|
||||||
|
if len(parts) > 8:
|
||||||
|
# Column 2: ip (format "Ip:1.2.3.4")
|
||||||
|
ip_field = parts[2].strip()
|
||||||
|
if ':' in ip_field:
|
||||||
|
ip = ip_field.split(':', 1)[1]
|
||||||
|
else:
|
||||||
|
ip = ip_field
|
||||||
|
|
||||||
|
# Column 8: expiration
|
||||||
|
expiry = parts[8].strip()
|
||||||
|
|
||||||
|
print(f" 🚫 {ip} (bis {expiry})")
|
||||||
|
|
||||||
|
if len(shop_decisions) > 20:
|
||||||
|
print(f" ... und {len(shop_decisions) - 20} weitere")
|
||||||
|
else:
|
||||||
|
print("Keine aktiven CrowdSec-Bans für diesen Shop")
|
||||||
|
else:
|
||||||
|
print("Konnte Decisions nicht abrufen")
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main menu"""
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" GeoIP Shop Blocker Manager - Final")
|
||||||
|
print(" PHP + CrowdSec Watcher (systemd service)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if check_crowdsec():
|
||||||
|
print(" ✅ CrowdSec: Aktiv")
|
||||||
|
else:
|
||||||
|
print(" ⚠️ CrowdSec: Nicht verfügbar")
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
code, stdout, _ = run_command("systemctl is-active geoip-crowdsec-watcher.service")
|
||||||
|
if code == 0 and stdout.strip() == "active":
|
||||||
|
print(" ✅ Watcher-Service: Läuft")
|
||||||
|
else:
|
||||||
|
print(" ⚠️ Watcher-Service: Nicht aktiv")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("\n[1] GeoIP-Blocking AKTIVIEREN")
|
||||||
|
print("[2] GeoIP-Blocking DEAKTIVIEREN")
|
||||||
|
print("[3] Logs anzeigen")
|
||||||
|
print("[4] Status anzeigen")
|
||||||
|
print("[0] Beenden")
|
||||||
|
|
||||||
|
choice = input("\nWähle eine Option: ").strip()
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
shops = get_available_shops()
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
available_shops = [s for s in shops if s not in active_shops]
|
||||||
|
|
||||||
|
if not available_shops:
|
||||||
|
print("\n⚠️ Keine Shops verfügbar")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("\n📋 Verfügbare Shops:")
|
||||||
|
for i, shop in enumerate(available_shops, 1):
|
||||||
|
print(f" [{i}] {shop}")
|
||||||
|
|
||||||
|
shop_choice = input("\nWähle einen Shop: ").strip()
|
||||||
|
try:
|
||||||
|
shop_idx = int(shop_choice) - 1
|
||||||
|
if 0 <= shop_idx < len(available_shops):
|
||||||
|
selected_shop = available_shops[shop_idx]
|
||||||
|
confirm = input(f"\n⚠️ Aktivieren für '{selected_shop}'? (ja/nein): ").strip().lower()
|
||||||
|
if confirm in ['ja', 'j', 'yes', 'y']:
|
||||||
|
activate_blocking(selected_shop)
|
||||||
|
else:
|
||||||
|
print("❌ Ungültig")
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Ungültig")
|
||||||
|
|
||||||
|
elif choice == "2":
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
|
||||||
|
if not active_shops:
|
||||||
|
print("\n⚠️ Keine aktiven Shops")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("\n📋 Aktive Shops:")
|
||||||
|
for i, shop in enumerate(active_shops, 1):
|
||||||
|
print(f" [{i}] {shop}")
|
||||||
|
|
||||||
|
shop_choice = input("\nWähle einen Shop: ").strip()
|
||||||
|
try:
|
||||||
|
shop_idx = int(shop_choice) - 1
|
||||||
|
if 0 <= shop_idx < len(active_shops):
|
||||||
|
selected_shop = active_shops[shop_idx]
|
||||||
|
confirm = input(f"\n⚠️ Deaktivieren für '{selected_shop}'? (ja/nein): ").strip().lower()
|
||||||
|
if confirm in ['ja', 'j', 'yes', 'y']:
|
||||||
|
deactivate_blocking(selected_shop)
|
||||||
|
else:
|
||||||
|
print("❌ Ungültig")
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Ungültig")
|
||||||
|
|
||||||
|
elif choice == "3":
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
if not active_shops:
|
||||||
|
print("\n⚠️ Keine aktiven Shops")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("\n📋 Shops mit Logs:")
|
||||||
|
for i, shop in enumerate(active_shops, 1):
|
||||||
|
print(f" [{i}] {shop}")
|
||||||
|
|
||||||
|
shop_choice = input("\nWähle einen Shop: ").strip()
|
||||||
|
try:
|
||||||
|
shop_idx = int(shop_choice) - 1
|
||||||
|
if 0 <= shop_idx < len(active_shops):
|
||||||
|
show_logs(active_shops[shop_idx])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif choice == "4":
|
||||||
|
shops = get_available_shops()
|
||||||
|
active_shops = get_active_shops()
|
||||||
|
print(f"\n📊 Status:")
|
||||||
|
print(f" Shops gesamt: {len(shops)}")
|
||||||
|
print(f" Aktive Blockings: {len(active_shops)}")
|
||||||
|
if active_shops:
|
||||||
|
for shop in active_shops:
|
||||||
|
print(f" ✓ {shop}")
|
||||||
|
|
||||||
|
elif choice == "0":
|
||||||
|
print("\n👋 Auf Wiedersehen!")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print("❌ Als root ausführen!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n👋 Abgebrochen")
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user