|
|
@@ -862,7 +862,8 @@ def import_backup():
|
|
|
@api_required
|
|
|
def api_cluster_adblock():
|
|
|
data = request.json
|
|
|
- action, master_ip = data.get('action'), request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
|
|
|
+ action = data.get('action')
|
|
|
+ master_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
|
|
|
|
|
|
if 'lists' in data: fm.save_adblock_lists(data['lists'])
|
|
|
if 'rules' in data: fm.save_custom_rules(data['rules'])
|
|
|
@@ -894,13 +895,17 @@ def api_cluster_adblock():
|
|
|
@api_required
|
|
|
def api_add_slave():
|
|
|
data = request.json
|
|
|
- if not data or 'domain' not in data: return jsonify({"status": "error", "message": "Missing parameters"}), 400
|
|
|
+ if not data or 'domain' not in data:
|
|
|
+ return jsonify({"status": "error", "message": "Missing parameters"}), 400
|
|
|
|
|
|
domain = re.sub(r'[^a-zA-Z0-9\.\-\/]', '', data['domain']).replace('../', '')
|
|
|
master_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
|
|
|
|
|
|
conf = fm.read_file(config.LOCAL_CONF)
|
|
|
- if f'zone "{domain}"' in conf: return jsonify({"status": "error", "message": "Zone already exists"}), 409
|
|
|
+
|
|
|
+ # ΠΡΟΣΤΑΣΙΑ: Αν η ζώνη υπάρχει ήδη, ΔΕΝ την πειράζουμε καθόλου. Κρατάει τον Master που είχε!
|
|
|
+ if f'zone "{domain}"' in conf:
|
|
|
+ return jsonify({"status": "error", "message": "Zone already exists"}), 409
|
|
|
|
|
|
path = os.path.join(config.CACHE_PATH, f"db.{domain}")
|
|
|
conf += f'\nzone "{domain}" {{\n type slave;\n file "{path}";\n masters {{ {master_ip}; }};\n masterfile-format text;\n}};\n'
|
|
|
@@ -909,9 +914,7 @@ def api_add_slave():
|
|
|
subprocess.run(["rndc", "reconfig"])
|
|
|
fm.log_audit("API_SYNC", domain, f"Auto-created as Slave (Master Auto-Detected: {master_ip})")
|
|
|
|
|
|
- settings = fm.load_panel_settings()
|
|
|
- settings['master_node'] = {"name": f"Auto-Linked Master ({master_ip})", "url": f"http://{master_ip}:5000"}
|
|
|
- fm.save_panel_settings(settings)
|
|
|
+ # ΑΦΑΙΡΕΘΗΚΕ ΤΟ AUTO-LINKING! Ο κώδικας δεν πειράζει πια τα settings του Slave.
|
|
|
return jsonify({"status": "success", "message": f"Slave zone {domain} configured."})
|
|
|
|
|
|
@app.route('/api/v1/cluster/zone/<domain>', methods=['DELETE'])
|
|
|
@@ -934,11 +937,7 @@ def api_delete_zone(domain):
|
|
|
@app.route('/api/v1/cluster/ping', methods=['GET'])
|
|
|
@api_required
|
|
|
def api_ping():
|
|
|
- master_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
|
|
|
- settings = fm.load_panel_settings()
|
|
|
- if settings.get('master_node', {}).get('url') != f"http://{master_ip}:5000":
|
|
|
- settings['master_node'] = {"name": f"Master Node ({master_ip})", "url": f"http://{master_ip}:5000"}
|
|
|
- fm.save_panel_settings(settings)
|
|
|
+ # ΑΦΑΙΡΕΘΗΚΕ ΤΟ AUTO-LINKING! Το Ping πλέον απλά επιβεβαιώνει ότι το API Key είναι σωστό.
|
|
|
return jsonify({"status": "success", "message": "Node is synchronized"})
|
|
|
|
|
|
@app.route('/cluster/check_node', methods=['POST'])
|
|
|
@@ -967,13 +966,21 @@ def check_cluster_node():
|
|
|
@app.route('/api/v1/cluster/service', methods=['POST'])
|
|
|
@api_required
|
|
|
def api_cluster_service():
|
|
|
- action = request.json.get('action')
|
|
|
+ # Fix: Ασφαλής ανάγνωση του JSON, χωρίς να σκάει
|
|
|
+ data = request.get_json(force=True, silent=True) or {}
|
|
|
+ action = data.get('action')
|
|
|
+
|
|
|
if action in ['start', 'stop', 'restart', 'reload']:
|
|
|
try:
|
|
|
- subprocess.run(["systemctl", action, config.BIND_SERVICE], check=True)
|
|
|
- return jsonify({"status": "success", "message": f"Service {action}ed successfully"})
|
|
|
+ # Fix: Πιάνουμε το πραγματικό error του systemctl αν αποτύχει
|
|
|
+ res = subprocess.run(["systemctl", action, config.BIND_SERVICE], capture_output=True, text=True)
|
|
|
+ if res.returncode == 0:
|
|
|
+ return jsonify({"status": "success", "message": f"Service {action}ed successfully"})
|
|
|
+ else:
|
|
|
+ return jsonify({"status": "error", "message": f"Systemctl failed: {res.stderr.strip()}"})
|
|
|
except Exception as e:
|
|
|
return jsonify({"status": "error", "message": str(e)})
|
|
|
+
|
|
|
elif action == 'status':
|
|
|
try:
|
|
|
res = subprocess.run(["systemctl", "is-active", config.BIND_SERVICE], capture_output=True, text=True)
|
|
|
@@ -981,49 +988,64 @@ def api_cluster_service():
|
|
|
return jsonify({"status": "success", "service_status": "active" if is_active else "inactive"})
|
|
|
except:
|
|
|
return jsonify({"status": "error", "service_status": "unknown"})
|
|
|
- return jsonify({"status": "error", "message": "Invalid action"})
|
|
|
+
|
|
|
+ return jsonify({"status": "error", "message": "Invalid action provided"})
|
|
|
|
|
|
# --- REMOTE SERVICE PROXY (MASTER TO SLAVE) ---
|
|
|
@app.route('/cluster/node_service', methods=['POST'])
|
|
|
@login_required
|
|
|
def cluster_node_service():
|
|
|
- node_name = request.json.get('node_name')
|
|
|
- action = request.json.get('action')
|
|
|
- node = next((n for n in fm.load_panel_settings().get('cluster_nodes', []) if n['name'] == node_name), None)
|
|
|
- if not node: return jsonify({"status": "error", "message": "Node not found"})
|
|
|
+ # Fix: Απαραίτητο import για να μην σκάει η urllib αν πάρει Error από τον Slave
|
|
|
+ import urllib.request, urllib.error
|
|
|
|
|
|
try:
|
|
|
+ data = request.get_json(force=True, silent=True) or {}
|
|
|
+ node_name = data.get('node_name')
|
|
|
+ action = data.get('action')
|
|
|
+
|
|
|
+ node = next((n for n in fm.load_panel_settings().get('cluster_nodes', []) if n['name'] == node_name), None)
|
|
|
+ if not node:
|
|
|
+ return jsonify({"status": "error", "message": "Node not found."})
|
|
|
+
|
|
|
url = f"{node['url'].rstrip('/')}/api/v1/cluster/service"
|
|
|
- proxy_handler = urllib.request.ProxyHandler({})
|
|
|
- opener = urllib.request.build_opener(proxy_handler)
|
|
|
payload = {"action": action}
|
|
|
+
|
|
|
+ proxy_handler = urllib.request.ProxyHandler({})
|
|
|
+ https_handler = urllib.request.HTTPSHandler(context=config.ssl_ctx)
|
|
|
+ opener = urllib.request.build_opener(proxy_handler, https_handler)
|
|
|
+
|
|
|
req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers={"X-API-Key": node['api_key'], "Content-Type": "application/json"}, method='POST')
|
|
|
|
|
|
- with opener.open(req, timeout=10) as response:
|
|
|
+ # Fix: Timeout στα 15 δευτερόλεπτα! Το BIND μπορεί να κάνει ώρα να κάνει restart
|
|
|
+ with opener.open(req, timeout=15) as response:
|
|
|
return jsonify(json.loads(response.read().decode('utf-8')))
|
|
|
+
|
|
|
+ except urllib.error.HTTPError as e:
|
|
|
+ return jsonify({"status": "error", "message": f"Slave returned HTTP Error {e.code}. Check API keys."})
|
|
|
except Exception as e:
|
|
|
- return jsonify({"status": "error", "message": str(e)})
|
|
|
-
|
|
|
- # --- GLOBAL CLUSTER SYNC (MASTER ACTION) ---
|
|
|
- @app.route('/api/cluster/sync_all', methods=['POST'])
|
|
|
- @admin_required
|
|
|
- def sync_all_cluster():
|
|
|
- conf = fm.read_file(config.LOCAL_CONF)
|
|
|
- # Βρίσκουμε όλα τα domains που είναι "type master"
|
|
|
- zones = re.findall(r'zone\s+"([^"]+)"\s*\{.*?type\s+master;', conf, re.DOTALL)
|
|
|
- # Εξαιρούμε το RPZ, θα συγχρονιστεί από το AdBlock μενού
|
|
|
- zones = [z for z in zones if z != "rpz.blacklist"]
|
|
|
+ return jsonify({"status": "error", "message": f"Connection failed: {str(e)}"})
|
|
|
+
|
|
|
+# --- GLOBAL CLUSTER SYNC (MASTER ACTION) ---
|
|
|
+@app.route('/api/cluster/sync_all', methods=['POST'])
|
|
|
+@admin_required
|
|
|
+def sync_all_cluster():
|
|
|
+ conf = fm.read_file(config.LOCAL_CONF)
|
|
|
+ # Βρίσκουμε όλα τα domains που είναι "type master"
|
|
|
+ zones = re.findall(r'zone\s+"([^"]+)"\s*\{.*?type\s+master;', conf, re.DOTALL)
|
|
|
+ # Εξαιρούμε το RPZ, θα συγχρονιστεί από το AdBlock μενού
|
|
|
+ zones = [z for z in zones if z != "rpz.blacklist"]
|
|
|
+
|
|
|
+ if not zones:
|
|
|
+ return jsonify({"status": "error", "message": "No Master zones found to sync."})
|
|
|
|
|
|
- if not zones:
|
|
|
- return jsonify({"status": "error", "message": "No Master zones found to sync."})
|
|
|
+ def bulk_sync():
|
|
|
+ for z in zones:
|
|
|
+ cm.broadcast_to_cluster('add_slave', z)
|
|
|
+ time.sleep(0.5) # Μικρή καθυστέρηση για να μην "μπουκώσει" ο Slave
|
|
|
|
|
|
- def bulk_sync():
|
|
|
- for z in zones:
|
|
|
- cm.broadcast_to_cluster('add_slave', z)
|
|
|
- time.sleep(0.5) # Μικρή καθυστέρηση για να μην "μπουκώσει" ο Slave
|
|
|
-
|
|
|
- threading.Thread(target=bulk_sync, daemon=True).start()
|
|
|
- return jsonify({"status": "success", "message": f"Started pushing {len(zones)} zones to all Nodes!"})
|
|
|
+ import threading
|
|
|
+ threading.Thread(target=bulk_sync, daemon=True).start()
|
|
|
+ return jsonify({"status": "success", "message": f"Started pushing {len(zones)} zones to all Nodes!"})
|
|
|
if __name__ == '__main__':
|
|
|
# Είναι σημαντικό το threaded=True για να μην κάνει "block" το UI στο self-ping
|
|
|
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
|