Rspamd: cross-OS installer fixes and proxied Web UI at /emailPremium/Rspamd/ui/

- plogical/mailUtilities: ensure config dirs before writes; fix broken
  post-install indentation; write install success on EL; EL7 uses yum;
  create /etc/yum.repos.d when adding rspamd.repo
- plogical/processUtilities: broader RHEL family detection (Debian ID,
  CentOS Stream, RHEL, Rocky 10, openEuler/Virtuozzo, os-release fallback)
- emailPremium: admin-only reverse proxy to 127.0.0.1:11334; url route
  Rspamd/ui; safe machineIP read; rspamd_ui_url for template
- Rspamd template: primary Open Rspamd Web UI link; action-buttons anchor
  styling; SSH tunnel as alternative
This commit is contained in:
master3395
2026-04-10 00:38:31 +02:00
parent 85571c917d
commit 352eebf0b5
5 changed files with 365 additions and 87 deletions

View File

@@ -89,6 +89,15 @@
flex-wrap: wrap;
margin-bottom: 25px;
}
/* Anchor styled as button (Open Web UI link) — same visual weight as <button class="action-btn"> */
.action-buttons a.action-btn {
display: inline-flex;
align-items: center;
text-decoration: none;
box-sizing: border-box;
vertical-align: middle;
}
.action-btn {
background: #5856d6;
@@ -636,7 +645,11 @@
<div class="content-section">
<h2 class="section-title">{% trans "Rspamd Management" %}</h2>
<div class="action-buttons">
<button type="button" data-toggle="modal" data-target="#ViewRspamdlog" ng-click="FetchRspamdLog()" class="action-btn primary">
<a href="{{ rspamd_ui_url }}" target="_blank" rel="noopener noreferrer" class="action-btn primary">
<i class="fas fa-window-maximize"></i>
{% trans "Open Rspamd Web UI" %}
</a>
<button type="button" data-toggle="modal" data-target="#ViewRspamdlog" ng-click="FetchRspamdLog()" class="action-btn secondary">
<i class="fas fa-file-alt"></i>
{% trans "View Rspamd Logs" %}
</button>
@@ -649,6 +662,23 @@
{% trans "Uninstall Rspamd" %}
</button>
</div>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; margin-top: 16px; line-height: 1.6;">
{% trans "Opens the official Rspamd web interface in a new tab (path /emailPremium/Rspamd/ui/ — proxied through CyberPanel, admin session required)." %}
</p>
</div>
<div class="content-section">
<h2 class="section-title">{% trans "Alternative: SSH tunnel" %}</h2>
<p style="color: var(--text-secondary, #64748b); font-size: 14px; line-height: 1.6;">
{% trans "If the proxied UI misbehaves, connect to port 11334 on the server via SSH and use your local browser." %}
</p>
<pre style="background: var(--bg-secondary,#f8f9ff); padding: 12px; border-radius: 8px; font-size: 13px; overflow-x: auto; border: 1px solid var(--border-light, #e8e9ff);">ssh -N -L 11334:127.0.0.1:11334 root@{{ ipAddress }}</pre>
<p style="margin-top: 12px;">
<a href="http://127.0.0.1:11334/" target="_blank" rel="noopener noreferrer" class="action-btn secondary">
<i class="fas fa-external-link-alt"></i>
{% trans "Open Rspamd UI (when tunnel is active)" %}
</a>
</p>
</div>
<!-- ClamAV Configuration -->

View File

@@ -33,7 +33,8 @@ urlpatterns = [
path('installMailScanner', views.installMailScanner, name='installMailScanner'),
path('installStatusMailScanner', views.installStatusMailScanner, name='installStatusMailScanner'),
# Rspamd
# Rspamd (proxied controller UI — must stay above catch-all domain route)
re_path(r'^Rspamd/ui(?:/(?P<subpath>.*))?$', views.rspamd_ui_proxy, name='RspamdUI'),
path('Rspamd', views.Rspamd, name='Rspamd'),
path('installRspamd', views.installRspamd, name='installRspamd'),
path('installStatusRspamd', views.installStatusRspamd, name='installStatusRspamd'),

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import os
import time
import http.client
from django.shortcuts import redirect
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from loginSystem.models import Administrator
from mailServer.models import Domains, EUsers
@@ -1244,18 +1246,153 @@ def Rspamd(request):
checkIfRspamdInstalled = 0
ipFile = "/etc/cyberpanel/machineIP"
f = open(ipFile)
ipData = f.read()
ipAddress = ipData.split('\n', 1)[0]
ipAddress = '127.0.0.1'
try:
ipFile = "/etc/cyberpanel/machineIP"
with open(ipFile, 'r') as f:
ipData = f.read()
first_line = ipData.split('\n', 1)[0].strip()
if first_line:
ipAddress = first_line
except (OSError, IOError, IndexError):
pass
if mailUtilities.checkIfRspamdInstalled() == 1:
checkIfRspamdInstalled = 1
rspamd_ui_url = request.build_absolute_uri('/emailPremium/Rspamd/ui/')
proc = httpProc(request, 'emailPremium/Rspamd.html',
{'checkIfRspamdInstalled': checkIfRspamdInstalled, 'ipAddress': ipAddress}, 'admin')
{
'checkIfRspamdInstalled': checkIfRspamdInstalled,
'ipAddress': ipAddress,
'rspamd_ui_url': rspamd_ui_url,
}, 'admin')
return proc.render()
_RSPAMD_UPSTREAM = ('127.0.0.1', 11334)
_RSPAMD_HOP_RESPONSE = frozenset({
'connection', 'transfer-encoding', 'keep-alive', 'proxy-authenticate',
'proxy-authorization', 'te', 'trailers', 'upgrade', 'content-encoding',
})
@csrf_exempt
def rspamd_ui_proxy(request, subpath=None):
"""Reverse-proxy Rspamd controller UI (localhost:11334) for logged-in admins only."""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
return ACLManager.loadError()
except KeyError:
return redirect(loadLoginPage)
if mailUtilities.checkIfRspamdInstalled() != 1:
return HttpResponse(
'Rspamd is not installed.',
status=503,
content_type='text/plain; charset=utf-8',
)
proxy_base = request.build_absolute_uri('/emailPremium/Rspamd/ui').rstrip('/')
path = '/'
if subpath:
path = '/' + subpath.lstrip('/')
q = request.META.get('QUERY_STRING', '')
if q:
full_path = path + '?' + q
else:
full_path = path
forward_method = request.method
if forward_method == 'HEAD':
forward_method = 'GET'
body = None
if forward_method in ('POST', 'PUT', 'PATCH', 'DELETE'):
body = request.body
headers = {}
acc = request.META.get('HTTP_ACCEPT')
if acc:
headers['Accept'] = acc
al = request.META.get('HTTP_ACCEPT_LANGUAGE')
if al:
headers['Accept-Language'] = al
ua = request.META.get('HTTP_USER_AGENT')
if ua:
headers['User-Agent'] = ua
auth = request.META.get('HTTP_AUTHORIZATION')
if auth:
headers['Authorization'] = auth
ct = request.META.get('CONTENT_TYPE')
if ct and forward_method in ('POST', 'PUT', 'PATCH'):
headers['Content-Type'] = ct
cookie = request.META.get('HTTP_COOKIE')
if cookie:
headers['Cookie'] = cookie
xhr = request.META.get('HTTP_X_REQUESTED_WITH')
if xhr:
headers['X-Requested-With'] = xhr
conn = None
try:
conn = http.client.HTTPConnection(
_RSPAMD_UPSTREAM[0], _RSPAMD_UPSTREAM[1], timeout=120,
)
conn.request(forward_method, full_path, body=body, headers=headers)
upstream = conn.getresponse()
data = upstream.read()
status = upstream.status
except (ConnectionRefusedError, OSError, http.client.HTTPException) as _e:
logging.CyberCPLogFileWriter.writeToFile(
'rspamd_ui_proxy upstream error: %s' % (type(_e).__name__,),
)
return HttpResponse(
'Could not reach Rspamd on 127.0.0.1:11334. Is rspamd running?',
status=502,
content_type='text/plain; charset=utf-8',
)
finally:
if conn is not None:
try:
conn.close()
except Exception:
pass
if request.method == 'HEAD':
out = HttpResponse(status=status)
data = b''
else:
out = HttpResponse(data, status=status)
for hdr, val in upstream.getheaders():
key = hdr.lower()
if key in _RSPAMD_HOP_RESPONSE:
continue
if key == 'location':
val = _rewrite_rspamd_location(val, proxy_base)
if request.method == 'HEAD' and key == 'content-length':
continue
out[hdr] = val
return out
def _rewrite_rspamd_location(location, proxy_base):
if not location:
return location
if location.startswith('http://127.0.0.1:11334'):
return proxy_base + location[len('http://127.0.0.1:11334'):]
if location.startswith('http://[::1]:11334'):
return proxy_base + location[len('http://[::1]:11334'):]
if location.startswith('/') and not location.startswith('//'):
return proxy_base + location
return location
def installRspamd(request):
try:
userID = request.session['userID']

View File

@@ -853,6 +853,10 @@ return custom_keywords
writeToFile.writelines("Configuring RSPAMD repo..\n")
writeToFile.close()
try:
os.makedirs('/etc/yum.repos.d', mode=0o755, exist_ok=True)
except OSError:
pass
command = 'curl https://rspamd.com/rpm-stable/centos-7/rspamd.repo > /etc/yum.repos.d/rspamd.repo'
ProcessUtilities.normalExecutioner(command, True)
@@ -868,20 +872,39 @@ return custom_keywords
elif ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
el_major = mailUtilities._rhel_el_major_version()
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Configuring RSPAMD repo..\n")
writeToFile.writelines(
"Configuring RSPAMD repo for EL%s (rspamd.com rpm-stable)...\n" % el_major
)
writeToFile.close()
command = 'curl https://rspamd.com/rpm-stable/centos-8/rspamd.repo > /etc/yum.repos.d/rspamd.repo'
try:
os.makedirs('/etc/yum.repos.d', mode=0o755, exist_ok=True)
except OSError:
pass
command = (
'curl -fsSL https://rspamd.com/rpm-stable/centos-%s/rspamd.repo '
'-o /etc/yum.repos.d/rspamd.repo' % el_major
)
ProcessUtilities.normalExecutioner(command, True)
command = 'rpm --import https://rspamd.com/rpm-stable/gpg.key'
ProcessUtilities.normalExecutioner(command, True)
command = 'dnf update -y'
ProcessUtilities.normalExecutioner(command, True)
if el_major == '7':
command = 'yum update -y'
ProcessUtilities.normalExecutioner(command, True)
command = (
'sudo yum install -y rspamd clamav-server clamav-data clamav-update '
'clamav-filesystem clamav clamav-scanner-systemd clamav-devel clamav-lib '
'clamav-server-systemd'
)
else:
command = 'dnf update -y'
ProcessUtilities.normalExecutioner(command, True)
command = 'sudo dnf install -y rspamd clamav clamd clamav-update'
command = 'sudo dnf install -y rspamd clamav clamd clamav-update'
else:
command = 'DEBIAN_FRONTEND=noninteractive apt-get install rspamd clamav clamav-daemon -y'
@@ -891,9 +914,24 @@ return custom_keywords
f.flush()
res = subprocess.call(command, stdout=f, stderr=f, shell=True)
if res != 0:
with open(mailUtilities.RspamdInstallLogPath, 'a') as lf:
lf.write(
'Package install failed (exit code %s). '
'On EL9, ensure rspamd.repo matches your OS (EL8 RPMs cause GPG errors on EL9).\n' % res
)
lf.write('Can not be installed.[404]\n')
logging.CyberCPLogFileWriter.writeToFile(
'[Could not Install Rspamd.] dnf/yum exit %s' % res
)
return 0
###### makefile
path = "/etc/rspamd/local.d/antivirus.conf"
try:
os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True)
except OSError:
pass
content ="""# ================= DO NOT MODIFY THIS FILE =================
#
# Manual changes will be lost when this file is regenerated.
@@ -957,6 +995,10 @@ clamav {
### disable dkim signing in rspamd in ref to https://github.com/usmannasir/cyberpanel/issues/1176
DKIMPath = '/etc/rspamd/local.d/dkim_signing.conf'
try:
os.makedirs(os.path.dirname(DKIMPath), mode=0o755, exist_ok=True)
except OSError:
pass
WriteToFile = open(DKIMPath, 'w')
WriteToFile.write('enabled = false;\n')
@@ -984,6 +1026,10 @@ clamav {
wpath = "/etc/rspamd/local.d/redis.conf"
try:
os.makedirs(os.path.dirname(wpath), mode=0o755, exist_ok=True)
except OSError:
pass
wdata = """
write_servers = "127.0.0.1";
read_servers = "127.0.0.1";
@@ -994,34 +1040,30 @@ read_servers = "127.0.0.1";
wirtedata2.close()
if res == 1:
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Can not be installed.[404]\n")
writeToFile.close()
logging.CyberCPLogFileWriter.writeToFile("[Could not Install Rspamd.]")
return 0
else:
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
command = 'setsebool -P antivirus_can_scan_system 1'
cmd = shlex.split(command)
if ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
command = 'setsebool -P antivirus_can_scan_system 1'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'setsebool -P clamd_use_jit 1'
cmd = shlex.split(command)
command = 'setsebool -P clamd_use_jit 1'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'usermod -a -G clamscan _rspamd'
cmd = shlex.split(command)
command = 'usermod -a -G clamscan _rspamd'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
clamavcontent = """
try:
os.makedirs('/etc/clamd.d', mode=0o755, exist_ok=True)
except OSError:
pass
clamavcontent = """
User clamscan
PidFile /var/run/clamd.scan/clamd.pid
TCPSocket 3310
@@ -1034,49 +1076,60 @@ ScanMail true
ScanArchive true
#LogFile /var/log/clamd.scan/clamav.log
"""
writeToFile = open('/etc/clamd.d/scan.conf', 'w')
writeToFile.write(clamavcontent)
writeToFile.close()
writeToFile = open('/etc/clamd.d/scan.conf', 'w')
writeToFile.write(clamavcontent)
writeToFile.close()
command = 'touch /var/log/clamd.scan/clamav.log'
ProcessUtilities.normalExecutioner(command, False, 'clamscan')
command = 'touch /var/log/clamd.scan/clamav.log'
ProcessUtilities.normalExecutioner(command, False, 'clamscan')
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Updating Freshclam database..\n")
writeToFile.close()
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Updating Freshclam database..\n")
writeToFile.close()
command = 'freshclam'
cmd = shlex.split(command)
command = 'freshclam'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'systemctl start clamd@scan'
cmd = shlex.split(command)
command = 'systemctl start clamd@scan'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'systemctl restart rspamd'
cmd = shlex.split(command)
command = 'systemctl restart rspamd'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
elif ProcessUtilities.decideDistro() == ProcessUtilities.ubuntu or ProcessUtilities.decideDistro() == ProcessUtilities.ubuntu20:
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'usermod -a -G clamav _rspamd'
cmd = shlex.split(command)
time.sleep(5)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Rspamd Installed.[200]\n")
writeToFile.close()
command = 'chown -R clamav:clamav /var/run/clamav'
cmd = shlex.split(command)
elif ProcessUtilities.decideDistro() == ProcessUtilities.ubuntu or ProcessUtilities.decideDistro() == ProcessUtilities.ubuntu20:
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'usermod -a -G clamav _rspamd'
cmd = shlex.split(command)
clamavcontent = """
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'chown -R clamav:clamav /var/run/clamav'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
try:
os.makedirs('/etc/clamav', mode=0o755, exist_ok=True)
except OSError:
pass
clamavcontent = """
User clamav
PidFile /var/run/clamav/clamd.pid
TCPSocket 3310
@@ -1089,32 +1142,31 @@ ScanMail true
ScanArchive true
LogFile /var/log/clamav/clamav.log
"""
writeToFile = open('/etc/clamav/clamd.conf', 'w')
writeToFile.write(clamavcontent)
writeToFile.close()
writeToFile = open('/etc/clamav/clamd.conf', 'w')
writeToFile.write(clamavcontent)
writeToFile.close()
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Updating Freshclam database..\n")
writeToFile.close()
writeToFile = open(mailUtilities.RspamdInstallLogPath, 'a')
writeToFile.writelines("Updating Freshclam database..\n")
writeToFile.close()
command = 'freshclam'
cmd = shlex.split(command)
command = 'freshclam'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'systemctl restart clamav-daemon'
cmd = shlex.split(command)
command = 'systemctl restart clamav-daemon'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
command = 'systemctl restart rspamd'
cmd = shlex.split(command)
command = 'systemctl restart rspamd'
cmd = shlex.split(command)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
with open(mailUtilities.RspamdInstallLogPath, 'a') as f:
res = subprocess.call(cmd, stdout=f)
time.sleep(5)
@@ -1652,6 +1704,21 @@ LogFile /var/log/clamav/clamav.log
str(msg) + " [checkIfMailScannerInstalled]")
return 0
@staticmethod
def _rhel_el_major_version():
"""Parse PLATFORM_ID from /etc/os-release (e.g. platform:el9 -> '9'). Default '8'."""
import re
try:
with open('/etc/os-release', 'r') as os_release:
for line in os_release:
if line.startswith('PLATFORM_ID='):
m = re.search(r'el(\d+)', line)
if m:
return m.group(1)
except Exception:
pass
return '8'
@staticmethod
def FetchPostfixHostname():
try:

