Fix v2 API validation, error handling, and security hardening

- Use submitDBCreation/submitDBDeletion wrappers for database ops
  (adds package limits, uniqueness checks)
- Check return values from email, FTP, DNS, SSL, PHP utilities
- Run backup creation in multiprocessing.Process (non-blocking)
- Fix vhost config filename from vhconf.conf to vhost.conf
- Fix open_basedir detection to read actual vhost config file
- Scope DNS record deletion to site's own zone
- Add missing context vars (createWebsite, ipAddress)
- Add error handling to v2Fetch and log viewer component
- Add config_type and lines input validation
- Fix FTP form hidden input binding issue
- Add DeleteDocRoot key for domain deletion
- Cast isAlias/ssl to int for domain creation
This commit is contained in:
usmannasir
2026-02-24 02:39:36 +05:00
parent 97b3e18b56
commit 1b602fdc41
4 changed files with 87 additions and 35 deletions

View File

@@ -34,6 +34,9 @@ async function v2Fetch(url, options = {}) {
config.body = JSON.stringify(options.body);
}
const resp = await fetch(url, config);
if (!resp.ok) {
return { status: 0, error_message: 'Server error (' + resp.status + ')' };
}
return resp.json();
}
@@ -187,16 +190,20 @@ function logViewerComponent(apiUrl) {
logType: 'access',
lines: 100,
loading: false,
error: '',
async load() {
this.loading = true;
this.error = '';
try {
const data = await v2Fetch(apiUrl + '?type=' + this.logType + '&lines=' + this.lines);
if (data.status === 1) {
this.logs = data.logs || [];
} else {
this.error = data.error_message || 'Failed to load logs';
}
} catch (e) {
// silent
this.error = 'Failed to load logs';
}
this.loading = false;
},

View File

@@ -44,9 +44,8 @@
<input type="password" class="v2-input" x-model="formData.ftpPassword" placeholder="Strong password">
</div>
</div>
<input type="hidden" x-model="formData.ftpDomain" value="{{ website.domain }}">
<div style="display:flex; gap:8px; margin-top:8px;">
<button class="v2-btn v2-btn-primary" @click="formData.ftpDomain = '{{ website.domain }}'; create(formData)">Create</button>
<button class="v2-btn v2-btn-primary" @click="create(formData)">Create</button>
<button class="v2-btn v2-btn-outline" @click="showForm = false">Cancel</button>
</div>
</div>

View File

@@ -33,6 +33,11 @@
</div>
</div>
<!-- Alerts -->
<template x-if="error">
<div class="v2-alert v2-alert-danger"><i class="fas fa-exclamation-circle"></i> <span x-text="error"></span></div>
</template>
<!-- Log viewer -->
<div class="v2-card">
<div class="v2-card-header">

View File

@@ -1,4 +1,6 @@
import json
import os
import socket
from django.shortcuts import redirect, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from plogical.httpProc import httpProc
@@ -58,6 +60,17 @@ def _json(status, message, **extra):
return HttpResponse(json.dumps(data), content_type='application/json')
def _get_server_ip():
"""Get the server's public IP address."""
try:
return ACLManager.GetServerIP()
except:
try:
return socket.gethostbyname(socket.gethostname())
except:
return 'N/A'
# ---------------------------------------------------------------------------
# Page views
# ---------------------------------------------------------------------------
@@ -69,9 +82,11 @@ def site_list(request):
return _login_redirect()
websites = ACLManager.findWebsiteObjects(currentACL, userID)
createWebsite = ACLManager.currentContextPermission(currentACL, 'createWebsite')
data = {
'site_context': None,
'websites': websites,
'createWebsite': createWebsite,
}
proc = httpProc(request, 'panelv2/site_list.html', data, 'listWebsites')
return proc.render()
@@ -343,6 +358,7 @@ def site_security(request, site_id):
data = {
'site_context': _site_context(website),
'website': website,
'ipAddress': _get_server_ip(),
}
proc = httpProc(request, 'panelv2/security.html', data, 'listWebsites')
return proc.render()
@@ -387,12 +403,13 @@ def api_databases(request, site_id):
elif request.method == 'POST':
try:
data = json.loads(request.body)
data['databaseWebsite'] = website.domain
from plogical.mysqlUtilities import mysqlUtilities
mysqlUtilities.createDatabase(data['dbName'], data['dbUser'], data['dbPassword'])
newDB = Databases(website=website, dbName=data['dbName'], dbUser=data['dbUser'])
newDB.save()
return _json(1, 'None')
result = mysqlUtilities.submitDBCreation(
data['dbName'], data['dbUser'], data['dbPassword'], website.domain
)
if result[0] == 1:
return _json(1, 'None')
return _json(0, result[1])
except BaseException as msg:
return _json(0, str(msg))
@@ -401,9 +418,10 @@ def api_databases(request, site_id):
data = json.loads(request.body)
db = Databases.objects.get(pk=data['id'], website=website)
from plogical.mysqlUtilities import mysqlUtilities
mysqlUtilities.deleteDatabase(db.dbName, db.dbUser)
db.delete()
return _json(1, 'None')
result = mysqlUtilities.submitDBDeletion(db.dbName)
if result[0] == 1:
return _json(1, 'None')
return _json(0, result[1])
except BaseException as msg:
return _json(0, str(msg))
@@ -431,8 +449,10 @@ def api_email(request, site_id):
try:
data = json.loads(request.body)
from plogical.mailUtilities import mailUtilities
mailUtilities.createEmailAccount(data['domain'], data['username'], data['password'])
return _json(1, 'None')
result = mailUtilities.createEmailAccount(data['domain'], data['username'], data['password'])
if result[0] == 1:
return _json(1, 'None')
return _json(0, result[1])
except BaseException as msg:
return _json(0, str(msg))
@@ -440,8 +460,10 @@ def api_email(request, site_id):
try:
data = json.loads(request.body)
from plogical.mailUtilities import mailUtilities
mailUtilities.deleteEmailAccount(data['email'])
return _json(1, 'None')
result = mailUtilities.deleteEmailAccount(data['email'])
if result[0] == 1:
return _json(1, 'None')
return _json(0, result[1])
except BaseException as msg:
return _json(0, str(msg))
@@ -531,9 +553,12 @@ def api_dns(request, site_id):
elif request.method == 'DELETE':
try:
data = json.loads(request.body)
record = DNSRecords.objects.get(pk=data['id'])
dns_domain = DNSDomains.objects.get(name=website.domain)
record = DNSRecords.objects.get(pk=data['id'], domainOwner=dns_domain)
record.delete()
return _json(1, 'None')
except DNSDomains.DoesNotExist:
return _json(0, 'DNS zone not found for this domain')
except BaseException as msg:
return _json(0, str(msg))
@@ -592,12 +617,15 @@ def api_backup(request, site_id):
elif request.method == 'POST':
try:
import time
from multiprocessing import Process
from plogical.backupUtilities import submitBackupCreation
backupDomain = website.domain
backupName = 'backup-%s-%s' % (backupDomain, time.strftime('%m.%d.%Y_%H-%M-%S'))
backupPath = '/home/%s/backup' % backupDomain
tempStoragePath = '%s/%s' % (backupPath, backupName)
submitBackupCreation(tempStoragePath, backupName, backupPath, backupDomain)
p = Process(target=submitBackupCreation,
args=(tempStoragePath, backupName, backupPath, backupDomain))
p.start()
return _json(1, 'None')
except BaseException as msg:
return _json(0, str(msg))
@@ -606,7 +634,6 @@ def api_backup(request, site_id):
try:
data = json.loads(request.body)
backup = Backups.objects.get(pk=data['id'], website=website)
import os
backup_file = '/home/%s/backup/%s' % (website.domain, backup.fileName)
if os.path.exists(backup_file):
os.remove(backup_file)
@@ -630,7 +657,6 @@ def api_logs(request, site_id):
return _json(0, 'Unauthorized or site not found')
if request.method == 'GET':
import os
log_type = request.GET.get('type', 'access')
if log_type not in ('access', 'error'):
log_type = 'access'
@@ -638,6 +664,8 @@ def api_logs(request, site_id):
lines = int(request.GET.get('lines', 50))
except (ValueError, TypeError):
lines = 50
if lines < 1:
lines = 50
if lines > 1000:
lines = 1000
@@ -650,8 +678,11 @@ def api_logs(request, site_id):
if os.path.exists(log_path):
try:
from plogical.processUtilities import ProcessUtilities
result = ProcessUtilities.outputExecutioner('tail -n %d %s' % (lines, log_path))
log_lines = result.strip().split('\n') if result.strip() else []
result = ProcessUtilities.outputExecutioner(
'tail -n %d %s' % (lines, log_path)
)
if result and result.strip():
log_lines = result.strip().split('\n')
except:
pass
@@ -673,10 +704,11 @@ def api_config(request, site_id):
if request.method == 'GET':
config_type = request.GET.get('type', 'vhost')
import os
if config_type not in ('vhost', 'rewrite'):
config_type = 'vhost'
content = ''
if config_type == 'vhost':
vhost_path = '/usr/local/lsws/conf/vhosts/%s/vhconf.conf' % website.domain
vhost_path = '/usr/local/lsws/conf/vhosts/%s/vhost.conf' % website.domain
if os.path.exists(vhost_path):
with open(vhost_path, 'r') as f:
content = f.read()
@@ -700,10 +732,12 @@ def api_config(request, site_id):
elif config_type == 'php':
from plogical.vhost import vhost
vhFile = '/usr/local/lsws/conf/vhosts/%s/vhost.conf' % website.domain
vhost.changePHP(vhFile, data['phpVersion'])
website.phpSelection = data['phpVersion']
website.save()
return _json(1, 'None')
result = vhost.changePHP(vhFile, data['phpVersion'])
if result[0] == 1:
website.phpSelection = data['phpVersion']
website.save()
return _json(1, 'None')
return _json(0, str(result[1]))
return _json(0, 'Unknown config type')
except BaseException as msg:
return _json(0, str(msg))
@@ -737,8 +771,8 @@ def api_domains(request, site_id):
'domainName': data['domain'],
'path': data.get('path', ''),
'phpSelection': data.get('php', website.phpSelection),
'ssl': data.get('ssl', 0),
'alias': data.get('isAlias', 0),
'ssl': int(data.get('ssl', 0)),
'alias': int(data.get('isAlias', 0)),
'openBasedir': 1,
}
result = wm.submitDomainCreation(userID, create_data)
@@ -756,7 +790,7 @@ def api_domains(request, site_id):
child = ChildDomains.objects.get(pk=data['id'], master=website)
from websiteFunctions.website import WebsiteManager
wm = WebsiteManager()
delete_data = {'websiteName': child.domain}
delete_data = {'websiteName': child.domain, 'DeleteDocRoot': 0}
result = wm.submitDomainDeletion(userID, delete_data)
result_data = json.loads(result.content)
if result_data.get('status') == 1:
@@ -780,11 +814,18 @@ def api_security(request, site_id):
return _json(0, 'Unauthorized or site not found')
if request.method == 'GET':
try:
config = json.loads(website.config) if website.config else {}
except:
config = {}
open_basedir = config.get('openBasedir', 1)
# Check open_basedir by reading the vhost config file
open_basedir = 1 # default to enabled
vhost_path = '/usr/local/lsws/conf/vhosts/%s/vhost.conf' % website.domain
if os.path.exists(vhost_path):
try:
with open(vhost_path, 'r') as f:
vhost_content = f.read()
# If no php_admin_value open_basedir line exists, it's disabled
if 'open_basedir' not in vhost_content:
open_basedir = 0
except:
pass
return _json(1, 'None', openBasedir=open_basedir)
elif request.method == 'POST':