mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-16 11:36:48 +01:00
- panelAccess: plugin for panel access settings and OLS proxy - fix-pureftpd-quota-once.sh: one-time quota fix script - to-do: firewall banned IPs, panel access store, reverse proxy CSRF docs
278 lines
11 KiB
Python
278 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
OpenLiteSpeed reverse-proxy setup for Panel Access (custom domain).
|
|
Creates a proxy-only vhost so the panel is reachable at the custom domain
|
|
without manual OLS configuration.
|
|
"""
|
|
import os
|
|
import re
|
|
|
|
# Path used by CyberPanel for panel port (same as ProcessUtilities.portPath); SSH login message uses this
|
|
BIND_CONF = '/usr/local/lscp/conf/bind.conf'
|
|
|
|
|
|
def get_panel_port():
|
|
"""Detect panel port from bind.conf (*:PORT). Fallback 8090 (default), then 2087 (common alternate)."""
|
|
if os.environ.get('PANEL_BACKEND_URL'):
|
|
# Let caller parse URL if they need port from env
|
|
pass
|
|
try:
|
|
if os.path.isfile(BIND_CONF):
|
|
with open(BIND_CONF, 'r') as f:
|
|
line = f.read().strip()
|
|
if '*' in line and ':' in line:
|
|
port = line.split(':')[1].strip().split()[0]
|
|
if port.isdigit():
|
|
return port
|
|
except (OSError, IOError):
|
|
pass
|
|
return '8090'
|
|
|
|
|
|
def get_panel_backend_url():
|
|
"""Panel backend URL for proxy. Prefer PANEL_BACKEND_URL env; else detect port from bind.conf."""
|
|
url = os.environ.get('PANEL_BACKEND_URL', '').strip()
|
|
if url:
|
|
return url
|
|
port = get_panel_port()
|
|
return 'https://127.0.0.1:{}'.format(port)
|
|
|
|
|
|
# Used when module loads (can be overridden by get_panel_backend_url() at runtime)
|
|
PANEL_BACKEND_URL = os.environ.get('PANEL_BACKEND_URL') or ('https://127.0.0.1:' + get_panel_port())
|
|
LSWS_ROOT = '/usr/local/lsws'
|
|
VHOSTS_DIR = os.path.join(LSWS_ROOT, 'conf', 'vhosts')
|
|
PANEL_PROXY_VHROOT = '/usr/local/lsws/panel_proxy'
|
|
HTTPD_CONFIG = '/usr/local/lsws/conf/httpd_config.conf'
|
|
|
|
|
|
def _domain_from_origin(origin):
|
|
"""Extract host from origin (e.g. https://panel.example.com -> panel.example.com)."""
|
|
if not origin or not isinstance(origin, str):
|
|
return None
|
|
origin = origin.strip().lower()
|
|
if origin.startswith('http://'):
|
|
origin = origin[7:]
|
|
elif origin.startswith('https://'):
|
|
origin = origin[8:]
|
|
if '/' in origin:
|
|
origin = origin.split('/')[0]
|
|
if ':' in origin:
|
|
origin = origin.split(':')[0]
|
|
# Basic hostname validation
|
|
if origin and re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', origin):
|
|
return origin
|
|
return None
|
|
|
|
|
|
def _vhost_conf_content(domain, backend_url):
|
|
"""Generate vhost.conf content: proxy entire site to panel backend."""
|
|
# OLS: extprocessor (proxy) + context / (handler = proxy)
|
|
return """# Panel Access: reverse proxy to CyberPanel backend (do not edit manually)
|
|
docRoot {vhroot}/
|
|
vhDomain {domain}
|
|
enableGzip 1
|
|
|
|
extprocessor panelbackend {{
|
|
type proxy
|
|
address {backend}
|
|
maxConns 100
|
|
initTimeout 60
|
|
retryTimeout 0
|
|
respBuffer 0
|
|
}}
|
|
|
|
context / {{
|
|
type proxy
|
|
handler panelbackend
|
|
addDefaultCharset off
|
|
}}
|
|
|
|
errorlog $VH_ROOT/logs/error.log {{
|
|
logLevel WARN
|
|
rollingSize 10M
|
|
useServer 0
|
|
}}
|
|
|
|
accessLog $VH_ROOT/logs/access.log {{
|
|
rollingSize 10M
|
|
keepDays 7
|
|
useServer 0
|
|
}}
|
|
""".format(
|
|
vhroot=PANEL_PROXY_VHROOT,
|
|
domain=domain,
|
|
backend=backend_url,
|
|
)
|
|
|
|
|
|
def _virtual_host_block(domain):
|
|
"""VirtualHost block for httpd_config.conf (same shape as olsMasterMainConf but vhRoot fixed)."""
|
|
return """virtualHost {domain} {{
|
|
vhRoot {vhroot}
|
|
configFile $SERVER_ROOT/conf/vhosts/{domain}/vhost.conf
|
|
allowSymbolLink 1
|
|
enableScript 1
|
|
restrained 1
|
|
}}
|
|
""".format(domain=domain, vhroot=PANEL_PROXY_VHROOT)
|
|
|
|
|
|
def _domain_already_mapped(domain, lines):
|
|
"""Return True if domain is already in a listener map."""
|
|
for line in lines:
|
|
if 'map' in line and domain in line.split():
|
|
return True
|
|
return False
|
|
|
|
|
|
def _vhost_block_exists(domain, lines):
|
|
"""Return True if virtualHost {domain} already exists."""
|
|
marker = 'virtualHost {}'.format(domain)
|
|
for line in lines:
|
|
if marker in line and ('virtualHost' in line):
|
|
return True
|
|
return False
|
|
|
|
|
|
def setup_panel_proxy_vhost(domain_name):
|
|
"""
|
|
Create OpenLiteSpeed proxy vhost for the given domain (panel accessible at domain -> backend).
|
|
Returns (success: bool, message: str).
|
|
"""
|
|
try:
|
|
from plogical.processUtilities import ProcessUtilities
|
|
from plogical import installUtilities
|
|
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter
|
|
except ImportError:
|
|
return False, 'CyberPanel plumbing not available (run inside CyberPanel).'
|
|
|
|
if ProcessUtilities.decideServer() != ProcessUtilities.OLS:
|
|
return False, 'Only OpenLiteSpeed is supported for automatic proxy setup.'
|
|
|
|
domain = _domain_from_origin(domain_name) if _domain_from_origin(domain_name) else domain_name
|
|
if not domain or not re.match(r'^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$', domain):
|
|
return False, 'Invalid domain: {}'.format(domain_name)
|
|
|
|
vhost_dir = os.path.join(VHOSTS_DIR, domain)
|
|
vhost_conf = os.path.join(vhost_dir, 'vhost.conf')
|
|
|
|
# Create vhRoot and vhost dirs using ProcessUtilities for proper permissions
|
|
try:
|
|
# Use ProcessUtilities to create directories with root permissions
|
|
if not os.path.exists(PANEL_PROXY_VHROOT):
|
|
command = 'mkdir -p {}'.format(PANEL_PROXY_VHROOT)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
command = 'chmod 755 {}'.format(PANEL_PROXY_VHROOT)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
|
|
log_dir = os.path.join(PANEL_PROXY_VHROOT, 'logs')
|
|
if not os.path.exists(log_dir):
|
|
command = 'mkdir -p {}'.format(log_dir)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
command = 'chmod 755 {}'.format(log_dir)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
|
|
if not os.path.exists(vhost_dir):
|
|
command = 'mkdir -p {}'.format(vhost_dir)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
command = 'chmod 755 {}'.format(vhost_dir)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
except Exception as e:
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] makedirs: {}'.format(e))
|
|
return False, 'Could not create directories: {}'.format(e)
|
|
|
|
# Write vhost.conf (use detected panel URL so port 2087/8090 is correct)
|
|
# Write to temp file first, then move with ProcessUtilities for proper permissions
|
|
backend_url = get_panel_backend_url()
|
|
import tempfile
|
|
temp_file = None
|
|
try:
|
|
# Create temp file in /tmp
|
|
temp_fd, temp_file = tempfile.mkstemp(suffix='.conf', prefix='panel_access_', dir='/tmp')
|
|
with os.fdopen(temp_fd, 'w') as f:
|
|
f.write(_vhost_conf_content(domain, backend_url))
|
|
|
|
# Move temp file to final location using ProcessUtilities
|
|
command = 'cp {} {}'.format(temp_file, vhost_conf)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
command = 'chmod 644 {}'.format(vhost_conf)
|
|
ProcessUtilities.normalExecutioner(command)
|
|
|
|
# Clean up temp file
|
|
try:
|
|
os.unlink(temp_file)
|
|
except:
|
|
pass
|
|
except Exception as e:
|
|
# Clean up temp file on error
|
|
if temp_file and os.path.exists(temp_file):
|
|
try:
|
|
os.unlink(temp_file)
|
|
except:
|
|
pass
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] write vhost.conf: {}'.format(e))
|
|
return False, 'Could not write vhost config: {}'.format(e)
|
|
|
|
# Add virtualHost + map to httpd_config.conf (idempotent)
|
|
# Check if file exists using ProcessUtilities (runs with proper permissions)
|
|
try:
|
|
command = 'test -f {} && echo exists || echo notfound'.format(HTTPD_CONFIG)
|
|
result = ProcessUtilities.outputExecutioner(command).strip()
|
|
if result == 'notfound':
|
|
# Try to check with ls command as fallback
|
|
command2 = 'ls {} 2>&1'.format(HTTPD_CONFIG)
|
|
result2 = ProcessUtilities.outputExecutioner(command2).strip()
|
|
if 'No such file' in result2 or 'cannot access' in result2:
|
|
return False, 'OpenLiteSpeed config not found: {}'.format(HTTPD_CONFIG)
|
|
# File might exist but have permission issues - log and continue
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Warning: Config file check ambiguous, proceeding: {}'.format(result2))
|
|
except Exception as e:
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] Error checking config file: {}'.format(e))
|
|
# Don't fail here - let safeModifyHttpdConfig handle it
|
|
|
|
def modifier(current_lines):
|
|
out = list(current_lines)
|
|
if not _domain_already_mapped(domain, out):
|
|
for i, line in enumerate(out):
|
|
if 'listener' in line and 'Default' in line:
|
|
out.insert(i + 1, ' map {} {}\n'.format(domain, domain))
|
|
break
|
|
else:
|
|
raise ValueError('Default listener not found in httpd_config.conf')
|
|
if not _vhost_block_exists(domain, out):
|
|
out.append(_virtual_host_block(domain))
|
|
return out
|
|
|
|
try:
|
|
success, error = installUtilities.installUtilities.safeModifyHttpdConfig(
|
|
modifier,
|
|
'Panel Access: add proxy vhost for {}'.format(domain),
|
|
skip_validation=True, # Skip validation to avoid pre-existing config errors
|
|
)
|
|
if not success:
|
|
error_msg = error or 'Failed to update httpd_config.conf.'
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] safeModifyHttpdConfig failed: {}'.format(error_msg))
|
|
return False, error_msg
|
|
except Exception as e:
|
|
error_msg = 'Error calling safeModifyHttpdConfig: {}'.format(str(e))
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] {}'.format(error_msg))
|
|
return False, error_msg
|
|
|
|
# Reload OpenLiteSpeed
|
|
try:
|
|
cmd = '/usr/local/lsws/bin/lswsctrl reload'
|
|
subprocess = __import__('subprocess')
|
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
|
|
if r.returncode != 0:
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] lswsctrl reload: {}'.format(r.stderr or r.stdout))
|
|
except Exception as e:
|
|
CyberCPLogFileWriter.writeToFile('[panelAccess.ols_proxy] reload: {}'.format(e))
|
|
|
|
return True, 'Proxy for {} added. Reload OpenLiteSpeed if needed.'.format(domain)
|
|
|
|
|
|
def domain_from_origin(origin):
|
|
"""Public helper: extract hostname from origin URL."""
|
|
return _domain_from_origin(origin)
|