View File

@@ -188,17 +188,60 @@ class ProcessUtilities(multi.Thread):
return ProcessUtilities.ubuntu20
return ProcessUtilities.ubuntu
# Debian (no Ubuntu): use same apt paths as Ubuntu for CyberPanel mail stack
for _line in content.splitlines():
_ls = _line.strip()
if _ls.startswith('ID='):
_id = _ls.split('=', 1)[1].strip().strip('"').lower()
if _id == 'debian':
return ProcessUtilities.ubuntu
break
# Check for RedHat-based distributions
if os.path.exists(distroPathAlma):
with open(distroPathAlma, 'r') as f:
content = f.read()
if any(x in content for x in ['CentOS Linux release 8', 'AlmaLinux release 8', 'Rocky Linux release 8',
'Rocky Linux release 9', 'AlmaLinux release 9', 'CloudLinux release 9',
'CloudLinux release 8', 'AlmaLinux release 10']):
if any(x in content for x in ['AlmaLinux release 9', 'Rocky Linux release 9', 'AlmaLinux release 10']):
if any(x in content for x in ['CentOS Linux release 7', 'CentOS Linux release 8',
'CentOS Stream release 8', 'CentOS Stream release 9',
'AlmaLinux release 8', 'Rocky Linux release 8',
'Rocky Linux release 9', 'AlmaLinux release 9',
'CloudLinux release 9', 'CloudLinux release 8',
'AlmaLinux release 10', 'Rocky Linux release 10',
'Red Hat Enterprise Linux release 8',
'Red Hat Enterprise Linux release 9',
'Red Hat Enterprise Linux release 10']):
if any(x in content for x in ['AlmaLinux release 9', 'Rocky Linux release 9',
'AlmaLinux release 10', 'Rocky Linux release 10',
'Red Hat Enterprise Linux release 9',
'Red Hat Enterprise Linux release 10',
'CentOS Stream release 9']):
ProcessUtilities.alma9check = 1
return ProcessUtilities.cent8
# Fallback: /etc/os-release for RHEL family (some minimal images lack redhat-release text we match)
if os.path.exists('/etc/os-release'):
try:
with open('/etc/os-release', 'r') as f:
os_lines = f.read()
rid = ''
for line in os_lines.splitlines():
if line.startswith('ID='):
rid = line.split('=', 1)[1].strip().strip('"').lower()
break
if rid in ('almalinux', 'rocky', 'centos', 'rhel', 'cloudlinux', 'eurolinux',
'miraclelinux', 'openeuler', 'virtuozzo'):
ver_id = ''
for line in os_lines.splitlines():
if line.startswith('VERSION_ID='):
ver_id = line.split('=', 1)[1].strip().strip('"')
break
major = ver_id.split('.')[0] if ver_id else '8'
if major in ('9', '10'):
ProcessUtilities.alma9check = 1
return ProcessUtilities.cent8
except OSError:
pass
# Default to Ubuntu if no other distribution is detected
return ProcessUtilities.ubuntu