From e303548112c9500938be200d02ae7bd055c63267 Mon Sep 17 00:00:00 2001 From: master3395 Date: Wed, 28 Jan 2026 23:24:16 +0100 Subject: [PATCH] Add modify firewall rule and improve export/import functionality - Add modifyRule function to allow editing firewall rules without deletion - Add modify button and modal for firewall rules (similar to banned IPs) - Fix exportRules function to properly handle file downloads with blob response - Improve importRules function with better error handling and PNotify notifications - Add exportBannedIPs and importBannedIPs functionality - Add export/import buttons for banned IPs - Improve error handling and user feedback for all export/import operations - Add proper validation and duplicate detection for imports --- firewall/firewallManager.py | 640 ++++++++++++++++++++- firewall/static/firewall/firewall.js | 643 ++++++++++++++++++++-- firewall/templates/firewall/firewall.html | 156 +++++- firewall/urls.py | 4 + firewall/views.py | 42 ++ 5 files changed, 1421 insertions(+), 64 deletions(-) diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 6a90fe4a4..cc2883520 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -11,6 +11,7 @@ sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") django.setup() import json +import tempfile from plogical.acl import ACLManager import plogical.CyberCPLogFileWriter as logging from plogical.virtualHostUtilities import virtualHostUtilities @@ -139,6 +140,107 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def modifyRule(self, userID=None, data=None): + """ + Modify an existing firewall rule + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('modify_status', 0) + + ruleID = data.get('id') + newRuleName = data.get('ruleName', '').strip() + newRuleProtocol = data.get('ruleProtocol', '').strip() + newRulePort = data.get('rulePort', '').strip() + newRuleIP = data.get('ruleIP', '').strip() + + # Validate inputs + if not newRuleName: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Rule name is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRuleProtocol or newRuleProtocol not in ['tcp', 'udp']: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Valid protocol (tcp/udp) is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRulePort: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Port is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not newRuleIP: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'IP address is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Get existing rule + try: + existingRule = FirewallRules.objects.get(id=ruleID) + except FirewallRules.DoesNotExist: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'Rule not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Check if anything changed + changed = False + if (existingRule.name != newRuleName or + existingRule.proto != newRuleProtocol or + existingRule.port != newRulePort or + existingRule.ipAddress != newRuleIP): + changed = True + + if changed: + # Check if new rule already exists (different ID) + existingDuplicate = FirewallRules.objects.filter( + name=newRuleName, + proto=newRuleProtocol, + port=newRulePort, + ipAddress=newRuleIP + ).exclude(id=ruleID).first() + + if existingDuplicate: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': 'A rule with these settings already exists'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Delete old firewall rule + try: + FirewallUtilities.deleteRule(existingRule.proto, existingRule.port, existingRule.ipAddress) + logging.CyberCPLogFileWriter.writeToFile(f'Removed old firewall rule: {existingRule.proto}/{existingRule.port}/{existingRule.ipAddress}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not remove old firewall rule: {str(e)}') + + # Add new firewall rule + try: + FirewallUtilities.addRule(newRuleProtocol, newRulePort, newRuleIP) + logging.CyberCPLogFileWriter.writeToFile(f'Added new firewall rule: {newRuleProtocol}/{newRulePort}/{newRuleIP}') + except Exception as e: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': f'Failed to add firewall rule: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Update database record + existingRule.name = newRuleName + existingRule.proto = newRuleProtocol + existingRule.port = newRulePort + existingRule.ipAddress = newRuleIP + existingRule.save() + + final_dic = {'status': 1, 'modify_status': 1, 'error_message': "None"} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'modify_status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + def deleteRule(self, userID = None, data = None): try: @@ -1829,24 +1931,72 @@ class FirewallManager: except: banned_ips = [] - # Filter out expired bans + # Filter out expired bans and format data consistently current_time = time.time() active_banned_ips = [] for banned_ip in banned_ips: - if banned_ip.get('expires') == 'Never' or banned_ip.get('expires', 0) > current_time: - banned_ip['active'] = True - if banned_ip.get('expires') != 'Never': - banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip['expires'])) - else: - banned_ip['expires'] = 'Never' - banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) - else: - banned_ip['active'] = False - banned_ip['expires'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('expires', current_time))) - banned_ip['banned_on'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_ip.get('banned_on', current_time))) + # Get original values + expires_val = banned_ip.get('expires') + banned_on_val = banned_ip.get('banned_on', current_time) - active_banned_ips.append(banned_ip) + # Convert banned_on to timestamp if it's a string + if isinstance(banned_on_val, str): + try: + # Try parsing formatted date string + banned_on_val = time.mktime(time.strptime(banned_on_val, '%Y-%m-%d %H:%M:%S')) + except: + banned_on_val = current_time + elif not isinstance(banned_on_val, (int, float)): + banned_on_val = current_time + + # Check if expired + is_expired = False + if expires_val == 'Never' or expires_val is None: + expires_timestamp = None + expires_display = 'Never' + elif isinstance(expires_val, str): + if expires_val == 'Never': + expires_timestamp = None + expires_display = 'Never' + else: + try: + expires_timestamp = time.mktime(time.strptime(expires_val, '%Y-%m-%d %H:%M:%S')) + expires_display = expires_val + except: + expires_timestamp = None + expires_display = 'Never' + else: + expires_timestamp = expires_val + if expires_val > current_time: + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_val)) + else: + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_val)) + is_expired = True + + # Determine if active + if expires_timestamp is None: + is_active = True + else: + is_active = expires_timestamp > current_time and not is_expired + + # Format banned_on for display + banned_on_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(banned_on_val)) + + # Create formatted entry + formatted_ip = { + 'id': banned_ip.get('id'), + 'ip': banned_ip.get('ip'), + 'reason': banned_ip.get('reason', ''), + 'duration': banned_ip.get('duration', 'never'), + 'banned_on': banned_on_display, # String for display + 'banned_on_timestamp': banned_on_val * 1000, # Milliseconds for AngularJS date filter + 'expires': expires_display, # String for display + 'expires_timestamp': expires_timestamp * 1000 if expires_timestamp else None, # Milliseconds for AngularJS + 'active': is_active + } + + active_banned_ips.append(formatted_ip) final_dic = {'status': 1, 'bannedIPs': active_banned_ips} final_json = json.dumps(final_dic) @@ -1927,12 +2077,23 @@ class FirewallManager: } banned_ips.append(new_banned_ip) - # Ensure directory exists - os.makedirs(os.path.dirname(banned_ips_file), exist_ok=True) - - # Save to file - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) # Apply firewall rule to block the IP try: @@ -1992,9 +2153,23 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - # Save updated banned IPs - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) # Remove iptables rule try: @@ -2019,6 +2194,153 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def modifyBannedIP(self, userID=None, data=None): + """ + Modify a banned IP address (update reason and/or expiration) + """ + try: + admin = Administrator.objects.get(pk=userID) + if admin.acl.adminStatus != 1: + return ACLManager.loadError() + + banned_ip_id = data.get('id') + new_ip = data.get('ip', '').strip() + reason = data.get('reason', '').strip() + duration = data.get('duration', 'never') + + if not new_ip: + final_dic = {'status': 0, 'error_message': 'IP address is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Validate IP address format + import ipaddress + try: + ipaddress.ip_address(new_ip.split('/')[0]) # Handle CIDR notation + except ValueError: + final_dic = {'status': 0, 'error_message': 'Invalid IP address format'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + if not reason: + final_dic = {'status': 0, 'error_message': 'Reason is required'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except: + banned_ips = [] + + # Find and update the banned IP + old_ip = None + found = False + for banned_ip in banned_ips: + if banned_ip.get('id') == banned_ip_id: + found = True + old_ip = banned_ip['ip'] + + # Check if new IP is already banned (and not the same record) + ip_changed = (new_ip != old_ip) + if ip_changed: + for other_banned_ip in banned_ips: + if other_banned_ip.get('id') != banned_ip_id and other_banned_ip.get('ip') == new_ip and other_banned_ip.get('active', True): + final_dic = {'status': 0, 'error_message': f'IP address {new_ip} is already banned'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Update IP address if changed + if ip_changed: + # Remove old iptables rule + try: + if '/' in old_ip: + subprocess.run(['iptables', '-D', 'INPUT', '-s', old_ip, '-j', 'DROP'], check=False) + else: + subprocess.run(['iptables', '-D', 'INPUT', '-s', old_ip, '-j', 'DROP'], check=False) + logging.CyberCPLogFileWriter.writeToFile(f'Removed iptables rule for old IP {old_ip}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not remove old iptables rule for {old_ip}: {str(e)}') + + # Add new iptables rule + try: + if '/' in new_ip: + subprocess.run(['iptables', '-A', 'INPUT', '-s', new_ip, '-j', 'DROP'], check=True) + else: + subprocess.run(['iptables', '-A', 'INPUT', '-s', new_ip, '-j', 'DROP'], check=True) + logging.CyberCPLogFileWriter.writeToFile(f'Added iptables rule for new IP {new_ip}') + except subprocess.CalledProcessError as e: + final_dic = {'status': 0, 'error_message': f'Failed to add firewall rule for IP {new_ip}: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + banned_ip['ip'] = new_ip + + # Update reason + banned_ip['reason'] = reason + + # Update expiration if duration changed + if duration == 'never' or duration == 'permanent': + banned_ip['expires'] = 'Never' + banned_ip['duration'] = 'never' + else: + # Calculate new expiration time + current_time = time.time() + duration_map = { + '1h': 3600, + '6h': 21600, + '12h': 43200, + '24h': 86400, + '48h': 172800, + '7d': 604800, + '30d': 2592000 + } + duration_seconds = duration_map.get(duration, 86400) + banned_ip['expires'] = current_time + duration_seconds + banned_ip['duration'] = duration + + break + + if not found: + final_dic = {'status': 0, 'error_message': 'Banned IP record not found'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + + with open(temp_file, 'w') as f: + json.dump(banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + ip_display = new_ip if new_ip != old_ip else old_ip + change_msg = f'IP changed from {old_ip} to {new_ip}' if new_ip != old_ip else f'IP unchanged ({old_ip})' + logging.CyberCPLogFileWriter.writeToFile(f'Modified banned IP record: {change_msg}, reason={reason}, duration={duration}') + + final_dic = {'status': 1, 'message': f'Banned IP has been modified successfully'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'status': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + def deleteBannedIP(self, userID=None, data=None): """ Permanently delete a banned IP record @@ -2054,9 +2376,36 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - # Save updated banned IPs - with open(banned_ips_file, 'w') as f: + # Write to temp file in /tmp (web server user has write permissions here) + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, f'banned_ips_{int(time.time())}.json') + with open(temp_file, 'w') as f: json.dump(updated_banned_ips, f, indent=2) + + # Ensure /etc/cyberpanel directory exists with proper permissions + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command, None, True) + + # Move temp file to final location and set permissions using ProcessUtilities + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command, None, True) + + # Remove iptables rule to unban the IP + try: + # Remove iptables rule to unblock the IP + if '/' in ip_to_delete: + # CIDR notation + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_delete, '-j', 'DROP'], check=False) + else: + # Single IP + subprocess.run(['iptables', '-D', 'INPUT', '-s', ip_to_delete, '-j', 'DROP'], check=False) + + logging.CyberCPLogFileWriter.writeToFile(f'Deleted and unbanned IP {ip_to_delete}') + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f'Failed to remove iptables rule for {ip_to_delete}: {str(e)}') logging.CyberCPLogFileWriter.writeToFile(f'Deleted banned IP record for {ip_to_delete}') @@ -2227,6 +2576,253 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) + def exportBannedIPs(self, userID=None): + """ + Export all banned IPs to a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('exportStatus', 0) + + # Load banned IPs from file + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + banned_ips = json.load(f) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error reading banned IPs file: {str(e)}") + banned_ips = [] + + # Filter out expired bans for export (optional - you might want to include all) + current_time = time.time() + active_banned_ips = [] + for banned_ip in banned_ips: + expires_val = banned_ip.get('expires') + expires_timestamp = banned_ip.get('expires_timestamp') + + # Include if never expires or not yet expired + if expires_val == 'Never' or expires_timestamp is None: + active_banned_ips.append(banned_ip) + elif isinstance(expires_timestamp, (int, float)) and expires_timestamp > current_time: + active_banned_ips.append(banned_ip) + + # Create export data with metadata + export_data = { + 'version': '1.0', + 'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'), + 'total_banned_ips': len(active_banned_ips), + 'banned_ips': active_banned_ips + } + + # Create JSON response with file download + json_content = json.dumps(export_data, indent=2) + + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs exported successfully. Total IPs: {len(active_banned_ips)}") + + # Return file as download + response = HttpResponse(json_content, content_type='application/json') + response['Content-Disposition'] = f'attachment; filename="banned_ips_export_{int(time.time())}.json"' + + return response + + except BaseException as msg: + final_dic = {'exportStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + def importBannedIPs(self, userID=None, data=None): + """ + Import banned IPs from a JSON file + """ + try: + currentACL = ACLManager.loadedACL(userID) + + if currentACL['admin'] == 1: + pass + else: + return ACLManager.loadErrorJson('importStatus', 0) + + # Handle file upload + if hasattr(self.request, 'FILES') and 'import_file' in self.request.FILES: + import_file = self.request.FILES['import_file'] + + # Read file content + import_data = json.loads(import_file.read().decode('utf-8')) + else: + # Fallback to file path method + import_file_path = data.get('import_file_path', '') if data else '' + + if not import_file_path or not os.path.exists(import_file_path): + final_dic = {'importStatus': 0, 'error_message': 'Import file not found or invalid path'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + # Read and parse the import file + with open(import_file_path, 'r') as f: + import_data = json.load(f) + + # Validate the import data structure + if 'banned_ips' not in import_data: + final_dic = {'importStatus': 0, 'error_message': 'Invalid import file format. Missing banned_ips array.'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + imported_count = 0 + skipped_count = 0 + error_count = 0 + errors = [] + + # Load existing banned IPs + banned_ips_file = '/etc/cyberpanel/banned_ips.json' + existing_banned_ips = [] + if os.path.exists(banned_ips_file): + try: + with open(banned_ips_file, 'r') as f: + existing_banned_ips = json.load(f) + except: + existing_banned_ips = [] + + # Create a set of existing IPs for quick lookup + existing_ips = {banned_ip.get('ip') for banned_ip in existing_banned_ips} + + # Import IP address validation + import ipaddress + + for banned_ip_data in import_data['banned_ips']: + try: + ip_address = banned_ip_data.get('ip', '').strip() + reason = banned_ip_data.get('reason', '').strip() + + # Validate IP address + if not ip_address: + error_count += 1 + errors.append(f"Invalid entry: Missing IP address") + continue + + try: + ipaddress.ip_address(ip_address.split('/')[0]) # Handle CIDR notation + except ValueError: + error_count += 1 + errors.append(f"IP '{ip_address}': Invalid IP address format") + continue + + # Check if IP already exists + if ip_address in existing_ips: + skipped_count += 1 + continue + + # Validate reason + if not reason: + reason = 'Imported from backup' + + # Get duration or calculate from expires + duration = banned_ip_data.get('duration', 'never') + expires = banned_ip_data.get('expires', 'Never') + expires_timestamp = banned_ip_data.get('expires_timestamp') + + # Calculate expiration if needed + if expires == 'Never' or expires_timestamp is None: + expires_timestamp = None + expires_display = 'Never' + duration = 'never' + elif expires_timestamp and isinstance(expires_timestamp, (int, float)): + expires_display = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_timestamp)) + else: + expires_display = expires if isinstance(expires, str) else 'Never' + expires_timestamp = None + + # Create banned IP entry + banned_on = banned_ip_data.get('banned_on', time.time()) + if isinstance(banned_on, str): + try: + banned_on = time.mktime(time.strptime(banned_on, '%Y-%m-%d %H:%M:%S')) + except: + banned_on = time.time() + + new_banned_ip = { + 'id': banned_ip_data.get('id', randint(100000, 999999)), + 'ip': ip_address, + 'reason': reason, + 'banned_on': banned_on, + 'banned_on_timestamp': banned_on if isinstance(banned_on, (int, float)) else time.time(), + 'expires': expires_display, + 'expires_timestamp': expires_timestamp, + 'duration': duration, + 'active': True + } + + # Add iptables rule + try: + if '/' in ip_address: + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip_address, '-j', 'DROP'], check=True) + else: + subprocess.run(['iptables', '-A', 'INPUT', '-s', ip_address, '-j', 'DROP'], check=True) + logging.CyberCPLogFileWriter.writeToFile(f'Added iptables rule for imported IP {ip_address}') + except subprocess.CalledProcessError as e: + error_count += 1 + errors.append(f"IP '{ip_address}': Failed to add firewall rule - {str(e)}") + continue + + # Add to existing list + existing_banned_ips.append(new_banned_ip) + existing_ips.add(ip_address) + imported_count += 1 + + except Exception as e: + error_count += 1 + errors.append(f"IP '{banned_ip_data.get('ip', 'Unknown')}': {str(e)}") + logging.CyberCPLogFileWriter.writeToFile(f"Error importing banned IP {banned_ip_data.get('ip', 'Unknown')}: {str(e)}") + + # Save updated banned IPs + if imported_count > 0 or error_count > 0: + try: + # Create temp file + temp_file = f'/tmp/banned_ips_{randint(100000, 999999)}.json' + with open(temp_file, 'w') as f: + json.dump(existing_banned_ips, f, indent=2) + + # Ensure directory exists + command = f'mkdir -p {os.path.dirname(banned_ips_file)} && chmod 755 {os.path.dirname(banned_ips_file)}' + ProcessUtilities.executioner(command) + + # Move temp file to final location + command = f'mv {temp_file} {banned_ips_file}' + ProcessUtilities.executioner(command) + + # Set permissions + command = f'chmod 644 {banned_ips_file} && chown root:root {banned_ips_file}' + ProcessUtilities.executioner(command) + + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile(f"Error saving imported banned IPs: {str(e)}") + final_dic = {'importStatus': 0, 'error_message': f'Failed to save imported IPs: {str(e)}'} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + logging.CyberCPLogFileWriter.writeToFile(f"Banned IPs import completed. Imported: {imported_count}, Skipped: {skipped_count}, Errors: {error_count}") + + final_dic = { + 'importStatus': 1, + 'error_message': "None", + 'imported_count': imported_count, + 'skipped_count': skipped_count, + 'error_count': error_count, + 'errors': errors + } + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + + except BaseException as msg: + final_dic = {'importStatus': 0, 'error_message': str(msg)} + final_json = json.dumps(final_dic) + return HttpResponse(final_json) + diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 1853a3544..8656822c3 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -499,6 +499,281 @@ app.controller('firewallController', function ($scope, $http, $timeout, $window, }; + // Modify Firewall Rule Functions + $scope.handleModifyRuleClick = function(rule, event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (!rule) { + console.error('No rule provided'); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'No rule data provided', + type: 'error' + }); + } + return false; + } + + $scope.showModifyRuleModal(rule, event); + return false; + }; + + $scope.showModifyRuleModal = function(rule, event) { + console.log('=== showModifyRuleModal CALLED ==='); + console.log('Rule:', rule); + + if (!rule) { + console.error('No rule provided'); + return false; + } + + // Get modal element + var modalElement = document.getElementById('modifyRuleModal'); + if (!modalElement) { + console.error('Modal element not found'); + alert('Error: Modal element not found. Please refresh the page.'); + return false; + } + + // Set form values + var idField = document.getElementById('modifyRuleId'); + var nameField = document.getElementById('modifyRuleName'); + var protocolField = document.getElementById('modifyRuleProtocol'); + var ipField = document.getElementById('modifyRuleIP'); + var portField = document.getElementById('modifyRulePort'); + + if (idField) idField.value = rule.id || ''; + if (nameField) nameField.value = rule.name || ''; + if (protocolField) protocolField.value = rule.proto || 'tcp'; + if (ipField) ipField.value = rule.ipAddress || ''; + if (portField) portField.value = rule.port || ''; + + // Show modal using AngularJS $timeout + $timeout(function() { + // Clean up existing modals/backdrops + var existingBackdrops = document.querySelectorAll('.modal-backdrop'); + existingBackdrops.forEach(function(b) { b.remove(); }); + + var existingModals = document.querySelectorAll('.modal.show'); + existingModals.forEach(function(m) { + m.classList.remove('show'); + }); + + document.body.classList.remove('modal-open'); + + // Move modal to body if needed + if (modalElement.parentElement !== document.body) { + document.body.appendChild(modalElement); + } + + // Show modal + modalElement.classList.add('show', 'fade'); + modalElement.style.cssText = 'display: flex !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 99999 !important; opacity: 1 !important; visibility: visible !important; align-items: center !important; justify-content: center !important;'; + modalElement.removeAttribute('aria-hidden'); + modalElement.setAttribute('aria-hidden', 'false'); + modalElement.setAttribute('aria-modal', 'true'); + + document.body.classList.add('modal-open'); + document.body.style.overflow = 'hidden'; + + // Create backdrop + var backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop fade show'; + backdrop.style.cssText = 'position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 99998 !important; background-color: rgba(0, 0, 0, 0.5) !important;'; + backdrop.id = 'modifyRuleModalBackdrop'; + document.body.appendChild(backdrop); + + // Handle backdrop click + backdrop.addEventListener('click', function(e) { + if (e.target === backdrop) { + $scope.closeModifyRuleModal(); + } + }); + + // Try jQuery/Bootstrap modal if available + if (typeof $ !== 'undefined' && $.fn.modal) { + try { + var $modal = $('#modifyRuleModal'); + if ($modal.length > 0) { + if ($modal.parent()[0] !== document.body) { + $modal.appendTo('body'); + } + if (!$modal.data('bs.modal')) { + $modal.modal({show: false, backdrop: true, keyboard: true}); + } + $modal.modal('show'); + } + } catch (e) { + console.warn('jQuery modal failed, using direct display:', e); + } + } + }, 10); + }; + + $scope.closeModifyRuleModal = function() { + var modalElement = document.getElementById('modifyRuleModal'); + if (modalElement) { + // Try jQuery/Bootstrap modal first + if (typeof $ !== 'undefined' && $.fn.modal) { + try { + $('#modifyRuleModal').modal('hide'); + } catch (e) { + // Fall through to manual cleanup + } + } + + // Manual cleanup + modalElement.classList.remove('show', 'fade'); + modalElement.style.display = 'none'; + modalElement.setAttribute('aria-hidden', 'true'); + + // Remove backdrop + var backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(function(b) { b.remove(); }); + + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + } + }; + + $scope.modifyRule = function() { + var ruleId = document.getElementById('modifyRuleId').value; + var ruleName = document.getElementById('modifyRuleName').value.trim(); + var ruleProtocol = document.getElementById('modifyRuleProtocol').value; + var ruleIP = document.getElementById('modifyRuleIP').value.trim(); + var rulePort = document.getElementById('modifyRulePort').value.trim(); + + // Validation + if (!ruleName) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a rule name', + type: 'error' + }); + } else { + alert('Please enter a rule name'); + } + return; + } + + if (!ruleProtocol || (ruleProtocol !== 'tcp' && ruleProtocol !== 'udp')) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please select a valid protocol (TCP or UDP)', + type: 'error' + }); + } else { + alert('Please select a valid protocol'); + } + return; + } + + if (!ruleIP) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter an IP address', + type: 'error' + }); + } else { + alert('Please enter an IP address'); + } + return; + } + + if (!rulePort) { + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Validation Error', + text: 'Please enter a port number', + type: 'error' + }); + } else { + alert('Please enter a port number'); + } + return; + } + + $scope.rulesLoading = false; + $scope.actionFailed = true; + $scope.actionSuccess = true; + $scope.couldNotConnect = true; + + var url = "/firewall/modifyRule"; + var data = { + id: ruleId, + ruleName: ruleName, + ruleProtocol: ruleProtocol, + rulePort: rulePort, + ruleIP: ruleIP + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post(url, data, config).then(function(response) { + $scope.rulesLoading = true; + + if (response.data && response.data.modify_status === 1) { + // Close modal + $scope.closeModifyRuleModal(); + + // Refresh rules list + populateCurrentRecords(); + + $scope.actionFailed = true; + $scope.actionSuccess = false; + $scope.canNotAddRule = true; + $scope.ruleAdded = false; + $scope.couldNotConnect = true; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Firewall rule modified successfully', + type: 'success' + }); + } + } else { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = (response.data && response.data.error_message) || 'Failed to modify firewall rule'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error!', + text: (response.data && response.data.error_message) || 'Failed to modify firewall rule', + type: 'error' + }); + } + } + }, function(error) { + $scope.rulesLoading = true; + $scope.couldNotConnect = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + }; + + // Make modify rule functions available globally + window.showModifyRuleModalScope = $scope.showModifyRuleModal; + window.closeModifyRuleModalScope = $scope.closeModifyRuleModal; + window.modifyRuleScope = $scope.modifyRule; $scope.reloadFireWall = function () { @@ -3828,53 +4103,325 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); }; + // Export/Import Banned IPs Functions + $scope.exportBannedIPs = function () { + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + + var url = "/firewall/exportBannedIPs"; + var data = {}; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + }, + responseType: 'blob' + }; + + $http.post(url, data, config).then(function(response) { + $scope.bannedIPsLoading = true; + + // Check if response is JSON (error) or file download + if (response.data instanceof Blob) { + // Create blob URL and trigger download + var blob = new Blob([response.data], { type: 'application/json' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'banned_ips_export_' + Math.floor(Date.now() / 1000) + '.json'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Banned IPs exported successfully', + type: 'success' + }); + } + } else { + // Handle error response + try { + var errorData = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; + if (errorData.exportStatus === 0) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = errorData.error_message; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Export Failed', + text: errorData.error_message, + type: 'error' + }); + } + } + } catch (e) { + // If not JSON, try reading as text + var reader = new FileReader(); + reader.onload = function() { + try { + var errorData = JSON.parse(reader.result); + if (errorData.exportStatus === 0) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = errorData.error_message; + } + } catch (e2) { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = 'Failed to export banned IPs'; + } + }; + reader.readAsText(response.data); + } + } + }, function(error) { + $scope.bannedIPsLoading = true; + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = 'Could not connect to server. Please refresh this page.'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + }; + + $scope.importBannedIPs = function () { + // Create file input element + var input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.style.display = 'none'; + + input.onchange = function(event) { + var file = event.target.files[0]; + if (file) { + var reader = new FileReader(); + reader.onload = function(e) { + try { + var importData = JSON.parse(e.target.result); + + // Validate file format + if (!importData.banned_ips || !Array.isArray(importData.banned_ips)) { + $scope.$apply(function() { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = "Invalid import file format. Please select a valid banned IPs export file."; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Invalid File', + text: 'Invalid import file format. Please select a valid banned IPs export file.', + type: 'error' + }); + } + return; + } + + // Upload file to server + uploadBannedIPsImportFile(file); + } catch (error) { + $scope.$apply(function() { + $scope.bannedIPActionFailed = false; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPErrorMessage = "Invalid JSON file. Please select a valid banned IPs export file."; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Invalid File', + text: 'Invalid JSON file. Please select a valid banned IPs export file.', + type: 'error' + }); + } + } + }; + reader.readAsText(file); + } + }; + + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + }; + + function uploadBannedIPsImportFile(file) { + $scope.bannedIPsLoading = false; + $scope.bannedIPActionFailed = true; + $scope.bannedIPActionSuccess = true; + $scope.bannedIPCouldNotConnect = true; + + var formData = new FormData(); + formData.append('import_file', file); + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken'), + 'Content-Type': undefined + }, + transformRequest: angular.identity + }; + + $http.post("/firewall/importBannedIPs", formData, config).then(function(response) { + $scope.bannedIPsLoading = true; + + if (response.data.importStatus === 1) { + $scope.bannedIPActionSuccess = false; + populateBannedIPs(); // Refresh the list + + var message = `Import completed: ${response.data.imported_count} imported, ${response.data.skipped_count} skipped`; + if (response.data.error_count > 0) { + message += `, ${response.data.error_count} errors`; + if (response.data.errors && response.data.errors.length > 0) { + message += '\nErrors: ' + response.data.errors.slice(0, 5).join('; '); + if (response.data.errors.length > 5) { + message += ` ... and ${response.data.errors.length - 5} more`; + } + } + } + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Completed!', + text: message, + type: response.data.error_count > 0 ? 'notice' : 'success' + }); + } else { + alert(message); + } + } else { + $scope.bannedIPActionFailed = false; + $scope.bannedIPErrorMessage = response.data.error_message || 'Failed to import banned IPs'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Failed', + text: response.data.error_message || 'Failed to import banned IPs', + type: 'error' + }); + } + } + }, function(error) { + $scope.bannedIPsLoading = true; + $scope.bannedIPCouldNotConnect = false; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); + } + // Export/Import Firewall Rules Functions $scope.exportRules = function () { $scope.rulesLoading = false; $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/exportFirewallRules"; - + var url = "/firewall/exportFirewallRules"; var data = {}; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') - } + }, + responseType: 'blob' }; - $http.post(url, data, config).then(exportSuccess, exportError); - - function exportSuccess(response) { + $http.post(url, data, config).then(function(response) { $scope.rulesLoading = true; // Check if response is JSON (error) or file download - if (typeof response.data === 'string' && response.data.includes('{')) { - try { - var errorData = JSON.parse(response.data); - if (errorData.exportStatus === 0) { - $scope.actionFailed = false; - $scope.actionSuccess = true; - $scope.errorMessage = errorData.error_message; - return; + if (response.data instanceof Blob) { + // Check if it's actually a JSON error by reading the blob + var reader = new FileReader(); + reader.onload = function() { + try { + var text = reader.result; + // Check if it's JSON error + if (text.trim().startsWith('{')) { + var errorData = JSON.parse(text); + if (errorData.exportStatus === 0) { + $scope.$apply(function() { + $scope.actionFailed = false; + $scope.actionSuccess = true; + $scope.errorMessage = errorData.error_message; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Export Failed', + text: errorData.error_message, + type: 'error' + }); + } + return; + } + } + } catch (e) { + // Not JSON, proceed with download } - } catch (e) { - // If not JSON, assume it's the file content - } + + // It's a valid file, trigger download + var blob = new Blob([response.data], { type: 'application/json' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'firewall_rules_export_' + Math.floor(Date.now() / 1000) + '.json'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + $scope.$apply(function() { + $scope.actionFailed = true; + $scope.actionSuccess = false; + }); + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Success!', + text: 'Firewall rules exported successfully', + type: 'success' + }); + } + }; + reader.readAsText(response.data); + } else { + // Handle as text response (shouldn't happen with blob) + $scope.actionFailed = true; + $scope.actionSuccess = false; } - - // If we get here, it's a successful file download - $scope.actionFailed = true; - $scope.actionSuccess = false; - } - - function exportError(response) { + }, function(error) { $scope.rulesLoading = true; $scope.actionFailed = false; $scope.actionSuccess = true; $scope.errorMessage = "Could not connect to server. Please refresh this page."; - } + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } + }); }; $scope.importRules = function () { @@ -3942,7 +4489,7 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) function importSuccess(response) { $scope.rulesLoading = true; - if (response.data.importStatus === 1) { + if (response.data && response.data.importStatus === 1) { $scope.actionFailed = true; $scope.actionSuccess = false; @@ -3950,20 +4497,38 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) populateCurrentRecords(); // Show import summary - var summary = `Import completed successfully!\n` + - `Imported: ${response.data.imported_count} rules\n` + - `Skipped: ${response.data.skipped_count} rules\n` + - `Errors: ${response.data.error_count} rules`; - - if (response.data.errors && response.data.errors.length > 0) { - summary += `\n\nErrors:\n${response.data.errors.join('\n')}`; + var message = `Import completed: ${response.data.imported_count} imported, ${response.data.skipped_count} skipped`; + if (response.data.error_count > 0) { + message += `, ${response.data.error_count} errors`; + if (response.data.errors && response.data.errors.length > 0) { + message += '\nErrors: ' + response.data.errors.slice(0, 5).join('; '); + if (response.data.errors.length > 5) { + message += ` ... and ${response.data.errors.length - 5} more`; + } + } } - alert(summary); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Completed!', + text: message, + type: response.data.error_count > 0 ? 'notice' : 'success' + }); + } else { + alert(message); + } } else { $scope.actionFailed = false; $scope.actionSuccess = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (response.data && response.data.error_message) || 'Failed to import firewall rules'; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Import Failed', + text: (response.data && response.data.error_message) || 'Failed to import firewall rules', + type: 'error' + }); + } } } @@ -3972,6 +4537,14 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) $scope.actionFailed = false; $scope.actionSuccess = true; $scope.errorMessage = "Could not connect to server. Please refresh this page."; + + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Connection Error', + text: 'Could not connect to server. Please refresh this page.', + type: 'error' + }); + } } } diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index a04e17f21..ff51e787f 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -618,7 +618,7 @@ } /* Modal Styles - Ensure it's above everything */ - #modifyBannedIPModal { + #modifyBannedIPModal, #modifyRuleModal { z-index: 99999 !important; position: fixed !important; top: 0 !important; @@ -627,7 +627,7 @@ height: 100% !important; } - #modifyBannedIPModal.show { + #modifyBannedIPModal.show, #modifyRuleModal.show { display: flex !important; opacity: 1 !important; visibility: visible !important; @@ -635,11 +635,11 @@ justify-content: center !important; } - #modifyBannedIPModal.fade.show { + #modifyBannedIPModal.fade.show, #modifyRuleModal.fade.show { opacity: 1 !important; } - #modifyBannedIPModal .modal-dialog { + #modifyBannedIPModal .modal-dialog, #modifyRuleModal .modal-dialog { z-index: 100000 !important; position: relative !important; margin: 1.75rem auto !important; @@ -664,7 +664,9 @@ /* Make sure modal is not hidden by parent containers */ .modern-container #modifyBannedIPModal, - .banned-ips-panel #modifyBannedIPModal { + .banned-ips-panel #modifyBannedIPModal, + .modern-container #modifyRuleModal, + .rules-panel #modifyRuleModal { position: fixed !important; z-index: 99999 !important; } @@ -851,6 +853,52 @@ transform: translateY(-1px); box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3); } + + .export-import-buttons { + display: flex; + gap: 0.75rem; + align-items: center; + } + + .btn-export, .btn-import { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + border: none; + } + + .btn-export { + background: #10b981; + color: white; + } + + .btn-export:hover:not(:disabled) { + background: #059669; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); + } + + .btn-import { + background: #3b82f6; + color: white; + } + + .btn-import:hover:not(:disabled) { + background: #2563eb; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); + } + + .btn-export:disabled, .btn-import:disabled { + opacity: 0.5; + cursor: not-allowed; + } /* Ensure actions column and buttons are always visible */ .banned-table td.actions { @@ -1114,7 +1162,14 @@ {$ rule.port $} - + + + + + @@ -1296,6 +1369,75 @@ + + +