mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-10-26 00:36:34 +02:00
Add firewall rule management features and enhance repository setup
- Implemented functionality to edit existing firewall rules, including validation and error handling. - Added endpoints for exporting and importing firewall rules in JSON format, allowing users to manage rules more efficiently. - Enhanced the user interface with modals for editing rules and buttons for exporting/importing rules. - Updated the `cyberpanel.sh` script to support AlmaLinux 10 and improved LiteSpeed GPG key import with fallback options. - Refactored repository setup to accommodate different OS versions, ensuring compatibility with CentOS and AlmaLinux.
This commit is contained in:
@@ -270,6 +270,11 @@ setup_epel_repo() {
|
||||
yum install -y https://cyberpanel.sh/dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
|
||||
Check_Return "yum repo" "no_exit"
|
||||
;;
|
||||
"10")
|
||||
# AlmaLinux 10 EPEL support
|
||||
yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
|
||||
Check_Return "yum repo" "no_exit"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -309,11 +314,12 @@ gpgcheck=1
|
||||
EOF
|
||||
elif [[ "$Server_OS_Version" = "10" ]] && uname -m | grep -q 'x86_64'; then
|
||||
cat <<EOF >/etc/yum.repos.d/MariaDB.repo
|
||||
# MariaDB 10.11 CentOS repository list - created 2021-08-06 02:01 UTC
|
||||
# MariaDB 10.11 RHEL10 repository list - AlmaLinux 10 compatible
|
||||
# http://downloads.mariadb.org/mariadb/repositories/
|
||||
[mariadb]
|
||||
name = MariaDB
|
||||
baseurl = http://yum.mariadb.org/10.11/rhel9-amd64/
|
||||
baseurl = http://yum.mariadb.org/10.11/rhel10-amd64/
|
||||
module_hotfixes=1
|
||||
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
@@ -1117,8 +1123,14 @@ log_function_start "Pre_Install_Setup_Repository"
|
||||
log_info "Setting up package repositories for $Server_OS $Server_OS_Version"
|
||||
if [[ $Server_OS = "CentOS" ]] ; then
|
||||
log_debug "Importing LiteSpeed GPG key"
|
||||
rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed
|
||||
#import the LiteSpeed GPG key
|
||||
# Import LiteSpeed GPG key with fallback
|
||||
rpm --import https://cyberpanel.sh/rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || {
|
||||
warning "Primary GPG key import failed, trying alternative source"
|
||||
rpm --import https://rpms.litespeedtech.com/centos/RPM-GPG-KEY-litespeed || {
|
||||
error "Failed to import LiteSpeed GPG key from all sources"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
yum clean all
|
||||
yum autoremove -y epel-release
|
||||
@@ -1151,7 +1163,12 @@ if [[ $Server_OS = "CentOS" ]] ; then
|
||||
dnf config-manager --set-enabled crb
|
||||
fi
|
||||
|
||||
yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
|
||||
# Install appropriate remi-release based on version
|
||||
if [[ "$Server_OS_Version" = "9" ]]; then
|
||||
yum install -y https://rpms.remirepo.net/enterprise/remi-release-9.rpm
|
||||
elif [[ "$Server_OS_Version" = "10" ]]; then
|
||||
yum install -y https://rpms.remirepo.net/enterprise/remi-release-10.rpm
|
||||
fi
|
||||
Check_Return "yum repo" "no_exit"
|
||||
fi
|
||||
|
||||
@@ -1341,8 +1358,22 @@ if [[ "$Server_OS" = "CentOS" ]] || [[ "$Server_OS" = "openEuler" ]] ; then
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel
|
||||
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel MariaDB-server MariaDB-client MariaDB-devel curl-devel git platform-python-devel tar socat python3 zip unzip bind-utils gpgme-devel openssl-devel boost-devel boost-program-options
|
||||
Check_Return
|
||||
|
||||
# Fix boost library compatibility for galera-4 on AlmaLinux 10
|
||||
if [[ "$Server_OS_Version" = "10" ]]; then
|
||||
# Create symlink for boost libraries if needed
|
||||
if [ ! -f /usr/lib64/libboost_program_options.so.1.75.0 ]; then
|
||||
BOOST_VERSION=$(find /usr/lib64 -name "libboost_program_options.so.*" | head -1 | sed 's/.*libboost_program_options\.so\.//')
|
||||
if [ -n "$BOOST_VERSION" ]; then
|
||||
ln -sf /usr/lib64/libboost_program_options.so.$BOOST_VERSION /usr/lib64/libboost_program_options.so.1.75.0
|
||||
log_info "Created boost library symlink for galera-4 compatibility: $BOOST_VERSION -> 1.75.0"
|
||||
else
|
||||
warning "Could not find boost libraries, galera-4 may not work properly"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
elif [[ "$Server_OS_Version" = "20" ]] || [[ "$Server_OS_Version" = "22" ]] || [[ "$Server_OS_Version" = "24" ]] ; then
|
||||
dnf install -y libnsl zip wget strace net-tools curl which bc telnet htop libevent-devel gcc libattr-devel xz-devel mariadb-devel curl-devel git python3-devel tar socat python3 zip unzip bind-utils gpgme-devel
|
||||
Check_Return
|
||||
|
||||
121
firewall/EXPORT_IMPORT_FIREWALL_RULES.md
Normal file
121
firewall/EXPORT_IMPORT_FIREWALL_RULES.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Firewall Rules Export/Import Feature
|
||||
|
||||
## Overview
|
||||
|
||||
This feature allows CyberPanel administrators to export and import firewall rules between servers, making it easy to replicate security configurations across multiple servers.
|
||||
|
||||
## Features
|
||||
|
||||
### Export Functionality
|
||||
- Exports all custom firewall rules to a JSON file
|
||||
- Excludes default CyberPanel rules (CyberPanel Admin, SSHCustom) to prevent conflicts
|
||||
- Includes metadata such as export timestamp and rule count
|
||||
- Downloads file directly to the user's browser
|
||||
|
||||
### Import Functionality
|
||||
- Imports firewall rules from a previously exported JSON file
|
||||
- Validates file format before processing
|
||||
- Skips duplicate rules (same name, protocol, port, and IP address)
|
||||
- Excludes default CyberPanel rules from import
|
||||
- Provides detailed import summary (imported, skipped, error counts)
|
||||
- Shows specific error messages for failed imports
|
||||
|
||||
## Usage
|
||||
|
||||
### Exporting Rules
|
||||
1. Navigate to the Firewall section in CyberPanel
|
||||
2. Click the "Export Rules" button in the Firewall Rules panel header
|
||||
3. The system will generate and download a JSON file containing your custom rules
|
||||
|
||||
### Importing Rules
|
||||
1. Navigate to the Firewall section in CyberPanel
|
||||
2. Click the "Import Rules" button in the Firewall Rules panel header
|
||||
3. Select a previously exported JSON file
|
||||
4. The system will process the import and show a summary of results
|
||||
|
||||
## File Format
|
||||
|
||||
The exported JSON file has the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"exported_at": "2024-01-15 14:30:25",
|
||||
"total_rules": 5,
|
||||
"rules": [
|
||||
{
|
||||
"name": "Custom Web Server",
|
||||
"proto": "tcp",
|
||||
"port": "8080",
|
||||
"ipAddress": "0.0.0.0/0"
|
||||
},
|
||||
{
|
||||
"name": "Database Access",
|
||||
"proto": "tcp",
|
||||
"port": "3306",
|
||||
"ipAddress": "192.168.1.0/24"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Only administrators can export/import firewall rules
|
||||
- Default CyberPanel rules are excluded to prevent system conflicts
|
||||
- Import process validates file format and rule data
|
||||
- Failed imports are logged for troubleshooting
|
||||
- Duplicate rules are automatically skipped
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system provides comprehensive error handling:
|
||||
- Invalid file format detection
|
||||
- Missing required fields validation
|
||||
- Individual rule import error tracking
|
||||
- Detailed error messages for troubleshooting
|
||||
- Import summary with counts of successful, skipped, and failed imports
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Backend Components
|
||||
- `exportFirewallRules()` method in `FirewallManager`
|
||||
- `importFirewallRules()` method in `FirewallManager`
|
||||
- New URL patterns for export/import endpoints
|
||||
- File upload handling for import functionality
|
||||
|
||||
### Frontend Components
|
||||
- Export/Import buttons in firewall UI
|
||||
- File download handling for exports
|
||||
- File upload dialog for imports
|
||||
- Progress indicators and error messaging
|
||||
- Import summary display
|
||||
|
||||
### Database Integration
|
||||
- Uses existing `FirewallRules` model
|
||||
- Maintains referential integrity
|
||||
- Preserves rule relationships and constraints
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Time Efficiency**: Significantly reduces time to replicate firewall rules across servers
|
||||
2. **Error Reduction**: Minimizes human error in manual rule creation
|
||||
3. **Consistency**: Ensures identical security policies across multiple servers
|
||||
4. **Backup**: Provides a way to backup and restore firewall configurations
|
||||
5. **Migration**: Simplifies server migration and setup processes
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Compatible with CyberPanel's existing firewall system
|
||||
- Works with both TCP and UDP protocols
|
||||
- Supports all IP address formats (single IPs, CIDR ranges)
|
||||
- Maintains compatibility with existing firewall utilities
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements could include:
|
||||
- Rule conflict detection and resolution
|
||||
- Selective rule import (choose specific rules)
|
||||
- Rule templates and presets
|
||||
- Bulk rule management
|
||||
- Integration with configuration management tools
|
||||
@@ -168,6 +168,79 @@ class FirewallManager:
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def editRule(self, userID = None, data = None):
|
||||
"""
|
||||
Edit an existing firewall rule
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson('edit_status', 0)
|
||||
|
||||
ruleID = data['id']
|
||||
newRuleName = data['ruleName']
|
||||
newRuleProtocol = data['ruleProtocol']
|
||||
newRulePort = data['rulePort']
|
||||
newRuleIP = data['ruleIP']
|
||||
|
||||
# Get the existing rule
|
||||
try:
|
||||
existingRule = FirewallRules.objects.get(id=ruleID)
|
||||
except FirewallRules.DoesNotExist:
|
||||
final_dic = {'status': 0, 'edit_status': 0, 'error_message': 'Rule not found'}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
# Store old values for system firewall update
|
||||
oldProtocol = existingRule.proto
|
||||
oldPort = existingRule.port
|
||||
oldIP = existingRule.ipAddress
|
||||
|
||||
# Check if any values actually changed
|
||||
if (existingRule.name == newRuleName and
|
||||
existingRule.proto == newRuleProtocol and
|
||||
existingRule.port == newRulePort and
|
||||
existingRule.ipAddress == newRuleIP):
|
||||
final_dic = {'status': 1, 'edit_status': 1, 'error_message': "No changes detected"}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
# Check if another rule with the same name already exists (excluding current rule)
|
||||
if existingRule.name != newRuleName:
|
||||
duplicateRule = FirewallRules.objects.filter(name=newRuleName).exclude(id=ruleID).first()
|
||||
if duplicateRule:
|
||||
final_dic = {'status': 0, 'edit_status': 0, 'error_message': f'A rule with name "{newRuleName}" already exists'}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
# Update the rule in the system firewall
|
||||
# First remove the old rule
|
||||
FirewallUtilities.deleteRule(oldProtocol, oldPort, oldIP)
|
||||
|
||||
# Then add the new rule
|
||||
FirewallUtilities.addRule(newRuleProtocol, newRulePort, newRuleIP)
|
||||
|
||||
# Update the database record
|
||||
existingRule.name = newRuleName
|
||||
existingRule.proto = newRuleProtocol
|
||||
existingRule.port = newRulePort
|
||||
existingRule.ipAddress = newRuleIP
|
||||
existingRule.save()
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Firewall rule edited successfully. ID: {ruleID}, Name: {newRuleName}")
|
||||
|
||||
final_dic = {'status': 1, 'edit_status': 1, 'error_message': "None"}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
except BaseException as msg:
|
||||
final_dic = {'status': 0, 'edit_status': 0, 'error_message': str(msg)}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def reloadFirewall(self, userID = None, data = None):
|
||||
try:
|
||||
|
||||
@@ -1755,6 +1828,164 @@ class FirewallManager:
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
def exportFirewallRules(self, userID = None):
|
||||
"""
|
||||
Export all custom firewall rules to a JSON file, excluding default CyberPanel rules
|
||||
"""
|
||||
try:
|
||||
currentACL = ACLManager.loadedACL(userID)
|
||||
|
||||
if currentACL['admin'] == 1:
|
||||
pass
|
||||
else:
|
||||
return ACLManager.loadErrorJson('exportStatus', 0)
|
||||
|
||||
# Get all firewall rules
|
||||
rules = FirewallRules.objects.all()
|
||||
|
||||
# Default CyberPanel rules to exclude
|
||||
default_rules = ['CyberPanel Admin', 'SSHCustom']
|
||||
|
||||
# Filter out default rules
|
||||
custom_rules = []
|
||||
for rule in rules:
|
||||
if rule.name not in default_rules:
|
||||
custom_rules.append({
|
||||
'name': rule.name,
|
||||
'proto': rule.proto,
|
||||
'port': rule.port,
|
||||
'ipAddress': rule.ipAddress
|
||||
})
|
||||
|
||||
# Create export data with metadata
|
||||
export_data = {
|
||||
'version': '1.0',
|
||||
'exported_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'total_rules': len(custom_rules),
|
||||
'rules': custom_rules
|
||||
}
|
||||
|
||||
# Create JSON response with file download
|
||||
json_content = json.dumps(export_data, indent=2)
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules exported successfully. Total rules: {len(custom_rules)}")
|
||||
|
||||
# Return file as download
|
||||
response = HttpResponse(json_content, content_type='application/json')
|
||||
response['Content-Disposition'] = f'attachment; filename="firewall_rules_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 importFirewallRules(self, userID = None, data = None):
|
||||
"""
|
||||
Import firewall rules 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 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 'rules' not in import_data:
|
||||
final_dic = {'importStatus': 0, 'error_message': 'Invalid import file format. Missing rules array.'}
|
||||
final_json = json.dumps(final_dic)
|
||||
return HttpResponse(final_json)
|
||||
|
||||
imported_count = 0
|
||||
skipped_count = 0
|
||||
error_count = 0
|
||||
errors = []
|
||||
|
||||
# Default CyberPanel rules to exclude from import
|
||||
default_rules = ['CyberPanel Admin', 'SSHCustom']
|
||||
|
||||
for rule_data in import_data['rules']:
|
||||
try:
|
||||
# Skip default rules
|
||||
if rule_data.get('name', '') in default_rules:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Check if rule already exists
|
||||
existing_rule = FirewallRules.objects.filter(
|
||||
name=rule_data['name'],
|
||||
proto=rule_data['proto'],
|
||||
port=rule_data['port'],
|
||||
ipAddress=rule_data['ipAddress']
|
||||
).first()
|
||||
|
||||
if existing_rule:
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Add the rule to the system firewall
|
||||
FirewallUtilities.addRule(
|
||||
rule_data['proto'],
|
||||
rule_data['port'],
|
||||
rule_data['ipAddress']
|
||||
)
|
||||
|
||||
# Add the rule to the database
|
||||
new_rule = FirewallRules(
|
||||
name=rule_data['name'],
|
||||
proto=rule_data['proto'],
|
||||
port=rule_data['port'],
|
||||
ipAddress=rule_data['ipAddress']
|
||||
)
|
||||
new_rule.save()
|
||||
|
||||
imported_count += 1
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
errors.append(f"Rule '{rule_data.get('name', 'Unknown')}': {str(e)}")
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Error importing rule {rule_data.get('name', 'Unknown')}: {str(e)}")
|
||||
|
||||
logging.CyberCPLogFileWriter.writeToFile(f"Firewall rules 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ app.controller('firewallController', function ($scope, $http) {
|
||||
$scope.couldNotConnect = true;
|
||||
$scope.rulesDetails = false;
|
||||
|
||||
// Edit modal variables
|
||||
$scope.showEditModal = false;
|
||||
$scope.editingRule = {};
|
||||
|
||||
firewallStatus();
|
||||
|
||||
populateCurrentRecords();
|
||||
@@ -503,6 +507,249 @@ app.controller('firewallController', function ($scope, $http) {
|
||||
|
||||
};
|
||||
|
||||
// Export/Import Functions
|
||||
$scope.exportRules = function () {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
url = "/firewall/exportFirewallRules";
|
||||
|
||||
var data = {};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(exportSuccess, exportError);
|
||||
|
||||
function exportSuccess(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;
|
||||
}
|
||||
} catch (e) {
|
||||
// If not JSON, assume it's the file content
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's a successful file download
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
}
|
||||
|
||||
function exportError(response) {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
|
||||
}
|
||||
};
|
||||
|
||||
$scope.importRules = 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.rules || !Array.isArray(importData.rules)) {
|
||||
$scope.$apply(function() {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Invalid import file format. Please select a valid firewall rules export file.";
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload file to server
|
||||
uploadImportFile(file);
|
||||
} catch (error) {
|
||||
$scope.$apply(function() {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Invalid JSON file. Please select a valid firewall rules export file.";
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
document.body.removeChild(input);
|
||||
};
|
||||
|
||||
function uploadImportFile(file) {
|
||||
$scope.rulesLoading = false;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = 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/importFirewallRules", formData, config).then(importSuccess, importError);
|
||||
|
||||
function importSuccess(response) {
|
||||
$scope.rulesLoading = true;
|
||||
|
||||
if (response.data.importStatus === 1) {
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
|
||||
// Refresh rules list
|
||||
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')}`;
|
||||
}
|
||||
|
||||
alert(summary);
|
||||
} else {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}
|
||||
|
||||
function importError(response) {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
|
||||
}
|
||||
}
|
||||
|
||||
// Edit Rule Functions
|
||||
$scope.editRule = function(rule) {
|
||||
$scope.editingRule = {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
proto: rule.proto,
|
||||
port: rule.port,
|
||||
ipAddress: rule.ipAddress
|
||||
};
|
||||
$scope.showEditModal = true;
|
||||
};
|
||||
|
||||
$scope.closeEditModal = function() {
|
||||
$scope.showEditModal = false;
|
||||
$scope.editingRule = {};
|
||||
};
|
||||
|
||||
$scope.saveEditedRule = function() {
|
||||
// Basic validation
|
||||
if (!$scope.editingRule.name || !$scope.editingRule.port || !$scope.editingRule.ipAddress) {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Please fill in all required fields.";
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.rulesLoading = false;
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = true;
|
||||
|
||||
url = "/firewall/editRule";
|
||||
|
||||
var data = {
|
||||
id: $scope.editingRule.id,
|
||||
ruleName: $scope.editingRule.name,
|
||||
ruleProtocol: $scope.editingRule.proto,
|
||||
rulePort: $scope.editingRule.port,
|
||||
ruleIP: $scope.editingRule.ipAddress
|
||||
};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(editSuccess, editError);
|
||||
|
||||
function editSuccess(response) {
|
||||
$scope.rulesLoading = true;
|
||||
|
||||
if (response.data.edit_status === 1) {
|
||||
// Close modal and refresh rules
|
||||
$scope.closeEditModal();
|
||||
populateCurrentRecords();
|
||||
|
||||
$scope.actionFailed = true;
|
||||
$scope.actionSuccess = false;
|
||||
} else {
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = response.data.error_message;
|
||||
}
|
||||
}
|
||||
|
||||
function editError(response) {
|
||||
$scope.rulesLoading = true;
|
||||
$scope.actionFailed = false;
|
||||
$scope.actionSuccess = true;
|
||||
$scope.errorMessage = "Could not connect to server. Please refresh this page.";
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal when clicking outside or pressing Escape
|
||||
$scope.$on('$locationChangeStart', function() {
|
||||
$scope.closeEditModal();
|
||||
});
|
||||
|
||||
// Keyboard support for modal
|
||||
$(document).on('keydown', function(e) {
|
||||
if ($scope.showEditModal && e.keyCode === 27) { // Escape key
|
||||
$scope.$apply(function() {
|
||||
$scope.closeEditModal();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Focus management for modal
|
||||
$scope.$watch('showEditModal', function(newVal) {
|
||||
if (newVal) {
|
||||
setTimeout(function() {
|
||||
$('#editRuleModal input[name="ruleName"]').focus();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -421,6 +421,205 @@
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: var(--bg-info-light, #dbeafe);
|
||||
color: var(--info-color, #3b82f6);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: var(--info-color, #3b82f6);
|
||||
color: var(--text-light, white);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Export/Import Buttons */
|
||||
.export-import-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-export, .btn-import {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-light, white);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.btn-export:hover, .btn-import:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-export:disabled, .btn-import:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Edit Modal Styles */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-secondary, white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, var(--firewall-gradient-start, #ef4444) 0%, var(--firewall-gradient-end, #dc2626) 100%);
|
||||
color: var(--text-light, white);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: var(--text-light, white);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.edit-rule-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid var(--border-color, #e8e9ff);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
background: var(--bg-tertiary, #f8f9ff);
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-save {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 120px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: var(--bg-muted, #f1f5f9);
|
||||
color: var(--text-secondary, #475569);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: var(--bg-muted-hover, #e2e8f0);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: var(--firewall-accent, #ef4444);
|
||||
color: var(--text-light, white);
|
||||
}
|
||||
|
||||
.btn-save:hover {
|
||||
background: var(--firewall-accent-dark, #dc2626);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px var(--firewall-shadow, rgba(239, 68, 68, 0.3));
|
||||
}
|
||||
|
||||
.btn-save:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
@@ -600,7 +799,25 @@
|
||||
</div>
|
||||
{% trans "Firewall Rules" %}
|
||||
</div>
|
||||
<div ng-show="rulesLoading" class="loading-spinner"></div>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div ng-show="rulesLoading" class="loading-spinner"></div>
|
||||
<div class="export-import-buttons">
|
||||
<button type="button"
|
||||
ng-click="exportRules()"
|
||||
class="btn-export"
|
||||
ng-disabled="rulesLoading">
|
||||
<i class="fas fa-download"></i>
|
||||
{% trans "Export Rules" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="importRules()"
|
||||
class="btn-import"
|
||||
ng-disabled="rulesLoading">
|
||||
<i class="fas fa-upload"></i>
|
||||
{% trans "Import Rules" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Rule Section -->
|
||||
@@ -680,12 +897,20 @@
|
||||
<span class="port-number">{$ rule.port $}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
|
||||
class="btn-delete"
|
||||
title="{% trans 'Delete Rule' %}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button type="button"
|
||||
ng-click="editRule(rule)"
|
||||
class="btn-edit"
|
||||
title="{% trans 'Edit Rule' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
ng-click="deleteRule(rule.id, rule.proto, rule.port, rule.ipAddress)"
|
||||
class="btn-delete"
|
||||
title="{% trans 'Delete Rule' %}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -717,6 +942,75 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Rule Modal -->
|
||||
<div id="editRuleModal" class="modal" ng-show="showEditModal" ng-click="closeEditModal()">
|
||||
<div class="modal-content" ng-click="$event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Edit Firewall Rule" %}
|
||||
</h3>
|
||||
<button type="button" class="modal-close" ng-click="closeEditModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="edit-rule-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Rule Name" %}</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="editingRule.name"
|
||||
name="ruleName"
|
||||
placeholder="{% trans 'e.g., Allow SSH' %}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Protocol" %}</label>
|
||||
<select ng-model="editingRule.proto" class="form-control select-control">
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "IP Address" %}</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="editingRule.ipAddress"
|
||||
placeholder="{% trans '0.0.0.0/0 for all IPs' %}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">{% trans "Port" %}</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="editingRule.port"
|
||||
placeholder="{% trans 'e.g., 80' %}"
|
||||
required>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button"
|
||||
class="btn-cancel"
|
||||
ng-click="closeEditModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn-save"
|
||||
ng-click="saveEditedRule()"
|
||||
ng-disabled="rulesLoading">
|
||||
<i class="fas fa-save"></i>
|
||||
{% trans "Save Changes" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -32,6 +32,13 @@ urlpatterns = [
|
||||
path('modSecRulesPacks', views.modSecRulesPacks, name='modSecRulesPacks'),
|
||||
path('getOWASPAndComodoStatus', views.getOWASPAndComodoStatus, name='getOWASPAndComodoStatus'),
|
||||
path('installModSecRulesPack', views.installModSecRulesPack, name='installModSecRulesPack'),
|
||||
|
||||
# Firewall Export/Import
|
||||
path('exportFirewallRules', views.exportFirewallRules, name='exportFirewallRules'),
|
||||
path('importFirewallRules', views.importFirewallRules, name='importFirewallRules'),
|
||||
|
||||
# Firewall Rule Edit
|
||||
path('editRule', views.editRule, name='editRule'),
|
||||
path('getRulesFiles', views.getRulesFiles, name='getRulesFiles'),
|
||||
path('enableDisableRuleFile', views.enableDisableRuleFile, name='enableDisableRuleFile'),
|
||||
|
||||
|
||||
@@ -648,3 +648,36 @@ def saveLitespeed_conf(request):
|
||||
return fm.saveLitespeed_conf(userID, json.loads(request.body))
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def exportFirewallRules(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.exportFirewallRules(userID)
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def importFirewallRules(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager(request)
|
||||
|
||||
# Handle file upload
|
||||
if request.method == 'POST' and 'import_file' in request.FILES:
|
||||
return fm.importFirewallRules(userID, None)
|
||||
else:
|
||||
# Handle JSON data
|
||||
return fm.importFirewallRules(userID, json.loads(request.body))
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
|
||||
|
||||
def editRule(request):
|
||||
try:
|
||||
userID = request.session['userID']
|
||||
fm = FirewallManager()
|
||||
return fm.editRule(userID, json.loads(request.body))
|
||||
except KeyError:
|
||||
return redirect(loadLoginPage)
|
||||
20
key.pem
20
key.pem
@@ -1,20 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDNDCCAhwCCQDEgz2Vkmv5NDANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV
|
||||
UzEPMA0GA1UECAwGRGVuaWFsMRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UE
|
||||
CgwDRGlzMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wHhcNMjIwODI2MTEwNzEy
|
||||
WhcNMzIwODIzMTEwNzEyWjBcMQswCQYDVQQGEwJVUzEPMA0GA1UECAwGRGVuaWFs
|
||||
MRQwEgYDVQQHDAtTcHJpbmdmaWVsZDEMMAoGA1UECgwDRGlzMRgwFgYDVQQDDA93
|
||||
d3cuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq
|
||||
BesL5DNNansmBYyn0DSqJyBOwGEIH/Ur4KFbKICrhy376gec2UyGG5lurgQa8Nz6
|
||||
Gt7Z1B9LbLVE3bXS1f82bJHyPFUP8WmQuC+/3ZMRPFiG7/4n//0QwMptkDvGb5E1
|
||||
0NXfJjuGHNfpVaHAs83v9mvUKc7oOjxwa+lbkhobD8HKByzAi/fpD90WQS9JRUJX
|
||||
8lGquw+k5flL31AkqCyZOJw22VEoIpoF7RSh0xZvpsLze6G1thY5R27YWChcOR0E
|
||||
m9/TUZ9/oCaP3WBvCldjr5OT6eZxJJzlt1jYndQKUyO5OtaJuNd4MkCNz9u9wwjE
|
||||
CfZF7VQj8FABR9tqcVEfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGCNMmGXem6k
|
||||
iqvJG2eFvuSdIh+x7x8EnXpRi2lW0ZqLYZfZuiMQL1fedhVoix+rzo9/Qhj6BTnt
|
||||
5cC5oexhj/s76gqehNatMC0HAbIcK+MpvmwNoA/7U/bQAlbNxR3aLKSV0/B5YlTP
|
||||
9yUoMtBqGEiesqAVAD26jOG5Ch1fHHElPtp3rE6qxGsAL0Eu+2Gezq3OqW2ejlvY
|
||||
hZFpB/ZEynmYDUjT02+2J+3bAhfGaeUXC75YsbyfRAdc0OHa9/r7RhK+tgzmUhJt
|
||||
sKUDW2OIwOTumUOSDgh1ayeTBddRAcMyIoGFeHF9degfJFiSo4vQrmaqFr9/bUFp
|
||||
pn+y1/A3gNY=
|
||||
-----END CERTIFICATE-----
|
||||
Reference in New Issue
Block a user