Files
CyberPanel/panelAccess/ols_proxy.py
master3395 a4385d55c2 Add panelAccess plugin, pureftpd quota fix, and to-do docs
- 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
2026-02-15 00:02:40 +01:00

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)